mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-02 03:35:09 +02:00
7efa85cb4f
* feat: add bin/gstack-pr-title-rewrite.sh shared helper Single source of truth for "rewrite a PR title to start with v<VERSION>". Three cases: already correct (no-op), different prefix (replace), no prefix (prepend). Rejects malformed VERSION (anything outside ^[0-9]+(\.[0-9]+)*$) with exit code 2. Uses literal case prefix match instead of bash's pattern- matching # operator so a VERSION with glob metacharacters cannot mismatch. Free bun test covers the four branches plus malformed-input rejection, plain-words-not-stripped, single-segment-not-stripped, idempotence, and missing-args. 9 tests, ~400ms. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(skills): /ship and /document-release always prefix PR titles with v<VERSION> ship/SKILL.md.tmpl Step 19: idempotency block now always rewrites titles to start with v$NEW_VERSION via the new helper. Removes the "custom title kept intentionally" loophole that let unprefixed titles persist forever. Adds a post-edit self-check that re-fetches the title and retries once if the edit didn't stick. Inline comments on the create-PR snippets at lines 867 and 876 make the rule unmissable. document-release/SKILL.md.tmpl Step 9: new "PR/MR title sync" sub-step calls the same helper after the body update. Catches the case where Step 8 bumped VERSION after /ship had already created the PR — title now follows VERSION instead of going stale. Golden fixtures regenerated for claude/codex/factory ship variants. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ci): pr-title-sync rewrites titles unconditionally Drops the "eligible only if already prefixed" gate. Sources the new shared helper, rewrites unconditionally on every VERSION change. Defense-in-depth backstop for PRs opened outside the skills (manual gh pr create, web UI). Uses env: for OLD_TITLE so YAML expression injection cannot reach run:. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: bump version and changelog (v1.23.0.0) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
55 lines
2.0 KiB
TypeScript
55 lines
2.0 KiB
TypeScript
import { describe, test, expect } from 'bun:test';
|
|
import { spawnSync } from 'child_process';
|
|
import * as path from 'path';
|
|
|
|
const HELPER = path.join(import.meta.dir, '..', 'bin', 'gstack-pr-title-rewrite.sh');
|
|
|
|
function rewrite(version: string, title: string): { stdout: string; status: number; stderr: string } {
|
|
const r = spawnSync(HELPER, [version, title], { encoding: 'utf-8' });
|
|
return { stdout: (r.stdout ?? '').trimEnd(), status: r.status ?? -1, stderr: r.stderr ?? '' };
|
|
}
|
|
|
|
describe('gstack-pr-title-rewrite', () => {
|
|
test('already correct: no change', () => {
|
|
const r = rewrite('1.2.3.4', 'v1.2.3.4 feat: foo');
|
|
expect(r.status).toBe(0);
|
|
expect(r.stdout).toBe('v1.2.3.4 feat: foo');
|
|
});
|
|
|
|
test('different version prefix: replaces it', () => {
|
|
expect(rewrite('1.2.3.5', 'v1.2.3.4 feat: foo').stdout).toBe('v1.2.3.5 feat: foo');
|
|
});
|
|
|
|
test('different prefix length (3-part vs 4-part): replaces it', () => {
|
|
expect(rewrite('1.2.3.4', 'v1.2.3 feat: foo').stdout).toBe('v1.2.3.4 feat: foo');
|
|
});
|
|
|
|
test('no version prefix: prepends', () => {
|
|
expect(rewrite('1.2.3.4', 'feat: foo').stdout).toBe('v1.2.3.4 feat: foo');
|
|
});
|
|
|
|
test('does not mistake plain words for a prefix', () => {
|
|
expect(rewrite('1.2.3.4', 'version 5 feature').stdout).toBe('v1.2.3.4 version 5 feature');
|
|
});
|
|
|
|
test('does not strip a single-segment prefix like v1', () => {
|
|
expect(rewrite('1.2.3.4', 'v1 feat: foo').stdout).toBe('v1.2.3.4 v1 feat: foo');
|
|
});
|
|
|
|
test('errors on missing args', () => {
|
|
const r = spawnSync(HELPER, ['1.2.3.4'], { encoding: 'utf-8' });
|
|
expect(r.status).not.toBe(0);
|
|
});
|
|
|
|
test('rejects malformed VERSION with shell metacharacters', () => {
|
|
expect(rewrite('1.*.*.*', 'feat: foo').status).toBe(2);
|
|
expect(rewrite('1.2.3.4; rm -rf /', 'feat: foo').status).toBe(2);
|
|
});
|
|
|
|
test('idempotent: applying twice yields the same result', () => {
|
|
const once = rewrite('1.2.3.4', 'feat: foo').stdout;
|
|
const twice = rewrite('1.2.3.4', once).stdout;
|
|
expect(twice).toBe(once);
|
|
});
|
|
});
|