mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 22:14:25 +01:00
fix: win32 NUL discard for the Go build check; unique Health Stack names (rule 23 grant)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
6d2a9e1ad9
commit
2147aeffc3
3 files changed
+47
-5
No files matched your search
+6
-4
@@ -15,7 +15,6 @@
|
||||
*/
|
||||
import { createHash } from 'node:crypto';
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { slugFromEnvironment } from './bin-context';
|
||||
import { resolveStateRoot } from './state-root';
|
||||
@@ -61,14 +60,17 @@ export function healthStack(claudeMd: string): Check[] {
|
||||
if (start < 0) return [];
|
||||
const checks: Check[] = [];
|
||||
const seen = new Map<string, number>();
|
||||
const taken = new Set<string>();
|
||||
for (const line of lines.slice(start + 1)) {
|
||||
if (/^#{1,6}\s/.test(line)) break;
|
||||
const m = /^\s*[-*]\s*([A-Za-z0-9_-]+):\s*(.+?)\s*$/.exec(line);
|
||||
if (!m || ['test', TESTS].includes(m[1].toLowerCase())) continue;
|
||||
const key = m[1].toLowerCase();
|
||||
const n = (seen.get(key) ?? 0) + 1;
|
||||
let n = seen.get(key) ?? 0;
|
||||
let name = key;
|
||||
while (taken.has(name)) name = `${key}-${++n + 1}`;
|
||||
seen.set(key, n);
|
||||
const name = n === 1 ? key : `${key}-${n}`;
|
||||
taken.add(name);
|
||||
checks.push({ name, command: m[2].replace(/`/g, ''), source: 'CLAUDE.md Health Stack' });
|
||||
}
|
||||
return checks;
|
||||
@@ -106,7 +108,7 @@ export function manifestChecks(root: string): Check[] {
|
||||
const cargo = read(path.join(root, 'Cargo.toml'));
|
||||
if (cargo !== null) checks.push({ name: 'build', command: 'cargo check --all-targets', source: 'Cargo.toml' });
|
||||
if (cargo !== null && (/^\[(workspace\.)?lints\.clippy\]/m.test(cargo) || ['clippy.toml', '.clippy.toml'].some(f => fs.existsSync(path.join(root, f))))) checks.push({ name: 'lint', command: 'cargo clippy --all-targets', source: 'Cargo.toml' });
|
||||
if (fs.existsSync(path.join(root, 'go.mod'))) checks.push({ name: 'vet', command: 'go vet ./...', source: 'go.mod' }, { name: 'build', command: `go build -o ${os.devNull} ./...`, source: 'go.mod' });
|
||||
if (fs.existsSync(path.join(root, 'go.mod'))) checks.push({ name: 'vet', command: 'go vet ./...', source: 'go.mod' }, { name: 'build', command: `go build -o ${process.platform === 'win32' ? 'NUL' : '/dev/null'} ./...`, source: 'go.mod' });
|
||||
const seen = new Set<string>();
|
||||
return checks.filter(c => (seen.has(c.name) ? false : (seen.add(c.name), true)));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* The Go build check gstack-checks detects runs through the same bash invocation pregate's supervise uses (Git Bash on
|
||||
* Windows), so its discard path must survive bash: os.devNull on Windows is \\.\nul, which bash mangles to \.nul.
|
||||
* Kept out of test/gstack-checks.test.ts so a lane with Go can run it on its own. Skips where Go is not installed.
|
||||
*/
|
||||
import { afterAll, describe, expect, test } from 'bun:test';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { manifestChecks } from '../lib/checks';
|
||||
import { bashCommandInvocation } from '../lib/script-invocation';
|
||||
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-checks-go-'));
|
||||
afterAll(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
||||
const write = (rel: string, text: string) => { fs.mkdirSync(path.dirname(path.join(tmp, rel)), { recursive: true }); fs.writeFileSync(path.join(tmp, rel), text); };
|
||||
const files = (dir: string): string[] => fs.readdirSync(dir, { withFileTypes: true }).flatMap(e => e.isDirectory() ? files(path.join(dir, e.name)).map(f => `${e.name}/${f}`) : [e.name]).sort();
|
||||
|
||||
describe('the detected Go build check', () => {
|
||||
// Value: protects=the build check passes and writes nothing on a one-main module and a three-main module;
|
||||
// fails_when=the discard path is mangled (a false FAIL on several mains) or dropped (a binary lands in the tree);
|
||||
// why_new=the -o discard is new; seam=the production bash invocation; no_claim=Go toolchains without go on PATH
|
||||
test.skipIf(!Bun.which('go'))('builds every package through bash and leaves the tree unchanged', () => {
|
||||
// One root main package (go build would write ./x) and three main packages (a mangled -o path fails with several).
|
||||
for (const [mod, mains] of [['one', ['main.go']], ['many', ['main.go', 'cmd/a/main.go', 'cmd/b/main.go']]] as const) {
|
||||
const dir = path.join(tmp, mod);
|
||||
write(`${mod}/go.mod`, `module example.com/${mod}\n\ngo 1.21\n`);
|
||||
for (const rel of mains) write(`${mod}/${rel}`, 'package main\n\nfunc main() {}\n');
|
||||
const before = files(dir);
|
||||
const build = manifestChecks(dir).find(c => c.name === 'build')!;
|
||||
const inv = bashCommandInvocation(build.command);
|
||||
if (!inv.ok) throw new Error(inv.error);
|
||||
const r = spawnSync(inv.cmd, inv.argv, { cwd: dir, encoding: 'utf8', timeout: 120_000, env: { ...process.env, GOWORK: 'off' } });
|
||||
expect(`${r.stdout}${r.stderr}`).toBe('');
|
||||
expect(r.status).toBe(0);
|
||||
expect(files(dir)).toEqual(before);
|
||||
}
|
||||
}, 150_000);
|
||||
});
|
||||
@@ -60,6 +60,7 @@ describe('detection', () => {
|
||||
.toEqual([['typecheck', 'tsc --noEmit'], ['shell', 'shellcheck *.sh']]);
|
||||
// A repeated key keeps both checks under their own names, and a tests row is pregate's, so status never reads one verdict for two checks.
|
||||
expect(healthStack('## Health Stack\n- lint: eslint .\n- lint: stylelint "**/*.css"\n- lint: oxlint\n- tests: npm test\n').map(c => c.name)).toEqual(['lint', 'lint-2', 'lint-3']);
|
||||
expect(healthStack('## Health Stack\n- lint: eslint .\n- lint-2: oxlint\n- lint: stylelint x\n').map(c => c.name)).toEqual(['lint', 'lint-2', 'lint-3']);
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-checks-manifest-'));
|
||||
dirs.push(dir);
|
||||
fs.writeFileSync(path.join(dir, 'tsconfig.json'), '{}');
|
||||
@@ -68,7 +69,7 @@ describe('detection', () => {
|
||||
fs.writeFileSync(path.join(dir, 'bun.lock'), '');
|
||||
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ scripts: { lint: 'eslint .' }, devDependencies: { typescript: '5' } }));
|
||||
fs.writeFileSync(path.join(dir, 'go.mod'), 'module x\n');
|
||||
expect(manifestChecks(dir).map(c => c.command)).toEqual(['bunx tsc --noEmit', 'bun run lint', 'go vet ./...', `go build -o ${os.devNull} ./...`]);
|
||||
expect(manifestChecks(dir).map(c => c.command)).toEqual(['bunx tsc --noEmit', 'bun run lint', 'go vet ./...', `go build -o ${process.platform === 'win32' ? 'NUL' : '/dev/null'} ./...`]);
|
||||
// Clippy is a rustup component Cargo.toml does not declare: it runs only when the repo opts in.
|
||||
const rs = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-checks-rs-'));
|
||||
dirs.push(rs);
|
||||
|
||||
Reference in new issue
Block a user