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
2 changes: 2 additions & 0 deletions .changeset/calm-nix-builds.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
19 changes: 18 additions & 1 deletion .github/scripts/detect-code-changes.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,36 @@ is_docs_only_path() {
esac
}

is_nix_lock_input_path() {
local path="$1"

case "$path" in
bun.lock | bunfig.toml | package.json | packages/*/package.json | flake.nix | flake.lock | nix/*)
return 0
;;
*)
return 1
;;
esac
}
Comment on lines +31 to +42

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Lock Gate Lacks Tests

The new path allowlist decides whether CI runs the generated-lock consistency check, but no tests cover lock inputs, unrelated paths, or mixed-file diffs. A future path change or pattern regression could silently skip stale-lock detection, so focused positive and negative cases would provide useful regression protection.

Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/scripts/detect-code-changes.sh
Line: 31-42

Comment:
**Lock Gate Lacks Tests**

The new path allowlist decides whether CI runs the generated-lock consistency check, but no tests cover lock inputs, unrelated paths, or mixed-file diffs. A future path change or pattern regression could silently skip stale-lock detection, so focused positive and negative cases would provide useful regression protection.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added integration coverage for every current lock input plus unrelated code, docs-only, and mixed-file diffs. The tests exercise the real detector in temporary Git repositories and assert both GitHub outputs.

Commit: 0ce58ba9

Responded by Pi using openai/gpt-5.6-sol.

This comment was generated by Pi using GPT-5.6 Sol


code_changed=false
nix_lock_changed=false
while IFS= read -r path; do
[[ -z "$path" ]] && continue

if ! is_docs_only_path "$path"; then
code_changed=true
break
fi
if is_nix_lock_input_path "$path"; then
nix_lock_changed=true
fi
# Disable rename detection so code-to-docs renames still expose the removed code path.
done < <(git diff --name-only --no-renames "$base_sha" "$head_sha")

if [[ -n "${GITHUB_OUTPUT:-}" ]]; then
echo "code_changed=$code_changed" >> "$GITHUB_OUTPUT"
echo "nix_lock_changed=$nix_lock_changed" >> "$GITHUB_OUTPUT"
fi

if [[ "$code_changed" == "true" ]]; then
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/nix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ jobs:
runs-on: ubuntu-latest
outputs:
code: ${{ steps.detect.outputs.code_changed }}
nix_lock: ${{ steps.detect.outputs.nix_lock_changed }}
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand Down Expand Up @@ -44,13 +45,16 @@ jobs:
extra-trusted-public-keys = nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=

- name: Verify Nix dependency lockfile
if: needs.changes.outputs.nix_lock == 'true'
run: |
nix run .#update-bun-lock
if ! git diff --exit-code nix/bun.lock.nix; then
echo "::error::Nix lockfile is out of date. Please run 'bun run nix:update-lock' and commit the changes to nix/bun.lock.nix."
exit 1
fi

# The host check realizes bun2nix paths that the following cross-system
# evaluation references, even though that evaluation does not build outputs.
- name: Check flake outputs
run: nix flake check --print-build-logs

Expand Down
104 changes: 104 additions & 0 deletions scripts/quality/detect-code-changes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { afterEach, describe, expect, test } from "bun:test";
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";

const repositoryRoots: string[] = [];
const detectorPath = path.resolve(import.meta.dir, "../../.github/scripts/detect-code-changes.sh");

afterEach(() => {
for (const root of repositoryRoots.splice(0)) rmSync(root, { recursive: true, force: true });
});

/** Run Git in a temporary repository and fail with its diagnostic output. */
function runGit(repositoryRoot: string, args: string[]): string {
const result = Bun.spawnSync(["git", ...args], { cwd: repositoryRoot });
if (result.exitCode !== 0) {
throw new Error(result.stderr.toString() || result.stdout.toString());
}
return result.stdout.toString().trim();
}

/** Evaluate the change detector against files added after an initial commit. */
function detectAddedPaths(paths: string[]): Record<string, string> {
const repositoryRoot = mkdtempSync(path.join(tmpdir(), "hunk-detect-code-changes-"));
const outputPath = path.join(repositoryRoot, "github-output.txt");
repositoryRoots.push(repositoryRoot);

runGit(repositoryRoot, ["init", "--quiet"]);
runGit(repositoryRoot, ["config", "user.email", "tests@hunk.dev"]);
runGit(repositoryRoot, ["config", "user.name", "Hunk Tests"]);
writeFileSync(path.join(repositoryRoot, ".seed"), "initial\n");
runGit(repositoryRoot, ["add", ".seed"]);
runGit(repositoryRoot, ["commit", "--quiet", "-m", "initial"]);
const baseSha = runGit(repositoryRoot, ["rev-parse", "HEAD"]);

for (const changedPath of paths) {
const absolutePath = path.join(repositoryRoot, changedPath);
mkdirSync(path.dirname(absolutePath), { recursive: true });
writeFileSync(absolutePath, `${changedPath}\n`);
}
runGit(repositoryRoot, ["add", "."]);
runGit(repositoryRoot, ["commit", "--quiet", "-m", "changes"]);
const headSha = runGit(repositoryRoot, ["rev-parse", "HEAD"]);

const result = Bun.spawnSync(["bash", detectorPath, baseSha, headSha], {
cwd: repositoryRoot,
env: { ...process.env, GITHUB_OUTPUT: outputPath },
});
if (result.exitCode !== 0) {
throw new Error(result.stderr.toString() || result.stdout.toString());
}

return Object.fromEntries(
readFileSync(outputPath, "utf8")
.trim()
.split("\n")
.map((line) => line.split("=", 2)),
);
}

const nixLockInputs = [
"bun.lock",
"bunfig.toml",
"package.json",
"packages/hunk/package.json",
"flake.nix",
"flake.lock",
"nix/bun.lock.nix",
];

describe("CI code-change detection", () => {
for (const lockInput of nixLockInputs) {
test.skipIf(process.platform === "win32")(`classifies ${lockInput} as a Nix lock input`, () => {
expect(detectAddedPaths([lockInput])).toEqual({
code_changed: "true",
nix_lock_changed: "true",
});
});
}

test.skipIf(process.platform === "win32")(
"does not classify unrelated code as a lock input",
() => {
expect(detectAddedPaths(["packages/hunk/src/main.tsx"])).toEqual({
code_changed: "true",
nix_lock_changed: "false",
});
},
);

test.skipIf(process.platform === "win32")("keeps docs-only changes out of both gates", () => {
expect(detectAddedPaths(["docs/configuration.md"])).toEqual({
code_changed: "false",
nix_lock_changed: "false",
});
});

test.skipIf(process.platform === "win32")("detects a lock input in a mixed-file change", () => {
expect(detectAddedPaths(["docs/configuration.md", "packages/hunk/package.json"])).toEqual({
code_changed: "true",
nix_lock_changed: "true",
});
});
});
Loading