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
3 changes: 0 additions & 3 deletions .github/actionlint.yaml

This file was deleted.

4 changes: 2 additions & 2 deletions .github/workflows/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
# PR/manual checks use macOS because static.ts needs plutil. Direct pushes
# require mise run verify locally; a green release job does not prove tests.
if: ${{ github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' }}
runs-on: blacksmith-6vcpu-macos-latest
runs-on: macos-26
timeout-minutes: 20
permissions:
contents: read
Expand Down Expand Up @@ -49,7 +49,7 @@ jobs:
release:
name: GitHub Release
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && !contains(github.event.head_commit.message, '[skip ci]') }}
runs-on: blacksmith-2vcpu-ubuntu-2404-arm
runs-on: ubuntu-24.04-arm
timeout-minutes: 15
environment: release
permissions:
Expand Down
8 changes: 7 additions & 1 deletion docs/github-pipelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,15 @@ creates tag-only GitHub Releases.

| Workflow | Trigger | Contract |
| --- | --- | --- |
| Verify | Push to `main`, pull request, manual dispatch | Run every deterministic domain in parallel through `./scripts/verify/run.ts --skip-security` on macOS. Pushes to `main` skip this job and run only release evaluation. |
| Verify | Push to `main`, pull request, manual dispatch | Run every deterministic domain with bounded concurrency through `./scripts/verify/run.ts --skip-security` on macOS. Pushes to `main` skip this job and run only release evaluation. |
| Scan | Pull request, weekly schedule, manual dispatch | Call the shared `uinaf/.github` scan workflow: Gitleaks, TruffleHog, Actionlint, and Zizmor against full Git history. |

This public repository uses standard GitHub-hosted runners: `macos-26` for
native macOS repository checks and `ubuntu-24.04-arm` for release evaluation.
Both jobs retain ARM64 execution. The verification runner admits at most four
checks at once, reserving one logical CPU where available because checks spawn
their own workers.

CI does not use path filters. Repository checks and secret scans do not run
on push: pull requests verify and scan before merge, and the weekly schedule
scans history. Direct pushes require `mise run verify` locally before pushing.
Expand Down
10 changes: 7 additions & 3 deletions scripts/verify/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { NodeServices } from "@effect/platform-node";
import { Clock, Console, Effect, FileSystem, Schema } from "effect";
import { dirname, resolve } from "node:path";
import { availableParallelism } from "node:os";
import { fileURLToPath } from "node:url";
import { CommandRunner } from "../lib/command.ts";
import { CliFailure, fail, runMain } from "../lib/program.ts";
Expand Down Expand Up @@ -44,7 +45,7 @@ const usageText = `Usage:
scripts/verify/run.ts [--skip-security] [--domain NAME ...]
scripts/verify/run.ts --list [--json]

Without --domain, runs every deterministic check in parallel. The full-history
Without --domain, runs every deterministic check with bounded concurrency. The full-history
secret scan runs afterwards unless --skip-security is set. A focused domain
omits complete-only parity checks; request --domain security explicitly for
secret scans.
Expand Down Expand Up @@ -126,7 +127,10 @@ const runCheck = Effect.fn("runCheck")(function*(check: Check, timeoutMs = 300_0
return { check, durationMs: finished - started, ...result };
});

export const runChecks = Effect.fn("runChecks")(function*(checks: readonly Check[], timeoutMs = 300_000) {
// Checks can spawn their own workers; reserve capacity for their children and reporting.
const checkConcurrency = Math.max(1, Math.min(4, availableParallelism() - 1));

export const runChecks = Effect.fn("runChecks")(function*(checks: readonly Check[], timeoutMs = 300_000, concurrency = checkConcurrency) {
const results = yield* Effect.forEach(checks, (check) => runCheck(check, timeoutMs).pipe(
Effect.tap((result) => Effect.gen(function*() {
const seconds = (result.durationMs / 1000).toFixed(2);
Expand All @@ -140,7 +144,7 @@ export const runChecks = Effect.fn("runChecks")(function*(checks: readonly Check
process.stderr.write(`FAILED: ${result.check.id} exited ${result.status} (${seconds}s)\n`);
});
})),
), { concurrency: "unbounded" });
), { concurrency });
return results.every((result) => result.status === 0);
});

Expand Down
28 changes: 27 additions & 1 deletion scripts/verify/runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ test("verification reports completed checks before a stalled child and cleans it
`], output: "failure" },
{ id: "success", domain: "static", command: [process.execPath, "-e", ""], output: "success" },
{ id: "stalled", domain: "static", command: [process.execPath, "-e", `require('node:fs').writeFileSync(${JSON.stringify(pidFile)}, String(process.pid)); process.stdout.write('progress before stall\\n'); process.stderr.write('warning before stall\\n'); setTimeout(() => process.exit(8), 9000)`], output: "timeout" },
], 1500).pipe(Effect.provide(CommandRunner.layer), Effect.provide(NodeServices.layer)));
], 1500, 3).pipe(Effect.provide(CommandRunner.layer), Effect.provide(NodeServices.layer)));
assert.equal(result, false);
assert.equal(failedBeforeTermination, true);
assert.ok(output.join("").indexOf("FAILED: quick") < output.join("").indexOf("FAILED: stalled"));
Expand All @@ -55,3 +55,29 @@ test("verification reports completed checks before a stalled child and cleans it
rmSync(root, { recursive: true, force: true });
}
});

test("verification bounds active checks and drains queued checks after failure", async () => {
let active = 0;
let peak = 0;
const completed: string[] = [];
const runner = CommandRunner.of({
run: (command) => Effect.gen(function*() {
active += 1;
peak = Math.max(peak, active);
yield* Effect.yieldNow;
active -= 1;
completed.push(command);
return { status: command === "first" ? 7 : 0, stdout: "", stderr: "" };
}),
});
const checks = ["first", "second", "third", "fourth"].map((id) => ({
id, domain: "static", command: [id] as [string], output: id,
}));
const result = await Effect.runPromise(runChecks(checks, 300_000, 2).pipe(
Effect.provideService(CommandRunner, runner),
));
assert.equal(result, false);
assert.equal(peak, 2);
assert.equal(active, 0);
assert.deepEqual(completed.sort(), ["first", "fourth", "second", "third"]);
});
Loading