From 5c434ee7d6d76c36cd1ef33b1b9ceb40a0719c60 Mon Sep 17 00:00:00 2001 From: auyua9 Date: Mon, 14 Sep 2026 06:25:19 +0800 Subject: [PATCH 1/4] test(publish): pin the rc-guard release-subject skip regex The rc-guard release-PR skip is load-bearing (prevents an RC build racing the imminent stable-tag push on the release commit; the v1.6.4 race history is documented in the workflow comments) and its subject regex is subtle: nocasematch, anchored, optional (#NNNN) squash-merge suffix, full semver required. Add a unit test that extracts RELEASE_SUBJECT_RE from publish.yml (failing loudly if the Decide step changes) and exercises it under the same bash semantics via a child bash, following the build-web-optin.test.ts precedent of reading the workflow from tests. --- gitnexus/test/unit/publish-rc-guard.test.ts | 69 +++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 gitnexus/test/unit/publish-rc-guard.test.ts diff --git a/gitnexus/test/unit/publish-rc-guard.test.ts b/gitnexus/test/unit/publish-rc-guard.test.ts new file mode 100644 index 000000000..6cdf0047c --- /dev/null +++ b/gitnexus/test/unit/publish-rc-guard.test.ts @@ -0,0 +1,69 @@ +import { readFileSync } from 'node:fs'; +import { execFileSync } from 'node:child_process'; +import path from 'node:path'; +import { describe, expect, it } from 'vitest'; + +/** + * Regression coverage for the rc-guard release-PR skip in + * .github/workflows/publish.yml. The `chore: release vX.Y.Z` subject match is + * load-bearing: it prevents an RC build firing on the release-PR commit from + * racing the imminent stable-tag push on the same SHA (see the v1.6.4 race + * history referenced in the workflow comments). This test pins the exact + * regex shipped in the workflow by extracting it from the YAML and running it + * under the same bash semantics (`shopt -s nocasematch`, anchored POSIX ERE). + */ +const REPO_ROOT = path.resolve(__dirname, '../../..'); +const WORKFLOW = path.join(REPO_ROOT, '.github/workflows/publish.yml'); + +function releaseSubjectRegex(): string { + const yaml = readFileSync(WORKFLOW, 'utf8'); + const match = yaml.match(/RELEASE_SUBJECT_RE='([^']+)'/); + if (!match) { + throw new Error('RELEASE_SUBJECT_RE not found in publish.yml — did the rc-guard Decide step change?'); + } + return match[1]; +} + +function subjectMatches(subject: string, regex: string): boolean { + const script = [ + 'set -euo pipefail', + 'shopt -s nocasematch', + `SUBJECT=${JSON.stringify(subject)}`, + `REGEX=${JSON.stringify(regex)}`, + '[[ "$SUBJECT" =~ $REGEX ]] && echo MATCH || echo NO_MATCH', + 'shopt -u nocasematch', + ].join('\n'); + const out = execFileSync('bash', ['-c', script], { encoding: 'utf8' }).trim(); + return out === 'MATCH'; +} + +describe('rc-guard release-subject regex (publish.yml)', () => { + const regex = releaseSubjectRegex(); + + it('matches canonical release subjects', () => { + expect(subjectMatches('chore: release v1.6.4', regex)).toBe(true); + expect(subjectMatches('chore: release v10.20.30', regex)).toBe(true); + }); + + it('matches squash-merge subjects with the (#NNNN) suffix', () => { + expect(subjectMatches('chore: release v1.6.4 (#1474)', regex)).toBe(true); + }); + + it('stays case-insensitive for IDE auto-capitalization', () => { + expect(subjectMatches('Chore: Release v1.2.3', regex)).toBe(true); + }); + + it('does not match ordinary chore commits', () => { + expect(subjectMatches('chore: bump deps (#1500)', regex)).toBe(false); + }); + + it('does not match release-like subjects with extra suffixes or prefixes', () => { + expect(subjectMatches('chore: release v1.6.4 hotfix', regex)).toBe(false); + expect(subjectMatches('revert: chore: release v1.6.4', regex)).toBe(false); + }); + + it('requires a full semver', () => { + expect(subjectMatches('chore: release v1.6', regex)).toBe(false); + expect(subjectMatches('chore: release v1.6.x', regex)).toBe(false); + }); +}); From 4a7ecfef5f59c4ea9ed0e1c4e63659bd181ff44f Mon Sep 17 00:00:00 2001 From: auyua9 Date: Mon, 14 Sep 2026 07:10:52 +0800 Subject: [PATCH 2/4] test(publish): register rc-guard test in the cross-platform suite The rc-guard regression test spawns the POSIX-only bash executable, so per the suite's own policy it must run on the Windows/macOS matrix rather than only the Ubuntu full suite. Register it in SPAWN_CLI. --- gitnexus/scripts/cross-platform-tests.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gitnexus/scripts/cross-platform-tests.ts b/gitnexus/scripts/cross-platform-tests.ts index 07b10fe25..a69fac087 100644 --- a/gitnexus/scripts/cross-platform-tests.ts +++ b/gitnexus/scripts/cross-platform-tests.ts @@ -267,6 +267,10 @@ const SPAWN_CLI = [ // tree-sitter grammars at import time. The offender regexes match grammar // paths with either separator, which only the Windows runner proves. 'test/integration/optional-grammars/registry-import-closure.test.ts', + // Spawns `bash` to exercise the rc-guard release-subject regex under real + // nocasematch semantics; bash availability/behavior differs on Windows and + // macOS runners, so the suite must cover it beyond the Ubuntu full run. + 'test/unit/publish-rc-guard.test.ts', ]; // Worker threads tests — exercise real worker_threads which have From 822fb83cb4a6808cdefd00fe9ea5cb1c783eefba Mon Sep 17 00:00:00 2001 From: auyua9 Date: Mon, 14 Sep 2026 09:26:45 +0800 Subject: [PATCH 3/4] style: apply prettier to the rc-guard regression test --- gitnexus/test/unit/publish-rc-guard.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gitnexus/test/unit/publish-rc-guard.test.ts b/gitnexus/test/unit/publish-rc-guard.test.ts index 6cdf0047c..b7b7e993c 100644 --- a/gitnexus/test/unit/publish-rc-guard.test.ts +++ b/gitnexus/test/unit/publish-rc-guard.test.ts @@ -19,7 +19,9 @@ function releaseSubjectRegex(): string { const yaml = readFileSync(WORKFLOW, 'utf8'); const match = yaml.match(/RELEASE_SUBJECT_RE='([^']+)'/); if (!match) { - throw new Error('RELEASE_SUBJECT_RE not found in publish.yml — did the rc-guard Decide step change?'); + throw new Error( + 'RELEASE_SUBJECT_RE not found in publish.yml — did the rc-guard Decide step change?', + ); } return match[1]; } From d564e1bbf2f241d7ef1f95c9d2752b10652d66e0 Mon Sep 17 00:00:00 2001 From: auyua9 Date: Sat, 19 Sep 2026 01:21:57 +0800 Subject: [PATCH 4/4] test(publish): probe for a nocasematch-capable bash instead of assuming one The rc-guard suite spawned bare 'bash', which on an ordinary Windows PATH resolves to the WSL launcher and fails all six cases when no distribution is installed. Resolve candidates with a real nocasematch capability probe (GITNEXUS_TEST_BASH override, then Git for Windows bash.exe on win32, then PATH bash), skip the suite with an explicit reason when none passes, keep an always-on probe self-test, and document the prerequisite in CONTRIBUTING.md. --- CONTRIBUTING.md | 9 ++ gitnexus/test/unit/publish-rc-guard.test.ts | 114 ++++++++++++++++---- 2 files changed, 100 insertions(+), 23 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 23a6ae28c..2f68d8c51 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,6 +15,15 @@ This project uses the [PolyForm Noncommercial License 1.0.0](https://polyformpro **Prerequisites:** Node.js — `gitnexus/` requires `^22.18.0 || >=24.11.0` and `gitnexus-web/` requires `^20.19.0 || >=22.12.0` (enforced via the `engines` field in each package). Use `nvm install` to match the local version. +One unit test (`gitnexus/test/unit/publish-rc-guard.test.ts`) also spawns a +real `bash` to exercise the release-subject regex under `nocasematch` +semantics. On Linux and macOS the PATH `bash` is used as-is. On Windows the +test probes Git for Windows' `bash.exe` (both the Program Files and the +per-user install) before falling back to the PATH entry, because the PATH +`bash` is frequently the WSL launcher. The suite skips with an explicit +message when no suitable bash is found; point `GITNEXUS_TEST_BASH` at a +specific executable to override discovery. + 1. Clone the repository. 2. **Shared package:** `cd gitnexus-shared && npm install && npm run build` 3. **CLI / MCP package:** `cd ../gitnexus && npm install && npm run build` diff --git a/gitnexus/test/unit/publish-rc-guard.test.ts b/gitnexus/test/unit/publish-rc-guard.test.ts index b7b7e993c..27ca147a0 100644 --- a/gitnexus/test/unit/publish-rc-guard.test.ts +++ b/gitnexus/test/unit/publish-rc-guard.test.ts @@ -35,37 +35,105 @@ function subjectMatches(subject: string, regex: string): boolean { '[[ "$SUBJECT" =~ $REGEX ]] && echo MATCH || echo NO_MATCH', 'shopt -u nocasematch', ].join('\n'); - const out = execFileSync('bash', ['-c', script], { encoding: 'utf8' }).trim(); + const out = execFileSync(BASH, ['-c', script], { encoding: 'utf8' }).trim(); return out === 'MATCH'; } -describe('rc-guard release-subject regex (publish.yml)', () => { - const regex = releaseSubjectRegex(); +// Probe script: only a real bash with nocasematch semantics (the behavior +// this suite pins) prints BASH_OK. A Windows PATH `bash` that is actually +// the WSL launcher exits with an error when no distribution is installed, +// so it fails this probe instead of failing six unit tests. +const BASH_PROBE = 'shopt -s nocasematch; [[ "Chore" =~ ^chore$ ]] && echo BASH_OK'; - it('matches canonical release subjects', () => { - expect(subjectMatches('chore: release v1.6.4', regex)).toBe(true); - expect(subjectMatches('chore: release v10.20.30', regex)).toBe(true); - }); +function probeBash(candidate: string): boolean { + try { + const out = execFileSync(candidate, ['-c', BASH_PROBE], { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + }).trim(); + return out === 'BASH_OK'; + } catch { + return false; + } +} - it('matches squash-merge subjects with the (#NNNN) suffix', () => { - expect(subjectMatches('chore: release v1.6.4 (#1474)', regex)).toBe(true); - }); +// Resolve a bash executable that can actually run the nocasematch ERE +// semantics this suite extracts from publish.yml. Candidates are probed in +// order; the first one that passes the nocasematch probe wins: +// 1. GITNEXUS_TEST_BASH (explicit override for unusual installs) +// 2. on win32, Git for Windows' bash.exe — probed before the PATH entry +// because the PATH `bash` on Windows is frequently the System32 WSL +// launcher (both the Program Files and the per-user install) +// 3. plain `bash` from PATH (the POSIX default) +// When no candidate passes, the suite below skips with an explicit reason +// instead of failing: CONTRIBUTING lists Node.js as the prerequisite, so a +// contributor without any suitable bash is supported, not broken. +function resolveBash(): string | null { + const candidates: string[] = []; + const override = process.env['GITNEXUS_TEST_BASH']; + if (override) { + candidates.push(override); + } + if (process.platform === 'win32') { + const programFiles = process.env['ProgramFiles']; + const localAppData = process.env['LocalAppData']; + if (programFiles) { + candidates.push(path.join(programFiles, 'Git', 'bin', 'bash.exe')); + } + if (localAppData) { + candidates.push(path.join(localAppData, 'Programs', 'Git', 'bin', 'bash.exe')); + } + } + candidates.push('bash'); + for (const candidate of candidates) { + if (probeBash(candidate)) { + return candidate; + } + } + return null; +} - it('stays case-insensitive for IDE auto-capitalization', () => { - expect(subjectMatches('Chore: Release v1.2.3', regex)).toBe(true); - }); +const BASH = resolveBash(); - it('does not match ordinary chore commits', () => { - expect(subjectMatches('chore: bump deps (#1500)', regex)).toBe(false); - }); +describe.skipIf(!BASH)( + `rc-guard release-subject regex (publish.yml)${BASH ? '' : ' — skipped: no bash with nocasematch semantics found (install Git for Windows or point GITNEXUS_TEST_BASH at one)'}`, + () => { + const regex = releaseSubjectRegex(); - it('does not match release-like subjects with extra suffixes or prefixes', () => { - expect(subjectMatches('chore: release v1.6.4 hotfix', regex)).toBe(false); - expect(subjectMatches('revert: chore: release v1.6.4', regex)).toBe(false); - }); + it('matches canonical release subjects', () => { + expect(subjectMatches('chore: release v1.6.4', regex)).toBe(true); + expect(subjectMatches('chore: release v10.20.30', regex)).toBe(true); + }); - it('requires a full semver', () => { - expect(subjectMatches('chore: release v1.6', regex)).toBe(false); - expect(subjectMatches('chore: release v1.6.x', regex)).toBe(false); + it('matches squash-merge subjects with the (#NNNN) suffix', () => { + expect(subjectMatches('chore: release v1.6.4 (#1474)', regex)).toBe(true); + }); + + it('stays case-insensitive for IDE auto-capitalization', () => { + expect(subjectMatches('Chore: Release v1.2.3', regex)).toBe(true); + }); + + it('does not match ordinary chore commits', () => { + expect(subjectMatches('chore: bump deps (#1500)', regex)).toBe(false); + }); + + it('does not match release-like subjects with extra suffixes or prefixes', () => { + expect(subjectMatches('chore: release v1.6.4 hotfix', regex)).toBe(false); + expect(subjectMatches('revert: chore: release v1.6.4', regex)).toBe(false); + }); + + it('requires a full semver', () => { + expect(subjectMatches('chore: release v1.6', regex)).toBe(false); + expect(subjectMatches('chore: release v1.6.x', regex)).toBe(false); + }); + }, +); + +// Runs on every platform, including machines where the suite above skips: +// proves the capability probe actually rejects an unusable candidate instead +// of silently treating every spawn failure as "bash found". +describe('rc-guard bash resolution probe', () => { + it('rejects a candidate that cannot run the nocasematch probe', () => { + expect(probeBash('gitnexus-definitely-not-a-shell')).toBe(false); }); });