also looking at this
fix: validate complete workspace release before tagging
#365
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.
| @@ -61,8 +61,8 @@ | ||
| 61 | 61 | "changelog:generate": "bun scripts/generate-release-changelog.ts", |
| 62 | 62 | "test:release": "bun test ./scripts/changelog-preview.test.ts ./scripts/release-workflow.test.ts ./scripts/release-manifest.test.ts ./scripts/verify-sbom.test.ts", |
| 63 | 63 | "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\"" | |
| 66 | 66 | }, |
| 67 | 67 | "devDependencies": { |
| 68 | 68 | "@stacksjs/buddy": "0.72.56", |
| @@ -128,48 +128,57 @@ test('patch release uses the guarded changelog generator before committing', () | ||
| 128 | 128 | expect(project.scripts['changelog:generate']).toBe('bun scripts/generate-release-changelog.ts') |
| 129 | 129 | expect(command).toContain('--no-changelog') |
| 130 | 130 | 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') | |
| 132 | 133 | }) |
| 133 | 134 | |
| 134 | test('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') | |
| 135 | function 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 | ||
| 151 | test('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) | |
| 142 | 154 | const bumpx = Bun.resolveSync('@stacksjs/bumpx/bin/cli.js', import.meta.dir) |
| 143 | 155 | 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') | |
| 144 | 162 | 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, | |
| 148 | 165 | ], { cwd: directory, stdout: 'pipe', stderr: 'pipe', timeout: 30_000 }) |
| 149 | 166 | expect(result.exitCode, result.stderr.toString()).toBe(0) |
| 150 | 167 | 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)') | |
| 156 | 170 | expect(git('status', '--porcelain')).toBe('') |
| 157 | 171 | }) |
| 158 | 172 | }) |
| 159 | 173 | |
| 160 | test('the published release tool restores workspace and native versions when a required command fails', () => { | |
| 174 | test('the published release tool restores all workspace and native versions on a failed command', () => { | |
| 161 | 175 | 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) | |
| 167 | 177 | const head = git('rev-parse', 'HEAD') |
| 168 | const paths = ['package.json', 'packages/zig/build.zig.zon', 'packages/zig/pantry.json'] | |
| 169 | 178 | const before = paths.map(path => readFileSync(join(directory, path), 'utf8')) |
| 170 | 179 | const bumpx = Bun.resolveSync('@stacksjs/bumpx/bin/cli.js', import.meta.dir) |
| 171 | 180 | 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', | |
| 173 | 182 | '--execute', 'bun -e "process.exit(23)"', |
| 174 | 183 | ], { cwd: directory, stdout: 'pipe', stderr: 'pipe', timeout: 30_000 }) |
| 175 | 184 | expect(result.exitCode).not.toBe(0) |
| @@ -79,7 +79,10 @@ if (import.meta.main) { | ||
| 79 | 79 | console.log('Release checkout contains every tracked package manifest') |
| 80 | 80 | process.exit(0) |
| 81 | 81 | } |
| 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) | |
| 83 | 86 | console.log(`Verified release v${version}`) |
| 84 | 87 | if (process.env.GITHUB_OUTPUT) |
| 85 | 88 | appendFileSync(process.env.GITHUB_OUTPUT, `version=${version}\n`) |