Skip to content

feat(release-promote): auto-discover slug from merged PR roadmap labels#130

Merged
ntatschner merged 1 commit into
nextfrom
feat/release-promote-auto-slug
May 28, 2026
Merged

feat(release-promote): auto-discover slug from merged PR roadmap labels#130
ntatschner merged 1 commit into
nextfrom
feat/release-promote-auto-slug

Conversation

@ntatschner
Copy link
Copy Markdown
Collaborator

Summary

  • scripts/release-promote.mjs now auto-discovers the roadmap slug from roadmap/<slug> labels on PRs merged between the previous track tag and the release's target SHA. Eliminates the last manual operator step in the roadmap-tracking chain.
  • Resolution priority: explicit --roadmap-item-slug <slug> flag → --no-auto-slug opt-out → auto-discover.
  • 13 new tests cover the two new pure functions: parseSlugsFromPrLabels + previousTrackTagBelow.
  • IO glue (discoverSlugFromMergedPrs, resolveSlugForTag) is non-exported; failure-tolerant — any gh or git error returns null and the release proceeds with no annotation (status quo).

Why

After PRs #113 + #125 + #128 + #129, the roadmap-tracking chain looked like:

  1. Feature start → roadmap-tracking skill stashes slug
  2. PR create → pr-roadmap-link skill plants Roadmap-Item: trailer + roadmap/<slug> label on the PR
  3. Release time → operator hand-types --roadmap-item-slug <slug>
  4. CI emit → tag annotation feeds the server

Step 3 was the brittle link. Reviewers hand-off branches; operators forget which feature shipped in which release; the slug placeholder SS could sit in a repo variable for months. The whole chain was designed to make step 3 redundant — this PR cashes that in.

Behaviour matrix

Scenario What auto-discover does
Explicit --roadmap-item-slug X passed Uses X. No discovery.
--no-auto-slug passed Skips discovery. Tag has no annotation.
No prior tag exists for this track Returns null silently. Tag unannotated. Same as today.
gh CLI unavailable / 401 / network error Returns null with a [auto-slug] gh pr list failed warning. Release proceeds.
0 PRs in range have roadmap/* labels Returns null. Logs slug: (none) — N PR(s) since <prevTag>, none with roadmap/* labels.
1 unique slug found Uses it. Logs slug: X (auto-discovered from PRs since <prevTag>).
2+ unique slugs found Refuses to proceed. Prints all candidates + asks operator to pass --roadmap-item-slug X or --no-auto-slug.

The "refuse on ambiguity" branch is deliberate — guessing one of multiple slugs would silently misattribute work; better to demand the operator's explicit pick.

Test plan

  • node --test scripts/release-promote.test.mjs (89/89 — 76 existing + 13 new, all pure-function tests)
  • node scripts/release-promote.mjs --help renders correctly with the new flag + resolution rules
  • node -c scripts/release-promote.mjs syntax clean
  • After merge: cut a real release with a PR carrying roadmap/X label, verify the tag annotation reads Roadmap-Item: X
  • After merge: cut a release with no labeled PRs in range, verify tag is unannotated + log says slug: (none)
  • After merge: cut a release with two labeled PRs (different slugs), verify script refuses and demands --roadmap-item-slug

Pairs with #129 (the pr-roadmap-link skill that plants the labels this script reads). Should land after #129 so the labels exist on PRs before this discovery starts looking for them — but landing it earlier is fine, it just stays silent (zero matches) until the first labeled PR ships.

The roadmap-tracking chain ends with the operator typing
`--roadmap-item-slug <slug>` on the release-promote command. That's
the last manual step — reconstruct "which roadmap item did this
release ship work for?" from human memory at release time, after
the fact. Fragile across reviewer hand-offs.

Replace that manual lookup with discovery: scan PRs merged between
the previous track tag and the release's target SHA, extract their
`roadmap/<slug>` labels (planted by the `pr-roadmap-link` skill at
PR-create time), and use the unique slug as the tag annotation
automatically.

Resolution priority (printed at promote time so the operator sees
the chosen path):
  1. Explicit `--roadmap-item-slug <slug>` — wins, no discovery.
  2. `--no-auto-slug` flag — skip discovery; unsigned tag.
  3. Auto-discover (default):
     - 0 slugs in range → no annotation (status quo)
     - 1 slug → use it; log the discovery source
     - >1 slugs → fail loud; demand explicit choice

Discovery details:
  - Finds the previous track tag via `previousTrackTagBelow`
    (channel-agnostic within the same track; rc.2 finds beta.2,
    new minor's alpha.1 finds previous live).
  - `gh pr list --state merged --base next --search merged:>{date}
    --json number,mergeCommit,labels` — coarse date filter, then
    narrows by `git merge-base --is-ancestor` to PRs whose merge
    commit is in (prevTag, targetSha].
  - `parseSlugsFromPrLabels` extracts unique `roadmap/<slug>`
    suffixes from the PR labels.
  - Network/auth failures degrade gracefully: any gh error skips
    discovery (returns null), and the caller treats that the same
    as "no slug to annotate" — release never blocks on the
    discovery hop.

Two new pure functions exported for testing:
  - `parseSlugsFromPrLabels(prs)` — dedupes slugs across PRs;
    tolerates both gh's object-label shape and string-label shape.
  - `previousTrackTagBelow(tagList, track, currentVersion)` —
    track-scoped, channel-agnostic; returns the highest tag of the
    same track strictly below currentVersion.

13 new tests cover the pure parts; the IO glue is exercised via
the existing live release flow.

Pairs with the `pr-roadmap-link` skill (PR #129). Together they
close the loop: pick slug at feature start → confirm at PR time →
auto-flow at release time → server records the channel transition.
The operator's only remaining decision is "is this PR tracked, and
which slug?" — exactly the kind of intent only a human knows.
@ntatschner ntatschner merged commit 642ee6d into next May 28, 2026
9 checks passed
@ntatschner ntatschner deleted the feat/release-promote-auto-slug branch May 28, 2026 02:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant