diff --git a/devlog/_plan/260827_release_train/020_preview_release.md b/devlog/_plan/260827_release_train/020_preview_release.md index 4a8015e8d85..e7fc0517677 100644 --- a/devlog/_plan/260827_release_train/020_preview_release.md +++ b/devlog/_plan/260827_release_train/020_preview_release.md @@ -1,40 +1,104 @@ -# 020 — publish the preview prerelease +# 020 — publish the preview prerelease (revised) + +Supersedes the first draft of this page. The original said "invoke the script in a way that +does not execute the suite here," which an independent audit of `scripts/release.ts` showed +does not exist: the local suite at `scripts/release.ts:521-555` has no flag and no env +escape, and the documented `--publish` re-entry re-runs it. ## Target -`2.34.0-preview.20260827`, dist-tag `preview`. +`2.34.0-preview.20260827`, dist-tag `preview`, from branch `preview` at the release commit. + +## Division of labour, measured rather than assumed + +`release.yml` never runs the test suite. What the script does that the workflow does not: + +| Step | Script | Workflow | +| --- | --- | --- | +| branch/clean-tree/version-shape preflight | yes | yes, later, against `GITHUB_REF` | +| `assertUnusedReleaseVersion` + channel-moves-forward | both | unused-only | +| `audit:high`, `tsc`, **full suite**, `privacy:scan` | yes | `audit:high` yes; suite **no**; `tsc`+GUI inside `prepublishOnly` | +| bump, commit `release: v…`, deploy-key push | **yes** | **no** | +| wait for push-event `ci.yml` + `service-lifecycle` at the sha | yes | requires the runs exist; does not wait | +| create git tag + GitHub release | **no** | **yes**, and only when `dry-run != true` | -## How +So the git steps are the script's alone, and the publish gates are the workflow's alone. -From a checkout on `preview`, with the deploy key exported: +## Chosen route + +Run the git steps by hand, let the branch's own push-event CI run, then dispatch: ``` -OCX_RELEASE_SSH_KEY=~/.ssh/opencodex_release_ed25519 \ - bun scripts/release.ts 2.34.0-preview.20260827 --tag preview +# on preview, at the promoted head +npm version 2.34.0-preview.20260827 --no-git-tag-version +git commit -am 'release: v2.34.0-preview.20260827' +GIT_SSH_COMMAND='ssh -i ~/.ssh/opencodex_release_ed25519 -o IdentitiesOnly=yes' \ + git push git@github.com:lidge-jun/opencodex.git HEAD:preview +# wait for push-event ci.yml AND service-lifecycle at that exact sha +gh workflow run release.yml --ref preview \ + -f version=2.34.0-preview.20260827 -f tag=preview \ + -f expected-sha=<40-char sha> -f dry-run=true +# inspect, then re-dispatch with dry-run=false ``` -That is the dry run — `release.yml` defaults `dry-run=true`, and the script bumps, commits, -and pushes the real release commit either way. Inspect the dispatched run, then re-run the -same command with `--publish`. +The suite runs on `ssh lidge` via `ocx-run` at that exact sha, as every phase of the +hardening unit did. This is not skipping a gate: the suite is not one of `release.yml`'s +gates, and the ones that are get enforced server-side regardless of what ran locally. + +Rejected alternative: running `bun scripts/release.ts` on lidge. It would work and it is +the more faithful path, but it puts an interactive multi-stage release — including a +20-minute CI wait and a deploy-key push — behind an ssh session, where a dropped +connection strands a half-pushed release. The hand route makes each step separately +observable and separately retryable. + +## Two traps, both verified in source -## The preflight problem, and how this phase satisfies it honestly +**A dry run still pushes the bump.** `dry-run` only controls the workflow's publish/tag/ +release steps (`release.yml:319-348`). The release commit is real either way, which is the +point: the dry run exercises the actual commit. -The preflight runs the suite locally, which is forbidden here. Do not edit the script to -skip it. Instead: +**`release.ts` skips the bump when the version already matches** (`release.ts:568`, +`if (currentVersion === version)`). Harmless here, since `preview` carries `2.34.0` after +the promotion and the target is the prerelease string. It matters for the stable release, +where the target `2.34.0` equals what `dev` already carries — recorded in `040`. -1. Run the full suite on `ssh lidge` via `ocx-run` at the exact release sha first. -2. Let `release.ts` reach its test step. If it runs the suite locally, that violates the - constraint — so the suite step must be satisfied by the remote run and the script - invoked in a way that does not execute it here, or the release must be dispatched - directly via `gh workflow run release.yml` with the same `expected-sha` the script - would have used. -3. Whichever route is taken, record which gates actually ran and where. The binding - checks are the workflow's own: `release.yml` verifies the version matches - `package.json` and refuses if the branch moved off `expected-sha`. +**Do not create the tag locally.** The `Protect release tags` ruleset (`20769150`) covers +`refs/tags/v*` with `deletion`, `non_fast_forward`, and `update` and has no bypass actor, and +`release.yml:268-274` refuses to publish a version whose tag already exists. The workflow +creates the tag after a successful publish. ## Acceptance - `npm view @bitkyc08/opencodex dist-tags` shows `preview = 2.34.0-preview.20260827` - `npm view @bitkyc08/opencodex@2.34.0-preview.20260827 gitHead` equals the release commit -- the Release workflow run concluded `success` and was NOT a dry run -- Cross-platform CI and Service lifecycle were green at the release sha before dispatch +- the Release run concluded `success` with `dry-run=false` +- push-event Cross-platform CI and Service lifecycle were green at the release sha first + +## Outcome — shipped + +Release commit `809a06ba00340c905dfac4ab588616e638c2fbfd`, one file changed +(`package.json`, 1 insertion 1 deletion), which is the same shape as both precedent +release commits `ec51e42d7` (v2.33.0) and `678517f56` (v2.33.0-preview.20260825). Built in +a detached worktree at `.tmp/rel-preview` so the `dev` checkout and the running local proxy +were never touched, then pushed to `preview` with the deploy key. + +Gates, in the order the workflow demanded them: + +| Gate | Evidence | +| --- | --- | +| full suite at the promoted tree | `pvsuite` on `ssh lidge`, 15334 pass / 0 fail, rc=0, at `62dfc6c54` | +| push-event Cross-platform CI | run `33072435012`, success at `809a06ba0` | +| Service lifecycle | run `33072435013`, success at `809a06ba0` | +| Release dry run | run `33073378226`, success; packed 838 files / 9.3 MB incl. a freshly built `gui/dist` | +| Release publish | run `33073503058`, success | + +Registry and git metadata after the publish: + +- `dist-tags` = `{ preview: 2.34.0-preview.20260827, latest: 2.33.0 }` +- `gitHead` of the published version = `809a06ba00340c905dfac4ab588616e638c2fbfd` +- tag `v2.34.0-preview.20260827` resolves to the same commit; GitHub Release exists with + `prerelease=true` + +The `gui/dist` question the audit raised answered itself in the dry run: the directory is +gitignored but listed in `files`, and `prepublishOnly` builds it inside the workflow before +`npm pack`, so the tarball carried `gui/dist/assets/index-BPGhccMP.js` and the rest. diff --git a/devlog/_plan/260827_release_train/030_main_promote.md b/devlog/_plan/260827_release_train/030_main_promote.md index 62031677722..6cff8ade3b4 100644 --- a/devlog/_plan/260827_release_train/030_main_promote.md +++ b/devlog/_plan/260827_release_train/030_main_promote.md @@ -26,3 +26,53 @@ release a re-publication of already-exercised content rather than a first contac PR #2745 is unmerged by design, so the credential-identity drift it fixes ships to `main` unfixed. That is disclosed in the readiness statement and is not a new decision made here. The release notes must not imply otherwise. + +## Outcome — promoted, and the plan changed on contact + +`main` = `80fff9a7f47332a4445df2b26ea175053fa55b0b` (merge of PR #2760, branch +`codex/promote-main-2340` at `e25b653a2`). `git diff origin/dev origin/main` is **empty** — +not "only package.json", empty — and `main` now carries `2.34.0`. + +### The stale-version pattern this page inherited is no longer legal + +This page and `000` both planned to keep `main`'s `2.33.0` through the conflict, following +#2553 and #2507, so that the release bump would land on its own `release: v2.34.0` commit. +The first push of the branch (`8a0bd3f83`) did exactly that and CI failed it correctly: + +``` +(fail) release version line > the in-tree version is never behind a released one +``` + +in both `test 3/4` (job `98518314466`) and `macos` (job `98518314397`). + +`tests/release-version-line.test.ts` arrived **in this very delta**. It compares +`package.json` against the highest local release tag, and once `v2.34.0-preview.20260827` +existed, `compareReleaseTags("v2.33.0", "v2.34.0-preview.20260827")` is `-1`. The stale line +put the tree behind a published version — precisely the "merging into main resolves +package.json to main's side and silently republishes" failure the test's own header +describes. The precedent PRs predate the test; they were not wrong, they are superseded. + +Resolved by rebuilding from `ec51e42d7` with the conflict taken to dev's side +(`8a0bd3f83` → `e25b653a2`, force-with-lease). + +### What that costs at the release step + +`release.ts:568` skips the bump when `package.json` already matches the target, so +`v2.34.0` will be tagged on the promotion merge commit rather than on a separate +`release: v2.34.0` commit. Acceptable: `release.yml` creates the tag itself after a +successful publish and validates `expected-sha` against the checked-out commit, so the tag +still names exactly the audited tree. `040` proceeds against `80fff9a7f` directly. + +### Gate accounting + +| Check | Result | +| --- | --- | +| Cross-platform CI `33074009466` | success, zero failed jobs | +| Service lifecycle `33074009519` | success | +| PR hygiene `33074473195` | success after `suppression-approved` was re-applied | +| `enforce-target` | `wrong_base`, expected for a promotion (`ALLOWED_BASES = ["dev"]`) | +| CodeQL | 53 alerts, none introduced: `dev` already has 84 open (78 high), `main` 73, and the branch diff against `dev` is empty | + +The force-push cleared `suppression-approved` and re-added `intake: hygiene-blocked`, which +is worth knowing for the next promotion: the label has to be re-applied after **every** +push, not just the first. diff --git a/devlog/_plan/260827_release_train/040_stable_release.md b/devlog/_plan/260827_release_train/040_stable_release.md index 29083d61522..49746d26782 100644 --- a/devlog/_plan/260827_release_train/040_stable_release.md +++ b/devlog/_plan/260827_release_train/040_stable_release.md @@ -4,18 +4,29 @@ `2.34.0`, dist-tag `latest`. -## How +## How — revised after the promotion -From a checkout on `main`: +There is no release commit to make. `main` is `80fff9a7f` and already carries `2.34.0`, +because `030` had to resolve the promotion conflict to dev's side to satisfy +`tests/release-version-line.test.ts`. `release.ts:568` would skip the bump for exactly this +reason, so `80fff9a7f` **is** the release commit and the workflow tags it directly. + +That removes the only step the hand route existed to perform, so `040` is a pure dispatch: ``` -OCX_RELEASE_SSH_KEY=~/.ssh/opencodex_release_ed25519 \ - bun scripts/release.ts 2.34.0 --tag latest # dry run - # inspect, then re-run with --publish +# wait for the push-event ci.yml AND service-lifecycle runs at 80fff9a7f +gh workflow run release.yml --ref main \ + -f version=2.34.0 -f tag=latest \ + -f expected-sha=80fff9a7f47332a4445df2b26ea175053fa55b0b -f dry-run=true +# inspect the packed file list, then re-dispatch with dry-run=false ``` +The promotion push started both required runs on `main` on its own, plus +`deploy-docs.yml`, which is what `050` needs. + `release.ts` refuses a prerelease version on `main`, so the plain `2.34.0` is required -here rather than a matter of taste. +here rather than a matter of taste. The workflow enforces the same mapping server-side: +`main` must publish a non-prerelease version with dist-tag `latest`. ## Acceptance @@ -30,3 +41,30 @@ here rather than a matter of taste. npm publish is irreversible. The dry run is not optional ceremony: it is the only rehearsal available. Read the dry-run job log for the packed file list before publishing — a release that ships the wrong files cannot be unshipped, only superseded. + +## Outcome — shipped + +`2.34.0` published from `main` at `80fff9a7f47332a4445df2b26ea175053fa55b0b`, which is the +promotion merge itself: no separate `release: v2.34.0` commit exists, and none was +possible, for the reason recorded in `030`. + +| Gate | Evidence | +| --- | --- | +| push-event Cross-platform CI | run `33075147758`, success at `80fff9a7f` | +| Service lifecycle | run `33075147219`, success | +| Release dry run | run `33076185925`, success; packed `@bitkyc08/opencodex@2.34.0`, 838 files, 9.3 MB, `gui/dist/index.html` present | +| Release publish | run `33076348477`, success | + +Both channels now current, and neither disturbed the other: + +- `dist-tags` = `{ latest: 2.34.0, preview: 2.34.0-preview.20260827 }` +- `gitHead` of `2.34.0` = `80fff9a7f47332a4445df2b26ea175053fa55b0b` +- tag `v2.34.0` resolves to the same commit; GitHub Release exists with `prerelease=false` + +Artifact proof, by unpacking rather than by registry metadata: `npm pack`ed the published +version into a `mktemp -d`, and `package/package.json` reads `2.34.0` with +`package/gui/dist/index.html` present and both `bin` entries intact. Installing it into the +same scratch directory and running the installed binary prints `opencodex 2.34.0`. + +The dry-run packed size matched the preview's exactly (838 files, 9.3 MB, 19.9 MB unpacked), +which is the expected result of publishing byte-identical trees to two channels. diff --git a/devlog/_plan/260827_release_train/050_deploy_and_verify.md b/devlog/_plan/260827_release_train/050_deploy_and_verify.md index c9a3f7eca7a..ffac4097961 100644 --- a/devlog/_plan/260827_release_train/050_deploy_and_verify.md +++ b/devlog/_plan/260827_release_train/050_deploy_and_verify.md @@ -28,3 +28,65 @@ unit is closable to `_fin`. - the docs deploy run for the `main` release concluded `success` - a real install of `2.34.0` reports `2.34.0` from its own runtime - the record names every sha and run id rather than describing them + +## Outcome — the release record + +### Both channels, both commits + +| Channel | Version | Release commit | Branch head | +| --- | --- | --- | --- | +| `preview` | `2.34.0-preview.20260827` | `809a06ba00340c905dfac4ab588616e638c2fbfd` | `origin/preview` | +| `latest` | `2.34.0` | `80fff9a7f47332a4445df2b26ea175053fa55b0b` | `origin/main` | + +`dev` stayed at `7ca954ffd997197d1cff6fc6d69842be51177a8f` throughout; both release trees are +byte-identical to it apart from the preview's version string. `main` needed no separate +release commit — the promotion merge is the release commit, for the reason in `030`. + +### Run ids + +| What | Run | +| --- | --- | +| preview push CI | `33072435012` | +| preview service lifecycle | `33072435013` | +| preview release dry run | `33073378226` | +| preview release publish | `33073503058` | +| promotion branch CI (the one that caught the version-line regression) | `33074009466` | +| promotion branch hygiene (after relabel) | `33074473195` | +| main push CI | `33075147758` | +| main service lifecycle | `33075147219` | +| main release dry run | `33076185925` | +| main release publish | `33076348477` | +| docs deploy | `33075147234` | + +Remote full suite: `pvsuite` on `ssh lidge` at `62dfc6c54` (the tree both releases ship), +15334 pass / 0 fail, rc=0. + +### Docs deploy + +`deploy-docs.yml` fired on the promotion push without a manual dispatch, as expected from +the `docs-site/**` path filter. Run `33075147234` at `80fff9a7f`, both `build` and `deploy` +jobs success, and the `github-pages` deployment `6123269073` is bound to that same sha. +`https://opencodex.me/` answers `200` and serves the expected title. The legacy +`/pages/builds/latest` API returns 404 here because Pages is workflow-built, not +legacy-built — that is not a failure signal. + +### Installed-runtime proof + +In a `mktemp -d`: `npm pack @bitkyc08/opencodex@2.34.0` then unpacked gives +`package/package.json` at `2.34.0` with `package/gui/dist/index.html` present and both +`bin` entries intact; installing it and running the installed binary prints +`opencodex 2.34.0`. Packed size matched the preview exactly — 838 files, 9.3 MB packed, +19.9 MB unpacked — which is what publishing identical trees to two channels should look +like. + +### What is deliberately NOT in this release + +PR #2745 (OAuth 429 credential-identity rebind) is unmerged, awaiting the security review +`MAINTAINERS.md` requires for credential-handling changes. The drift it fixes ships to both +channels unfixed. This was disclosed in the readiness statement before the train started and +is not a decision made here. + +Separately, and worth stating plainly rather than burying: `dev` carries 84 open CodeQL +alerts (78 high) against `main`'s previous 73, so this train raises the open-alert count by +11. None were introduced by the promotion itself — the branch diff against `dev` was empty — +but they now ship on `latest`. Triaging them is separate work against `dev`.