Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/agent-memory/archgate-developer/MEMORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Skipping steps 2 or 3 is a workflow violation. The user should NEVER have to inv
- **Git credential tests need system-level isolation on Windows** — Overriding `Bun.env.HOME` is NOT sufficient to isolate `git credential fill/approve` calls in tests. Windows Credential Manager is a system-level API, not file-based. Tests MUST set `Bun.env.GIT_CONFIG_NOSYSTEM = "1"` and `Bun.env.GIT_CONFIG_GLOBAL = <path-to-empty-file>` to prevent git from reading the real credential helper config. Without this, tests on machines with stored credentials will pick up real tokens.
- **GCM prompt suppression requires 5 env vars** — `GIT_TERMINAL_PROMPT=0` alone does NOT prevent Git Credential Manager (GCM) from showing GUI prompts on Windows or askpass prompts on Linux. The full set for `gitCredentialEnv()` in `src/helpers/credential-store.ts` is: `GIT_TERMINAL_PROMPT=0`, `GCM_INTERACTIVE=never`, `GCM_GUI_PROMPT=false`, `GIT_ASKPASS=""`, `SSH_ASKPASS=""`. Omitting any one can trigger unexpected prompts in editor contexts where the CLI runs as a subprocess.
- **Module-level `{ ...Bun.env }` captures env at import time** — Spreading `Bun.env` into a module-level constant freezes the env snapshot. Tests that override `Bun.env.HOME` after import won't affect the constant. Fix: use a function that returns `{ ...Bun.env, ... }` on each call so it picks up test-time overrides. Applied in `src/helpers/credential-store.ts`.
- **`Bun.Glob.scan({ dot: false })` silently drops dot-prefixed segments — even on explicit paths** — `dot: false` (the default) skips matches whose path contains a `.`-prefixed segment, including patterns that explicitly name the dir (e.g. `.github/workflows/release.yml`). Behavior also varies across platforms — Windows reliably drops the match while Linux can match the same pattern, so a rule appears to "work in CI" but no-ops locally. For code repos where `.github/`, `.husky/`, `.vscode/` are first-class source dirs, ALWAYS pass `dot: true` to `Bun.Glob.scan()`. Applied in `src/engine/runner.ts` (`ctx.glob`, `ctx.grepFiles`) and `src/engine/git-files.ts` (`resolveScopedFiles`). See archgate/cli#222.

## Validation Pipeline

Expand Down
5 changes: 4 additions & 1 deletion src/engine/git-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,10 @@ export async function resolveScopedFiles(
patterns.map(async (pattern) => {
const glob = new Bun.Glob(pattern);
const files: string[] = [];
for await (const file of glob.scan({ cwd: projectRoot, dot: false })) {
// dot: true so ADR `files:` globs can target dot-prefixed source dirs
// like `.github/`, `.husky/`, `.vscode/`. The git-tracked-files filter
// below already excludes ignored files. See archgate/cli#222.
for await (const file of glob.scan({ cwd: projectRoot, dot: true })) {
const normalized = file.replaceAll("\\", "/");
if (trackedFiles && !trackedFiles.has(normalized)) continue;
files.push(normalized);
Expand Down
9 changes: 7 additions & 2 deletions src/engine/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,10 @@ function createRuleContext(
safeGlob(pattern);
const g = new Bun.Glob(pattern);
const results: string[] = [];
for await (const file of g.scan({ cwd: projectRoot, dot: false })) {
// dot: true so rules can target dot-prefixed paths like `.github/`,
// `.husky/`, `.vscode/` — first-class source dirs in code repos.
// See https://github.com/archgate/cli/issues/222.
for await (const file of g.scan({ cwd: projectRoot, dot: true })) {
results.push(file.replaceAll("\\", "/"));
}
return results.sort();
Expand Down Expand Up @@ -135,7 +138,9 @@ function createRuleContext(
const g = new Bun.Glob(fileGlob);
const allMatches: GrepMatch[] = [];

for await (const file of g.scan({ cwd: projectRoot, dot: false })) {
// dot: true to match dot-prefixed source dirs (`.github/`, etc.).
// See https://github.com/archgate/cli/issues/222.
for await (const file of g.scan({ cwd: projectRoot, dot: true })) {
const normalized = file.replaceAll("\\", "/");
const absPath = safePath(projectRoot, file);
try {
Expand Down
23 changes: 23 additions & 0 deletions tests/engine/git-files.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,5 +93,28 @@ describe("git-files", () => {
expect(files).toContain("src/foo.ts");
expect(files).not.toContain("src/bar.md");
});

// Regression: archgate/cli#222 — ADR `files:` globs must match
// dot-prefixed source dirs like `.github/`. Bun.Glob with `dot: false`
// silently drops these on Windows, so ADRs scoped to `.github/**` had
// empty scopedFiles on Windows local-dev runs.
test("resolves dot-prefixed paths (regression archgate/cli#222)", async () => {
await git(["init"], tempDir);
mkdirSync(join(tempDir, ".github", "workflows"), { recursive: true });
writeFileSync(
join(tempDir, ".github", "workflows", "release.yml"),
"name: release\n"
);
writeFileSync(
join(tempDir, ".github", "workflows", "ci.yml"),
"name: ci\n"
);
await git(["add", "."], tempDir);
const files = await resolveScopedFiles(tempDir, [
".github/workflows/*.yml",
]);
expect(files).toContain(".github/workflows/release.yml");
expect(files).toContain(".github/workflows/ci.yml");
});
});
});
74 changes: 74 additions & 0 deletions tests/engine/runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,80 @@ describe("runChecks", () => {
expect(foundFiles).toContain("src/b.ts");
});

// Regression: archgate/cli#222 — ctx.glob() must match dot-prefixed source
// dirs like `.github/`, `.husky/`, `.vscode/`. Bun.Glob with `dot: false`
// silently drops these matches on Windows, turning rules targeting
// `.github/workflows/*.yml` into no-ops on local Windows runs while still
// working on Linux CI.
test("glob matches dot-prefixed directories (regression archgate/cli#222)", async () => {
mkdirSync(join(tempDir, ".github", "workflows"), { recursive: true });
writeFileSync(join(tempDir, ".github", "workflows", "release.yml"), "");
writeFileSync(join(tempDir, ".github", "workflows", "ci.yml"), "");

let exactMatch: string[] = [];
let starMatch: string[] = [];
let recursiveMatch: string[] = [];

const loaded = makeLoadedAdr(
{},
{
rules: {
"dot-glob-test": {
description: "Test dot-prefixed glob",
async check(ctx) {
exactMatch = await ctx.glob(".github/workflows/release.yml");
starMatch = await ctx.glob(".github/workflows/*.yml");
recursiveMatch = await ctx.glob(".github/**/*.yml");
},
},
},
}
);

await runChecks(tempDir, [loaded]);
expect(exactMatch).toEqual([".github/workflows/release.yml"]);
expect(starMatch).toContain(".github/workflows/release.yml");
expect(starMatch).toContain(".github/workflows/ci.yml");
expect(recursiveMatch).toContain(".github/workflows/release.yml");
expect(recursiveMatch).toContain(".github/workflows/ci.yml");
});

test("grepFiles matches dot-prefixed directories (regression archgate/cli#222)", async () => {
mkdirSync(join(tempDir, ".github", "workflows"), { recursive: true });
writeFileSync(
join(tempDir, ".github", "workflows", "release.yml"),
"name: release\non: push\n"
);

let matches: Array<{
file: string;
line: number;
column: number;
content: string;
}> = [];

const loaded = makeLoadedAdr(
{},
{
rules: {
"dot-grep-test": {
description: "Test grepFiles dot-prefix",
async check(ctx) {
matches = await ctx.grepFiles(
/release/,
".github/workflows/*.yml"
);
},
},
},
}
);

await runChecks(tempDir, [loaded]);
expect(matches).toHaveLength(1);
expect(matches[0].file).toBe(".github/workflows/release.yml");
});

test("grepFiles helper works in rule context", async () => {
writeFileSync(join(tempDir, "src", "a.ts"), 'const x = "hello";\n');
writeFileSync(join(tempDir, "src", "b.ts"), "const y = 42;\n");
Expand Down
Loading