mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-05 13:15:24 +02:00
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>
This commit is contained in:
Executable
+44
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
# Rewrite a PR/MR title to start with v<NEW_VERSION>.
|
||||
#
|
||||
# Usage: bin/gstack-pr-title-rewrite.sh <NEW_VERSION> <CURRENT_TITLE>
|
||||
# Output: corrected title on stdout.
|
||||
#
|
||||
# Rule: PR titles MUST start with v<NEW_VERSION>. Three cases:
|
||||
# 1. Already starts with "v<NEW_VERSION> " -> no change.
|
||||
# 2. Starts with a different "v<digits and dots> " prefix -> replace prefix.
|
||||
# 3. No version prefix -> prepend "v<NEW_VERSION> ".
|
||||
#
|
||||
# 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
|
||||
# "version 5".
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [ $# -lt 2 ]; then
|
||||
echo "usage: $0 <NEW_VERSION> <CURRENT_TITLE>" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
NEW_VERSION="$1"
|
||||
TITLE="$2"
|
||||
|
||||
# Reject malformed NEW_VERSION early. Real values are dot-separated digits;
|
||||
# anything with shell pattern metacharacters or whitespace is a caller bug.
|
||||
if ! printf '%s' "$NEW_VERSION" | grep -qE '^[0-9]+(\.[0-9]+)*$'; then
|
||||
echo "error: NEW_VERSION must be dot-separated digits, got: $NEW_VERSION" >&2
|
||||
exit 2
|
||||
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).
|
||||
case "$TITLE" in
|
||||
"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"
|
||||
@@ -0,0 +1,54 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user