mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 17:45:53 +01:00
fix(release): gate channel on published package closure
Publish the user-facing CLI last, wait for registry visibility after every workspace package, and verify a clean canary install before tagging. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
8699e457e0
commit
7a10927ca3
7 files changed
+140
-27
No files matched your search
@@ -232,9 +232,13 @@ After setup:
|
||||
Install-path check:
|
||||
|
||||
```bash
|
||||
npx paperclipai@canary onboard
|
||||
npm install --prefix "$(mktemp -d)" paperclipai@canary --no-audit --no-fund
|
||||
```
|
||||
|
||||
The release script runs this clean-prefix install after publishing every workspace
|
||||
package dependency-first and publishing `paperclipai` last. A package that is not
|
||||
yet registry-visible stops the train before the channel entrypoint can advance.
|
||||
|
||||
## 12. Verify the Stable Workflow
|
||||
|
||||
After at least one good canary exists:
|
||||
|
||||
+4
-1
@@ -62,8 +62,11 @@ It:
|
||||
|
||||
- verifies the pushed commit
|
||||
- computes the canary version for the current UTC date
|
||||
- publishes under npm dist-tag `canary`
|
||||
- publishes workspace packages dependency-first under npm dist-tag `canary`
|
||||
- waits for each package version to become registry-visible before continuing
|
||||
- publishes the user-facing `paperclipai` package last, so `paperclipai@canary` does not advance before the full package set exists
|
||||
- verifies that `canary` resolves to the just-published version and that published internal dependencies exist on npm
|
||||
- installs `paperclipai@canary` into a clean temporary prefix as the final npm gate
|
||||
- fails by default if npm leaves `latest` pointing at a canary; use `--allow-canary-latest` only when that state is intentional
|
||||
- creates a git tag `canary/vYYYY.MDD.P-canary.N`
|
||||
|
||||
|
||||
@@ -385,6 +385,43 @@ publish_package_to_npm() {
|
||||
return 1
|
||||
}
|
||||
|
||||
publish_package_to_npm_and_wait() {
|
||||
local dist_tag="$1"
|
||||
local package_name="$2"
|
||||
local package_version="$3"
|
||||
local publish_tool="${4:-pnpm}"
|
||||
local attempts="${5:-12}"
|
||||
local delay_seconds="${6:-5}"
|
||||
|
||||
publish_package_to_npm "$dist_tag" "$package_name" "$package_version" "$publish_tool" || return 1
|
||||
|
||||
if wait_for_npm_package_version "$package_name" "$package_version" "$attempts" "$delay_seconds"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
release_warn "npm accepted ${package_name}@${package_version}, but the version did not become registry-visible."
|
||||
return 1
|
||||
}
|
||||
|
||||
verify_npm_installable() {
|
||||
local package_spec="$1"
|
||||
local expected_version="$2"
|
||||
local install_dir
|
||||
local installed_version
|
||||
|
||||
install_dir="$(mktemp -d "${TMPDIR:-/tmp}/paperclip-release-install.XXXXXX")"
|
||||
|
||||
if ! npm install --prefix "$install_dir" "$package_spec" --no-audit --no-fund; then
|
||||
rm -rf "$install_dir"
|
||||
return 1
|
||||
fi
|
||||
|
||||
installed_version="$(node -e "console.log(require(process.argv[1]).version)" "$install_dir/node_modules/paperclipai/package.json")"
|
||||
rm -rf "$install_dir"
|
||||
|
||||
[ "$installed_version" = "$expected_version" ]
|
||||
}
|
||||
|
||||
wait_for_release_registry_state() {
|
||||
local attempts="${1:-12}"
|
||||
local delay_seconds="${2:-5}"
|
||||
|
||||
@@ -17,6 +17,7 @@ function runPublishHelper({
|
||||
distTag = "canary",
|
||||
callerPipefail = true,
|
||||
publishTool = "pnpm",
|
||||
waitForRegistry = false,
|
||||
}) {
|
||||
const fixtureDir = mkdtempSync(join(tmpdir(), "paperclip-release-lib-"));
|
||||
const binDir = join(fixtureDir, "bin");
|
||||
@@ -129,7 +130,11 @@ exec npm "$@"
|
||||
const script = `
|
||||
${shellOptions}
|
||||
source "${repoRoot}/scripts/release-lib.sh"
|
||||
publish_package_to_npm ${distTag} @paperclipai/example 1.2.3 ${publishTool}
|
||||
${
|
||||
waitForRegistry
|
||||
? `publish_package_to_npm_and_wait ${distTag} @paperclipai/example 1.2.3 ${publishTool} 1 0`
|
||||
: `publish_package_to_npm ${distTag} @paperclipai/example 1.2.3 ${publishTool}`
|
||||
}
|
||||
`;
|
||||
|
||||
let status = 0;
|
||||
@@ -239,3 +244,23 @@ test("publish_package_to_npm does not retry stable publishes without provenance"
|
||||
assert.match(result.calls, /^npm view @paperclipai\/example@1\.2\.3 version$/m);
|
||||
assert.doesNotMatch(result.calls, /--provenance=false/);
|
||||
});
|
||||
|
||||
test("publish_package_to_npm_and_wait confirms registry visibility before returning", () => {
|
||||
const result = runPublishHelper({
|
||||
pnpmMode: "success",
|
||||
npmVersionExists: true,
|
||||
waitForRegistry: true,
|
||||
});
|
||||
|
||||
assert.equal(result.status, 0);
|
||||
assert.match(result.calls, /^pnpm publish --no-git-checks --tag canary --access public$/m);
|
||||
assert.match(result.calls, /^npm view @paperclipai\/example@1\.2\.3 version$/m);
|
||||
});
|
||||
|
||||
test("publish_package_to_npm_and_wait blocks the release when registry visibility lags", () => {
|
||||
const result = runPublishHelper({ pnpmMode: "success", waitForRegistry: true });
|
||||
|
||||
assert.notEqual(result.status, 0);
|
||||
assert.match(result.calls, /^npm view @paperclipai\/example@1\.2\.3 version$/m);
|
||||
assert.match(result.output, /did not become registry-visible/);
|
||||
});
|
||||
@@ -8,6 +8,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = resolve(__dirname, "..");
|
||||
const manifestPath = join(repoRoot, "scripts", "release-package-manifest.json");
|
||||
const roots = ["packages", "server", "ui", "cli"];
|
||||
const CHANNEL_ENTRYPOINT_PACKAGE = "paperclipai";
|
||||
|
||||
function readJson(filePath) {
|
||||
return JSON.parse(readFileSync(filePath, "utf8"));
|
||||
@@ -211,7 +212,22 @@ function sortTopologically(packages) {
|
||||
}
|
||||
|
||||
function getReleasePackages() {
|
||||
return sortTopologically(buildReleasePackagePlan().filter((pkg) => pkg.publishFromCi));
|
||||
const ordered = sortTopologically(buildReleasePackagePlan().filter((pkg) => pkg.publishFromCi));
|
||||
const entrypoint = ordered.find((pkg) => pkg.name === CHANNEL_ENTRYPOINT_PACKAGE);
|
||||
|
||||
if (!entrypoint) {
|
||||
throw new Error(
|
||||
`release package graph is missing channel entrypoint ${CHANNEL_ENTRYPOINT_PACKAGE}`,
|
||||
);
|
||||
}
|
||||
|
||||
// npm trusted publishing can authenticate `npm publish`, but not a later
|
||||
// `npm dist-tag add`. Publish the user-facing CLI last so its channel tag
|
||||
// cannot advance until every other release package has been accepted by npm.
|
||||
return [
|
||||
...ordered.filter((pkg) => pkg.name !== CHANNEL_ENTRYPOINT_PACKAGE),
|
||||
entrypoint,
|
||||
];
|
||||
}
|
||||
|
||||
function replaceWorkspaceDeps(deps, version) {
|
||||
|
||||
@@ -24,6 +24,33 @@ test("release package list only contains CI-enrolled packages", () => {
|
||||
assert.ok(enabledPackages.every((pkg) => pkg.publishFromCi === true));
|
||||
});
|
||||
|
||||
test("release package list publishes the installable channel entrypoint last", () => {
|
||||
const enabledPackages = getReleasePackages();
|
||||
|
||||
assert.equal(enabledPackages.at(-1)?.name, "paperclipai");
|
||||
assert.ok(enabledPackages.slice(0, -1).some((pkg) => pkg.name === "@paperclipai/server"));
|
||||
});
|
||||
|
||||
test("release package list keeps runtime workspace dependencies ahead of consumers", () => {
|
||||
const enabledPackages = getReleasePackages();
|
||||
const publishIndexByName = new Map(enabledPackages.map((pkg, index) => [pkg.name, index]));
|
||||
|
||||
for (const pkg of enabledPackages) {
|
||||
for (const section of ["dependencies", "optionalDependencies", "peerDependencies"]) {
|
||||
for (const [dependencyName, spec] of Object.entries(pkg.pkg[section] ?? {})) {
|
||||
if (typeof spec !== "string" || !spec.startsWith("workspace:")) continue;
|
||||
const dependencyIndex = publishIndexByName.get(dependencyName);
|
||||
if (dependencyIndex === undefined) continue;
|
||||
|
||||
assert.ok(
|
||||
dependencyIndex < publishIndexByName.get(pkg.name),
|
||||
`${dependencyName} must publish before ${pkg.name}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("Hermes release surface publishes the unified built-in package and keeps gateway as a shim", () => {
|
||||
const packages = buildReleasePackagePlan();
|
||||
const hermes = packages.find((pkg) => pkg.name === "@paperclipai/hermes-paperclip-adapter");
|
||||
|
||||
+24
-23
@@ -250,6 +250,9 @@ if [ "$VERSION_IN_CLI_PACKAGE" != "$TARGET_PUBLISH_VERSION" ]; then
|
||||
release_fail "versioning drift detected. Expected $TARGET_PUBLISH_VERSION but found $VERSION_IN_CLI_PACKAGE."
|
||||
fi
|
||||
|
||||
VERIFY_ATTEMPTS="${NPM_PUBLISH_VERIFY_ATTEMPTS:-12}"
|
||||
VERIFY_DELAY_SECONDS="${NPM_PUBLISH_VERIFY_DELAY_SECONDS:-5}"
|
||||
|
||||
release_info ""
|
||||
if [ "$dry_run" = true ]; then
|
||||
release_info "==> Step 5/7: Previewing publish payloads (--dry-run)..."
|
||||
@@ -281,12 +284,24 @@ else
|
||||
node "$REPO_ROOT/scripts/prepare-bundled-package.mjs" "$REPO_ROOT/$pkg_dir" "$publish_dir"
|
||||
cd "$publish_dir"
|
||||
fi
|
||||
publish_package_to_npm "$DIST_TAG" "$pkg_name" "$pkg_version" "$publish_tool"
|
||||
if ! publish_package_to_npm_and_wait \
|
||||
"$DIST_TAG" \
|
||||
"$pkg_name" \
|
||||
"$pkg_version" \
|
||||
"$publish_tool" \
|
||||
"$VERIFY_ATTEMPTS" \
|
||||
"$VERIFY_DELAY_SECONDS"; then
|
||||
if [ "$publish_tool" = "npm" ]; then
|
||||
rm -rf "$publish_dir"
|
||||
fi
|
||||
release_fail "stopping release: npm did not publish and expose ${pkg_name}@${pkg_version}"
|
||||
fi
|
||||
if [ "$publish_tool" = "npm" ]; then
|
||||
rm -rf "$publish_dir"
|
||||
fi
|
||||
release_info " ✓ Published version is registry-visible"
|
||||
done <<< "$VERSIONED_PACKAGE_INFO"
|
||||
release_info " ✓ Published all packages under dist-tag $DIST_TAG"
|
||||
release_info " ✓ Published the full package set under dist-tag $DIST_TAG"
|
||||
fi
|
||||
|
||||
release_info ""
|
||||
@@ -294,29 +309,9 @@ if [ "$dry_run" = true ]; then
|
||||
release_info "==> Step 6/7: Skipping npm verification in dry-run mode..."
|
||||
else
|
||||
release_info "==> Step 6/7: Confirming npm package availability and dist-tag integrity..."
|
||||
VERIFY_ATTEMPTS="${NPM_PUBLISH_VERIFY_ATTEMPTS:-12}"
|
||||
VERIFY_DELAY_SECONDS="${NPM_PUBLISH_VERIFY_DELAY_SECONDS:-5}"
|
||||
REGISTRY_STATE_VERIFY_ATTEMPTS="${NPM_REGISTRY_STATE_VERIFY_ATTEMPTS:-12}"
|
||||
REGISTRY_STATE_VERIFY_DELAY_SECONDS="${NPM_REGISTRY_STATE_VERIFY_DELAY_SECONDS:-5}"
|
||||
MISSING_PUBLISHED_PACKAGES=""
|
||||
|
||||
while IFS=$'\t' read -r _pkg_dir pkg_name pkg_version; do
|
||||
[ -z "$pkg_name" ] && continue
|
||||
release_info " Checking $pkg_name@$pkg_version"
|
||||
if wait_for_npm_package_version "$pkg_name" "$pkg_version" "$VERIFY_ATTEMPTS" "$VERIFY_DELAY_SECONDS"; then
|
||||
release_info " ✓ Found on npm"
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ -n "$MISSING_PUBLISHED_PACKAGES" ]; then
|
||||
MISSING_PUBLISHED_PACKAGES="${MISSING_PUBLISHED_PACKAGES}, "
|
||||
fi
|
||||
MISSING_PUBLISHED_PACKAGES="${MISSING_PUBLISHED_PACKAGES}${pkg_name}@${pkg_version}"
|
||||
done <<< "$VERSIONED_PACKAGE_INFO"
|
||||
|
||||
[ -z "$MISSING_PUBLISHED_PACKAGES" ] || release_fail "publish completed but npm never exposed: $MISSING_PUBLISHED_PACKAGES"
|
||||
|
||||
release_info " ✓ Verified all versioned packages are available on npm"
|
||||
release_info " ✓ Every version was registry-visible before the next package publish"
|
||||
|
||||
verify_args=(
|
||||
--channel "$channel"
|
||||
@@ -342,6 +337,12 @@ else
|
||||
|
||||
release_fail "publish completed, but npm dist-tags or registry metadata never converged for ${TARGET_PUBLISH_VERSION}"
|
||||
fi
|
||||
|
||||
release_info " Installing paperclipai@$DIST_TAG into a clean prefix..."
|
||||
if ! verify_npm_installable "paperclipai@$DIST_TAG" "$TARGET_PUBLISH_VERSION"; then
|
||||
release_fail "paperclipai@$DIST_TAG did not install cleanly at expected version ${TARGET_PUBLISH_VERSION}"
|
||||
fi
|
||||
release_info " ✓ Clean-prefix install resolved ${TARGET_PUBLISH_VERSION}"
|
||||
fi
|
||||
|
||||
release_info ""
|
||||
|
||||
Reference in new issue
Block a user