Task — engineering-spec@1

"CI leg rebuilds + recommits apps/console/web on real source changes - structural follow-up to TASK-IMP-080's served-bundle version-drift fix"

doneTASK-IMP-081
module improvement · class product · priority p1 · created 2026-07-13 · shipped 2026-07-13
depends on TASK-IMP-080 · blocks none

§1

  1. The changes job (.github/workflows/deploy.yml) MUST gain a web_src paths-filter output covering apps/web/** and VERSION, alongside the existing services output - so a source or VERSION change to the web app is machine-detectable the same way a services change already is.
  2. A new rebuild-web job MUST run only when web_src == 'true' (never unconditionally): stamp-sw.mjs's cache id is new Date().toISOString()-derived, not content-hash-derived, so an ungated rebuild would produce a spurious diff - and a spurious bot commit - on every single deploy.yml trigger, including ones that never touched the web app.
  3. When gated in, the job MUST rebuild apps/web (npm ci && npm run build, identical to the command TASK-IMP-080 §1 already prints as the manual rebuild instruction) and, if apps/console/web differs from the committed copy, commit it back to main as cyberos-bot <bot@cyberskill.world> and push using the same VERSION_BUMP_SSH_KEY deploy key version.yml already uses to bypass the branch ruleset - no new secret, no new bot identity.
  4. The commit message MUST carry [skip ci]: unlike version.yml's bump commit (which must stay visible to tag-triggered workflows per TASK-IMP-071), this commit only refreshes a served artifact and has no reason to re-trigger deploy.yml a second time for itself.
  5. A push blocked by the branch ruleset MUST degrade to a GITHUB_STEP_SUMMARY warning, exactly like version.yml's existing degrade path - never a hard job failure, so a ruleset misconfiguration cannot turn a client-only push red.
  6. The deploy job's needs: MUST include rebuild-web, so deploy.sh's VPS-side git pull always runs after any bundle-refresh push from this job has landed on main - closing the ordering gap, not just the detection gap.
  7. rebuild-web MUST be continue-on-error: true. It is additive to the services roll, not a gate on it: apps/web and services/ are independent deployables, and a real build failure in this job (not just a blocked push) must not trip deploy's shared needs:/failure() check and block an otherwise-healthy services roll for an unrelated breakage. The job still shows red in the Actions UI on a genuine failure - only its blast radius is contained.

Lean profile: one workflow file, one new job, one extended paths-filter output, one extended needs: list; the commit-back mechanics are a direct reuse of version.yml's already-proven pattern rather than a new design.

§2 — Why this shape (Option A over Option B)

TASK-IMP-080 §9 named two possible shapes: (A) a CI leg that rebuilds and commits the bundle back, or (B) retiring the tracked-output model entirely - gitignoring apps/console/web and shipping it via a new job mirroring the docs job's ship.sh rsync pattern (which already gitignores apps/console/docs for exactly this reason).

Option B is the architecturally cleaner end state and is what §9's own phrasing leans toward ("ships it like the docs job... retiring the tracked-output model"). It was not chosen here because it is a migration, not just a new job: apps/console/web is currently a git-pull-tracked directory that Caddy serves via the directory bind deploy/vps/deploy.sh documents. The commit that adds it to .gitignore would, on the VPS's very next git pull, remove those tracked files from the working tree - before any new ship.sh-style step could repopulate them - opening a live-site outage window with no way to rehearse or time the sequencing against the real VPS from this session. TASK-IMP-080 §9 explicitly flagged the deploy.sh caddy-bind implications as something to "decide together," not something to force through unrehearsed to close out a SHOULD-priority follow-up.

Option A carries none of that risk: it is a strictly additive job that reuses version.yml's already-battle-tested commit-back mechanics (same bot identity, same deploy key, same graceful-degrade-on-blocked-push behavior) and changes nothing about how apps/console/web is tracked or served. It converts TASK-IMP-080's fix from "the next commit fails loudly if someone forgets to rebuild" into "nothing needs to remember" - the same posture upgrade check-version-sync.sh already gave every other artifact, now closing the one gap that check can only detect, not prevent.

Sequencing note: because version.yml's bump commit and this job's own trigger both watch VERSION, a push that changes both apps/web/** source and lands moments before a version bump can produce two rebuild-web runs in quick succession - one against the pre-bump VERSION, one (triggered by the bump commit itself, since VERSION is in the filter) against the correct post-bump VERSION. This is self-correcting by construction: the second run's diff against the first run's (stale-version) commit is non-empty, so it supersedes it with the correct state. No new race class is introduced beyond the one version.yml and deploy.yml already share today (a bump commit already re-triggers a second deploy roll); this job's [skip ci] commit message additionally prevents it from re-triggering yet another full workflow run for itself.

§5 (run 2026-07-13)

Testing pass (2026-07-13, post gate-1 "go")

Stephen approved gate 1 (human review) in chat. Re-ran the machine-checkable verification set unchanged since review, to confirm nothing drifted between review and test:

Acceptance (2026-07-13)

Stephen approved gate 2 (human acceptance) in chat: "i approve". TASK-IMP-081 lands as done, shipped: 2026-07-13. The two sandbox-inherent gaps flagged above (live-deploy-key push, real build inside the actual CI checkout) remain unexercised — they close naturally on the first real deploy.yml run this job is present for, not before.

§9

§10

FailureDetectionRecovery
apps/web/** changes but the branch ruleset blocks the bot's pushGITHUB_STEP_SUMMARY warning on the run, same as version.yml's existing degrade pathadd/rotate the deploy key in the ruleset bypass list (docs/deploy/RELEASE.md), or rebuild manually per TASK-IMP-080's printed command
apps/web genuinely fails to build (real tsc/vite error on main, not a blocked push)rebuild-web job shows red in the Actions UI (continue-on-error: true does not hide the failure, only its blast radius)fix the web-app source; continue-on-error deliberately keeps this from blocking the unrelated services roll in the same run
two rebuild-web runs race a version bumpsecond run's diff is non-empty against the first (pre-bump) commit and supersedes itnone needed - self-correcting
deploy job's VPS git pull races ahead of this job's pushneeds: [..., rebuild-web] on the deploy job forces orderingnone needed by design
this job's own commit somehow lacks [skip ci] in a future editit would re-trigger a redundant (but idempotent, harmless) deploy.yml run - not a correctness bug, just wasted CI minutesrestore [skip ci] in the commit message

End of TASK-IMP-081.

Audit

  • ISS-001: draft's deploy job added rebuild-web to needs: with no continue-on-error, relying on the job's own if: (gated on web_src) to keep it out of the way. That only covers the SKIPPED case cleanly. A genuine build failure inside rebuild-web (a real tsc/vite error on main, not just a blocked push) is a different job outcome - failure() - and deploy's existing if: always() && !failure() && !cancelled() would then correctly-per-its-own-logic but wrongly-for-this-task block the entire services roll for a completely unrelated apps/web breakage, since services/ and apps/web are independent deployables. Resolved: rebuild-web set to continue-on-error: true (job still shows red in the Actions UI on a genuine failure; only its blast radius on the unrelated services pipeline is contained). §1 clause 7 and §10 added; re-validated with actionlint (clean) after the fix. Score = 10/10.