From f7f07fd07a2676ee79bed941500635c8ef3852a8 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Thu, 27 Aug 2026 21:01:14 +0900 Subject: [PATCH] docs(devlog): plan the dev -> preview -> main release train Roadmap for promoting dev (7c333f30e) to both published channels, written before any branch moves because a promotion that drops a real commit is not recoverable by rerunning it. The reconciliation that made this worth writing down: preview looks 2 commits AHEAD of dev, and cf762b1f5 displays a 24-line change to tests/api-usage.test.ts that `git merge-base --is-ancestor` confirms is genuinely not in dev's history. Content tells a different story than ancestry - the file is byte-identical between the branches, and since the merge-base preview's only exclusive change is package.json's version string. cf762b1f5 is a merge whose second parent IS the merge-base, so the change reached dev by another commit. Nothing is lost. Two corrections are recorded as an amendment rather than silently applied: - The local test suite in scripts/release.ts is not the binding gate. release.yml re-enforces expected-sha, version match, branch/tag agreement, a push-event CI run for the exact sha, the service-lifecycle gate, audit:high, and typecheck+build via prepublishOnly - and it creates the tag and GitHub Release itself. So the release path is bump, push, wait for the branch's own CI, then dispatch with the same expected-sha. - enforce-target's ALLOWED_BASES is exactly [dev], so a promotion PR is a hard wrong_base failure and gets [WRONG BRANCH] prepended. Every promotion in this repo's history carries that and merged anyway; it is a known false positive, not a stop signal. Version targets, checked against the live registry rather than assumed: 2.34.0-preview.20260827 for preview and 2.34.0 for latest. Both move their channel forward past 2.33.0, 2.34.0 is unused on npm and as a git tag, and it is already what dev's package.json reads. --- .../000_state_and_decisions.md | 139 ++++++++++++++++++ .../010_preview_promote.md | 34 +++++ .../020_preview_release.md | 40 +++++ .../260827_release_train/030_main_promote.md | 28 ++++ .../040_stable_release.md | 32 ++++ .../050_deploy_and_verify.md | 30 ++++ 6 files changed, 303 insertions(+) create mode 100644 devlog/_plan/260827_release_train/000_state_and_decisions.md create mode 100644 devlog/_plan/260827_release_train/010_preview_promote.md create mode 100644 devlog/_plan/260827_release_train/020_preview_release.md create mode 100644 devlog/_plan/260827_release_train/030_main_promote.md create mode 100644 devlog/_plan/260827_release_train/040_stable_release.md create mode 100644 devlog/_plan/260827_release_train/050_deploy_and_verify.md diff --git a/devlog/_plan/260827_release_train/000_state_and_decisions.md b/devlog/_plan/260827_release_train/000_state_and_decisions.md new file mode 100644 index 00000000000..f3752f5de86 --- /dev/null +++ b/devlog/_plan/260827_release_train/000_state_and_decisions.md @@ -0,0 +1,139 @@ +# 260827 release train — dev 7c333f30e to published + +Research and decisions for promoting `dev` to both published channels. Written before any +branch moves, because a promotion that drops a real commit is not recoverable by rerunning it. + +## Starting state, measured + +| Thing | Value | +| --- | --- | +| `dev` | `7c333f30e`, 293 commits ahead of `main` | +| `main` | `ec51e42d7`, `release: v2.33.0` | +| `preview` | `678517f56`, `release: v2.33.0-preview.20260825` | +| `package.json` on dev | `2.34.0` | +| npm `latest` | `2.33.0` | +| npm `preview` | `2.33.0-preview.20260825` | +| package name | `@bitkyc08/opencodex` | + +Both channels are a full release behind `dev`. + +## The preview reconciliation question + +`preview` is 2 commits *ahead* of `dev`, which on its face means promoting `dev` onto +`preview` would discard work. It does not, and the reason is worth writing down rather than +asserting. + +The two commits are `cf762b1f5` (a `dev` merge) and `678517f56` (the version bump). +`cf762b1f5` shows a 24-line change to `tests/api-usage.test.ts`, which looks like real +content — and `git merge-base --is-ancestor cf762b1f5 origin/dev` answers **NO**, so it is +genuinely not in `dev`'s history. + +The resolution is that ancestry and content are different questions. Measuring content +directly: + +``` +git diff --stat origin/dev origin/preview -- tests/api-usage.test.ts # empty +git diff $(git merge-base origin/dev origin/preview) origin/preview --name-only +# -> package.json +``` + +So since the merge-base (`e1fb67559`), the *only* file `preview` changes is `package.json`, +and the only change is the version string. `cf762b1f5` is a merge whose second parent IS the +merge-base, so the test change it displays arrived on `dev` through a different commit and is +already present there. Nothing real is lost. + +**Decision:** promote `dev` onto `preview` and let the release bump re-establish the version. +The stale `2.33.0-preview.20260825` string is exactly what the bump is going to overwrite. + +## Version numbers + +The published scheme, read from the registry rather than assumed: +`2.29.0-preview.20260821`, `2.30.0-preview.20260821`, `2.31.0-preview.20260822`, +`2.32.0-preview.20260824`, `2.32.1-preview.20260825`, `2.33.0-preview.20260825`. So preview is +`-preview.` and stable is plain semver. + +- preview target: **`2.34.0-preview.20260827`** +- stable target: **`2.34.0`** + +`2.34.0` is what `dev`'s `package.json` already reads (wp2 of the hardening unit moved it +there), it is unused on npm (404) and unused as a git tag, and both targets move their +channel forward past `2.33.0`, which is what `assertChannelVersionMovesForward` requires. + +## Mechanics that constrain the plan + +`main` and `preview` rulesets are `[deletion, non_fast_forward, pull_request]` with bypass +actors `DeployKey = always` and `RepositoryRole 5 = pull_request`. Two consequences: + +- Content promotion cannot be a push. It travels as a PR merged with `--admin`. +- The release bump push *can* work, but only through the deploy key. `scripts/release.ts` + reads `OCX_RELEASE_SSH_KEY`; the key at `~/.ssh/opencodex_release_ed25519` was confirmed to + authenticate and to see both branches. + +`scripts/release.ts` is the release authority and does the whole sequence: preflight +(clean tree, `audit:high`, `tsc`, the CI-matching test grouping, `privacy:scan`), bump, +commit, push, wait for **both** Cross-platform CI and Service lifecycle at the release sha, +then dispatch `release.yml` with `version`, `tag`, `expected-sha`, `dry-run`. + +`release.yml` requires `expected-sha` and defaults `dry-run=true`, so a dry run exercises the +real release commit. Re-running with `--publish` is the documented second step and the script +is written to be idempotent across it. + +## Constraint that shapes execution + +The preflight runs the full test suite locally, which this session is forbidden to do. That +is not a reason to bypass the preflight — it is a reason to run the suite where it belongs +(`ssh lidge` via `ocx-run` at the exact release sha) and to let the workflow gates be the +binding check. The plan records how each phase satisfies the preflight's intent without +running `bun run test` on this machine. +## Amendment — two things the first pass got wrong or left open + +### The preflight is not the binding gate (resolves the `020` open question) + +`020` framed the local test suite as a problem to work around. Reading `release.yml` +end to end shows the gates that actually bind are all server-side, and the script's local +preflight duplicates them as a convenience: + +| Gate | Where it lives | +| --- | --- | +| `expected-sha` matches the checked-out commit | `release.yml` "Verify dispatched SHA" | +| version equals `package.json` | "Verify version matches package.json" | +| branch/version/dist-tag agree (main=stable+latest, preview=prerelease+preview) | "Require successful Cross-platform CI" | +| a **push-event** `ci.yml` run succeeded for this sha on this branch | same step — a PR run explicitly does not qualify | +| Service lifecycle succeeded when service paths changed since the previous merged tag | same step | +| `audit:high` | "Dependency audit" job step | +| typecheck + build | `prepublishOnly`, run by `npm publish` | +| tag/release do not already exist at another sha | "Preflight release metadata" | + +The workflow also creates the git tag and the GitHub Release itself (`git tag` / +`gh release create`), so nothing about tagging depends on the local script. + +So the route is: bump and push the release commit, let the branch's own push-event CI run, +then dispatch `release.yml` directly with `gh workflow run`. That is exactly what +`scripts/release.ts` does at its end, minus the local suite. Recording it this way is not a +shortcut around a gate; it is declining to run a *duplicate* of gates the workflow enforces +anyway, on a machine that is not allowed to run them. + +The suite still runs — on `ssh lidge` at the exact release sha, as every phase of the +hardening unit did. + +### `[WRONG BRANCH]` on promotion PRs is expected, and is how this repo has always released + +`010` guessed that `enforce-target` would complain. It does more than complain: +`ALLOWED_BASES = ["dev"]` (`enforce-pr-target.yml:254`), so a promotion PR gets a +`wrong_base` failure AND has `[WRONG BRANCH] ` prepended to its title. + +This is not a new problem to solve. Every promotion in this repository's history carries it: + +``` +#2553 codex/promote-main-2330 -> main [WRONG BRANCH] merge dev into main for the v2.33.0 release +#2507 dev -> main [WRONG BRANCH] release: promote dev into main for v2.32.1 +#2551 dev -> preview [WRONG BRANCH] merge dev into preview for the v2.33.0-preview... +``` + +All merged. The gate has no promotion exemption and the maintainers evidently merge through +it with admin rather than teaching it about release branches. Follow that precedent rather +than inventing an exemption: the check failing on a promotion PR is a known false positive, +and the merge is `--admin` regardless. + +Worth stating plainly since it looks alarming in the checks list: on a promotion PR, +`enforce-target` failing is the expected outcome, not a signal to stop. \ No newline at end of file diff --git a/devlog/_plan/260827_release_train/010_preview_promote.md b/devlog/_plan/260827_release_train/010_preview_promote.md new file mode 100644 index 00000000000..c954e1c5b7e --- /dev/null +++ b/devlog/_plan/260827_release_train/010_preview_promote.md @@ -0,0 +1,34 @@ +# 010 — promote dev content onto preview + +## What changes + +`preview` moves from `678517f56` to a merge carrying `dev`'s tree at `7c333f30e`. +No file in the repository is edited by this phase; it is a branch move only. + +## How + +`preview` requires a pull request, so: + +``` +gh pr create --base preview --head dev --title 'release: promote dev to preview' ... +gh pr merge --merge --admin +``` + +A PR from `dev` directly rather than a `codex/` branch, because the content being promoted +IS `dev` — an intermediate branch would add a commit that says nothing. + +Note `enforce-target` rejects PRs that do not target `dev`. A promotion PR targets +`preview` by definition, so expect that check to complain and confirm it is the +promotion exemption rather than a real finding before overriding it. + +## Acceptance + +- `git diff --stat origin/dev origin/preview` shows only `package.json` (the stale version + string) or nothing at all +- the merge sha is recorded +- no `src/` or `tests/` file differs between the two branches + +## What would make this wrong + +Promoting before confirming the reconciliation in `000`. That check is done: since the +merge-base, `preview`'s only exclusive change is the version string. diff --git a/devlog/_plan/260827_release_train/020_preview_release.md b/devlog/_plan/260827_release_train/020_preview_release.md new file mode 100644 index 00000000000..4a8015e8d85 --- /dev/null +++ b/devlog/_plan/260827_release_train/020_preview_release.md @@ -0,0 +1,40 @@ +# 020 — publish the preview prerelease + +## Target + +`2.34.0-preview.20260827`, dist-tag `preview`. + +## How + +From a checkout on `preview`, with the deploy key exported: + +``` +OCX_RELEASE_SSH_KEY=~/.ssh/opencodex_release_ed25519 \ + bun scripts/release.ts 2.34.0-preview.20260827 --tag preview +``` + +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 preflight problem, and how this phase satisfies it honestly + +The preflight runs the suite locally, which is forbidden here. Do not edit the script to +skip it. Instead: + +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`. + +## 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 diff --git a/devlog/_plan/260827_release_train/030_main_promote.md b/devlog/_plan/260827_release_train/030_main_promote.md new file mode 100644 index 00000000000..62031677722 --- /dev/null +++ b/devlog/_plan/260827_release_train/030_main_promote.md @@ -0,0 +1,28 @@ +# 030 — promote dev content onto main + +## What changes + +`main` moves from `ec51e42d7` (293 commits behind) to a merge carrying `dev`'s tree. +This is the promotion the readiness statement in `260827_dev_hardening/070` was written for. + +## How + +Same shape as `010`: a PR from `dev` to `main`, merged with `--admin`, because `main` +requires a pull request and the `RepositoryRole` bypass is `pull_request` only. + +## Ordering + +After `020`, not before. Publishing the preview channel first is what makes the stable +release a re-publication of already-exercised content rather than a first contact. + +## Acceptance + +- `git diff --stat origin/dev origin/main` shows only `package.json` or nothing +- the merge sha is recorded +- `main` contains `origin/dev` (`git merge-base --is-ancestor origin/dev origin/main`) + +## Carried forward from the hardening unit + +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. diff --git a/devlog/_plan/260827_release_train/040_stable_release.md b/devlog/_plan/260827_release_train/040_stable_release.md new file mode 100644 index 00000000000..29083d61522 --- /dev/null +++ b/devlog/_plan/260827_release_train/040_stable_release.md @@ -0,0 +1,32 @@ +# 040 — publish the stable release + +## Target + +`2.34.0`, dist-tag `latest`. + +## How + +From a checkout on `main`: + +``` +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 +``` + +`release.ts` refuses a prerelease version on `main`, so the plain `2.34.0` is required +here rather than a matter of taste. + +## Acceptance + +- `npm view @bitkyc08/opencodex dist-tags` shows `latest = 2.34.0` +- `npm view @bitkyc08/opencodex@2.34.0 gitHead` equals the `main` release commit +- the `v2.34.0` git tag exists and points at that commit +- the published tarball's `package.json` version reads `2.34.0` — checked by unpacking, + not by trusting registry metadata + +## Risk + +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. diff --git a/devlog/_plan/260827_release_train/050_deploy_and_verify.md b/devlog/_plan/260827_release_train/050_deploy_and_verify.md new file mode 100644 index 00000000000..c9a3f7eca7a --- /dev/null +++ b/devlog/_plan/260827_release_train/050_deploy_and_verify.md @@ -0,0 +1,30 @@ +# 050 — docs deploy and installed-runtime proof + +## Docs deploy + +`deploy-docs.yml` triggers on push to `main` under `docs-site/**`. The `030` promotion +carries 293 commits of `docs-site` changes, so the deploy should fire on that merge without +a manual dispatch. Confirm rather than assume: if the path filter did not match, dispatch it. + +## Installed-runtime proof + +Registry metadata is not the same claim as a working install. Install the published +version into a scratch prefix and run it: + +``` +cd "$(mktemp -d)" && npm i @bitkyc08/opencodex@2.34.0 && npx ocx --version +``` + +The version the runtime reports is the evidence, not what the registry says it stored. + +## Release record + +Write the outcome to this unit: both channel versions, both release commits, both workflow +run ids, the docs deploy run id, and what was deliberately NOT included (#2745). Then the +unit is closable to `_fin`. + +## Acceptance + +- 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