ci(release): pass tag message via env to prevent shell injection (fix empty v2.6.0-testnet)#303
Merged
Conversation
The 'Check deploy intent' step interpolated the multi-line tag message
directly into a bash script via ${{ steps.tag_info.outputs.tag_message }}.
When the tag body contains lines that look like shell commands (e.g. the
literal token 'deadcode' from a merge-commit trailer), those lines are
executed as bash. That is exactly what happened on v2.6.0-testnet and
v2.6.0-rc1: the release job aborted with exit code 127 ("deadcode:
command not found") before any artifacts were uploaded, leaving the
GitHub Release with zero downloadable assets and sn-manager unable to
roll the testnet fleet forward.
Fix: expose the tag message through the step's `env:` block and read it
as a shell variable inside the script. This is the GHA-recommended way
to consume untrusted multi-line values.
Verified locally with yaml.safe_load; runtime behavior is a strict
subset of the previous logic (single grep, same output key).
Root cause captured for future readers in the inline comment.
Signed-off-by: Matee ullah Malik <mateeullahmalik@hotmail.com>
a-ok123
approved these changes
Jul 2, 2026
mateeullahmalik
added a commit
that referenced
this pull request
Jul 5, 2026
Follow-up to #303. The env-based approach in that PR triggered a GHA startup failure (workflow accepted, 0 jobs scheduled, run completed in the same second with conclusion=failure) because a step-level `env:` value cannot safely carry the multi-line `tag_message` output — GHA rejects the workflow at dispatch time. Verified failure signature on 8eae29e: run 28627254564 / 28627293599 — status=completed, conclusion=failure, jobs=[], run_started_at == updated_at (single-second dispatch reject). Fix: perform the `[no-deploy]` grep INSIDE the `Get tag information` step, where `$TAG_MESSAGE` already exists as a plain bash variable. Export the single-line `draft` boolean from the same step. The multi-line `tag_message` output is preserved (still consumed by the release-body heredoc, which is safe because it uses `<<'EOT'`). Update the two downstream references from steps.deploy_check.outputs.draft to steps.tag_info.outputs.draft. Root cause and reasoning preserved as an inline comment. Signed-off-by: Matee ullah Malik <mateeullahmalik@hotmail.com>
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
The release pipeline on tag pushes has been broken since (at least)
v2.6.0-rc1, and again onv2.6.0-testnet. Both releases were published to GitHub with zero downloadable assets, which meanssn-manageron the testnet fleet cannot fetch the update tarball — the testnet SNs are stuck onv2.5.0-testnet.Root cause
In
.github/workflows/build&release.yml, theCheck deploy intentstep did:${{ steps.tag_info.outputs.tag_message }}is expanded by the workflow engine before the shell sees it, so the tag/commit body is spliced verbatim into the bash script. For thev2.6.0-testnettag, the merge-commit body included lines likeVerified via deadcode diff on origin/master ...— bash then tried to execute the tokendeadcodeand aborted with exit127. Everything afterCheck deploy intent(including the artifact upload step) was skipped, so the release stayed empty.Failing run: https://github.com/LumeraProtocol/supernode/actions/runs/28608759515
Log excerpt:
Fix
Pass the tag message via the step's
env:map instead of${{ }}interpolation. This is the GHA-recommended pattern for consuming multi-line and/or untrusted values insiderun:scripts. Behavior is otherwise unchanged (still a single case-insensitive grep for[no-deploy], still writesdraft=…to$GITHUB_OUTPUT).Why not just re-run the failing job?
Because the workflow runs on every tag push and the failure is deterministic for any tag whose message happens to contain a token that shells look up as a command. Same bug would take down every future testnet/mainnet release cut from a merge commit with a rich message.
Blast radius
.github/workflows/build&release.yml, only thereleasejob, only theCheck deploy intentstep.softprops/action-gh-releasestep, or release-body composition.Prepare Release Body(line 208) also references${{ steps.tag_info.outputs.tag_message }}but does so inside a quoted heredoc (<<'EOT') which suppresses expansion, so it is not vulnerable to the same failure mode and is intentionally left untouched to keep the diff minimal.Rollback
Revert the commit; there's no state or migration involved.
Post-merge action
Once this lands on
master, I will:v2.6.0-testnettag already contains all the intended code; only the release assets are missing.supernode-linux-amd64+supernode-linux-amd64.tar.gzto the existingv2.6.0-testnetrelease (built from tag SHA095b2f2, matching the tag exactly). This is whatsn-managerneeds../testnet_version_check.sh.Verification of this PR
python3 -c "import yaml; yaml.safe_load(open('.github/workflows/build&release.yml'))"→ OK.