Bind workflow expressions and drop persisted credentials - #6258
Merged
Conversation
ChrisJBurns
force-pushed
the
harden-ci-expressions
branch
from
August 10, 2026 16:56
d67336d to
94c7894
Compare
`${{ }}` is substituted into a run: block before the shell parses it,
so the value becomes part of the command text rather than an argument
to it. Binding to env: and referencing "$VAR" makes the shell treat it
as data regardless of content.
image-build-and-publish already bound VERSION in env: on four of its
steps and then re-interpolated the same expression in the script; those
now use "$VERSION". test-e2e-lifecycle was interpolating two values that
were already shell variables, one from a workflow-level env: and one
written to $GITHUB_ENV, so the expressions were redundant.
Also stops these three workflows persisting the token in .git/config.
All of them declare contents: read, so no job in them can push, which
makes the credential unusable as well as unnecessary.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
security-events: write sat at the workflow level in security-scan.yml, granting it to govulncheck, which never uploads SARIF. It moves to the two jobs that do. run-on-main.yml still grants the workflow the same permission when it calls it, so nothing is lost. issue-triage.yml declares contents: read, so no job in it can push and the checkout token is unusable as well as unnecessary. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #6258 +/- ##
==========================================
+ Coverage 72.70% 72.71% +0.01%
==========================================
Files 742 742
Lines 77299 77299
==========================================
+ Hits 56201 56211 +10
+ Misses 17132 17126 -6
+ Partials 3966 3962 -4 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
ChrisJBurns
force-pushed
the
harden-ci-expressions
branch
from
August 10, 2026 17:06
94c7894 to
7b1339b
Compare
This was referenced Aug 10, 2026
rdimitrov
approved these changes
Aug 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
${{ }}is substituted into arun:block before the shell parses it, so the value lands as command text rather than as an argument. Binding toenv:and referencing"$VAR"makes the shell treat it as data whatever it contains. This removes 16 such interpolations..git/configfor the whole job. All four declarecontents: read, so no job in them can push — the credential is unusable as well as unnecessary. That's 8 checkouts.Most of this is deleting redundancy rather than adding plumbing:
image-build-and-publish.ymlVERSION: ${{ steps.version-string.outputs.tag }}inenv:and then re-interpolated the same expression in the script. Those now use"$VERSION"; the five steps that lacked the binding get it.test-e2e-lifecycle.yml${{ env.YARDSTICK_IMAGE }}and${{ env.VMCP_IMAGE }}were already shell variables — one from a workflow-levelenv:, one written to$GITHUB_ENVby an earlier step. The interpolation was redundant; now$YARDSTICK_IMAGE/${VMCP_IMAGE}.release-notes.ymlsteps.tag.outputs.tagandsteps.verify.outputs.filebound asTAG/FILE, matching the pattern the "Post notes as a comment" step in the same file already used.skills-build-and-publish.ymlpersist-credentials: false.issue-triage.ymlpersist-credentials: false.security-scan.ymlsecurity-events: writesat at the workflow level, granting it togovulncheck, which never uploads SARIF. Moved to the two jobs that do.Expressions in
with:blocks are untouched — those aren't shell input and aren't what this is about.Totals against
main:template-injection45 → 29,artipacked17 → 9,excessive-permissions2 → 1.Rebased onto
mainnow that #6251, #6254 and #6255 have merged.Part of #6253
Type of change
Test plan
Unit tests (
task test)E2E tests (
task test-e2e)Linting (
task lint-fix)Manual testing (describe below)
Confirmed via each workflow's
permissions:that all four declarecontents: read, sopersist-credentials: falsecannot break a push — none is possible.All workflows still parse as YAML;
actionlintreports 12 findings before and after, all pre-existing.zizmortemplate-injection45 → 29,artipacked17 → 9 andexcessive-permissions2 → 1, and norun:block in any of the four files contains an expression any more.skills-build-and-publish.ymlandtest-e2e-lifecycle.ymlrun on pull requests, so this PR exercises them directly.Does this introduce a user-facing change?
No.
Special notes for reviewers
image-build-and-publish.ymlandrelease-notes.ymlare not exercised by this PR — the first runs on push tomainand on release, the second onworkflow_run. Both surface within one merge tomainrather than at release time, which is why they're grouped here rather than with the release workflows. Worth a careful read of theVERSION/TAGsubstitutions in particular.skills-build-and-publish.ymlwas excluded from Stop persisting git credentials in CI workflow checkouts #6255 as "publish path". That was wrong — it is called fromrun-on-pr.ymland runs on every PR, so it belongs with the CI batch. Corrected here.helm-publish.yml,create-release-tag.yml,create-release-pr.ymlorreleaser.ymlis touched. Those remain deferred per zizmor findings backlog: pins, expressions and checkout credentials #6253 — including the two remaining High-severitytemplate-injectionfindings, which all live there.Generated with Claude Code