diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9953d460..e0823a6c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -219,7 +219,8 @@ jobs: # R-09 / RL-16. The release gate itself is only invoked for real at tag # time, which is the wrong place to find a bug in it — so its decision # logic is exercised here, on every pull request, against fixtures. Same - # reason Server/scripts/docker-smoke.sh is called from both workflows. + # reason Server/scripts/docker-smoke.sh is called from all three + # workflows (ci.yml, release.yml, nightly-docker-smoke.yml). # This also parses the required-check list out of # b0-dev-branch-protection.sh, so a change to that list's shape fails here # rather than silently weakening the gate. @@ -532,10 +533,13 @@ jobs: # Image build is verification only, so it is skipped on dev to keep day-to-day # work on the fast check suite. Runs for main pushes and PRs targeting main. + # + # Keep in sync with nightly-docker-smoke.yml's nightly-docker-smoke job. server-docker-build: name: Server Docker Build (verify) if: github.ref_name == 'main' || github.base_ref == 'main' runs-on: ubuntu-latest + timeout-minutes: 20 steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 diff --git a/.github/workflows/nightly-docker-smoke.yml b/.github/workflows/nightly-docker-smoke.yml new file mode 100644 index 00000000..98df195d --- /dev/null +++ b/.github/workflows/nightly-docker-smoke.yml @@ -0,0 +1,68 @@ +# Nightly Docker boot smoke against dev (B3-6 item 8). dev is not a push +# trigger in ci.yml, so an image regression on dev is otherwise only caught +# once a dev -> main PR opens. +# +# Its own file rather than a schedule on ci.yml, which is what the plan first +# proposed: a scheduled run attaches its check runs to the DEFAULT branch's +# tip, so the jobs that would have to be skipped to scope the nightly to the +# smoke would land on main's tip as `skipped` under names that are required +# contexts. scripts/verify-gate-evidence.mjs:45-61 keeps the latest attempt +# per name and does not count `skipped` as success, so release.yml's +# gate-evidence job would then refuse to tag that commit. The job name below +# matches no required context, and ci.yml is left alone. +# +# A schedule only ever runs from the default branch: this file does nothing +# until it reaches main, and the first nightly follows the next release merge. +name: Nightly Docker Smoke + +on: + schedule: + - cron: "0 3 * * *" + workflow_dispatch: + +concurrency: + group: nightly-docker-smoke-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + # Keep in sync with ci.yml's server-docker-build job. + nightly-docker-smoke: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + with: + # This file is read from the default branch on a schedule; dev is the + # branch the nightly exists to smoke. + ref: dev + + # So every run's log states what was smoked, rather than leaving it to be + # re-derived from the trigger. + - name: Print checked-out revision + env: + EVENT: ${{ github.event_name }} + REF: ${{ github.ref }} + run: | + echo "event=$EVENT ref=$REF" + git rev-parse HEAD + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + + - name: Build image (no push) + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 + with: + context: Server/ + push: false + load: true + tags: owncord-smoke:candidate + build-args: VERSION=ci + cache-from: type=gha + cache-to: type=gha,mode=max + + # Same script release.yml and ci.yml run. + - name: Boot-smoke Docker image + run: bash Server/scripts/docker-smoke.sh owncord-smoke:candidate diff --git a/docs/plans/b3-server-architecture-guardrails-2026-08-29.md b/docs/plans/b3-server-architecture-guardrails-2026-08-29.md index a3ca49c5..f89fe979 100644 --- a/docs/plans/b3-server-architecture-guardrails-2026-08-29.md +++ b/docs/plans/b3-server-architecture-guardrails-2026-08-29.md @@ -997,6 +997,86 @@ db`). No control file is committed. check, not a parse — skipped, and no file under `auth/`, `service/` or `api/auth_*` was touched. +#### Evidence — item 8 (Docker smoke nightly on `dev`) + +- Branch `feat/b3-6-docker-smoke-nightly`. Code commit: `3f27447e` feat(b3-6): + nightly docker smoke on dev — its own workflow, plus a timeout on ci.yml's + verify job. A temporary commit added a `push:` trigger so the proof run + below could happen at all — neither of the file's real triggers can fire + before it is on the default branch — and was dropped once the run was + recorded. +- **Deviation from this item's text, and why.** The item says `ci.yml` gains + the `schedule:` trigger and that the job is deliberately "not moved to its + own workflow file". It is moved. Scoping a schedule inside `ci.yml` means + skipping the other jobs with a job-level `if:`, and **a skipped job still + writes a check run** — observed on `main`'s tip, where + `Tauri Full Build (${{ matrix.os }})` reports `conclusion: skipped` beside + the green required contexts (`gh api repos/J3vb/OwnCord/commits/main/check-runs`). + A scheduled run is attached to the **default branch's tip SHA** whatever it + later checks out, so each nightly would stamp `skipped` onto `main`'s tip + under seven of the twelve `contexts` in `b0-dev-branch-protection.sh`: + Client Static Checks, Client Unit Tests, Rust Unit Tests, Repository + Hygiene, Docs & Ledger Consistency, Client E2E (Playwright), Client E2E + (parity subset, blocking). `scripts/verify-gate-evidence.mjs:45-61` keeps + the latest attempt per name and treats `skipped` as not-success, and + `release.yml`'s `gate-evidence` job runs it on the tagged SHA and gates + every build and publish job — so a release cut from a `main` tip that had + sat through one nightly would be refused, for a reason that has nothing to + do with that commit. `nightly-docker-smoke.yml`'s single job matches no + required context, so it cannot overwrite gate evidence, and `ci.yml`'s job + selection is untouched. + (`server-build-test` would have escaped: a skipped matrix job reports under + the unexpanded name, which is not a required context. `admin-e2e` is not + required, and the three `Analyze` contexts come from CodeQL default setup.) +- **When the nightly actually starts.** A scheduled workflow only ever runs + from the default branch, so this file does nothing on `dev`: the first + nightly fires after it reaches `main` at the next release merge. Until + then, `workflow_dispatch` is the only way to run it. +- Contents: `on: schedule` (`0 3 * * *`) + `workflow_dispatch`; a + `concurrency` group with `cancel-in-progress: true`; + `permissions: contents: read`; one job, `nightly-docker-smoke`, with + `timeout-minutes: 20`; `actions/checkout` on the same pinned SHA `ci.yml` + uses, with `ref: dev`, since a schedule reads the file from `main`; a + "Print checked-out revision" step logging `github.event_name`, + `github.ref` and `git rev-parse HEAD`; then the buildx, build and + `Server/scripts/docker-smoke.sh` steps of `server-docker-build`, verbatim. + Both jobs carry a one-line keep-in-sync comment naming the other. +- Numbers: `ci.yml` +3 lines (a two-line comment and `timeout-minutes: 20`); + `nightly-docker-smoke.yml` 68 lines; the shared buildx/build/smoke steps + diff clean between the two files, ignoring comments. +- GREEN: `npx prettier --check` on both workflows → "All matched files use + Prettier code style!"; `actionlint` on all five workflows → clean; + `node scripts/check-workflow-guards.mjs` → + `4 guard(s) present in 1 metered workflow(s)`, and its `--selftest` → all + assertions pass; + `npm run check:docs` → passed; `node scripts/run.mjs --list` → picks the new + workflow up in the actionlint file list, which is built from `git ls-files`. +- Proof, observed: run `33301623322` + (https://github.com/J3vb/OwnCord/actions/runs/33301623322), workflow + "Nightly Docker Smoke", fired by the temporary push trigger that was dropped + from the branch afterwards. "Print checked-out revision" logged + `event=push ref=refs/heads/feat/b3-6-docker-smoke-nightly`, and + `git rev-parse HEAD` printed `75d64dd412b6e81a19ae0cb2e09ecfc84d6f644e` — + `origin/dev`'s tip (`75d64dd4`) at the time of the run, not the branch's, + which is the whole point of `ref: dev`. Build and boot-smoke green. Re-read + it with + `gh run view 33301623322 --log | grep -A2 "Print checked-out revision"`. +- The one behavioural difference from `ci.yml`'s job: a scheduled run has + `github.ref = refs/heads/main`, so the buildx `type=gha` cache is scoped to + the default branch while the layers written into it come from `dev`'s tree. + The cache is content-addressed, so a layer is only ever reused where its + content matches — the mismatch costs cache hits, never correctness. +- A red nightly is the repository owner's: GitHub emails a scheduled + workflow's failure to the account that owns it. No new process, just where + it lands. +- Verified against HEAD: the item states "`concurrency` and `timeout-minutes` + are already present (B1-7's guard check enforces both)". Only `concurrency` + was. `scripts/check-workflow-guards.mjs` audits only the workflows in + `METERED`, which is `[".github/workflows/claude.yml"]` — it never looked at + `ci.yml` — and `server-docker-build` was one of seven jobs of eleven there + with no `timeout-minutes`, so it inherited GitHub's 360-minute default. + Hence the one-line `ci.yml` change; the new workflow declares its own. + ## B3-7 — Alpha-shaped test dataset Roadmap workstream 12. Beside the slice.