Skip to content

Add release-merge: the release branch into the base branch, then the base branch into develop - #10

Merged
glaubersilva merged 25 commits into
mainfrom
soft-5188-release-merge-action
Nov 4, 2026
Merged

glaubersilva merged 25 commits into
mainfrom
soft-5188-release-merge-action

Conversation

@glaubersilva

@glaubersilva glaubersilva commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

Related

Resolves SOFT-5188.

Summary

Every plugin repository merges a release the same way: the release branch into the base branch (main, or master where that is its name), then the base branch back into develop. release-merge is that part as a composite action, so a repository's deploy workflow runs it, builds from the result, and goes on to its own steps. Publishing the GitHub release is not part of it: #16 adds release-publish for that, and the caller runs it after the build at the commit this action leaves on the base branch. #16 also brings the deploy workflow to copy, templates/workflows/release.yml, which runs the two actions with the build between them; this PR ships the action alone.

The action follows the shape #3 and #7 set: a CommonJS module run by actions/github-script, inputs read from env, unit tests with node:test against the shared harness. Everything goes through Octokit, the merges included: the action runs no git and needs no checkout.

Changes

  • Inputs: branch, version, base-branch (default main), develop-branch (default develop; empty for a repository without one, which skips the second merge), token (default github.token).
  • Before merging, the version is checked: three or four numeric parts, every file the branch's .puprc lists under paths.versions holds it, and its tag either does not exist or belongs to a release published from this branch (a retry). A tag without a release, or made from another commit, is refused with nothing merged.
  • Both merges are made by the API (repos.merge), which always makes a merge commit; GitHub makes it under the token's identity, github-actions[bot], and signs it. The second merge takes the merge commit itself as head, so develop receives exactly what was released. A base that already holds the head answers 204 and nothing is written, so a failed run is rerun as it is.
  • A conflict is GitHub's 409: nothing is written for that branch, the action prints MERGE_CONFLICT on a line of its own (the marker the release dashboard reads from the log), names the branch in the conflict output and fails the step. The other 409, a base that moved while the merge was being made, fails without the marker and is run again.
  • Outputs: version, branch, base-sha (the merge commit on the base branch: the caller checks it out to build, and publishes the release there) and conflict.
  • The token only ever reaches github-script's client: nothing is handed to git, nothing is in the log. The README lists what the repository has to allow it: writing the merges straight to the base branch and develop, since they are not pull requests; a rule that requires signed commits is satisfied. The caller template puts the job in an environment with required reviewers.
  • The README asks the caller to serialize the deploys of a repository with a concurrency group: two releases prepared with the same version would otherwise merge side by side.
  • Shared with the other actions: .github/lib/github.js (readRepository, isNotFound, readTag, which reads the commit behind an annotated tag too, and tagExists on top of it), .github/lib/exec.js (runOrFail), .github/lib/puprc.js (readVersionFiles, versionIn), validateVersion and STELLARWP_VERSION_PATTERN in .github/lib/versions.js; recordedRealExec moves to tests/support/actions.js, whose recorded fetch now answers a 204 without a body.
  • Tests run the action against the recorded Octokit and assert the requests it sends, and the ones it does not send before a refusal; no git on disk.

How to test

  1. In a plugin repository with main and develop, write a workflow_dispatch workflow with branch and version inputs that runs this action and then checks out ${{ steps.merge.outputs.base-sha }} (the README's first snippet), with uses: pointing at this branch's commit.
  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 <that workflow> -f branch=release/x.y.z -f version=x.y.z. main gets Merge release/x.y.z into main, develop gets Merge main back into develop (release/x.y.z), both by github-actions[bot] and verified, and base-sha is the merge commit on main.
  4. Run step 3 again: the job succeeds and both merges report "already has …: nothing to merge".
  5. Change the same line on develop and on a new release branch, then run step 3 for that branch: the job fails on the develop merge with a MERGE_CONFLICT: line, main has the release, develop is unchanged.
  6. node --test (301 tests).

The steps above were run on the two private test plugins with release-merge and release-publish pinned to 356cf3e, this stack's head, and the deploy callers of both checking out base-sha after the merge:

Case Run
Release 2.1.45: both merges made by the API, by github-actions[bot] and verified; the zip built from a checkout of base-sha attached to the release, tagged at the merge commit plugin-a #41695347631
Release 1.1.16 without assets: both merges, the release at the merge commit plugin-b #41695353208
Same release again: both merges answered "already has", nothing written; release skipped plugin-b #41695528101
develop-branch: '': main merged and 2.1.46 published, develop never read nor touched plugin-a #41695976126
Refused with nothing merged: a version the branch does not hold, a branch half prepared (readme.txt not bumped), a published version whose tag does not contain the branch, a branch the repository does not have plugin-a #41695536526, #41695631354, plugin-b #41695712182, plugin-a #41695121113
The release conflicts with main (a branch cut before 2.1.45 went out, bumped on the same lines): MERGE_CONFLICT on its own line, conflict=main, nothing written; after merging main into the branch by hand, the rerun merges and publishes 2.1.46 plugin-a #41695705946, then #41695976126
develop conflicts with the release: main merged, MERGE_CONFLICT on its own line, no tag, no release; after resolving develop by hand, the rerun finds both merges done and publishes 1.1.17 plugin-b #41695982945, #41696173936
The whole cycle from the release dashboard (nexcess-one#149's deploy contract, branch + version): prepare, RC build, then this action merging the API way and release-publish at base-sha; 2.1.47 with the zip, 1.1.18 without plugin-a #41702581004, plugin-b #41702184209

Stack created with GitHub Stacks CLI • Give Feedback 💬

Summary by CodeRabbit

  • New Features
    • Added a manually triggered release workflow that merges a release branch into the base branch, then into the develop branch when configured.
    • The workflow checks release versions and existing tags before merging, reports conflicts and failures, and leaves already up-to-date branches unchanged.
    • After a successful merge, the checkout is left at the updated base branch for subsequent build and publish steps.
  • Documentation
    • Added guidance on workflow inputs, merge behavior, conflict handling, and setup requirements.

release-prep's three-or-four-parts pattern moves to .github/lib/versions.js, where the other version
rules live, so the release-merge action applies the same rule from the same place. The wrapper around
the real @actions/exec moves from the release-prep test into tests/support, for the tests that run
git for real.
Merges the release branch into the base branch and the base back into develop, then publishes the
GitHub release for the version. Every step is safe to rerun: a branch that already holds the other
merges as a no-op and an existing release is skipped. A conflict abandons the merge, prints a
MERGE_CONFLICT line the release dashboard reads, and fails with the branch named.

The token reaches git through the environment of the fetch and the pushes only, never the
checkout's config, so the caller can keep persist-credentials off and run third-party actions after
it. The tests run the merges with real git against a bare repository on disk.
@linear

linear Bot commented Oct 30, 2026

Copy link
Copy Markdown

SOFT-5188

@coderabbitai

coderabbitai Bot commented Oct 30, 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

No actionable comments were generated in the recent review.

Walkthrough

This change adds a composite GitHub Action that validates release inputs and version data, merges a release branch into a base branch, and optionally merges the base branch into develop. It also adds a manual workflow template, shared helpers, documentation, and tests.

Changes: Release merge

Layer / File(s) Summary
Shared helpers and release-prep integration
.github/lib/versions.js, .github/lib/exec.js, .github/lib/github.js, .github/lib/puprc.js, .github/actions/release-prep/release-prep.js, tests/support/actions.js, tests/actions/release-prep/release-prep.test.js
Adds shared helpers for version validation, command execution, repository and tag lookups, and .puprc version data. The release-prep action and its test use shared helpers.
Release validation and branch merges
.github/actions/release-merge/action.yml, .github/actions/release-merge/release-merge.js, tests/actions/release-merge/release-merge.test.js
Adds action inputs and outputs, release preflight and version-file checks, and merges from release to base and optionally from base to develop. Tests cover input checks, retries, conflicts, outputs, and failure paths.
Workflow template and action documentation
templates/workflows/release-merge.yml, .github/actions/release-merge/README.md, README.md
Adds a manually dispatched workflow that invokes the action. Documents the action’s inputs, outputs, merge behavior, conflict handling, and caller requirements. Updates the Phase 2 checklist.

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant Workflow as release-merge workflow
  participant Action as Release merge action
  participant GitHub as GitHub API
  participant Git as Git
  Workflow->>Action: pass branch and version
  Action->>GitHub: check tag and release
  Action->>Git: fetch branches and merge release into base
  Action->>Git: push base and optionally merge base into develop
Loading

Possibly related PRs

  • stellarwp/plugin-toolbox#16: PR #16 adds release-publish, which uses the base-sha output produced by this action.
  • stellarwp/plugin-toolbox#12: PR #12 checks for existing tags during release preparation. This action also checks tag and release state before merging.
  • stellarwp/plugin-toolbox#7: PR #7 creates and prepares the release branch that this action merges, including its .puprc version files.

Suggested labels: enhancement

Suggested reviewers: dpanta94

Merge Risk: Minimal · previous review 0891a

No actionable issue is established for this change. It is mergeable after normal checks.

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 and specifically summarizes the main change: adding the release-merge action that merges the release branch into the base branch and then into develop.
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 19 functions across 9 files. (1 skipped: 1 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: b641b02f-aa70-48db-8f11-153f87b301ba
Commits

Reviewing files that changed from the base of the PR and between 073aae1 and 0891a30.

Files selected for processing: 1
  • .github/actions/release-merge/README.md
Files skipped from review as they are similar to previous changes: 1
  • .github/actions/release-merge/README.md

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

@glaubersilva glaubersilva self-assigned this Oct 30, 2026
A tag with a published release was read as a retry and the merges went on with the publish
skipped. That is right when the release came from this branch, and wrong when the version went out
earlier from another commit and the branch was never bumped past it: the new changes would land under
a version that is already out. The tag is now fetched and the action goes on only when it contains
the branch; otherwise it refuses before merging, naming the fix.
Two releases prepared with the same version and deployed at the same time would merge side by side,
the second landing its changes under a version the first had just published. One concurrency group
for the whole repository makes the second wait, and the action then refuses it on the published tag.
The README says why the caller has to do this.
readRepository, isNotFound and tagExists move to .github/lib/github.js and runOrFail to
.github/lib/exec.js, so release-prep and release-merge read GITHUB_REPOSITORY, a tag and a command's
failure the same way. The docs name the base branch as main or master wherever it comes up, say
which rule release-prep applies and when, and note the two step outputs the action does not expose.
The version becomes the tag and the title of the release, so it has to be the one release-prep wrote
into the branch. The action now reads every file the branch's .puprc lists under paths.versions,
with the regex pup uses for each, and refuses before merging when any holds a different version, when
one holds none, or when the branch has no .puprc. The reader lives in .github/lib/puprc.js.
The release dashboard now sends the deploy step the branch, as it sends prepare, so the template
declares branch and passes it to the action as it is.
…ex JavaScript rejects

A publish value that is neither true nor false failed only in the publish step, after both merges
were pushed; the merge step reads it now too, so the run stops before anything is merged. A .puprc
regex pup accepts but JavaScript does not surfaced as a bare SyntaxError; the error now names the
file and the regex. The README says where in the sequence the tag ancestry check runs.
@glaubersilva
glaubersilva marked this pull request as ready for review October 30, 2026 21:41
@glaubersilva
glaubersilva requested a review from d4mation October 31, 2026 01:35
Comment thread .github/actions/release-merge/README.md Resolved
…the branch's tip

createRelease took the base branch's name as target_commitish, so the
tag was made at whatever the base pointed to when the publish step ran:
a pull request merged into the base between the push and the publish
would have been tagged instead of the release. The merge step now reads
the commit it left the base on and hands it to the publish step, which
tags that commit. It is also an output of the action, base-sha, since it
is the commit the caller builds from.

SOFT-5188
The merge back into develop was unconditional: the fetch asked for
refs/heads/develop and a repository without that branch (the TEC plugins
keep main and release branches only) failed before anything was merged.
An empty develop-branch now skips the second merge: the release is merged
into the base and pushed, nothing is merged back, and the develop-branch
output is empty.

SOFT-5188

@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 .github/actions/release-merge/README.md Resolved
… through a dispatch input

A workflow_dispatch input left empty takes its default, which a test run
confirmed, so a repository without develop writes develop-branch: ''
in its caller.

SOFT-5188
@glaubersilva glaubersilva changed the title Add a release-merge action that merges the release into the base and develop, then publishes it Add a release-merge action that merges the release into the base and, when there is one, develop Nov 2, 2026
The publish step, the publish input and the published / release-url
outputs are gone. The action merges, pushes and leaves the commit to
publish at in base-sha; the caller builds from it and publishes the
GitHub release after the build, so the release can carry the build and
is published only once it exists. validateVersion moves to the shared
lib, for the publish action that follows.

SOFT-5188
@glaubersilva glaubersilva changed the title Add a release-merge action that merges the release into the base and, when there is one, develop Add release-merge: the release branch into the base branch, then the base branch into develop Nov 2, 2026
The default queue keeps one pending run and cancels it when another
arrives, so a third release requested while one waited for approval
dropped the second. queue: max keeps them all, in order.

SOFT-5188
…: max

The plugin repositories lint their workflows with actionlint 1.7, which
refuses the key, and the template exists to be copied into them. The
README says what the default does and that a cancelled deploy is started
again.

SOFT-5188
…lishes

No repository merges a release without publishing it, so a template
around release-merge alone had no consumer. The complete deploy
workflow, release-merge then the build then release-publish, comes
with the publish action.

SOFT-5188
@glaubersilva
glaubersilva requested a review from larodiel November 3, 2026 16:10
The merges, the branch and version-file reads and the ancestry check go
through Octokit: repos.merge, repos.getBranch, repos.getContent and
repos.compareCommitsWithBasehead. GitHub makes the merge commits under the
token's identity and signs them, so the bot identity, the token handed to
git through its environment and the shallow-clone handling are gone, and
the action needs no checkout. The caller checks out base-sha afterwards
to build from it.

A 204 from the merge endpoint is the retry case. A 409 that says "Merge
conflict" prints the MERGE_CONFLICT marker as before; a 409 for a base
that moved while the merge was being made fails without it, to be run
again. readTag in lib/github.js reads the commit a tag points at, through
the tag object when the tag is annotated, for the ancestry check against
the release branch; tagExists keeps its contract on top of it.

The tests run the action against a recorded client and assert the
requests it sends and the ones it does not, with no git on disk. The
recorded fetch answers a 204 without a body, as the merge endpoint does.

Answers the review on #10: the raw git commands, the 41898282+ prefix
(the constant is gone) and the dashboard named in the regex comment (the
regex is gone; the marker line is the one place left that names it).
glaubersilva added a commit that referenced this pull request Nov 3, 2026
The merges, the branch and version-file reads and the ancestry check go
through Octokit: repos.merge, repos.getBranch, repos.getContent and
repos.compareCommitsWithBasehead. GitHub makes the merge commits under the
token's identity and signs them, so the bot identity, the token handed to
git through its environment and the shallow-clone handling are gone, and
the action needs no checkout. The caller checks out base-sha afterwards
to build from it.

A 204 from the merge endpoint is the retry case. A 409 that says "Merge
conflict" prints the MERGE_CONFLICT marker as before; a 409 for a base
that moved while the merge was being made fails without it, to be run
again. readTag in lib/github.js reads the commit a tag points at, through
the tag object when the tag is annotated, for the ancestry check against
the release branch; tagExists keeps its contract on top of it.

The tests run the action against a recorded client and assert the
requests it sends and the ones it does not, with no git on disk. The
recorded fetch answers a 204 without a body, as the merge endpoint does.

Answers the review on #10: the raw git commands, the 41898282+ prefix
(the constant is gone) and the dashboard named in the regex comment (the
regex is gone; the marker line is the one place left that names it).
@glaubersilva
glaubersilva requested a review from d4mation November 3, 2026 19:32
The release branch's tip is read once, and that commit is what the version files are read at, what
a published tag is compared against and what is merged into the base. Read by name at each step, a
commit pushed to the branch between the version check and the merge reached the base unchecked, and
a tag named like the branch could change what a read by name returned. The branch name stays in the
merge message and in the messages.
@glaubersilva
glaubersilva requested a review from d4mation November 4, 2026 18:08
A deploy that waits for an approval merges the branch's tip when the approval comes, which may not
be the commit the run was started on, built as the RC and reviewed. The new optional head input
takes that commit (github.sha in the caller) and the action refuses, before anything is merged, a
branch whose tip is another commit. Empty keeps today's behaviour: the tip at that moment merges.
@coderabbitai coderabbitai Bot mentioned this pull request Nov 4, 2026
@glaubersilva
glaubersilva force-pushed the soft-5188-release-merge-action branch from c7fa613 to eeb3d77 Compare November 4, 2026 20:15
@glaubersilva
glaubersilva merged commit 184de04 into main Nov 4, 2026
2 checks passed
@glaubersilva
glaubersilva deleted the soft-5188-release-merge-action branch November 4, 2026 21:11
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.

3 participants