ci(cutover): the wipe-day edge → main cutover, once five gates pass (D332) #25

Merged
whitlocktech merged 2 commits from feat/auto-cutover into edge 2026-11-02 09:38:34 -06:00
Member

What & why

The wipe-day edge → main cutover, automated, so the org lead approves no merge on a wipe day. This is D332 in docs/runicnpc/PLAN.md, approved in docs#411.

When it runs. .gitea/workflows/cutover.yml wakes hourly from the forced-wipe Thursday 17:00 UTC until Sunday 23:00 UTC. It works out the weekend itself (wipeWeekend), so there are no dates and no issue numbers anywhere in it.

Five gates, cheapest first:

  1. Public changed. Rust's public build is dated the wipe day and names the same depot manifest as staging, meaning the staging build was really promoted to live.
  2. Oxide caught up. Oxide's public release is 0 members behind, RunicNPC compiles, and the swap's field list matches public Rust.
  3. Carbon boot. rust-carbon restarts with edge's RunicNPC. It must prove it installed the public manifest, then rnpc.status must say framework=carbon navmesh=ready missing=- added=-.
  4. The drill on live Rust. The drill's live half runs on public Rust with edge's plugin and harness.
  5. PR checks. They run fresh on the cutover PR's head, just before the merge.

What it remembers. Gates are kept in drill.json for one combination of wipe, public manifest and edge commit; any change runs them all again. A failed Carbon or live attempt waits 3 hours before retrying, the cool-down the org lead approved.

The merge. A merge commit (not a squash), so release.yml still sees the fix/feat subjects. It is made with RUNICNPC_TOKEN in that one step, and pinned to the commit the gates ran on with head_commit_id.

The issues. The cutover PR lists every Closes #N found in the bodies of PRs merged into edge since the last cutover. Those issues close on the merge into main, and each then gets the release version as a comment.

If a gate blocks. One comment a day on the cutover PR. At the deadline it hands back to the org lead.

Safety.

  • A dispatch is dry unless dry_run is exactly false.
  • rust-carbon is started again if the job left it stopped.
  • It never touches rust-oxide.

The main copy. It is only the trigger and the step wiring; every step runs edge's code. A dry dispatch may name another branch with code_ref. The header states this contract, and a test checks the workflow's outputs against the script's.

Other changes. drill.js only gains exports; its report already kept other keys, and now a test proves it. pr-checks runs the new tests.

Merge this first, then the one-file ci: PR into main, so Thursday's schedule finds cutover.js on edge.

How it was tested

  • node --test tools/drill/cutover.test.js tools/drill/drill.test.js scripts/checkPlugin.test.js: 85 pass, 0 fail. These include tests against a fake Gitea, fake git and a fake panel: the merge body and the moved-head refusal, dry runs writing nothing, the cool-down timing, the edge-commit reset, and the install-record (.acf) parse.
  • node scripts/checkPlugin.js: plugin ok. Both workflows parse as YAML.
  • Real Gitea data: the Closes collection over the PRs merged into edge returns [20, 22] (#24's lines; #23's "Refs" is ignored).
  • Dry run, CI run 54 (on this branch at 512efa7, forced, dry; about 50 minutes): every gate refused, for the right reason.
    • Public Rust is still the 22 October build (manifest 1632958440323253420). Staging is 8152502841220737368 (2 November).
    • Public lacks the three fields edge carries for November, and gates 2, 3 and 4 each caught that: missing=BasePlayer.timeSinceAiming,BaseEntity.ServerPreProcessed,BaseNetworkable.ignoreNetworkLod.
    • The drill's panel key wrote to, restarted and read the console of rust-carbon. The build check matched the public manifest before reading the console.
    • The live half on public Rust ran 266 pass, 0 fail, blocked only by the missing fields.
    • rust-carbon was running before and after.
    • Nothing was opened, edited, commented or merged.
  • Two independent reviews. The first said FIX FIRST; every finding was fixed. The second said READY, and its three small findings are fixed in 29226ab.

Not verifiable before Thursday: the pass path (it needs Rust's wipe update), and the real merge (head_commit_id and the token's merge rights) against Gitea 1.24. If anything there fails, the workflow comments on the cutover PR and hands back at the deadline, so the worst case is today's manual merge.

Checklist

  • I have read CONTRIBUTING.md.
  • The change builds and existing tests/checks pass locally.
  • I have added or updated tests/docs where it makes sense.
  • My commits are reasonably scoped with clear messages.

AI-assisted contributions (required)

  • No AI tools were used to produce this contribution.
  • AI tools were used. Tool(s): Claude Code (Claude Opus 5.5, with Sonnet implementer, reviewer and test-runner agents). I have reviewed and understand
    every change, and take responsibility for it. AI-authored commits are
    marked with a Co-Authored-By / Assisted-By trailer.

License

  • I agree that my contribution is licensed under this project's license
    (GNU GPL v3.0 or later), and I have the right to contribute it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY

## What & why The wipe-day `edge` → `main` cutover, automated, so the org lead approves no merge on a wipe day. This is D332 in docs/runicnpc/PLAN.md, approved in docs#411. **When it runs.** `.gitea/workflows/cutover.yml` wakes hourly from the forced-wipe Thursday 17:00 UTC until Sunday 23:00 UTC. It works out the weekend itself (`wipeWeekend`), so there are no dates and no issue numbers anywhere in it. **Five gates, cheapest first:** 1. **Public changed.** Rust's public build is dated the wipe day and names the same depot manifest as staging, meaning the staging build was really promoted to live. 2. **Oxide caught up.** Oxide's public release is 0 members behind, RunicNPC compiles, and the swap's field list matches public Rust. 3. **Carbon boot.** `rust-carbon` restarts with `edge`'s RunicNPC. It must prove it installed the public manifest, then `rnpc.status` must say `framework=carbon navmesh=ready missing=- added=-`. 4. **The drill on live Rust.** The drill's live half runs on public Rust with `edge`'s plugin and harness. 5. **PR checks.** They run fresh on the cutover PR's head, just before the merge. **What it remembers.** Gates are kept in `drill.json` for one combination of wipe, public manifest and `edge` commit; any change runs them all again. A failed Carbon or live attempt waits 3 hours before retrying, the cool-down the org lead approved. **The merge.** A merge commit (not a squash), so `release.yml` still sees the `fix`/`feat` subjects. It is made with `RUNICNPC_TOKEN` in that one step, and pinned to the commit the gates ran on with `head_commit_id`. **The issues.** The cutover PR lists every `Closes #N` found in the bodies of PRs merged into `edge` since the last cutover. Those issues close on the merge into `main`, and each then gets the release version as a comment. **If a gate blocks.** One comment a day on the cutover PR. At the deadline it hands back to the org lead. **Safety.** - A dispatch is dry unless `dry_run` is exactly `false`. - `rust-carbon` is started again if the job left it stopped. - It never touches `rust-oxide`. **The `main` copy.** It is only the trigger and the step wiring; every step runs `edge`'s code. A dry dispatch may name another branch with `code_ref`. The header states this contract, and a test checks the workflow's outputs against the script's. **Other changes.** `drill.js` only gains exports; its `report` already kept other keys, and now a test proves it. pr-checks runs the new tests. Merge this first, then the one-file `ci:` PR into `main`, so Thursday's schedule finds `cutover.js` on `edge`. ## How it was tested - `node --test tools/drill/cutover.test.js tools/drill/drill.test.js scripts/checkPlugin.test.js`: **85 pass, 0 fail**. These include tests against a fake Gitea, fake git and a fake panel: the merge body and the moved-head refusal, dry runs writing nothing, the cool-down timing, the `edge`-commit reset, and the install-record (`.acf`) parse. - `node scripts/checkPlugin.js`: plugin ok. Both workflows parse as YAML. - **Real Gitea data:** the `Closes` collection over the PRs merged into `edge` returns **[20, 22]** (#24's lines; #23's "Refs" is ignored). - **Dry run, CI run 54** (on this branch at 512efa7, forced, dry; about 50 minutes): every gate refused, for the right reason. - Public Rust is still the 22 October build (manifest 1632958440323253420). Staging is 8152502841220737368 (2 November). - Public lacks the three fields `edge` carries for November, and gates 2, 3 and 4 each caught that: `missing=BasePlayer.timeSinceAiming,BaseEntity.ServerPreProcessed,BaseNetworkable.ignoreNetworkLod`. - The drill's panel key wrote to, restarted and read the console of `rust-carbon`. The build check matched the public manifest before reading the console. - The live half on public Rust ran **266 pass, 0 fail**, blocked only by the missing fields. - `rust-carbon` was running before and after. - Nothing was opened, edited, commented or merged. - **Two independent reviews.** The first said FIX FIRST; every finding was fixed. The second said **READY**, and its three small findings are fixed in 29226ab. **Not verifiable before Thursday:** the pass path (it needs Rust's wipe update), and the real merge (`head_commit_id` and the token's merge rights) against Gitea 1.24. If anything there fails, the workflow comments on the cutover PR and hands back at the deadline, so the worst case is today's manual merge. ## Checklist - [x] I have read [CONTRIBUTING.md](CONTRIBUTING.md). - [x] The change builds and existing tests/checks pass locally. - [x] I have added or updated tests/docs where it makes sense. - [x] My commits are reasonably scoped with clear messages. ## AI-assisted contributions (required) - [ ] No AI tools were used to produce this contribution. - [x] AI tools were used. Tool(s): `Claude Code (Claude Opus 5.5, with Sonnet implementer, reviewer and test-runner agents)`. I have reviewed and understand every change, and take responsibility for it. AI-authored commits are marked with a `Co-Authored-By` / `Assisted-By` trailer. ## License - [x] I agree that my contribution is licensed under this project's license (**GNU GPL v3.0 or later**), and I have the right to contribute it. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
wtclaude added 2 commits 2026-11-02 07:09:51 -06:00
A workflow that merges edge into main on the forced-wipe weekend with no
approval: hourly from Thursday 17:00 UTC to Sunday 23:00 UTC, through five
gates in order, then a merge commit made with RUNICNPC_TOKEN (that step only)
so release.yml cuts the release.

1. Rust's public build changed: dated the wipe day and naming the same depot
   manifest as staging (the staging build was promoted to live).
2. Oxide's public release caught up, RunicNPC compiles, the swap's fields match.
3. A Carbon boot on public Rust: rust-carbon proves the public manifest is
   installed, and rnpc.status says framework carbon, navmesh ready,
   missing=- added=-.
4. The drill's live half on public Rust with edge's plugin and harness.
5. PR checks, run fresh on the cutover PR's head just before the merge.

Gates are kept in drill.json for one (wipe, public manifest, edge commit);
any change runs them again. A failed Carbon or live attempt cools down for
3 hours. The merge is pinned to the commit the gates ran on. The cutover PR
lists every `Closes #N` from the PRs merged into edge since the last cutover,
so those issues close on the merge; afterwards each gets the release version.
One comment a day while a gate blocks; at the deadline it hands back.

The copy on main is only the trigger and step wiring; every step runs edge's
code (a dry dispatch may name another branch, code_ref). Dispatches are dry
unless dry_run is exactly false. rust-carbon is started again if the job left
it stopped. drill.js only gains exports; its report already kept other keys,
now tested.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
ci(cutover): no Closes listed twice, room for the worst case, the checkout pinned (D332)
All checks were successful
PR Checks / plugin-checks (pull_request) Successful in 14s
29226ab450
From the second review:
- A PR whose merge commit main already has is not listed again. edge's tip at
  a cutover is often that PR's merge commit, and its merged_at can be a moment
  after the cut-off's committer date, so its issues would have had the next
  release's comment too.
- timeout-minutes 240: the worst case is about 200, and a timeout must never
  cut off the step that starts rust-carbon again.
- A test pins the checkout: edge, except on a dry dispatch that names code_ref.

Dry run (CI run 54, before these): every gate refused for the right reason.
Public Rust is still the 22 October build, which lacks the three fields edge
carries for November, and gates 1 to 4 each saw it; the panel key wrote to,
restarted and read rust-carbon; the live half ran 266 pass, 0 fail; nothing
was opened, edited, commented or merged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
whitlocktech merged commit fa606e5a01 into edge 2026-11-02 09:38:34 -06:00
whitlocktech deleted branch feat/auto-cutover 2026-11-02 09:38:35 -06:00
Sign in to join this conversation.
No description provided.