Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
16bf728
chore: retire starter/workflows + release machinery — slides are sour…
opariffazman Jul 17, 2026
60232c4
docs: reconcile contracts to taught reality — deploy.yaml snapshot en…
opariffazman Jul 17, 2026
fb7d183
chore: fixed dockerfile
opariffazman Jul 17, 2026
61d4464
feat(launchpad): scripts/report.sh — prop-owned board report, learner…
opariffazman Jul 17, 2026
c2ca980
feat: typing race mode — teach why a backend server exists (#20)
opariffazman Jul 18, 2026
5bb9b18
feat(board): ship goes LIVE (green) only when learner's Pages site is…
opariffazman Jul 18, 2026
268ed33
fix(launchpad): restore valid ship.config.json color (#22d3ee)
opariffazman Jul 18, 2026
27cf8d6
feat(launchpad): report.sh 401 fails the step
opariffazman Jul 20, 2026
b31c50a
docs: unified 2D race UI design — shared track, live frac progress, /…
opariffazman Jul 20, 2026
bef7f4a
docs: unified race UI implementation plan — 9 TDD tasks
opariffazman Jul 20, 2026
7d03093
feat(board): race racers carry display-only typing frac via report()
opariffazman Jul 20, 2026
9e4b202
feat(board): ws progress carries typing frac into race broadcasts
opariffazman Jul 20, 2026
50bb721
feat(board): pure race row math — progressOf, laneOrder, ranks
opariffazman Jul 20, 2026
d7a190b
feat(board): cached GLB->PNG ship sprites for the 2D race track
opariffazman Jul 20, 2026
c3b8e98
fix(board): cache GLB load promise, hide sprite labels in race track
opariffazman Jul 20, 2026
a320d93
feat(board): shared DOM race-track component — rows, sprites, live glide
opariffazman Jul 20, 2026
2180593
fix(board): race-track banner apostrophe broke the string literal
opariffazman Jul 20, 2026
ce9732f
feat(board): projector renders race via shared race-track; drop 3D ra…
opariffazman Jul 20, 2026
3a474f8
feat(board): cockpit shows the full shared race field + live frac typing
opariffazman Jul 20, 2026
2c26306
fix(board): cancel pending frac report on prompt completion
opariffazman Jul 20, 2026
31aadda
feat(board): /operator console — start/reset/view buttons replace curl
opariffazman Jul 20, 2026
54610fd
fix(board): operator console survives locked-down localStorage
opariffazman Jul 20, 2026
1eba0da
fix(board): final-review wave — start guard, running podium, hud hide…
opariffazman Jul 20, 2026
d462be1
docs: spec notes running-phase podium medals
opariffazman Jul 20, 2026
77a10b1
fix(board): race sprites — 3/4 view, hull-fit framing, 128px canvas
opariffazman Jul 20, 2026
176ec56
fix(board): race sprites face the track — per-model nose axis, bigger…
opariffazman Jul 20, 2026
d32a3d1
fix(board): race sprites face right on all models, +30% size; cockpit…
opariffazman Jul 20, 2026
b86067b
feat(board): idle race VFX — starfield drift, ship bob, engine glow
opariffazman Jul 20, 2026
afc5da2
feat(board): ENTER runs the command — hold at 0.9, boost on run
opariffazman Jul 20, 2026
97fbb21
feat(board): cockpit pins own ship row above the typing dock
opariffazman Jul 20, 2026
52666b3
feat(board): synthesized cockpit SFX — Web Audio, zero asset files
opariffazman Jul 20, 2026
b2dc482
feat(board): retheme cockpit SFX from arcade to spaceship
opariffazman Jul 20, 2026
3956de9
feat(board): race prompts become sequenced slide-verbatim stories
opariffazman Jul 20, 2026
2723669
feat(board): terminal-style strict typing — greyed command lights up …
opariffazman Jul 20, 2026
a34382c
fix(board): terminal command always fits one line
opariffazman Jul 20, 2026
0b00bba
Merge remote-tracking branch 'upstream/main'
hisyamjaapa-devops Jul 21, 2026
5e6b01f
Pisahkan CI dan CD berdasarkan status pull request
hisyamjaapa-devops Jul 21, 2026
e261635
Fix duplicated CI CD workflow steps
hisyamjaapa-devops Jul 21, 2026
4609031
Add board URL to Vite build
hisyamjaapa-devops Jul 21, 2026
56b3f87
Configure separate CI and CD workflow
hisyamjaapa-devops Jul 21, 2026
ca30ff6
Update CI CD workflow based on pull request
hisyamjaapa-devops Jul 21, 2026
32d6cc0
Trigger workflow with updated board URL
hisyamjaapa-devops Jul 21, 2026
b5757e2
Retry CI with class token
hisyamjaapa-devops Jul 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 17 additions & 31 deletions .github/workflows/deploy.yaml
Original file line number Diff line number Diff line change
@@ -1,58 +1,44 @@
name: Ship It
name: cicd

on:
push:
branches: [main]
pull_request:
branches: [main]
types: [opened, synchronize, reopened, closed]

# Amali 5: permission untuk deploy GitHub Pages
permissions:
pages: write
id-token: write
contents: read

concurrency:
group: pages
cancel-in-progress: true

# The ship app lives in launchpad/ in the learner fork; run every step there.
defaults:
run:
working-directory: launchpad

jobs:
test:
if: github.event.pull_request.merged == false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 20
cache: npm
cache-dependency-path: launchpad/package-lock.json
- run: npm clean-install
- run: npm run test # node scripts/preflight.mjs — a bad ship.config.json ABORTS here
- run: npm run test
- run: bash scripts/report.sh
env:
BOARD_URL: ${{ vars.BOARD_URL }}
SHIPIT_TOKEN: ${{ secrets.SHIPIT_TOKEN }}

deploy:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: test # deploy only runs if the pre-flight gate is green
needs: test
if: always() && github.event.pull_request.merged == true
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deploy.outputs.page_url }}
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 20
cache: npm
cache-dependency-path: launchpad/package-lock.json
- run: npm clean-install
- run: npm run build
env:
VITE_BOARD_URL: ${{ vars.BOARD_URL }}
VITE_CALLSIGN: ${{ github.actor }}
- uses: actions/upload-pages-artifact@v5
with:
path: launchpad/dist
- id: deploy
uses: actions/deploy-pages@v5
- uses: actions/deploy-pages@v5

permissions:
pages: write
id-token: write
155 changes: 127 additions & 28 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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://<owner>.github.io[/<repo>]/`) — 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.
Expand All @@ -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
Expand All @@ -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

Expand All @@ -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
Expand Down
28 changes: 11 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
2 changes: 2 additions & 0 deletions board/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 ---
Expand Down
1 change: 1 addition & 0 deletions board/client/fallback.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export function createFallback(container) {
<span class="cs">@${escapeHtml(s.callsign)}</span>
<span class="st st-${escapeHtml(s.status)}">${escapeHtml(s.stage)} · ${escapeHtml(s.status)}</span>
<span class="model">${escapeHtml(s.shipModel || '')}</span>
${s.live ? '<span class="live">LIVE</span>' : ''}
</div>`).join('');
},
dispose() { el.remove(); },
Expand Down
1 change: 1 addition & 0 deletions board/client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ <h1>Mission&nbsp;Control</h1>
</ul>
</div>
<div id="toasts" aria-live="polite"></div>
<div id="race-hud" hidden><span id="hud-clients">0</span> connected · <span id="hud-hz">20</span>×/sec</div>
<div id="app"></div>
<script type="module" src="./main.js"></script>
</body>
Expand Down
Loading