Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 87 additions & 23 deletions devlog/_plan/260827_release_train/020_preview_release.md
Original file line number Diff line number Diff line change
@@ -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.
Comment on lines +44 to +46

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Correct the exact-SHA suite claim

The outcome identifies the preview release commit as 809a06ba0 but records the remote full suite at 62dfc6c54, so the suite did not run at the "exact sha" asserted here; the version bump means these are not even identical Git trees. Describe this as validation of the promoted pre-bump tree and rely explicitly on the push CI at 809a06ba0, or provide full-suite evidence from the actual release commit.

Useful? React with 👍 / 👎.


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.
50 changes: 50 additions & 0 deletions devlog/_plan/260827_release_train/030_main_promote.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
50 changes: 44 additions & 6 deletions devlog/_plan/260827_release_train/040_stable_release.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.
62 changes: 62 additions & 0 deletions devlog/_plan/260827_release_train/050_deploy_and_verify.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Close the completed release-train unit under _fin

This adds the terminal release record and documents every later phase as shipped, yet the entire unit remains under _plan; moreover, 010_preview_promote.md still lacks its required identified merge SHA, with 62dfc6c54 appearing only as an abbreviated suite target. Complete that missing acceptance record and move the unit to _fin, or document concrete remaining work instead of recording a terminal outcome in the open-plan area.

AGENTS.md reference: AGENTS.md:L83-L86

Useful? React with 👍 / 👎.


### 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
Comment on lines +84 to +86

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove the unfixed OAuth finding from tracked devlog

This publishes the identity-drift failure mode and its unmerged corrective PR while explicitly stating that the defect remains in both release channels; the nearby open-CodeQL assessment likewise belongs to ongoing security triage. Repository policy requires unreleased findings and severity assessments to stay in scratch space rather than tracked devlog, so remove these details until the fix or advisory is public.

AGENTS.md reference: AGENTS.md:L103-L112

Useful? React with 👍 / 👎.

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`.
Loading