1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
name: CI
on:
push:
branches:
pull_request:
branches:
schedule:
- cron: "0 4 * * 1"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
jobs:
test:
if: github.event_name != 'schedule'
name: test (${{ matrix.os }})
runs-on: ${{ matrix.os }}
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
os:
steps:
- uses: actions/checkout@v7
timeout-minutes: 2
- uses: dtolnay/rust-toolchain@nightly
timeout-minutes: 5
with:
components: rust-src
- uses: Swatinem/rust-cache@v2
timeout-minutes: 5
with:
cache-bin: true
- name: Install cargo-nextest
uses: taiki-e/install-action@v2
timeout-minutes: 5
with:
# The `nextest` tag this used to float on is an unversioned per-tool
# ref from the pre-v2 era; v2 is the maintained line and names the
# tool explicitly, so Dependabot can see what it is bumping.
tool: nextest
# No `cargo build` step before this: `cargo nextest run` compiles the
# same lib + bin units, so a separate build only paid for that compile
# twice on a cold cache. A build error surfaces here instead.
- name: Test
timeout-minutes: 30
run: cargo nextest run --all-features
- name: Test doctests
timeout-minutes: 15
run: cargo test --doc
clippy:
if: github.event_name != 'schedule'
name: clippy
runs-on: ubuntu-24.04-arm
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
timeout-minutes: 2
- uses: dtolnay/rust-toolchain@nightly
timeout-minutes: 5
with:
components: clippy, rust-src
- uses: Swatinem/rust-cache@v2
timeout-minutes: 5
- name: Lint
run: cargo clippy --all-targets -- -D warnings
- name: Package the crate (publish / docs.rs gate)
timeout-minutes: 10
# The documented no-env gate (AGENTS.md): `cargo package --allow-dirty`
# must succeed with no GRR_* env and no .env present. It parses and
# packages the manifest exactly as the published crate sees it, which is
# where the docs.rs break lived — ordinary build/clippy never validate
# that package-level view. Runs here (the single-OS ubuntu Rust job, not
# the ubuntu+windows test matrix) because the check is platform-
# independent and duplicating it per matrix leg is pure waste.
run: cargo package --allow-dirty
fmt:
if: github.event_name != 'schedule'
name: fmt
runs-on: ubuntu-24.04-arm
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
timeout-minutes: 2
- uses: dtolnay/rust-toolchain@nightly
timeout-minutes: 5
with:
# This job only runs `cargo fmt`; rust-src was carried over from the
# other jobs but nothing here needs it.
components: rustfmt
- name: Check formatting
run: cargo fmt --all -- --check
scripts:
if: github.event_name != 'schedule'
name: scripts typecheck
runs-on: ubuntu-24.04-arm
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
timeout-minutes: 2
- uses: actions/setup-node@v7
timeout-minutes: 5
with:
node-version: "26"
cache: npm
cache-dependency-path: package-lock.json
- name: Install script toolchain
run: npm install
- name: Typecheck scripts
run: npm run typecheck
- name: Generated artifacts are current
timeout-minutes: 5
# These gates used to run inside the `test` matrix, which executed
# them twice per PR (once on Linux, once on Windows). They compare
# committed bytes against generator output, which is platform-
# independent, so one Linux run covers both matrix legs.
run: |
node scripts/generate-commands.ts --check
node scripts/generate-skills.ts --check
node scripts/generate-coverage.ts --check
site:
# The site deploys straight from main (Cloudflare Pages builds it on
# push), so a broken .astro file or a bad import would ship before anyone
# noticed. Nothing else in this workflow compiles the site, which is why
# this job exists.
if: github.event_name != 'schedule'
name: site (build + check)
runs-on: ubuntu-24.04-arm
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
timeout-minutes: 2
- uses: actions/setup-node@v7
timeout-minutes: 5
with:
node-version: "26"
cache: npm
cache-dependency-path: site/package-lock.json
- name: Install site dependencies
run: npm install
working-directory: site
- name: Type-check the Astro sources
run: npm run check
working-directory: site
# Generated mascot assets plus the CLI-banner parity (AGENTS invariant
# 13: src/logo.rs's map must be a crop of this generator's idle map,
# with one palette). A separate step on purpose — `npm run check` is
# astro check, this is the asset generator's own gate.
- name: Check generated mascot assets + banner parity
run: node scripts/generate-mascot.mjs --check
working-directory: site
- name: Build the site
run: npm run build
working-directory: site
feature-subset:
# The per-service cargo features are gone; the one meaningful subset left
# is a CLI-only build (`--no-default-features --features cli`). Lib+bins
# only, deliberately not --all-targets: the integration tests exercise
# the curated clients, which are not part of this subset.
if: github.event_name != 'schedule'
name: feature-subset (cli)
runs-on: ubuntu-24.04-arm
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
timeout-minutes: 2
- uses: dtolnay/rust-toolchain@nightly
timeout-minutes: 5
with:
components: clippy, rust-src
- uses: Swatinem/rust-cache@v2
timeout-minutes: 5
with:
# Distinct key from the clippy job: a different feature set means a
# different target dir; sharing a key would evict each other.
key: cli-only
- name: Clippy (no default features, cli only)
run: cargo clippy --no-default-features --features cli -- -D warnings
security:
name: security audit
runs-on: ubuntu-24.04-arm
timeout-minutes: 15
# An advisory must not block unrelated PRs: the audit still reports in the
# job log and summary, but a new RUSTSEC advisory against a transitive
# dependency is not a reason to red a docs or feature PR. On the weekly
# schedule there is no unrelated PR, so there the audit is a real gate —
# advisory-only everywhere would mean it can never red anything at all.
continue-on-error: ${{ github.event_name == 'pull_request' }}
permissions:
contents: read
issues: write
checks: write
steps:
- uses: actions/checkout@v7
timeout-minutes: 2
- uses: dtolnay/rust-toolchain@nightly
timeout-minutes: 5
# Cargo.lock is deliberately untracked (.gitignore): every build resolves
# the latest semver-compatible versions instead of pinning. cargo-audit,
# however, reads a lockfile, and rustsec/audit-check passes
# `--file ./Cargo.lock` unconditionally — with no lock in the tree the
# job failed on EVERY run ("not found: Couldn't load ./Cargo.lock" →
# "Unexpected end of JSON input"), invisibly, because continue-on-error
# below keeps the run green. Resolve the graph the way a build does and
# audit that: the versions a fresh build will use are the ones worth
# checking against advisories.
- name: Resolve the lockfile the audit reads
timeout-minutes: 5
run: cargo generate-lockfile
- name: Audit Rust dependencies
uses: rustsec/audit-check@v2
timeout-minutes: 10
with:
token: ${{ secrets.GITHUB_TOKEN }}