chore(release): publish packages from CI
This commit is contained in:
52
.github/workflows/build-binaries.yml
vendored
52
.github/workflows/build-binaries.yml
vendored
@@ -11,12 +11,13 @@ on:
|
|||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
permissions:
|
permissions: {}
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
env:
|
env:
|
||||||
RELEASE_TAG: ${{ github.event.inputs.tag || github.ref_name }}
|
RELEASE_TAG: ${{ github.event.inputs.tag || github.ref_name }}
|
||||||
steps:
|
steps:
|
||||||
@@ -80,3 +81,50 @@ jobs:
|
|||||||
pi-windows-x64.zip \
|
pi-windows-x64.zip \
|
||||||
pi-windows-arm64.zip \
|
pi-windows-arm64.zip \
|
||||||
--clobber
|
--clobber
|
||||||
|
|
||||||
|
publish-npm:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build
|
||||||
|
environment: npm-publish
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
|
env:
|
||||||
|
RELEASE_TAG: ${{ github.event.inputs.tag || github.ref_name }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
with:
|
||||||
|
ref: ${{ env.RELEASE_TAG }}
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Install system dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev fd-find ripgrep
|
||||||
|
sudo ln -s $(which fdfind) /usr/local/bin/fd
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci --ignore-scripts
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Check
|
||||||
|
run: npm run check
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: npm test
|
||||||
|
|
||||||
|
- name: Verify release artifacts are committed
|
||||||
|
run: git diff --exit-code
|
||||||
|
|
||||||
|
- name: Publish npm packages
|
||||||
|
run: node scripts/publish.mjs
|
||||||
|
|||||||
30
AGENTS.md
30
AGENTS.md
@@ -143,36 +143,16 @@ Attribution:
|
|||||||
```
|
```
|
||||||
Verify both Node and Bun startup, model/account listing, interactive startup, and at least one real prompt with the intended default provider. The bare commands `/tmp/pi-local-release/node/pi` and `/tmp/pi-local-release/bun/pi` start interactive mode; run each in tmux, submit a prompt, and wait for the model reply before considering the interactive smoke test passed. Failures are release blockers unless the user explicitly accepts the risk.
|
Verify both Node and Bun startup, model/account listing, interactive startup, and at least one real prompt with the intended default provider. The bare commands `/tmp/pi-local-release/node/pi` and `/tmp/pi-local-release/bun/pi` start interactive mode; run each in tmux, submit a prompt, and wait for the model reply before considering the interactive smoke test passed. Failures are release blockers unless the user explicitly accepts the risk.
|
||||||
|
|
||||||
3. **Verify npm authentication**: run `npm whoami` before starting the release script. If it fails, stop and tell the user to run `npm login` manually first, then retry after they confirm `npm whoami` succeeds.
|
3. **Run the release script**:
|
||||||
|
|
||||||
4. **Brief the user on the WebAuthn flow before running anything**. Print exactly the following message and then stop and wait for the user to confirm in their next message:
|
|
||||||
|
|
||||||
```
|
|
||||||
Before the release publish step, read this carefully:
|
|
||||||
|
|
||||||
- `npm publish` uses WebAuthn 2FA.
|
|
||||||
- The safest flow is for you to run the publish command yourself, because you can see and open the npm authentication URL immediately.
|
|
||||||
- I will tell you the exact command to run.
|
|
||||||
- When npm prints an auth URL, cmd/ctrl-click it, log in in the browser, and select the "don't ask again for N minutes" option if available.
|
|
||||||
- This may happen more than once during publish.
|
|
||||||
- Do not rerun `npm run release:patch` or `npm run release:minor` after a failed publish; only rerun the publish command I give you.
|
|
||||||
|
|
||||||
Reply "ready" once you have read this and are ready to run the command locally.
|
|
||||||
```
|
|
||||||
|
|
||||||
Do not proceed to step 5 until the user explicitly confirms.
|
|
||||||
|
|
||||||
5. **Run the release script**:
|
|
||||||
```bash
|
```bash
|
||||||
npm run release:patch # fixes + additions
|
npm run release:patch # fixes + additions
|
||||||
npm run release:minor # breaking changes
|
npm run release:minor # breaking changes
|
||||||
```
|
```
|
||||||
Do not pass a `timeout` to the bash tool for this call. If publish fails during the WebAuthn/OTP step after version bump, stop and tell the user to run `npm run publish` themselves from the repo root. Never rerun the version bump on your own. After the user reports publish success, continue with the post-publish steps.
|
The release script bumps all package versions, updates changelogs, regenerates release artifacts, runs `npm run check`, commits `Release vX.Y.Z`, tags `vX.Y.Z`, adds fresh `## [Unreleased]` changelog sections, commits `Add [Unreleased] section for next cycle`, then pushes `main` and the tag. Do not rerun the release script after a tag was pushed.
|
||||||
|
|
||||||
6. **After publish succeeds**:
|
4. **CI publishes npm packages**: pushing the `vX.Y.Z` tag triggers `.github/workflows/build-binaries.yml`. The `publish-npm` job uses npm trusted publishing through GitHub Actions OIDC with environment `npm-publish`; no local `npm publish`, `npm whoami`, OTP, or WebAuthn flow is required.
|
||||||
- Add fresh `## [Unreleased]` sections to package changelogs.
|
|
||||||
- Commit with `Add [Unreleased] section for next cycle`.
|
5. **If CI publish fails**: inspect the failed `publish-npm` job. The publish helper is idempotent and skips package versions already present on npm, so rerun the tag workflow after fixing CI or transient npm issues. Do not rerun `npm run release:patch` or `npm run release:minor` for the same version.
|
||||||
- Push `main` and the release tag.
|
|
||||||
|
|
||||||
## User Override
|
## User Override
|
||||||
|
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ We treat npm dependency changes as reviewed code changes.
|
|||||||
- `package-lock.json` is the dependency ground truth. Pre-commit blocks accidental lockfile commits unless `PI_ALLOW_LOCKFILE_CHANGE=1` is set.
|
- `package-lock.json` is the dependency ground truth. Pre-commit blocks accidental lockfile commits unless `PI_ALLOW_LOCKFILE_CHANGE=1` is set.
|
||||||
- `npm run check` verifies pinned direct deps, native TypeScript import compatibility, and the generated coding-agent shrinkwrap.
|
- `npm run check` verifies pinned direct deps, native TypeScript import compatibility, and the generated coding-agent shrinkwrap.
|
||||||
- The published CLI package includes `packages/coding-agent/npm-shrinkwrap.json`, generated from the root lockfile, to pin transitive deps for npm users.
|
- The published CLI package includes `packages/coding-agent/npm-shrinkwrap.json`, generated from the root lockfile, to pin transitive deps for npm users.
|
||||||
- Release smoke tests use `npm run release:local` to build, pack, and create isolated npm and Bun installs outside the repo before publishing.
|
- Release smoke tests use `npm run release:local` to build, pack, and create isolated npm and Bun installs outside the repo before tagging a release.
|
||||||
- Local release installs, documented npm installs, and `pi update --self` use `--ignore-scripts` where supported.
|
- Local release installs, documented npm installs, and `pi update --self` use `--ignore-scripts` where supported.
|
||||||
- CI installs with `npm ci --ignore-scripts`, and a scheduled GitHub workflow runs `npm audit --omit=dev` plus `npm audit signatures --omit=dev`.
|
- CI installs with `npm ci --ignore-scripts`, and a scheduled GitHub workflow runs `npm audit --omit=dev` plus `npm audit signatures --omit=dev`.
|
||||||
- Shrinkwrap generation has an explicit allowlist for dependency lifecycle scripts; new lifecycle-script deps fail checks until reviewed.
|
- Shrinkwrap generation has an explicit allowlist for dependency lifecycle scripts; new lifecycle-script deps fail checks until reviewed.
|
||||||
|
|||||||
10
package.json
10
package.json
@@ -20,13 +20,13 @@
|
|||||||
"profile:tui": "node scripts/profile-coding-agent-node.mjs --mode tui",
|
"profile:tui": "node scripts/profile-coding-agent-node.mjs --mode tui",
|
||||||
"profile:rpc": "node scripts/profile-coding-agent-node.mjs --mode rpc",
|
"profile:rpc": "node scripts/profile-coding-agent-node.mjs --mode rpc",
|
||||||
"test": "npm run test --workspaces --if-present",
|
"test": "npm run test --workspaces --if-present",
|
||||||
"version:patch": "npm version patch -ws --no-git-tag-version && node scripts/sync-versions.js && npm install --package-lock-only",
|
"version:patch": "npm version patch -ws --no-git-tag-version && node scripts/sync-versions.js && npm install --package-lock-only --ignore-scripts",
|
||||||
"version:minor": "npm version minor -ws --no-git-tag-version && node scripts/sync-versions.js && npm install --package-lock-only",
|
"version:minor": "npm version minor -ws --no-git-tag-version && node scripts/sync-versions.js && npm install --package-lock-only --ignore-scripts",
|
||||||
"version:major": "npm version major -ws --no-git-tag-version && node scripts/sync-versions.js && npm install --package-lock-only",
|
"version:major": "npm version major -ws --no-git-tag-version && node scripts/sync-versions.js && npm install --package-lock-only --ignore-scripts",
|
||||||
"version:set": "npm version -ws",
|
"version:set": "npm version -ws",
|
||||||
"prepublishOnly": "npm run clean && npm run build && npm run check",
|
"prepublishOnly": "npm run clean && npm run build && npm run check",
|
||||||
"publish": "npm run prepublishOnly && npm publish -ws --access public",
|
"publish": "npm run prepublishOnly && node scripts/publish.mjs",
|
||||||
"publish:dry": "npm run prepublishOnly && npm publish -ws --access public --dry-run",
|
"publish:dry": "npm run prepublishOnly && node scripts/publish.mjs --dry-run",
|
||||||
"release:local": "node scripts/local-release.mjs",
|
"release:local": "node scripts/local-release.mjs",
|
||||||
"shrinkwrap:coding-agent": "node scripts/generate-coding-agent-shrinkwrap.mjs",
|
"shrinkwrap:coding-agent": "node scripts/generate-coding-agent-shrinkwrap.mjs",
|
||||||
"release:patch": "node scripts/release.mjs patch",
|
"release:patch": "node scripts/release.mjs patch",
|
||||||
|
|||||||
115
scripts/publish.mjs
Normal file
115
scripts/publish.mjs
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { spawnSync } from "node:child_process";
|
||||||
|
import { existsSync, readFileSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
|
||||||
|
const packages = [
|
||||||
|
{ directory: "packages/ai", name: "@earendil-works/pi-ai" },
|
||||||
|
{ directory: "packages/agent", name: "@earendil-works/pi-agent-core" },
|
||||||
|
{ directory: "packages/tui", name: "@earendil-works/pi-tui" },
|
||||||
|
{ directory: "packages/coding-agent", name: "@earendil-works/pi-coding-agent" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const dryRun = process.argv.includes("--dry-run");
|
||||||
|
const unknownArgs = process.argv.slice(2).filter((arg) => arg !== "--dry-run");
|
||||||
|
|
||||||
|
if (unknownArgs.length > 0) {
|
||||||
|
console.error(`Usage: node scripts/publish.mjs [--dry-run]`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function commandForPlatform(command) {
|
||||||
|
return process.platform === "win32" ? `${command}.cmd` : command;
|
||||||
|
}
|
||||||
|
|
||||||
|
function run(command, args, options = {}) {
|
||||||
|
console.log(`$ ${[command, ...args].join(" ")}`);
|
||||||
|
const result = spawnSync(commandForPlatform(command), args, {
|
||||||
|
cwd: options.cwd,
|
||||||
|
encoding: "utf8",
|
||||||
|
stdio: options.capture ? ["inherit", "pipe", "pipe"] : "inherit",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.status !== 0) {
|
||||||
|
const output = [result.stdout, result.stderr].filter(Boolean).join("\n");
|
||||||
|
throw new Error(output ? `Command failed: ${command} ${args.join(" ")}\n${output}` : `Command failed: ${command} ${args.join(" ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readPackageJson(directory) {
|
||||||
|
return JSON.parse(readFileSync(join(directory, "package.json"), "utf8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertBuildOutputExists(directory) {
|
||||||
|
if (!existsSync(join(directory, "dist"))) {
|
||||||
|
throw new Error(`${directory}/dist does not exist. Run npm run build before publishing.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validatePack(directory) {
|
||||||
|
const result = run("npm", ["pack", "--dry-run", "--ignore-scripts", "--json"], { capture: true, cwd: directory });
|
||||||
|
const packed = JSON.parse(result.stdout)[0];
|
||||||
|
console.log(` ${packed.filename}: ${packed.files.length} files, ${packed.size} bytes packed, ${packed.unpackedSize} bytes unpacked`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPublished(name, version) {
|
||||||
|
const result = spawnSync(commandForPlatform("npm"), ["view", `${name}@${version}`, "version", "--json"], {
|
||||||
|
encoding: "utf8",
|
||||||
|
stdio: ["inherit", "pipe", "pipe"],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.status === 0 && result.stdout.trim()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = [result.stdout, result.stderr].filter(Boolean).join("\n");
|
||||||
|
if (result.status !== 0 && (output.includes("E404") || output.includes("404 Not Found"))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(output ? `Failed to query ${name}@${version}\n${output}` : `Failed to query ${name}@${version}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const packageVersions = new Map();
|
||||||
|
for (const pkg of packages) {
|
||||||
|
const packageJson = readPackageJson(pkg.directory);
|
||||||
|
if (packageJson.name !== pkg.name) {
|
||||||
|
throw new Error(`${pkg.directory}/package.json has name ${packageJson.name}, expected ${pkg.name}`);
|
||||||
|
}
|
||||||
|
packageVersions.set(pkg.name, packageJson.version);
|
||||||
|
}
|
||||||
|
|
||||||
|
const versions = [...new Set(packageVersions.values())];
|
||||||
|
if (versions.length !== 1) {
|
||||||
|
throw new Error(`Publish packages are not lockstep versioned: ${versions.join(", ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Publishing pi packages at ${versions[0]}${dryRun ? " (dry run)" : ""}\n`);
|
||||||
|
|
||||||
|
for (const pkg of packages) {
|
||||||
|
const version = packageVersions.get(pkg.name);
|
||||||
|
assertBuildOutputExists(pkg.directory);
|
||||||
|
const published = isPublished(pkg.name, version);
|
||||||
|
|
||||||
|
if (dryRun) {
|
||||||
|
if (published) {
|
||||||
|
console.log(`${pkg.name}@${version} is already published; validating package contents only.`);
|
||||||
|
} else {
|
||||||
|
console.log(`${pkg.name}@${version} is not published; validating package contents before publish.`);
|
||||||
|
}
|
||||||
|
validatePack(pkg.directory);
|
||||||
|
console.log();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (published) {
|
||||||
|
console.log(`Skipping ${pkg.name}@${version}: already published\n`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
run("npm", ["publish", "--access", "public", "--provenance", "--ignore-scripts"], { cwd: pkg.directory });
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
@@ -10,11 +10,12 @@
|
|||||||
* 1. Check for uncommitted changes
|
* 1. Check for uncommitted changes
|
||||||
* 2. Bump version via npm run version:xxx or set an explicit version
|
* 2. Bump version via npm run version:xxx or set an explicit version
|
||||||
* 3. Update CHANGELOG.md files: [Unreleased] -> [version] - date
|
* 3. Update CHANGELOG.md files: [Unreleased] -> [version] - date
|
||||||
* 4. Generate the coding-agent npm-shrinkwrap.json
|
* 4. Regenerate release artifacts
|
||||||
* 5. Commit and tag
|
* 5. Run checks
|
||||||
* 6. Publish to npm
|
* 6. Commit and tag the release
|
||||||
* 7. Add new [Unreleased] section to changelogs
|
* 7. Add new [Unreleased] section to changelogs
|
||||||
* 8. Commit
|
* 8. Commit next-cycle changelog updates
|
||||||
|
* 9. Push main and the tag to trigger CI publishing
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { execSync } from "child_process";
|
import { execSync } from "child_process";
|
||||||
@@ -91,7 +92,7 @@ function bumpOrSetVersion(target) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Setting explicit version (${target})...`);
|
console.log(`Setting explicit version (${target})...`);
|
||||||
run(`npm version ${target} -ws --no-git-tag-version && node scripts/sync-versions.js && npm install --package-lock-only`);
|
run(`npm version ${target} -ws --no-git-tag-version && node scripts/sync-versions.js && npm install --package-lock-only --ignore-scripts`);
|
||||||
return getVersion();
|
return getVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,23 +164,25 @@ console.log("Updating CHANGELOG.md files...");
|
|||||||
updateChangelogsForRelease(version);
|
updateChangelogsForRelease(version);
|
||||||
console.log();
|
console.log();
|
||||||
|
|
||||||
// 4. Generate publish shrinkwrap
|
// 4. Regenerate release artifacts
|
||||||
console.log("Generating coding-agent shrinkwrap...");
|
console.log("Regenerating release artifacts...");
|
||||||
|
run("npm --prefix packages/ai run generate-models");
|
||||||
|
run("npm --prefix packages/ai run generate-image-models");
|
||||||
run("npm run shrinkwrap:coding-agent");
|
run("npm run shrinkwrap:coding-agent");
|
||||||
console.log();
|
console.log();
|
||||||
|
|
||||||
// 5. Commit and tag
|
// 5. Run checks
|
||||||
|
console.log("Running checks...");
|
||||||
|
run("npm run check");
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
// 6. Commit and tag
|
||||||
console.log("Committing and tagging...");
|
console.log("Committing and tagging...");
|
||||||
stageChangedFiles();
|
stageChangedFiles();
|
||||||
run(`git commit -m "Release v${version}"`);
|
run(`git commit -m "Release v${version}"`);
|
||||||
run(`git tag v${version}`);
|
run(`git tag v${version}`);
|
||||||
console.log();
|
console.log();
|
||||||
|
|
||||||
// 6. Publish
|
|
||||||
console.log("Publishing to npm...");
|
|
||||||
run("npm run publish");
|
|
||||||
console.log();
|
|
||||||
|
|
||||||
// 7. Add new [Unreleased] sections
|
// 7. Add new [Unreleased] sections
|
||||||
console.log("Adding [Unreleased] sections for next cycle...");
|
console.log("Adding [Unreleased] sections for next cycle...");
|
||||||
addUnreleasedSection();
|
addUnreleasedSection();
|
||||||
@@ -197,4 +200,4 @@ run("git push origin main");
|
|||||||
run(`git push origin v${version}`);
|
run(`git push origin v${version}`);
|
||||||
console.log();
|
console.log();
|
||||||
|
|
||||||
console.log(`=== Released v${version} ===`);
|
console.log(`=== Prepared release v${version}; CI publishing starts after the tag push ===`);
|
||||||
|
|||||||
Reference in New Issue
Block a user