Skip to content

Add release-publish: the GitHub release for a version at a commit, with a title, notes and assets - #16

Merged
glaubersilva merged 14 commits into
soft-5188-release-merge-actionfrom
soft-5188-release-publish-action
Nov 4, 2026
Merged

glaubersilva merged 14 commits into
soft-5188-release-merge-actionfrom
soft-5188-release-publish-action

Conversation

@glaubersilva

@glaubersilva glaubersilva commented Nov 2, 2026 •

Copy link
Copy Markdown
Member

Related

Part of SOFT-5188. Answers three review comments on #10: publish inputs, make_latest, the token and on: release.

Summary

release-publish creates the GitHub release for a version at a commit: the tag and the release in one step, with a title, notes from a file and assets. A repository's deploy workflow runs it after release-merge (#10) and its build, at the commit release-merge leaves on the base branch (base-sha), so the release is published only once the build exists and carries it. A repository that merges by hand runs it alone, with a branch as target.

Changes

  • Inputs: version, target (a commit hash; a branch name is resolved to its tip when the action runs, so a workflow that builds from a checkout passes that checkout's commit), title (the version by default), notes-file (a file in the checkout; GitHub's generated notes by default), files (assets, one path per line), token (default github.token). Outputs: published (true or skipped), release-url, sha.
  • With files, the release is created as a draft, the files are uploaded, and the draft is published last. A draft makes no tag, so a failed upload is undone by deleting the draft; every path is checked before anything is created. A repository with immutable releases needs this order, since a published release cannot be changed.
  • make_latest follows version order against the repository's current latest release, so a hotfix on an older line does not take "Latest" from a newer release.
  • A release that already exists is skipped; a tag that exists without a release is refused.
  • One template, templates/workflows/release.yml: the deploy workflow the dashboard dispatches, under the name it looks for by default. It runs release-merge, checks out base-sha (the merges go through the API, so no checkout is needed before it), leaves room for the build, then release-publish with target: ${{ steps.merge.outputs.base-sha }}, as GiveWP's deploy workflow does. Its first step fails when the run was not started on the branch it is asked to merge, so the live environment's branch policy covers what is merged and published; the dashboard starts the run on the release branch, a hand dispatch passes --ref. The merge-only template is gone (Add release-merge: the release branch into the base branch, then the base branch into develop #10), and the publish-only shape (a release a person merged by hand) is a snippet in release-publish's README, with the checked-out commit as target.
  • READMEs: what each action needs from the token (the merge: writing straight to the base branch and develop; the publish: creating the tag, so a tag ruleset should block deletion and updates, not creation), that a personal access token or a GitHub App token does trigger on: release while github.token does not, that the caller template's environment with required reviewers is the human gate, and the one release github.token cannot create: one at a commit that changes a workflow file relative to the default branch, which after release-merge never happens (the published commit is the base branch's tip) and is the hand-merged publish's case. The release-merge README and manifest point at release-publish; its code does not change here.
  • develop-branch: '' has to be written in the caller: a workflow_dispatch input left empty takes its default, so the empty value cannot be passed through one. Said in the release-merge README.
  • Tests in tests/actions/release-publish/release-publish.test.js, against the recorded Octokit and real files in a temporary directory. The recording fetch keeps a non-JSON body (an asset upload) as it was sent.

How to test

  1. In a plugin repository with main and develop, copy templates/workflows/release.yml to .github/workflows/, with both uses: pointing at this branch's commit, add a build step between the two actions and uncomment files: with the path it writes.
  2. Cut release/x.y.z from main, bump the version files .puprc lists on it (release-prep does this) and push.
  3. Run gh workflow run release.yml --ref release/x.y.z -f branch=release/x.y.z -f version=x.y.z (without --ref the first step fails and nothing is merged): main and develop get the merges, the release x.y.z exists with the file attached, and its tag points at the merge commit on main.
  4. Run it again: the job succeeds, both merges report "Already up to date" and the release is reported as already published.
  5. For the publish alone: a workflow with the README's snippet, run for a version lower than the current latest release, creates it at the checked-out commit and "Latest" stays on the newer one.
  6. node --test (332 tests). docker run --rm -v "$PWD:/repo" -w /repo rhysd/actionlint:latest templates/workflows/release.yml.

The steps were run on the two private test plugins, pinned to 356cf3e, this stack's head. The deploy callers of both and GiveWP's release-dashboard-deploy.yml have the shape of release.yml: release-merge, the checkout of base-sha, the build, then release-publish.

Case Run
With an asset: 2.1.45 created as a draft, the zip uploaded, published at the merge commit, "Latest" plugin-a #41695347631
Without assets: 1.1.16 created directly at the merge commit with generated notes plugin-b #41695353208
Same release again: reported as already published and skipped plugin-b #41695528101
Publish only, a hotfix on an older line (2.1.38.3 from a branch cut at 2.1.38): tagged at the branch's tip, titled "Hotfix Release 2.1.38.3", notes from a file, "Latest" stays on 2.1.46 plugin-a #41696332353
Merge conflict on develop: release-merge fails, release-publish never runs, no tag and no release; after the conflict is resolved by hand the rerun publishes 1.1.17 plugin-b #41695982945, #41696173936
Run started on main for release/M26.api5: refused by the first step, nothing merged; the same dispatch with --ref goes through plugin-b #41696338490, #41696173936
The whole cycle from the release dashboard: prepare, RC build, then release-merge, the checkout of base-sha and this action; 2.1.47 published with the zip, 1.1.18 without, both tagged at the merge commit plugin-a #41702581004, plugin-b #41702184209
A release whose branch changed a workflow file: 1.1.15 published with github.token at the merge commit (at e6cee72; release-publish.js is unchanged since) plugin-b #41532958048
After the second review, at dd3544c: a full cycle on both plugins (prepare, RC, deploy) with head: ${{ github.sha }} passed to release-merge. 2.1.48 with the zip and 1.1.19 without, each at its merge commit; the log says "The notes cover the changes since 2.1.47" / "since 1.1.18", and the release body links compare/2.1.47...2.1.48. Rerun of 2.1.48: already published, nothing merged, nothing published plugin-a #41894355947, plugin-b #41894362698, rerun plugin-a #41894510185
A run dispatched at an older commit of the release branch (head ≠ tip): refused by release-merge before any read or merge, "release/head-check is at 086e24d…, not at 56c9989…, the commit this run was started on"; dispatched at the tip, the check passes plugin-a #41891495478, plugin-a #41891523196
At d10f4e0, with a draft a person made by hand for 2.1.49 sitting in the repository: the deploy left it alone, created its own marked draft, uploaded the zip and published 2.1.49 under the plain title; 1.1.20 the same without assets plugin-a #41903702963, plugin-b #41903708578

Stack created with GitHub Stacks CLI • Give Feedback 💬

Summary by CodeRabbit

  • New Features

    • Added a manually triggered release workflow for a selected branch and version.
    • Releases can include a title, notes, and uploaded assets; releases with assets are published after uploads complete.
    • Existing releases are skipped, while a tag without an associated release prevents publication.
    • Releases can be published after a build at the commit produced by the merge.
    • Older version releases do not replace the current latest version.
    • Runs are serialized without cancelling an in-progress run.
  • Documentation

    • Added guidance and examples for configuring release publication, choosing a target commit, handling retries, and setting up tokens.

…le, notes and assets

release-merge published the release as its second step, right after the
merges and before the caller built anything, so a release went public
without its build and a failed build left a published release behind;
with immutable releases that could not be corrected. Publishing is now
the release-publish action, which the caller runs after its build at the
commit release-merge leaves on the base (its base-sha output).

release-publish takes a title (the version by default), a notes file
(GitHub's generated notes by default) and assets, one path per line.
With assets the release is created as a draft, the files are uploaded,
and the draft is published last: a draft makes no tag, so an upload that
fails is undone by deleting it. A hotfix on an older line no longer
takes Latest from a newer release (make_latest by version order). A
release that already exists is skipped, as before. validateVersion
moved to the shared lib for both actions.

release-merge loses the publish input and the published / release-url
outputs; the template runs the two actions with the build between them.

SOFT-5188
@linear

linear Bot commented Nov 2, 2026

Copy link
Copy Markdown

SOFT-5188

@coderabbitai

coderabbitai Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • Resume reviews
  • Trigger review
Walkthrough

The PR adds a composite action that publishes GitHub releases, a manually triggered workflow that invokes it, and tests for release creation and asset uploads. It also updates release-merge guidance to build before publishing at the merge action’s base-sha.

Changes: Release Publishing

Layer / File(s) Summary
Action inputs and release data
.github/actions/release-publish/action.yml, .github/actions/release-publish/release-publish.js, .github/actions/release-publish/README.md
The action defines version, target, title, notes, asset, and token inputs, plus publishing outputs. It validates inputs, resolves a commit or branch, reads optional files, and determines whether the release is marked latest. The README documents the action and its execution.
Release checks and publishing
.github/actions/release-publish/release-publish.js, tests/actions/release-publish/*, tests/support/actions.js
The action skips an existing release and refuses a tag without a release. It creates a release directly or uploads assets to a draft before publishing it. Tests cover API behavior, outputs, upload failures, input validation, and recorded request bodies.
Workflow integration
templates/workflows/release-publish.yml, templates/workflows/release-merge.yml, .github/actions/release-merge/action.yml, .github/actions/release-merge/README.md, README.md
The new manual workflow invokes the publishing action with the checked-out commit SHA. Release-merge guidance directs callers to build before publishing at base-sha. The project checklist marks the action complete.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant PublishWorkflow
  participant ReleasePublishAction
  participant GitHubAPI
  participant CheckoutFiles
  PublishWorkflow->>ReleasePublishAction: Pass version, target, title, and token
  ReleasePublishAction->>GitHubAPI: Resolve target and check release and tag
  ReleasePublishAction->>CheckoutFiles: Read optional notes and assets
  ReleasePublishAction->>GitHubAPI: Create release or draft
  ReleasePublishAction->>GitHubAPI: Upload assets and publish draft when assets exist
Loading

Possibly related PRs

Suggested labels: enhancement

Suggested reviewers: d4mation, larodiel

Merge Risk: Moderate · previous review ef353

The new release-publish template can race with the release-merge template when a repository uses both, can publish a branch other than the one the live environment's branch policy approved, and fails when the target commit changes workflow files. Fix these in the templates before merge. None of them affects repositories that use the action with the template's default scope.

Pre-merge checks: Passed 5
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly identifies the main change: adding the release-publish action to create a GitHub release for a version at a commit with title, notes, and assets.
Docstring Coverage Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentat…
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.
Full details: Docstring Coverage

Explanation

Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 12 functions across 3 files. (2 skipped: 2 unsupported.)

Finishing Touches
Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Enterprise
  • Run ID: cd77b438-299e-461d-bf7d-4f9688cf39a6
Commits

Reviewing files that changed from the base of the PR and between e6c9816 and ef353a6.

Files selected for processing: 2
  • templates/workflows/release-merge.yml
  • templates/workflows/release-publish.yml
Files skipped from review as they are similar to previous changes: 1
  • templates/workflows/release-merge.yml

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 8 remain after this review.

@glaubersilva
glaubersilva added this pull request to stack #17 November 2, 2026 19:35
glaubersilva added a commit to impress-org/givewp that referenced this pull request Nov 2, 2026
…p attached, through release-publish

release-merge no longer publishes: the toolbox split the release into its
own action, release-publish, run after the build at the commit the merge
left on master. The release is created with give.zip already on it and
only once the wp.org deploy succeeded, so a failed build or deploy leaves
no release behind. Pinned to plugin-toolbox 657eb50 (stellarwp/plugin-toolbox#16).
@glaubersilva glaubersilva self-assigned this Nov 2, 2026
@glaubersilva
glaubersilva force-pushed the soft-5188-release-publish-action branch 2 times, most recently from b529772 to eab1115 Compare November 2, 2026 20:10
…l publishes

The merge template had grown a publish step. Each template now shows one
action; a repository that merges with release-merge adds release-publish
to that workflow after its build, as GiveWP's deploy workflow does, and
the READMEs say so.

SOFT-5188
@glaubersilva
glaubersilva marked this pull request as ready for review November 2, 2026 20:27

@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.

  • Fix comments: Fix CodeRabbit comments on this PR

Comment thread templates/workflows/release-publish.yml Resolved
@glaubersilva glaubersilva changed the title Add release-publish: the GitHub release as its own action, with a title, notes and assets Add release-publish: the GitHub release for a version at a commit, with a title, notes and assets Nov 2, 2026
The release-publish template checked out the branch, invited a build
from that checkout, and passed the branch name as target, which the
action resolves to the branch's tip when it runs: a push between the
checkout and the publish would tag a commit other than the one the
assets came from. The template now records the checked-out commit and
passes it.

SOFT-5188
@glaubersilva
glaubersilva force-pushed the soft-5188-release-publish-action branch from 05d78d2 to 9603173 Compare November 2, 2026 20:46

@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.

  • Fix comments: Fix CodeRabbit comments on this PR

Comment thread templates/workflows/release-publish.yml Resolved
@glaubersilva
glaubersilva force-pushed the soft-5188-release-publish-action branch 2 times, most recently from 6568397 to e6c9816 Compare November 2, 2026 21:14
@glaubersilva
glaubersilva force-pushed the soft-5188-release-publish-action branch from e6c9816 to ef353a6 Compare November 2, 2026 21:37

@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.

  • Fix comments: Fix CodeRabbit comments on this PR

Comment thread templates/workflows/release-publish.yml Resolved
Comment thread templates/workflows/release-publish.yml Resolved
Comment thread templates/workflows/release-publish.yml Resolved
…ispatches

The per-action templates matched no repository: nobody merges without
publishing, and a repository that only publishes does not go through
the dashboard. release.yml, the name the dashboard looks for by
default, runs release-merge, leaves room for the build, then
release-publish at the merged commit. The publish-only shape stays as
a snippet in release-publish's README.

SOFT-5188
@glaubersilva
glaubersilva force-pushed the soft-5188-release-publish-action branch from ef353a6 to 879516b Compare November 2, 2026 21:49
…s github.token cannot create

The environment's branch policy protects the branch a run is started
on, not the branch input, so a run started on an allowed branch could
merge and publish any other. The template's first step fails when the
two differ; the dashboard starts the run on the release branch, and a
hand dispatch passes --ref. A failing step rather than an if on the job,
since a skipped job reads as a success to the dispatcher.

The release-publish README also says that a release at a commit that
changes a workflow file relative to the default branch needs a token
allowed to modify workflows, which github.token cannot be; after
release-merge the published commit is the base branch's tip, so the
case is the hand-merged publish only.

SOFT-5188
release-merge now merges through the GitHub API and touches no checkout,
so the template checks out base-sha after it, where the build runs, and
needs no checkout before it. release-publish's README shows the same
chain.
Comment thread templates/workflows/release.yml Resolved
@glaubersilva
glaubersilva force-pushed the soft-5188-release-publish-action branch from 356cf3e to 2f2f4f0 Compare November 4, 2026 18:00
…e released

generateReleaseNotes was called without previous_tag_name, so GitHub chose where the notes start,
and its choice is not documented: a hotfix on an older line, 2.1.38.3 after 2.1.46 is out, could
get the changes since 2.1.46. The releases are listed and the highest published version below the
one released is passed as previous_tag_name; drafts and tags that are not plain versions are
skipped. With nothing below, the start is left to GitHub as before.
…s deleted

The draft was deleted only when an upload failed. When the call that publishes it failed, which is
what a tag ruleset that blocks the tag produces, the draft stayed, and a rerun found no release by
the tag and made a second one. The publish is now undone the same way. The error says the draft was
deleted only when the delete succeeded; otherwise it says where to delete it by hand. A draft of the
version left behind by an earlier run, cancelled while uploading, is deleted before publishing.
GitHub ignores target_commitish when the tag already exists, so a tag named after the version that
someone creates between the first check and the publish attaches the release to that commit, and the
action reported the commit it asked for. With files, the tag is read before the draft is published: one
that appeared during the uploads deletes the draft and fails, naming its commit. Without files, it is
read right after the release is created: one that points elsewhere fails naming the commit the release
is at and its URL, since the release is public by then. The README asks the publish-only workflow for
the template's concurrency group.
…, not the branch

The input's description recommended the branch for a release a person merged by hand and said the
tag is made at the commit whatever the branch points to by then. A branch name is resolved to its
tip when the action runs, so a push after the checkout would be tagged with assets built from
another commit. The description now says what the README's publish-only snippet already does.
… template

The approval of the live environment may come long after the run was started, and release-merge
reads the branch's tip when its step runs. With head: github.sha a push to the release branch in
between is refused rather than merged unreviewed.
Comment on lines +348 to +362
// A draft of this version left by a run that was cancelled while uploading, or whose publish
// failed before the draft could be deleted. getReleaseByTag does not return drafts, so a rerun
// would otherwise leave it there and add a second one.
const releases = await listReleases({ github, owner, repo })
for (const stale of releases.filter((release) => release.draft && release.tag_name === version)) {
try {
await github.rest.repos.deleteRelease({ owner, repo, release_id: stale.id })
core.info(`Deleted a draft of ${version} left behind by an earlier run (${stale.html_url}).`)
} catch (error) {
core.warning(
`A draft of ${version} left behind by an earlier run could not be deleted (HTTP ${error.status}): ` +
`delete it by hand at ${stale.html_url}.`
)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This deletes every draft whose tag_name is version, not only drafts an earlier run of this action made. A draft a person created by hand for the upcoming version, for example to write the release notes in the GitHub UI, has the same tag_name and is deleted here without any check. Can the action mark the drafts it creates, for example with a fixed line in the body or a name it sets, and delete only drafts with that mark? The comment above and the README's "Running it again" section both describe the deleted drafts as ones left behind by an earlier run, which is narrower than what the filter matches.

@glaubersilva glaubersilva Nov 4, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

You're right, the filter took any draft with that tag name. Fixed in 2fb753b: the action names its draft <title> [release-publish draft], publishes it under the title, and deletes only drafts that carry the mark. The README and the comment say so now. Checked on a test plugin with a draft made by hand for the same version sitting there: the deploy left it alone, published 2.1.49 under the plain title (run).

The rerun deleted every draft whose tag name was the version, a draft a person made by hand to write
the notes included. The action now names its draft <title> [release-publish draft], publishes it
under the title, and deletes only drafts that carry the mark.
@glaubersilva
glaubersilva force-pushed the soft-5188-release-publish-action branch from d10f4e0 to 2fb753b Compare November 4, 2026 20:15
@glaubersilva
glaubersilva requested a review from d4mation November 4, 2026 20:16
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.

2 participants