ReviewOS

also looking at this

craft-native/craft

fix: validate complete workspace release before tagging

#365
Merged chris wants to merge codex/native-sdk-workspace-release into main
5 files +73 -33

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

bumpx.config.tsadded+3-0
Changes to bumpx.config.ts
@@ -0,0 +1,3 @@
1export default {
2 additionalFiles: ['packages/zig/build.zig.zon', 'packages/zig/pantry.json'],
3}
package.jsonmodified+2-2
Changes to package.json
@@ -61,8 +61,8 @@
6161 "changelog:generate": "bun scripts/generate-release-changelog.ts",
6262 "test:release": "bun test ./scripts/changelog-preview.test.ts ./scripts/release-workflow.test.ts ./scripts/release-manifest.test.ts ./scripts/verify-sbom.test.ts",
6363 "release": "bun scripts/release-version.ts --preflight && bun x --bun @stacksjs/bumpx prompt --recursive",
64 "release:patch": "bun scripts/release-version.ts --preflight && bun x --bun @stacksjs/bumpx patch --recursive --files package.json,packages/zig/build.zig.zon,packages/zig/pantry.json --commit --tag --push --yes --no-changelog --execute \"bun scripts/generate-release-changelog.ts && git add CHANGELOG.md\"",
65 "release:version": "bun scripts/release-version.ts --preflight && bun x --bun @stacksjs/bumpx --recursive --files package.json,packages/zig/build.zig.zon,packages/zig/pantry.json --commit --tag --push --yes --no-changelog --execute \"bun scripts/generate-release-changelog.ts && git add CHANGELOG.md\""
64 "release:patch": "bun scripts/release-version.ts --preflight && bun x --bun @stacksjs/bumpx patch --recursive --commit --tag --push --yes --no-changelog --execute \"bun scripts/release-version.ts --pending && bun scripts/generate-release-changelog.ts && git add CHANGELOG.md\"",
65 "release:version": "bun scripts/release-version.ts --preflight && bun x --bun @stacksjs/bumpx --recursive --commit --tag --push --yes --no-changelog --execute \"bun scripts/release-version.ts --pending && bun scripts/generate-release-changelog.ts && git add CHANGELOG.md\""
6666 },
6767 "devDependencies": {
6868 "@stacksjs/buddy": "0.72.56",
scripts/changelog-preview.test.tsmodified+34-25
Changes to scripts/changelog-preview.test.ts
@@ -128,48 +128,57 @@ test('patch release uses the guarded changelog generator before committing', ()
128128 expect(project.scripts['changelog:generate']).toBe('bun scripts/generate-release-changelog.ts')
129129 expect(command).toContain('--no-changelog')
130130 expect(command).toContain('bun scripts/generate-release-changelog.ts')
131 expect(command).toContain('--files package.json,packages/zig/build.zig.zon,packages/zig/pantry.json')
131 expect(command).not.toContain('--files')
132 expect(command).toContain('bun scripts/release-version.ts --pending && bun scripts/generate-release-changelog.ts')
132133})
133134
134test('an explicit version commits and tags the guarded changelog without its duplicate generator', () => {
135 fixture((directory, git) => {
136 mkdirSync(join(directory, 'packages/zig'), { recursive: true })
137 writeFileSync(join(directory, 'packages/zig/build.zig.zon'), '.{ .version = "1.0.1", }')
138 writeFileSync(join(directory, 'packages/zig/pantry.json'), '{"version":"1.0.1"}')
139 git('add', 'packages')
140 git('commit', '-qm', 'chore: register native release metadata')
141 git('commit', '--allow-empty', '-qm', 'Merge pull request #326 from example/feature')
135function nativeWorkspace(directory: string, git: (...args: string[]) => string): string[] {
136 mkdirSync(join(directory, 'packages/zig'), { recursive: true })
137 mkdirSync(join(directory, 'packages/sdk'), { recursive: true })
138 writeFileSync(join(directory, 'package.json'), '{"name":"fixture","version":"1.0.1","workspaces":["packages/*"]}')
139 writeFileSync(join(directory, 'packages/sdk/package.json'), '{"name":"fixture-sdk","version":"1.0.0"}')
140 writeFileSync(join(directory, 'packages/zig/build.zig.zon'), '.{ .version = "1.0.0", }')
141 writeFileSync(join(directory, 'packages/zig/pantry.json'), '{"version":"1.0.0"}')
142 // This is the real repository's ignore rule; explicit additionalFiles must
143 // retain the tracked native manifest while recursive workspace discovery runs.
144 writeFileSync(join(directory, '.gitignore'), 'pantry\n')
145 writeFileSync(join(directory, 'bumpx.config.ts'), readFileSync(join(import.meta.dir, '..', 'bumpx.config.ts')))
146 git('add', '.')
147 git('commit', '-qm', 'chore: register native and workspace release metadata')
148 return ['package.json', 'packages/sdk/package.json', 'packages/zig/build.zig.zon', 'packages/zig/pantry.json']
149}
150
151test('the real release configuration versions every workspace and native manifest before tagging', () => {
152 for (const release of ['1.0.2', 'patch']) fixture((directory, git) => {
153 const paths = nativeWorkspace(directory, git)
142154 const bumpx = Bun.resolveSync('@stacksjs/bumpx/bin/cli.js', import.meta.dir)
143155 const generator = join(import.meta.dir, 'generate-release-changelog.ts')
156 const validator = join(import.meta.dir, 'release-version.ts')
157 // The validator must read the fixture checkout rather than its own source root.
158 mkdirSync(join(directory, 'scripts'))
159 writeFileSync(join(directory, 'scripts/release-version.ts'), readFileSync(validator))
160 git('add', 'scripts')
161 git('commit', '-qm', 'chore: install actual release validator')
144162 const result = Bun.spawnSync([
145 process.execPath, bumpx, '--recursive', '--files', 'package.json,packages/zig/build.zig.zon,packages/zig/pantry.json', '--commit', '--tag', '--no-push', '--yes', '--no-changelog',
146 '--execute', `${shellQuote(process.execPath)} ${shellQuote(generator)} && git add CHANGELOG.md`,
147 '1.0.2',
163 process.execPath, bumpx, '--recursive', '--commit', '--tag', '--no-push', '--yes', '--no-changelog',
164 '--execute', `${shellQuote(process.execPath)} scripts/release-version.ts --pending && ${shellQuote(process.execPath)} ${shellQuote(generator)} && git add CHANGELOG.md`, release,
148165 ], { cwd: directory, stdout: 'pipe', stderr: 'pipe', timeout: 30_000 })
149166 expect(result.exitCode, result.stderr.toString()).toBe(0)
150167 expect(git('tag', '--list', 'v1.0.2')).toBe('v1.0.2\n')
151 const notes = git('show', 'HEAD:CHANGELOG.md')
152 expect(notes).toContain('[Compare changes](https://github.com/example/fixture/compare/v1.0.0...v1.0.2)')
153 expect(notes.match(/\[#326\]\(https:\/\/github\.com\/example\/fixture\/issues\/326\)/g)).toHaveLength(1)
154 expect(JSON.parse(git('show', 'HEAD:packages/zig/pantry.json')).version).toBe('1.0.2')
155 expect(git('show', 'HEAD:packages/zig/build.zig.zon')).toContain('"1.0.2"')
168 for (const path of paths) expect(git('show', `HEAD:${path}`)).toContain('1.0.2')
169 expect(git('show', 'HEAD:CHANGELOG.md')).toContain('/compare/v1.0.0...v1.0.2)')
156170 expect(git('status', '--porcelain')).toBe('')
157171 })
158172})
159173
160test('the published release tool restores workspace and native versions when a required command fails', () => {
174test('the published release tool restores all workspace and native versions on a failed command', () => {
161175 fixture((directory, git) => {
162 mkdirSync(join(directory, 'packages/zig'), { recursive: true })
163 writeFileSync(join(directory, 'packages/zig/build.zig.zon'), '.{ .version = "1.0.1", }')
164 writeFileSync(join(directory, 'packages/zig/pantry.json'), '{"version":"1.0.1"}')
165 git('add', 'packages')
166 git('commit', '-qm', 'chore: register native release metadata')
176 const paths = nativeWorkspace(directory, git)
167177 const head = git('rev-parse', 'HEAD')
168 const paths = ['package.json', 'packages/zig/build.zig.zon', 'packages/zig/pantry.json']
169178 const before = paths.map(path => readFileSync(join(directory, path), 'utf8'))
170179 const bumpx = Bun.resolveSync('@stacksjs/bumpx/bin/cli.js', import.meta.dir)
171180 const result = Bun.spawnSync([
172 process.execPath, bumpx, 'patch', '--recursive', '--files', paths.join(','), '--commit', '--tag', '--no-push', '--yes', '--no-changelog',
181 process.execPath, bumpx, 'patch', '--recursive', '--commit', '--tag', '--no-push', '--yes', '--no-changelog',
173182 '--execute', 'bun -e "process.exit(23)"',
174183 ], { cwd: directory, stdout: 'pipe', stderr: 'pipe', timeout: 30_000 })
175184 expect(result.exitCode).not.toBe(0)
scripts/release-version.tsmodified+4-1
Changes to scripts/release-version.ts
@@ -79,7 +79,10 @@ if (import.meta.main) {
7979 console.log('Release checkout contains every tracked package manifest')
8080 process.exit(0)
8181 }
82 const version = checkReleaseVersion(root, process.env.GITHUB_REF ?? '')
82 const ref = process.argv.includes('--pending')
83 ? `refs/tags/v${JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).version}`
84 : process.env.GITHUB_REF ?? ''
85 const version = checkReleaseVersion(root, ref)
8386 console.log(`Verified release v${version}`)
8487 if (process.env.GITHUB_OUTPUT)
8588 appendFileSync(process.env.GITHUB_OUTPUT, `version=${version}\n`)