Skip to content

Harden GitHub Actions workflows - #130

Merged
pancakereport merged 10 commits into
mainfrom
harden-workflows
Oct 16, 2026
Merged

pancakereport merged 10 commits into
mainfrom
harden-workflows

Conversation

@pancakereport

@pancakereport pancakereport commented Oct 14, 2026 •

Copy link
Copy Markdown

GitHub flagged test.yml as possibly malicious, and its runs on main have been held for approval since. The cause was a pattern already in the workflow: toJSON(secrets) and toJSON(vars) were passed to a third-party action on a tag its owner can move (oNaiPs/secrets-to-env-action@v1). If that tag were compromised, every repo secret would leak.

Changes

  • test.yml and a11y.yml: remove secrets-to-env-action. Each job's env: block now maps only the variables and secrets the app reads. The fork-PR dummy defaults are folded in with ||.
  • Pin third-party actions to commit SHAs: codecov-action and the google-github-actions steps in pr-staging.yml, which can get GCP deploy credentials. Each pin is the commit its current tag points to, so the actions run the same code as before.
  • Add .github/dependabot.yml so pinned versions get weekly update PRs:
    • github-actions: the SHA pins in the workflows.
    • pip: the pinned packages in both requirements.txt and requirements-dev.txt (Dependabot reads every requirements .txt file in the directory). For example, pip-audit currently reports PYSEC-2026-4312 in Werkzeug==3.1.8, fixed in 3.1.9. It only affects Windows/NTFS, so not this app, but the Audit workflow's pip-audit || true never fails CI on it.
    • Cooldown: Dependabot only proposes a release once it's 7 days old (its default is 3), so a malicious release is more likely to be caught before it shows up here.
    • Pins only help if updates are reviewed: Dependabot doesn't vet releases. Before merging one of its PRs, check the release it moves to (for an action, that the new SHA is the release it claims to be).
  • Skip preview deploys on Dependabot PRs (pr-staging.yml): a Dependabot PR runs its own updated workflows before review, and the deploy job's permissions: grant id-token: write even on Dependabot runs. A bumped google-github-actions pin would otherwise run, unreviewed, with the staging deployer's GCP access. That account has project-wide roles in staging-cdss-web-apps, including run.admin and iam.serviceAccountUser. Dependabot PRs only change action pins and requirement pins, which the test workflows still check; a preview isn't worth giving them that access. To preview a package bump anyway, open your own PR with it.
  • Narrow permissions:
    • a11y.yml goes down to contents: read; it never comments.
    • pr-staging.yml moves id-token, issues and pull-requests write from the whole workflow onto only the jobs that need them.
  • test.yml uses actions/checkout@v7 to match the other workflows.
  • Replace pytest-coverage-comment with Codecov flags:
    • Before: Codecov already commented coverage on every PR, but merged the unit and e2e uploads into one number, and the separate MishaKav/pytest-coverage-comment action added two more comments.
    • Now: each Codecov upload is labeled with its test type (flags: unit / flags: e2e), and codecov.yml adds the flags table to Codecov's comment, so one comment shows both. Codecov waits for both uploads (after_n_builds: 2) before commenting or setting its checks, so neither ever reflects only the unit tests.
    • Per-file report: still on the run's summary page, now for every run rather than only fork PRs.
    • Permissions: with the comment action gone, test.yml drops pull-requests: write and issues: write and is read-only.

audit.yml, lint.yml and codeql.yml are read-only and use only GitHub's own actions, so they're unchanged.

Note

If tests later need a new repo variable or secret, it has to be added to the env: block in test.yml / a11y.yml. The jobs no longer receive every variable and secret automatically.

🤖 Generated with Claude Code

pancakereport and others added 3 commits October 14, 2026 10:48
Passing toJSON(secrets) to a third-party action on a mutable tag hands
every repo secret to whoever controls that tag, and GitHub flagged the
workflow as possibly malicious. Map just the vars/secrets the app reads
via job-level env (folding in the fork-PR defaults) and pin
pytest-coverage-comment to the v1.12.3 commit SHA.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pin codecov and the google-github-actions steps (which hold GCP deploy
rights via OIDC) to the commits their current tags point at, and add
Dependabot to keep the pins updated. Drop the unused write permissions
from a11y, and grant pr-staging's OIDC and comment permissions only to
the jobs that need them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Coverage

Coverage for unit tests for Python 3.14
FileStmtsMissCoverMissing
server
   __init__.py52492%55, 65–67, 87
   cache.py6183%12
   forms.py1381490%24–26, 42–44, 71, 94, 169–172, 179, 187
   models.py2112688%60, 64, 67, 70, 73, 76–81, 143–152, 182, 200, 257, 260, 283, 327, 341
   views.py76751333%39, 49–85, 95–129, 140–144, 161–170, 186–203, 214–221, 230–252, 263–276, 285, 296–299, 317–318, 320–325, 327–328, 342–366, 375–397, 406–433, 450–452, 462–504, 544–574, 583–613, 633, 647–650, 652–655, 664–694, 703–720, 751–755, 764–810, 816–868, 880–882, 889–910, 918–939, 971, 995, 1040–1049, 1051, 1066–1067, 1072–1085, 1099, 1104, 1131–1132
server/controllers
   __init__.py821779%27–28, 36–39, 43, 72, 76, 85, 101–110
   auth_controllers.py402342%22–51, 57–59
   health_controllers.py21767%18–19, 24–28
server/services/auth
   __init__.py28389%25–26, 30
server/services/canvas
   __init__.py806124%18–25, 29, 33, 37, 43, 55–64, 69–105, 109–124, 128
   fake_canvas.py392633%9, 12, 15, 20–25, 28–29, 37–46, 49–56
server/services/core
   assign.py76396%25, 28, 114
   data.py613444%13–18, 22–26, 30–34, 38–40, 44–55, 90–92, 101–102, 106–107
   export.py27678%37–39, 41–43
   room.py611870%11–33, 57, 70–71, 74
   student.py192597%33, 37–38, 47, 284
server/services/email
   __init__.py50394%47, 74, 92
   smtp.py1002575%20, 60–62, 84–92, 104–105, 117–124, 128–130, 141–142
server/services/email/templates
   __init__.py33197%20
server/typings
   exception.py34488%42, 53, 59–60
server/utils
   date.py7186%30
   misc.py16756%13–19
TOTAL239180266% 

Tests Skipped Failures Errors Time
300 0 💤 0 ❌ 0 🔥 12.432s ⏱️

@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Coverage

Coverage for e2e tests for Python 3.14
FileStmtsMissCoverMissing
server
   __init__.py521277%13, 16–22, 27, 55, 65–67, 87
   cache.py6183%12
   forms.py1382185%24–26, 42–44, 69–71, 93–99, 169–172, 179, 187
   models.py2117067%27, 60, 64, 67, 70, 73, 76–81, 111, 126–132, 136, 140, 143–152, 161–162, 169–170, 177–182, 189–197, 200, 214, 217–219, 222, 226, 238, 242, 246, 250–251, 257, 260, 280, 283, 304, 309, 314, 319, 324, 327, 341
   views.py76765614%38–40, 49–85, 95–129, 140–144, 161–170, 186–203, 214–221, 230–252, 263–276, 285, 296–299, 310–329, 342–366, 375–397, 406–433, 442–453, 462–504, 515–523, 531–535, 544–574, 583–613, 622–655, 664–694, 703–720, 726–729, 739–746, 751–755, 764–810, 816–868, 874–884, 889–910, 915–945, 950–954, 962–972, 976–978, 983–1016, 1026–1030, 1037–1061, 1066–1067, 1072–1085, 1094, 1099, 1104, 1120–1136, 1146–1147
server/controllers
   __init__.py824644%26–44, 47–49, 60–87, 90, 101–110, 113
   auth_controllers.py402635%14–16, 22–51, 57–59
   dev_login_controllers.py584424%17–30, 35–52, 60–61, 68–79, 84–101
   health_controllers.py211243%9, 14–19, 24–28
server/services/auth
   __init__.py28775%25–26, 30, 62–63, 68–69
server/services/canvas
   __init__.py806124%18–25, 29, 33, 37, 43, 55–64, 69–105, 109–124, 128
   fake_canvas.py392633%9, 12, 15, 20–25, 28–29, 37–46, 49–56
server/services/core
   assign.py765824%10–13, 16, 19, 22, 25, 28, 36, 40–46, 54–55, 63, 67, 84–149, 163–180
   data.py614133%13–18, 22–26, 30–34, 38–40, 44–55, 62, 72–74, 79, 90–92, 96–97, 101–102, 106–107
   export.py272219%11–46
   layout_preview.py925738%81, 85, 90, 94, 105–110, 116–119, 128–130, 140, 147, 159–162, 167–171, 175–177, 199–200, 206–209, 218–222, 227, 239–259
   room.py61568%11–33, 41–89
   student.py19215917%33, 37–38, 42, 46–48, 57–60, 70–72, 79, 88–93, 104–107, 110–114, 122–136, 140–143, 152–157, 161, 165, 172–307
server/services/csv
   __init__.py261735%11–12, 20, 27–30, 40, 54–56, 63–68
server/services/email
   __init__.py503824%23–40, 44–81, 90–101
   smtp.py1007822%20, 28–41, 49–50, 54–63, 68–93, 99–131, 136–142
server/services/email/templates
   __init__.py332330%9–11, 14–17, 20, 30–41, 45–47
server/services/google
   __init__.py553929%21–36, 40–43, 47–53, 57–76
server/typings
   exception.py341362%20–36, 42, 47, 53, 59–60, 69, 72
server/utils
   date.py7443%17–19, 30
   misc.py161319%5–9, 13–19, 23
TOTAL2391160033% 

Tests Skipped Failures Errors Time
14 0 💤 0 ❌ 0 🔥 42.340s ⏱️

@codecov

codecov Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 66.45%. Comparing base (77fe711) to head (b934a2c).

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #130   +/-   ##
=======================================
  Coverage   66.45%   66.45%           
=======================================
  Files          29       29           
  Lines        2391     2391           
=======================================
  Hits         1589     1589           
  Misses        802      802           
Flag Coverage Δ
e2e 33.08% <ø> (?)
unit 66.45% <ø> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

Copy link
Copy Markdown

🚀 Staging Preview Deployed!

URL: https://preview-seating-pr-130-137456085712.us-west1.run.app

@pancakereport
pancakereport marked this pull request as draft October 14, 2026 18:55

- id: auth
uses: google-github-actions/auth@v3
uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's honestly not clear to me that the security of this kind of pin vs the potential for breaking / needing to pay attention to a CI tool in the future is worth it (given this is a trusted org...)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The added workflow in this PR .github/dependabot.yml should partially automate updating pins (i.e. open PRs when pins can be updated). We'd need to review/merge still.

Comment thread .github/workflows/test.yml Outdated
Comment thread .github/workflows/a11y.yml
pancakereport and others added 4 commits October 16, 2026 15:33
A Dependabot PR runs its own updated workflows before anyone reviews
it, and the preview deploy grants an OIDC token for the staging
deployer, so a malicious action release would get that GCP access
unreviewed. Skip both preview jobs for Dependabot PRs, and have
Dependabot wait 7 days after a release before proposing it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add the pip ecosystem for the repo root, which covers both
requirements.txt and requirements-dev.txt, on the same weekly schedule
and 7-day cooldown as the actions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@pancakereport
pancakereport marked this pull request as ready for review October 16, 2026 23:09
pancakereport and others added 3 commits October 16, 2026 16:42
Codecov already comments coverage on every PR, but merged the unit and
e2e uploads into one number. Label each upload with its test type and
show Codecov's flags table, so its comment covers both, and drop
pytest-coverage-comment along with test.yml's write permissions. The
per-file report now goes to the run's summary page for every run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codecov commented and set its checks as soon as the unit upload was
processed, then updated them about 30 seconds later for e2e. Have it
wait for both uploads first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@pancakereport
pancakereport merged commit 8113919 into main Oct 16, 2026
15 checks passed
@pancakereport
pancakereport deleted the harden-workflows branch October 16, 2026 23:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants