-
Notifications
You must be signed in to change notification settings - Fork 0
348 lines (321 loc) · 15.5 KB
/
Copy pathci.yml
File metadata and controls
348 lines (321 loc) · 15.5 KB
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
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
name: CI
# Builds, tests and lints this crate on every push and pull request, and
# checks the two installers beside it. Release, publish and spec-sync
# automation live elsewhere, on infrastructure this repository does not have —
# nothing here needs it.
#
# Everything runs on every PR: this is one crate on three runners, and a
# check nobody remembers to ask for is a check that does not run.
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
# One run per ref. Without this, three pushes to a branch inside a few minutes
# pay for three full runs of everything and only the last answer is read.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
jobs:
build:
name: build (${{ matrix.os }})
strategy:
fail-fast: false
# All three: the crate has `#[cfg(not(unix))]` arms no Unix leg
# compiles, so Windows is the only thing that checks them.
matrix:
os: [ubuntu-latest, macos-14, windows-2022]
runs-on: ${{ matrix.os }}
# These legs take one to two minutes. Twenty is not a budget, it is a
# tripwire: without one, a job that stops making progress runs to GitHub's
# six-hour default before anyone hears about it. That is not hypothetical —
# a test that blocked on `accept()` with no deadline wedged the Windows leg
# for its full six hours, and the only reason it went unnoticed for a day
# is that every run on `main` was canceled by the next push first.
#
# On all three rather than Windows alone: nobody here runs Windows as a
# daily driver, so CI is the only signal it has — but a Unix leg that
# suddenly needs twenty minutes has something wrong with it worth hearing
# about too.
timeout-minutes: 20
permissions:
contents: read
steps:
# One checkout and nothing else to fetch: the OpenAPI specs the commands
# are generated from are vendored in `openapi/`, so `cargo build` needs
# no second repository and no credential.
- uses: actions/checkout@v7
# rustup applies `rust-toolchain.toml` on its own, and it outranks the
# action below — that action ends in `rustup default`, which rustup
# ranks under a toolchain file. What the action would still do is fetch
# whatever stable is today and install the components onto *that*
# toolchain rather than the pinned one: two toolchains downloaded, and
# `cargo clippy` then run on the one that has no clippy. So it is told
# the version outright, read out of the file rather than written down
# here a second time.
#
# The sed is anchored to the line as written; a reformat that hides it
# empties this output and fails the install step, which is the loud half
# of the two ways this could go wrong.
- name: Read the pinned toolchain
id: toolchain
shell: bash
run: |
echo "toolchain=$(sed -n 's/^channel = "\(.*\)"/\1/p' rust-toolchain.toml | head -1)" >>"$GITHUB_OUTPUT"
# Both actions below have to be admitted by this repository's own
# Actions settings before the first push. A repository restricted to
# selected actions (Settings > Actions > General) that does not name
# them fails the entire run at startup: no jobs, no logs, and an error
# the REST API does not expose. Worth settling once, on the day this
# becomes a repository, rather than rediscovering it from a run that
# says nothing.
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ steps.toolchain.outputs.toolchain }}
components: rustfmt, clippy
# No cargo cache action here: this repo's Actions allowlist doesn't yet
# include one, so builds run uncached until that's settled. Slower, not
# broken.
# Platform-independent, so one leg is enough.
- name: Check formatting
if: matrix.os == 'ubuntu-latest'
run: cargo fmt --check
# `--locked` on all three: without it cargo is free to re-resolve the
# graph, so CI can go green on a dependency set that is not the one
# `Cargo.lock` names.
- name: Clippy
run: cargo clippy --locked --all-targets -- -D warnings
- name: Build
run: cargo build --locked --verbose
- name: Test
run: cargo test --locked --verbose
# The half of `mapbox completion`'s verification `cargo test` cannot
# reach: whether the shell the script names will actually load it. A
# shell the runner image does not have is skipped and named in the
# summary — the images carry bash, zsh and pwsh but not fish, so the
# fish leg is the one a laptop with fish installed covers.
- name: Source the completion scripts in real shells
shell: bash
run: sh scripts/test-completion.sh
# No Rust: this is where the repo's shell gets checked, install.sh above all
# — it is served to machines we do not control, and this is the only thing
# that checks it. Both runners, because the portability it has to get right
# — shasum vs sha256sum, BSD vs GNU tar and script(1) — differs between
# them.
installer:
name: install.sh (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-14]
runs-on: ${{ matrix.os }}
timeout-minutes: 20
permissions:
contents: read
steps:
- uses: actions/checkout@v7
- name: Parse as POSIX sh
run: |
sh -n scripts/install.sh
sh -n scripts/test-install.sh
sh -n scripts/test-completion.sh
# -s sh, not the default: bash would accept things `curl | sh` will not.
# Once is enough — the findings do not depend on the runner. The version
# is whatever the image ships and it does drift: 0.9 and 0.10 report the
# trap-invoked cleanup as unreachable where 0.11 does not, so both codes
# are suppressed at the function rather than pinned to one analyzer.
# Kept clean against 0.9, 0.10 and 0.11.
- name: Shellcheck
if: runner.os == 'Linux'
run: shellcheck -s sh scripts/install.sh scripts/test-install.sh scripts/test-completion.sh
- name: Install, verify and prompt against a local channel
run: sh scripts/test-install.sh
# The other half of the same job, for the installer install.sh stops short
# of. Both shells, because both are on a stock Windows 11 and they differ
# exactly where this script lives: the default TLS versions, what `2>` does
# to a native command's stderr, and the parser itself.
installer-windows:
name: install.ps1 (${{ matrix.shell }})
strategy:
fail-fast: false
matrix:
shell: [powershell, pwsh]
runs-on: windows-2022
timeout-minutes: 20
permissions:
contents: read
steps:
- uses: actions/checkout@v7
# `sh -n` in PowerShell: parse it, run none of it. Under pwsh in both
# legs - `shell:` is one of the few keys that takes no expression, so it
# cannot follow the matrix - which costs nothing: PSUseCompatibleSyntax
# below is what says 5.1 can parse this, and the suite parses both files
# with the real 5.1 parser when that is the leg running.
- name: Parse
if: matrix.shell == 'pwsh'
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$bad = 0
foreach ($file in 'scripts/install.ps1', 'scripts/test-install.ps1') {
$errors = $null
[void][System.Management.Automation.Language.Parser]::ParseFile(
(Resolve-Path $file).Path, [ref]$null, [ref]$errors)
foreach ($e in $errors) {
Write-Host "::error file=$file,line=$($e.Extent.StartLineNumber)::$($e.Message)"
$bad++
}
}
if ($bad) { exit 1 }
# Shellcheck's counterpart, and once is enough for the same reason: the
# findings do not depend on the shell running the analyzer. The module
# is installed rather than assumed - the runner image does not carry it.
- name: PSScriptAnalyzer
if: matrix.shell == 'pwsh'
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
Install-Module PSScriptAnalyzer -Scope CurrentUser -Force
# Write-Host is the point in an installer: the output is a message to
# a person, not data for a pipeline. The empty catches are
# best-effort cleanups, each with a comment saying what it is giving
# up on. The two naming rules are for modules that export cmdlets;
# these are scripts, and the harness's Expect-* names deliberately
# mirror test-install.sh's expect_*.
$exclude = @(
'PSAvoidUsingWriteHost'
'PSAvoidUsingEmptyCatchBlock'
'PSUseApprovedVerbs'
'PSUseShouldProcessForStateChangingFunctions'
)
# The rule that carries the weight here: it is what says Windows
# PowerShell 5.1 can parse what was written against 7. Not in the
# default set, so it has to be asked for.
$compat = @{
IncludeRules = @('PSUseCompatibleSyntax')
Rules = @{
PSUseCompatibleSyntax = @{ Enable = $true; TargetVersions = @('5.1', '7.0') }
}
}
$bad = 0
foreach ($file in 'scripts/install.ps1', 'scripts/test-install.ps1') {
$findings = @(Invoke-ScriptAnalyzer -Path $file -Severity Error, Warning -ExcludeRule $exclude)
$findings += @(Invoke-ScriptAnalyzer -Path $file -Settings $compat)
foreach ($f in $findings) {
Write-Host "::error file=$file,line=$($f.Line)::$($f.RuleName): $($f.Message)"
$bad++
}
}
if ($bad) { exit 1 }
# PSUseBOMForUnicodeEncodedFile above says a non-ASCII byte needs a byte
# order mark; this says there may be no non-ASCII byte to need one. The
# reason is in install.ps1's header: 5.1 reads a BOM-less .ps1 as the
# machine's ANSI code page, and a BOM breaks `irm | iex`.
- name: ASCII only
if: matrix.shell == 'pwsh'
shell: pwsh
run: |
$bad = 0
foreach ($file in 'scripts/install.ps1', 'scripts/test-install.ps1') {
$bytes = [IO.File]::ReadAllBytes((Resolve-Path $file).Path)
for ($i = 0; $i -lt $bytes.Length; $i++) {
if ($bytes[$i] -gt 127) {
Write-Host "::error file=$file::byte $i is 0x$('{0:x2}' -f $bytes[$i]); this file has to stay ASCII"
$bad++
break
}
}
}
if ($bad) { exit 1 }
# The step the matrix exists for. The shell is named in the command
# rather than in `shell:`, which takes no expression; -ExecutionPolicy
# is a Windows-only switch both of them accept, and is what lets 5.1 run
# an unsigned script out of a checkout.
- name: Install, verify and PATH against a local channel
shell: pwsh
run: ${{ matrix.shell }} -NoProfile -ExecutionPolicy Bypass -File scripts/test-install.ps1
# The one job here that can block on something other than this repo's own
# code, and the split is the whole design: a vulnerability fails it, an
# unmaintained or unsound crate does not.
#
# `Cargo.lock` pins the whole graph behind the handful of crates
# `Cargo.toml` names, and a release compiles the binaries users download
# from exactly that graph — so the gap this closes was never "we might be
# shipping something old", it was that nothing here would have said so. A
# vulnerability names a crate, a version and a fixed version, so there is
# one action and it fits in a lockfile line; merging past it ships a known
# hole to people who ran a one-line installer. An unmaintained crate names
# no version to move to: the action is a migration, and making that the
# price of an unrelated PR buys an ignore entry written under time pressure,
# which is the kind nobody ever removes. The two answers are cargo-audit's
# own exit codes rather than anything configured here, which is a further
# reason to pin its version below — a change to those defaults then arrives
# in a diff somebody wrote.
audit:
name: advisories (blocks on a vulnerability)
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
steps:
# No toolchain: nothing here compiles. This job reads a text file
# against a git repository of advisories.
- uses: actions/checkout@v7
# Not `rustsec/audit-check`, for the reason the note in `build` gives:
# an action has to be admitted by this repository's Actions settings,
# and a disallowed one fails the whole run at startup with no logs. A
# pinned tarball needs no such permission. Pinned by version *and* by
# hash, because upstream publishes no checksum of its own and this
# should run the bytes we looked at rather than whatever the URL serves
# later. Bumping the version means replacing both lines.
- name: Install cargo-audit
env:
AUDIT_VERSION: "0.22.2"
AUDIT_SHA256: ab28a1bdb54db4d5d8ad5981cf1f959410370b3d28250dbd35f6a44248620e39
run: |
name="cargo-audit-x86_64-unknown-linux-gnu-v${AUDIT_VERSION}"
curl -fsSL -o /tmp/cargo-audit.tgz \
"https://github.com/rustsec/rustsec/releases/download/cargo-audit%2Fv${AUDIT_VERSION}/${name}.tgz"
echo "${AUDIT_SHA256} /tmp/cargo-audit.tgz" | sha256sum -c -
# The archive has a versioned top-level directory and carries a
# README and two licenses beside the binary; name the one file that
# belongs on PATH.
tar xzf /tmp/cargo-audit.tgz -C "$HOME/.cargo/bin" \
--strip-components=1 "${name}/cargo-audit"
cargo-audit --version
# `cargo-audit`, not `cargo audit`. The `cargo` on the runner is a rustup
# shim and `rust-toolchain.toml` in this checkout outranks whatever the
# image installed, so going through it downloads the pinned toolchain in
# order to run a program that reads a text file.
#
# `--file` is how this keeps the property every cargo call in `build`
# gets from `--locked`. Left to its default, `cargo audit` with no
# lockfile in sight runs `cargo generate-lockfile` and audits the result
# — a graph nobody committed and nothing ships, reported green. Naming
# the file makes that an error instead: exit 2, "Couldn't load
# Cargo.lock".
#
# The advisory database is fetched every run and deliberately not cached.
# It is the one input here that is worthless the moment it is a day old.
#
# The output reaches the job summary whatever it said: an informational
# advisory exits 0, and a warning behind a green check is one nobody
# reads.
- name: Audit Cargo.lock
run: |
status=0
cargo-audit audit --color never --file Cargo.lock >audit.txt 2>&1 || status=$?
cat audit.txt
{
printf '```\n'
cat audit.txt
printf '```\n\n'
printf "A vulnerability fails this job; an unmaintained, unsound or "
printf "yanked crate is reported here and does not. "
printf "\`cargo audit\` says the same thing locally, before the push "
printf "rather than after it.\n"
} >>"$GITHUB_STEP_SUMMARY"
exit "$status"