diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml
index 75ff941..6d437df 100644
--- a/.github/workflows/deploy.yaml
+++ b/.github/workflows/deploy.yaml
@@ -12,7 +12,7 @@ jobs:
- uses: actions/setup-node@v7
- run: npm clean-install
- run: npm run test
-
+
deploy:
needs: test
runs-on: ubuntu-latest
diff --git a/CLAUDE.md b/CLAUDE.md
index 824d6f9..1fac829 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -40,8 +40,8 @@ Distinct from its siblings (do not blur them):
Pages; extensible to CF Pages). **No image** — the ship is static-only. Three.js, like `devops-bootcamp-app`.
- **`shipit-board` is dual-role:** the *shared* Mission Control on instructor EC2, **and** the
artifact each learner **builds + deploys to their own EC2** in the S4 capstone.
-- The freed `shipit-launchpad` name is reused for the **forkable learner repo**
- `Infratify/shipit-launchpad` (see Distribution below), not an image.
+- Learners fork **this monorepo** (`Infratify/devops-bootcamp-shipit`) and work in `launchpad/`
+ (see Distribution below). The planned payload-only `shipit-launchpad` release repo was never used.
## PINNED — the 4-session arc (lean: one concept per session)
@@ -64,28 +64,59 @@ already did `docker build` + ECR by hand in the AWS sessions — S4 *automates t
The one integration point. Keep it stable; slides and the reference workflows depend on it.
- **Identity** = the learner's GitHub username (`${{ github.actor }}`), used as `callsign`.
-- **Config** the board needs comes from the learner's `ship.config.json`: `color` (hex **or** a
- named-palette colour; sets the ship's hue — every saturated texel takes that hue; greys/blacks stay
- neutral — and drives the UI accent) and `shipModel` (which of the 4 ships the board renders in
- orbit); `shipName` is a cosmetic label, not identity.
-- **Transport:** each workflow stage POSTs one event to the board.
+- **Config:** the report script reads `color` + `shipModel` from the learner's `ship.config.json`,
+ so the board renders each learner's real colour AND model (hex or a named-palette colour; board
+ normalizes, greys/blacks stay neutral). `shipName` is cosmetic, never identity.
+- **Transport:** `launchpad/scripts/report.sh` (shipped in this repo — learners call it, never write
+ it) POSTs one event per invocation. The learner workflow adds ONE step (CI/CD 3 Amali 2) — a
+ **single liftoff report** after the Pages deploy:
+
+```yaml
+- name: Lapor ke papan
+ run: bash scripts/report.sh
+ env:
+ BOARD_URL: ${{ vars.BOARD_URL }}
+ SHIPIT_TOKEN: ${{ secrets.SHIPIT_TOKEN }}
+```
+
+ That `env:` block IS the lesson surface — one `vars` line, one `secrets` line, nothing else. The
+ HTTP mechanics live in the script. Extra beats (`report.sh pad running`, abort-on-failure) are
+ optional operator flourishes, never required of learners.
+
+What the script sends (slides show this as *anatomy* — learners read it, never type it):
```
POST $BOARD_URL/api/event
Authorization: Bearer $SHIPIT_TOKEN
Content-Type: application/json
-{
- "callsign": "octocat", // GitHub username
- "stage": "build", // pad | build | test | clearance | liftoff
- "status": "passed", // running | passed | failed | aborted | shipped
- "color": "#22d3ee", // hex or a colour name (e.g. "red"); board normalizes → hex, sets the ship's hue
- "shipModel":"fighter", // from ship.config.json: fighter · interceptor · hauler · scout
- "version": "v3", // optional; image/site tag (for rollback demo)
- "siteUrl": "https://…" // optional; the live deployed site to link from orbit
-}
+{ "callsign": "octocat", "stage": "liftoff", "status": "shipped",
+ "color": "#22d3ee", "shipModel": "fighter",
+ "siteUrl": "https://octocat.github.io/devops-bootcamp-shipit/" }
```
+`callsign` = `GITHUB_ACTOR` (Actions sets it automatically — no templating in the learner file);
+`color`/`shipModel` read from `ship.config.json`; stage/status from the script args (default
+`liftoff shipped`). `siteUrl` is **derived inside `report.sh`** from the vars Actions already sets
+(`GITHUB_REPOSITORY_OWNER` + repo name → `https://.github.io[/]/`) — NOT from the taught
+`env:` block, which stays the pinned two lines. Omitted when the script runs outside Actions.
+
+- **Board accepts more than the taught report** (operator flourishes only, never asked of learners):
+ `stage` ∈ `pad | build | test | clearance | liftoff`, `status` ∈ `running | passed | failed |
+ aborted | shipped`, plus optional `version`, `siteUrl`. Required: just `callsign` + a known
+ `stage`/`status` — `color`/`shipModel` default when absent or invalid (see `board/src/room.js`).
+- **`siteUrl` → LIVE (green).** When an event carries a `siteUrl`, the board probes it (`HEAD`, on
+ arrival + a periodic sweep) and broadcasts a per-ship `live` boolean on the roster; a ship shows a
+ green **LIVE** halo only when its *real* Pages site answers `200` (see `board/src/liveness.js`).
+ This ties the token + report to the actual deploy — a reported ship that never went live stays
+ neutral. A fresh deploy can 404 for ~1 min; the periodic re-check flips it green when the site
+ actually comes up. Non-200/timeout = "not live yet," never an error.
+- **The script's `curl` uses `--fail-with-body`** (pinned in `report.sh`'s header too): on a 401
+ the step **fails** (non-zero exit → red run) AND the run log prints `{"error":"unauthorized"}`,
+ so the wrong-token demo shows both the red X and the reason. Changed 2026-07-20 (was: no `-f`,
+ stay-green — the shape CI/CD 3 Amali 3 was delivered with). Plain `-f` would swallow the
+ response body — don't switch to it.
+
- `$BOARD_URL` is a **public** repo/environment **variable**.
- `$SHIPIT_TOKEN` is the **secret** taught in CI/CD 3 — a ship with no/late token can't report to
Mission Control (the "unauthorized" lesson). Do NOT accept unauthenticated events in prod mode.
@@ -105,15 +136,77 @@ Frozen — slides quote these verbatim.
(recolours the ship — sets its hue to `color`; every saturated texel takes that hue, greys/blacks
stay neutral — and drives the UI accent) · `shipModel` ∈ `fighter · interceptor · hauler · scout` · `emblem` ∈
`comet · bolt · star · ring · delta · phoenix`. `callsign` is **not** in config — it's the GitHub
- username, injected via `VITE_CALLSIGN` at build.
+ username. On the board it comes from `${{ github.actor }}` in the report step. The site *can*
+ display it via `VITE_CALLSIGN` at build, but **the taught workflow never sets it** — the app
+ falls back to empty (`launchpad/src/main.js`); don't assume the microsite shows a callsign.
- The ship is one of four low-poly spaceships (Quaternius, CC0), hue-set by `color`; the site and
board both render whichever `shipModel` the learner picked.
-- **The S2 fitness gate** is a config **validation** check (not a unit test): `npm test` →
+- **The S2 fitness gate** is a config **validation** check (not a unit test): taught as
+ `npm run test` (in `launchpad/`, via the workflow's `defaults.run.working-directory`) →
`node scripts/preflight.mjs` validates `ship.config.json` and **exits non-zero (ABORT)** on a bad
config (unparseable, bad hex, unknown emblem, over-long name). Teaches the *exit-code gate* (a
DevOps skill), not test authoring (a developer skill).
-- **Reference workflows** live as the `cicd1..4` **answer-key branches** on `Infratify/shipit-launchpad`
- (each = the full correct state at that session's end); slides lift the `deploy.yml` from them.
+- **The slides are the source of truth for the workflow** — learners build `deploy.yaml` from the
+ building blocks on the slides, nothing else. The authored answer keys (`starter/workflows/`) were
+ retired 2026-07-17: learners shipped a simpler file than they prescribed, and the extra plumbing
+ (config extraction, pad/abort beats) never earned its place *in the learner's workflow* — it now
+ lives in the prop's `launchpad/scripts/report.sh` instead, behind a two-line `env:` mapping that
+ doubles as the secrets/vars demo surface. A session's reference state is *derived* by running its
+ amali on a test fork (see Distribution).
+
+**Taught workflow — end of S3.** Snapshot derived from the delivered decks 2026-07-17, NOT a spec —
+regenerate from the slides if in doubt. Filename is `.github/workflows/deploy.yaml` (set in CI/CD 1:
+`.yaml`, not `.yml`); `permissions` sits at the bottom because that's where CI/CD 1 adds it; S1's
+`workflow_dispatch` was dropped when S3 rewrote `on:`:
+
+```yaml
+name: deploy
+on:
+ push:
+ branches: [main]
+ pull_request:
+
+defaults:
+ run:
+ working-directory: launchpad
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v7
+ - uses: actions/setup-node@v7
+ - run: npm clean-install
+ - run: npm run test
+
+ deploy:
+ needs: test
+ if: github.event_name == 'push'
+ environment: production
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v7
+ - uses: actions/setup-node@v7
+ - run: npm clean-install
+ - run: npm run build
+ - uses: actions/upload-pages-artifact@v5
+ with:
+ path: launchpad/dist
+ - uses: actions/deploy-pages@v5
+ - name: Lapor ke papan
+ run: bash scripts/report.sh
+ env:
+ BOARD_URL: ${{ vars.BOARD_URL }}
+ SHIPIT_TOKEN: ${{ secrets.SHIPIT_TOKEN }}
+
+permissions:
+ pages: write
+ id-token: write
+```
+
+No `contents: read` — checkout still works because learner forks are public. The deploy job binds
+`environment: production` (a learner-created environment with Required reviewers), NOT the
+conventional `github-pages` environment. S4 extends this file; it must not restructure it.
- **Per-session commands** (kelas-taip-bersama): fork → author `deploy.yml` step-by-step per session →
`git push` → watch. Full list in the spec §7.
- **Slides drift note:** the bootcamp slides repo (`~/repo/slides-devops-bootcamp`) quotes the two
@@ -122,13 +215,19 @@ Frozen — slides quote these verbatim.
## PINNED — learner distribution (fork, not template)
-- Learners **fork** `Infratify/shipit-launchpad`. Branches: **`main`** = payload only
- (`ship/` + `ship.config.json` + `preflight`, **no workflow, no `board/`**); **`cicd1..4`** = answer
- keys (`board/` enters at `cicd4`).
-- **The learner authors the workflow** — `.github/workflows/deploy.yml` is NOT shipped; they write
- it, it grows one job per session. That is the lesson.
-- **Discipline rule (load-bearing):** upstream `main` must never gain a workflow or re-touch
- `ship.config.json` after release, so learner **sync-fork** stays conflict-free.
+- Learners **fork** `Infratify/devops-bootcamp-shipit` (this monorepo) and work in `launchpad/`
+ (workflow steps use `working-directory: launchpad`). The payload-only `shipit-launchpad` release
+ repo + its build scripts (`scripts/release-launchpad.sh` & co.) were retired 2026-07-17, never used.
+- **The learner authors the workflow** — `.github/workflows/deploy.yml` is NOT shipped on `main`;
+ they write it from the slide building blocks, and it grows each session. That is the lesson.
+- **`cicdN` reference branches** (recovery/diff aid, not a spec): before each session the operator
+ follows that session's amali verbatim on a test fork — proving the slides run green — and pushes
+ the resulting state as branch `cicdN` on `Infratify/devops-bootcamp-shipit`.
+- **CI/CD 3 operator dep:** push `launchpad/scripts/report.sh` to upstream `main` before class
+ (new file — sync-fork safe); learners fetch it with `gh repo sync` + `git pull` at the start of
+ Amali 2 — the first live use of the fork model's "sync for instructor fixes" promise.
+- **Discipline rule (load-bearing):** upstream `main` must never gain `.github/workflows/` or
+ re-touch `launchpad/ship.config.json`, so learner **sync-fork** stays conflict-free.
## Conventions
@@ -140,7 +239,7 @@ Frozen — slides quote these verbatim.
## Bootcamp integration (context; the arc itself lives in the slides repo)
-`~/repo/slides-devops-bootcamp` → `outlines/2026/cicd1..4.md` + `slides/2026/cicd1..4/`. The
+`~/repo/slides-devops-bootcamp` → `outlines/2026/ci-cd1..4.md` + `slides/2026/ci-cd1..4/`. The
`$SHIPIT_TOKEN` is the CI/CD 3 secret; the **S4 deploy has the learner's pipeline build the `board`
image, push it to their GHCR, and deploy it to their own EC2 (from AWS 2) via SSM** — with a
rollback demo (redeploy the previous tag) as stretch. The instructor's shared board runs on
diff --git a/README.md b/README.md
index 06beb9a..8980485 100644
--- a/README.md
+++ b/README.md
@@ -13,29 +13,23 @@ CI/CD-native.
## Status
-🚧 **Scaffold only.** This repo currently holds the brief, not the build.
+**Built and in use.** Learners fork this repo in CI/CD 1 and grow `.github/workflows/deploy.yaml`
+one session at a time, from the building blocks on the slides — the workflow file is deliberately
+NOT shipped here. `CLAUDE.md` holds the design rationale + the pinned contracts, including a
+snapshot of the taught workflow.
-- **`PROMPT.md`** — kickoff brief. Open a fresh Claude session here and start from it.
-- **`CLAUDE.md`** — design rationale + the pinned pipeline↔board contract.
-
-## Layout (target)
+## Layout
```
-board/ Mission Control — ws hub + Three.js projector spectator → ghcr.io/infratify/shipit-board (:3000)
-launchpad/ the learner ship microsite template (build/test/Dockerfile) → ghcr.io/infratify/shipit-launchpad (:8080)
-scripts/ e2e.sh — full loop proof (to be written)
-docs/ specs + plans
+board/ Mission Control — ws hub + Three.js projector spectator → ghcr.io/infratify/shipit-board (:3000)
+launchpad/ the learner ship microsite — static Vite + Three.js, ships to GitHub Pages (no image)
```
## The 4-session arc it serves
| Session | Launch phase | Teaches |
|---|---|---|
-| CI/CD 1 | Pad live on Pages | first workflow · trigger · job/step/runner |
-| CI/CD 2 | Fuelling + systems check | build · test gate (red = ABORT) · matrix · artifact |
-| CI/CD 3 | Launch clearance | secrets · environment · manual approval |
-| CI/CD 4 | LIFTOFF → orbit | image → GHCR → deploy to EC2 · tags · rollback |
-
-## Next step
-
-Read `PROMPT.md`, then brainstorm → spec → plan before writing code.
+| CI/CD 1 | Pad live on Pages | first workflow · trigger · job/step/runner · Pages deploy |
+| CI/CD 2 | Systems check | test gate (red = ABORT) · `needs` · branch protection |
+| CI/CD 3 | First contact + clearance | secret vs variable · report to Mission Control · push/PR trigger split · manual approval |
+| CI/CD 4 | LIFTOFF → orbit | pipeline builds the board image → GHCR → deploy to your EC2 |
diff --git a/board/Dockerfile b/board/Dockerfile
index d0911fd..1a78b0d 100644
--- a/board/Dockerfile
+++ b/board/Dockerfile
@@ -5,6 +5,8 @@ COPY package.json package-lock.json ./
RUN npm ci
COPY vite.config.js ./
COPY client ./client
+# client/ship-mesh.js imports ../src/ships.js — the client shares it with the server
+COPY src ./src
RUN npm run build # → /app/dist
# --- runtime ---
diff --git a/board/client/fallback.js b/board/client/fallback.js
index c4c86f4..a90aa35 100644
--- a/board/client/fallback.js
+++ b/board/client/fallback.js
@@ -24,6 +24,7 @@ export function createFallback(container) {
@${escapeHtml(s.callsign)}${escapeHtml(s.stage)} · ${escapeHtml(s.status)}${escapeHtml(s.shipModel || '')}
+ ${s.live ? 'LIVE' : ''}
`).join('');
},
dispose() { el.remove(); },
diff --git a/board/client/index.html b/board/client/index.html
index 8c5a794..c39b2d1 100644
--- a/board/client/index.html
+++ b/board/client/index.html
@@ -17,6 +17,7 @@