mirror of https://github.com/garrytan/gstack.git
Merge 3e33b88dfc into 2be6c06ba8
This commit is contained in:
commit
f5115a4f5f
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* bin-context — tiny shared helpers for non-interactive gstack bins that need the
|
||||
* bin-context -- tiny shared helpers for non-interactive gstack bins that need the
|
||||
* project slug, current branch, and argv flags. Extracted from the decision bins
|
||||
* (gstack-decision-log / gstack-decision-search) so the slug/branch/flag plumbing
|
||||
* lives in one audited place instead of being copy-pasted per bin.
|
||||
|
|
@ -9,9 +9,44 @@ import { spawnSync } from "child_process";
|
|||
|
||||
/** Resolve the project slug via the `gstack-slug` helper (parses `SLUG=...`). */
|
||||
export function resolveSlug(slugBinPath: string): string {
|
||||
const r = spawnSync(slugBinPath, { encoding: "utf-8" });
|
||||
const m = (r.stdout || "").match(/^SLUG=(.+)$/m);
|
||||
return m ? m[1].trim() : "unknown";
|
||||
const parse = (out: string | null): string | undefined =>
|
||||
(out || "").match(/^SLUG=(.+)$/m)?.[1].trim();
|
||||
|
||||
// Direct call first, so POSIX behaviour is unchanged.
|
||||
const direct = spawnSync(slugBinPath, { encoding: "utf-8" });
|
||||
const fromDirect = parse(direct.stdout);
|
||||
if (fromDirect) return fromDirect;
|
||||
|
||||
// `gstack-slug` is an extension-less bash script. On Windows, spawnSync
|
||||
// without a shell goes through CreateProcess, which does not honour
|
||||
// shebangs: the call fails with ENOENT and stdout is null. `shell: true`
|
||||
// does not help either -- cmd.exe has no association for an extension-less
|
||||
// file. Naming the interpreter is what works. Retry whenever the direct
|
||||
// call yielded no slug rather than only on ENOENT, so a shim that exits
|
||||
// non-zero without output is covered too. bash specifically, not sh:
|
||||
// gstack-slug uses `[[ ]]` and `set -o pipefail`.
|
||||
const viaBash = spawnSync("bash", [slugBinPath], { encoding: "utf-8" });
|
||||
const fromBash = parse(viaBash.stdout);
|
||||
if (fromBash) return fromBash;
|
||||
|
||||
// A tooling failure is not an anonymous project, and a mute fallback is what
|
||||
// let this live: callers then read and write ~/.gstack/projects/unknown/ --
|
||||
// one shared bucket for every repo on the machine on the write side, and on
|
||||
// the read side a directory that does not exist, so the search returns an
|
||||
// empty list and exits 0. The session is told there are no prior decisions
|
||||
// and re-litigates settled calls in good faith. Keep the fallback; never
|
||||
// keep it quiet.
|
||||
const reason = (r: typeof direct): string =>
|
||||
(r.error as NodeJS.ErrnoException | undefined)?.code ??
|
||||
r.error?.message ??
|
||||
`exited ${r.status}`;
|
||||
process.stderr.write(
|
||||
`gstack: could not resolve the project slug from ${slugBinPath} ` +
|
||||
`(direct: ${reason(direct)}; via bash: ${reason(viaBash)}). ` +
|
||||
`Falling back to "unknown" -- decisions will read and write ` +
|
||||
`~/.gstack/projects/unknown/ instead of this project.\n`,
|
||||
);
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
/** Current git branch, or undefined on detached HEAD / outside a repo. */
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
/**
|
||||
* resolveSlug portability guard (free).
|
||||
*
|
||||
* `bin/gstack-slug` is an extension-less bash script. On Windows, spawnSync
|
||||
* without a shell goes through CreateProcess, which does not honour shebangs:
|
||||
* the direct call fails with ENOENT, stdout is null, and resolveSlug used to
|
||||
* return the literal "unknown". Both consumers -- gstack-decision-log and
|
||||
* gstack-decision-search -- then read and write ~/.gstack/projects/unknown/.
|
||||
* On the write side that is one anonymous bucket shared by every repo on the
|
||||
* machine; on the read side the directory does not exist, so the search
|
||||
* returns an empty list and exits 0. The session is told there are no prior
|
||||
* decisions and re-litigates settled calls in good faith.
|
||||
*
|
||||
* Two things are pinned here, because the bug had two halves:
|
||||
*
|
||||
* 1. the fallback -- an extension-less shebang script must resolve on every
|
||||
* platform, which is what the `bash <script>` retry buys;
|
||||
* 2. the diagnosis -- a resolution failure must be audible. A mute fallback
|
||||
* is what let this live long enough to corrupt sessions' reasoning.
|
||||
*/
|
||||
|
||||
import { describe, test, expect } from 'bun:test';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { resolveSlug } from '../lib/bin-context';
|
||||
|
||||
/** A stand-in for bin/gstack-slug: shebang script, deliberately no extension. */
|
||||
function fakeSlugBin(body: string): string {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-slug-'));
|
||||
const bin = path.join(dir, 'gstack-slug-fake');
|
||||
fs.writeFileSync(bin, body);
|
||||
fs.chmodSync(bin, 0o755);
|
||||
return bin;
|
||||
}
|
||||
|
||||
describe('resolveSlug', () => {
|
||||
test('resolves an extension-less shebang script on every platform', () => {
|
||||
const bin = fakeSlugBin('#!/usr/bin/env bash\necho "SLUG=demo-project"\necho "BRANCH=main"\n');
|
||||
expect(resolveSlug(bin)).toBe('demo-project');
|
||||
});
|
||||
|
||||
test('warns on stderr rather than returning "unknown" in silence', () => {
|
||||
const missing = path.join(os.tmpdir(), 'gstack-slug-that-does-not-exist-9f3a');
|
||||
const seen: string[] = [];
|
||||
const original = process.stderr.write.bind(process.stderr);
|
||||
(process.stderr as unknown as { write: unknown }).write = (chunk: unknown) => {
|
||||
seen.push(String(chunk));
|
||||
return true;
|
||||
};
|
||||
try {
|
||||
// The fallback value is kept, so no caller breaks...
|
||||
expect(resolveSlug(missing)).toBe('unknown');
|
||||
} finally {
|
||||
(process.stderr as unknown as { write: unknown }).write = original;
|
||||
}
|
||||
// ...but it must never be silent.
|
||||
expect(seen.join('')).toContain('could not resolve the project slug');
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue