Fix stable release dry-run notes gate (#9334)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - The release subsystem uses GitHub Actions dry-run previews so maintainers can validate stable and canary release behavior before publishing. > - Stable release publishing needs a release notes gate so real `latest` publishes never happen without authored notes. > - The same gate was also blocking stable dry-run previews, which made release QA fail before publish-sensitive work could be previewed. > - This pull request narrows the notes requirement to real non-dry-run stable publishes. > - The benefit is that stable dry-run dispatches can preview the release without same-day notes, while real stable publishes remain protected. ## Linked Issues or Issue Description No public GitHub issue exists for this release-QA blocker, so the bug is described inline below. ### What happened? Stable dry-run release previews fail when the same-day release notes file is absent. The release script runs the stable notes-file gate before release preview work even when `--dry-run` is set. ### Expected behavior `./scripts/release.sh stable --dry-run` should preview the stable release without requiring `releases/vYYYY.MDD.P.md`. Real non-dry-run stable publishes must still fail before build/publish work starts when the notes file is missing. ### Steps to reproduce 1. Check out current `master` before this fix. 2. Ensure the computed same-day stable release notes file does not exist under `releases/`. 3. Run `./scripts/release.sh stable --skip-verify --dry-run`. 4. Observe that the script exits with `stable release notes file is required` instead of reaching the release preview plan. ### Paperclip version or commit Reproduced on `master` at `9a1d4b7983dfd50e8eb40ee9770e44999d405f60`. ### Deployment mode Built from source / GitHub Actions release workflow. ## What Changed - Narrowed the stable release notes gate to `channel=stable` and `dry_run=false`. - Updated the release script usage note to say the notes file is required for non-dry-run stable releases. - Added a targeted Node test covering dry-run allowed behavior and non-dry-run blocked behavior. - Stubbed release fixture registry-version checks so the test isolates the notes gate without hitting npm. ## Verification - `node --test scripts/__tests__/release-dry-run-notes.test.mjs` passed with 2/2 subtests. - `bash -n scripts/release.sh` exited 0. ## Risks Low risk. The behavior change only relaxes the notes-file gate for stable dry-runs. The new test verifies real non-dry-run stable publish still fails before build/publish work starts when notes are missing. ## Model Used OpenAI Codex, GPT-5-based coding agent, with shell/tool execution in a local repository workspace. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
6f204605ad
commit
6a36ae47fe
|
|
@ -0,0 +1,150 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { chmodSync, copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
const repoRoot = new URL("../..", import.meta.url).pathname.replace(/\/$/, "");
|
||||
|
||||
function writeExecutable(path, body) {
|
||||
writeFileSync(path, body, { mode: 0o755 });
|
||||
}
|
||||
|
||||
function createReleaseFixture() {
|
||||
const fixtureDir = mkdtempSync(join(tmpdir(), "paperclip-release-dry-run-"));
|
||||
const scriptsDir = join(fixtureDir, "scripts");
|
||||
const binDir = join(fixtureDir, "bin");
|
||||
const callLog = join(fixtureDir, "calls.log");
|
||||
|
||||
mkdirSync(scriptsDir, { recursive: true });
|
||||
mkdirSync(join(fixtureDir, "releases"));
|
||||
mkdirSync(binDir);
|
||||
writeFileSync(callLog, "");
|
||||
|
||||
copyFileSync(join(repoRoot, "scripts", "release.sh"), join(scriptsDir, "release.sh"));
|
||||
chmodSync(join(scriptsDir, "release.sh"), 0o755);
|
||||
|
||||
writeFileSync(
|
||||
join(scriptsDir, "release-lib.sh"),
|
||||
`#!/usr/bin/env bash
|
||||
release_info() { echo "$@"; }
|
||||
release_fail() { echo "Error: $*" >&2; exit 1; }
|
||||
resolve_release_remote() { printf 'origin\\n'; }
|
||||
fetch_release_remote() { :; }
|
||||
git_current_branch() { printf 'master\\n'; }
|
||||
get_last_stable_tag() { printf 'v2026.709.0\\n'; }
|
||||
get_current_stable_version() { printf '2026.709.0\\n'; }
|
||||
utc_date_iso() { printf '2026-07-10\\n'; }
|
||||
list_public_package_info() { printf 'cli\\tpaperclipai\\t0.0.0\\n'; }
|
||||
next_stable_version() { printf '2026.710.0\\n'; }
|
||||
next_canary_version() { printf '2026.710.0-canary.0\\n'; }
|
||||
release_notes_file() { printf '%s/releases/v%s.md\\n' "$REPO_ROOT" "$1"; }
|
||||
stable_tag_name() { printf 'v%s\\n' "$1"; }
|
||||
canary_tag_name() { printf 'canary/v%s\\n' "$1"; }
|
||||
require_clean_worktree() { :; }
|
||||
require_npm_publish_auth() { :; }
|
||||
git_local_tag_exists() { return 1; }
|
||||
git_remote_tag_exists() { return 1; }
|
||||
npm_package_version_exists() { return 1; }
|
||||
set_public_package_version() { :; }
|
||||
`,
|
||||
);
|
||||
|
||||
writeExecutable(
|
||||
join(scriptsDir, "release-registry-versions.mjs"),
|
||||
`#!/usr/bin/env node
|
||||
const [mode] = process.argv.slice(2);
|
||||
if (mode === "fetch") {
|
||||
process.stdout.write('{"paperclipai":[]}\\n');
|
||||
process.exit(0);
|
||||
}
|
||||
if (mode === "assert-absent") {
|
||||
process.exit(0);
|
||||
}
|
||||
process.exit(2);
|
||||
`,
|
||||
);
|
||||
|
||||
writeExecutable(
|
||||
join(binDir, "git"),
|
||||
`#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
if [ "$1" = "-C" ]; then
|
||||
shift 2
|
||||
fi
|
||||
printf 'git %s\\n' "$*" >> "$FAKE_CALL_LOG"
|
||||
case "$1" in
|
||||
rev-parse)
|
||||
if [ "\${2:-}" = "HEAD" ]; then
|
||||
echo abcdef1234567890
|
||||
exit 0
|
||||
fi
|
||||
;;
|
||||
diff|ls-files)
|
||||
exit 0
|
||||
;;
|
||||
checkout)
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
exit 0
|
||||
`,
|
||||
);
|
||||
|
||||
writeExecutable(
|
||||
join(binDir, "pnpm"),
|
||||
`#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
printf 'pnpm %s\\n' "$*" >> "$FAKE_CALL_LOG"
|
||||
if [ "$*" = "build" ]; then
|
||||
echo "fixture stopped at workspace build"
|
||||
exit 42
|
||||
fi
|
||||
exit 0
|
||||
`,
|
||||
);
|
||||
|
||||
return { binDir, callLog, fixtureDir, script: join(scriptsDir, "release.sh") };
|
||||
}
|
||||
|
||||
function runRelease(args) {
|
||||
const fixture = createReleaseFixture();
|
||||
const result = spawnSync(fixture.script, args, {
|
||||
cwd: fixture.fixtureDir,
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
PATH: `${fixture.binDir}:${process.env.PATH}`,
|
||||
FAKE_CALL_LOG: fixture.callLog,
|
||||
},
|
||||
});
|
||||
|
||||
const calls = readFileSync(fixture.callLog, "utf8");
|
||||
rmSync(fixture.fixtureDir, { recursive: true, force: true });
|
||||
|
||||
return {
|
||||
calls,
|
||||
output: result.stdout + result.stderr,
|
||||
status: result.status,
|
||||
};
|
||||
}
|
||||
|
||||
test("stable dry-run preview does not require a pre-authored release notes file", () => {
|
||||
const result = runRelease(["stable", "--skip-verify", "--dry-run"]);
|
||||
|
||||
assert.equal(result.status, 42);
|
||||
assert.match(result.output, /==> Release plan/);
|
||||
assert.match(result.output, /==> Step 2\/7: Building workspace artifacts/);
|
||||
assert.doesNotMatch(result.output, /stable release notes file is required/);
|
||||
assert.match(result.calls, /^pnpm build$/m);
|
||||
});
|
||||
|
||||
test("stable publish still requires release notes before publish work starts", () => {
|
||||
const result = runRelease(["stable", "--skip-verify"]);
|
||||
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(result.output, /stable release notes file is required/);
|
||||
assert.doesNotMatch(result.output, /==> Step 2\/7: Building workspace artifacts/);
|
||||
assert.doesNotMatch(result.calls, /^pnpm /m);
|
||||
});
|
||||
|
|
@ -34,7 +34,7 @@ Notes:
|
|||
"canary" and create the git tag canary/vYYYY.MDD.P-canary.N.
|
||||
- Stable releases publish YYYY.MDD.P under the npm dist-tag "latest" and
|
||||
create the git tag vYYYY.MDD.P.
|
||||
- Stable release notes must already exist at releases/vYYYY.MDD.P.md.
|
||||
- Non-dry-run stable release notes must already exist at releases/vYYYY.MDD.P.md.
|
||||
- The script rewrites versions temporarily and restores the working tree on
|
||||
exit. Tags always point at the original source commit, not a generated
|
||||
release commit.
|
||||
|
|
@ -166,7 +166,7 @@ NOTES_FILE="$(release_notes_file "$TARGET_STABLE_VERSION")"
|
|||
require_clean_worktree
|
||||
require_npm_publish_auth "$dry_run"
|
||||
|
||||
if [ "$channel" = "stable" ] && [ ! -f "$NOTES_FILE" ]; then
|
||||
if [ "$channel" = "stable" ] && [ "$dry_run" = false ] && [ ! -f "$NOTES_FILE" ]; then
|
||||
release_fail "stable release notes file is required at $NOTES_FILE before publishing stable."
|
||||
fi
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue