diff --git a/.github/actions/ci-static-checks/action.yaml b/.github/actions/ci-static-checks/action.yaml index 28af06a9156..c6d4ca8cf9e 100644 --- a/.github/actions/ci-static-checks/action.yaml +++ b/.github/actions/ci-static-checks/action.yaml @@ -33,14 +33,11 @@ runs: shell: bash run: npm run validate:configs + # TypeScript checks and version sync run in the build-typecheck job. - name: Run static hook checks shell: bash run: | - npx prek run --all-files --stage pre-push \ - --skip tsc-plugin \ - --skip tsc-js \ - --skip tsc-cli \ - --skip version-tag-sync \ + npx prek run --all-files --stage pre-commit \ --skip test-cli \ --skip test-plugin \ --skip source-shape-test-budget \ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f66e7ab1b07..e1d5f0dd7e1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,6 +26,11 @@ exclude: ^(nemoclaw/dist/|nemoclaw/node_modules/|docs/_build/|\.venv/|uv\.lock$) +# Stage-less hooks run only while creating a commit. Hooks for later Git +# lifecycle stages declare their own `stages` explicitly below. +default_stages: + - pre-commit + # Which git hook shims `prek install` writes (separate from each hook's `stages:`). # https://prek.j178.dev/configuration/#default_install_hook_types default_install_hook_types: @@ -42,9 +47,11 @@ repos: hooks: - id: trailing-whitespace exclude: ^skills/[^/]+/skill\.oms\.sig$ + stages: [pre-commit] priority: 0 - id: end-of-file-fixer exclude: ^skills/[^/]+/skill\.oms\.sig$ + stages: [pre-commit] priority: 0 - id: mixed-line-ending args: ["--fix=lf"] @@ -98,6 +105,7 @@ repos: - "2" - -ci - -bn + stages: [pre-commit] priority: 5 - repo: local @@ -129,6 +137,7 @@ repos: priority: 10 - id: check-added-large-files args: ["--maxkb=2000"] + stages: [pre-commit] priority: 10 - id: check-case-conflict priority: 10 @@ -141,8 +150,10 @@ repos: - id: detect-private-key priority: 10 - id: check-executables-have-shebangs + stages: [pre-commit] priority: 10 - id: check-shebang-scripts-are-executable + stages: [pre-commit] priority: 10 - repo: local diff --git a/test/pr-workflow-contract.test.ts b/test/pr-workflow-contract.test.ts index ab05afac934..a677d37a6d4 100644 --- a/test/pr-workflow-contract.test.ts +++ b/test/pr-workflow-contract.test.ts @@ -19,6 +19,13 @@ type CodebaseGrowthGuardrailsWorkflow = { jobs: Record; }; +type PrekConfig = { + default_stages?: string[]; + repos: Array<{ + hooks?: Array<{ id: string; stages?: string[] }>; + }>; +}; + const sharedActionPaths = { staticChecks: "./.github/actions/ci-static-checks", buildTypecheck: "./.github/actions/ci-build-typecheck", @@ -129,6 +136,7 @@ function codeFilterMatchesChangedPaths(workflow: CiWorkflow, paths: string[]): b describe("pull request and main workflow contracts", () => { const prWorkflow = readYaml(".github/workflows/pr.yaml"); const mainWorkflow = readYaml(".github/workflows/main.yaml"); + const prekConfig = readYaml(".pre-commit-config.yaml"); const sharedActions = { staticChecks: readYaml(".github/actions/ci-static-checks/action.yaml"), buildTypecheck: readYaml(".github/actions/ci-build-typecheck/action.yaml"), @@ -166,6 +174,28 @@ describe("pull request and main workflow contracts", () => { ).toBe(true); }); + it("keeps ordinary hooks in pre-commit and heavyweight push hooks explicit", () => { + const hooks = prekConfig.repos.flatMap((repo) => repo.hooks ?? []); + const hook = (id: string) => hooks.find((candidate) => candidate.id === id); + + expect(prekConfig.default_stages).toEqual(["pre-commit"]); + expect(hook("test-cli")?.stages).toBeUndefined(); + expect(hook("test-plugin")?.stages).toBeUndefined(); + for (const id of [ + "trailing-whitespace", + "end-of-file-fixer", + "shfmt", + "check-added-large-files", + "check-executables-have-shebangs", + "check-shebang-scripts-are-executable", + ]) { + expect(hook(id)?.stages, id).toEqual(["pre-commit"]); + } + for (const id of ["tsc-plugin", "tsc-js", "tsc-cli", "version-tag-sync"]) { + expect(hook(id)?.stages, id).toEqual(["pre-push"]); + } + }); + it("reuses the same shared CI actions in PR and main workflows", () => { for (const [jobName, stepName, trustedActionPath, mainActionPath] of [ [ @@ -353,7 +383,7 @@ describe("pull request and main workflow contracts", () => { const staticRuns = stepRuns(sharedActions.staticChecks); const staticRunsJoined = staticRuns.join("\n"); const staticPrekRun = staticRuns.find((run) => - run.includes("npx prek run --all-files --stage pre-push"), + run.includes("npx prek run --all-files --stage pre-commit"), ); const buildRuns = stepRuns(sharedActions.buildTypecheck); const cliShardRuns = stepRuns(sharedActions.cliCoverageShard).join("\n"); @@ -363,12 +393,8 @@ describe("pull request and main workflow contracts", () => { expect(staticRuns).toContain("npm install --ignore-scripts"); expect(staticRuns).toContain("npm run validate:configs"); - expect(staticPrekRun).toContain("npx prek run --all-files --stage pre-push"); + expect(staticPrekRun).toContain("npx prek run --all-files --stage pre-commit"); for (const skippedHook of [ - "tsc-plugin", - "tsc-js", - "tsc-cli", - "version-tag-sync", "test-cli", "test-plugin", "source-shape-test-budget", diff --git a/test/sandbox-rlimit-hooks.test.ts b/test/sandbox-rlimit-hooks.test.ts index 211dadf95cf..08ae83a8684 100644 --- a/test/sandbox-rlimit-hooks.test.ts +++ b/test/sandbox-rlimit-hooks.test.ts @@ -57,7 +57,10 @@ function runLoggedDockerShell(command: string, tmp: string) { } function copyRlimitFixture(rlimitLib: string): void { - copyRlimitFixtureWithNprocLimit(rlimitLib, process.platform === "darwin" ? 4096 : 512); + // TEST-ONLY OVERRIDE: production remains 512 in scripts/lib/sandbox-rlimits.sh. + // RLIMIT_NPROC is shared by the real user, so that default can starve this + // test's own shell when Vitest runs many workers concurrently. + copyRlimitFixtureWithNprocLimit(rlimitLib, 4096); } function copyRlimitFixtureWithNprocLimit(rlimitLib: string, limit: number): void { @@ -290,6 +293,12 @@ function expectUnsupportedNprocDoesNotMaskPosixShNoFile(rlimitLib: string): void } describe("sandbox rlimit system hooks (#2173)", () => { + it("keeps the production nproc default at 512", () => { + expect(fs.readFileSync(SANDBOX_RLIMITS, "utf-8")).toMatch( + /^NEMOCLAW_SANDBOX_NPROC_LIMIT=512$/m, + ); + }); + it("rlimit helper enforces supported nofile limits under POSIX sh", () => { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-posix-sh-rlimit-")); const rlimitLib = path.join(tmp, "sandbox-rlimits.sh");