mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-06 10:48:12 +02:00
fix(release): draft-notes baseline survives candidate-cut stables (#11647)
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work
> - The release workflow drafts the upcoming stable's notes skeleton at
beta publish, ranging from the last stable to the beta's source commit
> - Stables cut from candidate branches leave their tag off master's
lineage, so the ancestor-only baseline rule skips them and falls back a
whole release too far
> - The first live draft did exactly that: it ranged from v2026.722.0
instead of the just-shipped v2026.817.0 and re-included everything
already released
> - This pull request replaces the baseline rule with a merge-base walk
over stable tags and adds a candidate-branch regression fixture
> - The benefit is a correct draft range for every stable lineage:
master-cut, candidate-cut, and promoted-older-source
## Linked Issues or Issue Description
**What happened?**
The first `draft_stable_notes` run (for beta `2026.818.0-beta.1`)
generated `releases/beta/v2026.818.0-beta.1.md` with the range
`v2026.722.0..664052f8e` — 311-commits-worth of already-shipped
v2026.817.0 content re-included.
**Expected behavior**
The draft ranges from the point the shipped stable's content diverges
from the beta source. For v2026.817.0 (cut from
`candidate/release-2026.817.0`) that is the promoted source commit
`8f7b8b3fd`, giving the 172 commits of genuinely-new work.
**Steps to reproduce**
Ship a stable from a candidate branch (tag lands off master's lineage),
then publish a beta from master and read the generated skeleton's range
line.
**Paperclip version or commit**
master at `664052f8e`.
Related (not duplicates): #11567 introduced the generator; its
ancestor-only rule was itself a review fix for the newest-by-version
rule, and this PR is the second iteration with the lineage case the
first fix missed.
## What Changed
- `scripts/draft-stable-notes.sh`: the range start comes from walking
stable tags newest-first and taking the first whose merge-base with the
beta source is a proper ancestor of the source. Candidate-cut stables
resolve to the promoted commit, master-lineage stables to the tag
itself, and tags containing the source are skipped (an older-source
promotion cannot produce an empty range). The skeleton header prints a
runnable short-sha range with the stable tag as a labeled baseline.
- `scripts/draft-stable-notes.test.mjs`: new regression fixture with the
stable tag on an unmerged candidate branch; the existing older-source
and fallback fixtures still pass unchanged.
## Verification
- `node --test scripts/draft-stable-notes.test.mjs` — 8 pass, including
the new fixture.
- Regenerated the live `2026.818.0-beta.1` draft against the real
repository: range start resolves to `v2026.817.0 (merge-base
8f7b8b3fd)`, 172 commits, shipped-in-stable subjects absent, post-beta
subjects present.
- `bash -n` on the script.
## Risks
- Low risk: the script only produces a draft file; publishes are
untouched. The live skeleton branch for `2026.818.0-beta.1` is
regenerated with the corrected output (machine-owned branch, force-push
by design).
## Model Used
Claude Fable 5 (Claude Code)
## Pre-submission 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
This commit is contained in:
1 parent
d1cd9c37f4
commit
e434829889
2 files changed
+55
-9
No files matched your search
@@ -70,28 +70,54 @@ if ! git -C "$repo_dir" rev-parse --verify "refs/tags/${beta_tag}" >/dev/null 2>
|
||||
release_fail "beta tag ${beta_tag} does not exist. Draft notes are generated from a published beta."
|
||||
fi
|
||||
source_sha="$(git -C "$repo_dir" rev-parse "${beta_tag}^{commit}")"
|
||||
source_short="$(git -C "$repo_dir" rev-parse --short "$source_sha")"
|
||||
|
||||
if [ -z "$out_file" ]; then
|
||||
out_file="${repo_dir}/releases/beta/v${beta_version}.md"
|
||||
fi
|
||||
|
||||
# Range start: the newest stable tag reachable from the source commit —
|
||||
# not the newest by version, which can sit on a divergent lineage (a
|
||||
# stable cut from a candidate branch, or a source that predates it) and
|
||||
# would produce an empty or wrong range. Before the first reachable
|
||||
# stable, fall back to the nearest beta tag strictly before the source;
|
||||
# with no marker at all, cover the source commit's full history.
|
||||
range_start="$(git -C "$repo_dir" describe --tags --match 'v[0-9]*' --abbrev=0 "$source_sha" 2>/dev/null || true)"
|
||||
range_label="$range_start"
|
||||
# Range start: walk stable tags newest-first and take the first whose
|
||||
# merge-base with the source is a proper ancestor of the source. A stable
|
||||
# cut from a candidate branch is not itself an ancestor of master, but
|
||||
# its merge-base with the source is the promoted source commit — exactly
|
||||
# the point the shipped stable's content diverges from. A stable that
|
||||
# already contains the source (promoting an older commit) is skipped so
|
||||
# the range never collapses to empty. Before the first stable, fall back
|
||||
# to the nearest beta tag strictly before the source; with no marker at
|
||||
# all, cover the source commit's full history.
|
||||
range_start=""
|
||||
range_label=""
|
||||
range_cmd_start=""
|
||||
while IFS= read -r stable_tag; do
|
||||
[ -n "$stable_tag" ] || continue
|
||||
mb="$(git -C "$repo_dir" merge-base "$stable_tag" "$source_sha" 2>/dev/null || true)"
|
||||
[ -n "$mb" ] || continue
|
||||
if [ "$mb" != "$source_sha" ]; then
|
||||
range_start="$mb"
|
||||
if [ "$mb" = "$(git -C "$repo_dir" rev-parse "${stable_tag}^{commit}")" ]; then
|
||||
range_label="$stable_tag"
|
||||
range_cmd_start="$stable_tag"
|
||||
else
|
||||
# rev-parse --short picks an abbreviation that is unambiguous in
|
||||
# this repository, unlike a fixed nine-character truncation.
|
||||
mb_short="$(git -C "$repo_dir" rev-parse --short "$mb")"
|
||||
range_label="${stable_tag} (merge-base ${mb_short})"
|
||||
range_cmd_start="$mb_short"
|
||||
fi
|
||||
break
|
||||
fi
|
||||
done < <(git -C "$repo_dir" tag -l 'v[0-9]*' --sort=-v:refname)
|
||||
if [ -z "$range_start" ]; then
|
||||
range_start="$(git -C "$repo_dir" describe --tags --match 'beta/v*' --abbrev=0 "${source_sha}^" 2>/dev/null || true)"
|
||||
range_label="$range_start"
|
||||
range_cmd_start="$range_start"
|
||||
fi
|
||||
if [ -n "$range_start" ]; then
|
||||
range="${range_start}..${source_sha}"
|
||||
else
|
||||
range="$source_sha"
|
||||
range_label="the beginning of history"
|
||||
range_cmd_start="the beginning of history"
|
||||
release_info "No stable or prior beta tag found; drafting from full history."
|
||||
fi
|
||||
|
||||
@@ -114,7 +140,7 @@ conventional='^(feat|fix)(\([^)]*\))?!?: '
|
||||
mkdir -p "$(dirname "$out_file")"
|
||||
{
|
||||
printf '# Paperclip stable draft — from beta %s\n\n' "$beta_version"
|
||||
printf '> Auto-generated at beta publish from `git log %s..%s`.\n' "${range_label}" "${source_sha:0:9}"
|
||||
printf '> Auto-generated at beta publish from `git log %s..%s` (baseline: %s).\n' "${range_cmd_start}" "${source_short}" "${range_label}"
|
||||
printf '> Edit freely during the soak: rewrite for release-notes voice,\n'
|
||||
printf '> fold noise, and call out anything a self-hoster must act on.\n'
|
||||
printf '> The stable promotion reads this file from master and publishes\n'
|
||||
|
||||
Reference in new issue
Block a user