From 878b4b8c4a9a5118fc3d0fbc9f13a2ef140c3719 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Fri, 12 Jun 2026 14:50:22 +0200 Subject: [PATCH] fix: drop the component suffix from the release branch name The stock node strategy always embeds the package name in the release branch (release-please--branches--main--components--agent-tty) even with include-component-in-tag: false. Override getBranchComponent() to follow the tag setting, yielding the plain release-please--branches--main. buildRelease() skips release creation when a merged PR's branch component mismatches the configured one, so any release PR still open on the old component-suffixed branch must be closed (not merged) after this lands; the next push to main recreates it on the new branch. There is no open release PR at the time of writing. Change-Id: I4961eeb13d5149e7d84fb0c74de53875a89d6c1f Co-Authored-By: Claude Fable 5 Signed-off-by: Thomas Kosiewski --- docs/RELEASE-PROCESS.md | 2 +- src/tools/release-please-runner.ts | 17 +++++++++ test/unit/tools/release-please-runner.test.ts | 36 +++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/docs/RELEASE-PROCESS.md b/docs/RELEASE-PROCESS.md index f53e2f3..9516af6 100644 --- a/docs/RELEASE-PROCESS.md +++ b/docs/RELEASE-PROCESS.md @@ -25,7 +25,7 @@ Three workflows cooperate: 1. **Release Please** ([`.github/workflows/release-please.yml`](../.github/workflows/release-please.yml)) runs on every push to `main`. It executes [`src/tools/release-please-runner.ts`](../src/tools/release-please-runner.ts), which runs release-please as a library with Communique registered as the changelog generator (`"changelog-type": "communique"` in [`release-please-config.json`](../release-please-config.json)). Each run: - tags and creates the GitHub Release for any release PR that merged since the last run, then dispatches the **Release** workflow for that tag; - - opens or updates the single release PR (title `chore(release): `, head branch `release-please--branches--main--components--agent-tty`), carrying the `package.json` version bump plus a new `CHANGELOG.md` section written by Communique (`communique generate HEAD --concise`); + - opens or updates the single release PR (title `chore(release): `, head branch `release-please--branches--main`), carrying the `package.json` version bump plus a new `CHANGELOG.md` section written by Communique (`communique generate HEAD --concise`); - dispatches **CI** and **Validate skills** onto the release branch (pushes made with the workflow token never trigger `pull_request` workflows on their own). 2. **Release** ([`.github/workflows/release.yml`](../.github/workflows/release.yml)) is the publish pipeline (see below). It is dispatched by Release Please after the tag exists, and still also triggers on manually pushed `v*` tags. 3. **CI** runs on the release PR like on any other PR and gates the merge. diff --git a/src/tools/release-please-runner.ts b/src/tools/release-please-runner.ts index f881367..5368dbd 100644 --- a/src/tools/release-please-runner.ts +++ b/src/tools/release-please-runner.ts @@ -329,6 +329,23 @@ function joinSections(head: string, entry: string, rest: string): string { * `release-please-config.json` keeps the standard `"release-type": "node"`. */ export class CommuniqueNodeStrategy extends Node { + /** + * The stock strategy always embeds the package name in the release branch + * (`release-please--branches--main--components--agent-tty`), even when + * `include-component-in-tag` is false. Follow the tag setting instead so + * this single-package repo gets the plain `release-please--branches--main`. + * + * Migration note: buildRelease() skips release creation when a merged PR's + * branch component does not match this value, so a release PR opened on + * the old component-suffixed branch must be closed — never merged — once + * this is live; the next push to main recreates it on the new branch. + */ + override async getBranchComponent(): Promise { + return this.includeComponentInTag + ? await super.getBranchComponent() + : undefined; + } + protected override async buildUpdates( options: BuildUpdatesOptions, ): Promise { diff --git a/test/unit/tools/release-please-runner.test.ts b/test/unit/tools/release-please-runner.test.ts index b81e5a3..247bf25 100644 --- a/test/unit/tools/release-please-runner.test.ts +++ b/test/unit/tools/release-please-runner.test.ts @@ -4,6 +4,7 @@ import { describe, expect, it } from 'vitest'; // compatibility contract the runner depends on but release-please does not // document — the merged release PR body must parse back into a version + // notes, otherwise no GitHub Release is created on merge. +import { BranchName } from 'release-please/build/src/util/branch-name.js'; import { PullRequestBody } from 'release-please/build/src/util/pull-request-body.js'; import { PullRequestTitle, @@ -12,6 +13,7 @@ import { import { Version } from 'release-please/build/src/version.js'; import { + CommuniqueNodeStrategy, UnreleasedAwareChangelog, assertLlmCredentials, buildCommuniqueArgs, @@ -358,6 +360,40 @@ describe('createCommuniqueChangelogNotes', () => { }); }); +describe('CommuniqueNodeStrategy branch component', () => { + // The branch component follows include-component-in-tag (the stock strategy + // always embeds the package name in the branch). The fake `this` works + // because the override only touches that flag and, on the true path, the + // stock implementation's `component` field. + it('drops the component when tags do not carry one', async () => { + await expect( + CommuniqueNodeStrategy.prototype.getBranchComponent.call({ + includeComponentInTag: false, + } as unknown as CommuniqueNodeStrategy), + ).resolves.toBeUndefined(); + }); + + it('keeps the stock component when tags carry one', async () => { + await expect( + CommuniqueNodeStrategy.prototype.getBranchComponent.call({ + includeComponentInTag: true, + component: 'agent-tty', + } as unknown as CommuniqueNodeStrategy), + ).resolves.toBe('agent-tty'); + }); + + it('round-trips the short branch name (release-time contract)', () => { + // buildRelease() parses the merged PR's branch and skips the release on a + // component mismatch, so the componentless name must parse back cleanly. + expect(BranchName.ofTargetBranch('main').toString()).toBe( + 'release-please--branches--main', + ); + const parsed = BranchName.parse('release-please--branches--main'); + expect(parsed?.getTargetBranch()).toBe('main'); + expect(parsed?.getComponent()).toBeUndefined(); + }); +}); + describe('workflow outputs', () => { it('maps releases and pull requests into workflow outputs', () => { expect(