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
18 changes: 11 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
name: CI

# Automatic runs are temporarily paused while GitHub Actions billing is
# unavailable. Keep the workflow runnable by hand so the CI definition remains
# easy to verify and restore. `scripts/ci-local.sh fast` is the local mirror of
# the `verify` job and runs from a pre-push hook in the meantime; keep the two
# in step, because a local gate that has drifted reports green for a rule this
# workflow would fail.
# `scripts/ci-local.sh fast` is the local mirror of the `verify` job and runs
# from a pre-push hook; keep the two in step, because a local gate that has
# drifted reports green for a rule this workflow would fail.
on:
push:
branches: [main]
pull_request:
workflow_dispatch:

# A new manual run for the same ref supersedes the run in flight.
Expand All @@ -16,7 +16,7 @@ concurrency:

jobs:
verify:
runs-on: ubuntu-latest
runs-on: [self-hosted, macOS, ARM64]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

Expand Down Expand Up @@ -72,6 +72,8 @@ jobs:

packed-cli-consumer:
needs: verify
# Hosted runner: manual-only while GitHub Actions billing is unavailable.
if: github.event_name == 'workflow_dispatch'
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
Expand Down Expand Up @@ -114,6 +116,8 @@ jobs:
# whose startup surface this job probes.
windows-compat:
needs: verify
# Hosted runner: manual-only while GitHub Actions billing is unavailable.
if: github.event_name == 'workflow_dispatch'
runs-on: windows-2025
strategy:
fail-fast: false
Expand Down
14 changes: 7 additions & 7 deletions .github/workflows/invariants.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ name: Invariants
# The rules in CLAUDE.md that nothing currently enforces. Deterministic checks
# only -- no model, no secrets, no network. Runs read-only: findings surface as
# GitHub annotations on the PR, which need no write permission.
# Automatic runs are temporarily paused while GitHub Actions billing is
# unavailable. Keep the workflow runnable by hand so the checks remain easy to
# verify and restore. `scripts/ci-local.sh fast` ports every check below and
# runs from a pre-push hook in the meantime; a check added here must be added
# there too.
# `scripts/ci-local.sh fast` ports every check below and runs from a pre-push
# hook; a check added here must be added there too.
on:
push:
branches: [main]
pull_request:
workflow_dispatch:

permissions:
Expand All @@ -20,7 +20,7 @@ concurrency:

jobs:
check:
runs-on: ubuntu-latest
runs-on: [self-hosted, macOS, ARM64]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
Expand Down Expand Up @@ -98,7 +98,7 @@ jobs:

- name: Relay authentication uses the shared middleware seam
run: |
test "$(grep -F 'app.use(\"/v1/*\", requireIdentity)' apps/relay/src/index.ts)" || {
test "$(grep -F 'app.use("/v1/*", requireIdentity)' apps/relay/src/index.ts)" || {
echo "::error file=apps/relay/src/index.ts::Missing the fail-closed /v1 identity middleware."; exit 1;
}
test "$(grep -F 'PUBLIC_V1_PATHS' apps/relay/src/middleware.ts)" || {
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ concurrency:

jobs:
build:
runs-on: ubuntu-latest
runs-on: [self-hosted, macOS, ARM64]
permissions:
contents: read
steps:
Expand Down Expand Up @@ -83,7 +83,7 @@ jobs:

publish:
needs: build
runs-on: ubuntu-latest
runs-on: [self-hosted, macOS, ARM64]
environment: npm
permissions:
contents: read
Expand Down Expand Up @@ -160,7 +160,7 @@ jobs:

attach:
needs: publish
runs-on: ubuntu-latest
runs-on: [self-hosted, macOS, ARM64]
permissions:
contents: write
steps:
Expand Down
35 changes: 31 additions & 4 deletions packages/cli/test/release-workflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,40 @@ function actionReferences(value: unknown): string[] {
}

describe("npm release workflow", () => {
it("keeps billable verification workflows manual-only while automatic runs are paused", () => {
for (const source of [ciWorkflow, invariantsWorkflow]) {
expect(source).toMatch(/^on:\n workflow_dispatch:\s*$/m);
expect(source).not.toMatch(/^ (?:pull_request|push):/m);
// Was "keep these workflows manual-only". That held while every job ran on a
// GitHub-hosted runner and hosted billing was unavailable. Now `verify` and
// the invariants check run on a self-hosted runner, which costs nothing, so
// blanket manual-only would keep the gate switched off for no reason.
//
// What still has to hold is narrower: a job on a *hosted* runner must not be
// reachable from an automatic trigger, or every push fails on the billing
// block and CI reads red for a reason unrelated to the change.
it("never lets an automatic trigger reach a job on a billable hosted runner", () => {
const hostedRunner = /runs-on: (?!\[self-hosted)/;

for (const [name, source] of [["ci", ciWorkflow], ["invariants", invariantsWorkflow]] as const) {
const automatic = /^ (?:pull_request|push):/m.test(source);
if (!automatic) continue;

// Job blocks are two-space keys under `jobs:`; split on them so each
// job's runs-on and its `if:` guard are read together.
const jobs = source.split(/\n(?= [a-z][a-z0-9-]*:\n)/).filter((block) => /runs-on:/.test(block));
const ungated = jobs
.filter((block) => hostedRunner.test(block))
.filter((block) => !/if: github\.event_name == 'workflow_dispatch'/.test(block))
.map((block) => block.trimStart().split(":")[0]);

expect(ungated, `${name}.yml runs hosted jobs on an automatic trigger`).toEqual([]);
}
});

it("runs the gate itself on the self-hosted runner, so a push is actually checked", () => {
// The point of the move: `verify` is what decides whether a change ships,
// and it has to run without hosted minutes.
expect(ciWorkflow).toMatch(/verify:\n {4}runs-on: \[self-hosted, macOS, ARM64\]/);
expect(invariantsWorkflow).toMatch(/check:\n {4}runs-on: \[self-hosted, macOS, ARM64\]/);
});

it("publishes the CLI for both supported listener platforms", () => {
const manifest = JSON.parse(readFileSync(join(root, "packages/cli/package.json"), "utf8"));
expect(manifest.os).toEqual(["darwin", "linux"]);
Expand Down
7 changes: 6 additions & 1 deletion scripts/verify-action-pins.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,13 @@
end
end

# `safe_load_file` needs Psych 3.3 (Ruby 3.0). The self-hosted runner has an
# older stock Ruby and failed here with `undefined method 'safe_load_file'`,
# which reads like a pinning violation rather than a missing method. Reading the
# file ourselves works on every Ruby that ships `safe_load` with keyword
# arguments, which is 2.6 onward.
Dir.glob(".github/workflows/*.{yml,yaml}").sort.each do |file|
visit.call(YAML.safe_load_file(file, aliases: false), file, [])
visit.call(YAML.safe_load(File.read(file), aliases: false), file, [])
end

unless failures.empty?
Expand Down