-
Notifications
You must be signed in to change notification settings - Fork 0
608 lines (559 loc) · 30.8 KB
/
Copy pathrequired.yml
File metadata and controls
608 lines (559 loc) · 30.8 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
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
# Copyright 2026 ResQ Systems, Inc.
# SPDX-License-Identifier: Apache-2.0
#
# `required` status check for the org ruleset (default-branch-baseline).
# It is the *only* required context, so it must actually gate: it depends on
# real lint/parse jobs and fails if any of them fail. (Secret/SAST scanning is
# already covered by the repo's Socket Security + Semgrep + CodeQL checks.)
name: required
on:
push:
# release/pins-* is listed so that a human pushing a fix onto a bump branch
# gets checks. It does NOT cover the bot's own push: GitHub suppresses
# run-triggering events originating from GITHUB_TOKEN, and that applies to
# push exactly as it does to pull_request. release.yml therefore calls
# `gh workflow run required.yml --ref <branch>` after pushing, because
# workflow_dispatch is one of the two documented exceptions.
branches: [main, master, 'release/pins-*']
pull_request:
# Load-bearing. This is how the pin-bump PR gets its `required` check without
# a PAT or GitHub App secret. Remove it and those PRs become unmergeable.
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
shellcheck:
name: shellcheck (install.sh + scripts)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# None of these jobs push; matches every other workflow in the repo.
persist-credentials: false
- name: Install shellcheck
run: sudo apt-get update -qq && sudo apt-get install -y shellcheck
# -S warning, not -S error. Every "In POSIX sh, X is undefined" check is
# SC30xx, and every SC30xx is WARNING severity — so -S error sits exactly
# one notch above the only rules that police this repo's hardest
# constraint, that install.sh stays POSIX because it is piped to sh.
# Verified: a #!/bin/sh file containing `arr=(a b)` and `[[ -n "$1" ]]`
# passes -S error and fails -S warning (SC3030, SC3010).
#
# The same notch swallowed SC2115 (`rm -rf "$dir/"` with an unset var) and
# SC2164 (`cd` without `||`) — both live hazards in a script that removes
# temp directories from a trap.
#
# The old comment here claimed "pre-existing warning debt". Measured: the
# repo is at ZERO findings across all 17 shell files at this level, so the
# debt was already paid and the gate was being held down for nothing.
#
# -x follows `source`/`.` includes; -exec handles filenames with spaces.
- name: Lint shell scripts
run: >-
find . -type f -name '*.sh' -not -path './node_modules/*' -not -path './.git/*'
-exec shellcheck -S warning -x {} +
# The check above lints each file under the shell its shebang declares.
# install.sh is the one file where the dialect is a distribution promise
# rather than a preference: it is curl-piped into whatever /bin/sh is, so
# it is asserted explicitly rather than left to the shebang being right.
- name: install.sh is POSIX sh
run: shellcheck -S warning -s sh -x install.sh
# Catch drift on the PR that causes it rather than at tag time. VERSION is
# the only authored copy of the version; SCRIPT_VERSION, $ScriptVersion
# and both hook digests are generated from it and from the hook files.
# Editing scripts/install-hooks.* without re-stamping would otherwise ship
# installers that reject the very file they are meant to run.
- name: Installers stamped from VERSION
run: sh bin/stamp.sh --check
# A released VERSION must keep describing the artifacts it was tagged on.
#
# `stamp.sh --check` only proves the tree is internally consistent: it
# hashes the working tree and compares against stamps in that same tree.
# It cannot notice that "0.4.4" means something different here than it
# does at the v0.4.4 tag. That gap is not hypothetical — it is why this
# step exists. A pin bump merged into main without a VERSION bump, every
# check stayed green, and main's install.sh was left expecting a hooks
# digest that the tag it downloads from does not have; running it from a
# main checkout refused to install hooks. The curl-piped path was
# unaffected, because the endpoint serves the tag, which is self-
# consistent — so the damage was invisible from outside.
#
# Only fires once the tag exists, so the ordinary flow (bump VERSION,
# merge, tag) is unaffected. It catches the reverse: editing a shipped
# artifact while VERSION still points at a tag that no longer matches it.
- name: A tagged VERSION still matches its artifacts
run: |
set -eu
version="$(tr -d ' \t\r\n' < VERSION)"
tag="v$version"
# checkout is shallow and fetches no tags, so asking the local repo
# whether the tag exists would answer "no" every time and this step
# would pass without comparing anything. Ask the remote, and treat
# only ls-remote's documented "no match" status as not-yet-tagged —
# any other failure is a broken check, not a clean tree.
set +e
git ls-remote --exit-code --tags origin "refs/tags/$tag" >/dev/null 2>&1
lookup=$?
set -e
case "$lookup" in
0) ;;
2) echo "ok — $tag is not tagged yet; nothing to compare against"; exit 0 ;;
*) echo "::error::could not determine whether $tag exists (git ls-remote exit $lookup)"; exit 1 ;;
esac
git fetch --no-tags --depth=1 origin "refs/tags/$tag:refs/tags/$tag"
rc=0
for f in install.sh install.ps1 scripts/install-hooks.sh \
scripts/install-hooks.ps1 scripts/install-resq.sh; do
if ! git cat-file -e "$tag:$f" 2>/dev/null; then
echo "::error::$f does not exist at $tag"
rc=1
continue
fi
tagged="$(git show "$tag:$f" | sha256sum | cut -d' ' -f1)"
here="$(sha256sum < "$f" | cut -d' ' -f1)"
if [ "$tagged" != "$here" ]; then
echo "::error::$f differs from $tag but VERSION is still $version — bump VERSION and restamp"
rc=1
fi
done
[ "$rc" -eq 0 ] && echo "ok — all five artifacts match $tag"
exit "$rc"
# Every source file carries the canonical copyright header.
#
# In every other ResQ repo this is enforced by `resq copyright`, run from
# the pre-commit hook. This repo SHIPS that hook installer and does not
# use it — core.hooksPath is unset here, because dev/ deliberately no
# longer keeps its own copy of the hooks (see hooks-sync.yml). So the one
# repo distributing the enforcement was the one repo without it, and it
# showed: 17 files had drifted to a stale holder name and 20 more carried
# no header at all.
#
# A CI check rather than a local hook, for the same reason: a hook can be
# skipped with GIT_HOOKS_SKIP=1 and only exists on machines that ran the
# installer. The holder string matches resq-cli's copyright default, and
# the year is left open so this does not need editing every January.
- name: Source files carry the copyright header
run: |
set -eu
rc=0
# grep -L lists files NOT matching, so find -exec does the whole job
# in one pass. A `for f in $(find ...)` loop would be SC2044-fragile
# on paths with spaces, and this repo now gates shellcheck at warning.
#
# `|| true` is LOAD-BEARING, for exactly the reason bin/gen-pins.sh
# documents about `grep -c`: grep reports "did I match anything?" in
# its exit status, and with -L the clean case lists NOTHING, so it
# exits 1. Under `set -e` the command substitution inherits that and
# kills the step — silently, with no error output, precisely when
# every file is compliant. The first version of this check failed on
# a fully compliant tree. It passed locally only because an
# interactive shell has no `set -e`.
# Classify each file ONCE. A file saying "Copyright 2026 ResQ Software"
# both lacks the canonical header and carries the stale holder, so a
# naive two-pass version reported it twice — as missing AND as stale —
# which contradicts the promise directly below. Stale wins, because it
# is the more specific and more actionable diagnosis.
stale="$(find . \( -name '*.sh' -o -name '*.ps1' -o -name '*.ts' \
-o -name '*.mjs' -o -name '*.bats' -o -name '*.bash' \) \
-not -path './worker/node_modules/*' -not -path './.git/*' \
-exec grep -lF 'ResQ Software' {} + || true)"
missing_all="$(find . \( -name '*.sh' -o -name '*.ps1' -o -name '*.ts' \
-o -name '*.mjs' -o -name '*.bats' -o -name '*.bash' \) \
-not -path './worker/node_modules/*' -not -path './.git/*' \
-exec grep -LE 'Copyright [0-9]{4} ResQ Systems, Inc\.' {} + || true)"
if [ -n "$stale" ]; then
# -x whole-line, -F literal: subtract the stale set from the missing
# set so each file produces exactly one diagnostic.
missing="$(printf '%s\n' "$missing_all" \
| grep -vxF -f <(printf '%s\n' "$stale") || true)"
else
missing="$missing_all"
fi
if [ -n "$missing" ]; then
printf '%s\n' "$missing" | while IFS= read -r f; do
[ -n "$f" ] && echo "::error file=$f::missing the copyright header (expected: Copyright <year> ResQ Systems, Inc.)"
done
rc=1
fi
# Report a wrong holder AS a wrong holder rather than as a missing
# header — that is the drift this check was added for.
if [ -n "$stale" ]; then
printf '%s\n' "$stale" | while IFS= read -r f; do
[ -n "$f" ] && echo "::error file=$f::stale holder 'ResQ Software'; the canonical holder is 'ResQ Systems, Inc.'"
done
rc=1
fi
[ "$rc" -eq 0 ] || exit 1
echo "ok - every source file carries the canonical header"
# The cargo fallback in install-resq.sh is not a rare path — it is the
# only one that runs, because crates publishes no resq-cli-v* release for
# resolve_tag to find. It must pin, and the unpinned form must require an
# explicit opt-out. Stubs cargo; compiles nothing, fetches nothing.
- name: install-resq.sh pins its cargo fallback
run: sh tests/installers/pinning.sh
- name: The pinned crates commits still exist upstream
# A commit SHA cannot be repointed, which is the point of pinning — but
# it can be mistyped or rebased away, and either way every user's
# install dies at `cargo install --rev`. Better a red check here.
#
# Ancestry, not merely existence: a commit can resolve while sitting on
# an abandoned branch, and pinning to something never merged is not the
# reviewed history this is meant to point at.
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
resq_pin="$(sed -n 's/^CRATES_COMMIT="\(.*\)"$/\1/p' scripts/install-resq.sh)"
hooks_pin="$(sed -n 's/^ *CRATES_COMMIT="\(.*\)"$/\1/p' scripts/install-hooks.sh)"
[ -n "$resq_pin" ] || { echo "::error::could not read CRATES_COMMIT from scripts/install-resq.sh"; exit 1; }
rc=0
for pin in "$resq_pin" "$hooks_pin"; do
[ -n "$pin" ] || continue
if ! body="$(curl -fsSL -H "Authorization: Bearer $GH_TOKEN" \
"https://api.github.com/repos/resq-software/crates/compare/master...$pin")"; then
echo "::error::$pin is not resolvable in resq-software/crates"
rc=1; continue
fi
status="$(printf '%s' "$body" | sed -n 's/.*"status":[[:space:]]*"\([a-z]*\)".*/\1/p' | head -1)"
case "$status" in
identical|behind) echo "ok — $pin is on master ($status)" ;;
*) echo "::error::$pin is '$status' relative to master; pin a commit that is merged"; rc=1 ;;
esac
done
# They may legitimately differ — hooks and the CLI can move at
# different times — but if they agree, say so: one crates revision per
# dev release is the easier thing to reason about.
if [ "$resq_pin" = "$hooks_pin" ]; then
echo "ok — install-resq.sh and install-hooks.sh pin the same crates commit"
else
echo "::notice::install-resq.sh pins $resq_pin, install-hooks.sh pins $hooks_pin"
fi
exit "$rc"
- name: Hook digests match the pinned crates commit
# install-hooks.{sh,ps1} fetch six files that become executables git
# runs on every commit and push. They used to come from a mutable
# branch with no verification; they are now pinned and digest-checked.
#
# Pins rot silently: if the crates templates change, the installers keep
# refusing perfectly good hooks and every fresh onboard fails with a
# checksum mismatch. This turns that into a failing check here instead,
# and equally catches a mistyped digest that would never match anything.
run: |
set -euo pipefail
sh_commit="$(sed -n 's/^ *CRATES_COMMIT="\(.*\)"$/\1/p' scripts/install-hooks.sh)"
ps_commit="$(sed -n "s/^ *\$cratesCommit = '\(.*\)'$/\1/p" scripts/install-hooks.ps1)"
[ -n "$sh_commit" ] || { echo "::error::could not read CRATES_COMMIT from install-hooks.sh"; exit 1; }
if [ "$sh_commit" != "$ps_commit" ]; then
echo "::error::installers disagree on the pinned commit: sh=$sh_commit ps1=$ps_commit"
exit 1
fi
base="https://raw.githubusercontent.com/resq-software/crates/$sh_commit/crates/resq-cli/templates/git-hooks"
rc=0
for h in pre-commit commit-msg prepare-commit-msg pre-push post-checkout post-merge; do
if ! actual="$(curl -fsSL --proto '=https' --tlsv1.2 "$base/$h" | sha256sum | cut -d' ' -f1)"; then
echo "::error::could not fetch $h from the pinned commit"
rc=1; continue
fi
# Assert the real digest appears in both installers rather than
# parsing their formats — a shell case and a PowerShell hashtable
# would need two brittle parsers to say the same thing.
for f in scripts/install-hooks.sh scripts/install-hooks.ps1; do
if ! grep -q "$actual" "$f"; then
echo "::error::$f has no digest matching $h at $sh_commit (expected $actual)"
rc=1
fi
done
done
[ "$rc" -eq 0 ] && echo "ok — all six hook digests match $sh_commit in both installers"
exit "$rc"
- name: Nix installer digest matches the pinned URL
# Same reasoning as the hook digests. The Nix installer is pinned to a
# versioned Determinate URL and checked before execution; if that URL's
# content ever changes, every install fails on a checksum mismatch. A
# red check here is a better way to learn that than a user's report.
#
# This pins what we execute, not what Determinate wrote — they publish
# no digest of their own. It closes the "silently different tomorrow"
# gap, not the "already compromised when we pinned it" one.
run: |
set -euo pipefail
url="$(sed -n 's/^NIX_INSTALL_URL="\(.*\)"$/\1/p' install.sh)"
want="$(sed -n 's/^NIX_INSTALLER_SHA256="\(.*\)"$/\1/p' install.sh)"
# An explicit if, not `A && B || C` — shellcheck flags that as SC2015
# because C also runs when A succeeds and B fails, which is a genuine
# trap even where, as here, it happened to be the behaviour wanted.
if [ -z "$url" ] || [ -z "$want" ]; then
echo "::error::could not read the Nix pin from install.sh"
exit 1
fi
actual="$(curl -fsSL --proto '=https' --proto-redir '=https' --tlsv1.2 "$url" | sha256sum | cut -d' ' -f1)"
if [ "$actual" != "$want" ]; then
echo "::error::$url now hashes to $actual, but install.sh pins $want"
echo "::error::Review the change, then update the pin in install.sh, scripts/lib/nix.sh and scripts/lib/nix.ps1."
exit 1
fi
# All FOUR copies must agree. install.ps1 declares its own
# $NixInstallUrl and $NixInstallerSha256, so checking only the two
# libs would let the Windows entry point drift while this job passes —
# and drift there means Windows users get a different installer than
# everyone else, which is the failure this check exists to prevent.
rc=0
ps_url="$(sed -n "s/.*\$NixInstallUrl *= *'\([^']*\)'.*/\1/p" install.ps1 | head -1)"
ps_sha="$(sed -n "s/.*\$NixInstallerSha256 *= *'\([^']*\)'.*/\1/p" install.ps1 | head -1)"
if [ -z "$ps_url" ] || [ -z "$ps_sha" ]; then
echo "::error::could not read the Nix pin from install.ps1 — the extraction here no longer matches the file"
rc=1
else
[ "$ps_url" = "$url" ] || { echo "::error::install.ps1 pins URL $ps_url, install.sh pins $url"; rc=1; }
[ "$ps_sha" = "$want" ] || { echo "::error::install.ps1 pins digest $ps_sha, install.sh pins $want"; rc=1; }
fi
for f in scripts/lib/nix.sh scripts/lib/nix.ps1; do
grep -q "$want" "$f" || { echo "::error::$f does not carry the pinned digest $want"; rc=1; }
grep -q "$url" "$f" || { echo "::error::$f does not carry the pinned URL $url"; rc=1; }
done
[ "$rc" -eq 0 ] && echo "ok — Nix installer pin agrees across install.sh, install.ps1 and both libs"
exit "$rc"
- name: Bun installer digests match their pinned tags
# Same contract as the Nix and hook pins. Bun's installers are run
# rather than reimplemented — they do CPU baseline detection we do not
# want to duplicate — so what we verify is the script, pinned to a tag.
#
# Note the two are checked independently: the shell installer is
# byte-identical to bun.sh/install, the PowerShell one is not, so they
# cannot be assumed to move together.
run: |
set -euo pipefail
rc=0
# Extract by variable name rather than by matching a URL or a bare
# 64-hex run. bun.ps1 carries two pins, so a "first hex string wins"
# pattern would silently check one of them twice.
verify() { # label, url, want
if [ -z "$2" ] || [ -z "$3" ]; then
echo "::error::could not read the $1 pin — the extraction in this workflow no longer matches the file"
rc=1; return
fi
actual="$(curl -fsSL --proto '=https' --proto-redir '=https' --tlsv1.2 "$2" | sha256sum | cut -d' ' -f1)"
if [ "$actual" != "$3" ]; then
echo "::error::$1: $2 now hashes to $actual, but the pin says $3"
echo "::error::Review the change, then update scripts/lib/bun.sh and scripts/lib/bun.ps1 together."
rc=1
else
echo "ok — $1 matches its pin"
fi
}
verify "bun.sh install.sh" \
"$(sed -n 's/.*bun_url="\([^"]*\)".*/\1/p' scripts/lib/bun.sh)" \
"$(sed -n 's/.*bun_sha="\([^"]*\)".*/\1/p' scripts/lib/bun.sh)"
verify "bun.ps1 install.ps1" \
"$(sed -n "s/.*bunUrl = '\([^']*\)'.*/\1/p" scripts/lib/bun.ps1)" \
"$(sed -n "s/.*bunSha = '\([^']*\)'.*/\1/p" scripts/lib/bun.ps1)"
verify "bun.ps1 install.sh" \
"$(sed -n "s/.*shUrl = '\([^']*\)'.*/\1/p" scripts/lib/bun.ps1)" \
"$(sed -n "s/.*shSha = '\([^']*\)'.*/\1/p" scripts/lib/bun.ps1)"
# The shell installer is pinned in two files; they must not drift.
a="$(sed -n 's/.*bun_sha="\([^"]*\)".*/\1/p' scripts/lib/bun.sh)"
b="$(sed -n "s/.*shSha = '\([^']*\)'.*/\1/p" scripts/lib/bun.ps1)"
if [ "$a" != "$b" ]; then
echo "::error::bun.sh and bun.ps1 pin different digests for install.sh: $a vs $b"
rc=1
fi
exit "$rc"
- name: gen-pins missing-PINS guard is reachable
# `grep -c` exits 1 on zero matches, so without `|| true` the command
# substitution's status kills the script under `set -e` one line before
# the die that explains what went wrong — the guard becomes unreachable
# in exactly the case it exists for.
#
# Fixed once, then silently reverted by an unrelated PR that staged a
# stale copy of the file. Nothing caught it because nothing tested it.
# Asserting on the message rather than on a source line means a future
# rewrite still has to preserve the behaviour.
run: |
set -euo pipefail
tmp="$(mktemp -d)"
cp -a . "$tmp/r"
cd "$tmp/r"
# gen-pins needs at least one v* tag before it reaches write_source.
git tag -f v0.0.1 HEAD >/dev/null 2>&1
printf 'not a worker\n' > worker/src/index.ts
# Assert the exit status too, not only the message. The guard exists
# to *stop* the run; a script that printed this text and exited 0
# would satisfy a message-only check while being broken in precisely
# the way that matters. `if out="$(...)"` keeps set -e out of the way.
if out="$(sh bin/gen-pins.sh --write 2>&1)"; then
echo "::error::gen-pins.sh exited 0 on a file with no PINS block; it must fail"
echo "::error::output was: ${out:-<no output>}"
exit 1
fi
# Match the whole diagnostic rather than a fragment, so a message that
# drifts into meaning something else cannot keep passing.
case "$out" in
*"expected exactly one 'const PINS = {' in worker/src/index.ts, found 0"*)
echo "ok — guard failed with: $out" ;;
*)
echo "::error::gen-pins.sh failed, but not with the missing-PINS diagnostic"
echo "::error::got: ${out:-<no output>}"
exit 1 ;;
esac
powershell:
name: PowerShell parse + analyze (*.ps1)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# None of these jobs push; matches every other workflow in the repo.
persist-credentials: false
- name: Parse-check (gating) + PSScriptAnalyzer (advisory)
shell: pwsh
run: |
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$ps1 = Get-ChildItem -Recurse -Filter *.ps1 -File | Where-Object { $_.FullName -notmatch '[\\/](node_modules|\.git)[\\/]' }
if (-not $ps1) { Write-Host 'No .ps1 files'; exit 0 }
# Gate on parse errors (genuinely broken scripts).
$fail = $false
foreach ($f in $ps1) {
$tok = $null; $perr = $null
[System.Management.Automation.Language.Parser]::ParseFile($f.FullName, [ref]$tok, [ref]$perr) | Out-Null
if ($perr) { $fail = $true; Write-Host "PARSE ERROR: $($f.FullName)"; $perr | ForEach-Object { Write-Host " $($_.Message)" } }
}
# PSScriptAnalyzer findings are advisory while the real debt is measured.
#
# This step used to pass `-Path $ps1.FullName`, where $ps1 is an ARRAY of
# FileInfo — so .FullName is String[] and -Path, which takes a single
# string, threw:
#
# Cannot convert 'System.Object[]' to the type 'System.String'
# required by parameter 'Path'
#
# The catch swallowed it, printed "skipped", and the job went green. The
# analyser had never examined a single line while the step name promised
# it had. Analysing per file removes the array entirely.
#
# The catch is kept for a genuinely absent module (offline runner), but it
# now names the stage that failed, so "not installed" can never again read
# the same as "found nothing".
# Availability and findings are tracked as SEPARATE facts, because
# conflating them is the defect this step is fixing. "No findings" may
# only be printed when every file was actually analysed; otherwise a
# broken analyser reads exactly like clean code.
#
# Import explicitly rather than trusting Get-Module -ListAvailable:
# that tests discoverability, not importability, and a module can sit
# on disk yet fail to load.
$analyzerReady = $false
try {
Install-Module PSScriptAnalyzer -Force -Scope CurrentUser -ErrorAction Stop
Import-Module PSScriptAnalyzer -Force -ErrorAction Stop
$analyzerReady = $true
} catch {
Write-Host "::warning::PSScriptAnalyzer unavailable, PowerShell was NOT analysed: $($_.Exception.Message)"
}
if ($analyzerReady) {
$issues = @()
$analyzed = 0
$failed = @()
foreach ($f in $ps1) {
try {
# -ErrorAction Stop, not Continue. With Continue, a per-file
# failure leaves $issues untouched and that file is silently
# counted as clean — the same "absence reads as success" shape
# this step exists to remove.
$issues += Invoke-ScriptAnalyzer -Path $f.FullName -Severity Error,Warning -ErrorAction Stop
$analyzed++
} catch {
$failed += $f.FullName
Write-Host "::warning::PSScriptAnalyzer failed on $($f.FullName): $($_.Exception.Message)"
}
}
if ($issues) {
Write-Host '::group::PSScriptAnalyzer (advisory)'
($issues | Format-Table RuleName, Severity, ScriptName, Line, Message -AutoSize | Out-String -Width 200) | Write-Host
Write-Host '::endgroup::'
# A count, so the debt is a number someone can watch shrink rather than
# a wall of text nobody reads.
$bySeverity = $issues | Group-Object Severity | ForEach-Object { "$($_.Name)=$($_.Count)" }
Write-Host "::notice::PSScriptAnalyzer: $($issues.Count) findings across $analyzed/$($ps1.Count) files ($($bySeverity -join ', '))"
} elseif ($failed.Count -gt 0) {
# Empty findings AND failed files is the false zero. Never report
# it as clean: an analyser that could not read a file has said
# nothing about that file, which is not the same as approving it.
Write-Host "::warning::PSScriptAnalyzer produced no findings, but $($failed.Count) of $($ps1.Count) file(s) could not be analysed — this is NOT a clean result"
} else {
Write-Host "PSScriptAnalyzer: no Error/Warning findings across all $analyzed file(s)"
}
}
if ($fail) { throw 'PowerShell parse errors found' }
worker:
name: worker (get.resq.software)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# None of these jobs push; matches every other workflow in the repo.
persist-credentials: false
# Node is pinned rather than inherited. The suite and worker-live import
# worker/src/index.ts directly and rely on native type stripping, which
# only exists from Node 23.6 — on an older default runner image the
# import fails outright. Leaving that to whatever the image ships is the
# kind of unstated assumption this repo pins everything else against.
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '24'
# setup-node v7 added package-manager-cache, which defaults to TRUE
# and turns on npm caching automatically when package.json names a
# package manager. zizmor flags that as cache poisoning and is right
# to: a poisoned cache would feed the job that decides which bytes
# get pinned. Restoring a few seconds of npm download is a trade
# worth making here. Not a suppression — caching is genuinely off.
package-manager-cache: false
- name: Install worker devDependencies
working-directory: worker
run: npm ci --no-audit --no-fund
# Type checking is a separate gate from the tests: tsc proves the source
# is internally consistent, the suite proves it behaves. Neither subsumes
# the other — both of this Worker's production outages typechecked fine.
- name: Typecheck
working-directory: worker
run: npm run typecheck
# Biome, pinned to the same 2.5.2 that resq-software/npm standardises on,
# so this repo and the org's TypeScript repo report the same findings.
# It runs on Biome's defaults with no biome.json on purpose — the org
# config's formatter unquotes the generated PINS keys that gen-pins.sh
# writes back, and the two would fight forever. Defaults lint clean.
- name: Lint (biome)
working-directory: worker
run: npm run lint
# Also validates the inline PINS in worker/src/index.ts against live
# GitHub, so digests that have rotted surface here as a 502 rather than in
# production, where the Worker would correctly refuse to serve anything.
#
# And it backs the single `as PinConfig` assertion on DEFAULT_PINS: the
# compiler checks the shape, this suite checks the strings really are hex,
# by running the exported validatePinConfig over those very pins.
- name: Worker test suite
run: |
set -euo pipefail
node --version
node worker/test/index.test.mjs
required:
name: required
needs: [shellcheck, powershell, worker]
if: always()
runs-on: ubuntu-latest
steps:
- name: Gate on required checks
run: |
set -euo pipefail
for r in \
"shellcheck=${{ needs.shellcheck.result }}" \
"powershell=${{ needs.powershell.result }}" \
"worker=${{ needs.worker.result }}"; do
name=${r%%=*}; res=${r#*=}
echo "$name: $res"
[ "$res" = "success" ] || { echo "::error::required check '$name' did not pass"; exit 1; }
done
echo "ok — all required checks passed"