Repository navigation
ci(v3): harden the release workflow before its first run - #5815
Conversation
release-v3.yml was added in #5318 and has never executed: every alpha has shipped through nightly-release-v3.yml. Reviewing it before the beta turns up four things that would each surface as a failed or misleading release. The largest: none of the six Apple secrets it reads (APPLE_SIGNING_CERT, APPLE_CERT_PASSWORD, APPLE_SIGNING_IDENTITY, APPLE_NOTARIZE_USER, APPLE_NOTARIZE_PASSWORD, APPLE_TEAM_ID) exist on the repository. On the first real run the macOS job would fail inside `security import` after three platforms had already built, the release job would never run, and nothing would be published. Changes: - New preflight job that fails in seconds rather than ten minutes in. It resolves the tag for both trigger paths, checks out that tag, and verifies v3/internal/version/version.txt matches it. version.txt is embedded, so a mismatch ships a binary that misreports its own version with no error at all. - Preflight also reports exactly which Apple secrets are missing, and either stops or, with the new allow_unsigned_macos input, continues with the signing and notarization steps skipped. No silent downgrade either way. - Publish SHA256SUMS alongside the binaries. Users currently have no way to verify a download. - Attest build provenance for every binary. The npm package already ships provenance; the binaries lagging behind it is a gap users can see. - New draft input, so the whole path can be rehearsed end to end and the resulting release deleted without ever publishing anything. The build jobs now take their checkout ref from preflight's resolved tag, so all three platforms provably build the same verified source. Verified: actionlint clean; the version-guard, missing-secret detection and checksum generation logic each exercised locally under bash -eo pipefail. Not exercised in CI, because doing that means creating a tag and a release. Claude-Session: https://claude.ai/code/session_01FigQqUQbNu9ngE4a2CSNm8
WalkthroughThe release workflow adds preflight tag and version validation, optional unsigned macOS publishing, consistent platform checkouts, conditional signing and notarization, checksum generation, provenance attestations, and draft release support. ChangesRelease workflow
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Workflow
participant Preflight
participant PlatformBuilds
participant ReleaseJob
participant GitHubRelease
Workflow->>Preflight: Resolve tag, version, and signing mode
Preflight->>PlatformBuilds: Provide tag and sign_macos
PlatformBuilds->>ReleaseJob: Upload platform artifacts
ReleaseJob->>GitHubRelease: Publish checksums, attestations, and release
Possibly related PRs
Suggested labels: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
leaanthony
left a comment
There was a problem hiding this comment.
The missing-secrets finding is the important one: with no APPLE_* secrets on the repo, the first tag push would have built three platforms, died in security import, and published nothing. Failing in preflight with the names listed is the right shape.
Version guard is worth having independently of that — an embedded version.txt that disagrees with the tag produces a binary that lies quietly, which is the worst class of release bug.
Checksums and provenance bring the binaries in line with what the npm package already does.
Approving on review; the path itself still needs the draft dry run before we trust it.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
.github/workflows/release-v3.yml (1)
68-70: 🔒 Security & Privacy | 🔵 Trivial | 🏗️ Heavy liftCheckouts persist the GitHub token in the workspace for the whole job. None of these
actions/checkout@v4steps setpersist-credentials: false, so.git/configretains a usable token for the entire job (readable bygo build, module code, or any other step that runs afterward) even though none of these jobs need to push. Same root cause at all four sites; fix them together.
.github/workflows/release-v3.yml#L68-L70: addpersist-credentials: falseto the preflight checkout..github/workflows/release-v3.yml#L132-L136: addpersist-credentials: falseto the build-linux checkout..github/workflows/release-v3.yml#L169-L173: addpersist-credentials: falseto the build-windows checkout..github/workflows/release-v3.yml#L201-L205: addpersist-credentials: falseto the build-sign-macos checkout.Based on learnings, this hardening should be applied consistently across all workflows in
.github/workflows/in one repo-wide pass rather than piecemeal per file, so treat this as a follow-up rather than blocking this PR.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.github/workflows/release-v3.yml around lines 68 - 70, Add persist-credentials: false to each actions/checkout@v4 step in .github/workflows/release-v3.yml at lines 68-70, 132-136, 169-173, and 201-205; apply the same hardening consistently to all checkout steps across .github/workflows/ in a repo-wide follow-up pass.Sources: Learnings, Linters/SAST tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/release-v3.yml:
- Around line 53-84: Replace direct GitHub expression interpolation in the shell
commands of the “Resolve tag and pre-release flag” and “Version file agrees with
the tag” steps with step-level env variables, then reference those variables
inside the scripts. Preserve the existing tag and pre-release resolution
behavior while ensuring inputs.tag, github.ref_name, and steps.meta.outputs.tag
are passed through env rather than embedded in shell code.
---
Nitpick comments:
In @.github/workflows/release-v3.yml:
- Around line 68-70: Add persist-credentials: false to each actions/checkout@v4
step in .github/workflows/release-v3.yml at lines 68-70, 132-136, 169-173, and
201-205; apply the same hardening consistently to all checkout steps across
.github/workflows/ in a repo-wide follow-up pass.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 371ba289-590f-4e73-873f-7afaa7bff1d3
📒 Files selected for processing (1)
.github/workflows/release-v3.yml
| - name: Resolve tag and pre-release flag | ||
| id: meta | ||
| run: | | ||
| if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then | ||
| TAG="${{ inputs.tag }}" | ||
| PRE="${{ inputs.pre_release }}" | ||
| else | ||
| TAG="${{ github.ref_name }}" | ||
| # Tags with a hyphen suffix (v3.0.0-beta.1) are pre-releases. | ||
| if [[ "$TAG" == *"-"* ]]; then PRE=true; else PRE=false; fi | ||
| fi | ||
| echo "tag=$TAG" >> "$GITHUB_OUTPUT" | ||
| echo "pre_release=$PRE" >> "$GITHUB_OUTPUT" | ||
| echo "Releasing $TAG (pre-release: $PRE)" | ||
|
|
||
| - uses: actions/checkout@v4 | ||
| with: | ||
| ref: ${{ steps.meta.outputs.tag }} | ||
|
|
||
| - name: Version file agrees with the tag | ||
| run: | | ||
| TAG="${{ steps.meta.outputs.tag }}" | ||
| FILE_VERSION="$(tr -d '[:space:]' < v3/internal/version/version.txt)" | ||
| # version.txt is embedded into the binary, so it is what `wails3 | ||
| # version` reports. If it disagrees with the tag, the release ships a | ||
| # binary that lies about which version it is, silently. | ||
| if [[ "$FILE_VERSION" != "$TAG" ]]; then | ||
| echo "::error::v3/internal/version/version.txt says '$FILE_VERSION' but the tag is '$TAG'." | ||
| echo "::error::The tagged commit must carry the matching version. Re-tag after bumping it." | ||
| exit 1 | ||
| fi | ||
| echo "version.txt matches the tag: $FILE_VERSION" |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Unquoted expression interpolation into run: is a shell-injection vector.
TAG="${{ inputs.tag }}", TAG="${{ github.ref_name }}", and TAG="${{ steps.meta.outputs.tag }}" (lines 57, 60, 74) expand attacker-influenceable strings directly into the shell script rather than passing them through env:. Git tag names are permitted to contain shell metacharacters (`, $, (, ), ;) — only control chars, space, ~^:?*[\, @{, and slash edge-cases are disallowed — so anyone able to push a tag (or trigger workflow_dispatch with a crafted tag input) can run arbitrary commands in this job, which later checks out and builds the release. The release job further downstream (TAG: ${{ needs.preflight.outputs.tag }} at line 338) already uses the safer env: pattern — the same pattern should be used here.
🔒 Proposed fix using env vars
- name: Resolve tag and pre-release flag
id: meta
+ env:
+ DISPATCH_TAG: ${{ inputs.tag }}
+ DISPATCH_PRE: ${{ inputs.pre_release }}
+ PUSH_REF_NAME: ${{ github.ref_name }}
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
- TAG="${{ inputs.tag }}"
- PRE="${{ inputs.pre_release }}"
+ TAG="$DISPATCH_TAG"
+ PRE="$DISPATCH_PRE"
else
- TAG="${{ github.ref_name }}"
+ TAG="$PUSH_REF_NAME"
if [[ "$TAG" == *"-"* ]]; then PRE=true; else PRE=false; fi
fi - name: Version file agrees with the tag
+ env:
+ TAG: ${{ steps.meta.outputs.tag }}
run: |
- TAG="${{ steps.meta.outputs.tag }}"
FILE_VERSION="$(tr -d '[:space:]' < v3/internal/version/version.txt)"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| - name: Resolve tag and pre-release flag | |
| id: meta | |
| run: | | |
| if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then | |
| TAG="${{ inputs.tag }}" | |
| PRE="${{ inputs.pre_release }}" | |
| else | |
| TAG="${{ github.ref_name }}" | |
| # Tags with a hyphen suffix (v3.0.0-beta.1) are pre-releases. | |
| if [[ "$TAG" == *"-"* ]]; then PRE=true; else PRE=false; fi | |
| fi | |
| echo "tag=$TAG" >> "$GITHUB_OUTPUT" | |
| echo "pre_release=$PRE" >> "$GITHUB_OUTPUT" | |
| echo "Releasing $TAG (pre-release: $PRE)" | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ steps.meta.outputs.tag }} | |
| - name: Version file agrees with the tag | |
| run: | | |
| TAG="${{ steps.meta.outputs.tag }}" | |
| FILE_VERSION="$(tr -d '[:space:]' < v3/internal/version/version.txt)" | |
| # version.txt is embedded into the binary, so it is what `wails3 | |
| # version` reports. If it disagrees with the tag, the release ships a | |
| # binary that lies about which version it is, silently. | |
| if [[ "$FILE_VERSION" != "$TAG" ]]; then | |
| echo "::error::v3/internal/version/version.txt says '$FILE_VERSION' but the tag is '$TAG'." | |
| echo "::error::The tagged commit must carry the matching version. Re-tag after bumping it." | |
| exit 1 | |
| fi | |
| echo "version.txt matches the tag: $FILE_VERSION" | |
| - name: Resolve tag and pre-release flag | |
| id: meta | |
| env: | |
| DISPATCH_TAG: ${{ inputs.tag }} | |
| DISPATCH_PRE: ${{ inputs.pre_release }} | |
| PUSH_REF_NAME: ${{ github.ref_name }} | |
| run: | | |
| if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then | |
| TAG="$DISPATCH_TAG" | |
| PRE="$DISPATCH_PRE" | |
| else | |
| TAG="$PUSH_REF_NAME" | |
| # Tags with a hyphen suffix (v3.0.0-beta.1) are pre-releases. | |
| if [[ "$TAG" == *"-"* ]]; then PRE=true; else PRE=false; fi | |
| fi | |
| echo "tag=$TAG" >> "$GITHUB_OUTPUT" | |
| echo "pre_release=$PRE" >> "$GITHUB_OUTPUT" | |
| echo "Releasing $TAG (pre-release: $PRE)" | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ steps.meta.outputs.tag }} | |
| - name: Version file agrees with the tag | |
| env: | |
| TAG: ${{ steps.meta.outputs.tag }} | |
| run: | | |
| FILE_VERSION="$(tr -d '[:space:]' < v3/internal/version/version.txt)" | |
| # version.txt is embedded into the binary, so it is what `wails3 | |
| # version` reports. If it disagrees with the tag, the release ships a | |
| # binary that lies about which version it is, silently. | |
| if [[ "$FILE_VERSION" != "$TAG" ]]; then | |
| echo "::error::v3/internal/version/version.txt says '$FILE_VERSION' but the tag is '$TAG'." | |
| echo "::error::The tagged commit must carry the matching version. Re-tag after bumping it." | |
| exit 1 | |
| fi | |
| echo "version.txt matches the tag: $FILE_VERSION" |
🧰 Tools
🪛 zizmor (1.26.1)
[warning] 68-70: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false
(artipacked)
[error] 57-57: code injection via template expansion (template-injection): may expand into attacker-controllable code
(template-injection)
[error] 60-60: code injection via template expansion (template-injection): may expand into attacker-controllable code
(template-injection)
[info] 74-74: code injection via template expansion (template-injection): may expand into attacker-controllable code
(template-injection)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/release-v3.yml around lines 53 - 84, Replace direct GitHub
expression interpolation in the shell commands of the “Resolve tag and
pre-release flag” and “Version file agrees with the tag” steps with step-level
env variables, then reference those variables inside the scripts. Preserve the
existing tag and pre-release resolution behavior while ensuring inputs.tag,
github.ref_name, and steps.meta.outputs.tag are passed through env rather than
embedded in shell code.
Source: Linters/SAST tools
Picks up the contribution terms (wailsapp#5816), the release workflow hardening (wailsapp#5815), the runtime/CLI version lockstep (wailsapp#5820) and the crash fixes merged since the first sync. Clean merge, no conflicts. The branch was six commits behind again within hours of the first sync, which is why this is now an audit blocker rather than something to remember: a release branch that quietly ages is how a release ships without fixes everyone believes are in it. Claude-Session: https://claude.ai/code/session_01FigQqUQbNu9ngE4a2CSNm8
Why now
release-v3.ymlwas added in #5318 and has never executed — 0 runs, ever. Every alpha has shipped throughnightly-release-v3.yml. With a beta coming, the tag-triggered release path is about to run for the first time on the release that matters most.Reviewing it turns up four things that would each produce a failed or misleading release.
The largest: none of the six Apple secrets the workflow reads exist on this repository. Current repo secrets are
CHANGELOG_PUSH_TOKEN,CLAUDE_CODE_OAUTH_TOKEN,CROWDIN_PERSONAL_TOKEN,NPM_TOKEN,OPENROUTER_API_KEY,SEMGREP_APP_TOKEN,SPONSORS_TOKEN,WAILS_REPO_TOKEN— noAPPLE_*, and none inherited from the org. On the first real run the macOS job fails insidesecurity importafter three platforms have already built, thereleasejob (whichneedsit) never runs, and nothing is published.Changes
v3/internal/version/version.txtmatches it. That file isgo:embed-ed, so it is whatwails3 versionreports: a mismatch ships a binary that misreports itself with no error at all.allow_unsigned_macosinput — continues with signing and notarization skipped. No silent downgrade in either direction.SHA256SUMSpublished with the binaries. Today a user has no way to verify a download.draftinput, so the entire path can be rehearsed end to end and the resulting release deleted, without publishing anything.Verification
actionlintclean.v3.0.0-beta.1against the currentversion.txt(v3.0.0-alpha2.117), accepts the matching tag.bash -eo pipefailwith the secrets both unset and set (the&&-in-loop idiom does not tripset -ein this position — checked, not assumed).sha256sum -cround-trip.The dry run this enables, and one trap in it
With
draft: truethe release can be rehearsed without publishing. The tag it needs is not free of consequence though: a throwaway tag must sort below the real beta under semver precedence, or it becomes what@latestresolves to for anything that considers prereleases.v3.0.0-test.1would outrankv3.0.0-beta.1(test>betaalphabetically) and hijack resolution — the same failure mode as the oldalpha.98-tuitag. A tag in the existingalpha2.series (e.g.v3.0.0-alpha2.999) sorts belowbeta.1and is safe.https://claude.ai/code/session_01FigQqUQbNu9ngE4a2CSNm8
Summary by CodeRabbit
New Features
Bug Fixes