Skip to content

Repair the release pipeline for 3.0 - #394

Merged
tylervick merged 1 commit into
mainfrom
release-3.0
Aug 7, 2026
Merged

tylervick merged 1 commit into
mainfrom
release-3.0

Conversation

@tylervick

@tylervick tylervick commented Aug 7, 2026

Copy link
Copy Markdown
Member

The release workflow could not have succeeded as written. Found while preparing to tag 3.0.

Fatal today

xcode-version: ^16 fails outright. macos-latest is now macos-26, which ships only Xcode 26.x — the identical break that just hit ci.yml. The release build cannot get past step 3.

The binary's version never had to match the tag. Version.swift was written only by the post-release bump_version job, so the released binary reported whatever happened to be on main. Confirmed locally:

$ swift build -c release && .build/.../xchtmlreport --version
2.5.2-pre.bc4e30e

So tagging 3.0.0 today would have published a binary announcing itself as 2.5.2-pre.bc4e30e. The tag is now stamped into Version.swift before the build, and a new step fails the release if the built binary disagrees with the tag.

rc tags would have shipped as full releases. prerelease was computed as contains(github.ref, '-'), but the accepted tag patterns are X.Y.Z and X.Y.ZrcN — neither contains a hyphen. Now keyed on rc.

Also fixed

  • ::set-output$GITHUB_OUTPUT (deprecated; still worked as of the 2.5.1 release, but on borrowed time)
  • codesign --deep dropped — deprecated by Apple, and the wrong check for a single executable; --strict on the binary is right
  • homebrew-bump.yml was half commented out and five majors stale. It now triggers on release: types: [released], which fires only for non-prereleases, so rc tags never reach homebrew-core. Manual dispatch still available.

Action versions, all badly behind: 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.

New: a dry run

workflow_dispatch with dry_run builds, 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_BASE64 was 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.swift3.0.0-pre, so anyone on brew install xctesthtmlreport --HEAD or mint install …@main sees they are on pre-3.0 code carrying the exit-code break, instead of a string claiming 2.5.x.

Not changed

bump_version still 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

    • Added support for manually previewing releases without publishing or notarizing them.
    • Improved release metadata and version verification, including prerelease detection.
    • Updated build signing and validation for more reliable release artifacts.
    • Homebrew updates now run automatically after stable releases while retaining manual triggering.
  • Maintenance

    • Updated the application version to 3.0.0-pre.

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]>
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The 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 3.0.0-pre.

Changes

Release automation

Layer / File(s) Summary
Dry-run build and binary validation
.github/workflows/release.yml, Sources/XCTestHTMLReport/Version.swift
Manual runs generate version metadata, stamp Version.swift, and validate the universal binary version and signature.
Publishing and versioning gates
.github/workflows/release.yml
Notarization, release creation, and version bumping run only for push events. Manual runs report the packaged artifact and skipped publishing steps.
Release-triggered Homebrew bump
.github/workflows/homebrew-bump.yml
The workflow resolves a normalized tag from manual input or the release event and passes it to the v8 Homebrew action.

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 -->
Loading

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 lift

Use a triggerable token and the correct release event.

The default GITHUB_TOKEN does 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 to published and gate its job with github.event.release.prerelease == false; released only 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

📥 Commits

Reviewing files that changed from the base of the PR and between 41f7019 and 656a257.

📒 Files selected for processing (3)
  • .github/workflows/homebrew-bump.yml
  • .github/workflows/release.yml
  • Sources/XCTestHTMLReport/Version.swift

Comment on lines +20 to +30
- 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"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 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:|^@@' || true

Repository: 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}")
PY

Repository: 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.

See more on https://sonarcloud.io/project/issues?id=XCTestHTMLReport_XCTestHTMLReport&issues=AZ_dvzoLYwCGQwzThDXV&open=AZ_dvzoLYwCGQwzThDXV&pullRequest=394

📍 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

Comment on lines +40 to +60
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 }}\"" \

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 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

@tylervick
tylervick merged commit 53adfaf into main Aug 7, 2026
5 of 6 checks passed
@tylervick
tylervick deleted the release-3.0 branch August 7, 2026 19:56
tylervick added a commit that referenced this pull request Aug 7, 2026
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.
tylervick added a commit that referenced this pull request Aug 7, 2026
…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.
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.

1 participant