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
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
name: CI
# Build / test / lint / audit gate. Runs on GitHub-hosted `ubuntu-latest` — free
# and unlimited for this public repo, with the 5 jobs running in parallel. (They
# used to be pinned to the single self-hosted ci-VM runner to save private-repo
# minutes; that rationale went away when the repo went public.)
# CodeQL, dependency-review, and OSSF Scorecard live in their own workflows
# (they are designed for GitHub-hosted runners — see those files + the README).
on:
push:
branches:
pull_request:
branches:
workflow_dispatch: # allow manual runs from the Actions tab / gh CLI
concurrency:
# One in-flight CI run per ref: a new push to a PR cancels that PR's own
# in-progress run rather than letting a superseded 5-job build finish. Keyed on
# the PR number (falling back to the ref for push/dispatch). On GitHub-hosted
# runners the jobs also run in parallel — so this is purely about not wasting a
# run on outdated commits.
group: ci-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
# Least privilege: every job only checks out the tree and hits package registries
# — none push, tag, comment, or upload — so trim the default GITHUB_TOKEN to read.
permissions:
contents: read
jobs:
rust:
name: Rust
# FORK-PR GUARD (applied to every job): same-repo PRs + push / dispatch only;
# fork PRs get NO CI. Originally to keep untrusted fork code (a slipped-in
# build.rs / proc-macro / npm lifecycle script) off the self-hosted homelab
# runner. Now that CI is GitHub-hosted (sandboxed; secrets aren't exposed to
# fork-PR workflows) that RCE risk is gone — so this is now a POLICY choice:
# drop this `if:` to give contributor PRs CI.
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
runs-on: ubuntu-latest # GitHub-hosted: free + parallel now the repo is public
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Install stable toolchain (clippy + rustfmt)
uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable
with:
toolchain: stable # required — SHA-pinned, so the @ref no longer selects the toolchain
components: clippy, rustfmt
- name: Cache cargo
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- name: Build
run: cargo build --all-targets --locked
- name: Test
run: cargo test --locked
- name: Clippy
run: cargo clippy --all-targets -- -D warnings
- name: Rustfmt
run: cargo fmt --all --check
cargo-deny:
name: cargo deny (licenses + bans + sources)
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} # fork-PR guard (see rust job)
runs-on: ubuntu-latest # GitHub-hosted: free + parallel now the repo is public
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
# cargo-deny shells out to `cargo metadata` to build the crate graph, so
# this job needs a Rust toolchain (cargo on PATH) even though it only runs
# the license/ban/source gates.
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable
with:
toolchain: stable # required — SHA-pinned ref no longer selects the toolchain
# Install a pinned cargo-deny binary directly (rather than
# EmbarkStudios/cargo-deny-action@v2, whose bundled RustSec advisory-DB
# fetch broke the gate on the old self-hosted runner every run).
# Config is deny.toml at the repo root; feature evaluation is set there
# (`[graph] all-features = true`).
- name: Install cargo-deny
uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2
with:
tool: cargo-deny@0.20.2
# We deliberately DO NOT run the `advisories` check here: it needs the
# RustSec advisory DB, which the dedicated `cargo audit` job below already
# covers (same DB, same fail-on-vuln semantics). This job enforces the
# AGPL-compatible license allowlist, duplicate/ban hygiene, and the
# crates.io-only source policy. (Not `--offline`: `cargo metadata` needs
# the registry index to resolve the dependency graph.)
- name: cargo-deny (licenses + bans + sources)
run: cargo-deny check bans licenses sources
cargo-audit:
name: cargo audit (RustSec)
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} # fork-PR guard (see rust job)
runs-on: ubuntu-latest # GitHub-hosted: free + parallel now the repo is public
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Install stable toolchain
uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable
with:
toolchain: stable # required — SHA-pinned ref no longer selects the toolchain
- name: Cache cargo
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
# Dedicated RustSec scan of Cargo.lock. Fails the build on any known
# vulnerability. Un-actionable transitive advisories (no fixed release, or
# a fix that needs a semver-major bump) are whitelisted in deny.toml's
# `[advisories] ignore = [...]`; mirror any such id here as an explicit
# `--ignore RUSTSEC-YYYY-NNNN` flag. Keep the list SHORT and documented.
# Install the pinned cargo-audit BINARY (like the cargo-deny job) rather than
# `cargo install ... || true`, which masked an install failure and let the
# next step die command-not-found — a green job that skipped the RustSec scan.
- name: Install cargo-audit
uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2
with:
tool: cargo-audit@0.22.2
- name: cargo audit
# `-D warnings` promotes unmaintained/unsound advisories to failures too.
run: cargo audit -D warnings
sidecar:
name: OAuth sidecar
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} # fork-PR guard (see rust job)
runs-on: ubuntu-latest # GitHub-hosted: free + parallel now the repo is public
defaults:
run:
working-directory: oauth-sidecar
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 24
cache: npm
cache-dependency-path: oauth-sidecar/package-lock.json
- name: Install
run: npm ci
- name: Build
run: npm run build
- name: Typecheck
run: npm run typecheck
- name: Lint (ESLint, --max-warnings 0)
run: npm run lint
- name: Format check (Prettier)
# Scoped to the tooling/config files the CI pipeline owns. The hand-
# authored src/ + test/ predate Prettier and are enforced for
# CORRECTNESS by ESLint above; a repo-wide `prettier --write` is left as
# a deliberate one-time follow-up (`npm run format:write`) so this gate
# never fights the code-fix workflows over whitespace. See
# .github/workflows/README.md.
run: npm run format:check
- name: npm audit (prod deps, high+)
# Sidecar runtime deps only (--omit=dev); dev tooling churn shouldn't
# break CI. Fails on high/critical advisories.
run: npm audit --omit=dev --audit-level=high
bot:
name: Invite bot (build + test + lint + deny + audit)
# The follow→invite bot is a STANDALONE workspace (bot/Cargo.toml has its own
# `[workspace]`), so the root Rust / cargo-deny / cargo-audit jobs above NEVER
# touch it. This job gives the bot the SAME gates as the app, scoped to `bot/`.
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} # fork-PR guard (see rust job)
runs-on: ubuntu-latest # GitHub-hosted: free + parallel now the repo is public
defaults:
run:
working-directory: bot
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Install stable toolchain (clippy + rustfmt)
uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable
with:
toolchain: stable # required — SHA-pinned, so the @ref no longer selects the toolchain
components: clippy, rustfmt
- name: Cache cargo
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
workspaces: bot # cache the bot workspace's target/, not the repo root
- name: Build
run: cargo build --all-targets --locked
- name: Test
run: cargo test --locked
- name: Clippy
run: cargo clippy --all-targets -- -D warnings
- name: Rustfmt
run: cargo fmt --all --check
# The bot's supply-chain gates live here (mirroring the app's jobs). The
# bot's own bot/deny.toml carries the AGPL-compatible license allowlist,
# including CDLA-Permissive-2.0 (webpki-root-certs) and MIT (zmij); r-efi's
# `MIT OR Apache-2.0 OR LGPL-2.1-or-later` OR-expression resolves to a
# permissive branch automatically.
- name: Install cargo-deny
uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2
with:
tool: cargo-deny@0.20.2
- name: cargo-deny (licenses + bans + sources)
run: cargo-deny check bans licenses sources
# Install the pinned cargo-audit BINARY (mirroring the cargo-deny step above)
# rather than `cargo install ... || true`: the `|| true` masked an install
# FAILURE, so the next step would die "cargo-audit: command not found" — a
# green-looking job that never actually ran the RustSec scan. install-action
# fails the job loudly if the tool can't be fetched. (Cheap nit.)
- name: Install cargo-audit
uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2
with:
tool: cargo-audit@0.22.2
- name: cargo audit
# `-D warnings` promotes unmaintained/unsound advisories to failures too.
run: cargo audit -D warnings
secrets:
name: Secret scan (gitleaks)
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} # fork-PR guard (see rust job)
runs-on: ubuntu-latest # GitHub-hosted: free + parallel now the repo is public
env:
GITLEAKS_VERSION: 8.28.0
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
fetch-depth: 0 # full history so gitleaks can scan past commits too
# Run the gitleaks BINARY directly rather than gitleaks/gitleaks-action@v2.
# The action gates on a GITLEAKS_LICENSE (required for any repo owned by a
# GitHub *org*/account it deems non-free) and does GITHUB_TOKEN-scoped PR
# bookkeeping — both of which trip on a personal-account private repo and
# failed the job regardless of findings. The binary is licence-free for
# this use and has none of that PR/org coupling.
- name: Install gitleaks
run: |
set -euo pipefail
url="https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz"
curl -sSfL "$url" -o /tmp/gitleaks.tar.gz
tar -xzf /tmp/gitleaks.tar.gz -C /tmp gitleaks
install -m 0755 /tmp/gitleaks "$RUNNER_TEMP/gitleaks"
"$RUNNER_TEMP/gitleaks" version
# Scan the full git history (checkout above sets fetch-depth: 0). Config is
# .gitleaks.toml at the repo root (extends the default ruleset + allowlists
# the documented test fixtures/placeholders). --redact keeps any match out
# of the logs; a non-zero exit (leak found) fails the job.
- name: gitleaks (history + tree)
run: |
"$RUNNER_TEMP/gitleaks" git --config .gitleaks.toml --redact --exit-code 1