Repair the release pipeline for 3.0 - #394
Conversation
The release path could not have succeeded as written: - xcode-version: ^16 fails outright. macos-latest is now macos-26, which ships only Xcode 26.x — the same break that just hit ci.yml. - Version.swift was written only by the post-release bump job, so the released binary reported whatever was on main rather than the tag it was built from. Confirmed locally: a binary built from main today reports 2.5.2-pre.bc4e30e, so tagging 3.0.0 would have shipped that. The tag is now stamped in before the build, and a new step fails the release if the binary disagrees with the tag. - rc tags carry no hyphen, so 'contains(github.ref, "-")' never fired and release candidates would have published as full releases. - ::set-output is deprecated; migrated to $GITHUB_OUTPUT. - codesign --deep is deprecated by Apple and is the wrong check for a lone executable; --strict on the binary is. Adds a workflow_dispatch dry run that builds, signs and packages without notarizing or publishing. Previously the only way to test a release was to cut one — which matters here because the signing certificate dates from 2021 and Developer ID certs last five years. Also unblocks the Homebrew step: it was half commented out and five majors stale, and now triggers on non-prerelease publication so rc tags never reach homebrew-core. Action versions: import-codesign-certs v1 -> v7, action-gh-release v1 -> v3, create-pull-request v3 -> v8, version-increment 2022.5.1 -> 2024.10.1, homebrew-bump-formula v3.10.1 -> v8. Version.swift goes to 3.0.0-pre so --HEAD and @main users see that they are on pre-3.0 code carrying the exit-code break. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
📝 WalkthroughWalkthroughThe release workflow now supports manual dry runs, version stamping, binary validation, and push-only publishing. The Homebrew workflow runs for released publications or manual dispatch. The project version is updated to ChangesRelease automation
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant GitHubActions
participant BuildJob
participant UniversalBinary
participant ReleaseJob
participant HomebrewAction
GitHubActions->>BuildJob: Run tagged release or manual dry run
BuildJob->>UniversalBinary: Build and stamp release version
UniversalBinary-->>BuildJob: Return version and signature checks
BuildJob->>ReleaseJob: Provide build metadata and artifact
ReleaseJob->>HomebrewAction: Trigger formula update after released publication
``
</details>
<!-- walkthrough_end -->
<!-- pre_merge_checks_walkthrough_start -->
<details>
<summary>🚥 Pre-merge checks | ✅ 5</summary>
<details>
<summary>✅ Passed checks (5 passed)</summary>
| Check name | Status | Explanation |
| :------------------------: | :------- | :--------------------------------------------------------------------------------------------------------- |
| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. |
| Title check | ✅ Passed | The title clearly describes the main change: repairing the release pipeline for version 3.0. |
| Docstring Coverage | ✅ Passed | No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. |
| Linked Issues check | ✅ Passed | Check skipped because no linked issues were found for this pull request. |
| Out of Scope Changes check | ✅ Passed | Check skipped because no linked issues were found for this pull request. |
</details>
</details>
<!-- pre_merge_checks_walkthrough_end -->
<!-- finishing_touch_checkbox_start -->
<details>
<summary>✨ Finishing Touches</summary>
<details>
<summary>📝 Generate docstrings</summary>
- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> Create stacked PR
- [ ] <!-- {"checkboxId": "3e1879ae-f29b-4d0d-8e06-d12b7ba33d98"} --> Commit on current branch
</details>
<details>
<summary>🧪 Generate unit tests (beta)</summary>
- [ ] <!-- {"checkboxId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} --> Create PR with unit tests
- [ ] <!-- {"checkboxId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} --> Commit unit tests in branch `release-3.0`
</details>
</details>
<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->
---
<sub>Comment `@coderabbitai help` to get the list of available commands.</sub>
<!-- tips_end -->
|
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
.github/workflows/release.yml (1)
136-144: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftUse a triggerable token and the correct release event.
The default
GITHUB_TOKENdoes not trigger downstream workflows. Use a GitHub App token or fine-grained PAT for release creation, or dispatch the Homebrew workflow directly. Set the Homebrew workflow topublishedand gate its job withgithub.event.release.prerelease == false;releasedonly covers prereleases converted to stable releases.
.github/workflows/release.yml:136-144.github/workflows/homebrew-bump.yml:4-7🤖 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.yml around lines 136 - 144, Update .github/workflows/release.yml lines 136-144 to create releases with a triggerable GitHub App token or fine-grained PAT instead of the default GITHUB_TOKEN, or dispatch the Homebrew workflow directly. Update .github/workflows/homebrew-bump.yml lines 4-7 to trigger on the published release event and gate its job with github.event.release.prerelease == false.
🤖 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/homebrew-bump.yml:
- Around line 20-30: Update the “Resolve tag” step to pass the manual version
and release tag through the workflow step’s env mapping, then read them via
quoted Bash variables instead of interpolating expressions into shell source.
Preserve the refs/tags normalization, and validate the resolved TAG before
writing it to GITHUB_OUTPUT so invalid or empty values cannot proceed to
publishing.
In @.github/workflows/release.yml:
- Around line 40-60: Validate VERSION in Generate Metadata against the
repository’s release-tag grammar before writing or reusing it, rejecting invalid
tag values. Pass the validated version through the workflow step’s env and
update the Stamp version into the binary shell command to consume "$VERSION"
rather than interpolating steps.metadata.outputs.version directly into shell
source.
- Line 27: Pin every changed GitHub Action to its exact full commit SHA instead
of a mutable version tag, while preserving the currently intended action version
in an inline comment. Apply this to .github/workflows/release.yml at lines 27,
33, 134-137, 156, and 166, and to .github/workflows/homebrew-bump.yml at line
34.
---
Outside diff comments:
In @.github/workflows/release.yml:
- Around line 136-144: Update .github/workflows/release.yml lines 136-144 to
create releases with a triggerable GitHub App token or fine-grained PAT instead
of the default GITHUB_TOKEN, or dispatch the Homebrew workflow directly. Update
.github/workflows/homebrew-bump.yml lines 4-7 to trigger on the published
release event and gate its job with github.event.release.prerelease == false.
🪄 Autofix
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: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: bd83e9ee-e892-4309-98b3-83acaa37237b
📒 Files selected for processing (3)
.github/workflows/homebrew-bump.yml.github/workflows/release.ymlSources/XCTestHTMLReport/Version.swift
| - name: Resolve tag | ||
| id: tag | ||
| run: | | ||
| if [[ -n "${{ github.event.inputs.version }}" ]]; then | ||
| TAG="${{ github.event.inputs.version }}" | ||
| else | ||
| TAG="${{ github.event.release.tag_name }}" | ||
| fi | ||
| # Tolerate a full ref being pasted into the manual input. | ||
| TAG="${TAG#refs/tags/}" | ||
| echo "tag=${TAG}" >> "$GITHUB_OUTPUT" |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Do not interpolate tag values into shell source.
Lines 23-26 expand manual input and release metadata before Bash parses the script. A crafted value can terminate the quoted assignment and execute commands. Map both values through env, then use quoted shell variables. Validate the resolved tag before publishing it.
Proposed fix
- name: Resolve tag
id: tag
+ env:
+ INPUT_VERSION: ${{ inputs.version }}
+ RELEASE_TAG: ${{ github.event.release.tag_name }}
run: |
- if [[ -n "${{ github.event.inputs.version }}" ]]; then
- TAG="${{ github.event.inputs.version }}"
+ if [[ -n "$INPUT_VERSION" ]]; then
+ TAG="$INPUT_VERSION"
else
- TAG="${{ github.event.release.tag_name }}"
+ TAG="$RELEASE_TAG"
fi🧰 Tools
🪛 zizmor (1.29.0)
[error] 23-23: code injection via template expansion (template-injection): may expand into attacker-controllable code
(template-injection)
[error] 24-24: code injection via template expansion (template-injection): may expand into attacker-controllable code
(template-injection)
[error] 26-26: 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/homebrew-bump.yml around lines 20 - 30, Update the
“Resolve tag” step to pass the manual version and release tag through the
workflow step’s env mapping, then read them via quoted Bash variables instead of
interpolating expressions into shell source. Preserve the refs/tags
normalization, and validate the resolved TAG before writing it to GITHUB_OUTPUT
so invalid or empty values cannot proceed to publishing.
Source: Linters/SAST tools
|
|
||
| - name: Configure Signing | ||
| uses: Apple-Actions/import-codesign-certs@v1 | ||
| uses: Apple-Actions/import-codesign-certs@v7 |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
printf '%s\n' '--- release workflow ---'
cat -n .github/workflows/release.yml | sed -n '1,180p'
printf '%s\n' '--- homebrew workflow ---'
cat -n .github/workflows/homebrew-bump.yml | sed -n '1,70p'
printf '%s\n' '--- changed workflow summary ---'
git diff --stat -- .github/workflows/release.yml .github/workflows/homebrew-bump.yml
printf '%s\n' '--- changed action references ---'
git diff --unified=3 -- .github/workflows/release.yml .github/workflows/homebrew-bump.yml | rg -n '^[+-].*uses:|^@@' || trueRepository: XCTestHTMLReport/XCTestHTMLReport
Length of output: 8891
🏁 Script executed:
python3 - <<'PY'
import re
from pathlib import Path
files = [
Path(".github/workflows/release.yml"),
Path(".github/workflows/homebrew-bump.yml"),
]
use_re = re.compile(r"^\s*uses:\s*([^#\s]+)")
sha_re = re.compile(r"^[0-9a-fA-F]{40}$")
for path in files:
print(path)
for number, line in enumerate(path.read_text().splitlines(), 1):
match = use_re.match(line)
if not match:
continue
ref = match.group(1).rsplit("@", 1)[-1]
kind = "full commit SHA" if sha_re.fullmatch(ref) else "tag or branch ref"
print(f"{number}: {match.group(1)} -> {kind}")
PYRepository: XCTestHTMLReport/XCTestHTMLReport
Length of output: 714
Pin each changed action to a full commit SHA.
These version tags can change after review. The actions access signing credentials or release tokens. Retain the intended version in an inline comment.
.github/workflows/release.yml: lines 27, 33, 134–137, 156, and 166..github/workflows/homebrew-bump.yml: line 34.
🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis
[failure] 27-27: Use full commit SHA hash for this dependency.
📍 Affects 2 files
.github/workflows/release.yml#L27-L27(this comment).github/workflows/release.yml#L33-L33.github/workflows/release.yml#L134-L137.github/workflows/release.yml#L156-L156.github/workflows/release.yml#L166-L166.github/workflows/homebrew-bump.yml#L34-L34
🤖 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.yml at line 27, Pin every changed GitHub Action to
its exact full commit SHA instead of a mutable version tag, while preserving the
currently intended action version in an inline comment. Apply this to
.github/workflows/release.yml at lines 27, 33, 134-137, 156, and 166, and to
.github/workflows/homebrew-bump.yml at line 34.
Source: Linters/SAST tools
| if [[ "$GITHUB_REF" =~ ^refs/tags/ ]]; then | ||
| VERSION="${GITHUB_REF#refs/tags/}" | ||
| else | ||
| VERSION="0.0.0-dryrun" | ||
| fi | ||
| # `rc` tags carry no hyphen, so a `contains(github.ref, '-')` test | ||
| # never fired for them and release candidates shipped as full releases. | ||
| if [[ "$VERSION" == *rc* ]]; then PRERELEASE=true; else PRERELEASE=false; fi | ||
| { | ||
| echo "version=${VERSION}" | ||
| echo "prerelease=${PRERELEASE}" | ||
| echo "bin_path=.build/universal/release/xchtmlreport" | ||
| echo "archive_name=xchtmlreport-${VERSION}.zip" | ||
| } >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Stamp version into the binary | ||
| # Version.swift was only ever written by the post-release bump job, so the | ||
| # released binary reported whatever happened to be on main rather than the | ||
| # tag it was built from. Stamping here makes `--version` match the tag. | ||
| run: | | ||
| echo "let version = \"${{ steps.metadata.outputs.version }}\"" \ |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Validate release tags before reuse.
Line 60 interpolates a tag-derived output into shell source. A tag containing a quote or shell syntax can alter the generated command. Validate VERSION against the repository release-tag grammar in Generate Metadata. Pass the validated value through env and use "$VERSION" in shell steps.
Proposed fix
- name: Stamp version into the binary
+ env:
+ VERSION: ${{ steps.metadata.outputs.version }}
run: |
- echo "let version = \"${{ steps.metadata.outputs.version }}\"" \
+ printf 'let version = "%s"\n' "$VERSION" \
> Sources/XCTestHTMLReport/Version.swift🧰 Tools
🪛 zizmor (1.29.0)
[info] 60-60: 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.yml around lines 40 - 60, Validate VERSION in
Generate Metadata against the repository’s release-tag grammar before writing or
reusing it, rejecting invalid tag values. Pass the validated version through the
workflow step’s env and update the Stamp version into the binary shell command
to consume "$VERSION" rather than interpolating steps.metadata.outputs.version
directly into shell source.
Source: Linters/SAST tools
The bump_version job ran on rc tags and proposed a version that went backwards: after 3.0.0rc1 it opened a PR setting Version.swift to 2.5.2-pre.53adfaf, regressing main from 3.0.0-pre. Cause: rc tags use the form X.Y.ZrcN, which is not valid semver (that would be X.Y.Z-rcN). version-increment cannot parse it, so it fell back to the last parseable tag (2.5.1) and incremented that. Same root cause as the prerelease-detection bug fixed in #394. Bumping after a release candidate is wrong regardless of parsing, so the job now skips prereleases outright, reusing the build job's prerelease output as the single source of truth.
…400) The release:types:[released] trigger added in #394 can never fire. The release is published by softprops/action-gh-release using GITHUB_TOKEN, and GitHub does not start new workflow runs from events that token creates. Confirmed on the 3.0.0 release: it published, and Brew Bump recorded zero runs. homebrew-bump.yml becomes a reusable workflow_call, invoked directly as a job of the release, guarded so prereleases never reach homebrew-core. Manual workflow_dispatch is kept for re-runs. Also hardens the Resolve tag step, which SonarCloud and CodeRabbit both flagged: it interpolated a workflow_dispatch input straight into a run block, in a job holding HOMEBREW_BUMP_ACCESS_TOKEN. The input now reaches the shell only through the environment and is validated against the tag shapes the release workflow accepts. Note this does not by itself unblock the 3.0.0 formula bump: the HOMEBREW_BUMP_ACCESS_TOKEN secret has expired and returns Bad credentials. It must be regenerated before any bump can succeed.
The release workflow could not have succeeded as written. Found while preparing to tag 3.0.
Fatal today
xcode-version: ^16fails outright.macos-latestis nowmacos-26, which ships only Xcode 26.x — the identical break that just hitci.yml. The release build cannot get past step 3.The binary's version never had to match the tag.
Version.swiftwas written only by the post-releasebump_versionjob, so the released binary reported whatever happened to be onmain. Confirmed locally:So tagging
3.0.0today would have published a binary announcing itself as2.5.2-pre.bc4e30e. The tag is now stamped intoVersion.swiftbefore the build, and a new step fails the release if the built binary disagrees with the tag.rctags would have shipped as full releases.prereleasewas computed ascontains(github.ref, '-'), but the accepted tag patterns areX.Y.ZandX.Y.ZrcN— neither contains a hyphen. Now keyed onrc.Also fixed
::set-output→$GITHUB_OUTPUT(deprecated; still worked as of the 2.5.1 release, but on borrowed time)codesign --deepdropped — deprecated by Apple, and the wrong check for a single executable;--stricton the binary is righthomebrew-bump.ymlwas half commented out and five majors stale. It now triggers onrelease: types: [released], which fires only for non-prereleases, sorctags never reach homebrew-core. Manual dispatch still available.Action versions, all badly behind:
import-codesign-certsv1→v7,action-gh-releasev1→v3,create-pull-requestv3→v8,version-increment2022.5.1→2024.10.1,homebrew-bump-formulav3.10.1→v8.New: a dry run
workflow_dispatchwithdry_runbuilds, signs and packages without notarizing or publishing. Previously the only way to test a release was to cut one.This matters more than it sounds:
BUILD_CERTIFICATE_BASE64was added in October 2021, and Developer ID certificates are valid for five years. If it has expired or is about to, the dry run surfaces that before a tag is pushed rather than halfway through a release.Version
Version.swift→3.0.0-pre, so anyone onbrew install xctesthtmlreport --HEADormint install …@mainsees they are on pre-3.0 code carrying the exit-code break, instead of a string claiming 2.5.x.Not changed
bump_versionstill increments the patch component. Major and minor bumps stay deliberate manual decisions made before tagging — automating a major bump is not something a release pipeline should do on its own.🤖 Generated with Claude Code
Summary by CodeRabbit
Release Process
Maintenance