From 6d15bd6014391c5c551b8fcb517cccec14fb1caf Mon Sep 17 00:00:00 2001 From: tangletools Date: Fri, 25 Sep 2026 21:22:25 -0600 Subject: [PATCH 1/5] build(release): prepare version and changelog after merge --- scripts/prepare-release.mjs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 scripts/prepare-release.mjs diff --git a/scripts/prepare-release.mjs b/scripts/prepare-release.mjs new file mode 100644 index 00000000..a8cf1951 --- /dev/null +++ b/scripts/prepare-release.mjs @@ -0,0 +1,16 @@ +#!/usr/bin/env node +import { execFileSync } from 'node:child_process' +import { readFileSync, writeFileSync } from 'node:fs' +const version=process.argv[2] +if(!version||!/^\d+\.\d+\.\d+$/.test(version)) throw new Error('usage: node scripts/prepare-release.mjs ') +const pkg=readFileSync('package.json','utf8') +const current=JSON.parse(pkg).version +if(current===version)throw new Error(`already ${version}`) +writeFileSync('package.json',pkg.replace(/^(\s*"version":\s*)"[^"]+"/m,(_m,p)=>`${p}"${version}"`)) +const git=(...a)=>execFileSync('git',a,{encoding:'utf8'}).trim() +const tag=git('describe','--tags','--abbrev=0','--match','v*') +const subjects=git('log','--no-merges','--format=%s',`${tag}..HEAD`).split('\n').filter(Boolean) +if(!subjects.length)throw new Error(`no commits since ${tag}`) +const old=readFileSync('CHANGELOG.md','utf8') +writeFileSync('CHANGELOG.md',`# Changelog\n\n## ${version}\n\n${subjects.map(s=>`- ${s}`).join('\n')}\n\n${old.replace(/^# Changelog\s*/,'')}`) +console.log(`Prepared ${version} from ${subjects.length} commits since ${tag}`) From 7ba4ebe97220c46b4570e0e34a879bd63b13cb43 Mon Sep 17 00:00:00 2001 From: tangletools Date: Fri, 25 Sep 2026 21:22:27 -0600 Subject: [PATCH 2/5] ci: keep release metadata out of feature PRs --- scripts/check-feature-release-files.mjs | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 scripts/check-feature-release-files.mjs diff --git a/scripts/check-feature-release-files.mjs b/scripts/check-feature-release-files.mjs new file mode 100644 index 00000000..e53eca0b --- /dev/null +++ b/scripts/check-feature-release-files.mjs @@ -0,0 +1,9 @@ +#!/usr/bin/env node +import { execFileSync } from 'node:child_process' +import { readFileSync } from 'node:fs' +const base=`origin/${process.env.GITHUB_BASE_REF||'main'}` +const baseVersion=JSON.parse(execFileSync('git',['show',`${base}:package.json`],{encoding:'utf8'})).version +const headVersion=JSON.parse(readFileSync('package.json','utf8')).version +const changed=execFileSync('git',['diff','--name-only',`${base}...HEAD`],{encoding:'utf8'}).trim().split('\n').filter(Boolean) +if(baseVersion!==headVersion||changed.includes('CHANGELOG.md'))throw new Error('feature PRs must not change package version or CHANGELOG.md; run Prepare Release after merge') +console.log(`release files clean at ${headVersion}`) From 310bd713f9fe89ed19721450d765133d14e225fd Mon Sep 17 00:00:00 2001 From: tangletools Date: Fri, 25 Sep 2026 21:22:34 -0600 Subject: [PATCH 3/5] ci: reject feature-time release prep --- .github/workflows/verify-pr.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/verify-pr.yml b/.github/workflows/verify-pr.yml index 5b59c700..f86f3cce 100644 --- a/.github/workflows/verify-pr.yml +++ b/.github/workflows/verify-pr.yml @@ -16,6 +16,9 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha }} persist-credentials: false + fetch-depth: 0 + - name: Keep release prep out of feature PRs + run: node scripts/check-feature-release-files.mjs - name: Retain exact source run: | mkdir -p verification From 5f00e5549dff20ee3594fdf8eec72dbe6a1ccd36 Mon Sep 17 00:00:00 2001 From: tangletools Date: Fri, 25 Sep 2026 21:22:36 -0600 Subject: [PATCH 4/5] ci(release): prepare release metadata after feature merges --- .github/workflows/prepare-release.yml | 52 +++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 .github/workflows/prepare-release.yml diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml new file mode 100644 index 00000000..1c95de6e --- /dev/null +++ b/.github/workflows/prepare-release.yml @@ -0,0 +1,52 @@ +name: Prepare Release +on: + workflow_dispatch: + inputs: + release_type: + description: Semver bump + required: true + type: choice + options: [patch, minor, major] + default: patch +permissions: + contents: write + pull-requests: write +concurrency: + group: prepare-release + cancel-in-progress: false +jobs: + prepare: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: main + fetch-depth: 0 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + - run: pnpm install --frozen-lockfile + - name: Prepare release metadata + id: release + env: + TYPE: ${{ inputs.release_type }} + run: | + current=$(node -p "require('./package.json').version") + version=$(CURRENT="$current" TYPE="$TYPE" node -e "const [a,b,c]=process.env.CURRENT.split('.').map(Number);const t=process.env.TYPE;console.log(t==='major'?(a+1)+'.0.0':t==='minor'?a+'.'+(b+1)+'.0':a+'.'+b+'.'+(c+1))") + node scripts/prepare-release.mjs "$version" + echo "version=$version" >> "$GITHUB_OUTPUT" + - name: Open release PR + env: + GH_TOKEN: ${{ github.token }} + VERSION: ${{ steps.release.outputs.version }} + run: | + branch="release/v$VERSION" + 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 CHANGELOG.md + git commit -m "chore(release): $VERSION" + git push origin "$branch" + gh pr create --base main --head "$branch" --title "chore(release): $VERSION" --body "Release-time preparation from merged main. Feature PRs do not carry version or changelog edits." From ad07b6233dafc638400989263b2715e955fb5f11 Mon Sep 17 00:00:00 2001 From: Drew Stone Date: Sun, 27 Sep 2026 21:55:50 -0600 Subject: [PATCH 5/5] fix(release): compare feature metadata from its branch point --- README.md | 1 + scripts/check-feature-release-files.mjs | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 326b9ec7..91af6666 100644 --- a/README.md +++ b/README.md @@ -455,6 +455,7 @@ The test suite packs the npm artifact and bundles `/worker`, `/specs`, and ## Prepare a release Feature PRs must leave the package version and `CHANGELOG.md` unchanged. +Feature checks compare against the common ancestor, so releases merged into `main` do not invalidate unchanged feature metadata. After features merge, run the **Prepare Release** workflow and choose a patch, minor, or major bump. The workflow uses Node and Git to open a `release/v` PR from current `main`. It does not install dependencies or publish a package. diff --git a/scripts/check-feature-release-files.mjs b/scripts/check-feature-release-files.mjs index 632cae2f..f4ce94ef 100644 --- a/scripts/check-feature-release-files.mjs +++ b/scripts/check-feature-release-files.mjs @@ -4,11 +4,12 @@ import { readFileSync } from 'node:fs' import { isDeepStrictEqual } from 'node:util' const baseRef = `origin/${process.env.GITHUB_BASE_REF || 'main'}` -const basePackage = JSON.parse(execFileSync('git', ['show', `${baseRef}:package.json`], { encoding: 'utf8' })) const headPackage = JSON.parse(readFileSync('package.json', 'utf8')) const changed = execFileSync('git', ['diff', '--name-only', `${baseRef}...HEAD`], { encoding: 'utf8' }).trim().split('\n').filter(Boolean) const headBranch = process.env.GITHUB_HEAD_REF || execFileSync('git', ['branch', '--show-current'], { encoding: 'utf8' }).trim() const releaseBranch = /^release\/v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.exec(headBranch) +const comparisonRef = releaseBranch ? baseRef : execFileSync('git', ['merge-base', baseRef, 'HEAD'], { encoding: 'utf8' }).trim() +const basePackage = JSON.parse(execFileSync('git', ['show', `${comparisonRef}:package.json`], { encoding: 'utf8' })) if (!releaseBranch) { if (basePackage.version !== headPackage.version || changed.includes('CHANGELOG.md')) {