Skip to content

Commit fa3ebf8

Browse files
authored
Merge pull request #24 from rogerchappel/agent/oss-23acc77d2c34-release-publishing
2 parents b8d0f8c + 114dbcb commit fa3ebf8

7 files changed

Lines changed: 70 additions & 4 deletions

File tree

.github/workflows/release-dry-run.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ jobs:
2929
cache: npm
3030
- name: Install dependencies
3131
run: npm ci
32+
- name: Validate prospective release tag
33+
run: npm run release:tag -- "v$(node -p \"require('./package.json').version\")"
3234
- name: Install ReleaseBox
3335
run: |
3436
set -euo pipefail

.github/workflows/release.yml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ jobs:
2727
registry-url: https://registry.npmjs.org
2828
- name: Install dependencies
2929
run: npm ci
30+
- name: Validate release tag
31+
run: npm run release:tag -- "$GITHUB_REF_NAME"
3032
- name: Install ReleaseBox
3133
run: |
3234
set -euo pipefail
@@ -37,11 +39,13 @@ jobs:
3739
run: node /tmp/releasebox/bin/releasebox.js check .
3840
- name: Run release checks
3941
run: npm run release:check
40-
- name: Build package
41-
run: npm pack
42+
- name: Publish package to npm
43+
run: npm publish --provenance --access public
4244
- name: Generate release notes
4345
run: node /tmp/releasebox/bin/releasebox.js notes . > RELEASE_NOTES.md
4446
- name: Create GitHub release
4547
env:
4648
GH_TOKEN: ${{ github.token }}
47-
run: gh release create "${GITHUB_REF_NAME}" --notes-file RELEASE_NOTES.md *.tgz
49+
run: |
50+
npm pack
51+
gh release create "${GITHUB_REF_NAME}" --notes-file RELEASE_NOTES.md *.tgz

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,4 +202,6 @@ npm run package:smoke
202202
npm run release:check
203203
```
204204

205-
The package smoke uses `npm pack --dry-run` and fails if the CLI, library entrypoint, license, security policy, changelog, or contribution guide would be missing from the published tarball.
205+
The package smoke checks the tarball contents, installs that tarball into a clean temporary consumer, and invokes the installed CLI.
206+
207+
Version releases are distributed through npm. Set `package.json` to the intended version, run `npm run release:tag -- v<version>` and `npm run release:check`, then push that exact tag. The tag workflow validates the tag again, publishes the public package to npm with provenance, and creates the GitHub release only after npm publication succeeds.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"test": "npm run build && node --test tests/*.test.mjs",
2626
"smoke": "bash scripts/smoke.sh",
2727
"package:smoke": "npm run build && node scripts/package-smoke.mjs",
28+
"release:tag": "node scripts/validate-release-tag.mjs",
2829
"release:check": "npm run check && npm test && npm run build && npm run smoke && npm run package:smoke"
2930
},
3031
"keywords": [

scripts/package-smoke.mjs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
#!/usr/bin/env node
22
import { execFileSync } from "node:child_process";
3+
import { mkdtempSync, rmSync } from "node:fs";
4+
import { tmpdir } from "node:os";
5+
import { join } from "node:path";
36

47
const requiredFiles = [
58
"dist/cli.js",
@@ -26,3 +29,16 @@ if (missing.length > 0) {
2629
}
2730

2831
console.log(`Package smoke passed with ${pack.files.length} files.`);
32+
33+
const consumer = mkdtempSync(join(tmpdir(), "promptdiff-package-smoke-"));
34+
try {
35+
const tarball = execFileSync("npm", ["pack", "--silent"], { encoding: "utf8" }).trim();
36+
execFileSync("npm", ["init", "--yes"], { cwd: consumer, stdio: "ignore" });
37+
execFileSync("npm", ["install", join(process.cwd(), tarball)], { cwd: consumer, stdio: "inherit" });
38+
const cli = join(consumer, "node_modules", ".bin", "promptdiff");
39+
const help = execFileSync(cli, ["--help"], { cwd: consumer, encoding: "utf8" });
40+
if (!help.includes("promptdiff compare")) throw new Error("Installed CLI help was not usable");
41+
rmSync(join(process.cwd(), tarball));
42+
} finally {
43+
rmSync(consumer, { recursive: true, force: true });
44+
}

scripts/validate-release-tag.mjs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/usr/bin/env node
2+
import { readFileSync } from "node:fs";
3+
4+
const tag = process.argv[2] ?? process.env.GITHUB_REF_NAME;
5+
const { version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
6+
const expected = `v${version}`;
7+
8+
if (!tag || !/^v\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(tag) || tag !== expected) {
9+
console.error(`Release tag must exactly match package version: expected ${expected}, received ${tag ?? "<missing>"}.`);
10+
process.exit(1);
11+
}
12+
13+
console.log(`Release tag ${tag} matches package version ${version}.`);

tests/release.test.mjs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import test from "node:test";
2+
import assert from "node:assert/strict";
3+
import { spawnSync } from "node:child_process";
4+
import { readFileSync } from "node:fs";
5+
6+
test("release tag must exactly match package version", () => {
7+
for (const tag of [undefined, "0.1.0", "v0.1", "v9.9.9"]) {
8+
const args = ["scripts/validate-release-tag.mjs", ...(tag ? [tag] : [])];
9+
const run = spawnSync(process.execPath, args, { encoding: "utf8", env: { ...process.env, GITHUB_REF_NAME: "" } });
10+
assert.equal(run.status, 1, tag);
11+
}
12+
assert.equal(spawnSync(process.execPath, ["scripts/validate-release-tag.mjs", "v0.1.0"]).status, 0);
13+
});
14+
15+
test("release workflow validates before publishing and publishes before GitHub release", () => {
16+
const workflow = readFileSync(".github/workflows/release.yml", "utf8");
17+
const validate = workflow.indexOf("npm run release:tag");
18+
const publish = workflow.indexOf("npm publish --provenance --access public");
19+
const githubRelease = workflow.indexOf("gh release create");
20+
assert.ok(validate >= 0 && validate < publish);
21+
assert.ok(publish < githubRelease);
22+
assert.match(workflow, /permissions:\n contents: write\n id-token: write/);
23+
});
24+
25+
test("dry run validates a prospective package tag", () => {
26+
const workflow = readFileSync(".github/workflows/release-dry-run.yml", "utf8");
27+
assert.match(workflow, /npm run release:tag -- "v\$\(node/);
28+
});

0 commit comments

Comments
 (0)