Skip to content

Publish releases with the runner's gh CLI instead of a third-party action - #427

Merged
tylervick merged 2 commits into
mainfrom
tylervick/gh-cli-release-414
Aug 11, 2026
Merged

Publish releases with the runner's gh CLI instead of a third-party action#427
tylervick merged 2 commits into
mainfrom
tylervick/gh-cli-release-414

Conversation

@tylervick

@tylervick tylervick commented Aug 11, 2026

Copy link
Copy Markdown
Member

Closes #414.

Removes softprops/action-gh-release from the release job and publishes with the runner's preinstalled gh CLI instead. That job holds the Developer ID signing identity, the notarization credentials and a contents: write token — it is the last place in the repository that should be reaching for a third-party action to do something the runner already does. zizmor reports it as superfluous-actions; CodeRabbit raised the same thing on #413.

Matching what the action actually did

I checked each input against the action's source at the pinned SHA and against the two releases this pipeline has produced, rather than against the sketch in the issue.

before after
tag tag_name defaults to github.ref github.ref_name, via $TAG
assets files: application/xchtmlreport-* same glob, expanded by the shell
notes generate_release_notes: true --generate-notes
title not set; action fell back to the tag not set; --generate-notes makes GitHub generate it, and the API returns the tag
prerelease needs.build.outputs.prerelease == 'true' --prerelease appended when $PRERELEASE is true
draft default false not passed
make_latest not sent not sent

On the title: gh without --notes-start-tag sends generate_release_notes: true and no name, so GitHub generates both. I called the generate-notes API against this repo to confirm what it returns for a name — 3.0.0rc2 for a tag of that name, i.e. the tag itself, which is exactly what 3.0.0rc1 and 3.0.0 ended up with.

On the assets: the application/ prefix is real, not incidental — download-artifact with no name creates a directory per artifact. The 3.0.0 run log shows Starting download of artifact to: /home/runner/work/XCTestHTMLReport/XCTestHTMLReport/application, and the glob resolves from the workspace.

Two deliberate differences

Both make the step stricter than what it replaces:

  • --verify-tag. Without it, gh release create will invent the tag off the default branch when the one it is handed does not exist. A tag push is what triggers this job, so a missing tag could only mean something has gone badly wrong; better to stop.
  • An unmatched glob now fails the step. The action's fail_on_unmatched_files defaults to false, so a build that produced no archive would have published a release with no binary attached to it and reported success.

Also: GH_REPO is set because this job deliberately has no checkout, so gh has no git remote to infer the repository from. Everything reaches the script through the environment rather than being interpolated into it.

What has and has not been exercised

Not exercised: the release job has not run. It is guarded on github.event_name == 'push', so the workflow_dispatch dry run skips it entirely — a green run on this PR says nothing at all about this change. That is precisely why #413 deferred it. Do not read CI on this PR as verification.

What I did check, all of it short of running the job:

  • actionlint on the workflow — clean apart from a pre-existing SC2129 on the dry-run summary step, which is untouched and present on main too.
  • shellcheck on the run block in isolation — clean.
  • zizmor --persona=pedantic, before and after: superfluous-actions drops from 2 findings to 1 (the remaining one is create-pull-request in bump_version, out of scope here). Every other finding count is unchanged — in particular no new template-injection, because nothing is interpolated into the script.
  • The action's source at the pinned SHA, for the defaults in the table above.
  • The 3.0.0 run log, for the artifact layout.

The real verification is a 3.0.1rc1 tag, cut immediately after this merges. rc tags are correctly treated as prereleases since #396, do not reach Homebrew, and skip bump_version, so this is the safe rehearsal — and it exercises the whole path for real: build, sign, notarize, publish. I will report the outcome here.

Until that tag has been cut and the release job has actually gone green, this is unverified.

Summary by CodeRabbit

  • Chores
    • Improved the release process for greater reliability.
    • Added validation to ensure releases are created for the correct tag.
    • Automatically generates release notes and identifies prereleases.
    • Uploads packaged binaries as release assets.

The release job is the most sensitive path in the repository: it holds the
Developer ID signing identity, the notarization credentials and a token with
contents: write. Until now it also handed all of that to a third-party action
to do something the runner can already do. zizmor flags it as superfluous and
CodeRabbit raised it on #413.

`gh release create` covers every input the action was given, and the resulting
release is identical in shape: --generate-notes sets both the body and the
title, and GitHub's generate-notes API returns the tag as the name, which is
what the action produced for 3.0.0rc1 and 3.0.0. Neither sends make_latest, so
GitHub's default still applies. Neither drafts.

Two things are deliberately stricter than what they replace. --verify-tag stops
gh from inventing a tag off the default branch if the one it is handed does not
exist; the tag is what triggered the run, so that could only mean something had
gone wrong. And a glob that matches nothing now fails the step, where the action
would happily have published a release with no binary attached to it.

Everything reaches the script through the environment rather than being
interpolated into it, and GH_REPO is set because this job has no checkout for
gh to infer the repository from.

Refs #414
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@tylervick, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 30 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2278e6a8-59a0-4810-9e40-3bd5107eae5a

📥 Commits

Reviewing files that changed from the base of the PR and between cec018d and 701d338.

📒 Files selected for processing (1)
  • .github/workflows/release.yml
📝 Walkthrough

Walkthrough

The release workflow replaces softprops/action-gh-release with gh release create. It passes repository and tag metadata, validates the triggering tag, generates release notes, handles prereleases, and uploads the packaged binary.

Changes

Release publishing

Layer / File(s) Summary
GitHub CLI release flow
.github/workflows/release.yml
The release job uses gh release create with repository and tag values, conditional prerelease handling, generated notes, and a failing binary glob check.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely identifies the replacement of the third-party release action with the runner's gh CLI.
Linked Issues check ✅ Passed The changes satisfy issue #414 by replacing softprops/action-gh-release while preserving tag releases, notes, prereleases, and asset uploads.
Out of Scope Changes check ✅ Passed The changes are limited to the release workflow and directly support the linked issue objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tylervick/gh-cli-release-414

Comment @coderabbitai help to get the list of available commands.

@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: 1

🤖 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.yml:
- Line 172: Update the release creation flow around gh release create to query
whether $TAG already exists before creating it. Treat only a not-found result as
absent; propagate authentication and network failures, and for existing releases
use gh release upload with --clobber only when replacing matching assets,
followed by gh release edit as needed.
🪄 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: d7b58642-5aa5-4b38-b839-aa2adc6749d9

📥 Commits

Reviewing files that changed from the base of the PR and between 822129f and cec018d.

📒 Files selected for processing (1)
  • .github/workflows/release.yml

Comment thread .github/workflows/release.yml
Raised by CodeRabbit on #427. The old action would have replaced the assets
of an already-published release; gh refuses. On a path that publishes signed,
notarized binaries that is the behaviour we want, so it is worth writing down
before someone reads the difference as a regression and adds a fallback.
@tylervick
tylervick merged commit 9105865 into main Aug 11, 2026
8 checks passed
@tylervick
tylervick deleted the tylervick/gh-cli-release-414 branch August 11, 2026 06:23
tylervick added a commit that referenced this pull request Aug 11, 2026
The 3.0.1rc1 tag failed to publish: `no matches found for
application/xchtmlreport-*`.

`actions/download-artifact` with no `name` created a directory per artifact
under v4, which is where the `application/` prefix came from, and 3.0.0 shipped
that way. Under v8 it extracts straight into the workspace instead, so the
archive has been landing one directory up since #404 bumped the version. Both
versions log "An extra directory with the artifact name will be created"; only
v4 does it.

Nothing caught this at the time because the release action it was feeding
defaults `fail_on_unmatched_files` to false. The next release would have
published a signed, notarized build with no binary attached to it and gone
green. It only surfaced now because the gh CLI step that replaced it in #427
fails on an unmatched pattern.

Naming the artifact and its destination pins the layout instead of inheriting
whatever the current major version does with it.
@tylervick

Copy link
Copy Markdown
Member Author

Verified — the release job ran for real

Reporting back as promised. 3.0.1rc1 was cut and the release job executed, so this is no longer resting on a skipped job reporting success.

It took two attempts, and the first one is the more interesting half.

Attempt 1 (9105865) — failed, and found a bug older than this PR

no matches found for `application/xchtmlreport-*`

actions/download-artifact with no name created a directory per artifact under v4 — the origin of the application/ prefix, and how 3.0.0 shipped. Under v8 it extracts straight into the workspace. #404 bumped v4 → v8 after 3.0.0, so the archive had been landing one directory above the glob ever since. Both versions log An extra directory with the artifact name will be created; only v4 does it.

softprops/action-gh-release defaults fail_on_unmatched_files to false, so on main this morning the next release would have published a signed, notarized release with no binary attached and gone green. It surfaced only because this PR made an unmatched pattern fail — the strictness @coderabbitai and I discussed above earned its keep on its first real run. Fixed in #431.

gh failed at pattern expansion before any API call, so attempt 1 created no release and no draft, and the tag was re-pointed at the fix.

Attempt 2 (a40420c) — published

Run 31465915594: build ✅, release ✅, bump_version correctly skipped.

Compared against the 3.0.0rc1 baseline this had to match:

3.0.0rc1 (action) 3.0.1rc1 (gh)
name 3.0.0rc1 3.0.1rc1
notes generated, ## What's Changed generated, ## What's Changed
prerelease true true
draft false false
target_commitish main main
asset xchtmlreport-3.0.0rc1.zip xchtmlreport-3.0.1rc1.zip, state: uploaded

3.0.0 also kept the Latest badge and the rc did not steal it, so the untouched make_latest default still behaves.

The published artifact itself

Downloaded the asset from the release and checked it on macOS, rather than trusting that the notarize step went green:

Architectures in the fat file: x86_64 arm64
--version                    3.0.1rc1
codesign -vvv --strict       valid on disk / satisfies its Designated Requirement
Authority                    Developer ID Application: Tyler Vick (352UZEKYPP)
                             Developer ID Certification Authority / Apple Root CA
spctl -a -vvv -t install     accepted
                             source=Notarized Developer ID

Universal, correctly stamped, Developer ID-signed and notarized — the signing and notarization path is unaffected by the swap, as intended.

Not covered

The full-release branch (a non-rc tag) still hasn't run: --prerelease is omitted rather than passed, and bump_version runs instead of skipping. Those differ from what was exercised only in the boolean, but they have not literally executed. The next real release is the first time that path runs.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@tylervick: This is strong end-to-end verification for the RC release path.

The gh migration preserved the required release metadata and uploaded asset. The downloaded artifact also confirms that signing, notarization, architecture, and version stamping remain correct.

The first attempt found a pre-existing artifact-path defect. The strict unmatched-file failure prevented a green release with no binary. This validates the decision to fail when the asset glob has no matches.

The remaining untested case is correctly limited to the non-rc branch. That branch must omit --prerelease and run bump_version.

🐇 ✅


✏️ Learnings added
Learnt from: tylervick
URL: https://github.com/XCTestHTMLReport/XCTestHTMLReport/pull/427

Timestamp: 2026-08-11T07:13:30.835Z
Learning: In `.github/workflows/release.yml`, after `actions/download-artifact` was upgraded from v4 to v8, unnamed artifact downloads extract directly into the workspace rather than under an artifact-name directory. The release asset glob must therefore match the workspace layout, not `application/xchtmlreport-*`. `gh release create` must retain strict unmatched-asset behavior because it fails before the GitHub API call and prevents publishing a release with no binary.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

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.

Replace softprops/action-gh-release with the runner's gh CLI

1 participant