also looking at this
fix: validate native SDK release prerequisites before tagging
#364
7 files
+153
-15
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,3 +61,18 @@ registry publication can still fail after the native archives become public, so | ||
| 61 | 61 | the final workflow status remains the release signal. If a post-release registry |
| 62 | 62 | publication fails, the GitHub release still contains the full verified archive |
| 63 | 63 | set; inspect and rerun the failed job rather than republishing the draft. |
| 64 | ||
| 65 | ## Release prerequisites and failed tags | |
| 66 | ||
| 67 | Install the locked release tools with `bun install --frozen-lockfile` first. | |
| 68 | Preflight now checks the Bumpx and Logsmith CLIs, a clean `main`, and the current | |
| 69 | `origin/main` before changing versions. Use a separate release clone if `main` | |
| 70 | is in use by another worktree. Required release commands fail before committing, | |
| 71 | tagging or pushing; Bumpx restores the version files when they fail. | |
| 72 | ||
| 73 | Retain a tag that failed publication. Choose a new unused version with | |
| 74 | `bun run release:version <version>` and set `CRAFT_CHANGELOG_FROM` to the last | |
| 75 | successfully published tag. The standard version and patch commands run the | |
| 76 | same manifest synchronization, changelog generator and publication gates. | |
| 77 | Confirm the final Releaser result and actual registry artifacts before apps | |
| 78 | adopt the new version. | |
| @@ -1,7 +1,7 @@ | ||
| 1 | 1 | import { afterAll, expect, test } from 'bun:test' |
| 2 | 2 | import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' |
| 3 | 3 | import { join } from 'node:path' |
| 4 | import { validateReleaseCheckout, checkReleaseVersion, validateReleaseVersion } from './release-version' | |
| 4 | import { validateReleaseCheckout, validateReleaseGit, checkReleaseVersion, validateReleaseVersion } from './release-version' | |
| 5 | 5 | import { createTempDir, removeAllTempDirs } from '../packages/typescript/src/temp-dir' |
| 6 | 6 | |
| 7 | 7 | // Every scratch directory this file made goes when the file is done (#357). |
| @@ -74,3 +74,36 @@ test('release preflight rejects missing tools before changing package versions o | ||
| 74 | 74 | expect(readFileSync(join(root, 'package.json'), 'utf8')).toBe(manifest) |
| 75 | 75 | expect(Bun.spawnSync(['git', 'tag', '--list'], { cwd: root, stdout: 'pipe' }).stdout.toString()).toBe('') |
| 76 | 76 | }) |
| 77 | ||
| 78 | test('release Git preflight requires clean main and checks the current remote', () => { | |
| 79 | const root = createTempDir('craft-release-main-') | |
| 80 | const remote = join(root, 'remote.git') | |
| 81 | const work = join(root, 'work') | |
| 82 | mkdirSync(work) | |
| 83 | const git = (cwd: string, ...args: string[]) => { | |
| 84 | const result = Bun.spawnSync(['git', ...args], { cwd, stdout: 'pipe', stderr: 'pipe' }) | |
| 85 | expect(result.exitCode, result.stderr.toString()).toBe(0) | |
| 86 | return result.stdout.toString() | |
| 87 | } | |
| 88 | git(root, 'init', '--bare', remote) | |
| 89 | git(work, 'init', '-b', 'main') | |
| 90 | git(work, 'config', 'user.email', 'release@example.com') | |
| 91 | git(work, 'config', 'user.name', 'Release fixture') | |
| 92 | git(work, 'commit', '--allow-empty', '-m', 'chore: initial release') | |
| 93 | git(work, 'remote', 'add', 'origin', remote) | |
| 94 | git(work, 'push', '-u', 'origin', 'main') | |
| 95 | expect(() => validateReleaseGit(work)).not.toThrow() | |
| 96 | writeFileSync(join(work, 'unfinished.txt'), 'unfinished') | |
| 97 | expect(() => validateReleaseGit(work)).toThrow('clean main') | |
| 98 | rmSync(join(work, 'unfinished.txt')) | |
| 99 | git(work, 'switch', '-c', 'feature') | |
| 100 | expect(() => validateReleaseGit(work)).toThrow('Release from main') | |
| 101 | git(work, 'switch', 'main') | |
| 102 | const peer = join(root, 'peer') | |
| 103 | git(root, 'clone', '--branch', 'main', remote, peer) | |
| 104 | git(peer, 'config', 'user.email', 'peer@example.com') | |
| 105 | git(peer, 'config', 'user.name', 'Release peer') | |
| 106 | git(peer, 'commit', '--allow-empty', '-m', 'chore: another source change') | |
| 107 | git(peer, 'push', 'origin', 'main') | |
| 108 | expect(() => validateReleaseGit(work)).toThrow('must match origin/main') | |
| 109 | }) | |
| @@ -20,6 +20,19 @@ export function validateReleaseDependencies(root: string): void { | ||
| 20 | 20 | } |
| 21 | 21 | } |
| 22 | 22 | |
| 23 | /** Releases originate from a clean main that matches the current remote. */ | |
| 24 | export function validateReleaseGit(root: string): void { | |
| 25 | const git = (...args: string[]): string => { | |
| 26 | const result = Bun.spawnSync(['git', ...args], { cwd: root, stdout: 'pipe', stderr: 'pipe', timeout: 30_000 }) | |
| 27 | if (result.exitCode !== 0) throw new Error(`Release Git preflight failed: ${result.stderr.toString().trim()}`) | |
| 28 | return result.stdout.toString().trim() | |
| 29 | } | |
| 30 | if (git('branch', '--show-current') !== 'main') throw new Error('Release from main, using an isolated checkout when main is already in use.') | |
| 31 | if (git('status', '--porcelain')) throw new Error('Release requires a clean main checkout.') | |
| 32 | git('fetch', 'origin', 'main') | |
| 33 | if (git('rev-parse', 'HEAD') !== git('rev-parse', 'origin/main')) throw new Error('Release main must match origin/main. Synchronize and verify its checks before releasing.') | |
| 34 | } | |
| 35 | ||
| 23 | 36 | export function validateReleaseVersion(ref: string, version: string, manifests: Record<string, unknown>): void { |
| 24 | 37 | const errors: string[] = [] |
| 25 | 38 | const identifier = '(?:0|[1-9]\\d*|[\\da-z-]*[a-z-][\\da-z-]*)' |
| @@ -58,6 +71,7 @@ if (import.meta.main) { | ||
| 58 | 71 | if (process.argv.includes('--preflight')) { |
| 59 | 72 | validateReleaseCheckout(root) |
| 60 | 73 | validateReleaseDependencies(root) |
| 74 | validateReleaseGit(root) | |
| 61 | 75 | console.log('Release checkout contains every tracked package manifest') |
| 62 | 76 | process.exit(0) |
| 63 | 77 | } |