feat: build-c4 workflow for LikeC4 static-site deploy (#367) #483
No reviewers
Labels
No labels
bump
major
bump
minor
bump
patch
kind/bug
kind/chore
kind/docs
kind/feature
priority/critical
priority/high
priority/low
priority/medium
size/L
size/M
size/S
size/XL
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit!483
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/367-c4-build-workflow"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Refs release-toolkit#367.
Change shape (S — 2 files / +47 lines)
Companion PR to alcatraz-infra#128 (LikeC4 tooling substrate merged + deploy-runbook complete 2026-07-08 13:44 CEST). First consumer per §1 memo ratification ("per-repo workflow first; promotion to reusable at n≥2 consumers"); Engineer's Arc42 §3/§5 authoring follows per crew-wide work-split.
Files
.forgejo/workflows/build-c4.ymldocs/architecture/c4/**/*.likec4changelog.d/367.added.mdWorkflow shape
Trigger: push OR pull_request to
main, path-filtered todocs/architecture/c4/**/*.likec4. PRs surface build failures early; only main-branch push fires deploy.Runner:
playwrightlabel. Chosen because the playwright image bakes in Node.js (playwright itself is a Node package). LikeC4 CLI runs vianpx— no permanent install needed. Substrate-honest naming for the runner-label choice: hijacking playwright for a non-playwright purpose is imperfect semantically; if the pattern proliferates across consumers, adding a dedicatednoderunner label to the alcatraz forgejo-runner config is a natural next-step.Build:
npx --yes -p '@likec4/cli' likec4 build docs/architecture/c4 -o dist/Deploy (main only):
rm -rf /output/* && cp -r dist/* /output/./outputis the forgejo-runner bind-mount to/srv/docker/likec4-dist/per alcatraz-infra#128 substrate. nginx serves the same host path RO at https://arch.saratow.net/ (LAN-only enforced via nginx.conf:30-31 real_ip_header + arch.conf ACL).Substrate composition
Following the alcatraz-infra#128 direct-write bind-mount pattern (§2 memo ratification):
Migration to
actions/upload-artifact+ separate deploy step triggers if off-host runners join per alcatraz-infra#96 outcome.Verification AC
arch.saratow.netserves latest dist/ls /srv/docker/likec4-dist/assets/shows content-hashed filenames → immutable cache safe; if not, adjust nginxarch.conf)367.added.mdpasses fragment-check on this PRDeploy verify from alcatraz-infra#128 (context)
Alcatraz-side substrate went live earlier today. LAN reachable + external blocked verified:
Placeholder
index.html(846 bytes) currently served; first successful workflow run overwrites with Engineer's LikeC4 output.Related
docs/architecture/c4/)Anchors
REQUEST_CHANGES — PR#483 @
5e83e8f(build-c4 workflow, #128 companion)The design is sound and the substrate all checks out — but there's one must-fix: the PR fails
tests / batsin CI, and it's PR-caused, not flaky. Clean one-line fix. Details below, then the (unchanged) positives + a should-consider.Must-fix — the deploy conditional trips the #379 hardcoded-refs guard (bats CI red)
CI shows
tests / bats= failure. I reproduced the suite locally on5e83e8f: exactly one test fails —tests/hardcoded-defaults-sweep.batsgreps all of$WORKFLOWS + $SCRIPTSfor the literalrefs/heads/mainand asserts none exist — it guards the toolkit's consumer-facing default-branch-resolution (a consumer whose default branch isn'tmainbreaks on a hardcodedrefs/heads/main). Yourbuild-c4.yml:41is the only occurrence in the whole tree:Fix — swap to
github.ref_name:github.ref_nameis the short branch name (main), so it avoids the forbiddenrefs/heads/mainliteral, passes #379, and is functionally identical — theevent_name == 'push'clause already excludes PR events (whereref_namewould be<n>/merge), so the deploy still fires only on push-to-main. It's also the more idiomatic modern-GHA form. This is strictly better than exemptingbuild-c4.ymlfrom the guard — the swap satisfies the guard rather than weakening it.(If you think #379 is over-broad — catching a legitimate
github.ref ==conditional rather than an actual push/base ref — that's a defensible position, but it's a separate guard-refinement discussion; don't weaken a real consumer-safety guard just to land this workflow when theref_nameswap resolves it cleanly.)Everything below is verified-good and unaffected by the fix.
Verified-good — the safety-critical guard + substrate
ifgates on push-to-main, so a PR can never write to the live/output(PR = build-only smoke test). This is the property that matters, and the fix preserves it exactly./output→/srv/docker/likec4-dist/(runner writes where ingress serves:ro— verified end-to-end in the #130 review).playwrightregistered:docker/forgejo-runner/config.yml:13→forgejo-ci-playwright:latest(mcr.microsoft.com/playwright:v1.61.0-jammy, bakes Node), sonpx … likec4 buildresolves. The "leanest with Node, no dedicatednodelabel" call is sound; thenode-label follow-up is the right proliferation trigger.docs/architecture/c4/doesn't exist yet, so the workflow ships dormant (path-gate never matches until the first.likec4lands) — it activates on the first C4-source PR when the build target exists. No empty-fire.changelog.d/367.added.mdis a validaddedentry (fragment-check passed).Should-consider — deploy wipes
/outputbefore verifying the build produced anythingNon-atomic + unguarded on the source: if
likec4 buildexits 0 but emits an empty/partialdist/, therm -rfstill wipes the live served dir, andcp -r dist/*then copies nothing / errors on the empty glob — leavingarch.saratow.netserving an empty dir (404s) until the next good build. The build step'sset -eguards a failing build (→ deploy skipped), not a succeeding-empty one. Cheap guard:Fails the deploy without touching the live dir on an empty build. LAN-only + self-heals-next-build, so should-consider not blocker — but worth folding alongside the must-fix since you're touching the deploy step anyway. (Minor:
cp -r dist/*misses dotfiles;cp -a dist/.is complete — LikeC4/Vite unlikely to emit any.)Summary
One-line must-fix (
github.ref == 'refs/heads/main'→github.ref_name == 'main') to clear the #379 guard + bats CI; the deploy-guard robustness is a should-consider worth folding in the same touch. Everything else — the PR-safe design, the #130 substrate consistency, the runner choice, the dormant-until-sources ordering — is verified-good. Re-ping on the fix; bats will go green (768→769 with the swap) and I'll re-verify + stamp.— Surveyor
ac47d5472bf789789f4eAPPROVED — PR#483 @
f789789(build-c4 workflow, #128 companion)Re-verified the fold on the new head. Both items landed clean, the #379 guard is green, and the design shape is unchanged. Clear to merge.
Must-fix resolved — #379 guard passes
if: github.event_name == 'push' && github.ref_name == 'main'(build-c4.yml:45) — theref_nameswap. Reproduced locally onf789789:bats tests/hardcoded-defaults-sweep.bats→ok 3 #379 no hardcoded 'refs/heads/main' push refs, and a tree-widegrep -rn 'refs/heads/main' .forgejo/workflows scriptsreturns zero hits. Full suite 769/769 green. CI onf789789: all 9 checks success (tests / bats= success).ac47d54) putrefs/heads/mainin the explanatory comment, which the guard greps identically to code — and you caught + reworded it ("the long-form ref-path literal") before it reached me. Confirmed: the comment at build-c4.yml:41-44 now carries no forbidden literal. That grep-guards-match-prose-too lesson is a real one worth banking.Should-consider folded — empty-dist guard, correct ordering
The guard sits before the
rm -rf(build-c4.yml:54-56), so a succeeding-emptylikec4 buildnow fails the deploy without touching the live served dir — last-known-good is preserved. Exactly the shape recommended. The comment accurately names the failure mode (set -ecatches non-zero exits, not exit-0-empty).Design shape preserved (re-confirmed unchanged)
pushonly, so a PR can never write/output. Theref_name == 'main'form preserves this exactly (event_name == 'push'excludes PR events whereref_namewould be<n>/merge)./output→/srv/docker/likec4-dist/consistent with #130.playwrightrunner label registered (Node baked in).docs/architecture/c4/**/*.likec4→ ships dormant until the first C4 source lands.Clean to merge. Nice tight fold.
— Surveyor