From ea7e9448d6fbba8483ca71942665a2d381be1ff7 Mon Sep 17 00:00:00 2001 From: jbetala7 Date: Sat, 6 Jun 2026 12:54:11 +0530 Subject: [PATCH] fix(pr-title): handle bare version titles without duplicating the prefix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gstack-pr-title-rewrite.sh anchored both its no-change case match ("v$NEW_VERSION "*) and its prefix-strip sed (/^v…+ /) on a trailing space, so a title that is a bare version with no description fell through to the prepend case and got a second prefix: v1.2.3.4 + "v1.2.3.4" -> "v1.2.3.4 v1.2.3.4" (should be no change) v1.2.3.4 + "v1.2.3" -> "v1.2.3.4 v1.2.3" (stale prefix kept) This violated the helper's documented case 1 (no change) and case 2 (replace prefix) and its idempotency contract. It is reachable in CI: .github/workflows/pr-title-sync.yml feeds the real PR title through the helper and then `gh pr edit`s the result, so a version-only PR title (the format ship/CHANGELOG uses for branch-ahead bumps) gets corrupted, and it never self-heals (the duplicated form then matches case 1). Match the bare "v$NEW_VERSION" form in the no-change case, strip an existing prefix at a space OR end-of-string, and emit a bare new prefix when the title had no description. Adds 3 regression tests (bare correct, bare different, bare idempotency) that fail on main and pass with the fix. Co-Authored-By: Claude Opus 4.8 (1M context) --- bin/gstack-pr-title-rewrite.sh | 26 +++++++++++++++++++++----- test/pr-title-rewrite.test.ts | 17 +++++++++++++++++ 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/bin/gstack-pr-title-rewrite.sh b/bin/gstack-pr-title-rewrite.sh index 4725ed7205..8251f621f2 100755 --- a/bin/gstack-pr-title-rewrite.sh +++ b/bin/gstack-pr-title-rewrite.sh @@ -5,10 +5,18 @@ # Output: corrected title on stdout. # # Rule: PR titles MUST start with v. Three cases: -# 1. Already starts with "v " -> no change. -# 2. Starts with a different "v " prefix -> replace prefix. +# 1. Already starts with "v" -> no change. +# 2. Starts with a different "v" prefix -> replace prefix. # 3. No version prefix -> prepend "v ". # +# Each version prefix may be followed by a space (then a description) OR sit at +# the end of the title as a bare version with no description (e.g. "v1.2.3", the +# format ship/CHANGELOG uses for version-only bumps). Both forms must be handled +# in cases 1 and 2, otherwise a bare version falls through to case 3 and gets a +# second prefix prepended, e.g. "v1.2.3" -> "v1.2.3.4 v1.2.3". The CI workflow +# .github/workflows/pr-title-sync.yml feeds real PR titles through this and then +# `gh pr edit`s the result, so the duplicated title would be written back. +# # The version-prefix regex matches two or more dot-separated digit segments # (covers v1.2, v1.2.3, v1.2.3.4) so the rule is portable across repos that # use 3-part or 4-part versions, but does NOT strip plain words like @@ -33,12 +41,20 @@ fi # Literal prefix match (case statement is glob-quoted by bash, but our # regex-validated NEW_VERSION has no glob metacharacters so this is safe). +# Match both "v " and a bare "v" title. case "$TITLE" in - "v$NEW_VERSION "*) + "v$NEW_VERSION "*|"v$NEW_VERSION") printf '%s\n' "$TITLE" exit 0 ;; esac -REST=$(printf '%s' "$TITLE" | sed -E 's/^v[0-9]+(\.[0-9]+)+ //') -printf 'v%s %s\n' "$NEW_VERSION" "$REST" +# Strip an existing different version prefix whether it is followed by a space +# (then a description) or sits at the end of the title (bare version). +REST=$(printf '%s' "$TITLE" | sed -E 's/^v[0-9]+(\.[0-9]+)+( |$)//') +if [ -n "$REST" ]; then + printf 'v%s %s\n' "$NEW_VERSION" "$REST" +else + # Title was nothing but a (different) version prefix; emit the bare new one. + printf 'v%s\n' "$NEW_VERSION" +fi diff --git a/test/pr-title-rewrite.test.ts b/test/pr-title-rewrite.test.ts index 28a7b61a24..cfdef402e8 100644 --- a/test/pr-title-rewrite.test.ts +++ b/test/pr-title-rewrite.test.ts @@ -24,6 +24,23 @@ describe('gstack-pr-title-rewrite', () => { expect(rewrite('1.2.3.4', 'v1.2.3 feat: foo').stdout).toBe('v1.2.3.4 feat: foo'); }); + test('bare correct version (no description): no change, not duplicated', () => { + // CHANGELOG/ship uses a version-only title for branch-ahead bumps. It must + // stay as-is, not become "v1.2.3.4 v1.2.3.4". + expect(rewrite('1.2.3.4', 'v1.2.3.4').stdout).toBe('v1.2.3.4'); + }); + + test('bare different version (no description): replaces, not duplicates', () => { + // Must strip the stale prefix even with nothing after it, otherwise CI + // writes back "v1.2.3.4 v1.2.3". + expect(rewrite('1.2.3.4', 'v1.2.3').stdout).toBe('v1.2.3.4'); + }); + + test('idempotent on a bare version title', () => { + const once = rewrite('1.2.3.4', 'v1.2.3').stdout; + expect(rewrite('1.2.3.4', once).stdout).toBe(once); + }); + test('no version prefix: prepends', () => { expect(rewrite('1.2.3.4', 'feat: foo').stdout).toBe('v1.2.3.4 feat: foo'); });