-
Notifications
You must be signed in to change notification settings - Fork 960
fix(release): repair dev version line and open future bumps as PRs #3013
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
cfe881f
71fdea2
8db0c79
afc2474
481e599
b2129b3
0c897d5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,170 @@ | ||
| name: Dev version bump | ||
|
|
||
| # When a release publishes, open a pull request that moves `dev` past the published | ||
| # version. Without this, `dev` keeps carrying a version that is at or behind a released | ||
| # one, and `tests/release-version-line.test.ts` fails on `dev` and on every pull request | ||
| # opened against it - inherited red a contributor cannot fix from their own diff. | ||
| # | ||
| # That has been repaired by hand four times: 32529c2b2, e4a85d134, 076ad3036, befcac3e1. | ||
| # The second of those ADDED the detector and two more repairs followed it, so more | ||
| # visibility was never the missing piece; a prepared change was. | ||
| # | ||
| # WHAT THIS DOES NOT DO. It does not push to `dev`. It opens a pull request and a human | ||
| # merges it, because ruleset `Protect dev` requires an approving review and code-owner | ||
| # sign-off that a bot cannot supply. Until that merge the red persists. This converts a | ||
| # forgotten chore into a queued, reviewable change - not into an automatic repair. | ||
| # | ||
| # A `release` event resolves this workflow file from the repository DEFAULT branch | ||
| # (`main`), not from `dev` - the same trap documented in cleanup-closed-pr-branches.yml. | ||
| # So merging this file to `dev` installs it but arms nothing; it first fires after an | ||
| # ordinary dev -> main promotion carries it there. | ||
| # | ||
| # There is deliberately no `workflow_dispatch`: a branch-selected manual run executes | ||
| # THAT branch body with `contents: write`. Re-drive a missed run by running | ||
| # `bun scripts/bump-dev-version.ts <released> package.json` locally and opening the pull | ||
| # request normally. | ||
| on: | ||
| release: | ||
| types: [published] | ||
|
|
||
| permissions: {} | ||
|
|
||
| concurrency: | ||
| group: dev-version-bump | ||
| cancel-in-progress: false | ||
|
|
||
| jobs: | ||
| open-bump-pr: | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| # Push the new codex/dev-version-* branch. Ruleset `Protect dev` covers only | ||
| # refs/heads/dev, so the bump branch is unprotected and this token cannot | ||
| # bypass dev review. It is the ruleset that keeps this job off dev, not the | ||
| # permission name. | ||
| contents: write | ||
| # Open the pull request. | ||
| pull-requests: write | ||
| steps: | ||
| - name: Checkout dev | ||
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 | ||
| with: | ||
| ref: dev | ||
| # Tags are load-bearing, not decoration: the freeness gate below is a bun | ||
| # test that reads the local tag set, and release-version-line.test.ts | ||
| # returns EARLY on an empty set. A shallow checkout would make that gate | ||
| # silently vacuous instead of failing loudly. | ||
| fetch-depth: 0 | ||
| # Do NOT set persist-credentials: false here as the read-only workflows do. | ||
| # This job has to push its bump branch. | ||
|
|
||
| # The repository-owned composite action, not a hand-pinned setup-bun SHA: it | ||
| # resolves the Bun version from package.json so the runtime SOT stays in one | ||
| # place. An independently pinned action here would drift from every other job. | ||
| - name: Setup project Bun | ||
| uses: ./.github/actions/setup-project-bun | ||
|
|
||
| - name: Install dependencies | ||
| run: bun install --frozen-lockfile | ||
|
|
||
| - name: Decide the version dev should carry | ||
| id: decide | ||
| env: | ||
| RELEASED_VERSION: ${{ github.event.release.tag_name }} | ||
| run: | | ||
| set -euo pipefail | ||
| bun scripts/bump-dev-version.ts "${RELEASED_VERSION}" package.json | ||
|
|
||
| - name: Prove the chosen version is unused | ||
| if: ${{ steps.decide.outputs.changed == 'true' }} | ||
| # The script decides the candidate from the released version SHAPE, which is all | ||
| # a pure function can see. Whether that candidate is actually FREE is a property | ||
| # of the tag set, so it is settled here by the detector that already owns the | ||
| # question. If this fails, no pull request is opened and the job goes red asking | ||
| # for a human decision - which is the correct outcome, not a fallback. | ||
| run: bun test tests/release-version-line.test.ts | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- repository conventions and learnings ---'
find /tmp/coderabbit-repo-knowledge/lidge-jun-opencodex-7afea732 -maxdepth 2 -type f -name '*.md' -print
for f in /tmp/coderabbit-repo-knowledge/lidge-jun-opencodex-7afea732/*/*.md; do
case "$f" in
*/learnings/*|*/architecture/*|*/conventions/*)
printf '\n--- %s ---\n' "$f"
cat "$f"
;;
esac
done
printf '%s\n' '--- workflow ---'
cat -n .github/workflows/dev-version-bump.yml
printf '%s\n' '--- test and directly bound release helpers ---'
rg -n -C 8 'release-version-line|NEXT_VERSION|npm|registry|bump-dev-version|pr create|git tag' tests .github scripts package.jsonRepository: lidge-jun/opencodex Length of output: 50376 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- workflow ---'
cat -n .github/workflows/dev-version-bump.yml
printf '%s\n' '--- release-version-line test ---'
if [ -f tests/release-version-line.test.ts ]; then
cat -n tests/release-version-line.test.ts
else
rg -n -l 'release.version.line|release-version-line|version line' tests | head -20
fi
printf '%s\n' '--- bump script ---'
cat -n scripts/bump-dev-version.ts
printf '%s\n' '--- package metadata ---'
rg -n -C 3 '"name"|"version"|"publishConfig"|npm' package.jsonRepository: lidge-jun/opencodex Length of output: 24726 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- release workflow and publish path ---'
rg -n -C 12 'npm publish|publish|tag_name|release|package.json|version' .github/workflows/release.yml .github/workflows 2>/dev/null | head -240
printf '%s\n' '--- release scripts and package scripts ---'
rg -n -C 10 'npm publish|npm view|registry|publish|assertChannelVersionMovesForward|packageJson|version' scripts package.json
printf '%s\n' '--- repository convention scope ---'
find /tmp/coderabbit-repo-knowledge/lidge-jun-opencodex-7afea732 -maxdepth 2 -type f -path '*/conventions/*' -print -exec cat {} \;Repository: lidge-jun/opencodex Length of output: 50376 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- release publish section ---'
sed -n '240,380p' .github/workflows/release.yml
printf '%s\n' '--- workflow references to registry/version checks ---'
rg -n -C 5 'registry\.npmjs|npm (view|publish|pack)|RELEASE_VERSION|package\.json' .github/workflows/release.yml scripts/release.ts scriptsRepository: lidge-jun/opencodex Length of output: 43408 🌐 Web query:
💡 Result: The npm registry enforces strict immutability for published packages [1][2][3]. Once a specific package name and version combination has been published, it cannot be overwritten, modified, or republished, even if the version is subsequently unpublished by the maintainer [4][5][6]. When you attempt to publish a package version that already exists in the registry, the npm registry will reject the request [5][6]. This rejection typically manifests as a 403 Forbidden or 409 Conflict error, depending on the context of the operation [1][2][3]. 1. 403 Forbidden: This error often occurs when the registry recognizes your authentication but refuses the action because the version already exists [1][3]. The error message frequently explicitly states: "You cannot publish over the previously published versions" [1][7][8]. 2. 409 Conflict: This error commonly occurs in automated environments, such as CI/CD pipelines, when multiple concurrent jobs attempt to publish the same version simultaneously, or when a re-run of a pipeline tries to publish a version that was successfully published in a previous execution [2]. To resolve these errors, you must always increment the version number in your package.json before attempting to publish again [1][2][3]. It is considered best practice to configure your release workflows to be idempotent, ensuring that they check if a version already exists before attempting a publish, or by ensuring that every release is serialized and always uses a new version number [2]. Historically, the npm client supported an --force (-f) flag to overwrite versions, but this was permanently disabled to maintain package integrity and prevent security issues [4]. Once a version is published, that unique identifier is permanently reserved for that specific package content [4][5]. Citations:
Check npm registry availability before opening the pull request.
🤖 Prompt for AI Agents |
||
|
|
||
| - name: Open the bump pull request | ||
| if: ${{ steps.decide.outputs.changed == 'true' }} | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| NEXT_VERSION: ${{ steps.decide.outputs.version }} | ||
| RELEASED_VERSION: ${{ github.event.release.tag_name }} | ||
| run: | | ||
| set -euo pipefail | ||
|
|
||
| branch="codex/dev-version-${NEXT_VERSION}" | ||
|
|
||
| # Idempotent: a second publish, a re-run, or a manual repair must not turn a | ||
| # successful release into a red job. | ||
| # | ||
| # Check the PULL REQUEST as well as the branch, not just the branch. A security | ||
| # review caught that: an open bump pull request whose head branch was deleted | ||
| # leaves the branch check passing, so the job would recreate the branch and then | ||
| # fail on `gh pr create` with "already exists" — turning a successful release red | ||
| # for a repair that was already queued. | ||
| open_prs="$(gh pr list --base dev --head "${branch}" --state open --json number --jq 'length')" | ||
| if [ "${open_prs}" != "0" ]; then | ||
| echo "::notice::a bump pull request for ${branch} is already open; nothing to do" | ||
| exit 0 | ||
| fi | ||
|
Comment on lines
+105
to
+109
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- repository knowledge scopes ---'
find /tmp/coderabbit-repo-knowledge/lidge-jun-opencodex-7afea732 -maxdepth 2 -type f -print
printf '%s\n' '--- first lines of available convention files ---'
for f in /tmp/coderabbit-repo-knowledge/lidge-jun-opencodex-7afea732/*/*.md; do
[ -f "$f" ] && { echo "### $f"; head -40 "$f"; }
done
printf '%s\n' '--- workflow outline/context ---'
wc -l .github/workflows/dev-version-bump.yml
sed -n '1,145p' .github/workflows/dev-version-bump.ymlRepository: lidge-jun/opencodex Length of output: 29061 🏁 Script executed: #!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
p = Path(".github/workflows/dev-version-bump.yml")
lines = p.read_text().splitlines()
for i, line in enumerate(lines, 1):
if 25 <= i <= 151 and (
"concurrency:" in line
or "group:" in line
or "cancel-in-progress:" in line
or "open-bump-pr:" in line
or "permissions:" in line
or "timeout-minutes" in line
or "Install dependencies" in line
or "gh pr list" in line
or "gh pr create" in line
):
print(f"{i}: {line}")
PYRepository: lidge-jun/opencodex Length of output: 574 🌐 Web query:
💡 Result: In GitHub Actions, job timeouts and concurrency settings manage how workflows execute and resource utilization. Job Timeout (timeout-minutes) The timeout-minutes keyword allows you to set a maximum duration (in minutes) for a job to run. If the job exceeds this limit, GitHub Actions terminates the job and marks it as failed [1][2]. - Default Limits: For all GitHub-hosted runners, the default execution time limit is 6 hours per job [2]. Some specialized runners, such as single-CPU runners, may have shorter default timeouts (e.g., 15 minutes) [3]. Self-hosted runners have a maximum execution time limit of 5 days per job [2]. Concurrency and cancel-in-progress The concurrency keyword ensures that only a single job or workflow run with the same concurrency group identifier runs at a time [4]. - cancel-in-progress: By default, if a new job or workflow is queued while another one in the same concurrency group is pending, the pending one is canceled to allow the new one to proceed [4]. Setting cancel-in-progress: true extends this behavior to also cancel any currently running job or workflow in that same group [4]. - Important Note: When using concurrency with reusable workflows, be cautious not to use the same concurrency group identifier in both the caller and the called workflow, as this can lead to unintended cancellation of the caller workflow [5]. Workflow Cancellation Behavior When a workflow or job is canceled, GitHub Actions re-evaluates the if conditions for running jobs and steps [6]. It sends cancellation signals (SIGINT/SIGTERM) to the processes on the runner [6]. If the processes do not terminate within a 5-minute cancellation window, the server forcibly terminates the jobs and steps [6]. Citations:
Bound the Because 🤖 Prompt for AI AgentsSource: Path instructions |
||
|
|
||
| # An existing branch is NOT terminal. If a previous run pushed the branch and then | ||
| # failed at `gh pr create`, exiting here would leave the repair permanently unqueued | ||
| # while every rerun reports success - the exact failure mode a reviewer caught. So | ||
| # reuse the branch and fall through to pull-request creation instead. | ||
| if git ls-remote --exit-code --heads origin "${branch}" >/dev/null 2>&1; then | ||
| echo "::notice::${branch} exists without an open pull request; validating it" | ||
| git fetch origin "${branch}" | ||
|
|
||
| # Fail closed on unexpected content. The branch carries the bot's own one-line | ||
| # bump, so anything else on it means a human or another job is using that name and | ||
| # this job must not push to it or open a pull request from it. | ||
| changed_files="$(git diff --name-only "origin/dev...origin/${branch}")" | ||
| if [ "${changed_files}" != "package.json" ]; then | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win Validate the complete generated manifest before reusing the branch.
Compare Proposed fix- branch_version="$(git show "origin/${branch}:package.json" | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).version")"
- if [ "${branch_version}" != "${NEXT_VERSION}" ]; then
- echo "::error::${branch} carries ${branch_version}, expected ${NEXT_VERSION}"
+ if ! git show "origin/${branch}:package.json" | cmp -s - package.json; then
+ echo "::error::${branch} does not match the generated package.json"
exit 1
fiAlso applies to: 127-128 🤖 Prompt for AI Agents |
||
| echo "::error::${branch} touches unexpected files: ${changed_files:-<none>}" | ||
| exit 1 | ||
| fi | ||
| branch_version="$(git show "origin/${branch}:package.json" | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).version")" | ||
| if [ "${branch_version}" != "${NEXT_VERSION}" ]; then | ||
| echo "::error::${branch} carries ${branch_version}, expected ${NEXT_VERSION}" | ||
| exit 1 | ||
| fi | ||
| git checkout -B "${branch}" "origin/${branch}" | ||
| else | ||
| git config user.name "github-actions[bot]" | ||
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | ||
| git checkout -b "${branch}" | ||
| git add package.json | ||
| git commit -m "fix(release): move dev to ${NEXT_VERSION} after ${RELEASED_VERSION}" | ||
| git push origin "${branch}" | ||
| fi | ||
|
|
||
| gh pr create \ | ||
| --base dev \ | ||
| --head "${branch}" \ | ||
| --title "fix(release): move dev to ${NEXT_VERSION} after ${RELEASED_VERSION}" \ | ||
| --body "$(cat <<BODY | ||
| ## Summary | ||
|
|
||
| \`${RELEASED_VERSION}\` published, so \`dev\` would otherwise keep a version at or | ||
| behind a released one and \`tests/release-version-line.test.ts\` would fail on | ||
| \`dev\` and on every pull request opened against it. This moves \`dev\` to | ||
| \`${NEXT_VERSION}\`. | ||
|
|
||
| Opened automatically by \`.github/workflows/dev-version-bump.yml\`. The same | ||
| repair was previously done by hand in 32529c2b2, e4a85d134, 076ad3036, and | ||
| befcac3e1. | ||
|
|
||
| ## Verification | ||
|
|
||
| \`bun test tests/release-version-line.test.ts\` ran against this exact tree | ||
| before the pull request was opened; the workflow refuses to open one if the | ||
| chosen version collides with a published release. | ||
|
|
||
| ## Checklist | ||
|
|
||
| - [x] Scope stays focused and avoids unrelated cleanup. | ||
| - [x] Docs or release notes were updated when needed. | ||
| - [x] Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults. | ||
| BODY | ||
| )" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -73,6 +73,21 @@ when a maintainer steps down. | |
| - Direct pushes are reserved for maintainer-owned integration work, urgent repairs, or incident | ||
| recovery. The same CI and documentation requirements still apply. | ||
| - Promotion from `dev` to `main` and npm releases is maintainer-controlled. | ||
| - **Closing out a release includes moving `dev`'s version line forward.** A published | ||
| release leaves `dev` carrying a version at or behind it, and | ||
| `tests/release-version-line.test.ts` then fails on `dev` and on every pull request | ||
| opened against it — red that contributors inherit and cannot fix from their own diff. | ||
| This was repaired by hand four times (`32529c2b2`, `e4a85d134`, `076ad3036`, | ||
| `befcac3e1`) before it was automated. | ||
|
|
||
| `.github/workflows/dev-version-bump.yml` now opens that bump as a pull request when a | ||
| release publishes. Merging it is part of closing the release; a bot cannot, because | ||
| `Protect dev` requires an approving review and code-owner sign-off. Two caveats worth | ||
| knowing: the workflow runs from the DEFAULT branch, so it only fires once it has been | ||
| promoted to `main`; and a pull request opened with `GITHUB_TOKEN` does not start | ||
| `pull_request` workflows, so the bump pull request arrives without CI. To re-drive a | ||
|
Comment on lines
+87
to
+88
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -eu
echo '--- repository knowledge files ---'
find /tmp/coderabbit-repo-knowledge/lidge-jun-opencodex-7afea732 \
-maxdepth 2 -type f -print | sort
echo '--- convention file headers ---'
for f in /tmp/coderabbit-repo-knowledge/lidge-jun-opencodex-7afea732/*/*.md; do
[ -f "$f" ] || continue
printf '\n### %s\n' "$f"
head -20 "$f"
done
echo '--- target lines ---'
cat -n MAINTAINERS.md | sed -n '76,94p'
echo '--- workflow files ---'
find .github -maxdepth 3 -type f -print 2>/dev/null | sort || true
echo '--- pull_request trigger declarations ---'
rg -n -U -g '*.yml' -g '*.yaml' '(^|\n)\s*on:\s*(\n|.){0,500}pull_request' .github 2>/dev/null || trueRepository: lidge-jun/opencodex Length of output: 27711 🏁 Script executed: #!/bin/bash
set -eu
echo '--- .github/AGENTS.md ---'
cat -n .github/AGENTS.md
echo '--- pull_request workflow headers ---'
for f in \
.github/workflows/ci.yml \
.github/workflows/issue-quality-tests.yml \
.github/workflows/react-doctor.yml \
.github/workflows/service-lifecycle.yml \
.github/workflows/dev-version-bump.yml
do
echo
echo "### $f"
cat -n "$f" | sed -n '1,55p'
doneRepository: lidge-jun/opencodex Length of output: 14839 🌐 Web query:
💡 Result: When a workflow uses the GITHUB_TOKEN to create or update a pull request, the resulting pull_request event (specifically with the opened, synchronize, or reopened activity types) triggers workflow runs that are placed in an approval-required state [1][2][3]. This behavior is a security feature designed to prevent recursive workflow runs while still allowing automation (like CI workflows) to execute on pull requests created or updated by other workflows [2][3]. Key details regarding this behavior: - Approval Mechanism: The pull request will display a banner in the merge box [2][3]. A user with write access to the repository must manually select Approve workflows to run before the workflow can proceed [2][3]. - Scope: This applies specifically to the pull_request event types opened, synchronize, and reopened [1][2]. Other pull_request activity types (such as labeled, edited, or closed) do not trigger new workflow runs when performed by the GITHUB_TOKEN [2][3]. - Alternatives: If you need workflows triggered by automation-created pull requests to run without manual approval, you can use a GitHub App installation access token or a personal access token instead of the GITHUB_TOKEN to perform the pull request creation or update actions [2][3]. For standard, manual pull request activity (by a human), these approval requirements do not apply; they are specific to events triggered by the GITHUB_TOKEN itself [2][3]. Citations:
🏁 Script executed: #!/bin/bash
set -eu
cat -n .github/workflows/dev-version-bump.yml | sed -n '47,115p'Repository: lidge-jun/opencodex Length of output: 3930 Correct the
🤖 Prompt for AI Agents |
||
| missed run by hand: `bun scripts/bump-dev-version.ts <released-version> package.json`, | ||
| then open the pull request normally. | ||
|
|
||
| ## The retired `dev2-go` line | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
| @@ -0,0 +1,134 @@ | ||||
| # dev version line: stop repairing it by hand | ||||
|
|
||||
| Unit: `devlog/_plan/260830_dev_version_line_bump_pr/` | ||||
|
|
||||
| Named for what it ships: a version-bump PULL REQUEST opened when a release publishes. | ||||
| The unit was briefly called `..._autobump`, which an audit correctly rejected — the | ||||
| workflow prepares the change and a human merges it, so nothing is automatic end to | ||||
| end. | ||||
| Goalplan: `repair-the-dev-version-line-and-add-a-post-relea` | ||||
|
|
||||
| ## The symptom, today | ||||
|
|
||||
| `dev` head `df8b3882f` carries `package.json` version `2.36.0`. Tag `v2.36.0` | ||||
| names `c7d8407d2`, which is `origin/main`. So the tree claims a version that is | ||||
| already published from a different commit, and | ||||
| `tests/release-version-line.test.ts` reports exactly that: | ||||
|
|
||||
| ``` | ||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Add a language tag to the fenced output block.
Proposed fix-```
+```text📝 Committable suggestion
Suggested change
🧰 Tools🪛 markdownlint-cli2 (0.23.2)[warning] 18-18: Fenced code blocks should have a language specified (MD040, fenced-code-language) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||||
| (fail) release version line > the in-tree version is never behind a released one | ||||
| error: package.json version 2.36.0 equals release tag v2.36.0, but this commit is | ||||
| not the one that tag names. The tree claims an already-published version: | ||||
| publishing is refused as a duplicate. Bump package.json. | ||||
| ``` | ||||
|
|
||||
| This fails CI jobs `test 2/4` and `macos` on `dev` itself (run 33312566315, cut at | ||||
| `c2778ca3a` — `dev` has since advanced to `df8b3882f` and the failure still | ||||
| reproduces there) and therefore on every PR opened against it. PR #3007 inherited | ||||
| the same two red jobs for a two-file GUI change, and branch protection refused the | ||||
| merge until it was overridden. | ||||
|
|
||||
| ## Why a one-line bump is not the fix | ||||
|
|
||||
| The same defect has been repaired by hand FOUR times: | ||||
|
|
||||
| | commit | what it did | | ||||
| |---|---| | ||||
| | `32529c2b2` | `2.24.2` -> `2.27.0`, after dev trailed the published channel by two releases | | ||||
| | `e4a85d134` | `2.32.1-preview.20260825` -> `2.34.0`; also ADDED `release-version-line.test.ts` | | ||||
| | `076ad3036` | `2.34.0` -> `2.35.0`, right after v2.34.0 shipped | | ||||
| | `befcac3e1` | `2.35.0` -> `2.36.0`, after v2.36.0-preview.20260829 shipped | | ||||
|
|
||||
| Note the second row: the detector was added DURING this sequence, and two more | ||||
| hand-repairs followed it. Visibility was never the missing piece — that is the | ||||
| finding that decided the design in `020`. | ||||
|
|
||||
| Four repairs of one cause is a missing actor, not four accidents. The cause is | ||||
| structural and visible in `scripts/release.ts`: the release runs only on `main` or | ||||
| `preview` (`allowedBranches = ["main", "preview"]`, line 496), bumps | ||||
| `package.json` there, commits `release: v<version>`, pushes THAT branch, and | ||||
| dispatches `release.yml`. The workflow ends at "Create GitHub release" — tag plus | ||||
| GitHub release, nothing more. No step in either file ever advances `dev`. The | ||||
| workflow declares `permissions: {}` at the top (line 32) and grants each job only | ||||
| `contents: read` or `contents: write`, which is what makes an added `dev` write | ||||
| there a security-review problem rather than a convenience. | ||||
|
|
||||
| So the version line on `dev` goes stale the moment a release publishes, and stays | ||||
| stale until a human notices red CI on an unrelated PR. The cost lands on | ||||
| contributors: inherited red they did not cause and cannot fix from their own diff. | ||||
|
|
||||
| What this unit can and cannot promise: it moves the repair from "someone eventually | ||||
| remembers" to "a reviewable PR is waiting." It does not make the red impossible, | ||||
| because the bump still needs a human merge — `Protect dev` requires an approving | ||||
| review and code-owner sign-off, which a bot cannot supply. Claiming more than that | ||||
| was the defect an audit caught in the first two drafts of `020`. | ||||
|
|
||||
| ## Constraint that shapes the design | ||||
|
|
||||
| `dev` carries the NEXT STABLE version; the preview train adds its own suffix at | ||||
| release time. That is the precedent `befcac3e1` states explicitly and the three | ||||
| earlier repairs followed. A mechanism must preserve it — bumping dev to a preview | ||||
| string would contradict every prior repair. | ||||
|
|
||||
| The existing test is already the right detector. It reads the local tag set, needs | ||||
| no network, and distinguishes "equal on the release commit" (legal) from "equal | ||||
| anywhere else" (duplicate). Nothing about the detector needs changing. What is | ||||
| missing is anything that PREPARES the repair: today the detector reports the problem | ||||
| to whoever happens to open the next PR, and the fix is left to memory. | ||||
|
|
||||
| ## Phase map | ||||
|
|
||||
| Each decade doc below is one full PABCD cycle. Dependency-ordered: the version | ||||
| repair lands first because it unblocks CI for everything else, then the actor that | ||||
| prepares the next repair as a reviewable PR, then the ship. | ||||
|
|
||||
| - `010_version_repair.md` — move `dev` off the consumed `2.36.0` (wp2). | ||||
| - `020_post_release_bump.md` — open the dev bump as a PR when a release publishes (wp3). | ||||
| Note: that workflow only runs once it reaches `main`, the default branch. Merging it | ||||
| to `dev` does not activate it. | ||||
| - `030_ship.md` — PR against `dev`, CI evidence, merge (wp4). | ||||
|
|
||||
| ## Audit record | ||||
|
|
||||
| TWO drafts of this roadmap were FAILED by an independent reviewer, and both verdicts | ||||
| changed the design rather than the wording. | ||||
|
|
||||
| Round 1: `020` chose a printed notice inside the release script and called it an | ||||
| autobump. The reviewer showed the existing test is already louder than any printout, | ||||
| and that two hand-repairs happened AFTER it landed. It also caught a wrong | ||||
| "highest tag" claim in `010` and a test plan citing a `--dry-run` flag and reusable | ||||
| shim helpers that do not exist. | ||||
|
|
||||
| Round 2: the replacement PR-workflow design could not have worked. A `release` event | ||||
| runs the workflow from the DEFAULT branch (`main`), which the scope forbade touching; | ||||
| the named comparator `compareReleaseVersions` sits behind a module-scope | ||||
| `process.exit` in `scripts/release.ts` and cannot be imported; and the "+minor" bump | ||||
| rule contradicted `befcac3e1`, which moved `dev` to `2.36.0` on a | ||||
| `v2.36.0-preview.*` publish. All three are fixed in the third draft, which imports | ||||
| `compareReleaseTags` from `scripts/release-notes.ts` instead, records the `main` | ||||
| promotion as a named follow-up in `030`, and replaces "+minor" with the two-branch | ||||
| rule in `020`. The unit was also renamed. | ||||
|
|
||||
| Round 3 caught the sequel to that last fix: "lowest unused stable" is not a pure | ||||
| function of the script's two inputs, because "unused" is a property of the tag set and | ||||
| the registry. The rule is now split — shape arithmetic in the script, freeness in the | ||||
| tag-aware detector that already exists. It also caught that the out-of-scope list | ||||
| below forbade the very promotion `030` depends on. | ||||
|
|
||||
| Every rejected option and its reason stay in `020` so the decision is auditable. | ||||
|
|
||||
| ## Out of scope | ||||
|
|
||||
| No publish, tag, or Release dispatch. No `main`/`preview` change IN THIS UNIT. No | ||||
| merge of `main` back into `dev` to "sync" the version: `010_wp2_version_line.md` | ||||
| names that as the trap that lands the consumed string on top of newer commits. | ||||
|
|
||||
| That `main` exclusion is a scope boundary, not a claim that `main` is irrelevant. The | ||||
| workflow in `020` cannot run until an ordinary maintainer-controlled promotion carries | ||||
| it to the default branch; `030` records that as the named follow-up. Two consequences | ||||
| worth stating plainly: | ||||
|
|
||||
| - Merging this unit into `dev` fixes the red CI immediately (that is `010`) but arms | ||||
| nothing (that is `020`, dormant until promotion). | ||||
| - The next release cut from the CURRENT `main` will still strand `dev` one last time. | ||||
| The loop closes on the release AFTER the workflow reaches `main`. | ||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| # 010 — move dev off the consumed 2.36.0 (wp2) | ||
|
|
||
| One line. `package.json` `version`: `2.36.0` -> `2.37.0`. | ||
|
|
||
| ## Why 2.37.0 | ||
|
|
||
| Verified against the real state, not read off a pattern: | ||
|
|
||
| | candidate | verdict | | ||
| |---|---| | ||
| | `2.36.0` (current) | tag `v2.36.0` names `c7d8407d2`, not dev's head; npm `latest` = 2.36.0. Consumed. | | ||
| | `2.36.1` | mechanically legal but labels the range a patch, against the `befcac3e1` precedent | | ||
| | `2.36.1-preview.*` | contradicts "dev carries the next STABLE version" | | ||
| | `2.37.0` | `npm view @bitkyc08/opencodex@2.37.0` -> E404; no `v2.37.0` in the tag set; forward of every tag | | ||
|
|
||
| Highest existing tag by the repository's own ordering is `v2.36.0` — NOT the | ||
| later-dated `v2.36.0-preview.20260830`. Sorting all 218 `v*` tags with | ||
| `compareReleaseTags` puts the stable release above its own prerelease, which is | ||
| correct SemVer precedence and the reason the failing message names `v2.36.0`: | ||
|
|
||
| ``` | ||
| top 5: v2.34.0 v2.35.0 v2.36.0-preview.20260829 v2.36.0-preview.20260830 v2.36.0 | ||
| HIGHEST = v2.36.0 | ||
| compareReleaseTags("v2.37.0", "v2.36.0") -> 1 | ||
| ``` | ||
|
|
||
| The first draft of this doc asserted the preview was highest while claiming to have | ||
| run the comparator. It had not. Run it. | ||
|
|
||
| npm dist-tags at the time of writing: `latest` = 2.36.0, `preview` = | ||
| 2.36.0-preview.20260830. | ||
|
|
||
| ## The diff | ||
|
|
||
| ```json | ||
| - "version": "2.36.0", | ||
| + "version": "2.37.0", | ||
| ``` | ||
|
|
||
| No other file carries the product version. `gui/package.json` is `0.0.0`, | ||
| `docs-site/package.json` is `0.0.1`, and `src/generated/*` hold catalog hashes. | ||
| Re-verify with a repo-wide search excluding `node_modules`, `.tmp`, `devlog`, | ||
| `gui/dist` before claiming the line is unique. | ||
|
|
||
| ## Verification | ||
|
|
||
| - `bun test tests/release-version-line.test.ts` — all three tests pass, including | ||
| "the in-tree version is never behind a released one" which currently fails. | ||
| - Re-run the freeness checks (`npm view`, `git tag --list`) immediately before | ||
| committing: another release landing mid-cycle would consume the candidate. | ||
|
|
||
| ## What this does not do | ||
|
|
||
| It does not publish, tag, or promote, and it does not stop the next release from | ||
| stranding dev again. That is `020`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: lidge-jun/opencodex
Length of output: 9075
🏁 Script executed:
Repository: lidge-jun/opencodex
Length of output: 394
🏁 Script executed:
Repository: lidge-jun/opencodex
Length of output: 14734
🏁 Script executed:
Repository: lidge-jun/opencodex
Length of output: 7179
Sensitive Data Exposure (CWE-522): Insufficiently Protected Credentials
Reachability: Internal · Exploitability: Difficult
Disable checkout credential persistence before running repository code.
actions/checkoutpersistsGITHUB_TOKENin Git configuration. The workflow then runs code fromdevbefore the final push step. A compromiseddevrevision could read the token and use its write permissions to create branches or pull requests.Set
persist-credentials: false. Configure Git authentication only in the final step, before its authenticatedgitcommands. The setup action, bump script, test, and lockfile do not require authenticated Git access earlier.🧰 Tools
🪛 zizmor (1.29.0)
[warning] 48-62: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false
(artipacked)
🤖 Prompt for AI Agents
Sources: Path instructions, Linters/SAST tools