Uh oh!
There was an error while loading. Please reload this page.
ci(actionlint): lint this repo's own workflows + clear the 29 pre-existing findings - #66
Conversation
actionlint (with its native shellcheck pass) reported 29 findings across 8
of this repo's reusable workflows. None had ever been checked by CI. Fixed
the real ones, suppressed the correct-code ones at the line.
Real fixes:
- advance-deploy-env.yml (SC2086): the git log range was an unquoted
$RANGE. Rewritten as an array, NOT simply quoted -- on the first push to
a branch the range is two argv entries (--max-count=50 and the SHA), so
"$RANGE" would have handed git one bogus argument and broken PR
discovery on that path entirely. Verified both push shapes produce
identical PR lists before and after.
- wip-limit-check.yml (SC2034): REPO_NAME was assigned and never used --
a copy-paste leftover from the workflows that do use it. Removed.
- wip-limit-check.yml (SC2188): `> /tmp/items.txt` is a redirection with
no command. Now `: > /tmp/items.txt`, matching the form already used in
kanban-reconcile.yml. Byte-identical truncate.
- kanban-reconcile.yml + fr-pass-comment.yml (SC2129): consecutive
redirects to $GITHUB_OUTPUT grouped into a single `{ ... } >>` block.
Verified byte-identical output.
Suppressed as correct code (23x SC2016): single-quoted GraphQL documents
where $org / $num / $p / $i / $f / $o are GraphQL variables interpolated
server-side. Expansion is exactly what must NOT happen. Each gets a
per-line `# shellcheck disable=SC2016` with the reason -- no file-wide
directive and no actionlint config exclusion, so future code is not
silently covered.
kanban-reconcile.yml (SC2034, `label`): NOT dead code. It is the 4th
`read` variable and exists to stop `read` folding field 4 of moves.tsv
into $reason -- removing it would append "<TAB><category>" to every
[OK]/[FAIL] log line. Kept, documented, suppressed.
Behaviour is otherwise unchanged: the remaining diff is comments.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>This repo publishes 16 reusable workflows that every other repo consumes
at `@main`, and it had no CI of its own -- nothing validated a workflow
file before it merged and went org-wide. This closes that gap.
actionlint parses each workflow, type-checks every ${{ }} expression,
validates the runs-on / uses / needs wiring, and runs shellcheck over
every `run:` block.
A hard gate from day one (any finding fails the job), which is only
affordable because the preceding commit cleared the backlog -- the tree
is at zero findings, so the job is green and can be marked a required
status check immediately.
Notes:
- actionlint is pinned by version AND verified against a pinned SHA-256
of the release tarball, rather than via a wrapper action that downloads
it for us. actions/checkout is pinned to a full commit SHA.
- A step asserts shellcheck is present: actionlint silently skips every
shell check when the binary is missing, which would leave the gate
green while checking far less.
- Includes a concurrency group so a re-push supersedes the previous run.
- Requests only `contents: read`.
- The `paths:` filter means the job does not run on PRs touching no
workflow file; the header documents what that implies before marking
the check required.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>LukasWodka
commented
Jul 25, 2026
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 8516ac9. Configure here.
LukasWodka
commented
Jul 25, 2026
Independent verification — and my brief was wrong on one pointI asked for the SC2086 finding to be fixed by "quoting it". That would have broken this workflow. Confirmed against On the first push to a branch (line 95), Likewise verified on
One thing to settle before this is marked requiredThe workflow's So it's one or the other:
My preference is the former: at 7 seconds, the saved runner time isn't worth a class of stuck PR, and this repo's whole problem was having no gate at all. Flagged rather than decided, since it changes branch-protection setup. This generalises to epic #930 — the "flip Also worth keeping: the silent-degradation guard in this job. actionlint skips every shell check and still exits 0 when |
A required status check with a `paths:` filter never runs on a PR that touches nothing matching it, and GitHub then waits forever for a status that cannot arrive -- so the PR is stuck. The job takes ~7s, which is far cheaper than that failure mode. Raised on the PR as the one thing to settle before marking it required. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
LukasWodka
commented
Jul 25, 2026
Pushed the Rationale is now in the workflow header: a required check filtered by path never runs on a PR that touches nothing matching it, and GitHub then waits indefinitely for a status that cannot arrive. At ~7s per run, dropping the filter is far cheaper than that class of stuck PR. The same principle applies to the shared bugbot run |
Uh oh!
There was an error while loading. Please reload this page.
Summary
This repo publishes 16 reusable workflows that every other repo in the org consumes at
@main, and it had no CI of its own — nothing linted or validated a workflow file before it merged and went org-wide. This PR closes that gap and clears the backlog that closing it exposed.Two commits, deliberately separate:
fix(workflows)— clear all 29 actionlint findings across 8 pre-existing workflows.ci(actionlint)— add the gate.Follows #65 (
code-quality.yml), which added the org's first shared code quality workflow. That file is already clean and is not touched here. Part of epic tracebloc/backend#930.actionlint findings: 29 → 0
Verified locally with actionlint 1.7.12 + shellcheck 0.11.0: zero findings across all 17 workflow files, and all 17 still parse as YAML.
advance-deploy-env.yml(SC2086) was the only finding whose naive fix would have broken production automation. The git log range was an unquoted$RANGE:The unquoted expansion was load-bearing: on the first push to a branch the range must reach
git logas two separate arguments. Simply quoting it to"$RANGE"— the obvious "fix" — would have passed--max-count=50 <sha>as a single bogus argument, and PR discovery on that path would have failed silently. Rewritten as an array instead:Verified against a real git repo with PR-style commit subjects: both push shapes produce identical PR lists before and after, and the naive-quote variant does indeed fail
git log. Everything else in this PR is lint-only or comments.Verdict on
kanban-reconcile.yml'slabel— real bug or dead code?Neither. It is load-bearing, and removing it would have introduced a bug.
labelis field 4 ofmoves.tsv(the move category). It is never referenced in that step, which is why SC2034 fires — but it is not dead:readassigns the remainder of the line to its last variable. Droplabeland$reasonsilently absorbs field 4, so every log line becomes[OK] backend#12 closed-not-merged (Code review)<TAB>cancelled. Confirmed by running both forms.The field itself is consumed — by
awk -F'\t' '{print $4}'in the Per-label summary step. So nothing is silently not happening: the reconciler's per-category summary works today.Kept as-is, with a comment explaining the sink and a targeted
# shellcheck disable=SC2034. No behavioural change.Other fixes (all lint-only, verified equivalent)
wip-limit-check.yml(SC2034) —REPO_NAMEwas assigned and never used, a copy-paste leftover from the workflows that do use it (fr-gate,advance-deploy-env, and others pass it as-F repo=). This one uses$REPO_FULLdirectly. Genuinely dead → removed.wip-limit-check.yml(SC2188) —> /tmp/items.txtis a redirection with no command. Not a broken line: it is a deliberate truncate before the append loop. Now: > /tmp/items.txt, which is both the portable form and the one already used inkanban-reconcile.yml. Byte-identical.kanban-reconcile.yml+fr-pass-comment.yml(SC2129) — grouped consecutive>> "$GITHUB_OUTPUT"redirects into one{ ... } >>block. Chose grouping over suppression: it is the cleaner code, and the appended bytes are identical (verified withcmp).The 23 SC2016 suppressions
All are single-quoted GraphQL documents where
$org/$num/$p/$i/$f/$oare GraphQL variables interpolated server-side bygh api graphql -F. Preventing shell expansion is precisely the point.Each gets a
# shellcheck disable=SC2016on the line immediately above the specific command, with the reason inline. No file-wide directive, no global disable, and no.github/actionlint.yamlexclusion — a repo-wide exclusion would silently cover future code as well as the lines it was written for. Verified that the directives are line-scoped: an identical unsuppressed expression on the next line still reports.The gate itself (
.github/workflows/actionlint.yml)pull_requestonpaths: ['.github/workflows/**'], plusworkflow_dispatchfor on-demand runs.code-quality.yml, which shipssoft-fail: truebecause it targets repos with unlinted backlogs.)checksums.txtasset), rather than via a wrapper action that downloads it for us.actions/checkoutis pinned to a full commit SHA (11d5960= v4, the same pin feat(quality): shared code-quality reusable workflow + house-rules checker #65 uses).concurrency— included; a re-push supersedes the previous run.permissions: contents: readonly.shellcheckis on the runner and passes-shellcheck shellcheckexplicitly. actionlint skips every shell check and still exits 0 when the binary is missing; measured on a deliberately broken workflow, that is 3 findings vs 0. Without this guard the gate could quietly stop checking the thing this PR is mostly about.Before requiring the check
The
paths:filter means the job does not run on a PR touching no workflow file (a README-only change, say), and a required check that never runs leaves such a PR waiting for a status forever. Either drop the filter when you mark it required, or keep the filter and leave the check advisory. This is documented in the workflow header rather than decided here.Test plan
actionlint(1.7.12 + shellcheck 0.11.0): 29 findings → 0, exit 0advance-deploy-env.ymlrange rewrite: identical PR extraction on both push shapes, in a real git repo"$RANGE"quote confirmed to breakgit log(proves the array was required)$GITHUB_OUTPUTcontent (cmp)labelremoval confirmed to corrupt[OK]/[FAIL]lines (proves it is load-bearing)Reviewer notes
Behaviour of the kanban automation is unchanged except for the
advance-deploy-env.ymlarray rewrite described above, which restores intended behaviour under quoting. No workflow's trigger, permissions, status transitions, or gate logic were altered.code-quality.ymlfrom #65 is untouched.🤖 Generated with Claude Code
Note
Medium Risk
Touches org-wide reusable deploy/kanban automation; the advance-deploy-env array change fixes first-push PR discovery but is the one path worth extra verification.
Overview
Adds a blocking
actionlintjob on every pull request so reusable workflows are validated before they ship at@main. The installer is version- and SHA-256–pinned,shellcheckpresence is asserted (so shell checks cannot silently skip), and findings are annotated on the diff and in the job summary. The trigger deliberately has nopaths:filter so a required check cannot leave unrelated PRs stuck waiting.Clears 29 pre-existing findings across eight workflows: mostly per-line
SC2016suppressions on GraphQLgh apisnippets, grouped>> "$GITHUB_OUTPUT"writes (SC2129), removal of unusedREPO_NAMEinwip-limit-check.yml,: >for file truncate (SC2188), and a documentedlabelsink inkanban-reconcile.yml’sreadloop (SC2034).The only behavioral workflow change is in
advance-deploy-env.yml: thegit logrange is a bash array ("${RANGE[@]}") instead of an unquoted string, so the first-push path still passes--max-count=50and the SHA as separate arguments—naive quoting would have broken PR discovery on that path.Reviewed by Cursor Bugbot for commit c7bcdf6. Bugbot is set up for automated code reviews on this repo. Configure here.