Repository navigation
Add a release-prep action that bumps the version, replaces TBDs and writes the changelog #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
glaubersilva
wants to merge
18
commits into
main
Choose a base branch
from
feat/release-prep-action
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
184b01c
Add an exec stand-in and a job summary reader to the test harness
glaubersilva 4b21bed
Leave the test runner's own reports out of the captured log
glaubersilva 5b35c2a
Add the release-prep logic: version, date and .puprc checks, pup, com…
glaubersilva 496f901
Add the release-prep action manifest and its README
glaubersilva 6d0e310
Add a release-prep caller to copy, and tick release-prep off the plan
glaubersilva 652b39c
Clear a credential the checkout persisted before pushing with the token
glaubersilva 2b6a48c
Say that GitHub refuses an empty required version before the action runs
glaubersilva e835e28
Refuse a version lower than the one the branch already has
glaubersilva ce2ef0e
Say what happens when the branch's version is missing or not numeric
glaubersilva 5122413
refactor: replace release name samples
glaubersilva 9a41799
docs(release-prep): state the warning behavior without the evaluative…
glaubersilva e7c13da
docs(release-prep): scope the version rule to the default changelogge…
glaubersilva 936eed4
refactor(release-prep): rename the branch input from ref to branch
glaubersilva 4087f53
docs(release-prep): attribute the version shape to the default change…
glaubersilva 0214ed1
feat(release-prep): resolve the latest pup release instead of a hardc…
glaubersilva 5232e2c
refactor: share compareVersions between the release-prep and resolve-…
glaubersilva 4aae955
feat(release-prep): run whichever distribution a pup release publishes
glaubersilva 4f31db8
docs(release-prep): rewrite the README without the generative filler
glaubersilva File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,219 @@ | ||
| # release-prep | ||
|
|
||
| Prepares a release branch: bumps the version, replaces the `@since TBD` placeholders, writes the | ||
| changelog, then commits and pushes the result to the branch: | ||
|
|
||
| ```yaml | ||
| - uses: actions/checkout@v6 | ||
| with: | ||
| ref: ${{ inputs.ref }} | ||
| persist-credentials: false | ||
|
|
||
| - uses: stellarwp/plugin-toolbox/.github/actions/release-prep@v1 | ||
| with: | ||
| branch: ${{ inputs.ref }} | ||
| version: ${{ inputs.version }} | ||
| ``` | ||
|
|
||
| The action opens no pull request; the commit is reviewed on the release branch's own pull request. | ||
| [`templates/workflows/release-prep.yml`](../../../templates/workflows/release-prep.yml) is a complete | ||
| workflow around it, ready to copy into a plugin repository. | ||
|
|
||
| ## What it changes | ||
|
|
||
| Three tools run, in this order, from the root of the checkout: | ||
|
|
||
| 1. `pup replace-version <version>` rewrites the version in every file `.puprc` lists under | ||
| `paths.versions`. | ||
| 2. `pup replace-tbd <version>` replaces the TBD placeholders (`@since TBD`, `@deprecated TBD`, | ||
| `_deprecated_function( __METHOD__, 'TBD' )` and the like) in the directories `.puprc` lists under | ||
| `checks.tbd.dirs`. | ||
| 3. The [changelogger action](https://github.com/stellarwp/changelogger) writes the pending entries in | ||
| `changelog/` into the changelog files and deletes them, with the version and the date. | ||
|
|
||
| Everything the working tree changed is then committed as `Prepare <branch> (<version>)` by | ||
| `github-actions[bot]` and pushed to `branch`. Run the action on a clean checkout, and put any step | ||
| that writes tracked files after it: the commit includes every change in the working tree. | ||
|
|
||
| pup is downloaded from its GitHub release into the runner's temp directory, so it never ends up in | ||
| the commit. See [Which pup runs](#which-pup-runs). | ||
|
|
||
| ## What the repository needs | ||
|
|
||
| A `.puprc` listing the version files and the directories that hold the TBD placeholders: | ||
|
|
||
| ```json | ||
| { | ||
| "paths": { | ||
| "versions": [ | ||
| { "file": "my-plugin.php", "regex": "(Version: )(.+)" }, | ||
| { "file": "my-plugin.php", "regex": "(define\\( 'MY_PLUGIN_VERSION', ')([^']+)" }, | ||
| { "file": "readme.txt", "regex": "(Stable tag: )(.+)" } | ||
| ] | ||
| }, | ||
| "checks": { | ||
| "tbd": { | ||
| "dirs": ["src", "includes"] | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| A `changelogger` section in `package.json`, and the pending entries in its `changesDir`: | ||
|
|
||
| ```json | ||
| { | ||
| "changelogger": { | ||
| "versioning": "stellarwp", | ||
| "changesDir": "changelog", | ||
| "files": [{ "path": "readme.txt", "strategy": "stellarwp-readme" }] | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| The action checks the `.puprc` before running anything, because pup does not: a missing file, one | ||
| that is not JSON, or one with no `paths.versions` makes pup change nothing and exit 0. The job | ||
| fails on any of the three, with the branch untouched. | ||
|
|
||
| Two gaps only produce a warning; the run continues: | ||
|
|
||
| - No `checks.tbd.dirs`: pup falls back to its default and replaces TBDs under `src/` only. | ||
| - No `changelogger` section: the changelogger uses its defaults — a keepachangelog `changelog.md` | ||
| with semver versioning. | ||
|
|
||
| A writing strategy given as a `.js` file is loaded from the checkout; it has to be there before the | ||
| action runs. One written in TypeScript has to be built in an earlier step. | ||
|
|
||
| ## Versions it refuses | ||
|
|
||
| The version has to be three or four numeric parts, such as `4.17.0` or `4.17.0.1`. That is the shape | ||
| the changelogger's default `stellarwp` versioning accepts. A repository with a custom changelogger | ||
| version strategy may accept other shapes, but this action still takes three or four numeric parts. | ||
| The changelog step would fail on anything else, so the action refuses it before pup has touched | ||
| anything. | ||
|
|
||
| - **An empty version is refused.** A tool that dispatches the workflow for a release with no version | ||
| set sends an empty one. When the workflow declares `version` as required, as the template does, | ||
| GitHub refuses the dispatch itself with HTTP 422 and no run starts. When the workflow declares it | ||
| optional, the run starts and the action fails it with "No version was given". Either way the | ||
| release needs a version before the step runs again. | ||
| - **A pre-release is refused.** `4.17.0-beta.1` is rejected by the `stellarwp` versioning, so the | ||
| action cannot prepare one, even though it is a valid tag name. A custom version strategy that | ||
| accepts pre-releases does not change this. | ||
| - **A version lower than the branch's is refused.** pup reads the version the branch already has | ||
| (`pup get-version`, the first file in `paths.versions`), and preparing `4.17.0` on a branch at | ||
| `4.18.0` would bump the plugin backwards: either the version or the branch is wrong. The same | ||
| version is accepted; that is what re-running a preparation finds. When the version files hold | ||
| something that is not a numeric version, such as `dev`, the job only warns. When pup cannot find a | ||
| version at all, it fails, as `replace-version` would. | ||
|
|
||
| Each refusal happens before anything is changed. So do a `date` that is not `YYYY-MM-DD`, a `branch` | ||
| that is not a valid branch name, and a checkout that is not on `branch`. | ||
|
|
||
| ## Inputs | ||
|
|
||
| | Input | Required | Default | What it does | | ||
| |---|---|---|---| | ||
| | `branch` | yes | | The release branch to push to, e.g. `release/M26.jigglypuff`. A branch name, not `refs/heads/…`. The checkout has to be on it; see [Pushing](#pushing) | | ||
| | `version` | yes | | The version to prepare, e.g. `4.17.0`. See [Versions it refuses](#versions-it-refuses) | | ||
| | `date` | no | today, in UTC | The changelog date as `YYYY-MM-DD`. Empty or `today` writes today | | ||
| | `token` | no | `github.token` | The token the commit is pushed with. See [Pushing](#pushing) | | ||
| | `pup-version` | no | `latest` | The pup release to run: a number such as `2.0.0`, or `latest` for the newest. See [Which pup runs](#which-pup-runs) | | ||
|
|
||
| ## Outputs | ||
|
|
||
| | Output | Example | What it is | | ||
| |---|---|---| | ||
| | `version` | `4.17.0` | The version prepared | | ||
| | `date` | `2026-10-08` | The date written in the changelog | | ||
| | `commit` | `3f2a…` | The commit pushed to the branch. Empty when nothing changed | | ||
| | `files` | `my-plugin.php` | The files the commit changed, one per line. Empty when nothing changed | | ||
|
|
||
| ## Which pup runs | ||
|
|
||
| `pup-version` pins a pup release (`2.0.0`) or tracks the newest one (`latest`, the default). A | ||
| pinned release downloads straight from its release page. `latest` reads the newest release through | ||
| the GitHub API, authenticated with `token` so the runner's shared unauthenticated quota is not | ||
| spent. | ||
|
|
||
| The release itself decides which kind of pup runs, not a version comparison: the action sends a | ||
| HEAD request to the release's `pup.phar` asset. Releases up to and including 2.0.0 publish one; the | ||
| phar downloads into the runner's temp directory and runs on the runner's PHP. A release that | ||
| answers 404 is the TypeScript line, which runs through its own GitHub Action — the same way the | ||
| changelog step calls the changelogger action. While every release carries a phar, that path never | ||
| runs, and callers change nothing when it starts to. | ||
|
|
||
| The manifest carries the call already, as the contract the pup action is expected to ship: | ||
|
|
||
| ```yaml | ||
| - uses: stellarwp/pup@v3 | ||
| with: | ||
| command: replace-version --version=4.17.0 | ||
| ``` | ||
|
|
||
| `uses:` takes a static ref, so the floating major tag is the moving pin. The branch-version check | ||
| — the refusal of a version lower than the branch already has — runs on the phar path only; the TS | ||
| contract does not define it yet. | ||
|
|
||
| ## Running it again | ||
|
|
||
| Preparing a version the branch already holds changes nothing: the versions match, no TBDs are left | ||
| and `changelog/` is empty. The action commits and pushes nothing and succeeds, so a retry of a | ||
| finished preparation is safe. | ||
|
|
||
| ## The date | ||
|
|
||
| The changelogger action writes the date exactly as it is given: `today` would land in the changelog | ||
| as the word, and an empty value as an empty date. The action resolves both to today's date, in UTC, | ||
| before the changelog step runs, and that is the date the `date` output reports. | ||
|
|
||
| ## Pushing | ||
|
|
||
| The job needs `contents: write`: | ||
|
|
||
| ```yaml | ||
| jobs: | ||
| prepare: | ||
| permissions: | ||
| contents: write | ||
| ``` | ||
|
|
||
| The action pushes with `token` itself, to `HEAD:refs/heads/<branch>`, so the checkout can use | ||
| `persist-credentials: false`. The token reaches git through the environment of that one push — | ||
| never on its command line, which the log would echo, and never in `.git/config`, where a later | ||
| step could read it. The token and the base64 credential built from it are masked in the log. | ||
|
|
||
| When the checkout keeps its own credentials, git would send two Authorization headers — its own | ||
| and the action's — and GitHub refuses the push with a 400. The action clears the | ||
| `http.<server>/.extraheader` key before adding its own header, so only `token` is sent. | ||
|
|
||
| The push is a fast-forward. If the branch moved after the checkout, the push is refused and the | ||
| job fails with git's output. | ||
|
|
||
| The checkout has to be on `branch` itself, and the action checks that before changing anything. | ||
| Pushing from another branch would also fast-forward whenever the release branch is an ancestor of | ||
| it — for instance a run started from `main` whose checkout was given no `ref` — and the release | ||
| would pick up every commit of that branch. A detached checkout is refused for the same reason. | ||
|
|
||
| Pass a PAT or app token as `token` when a ruleset on the release branches blocks `github.token`, | ||
| or when a workflow in the repository has to run on the push. GitHub creates no workflow run for a | ||
| push made with `GITHUB_TOKEN`. | ||
|
|
||
| ## How it runs | ||
|
|
||
| Two [actions/github-script](https://github.com/actions/github-script) steps run `release-prep.js`, | ||
| with the changelogger action between them. The first validates the inputs and the `.puprc`, then | ||
| runs pup. The second commits and pushes. github-script supplies the Node runtime and `exec`; there | ||
| is no dependency to install and no bundle to build. | ||
|
|
||
| The script is loaded by path rather than inlined in the manifest because github-script's `require` | ||
| resolves against the workspace of the repository being built, not this action's directory. The | ||
| path comes from `${{ github.action_path }}`. | ||
|
|
||
| Inputs reach the script through `process.env`, never by being interpolated into it, so a version | ||
| or a branch name is data and cannot become part of the program. The commands run with their | ||
| arguments as a list, without a shell. | ||
|
|
||
| `tests/actions/release-prep/release-prep.test.js` covers the rules above. The pup steps run | ||
| against a stand-in for `exec`; the commit and push run real git against a local repository. Run | ||
| `npm install` once, then `node --test`. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| # Prepares a release branch: bumps the version, replaces the TBD placeholders, writes the changelog, | ||
| # then commits that and pushes it to the branch. It opens no pull request, so the change is reviewed | ||
| # wherever the release itself is reviewed. | ||
| # | ||
| # The checks and the git work live in release-prep.js, run by actions/github-script, so there is no | ||
| # dependency to install and nothing to build. The changelog is written by the changelogger's own | ||
| # action between the two script steps, rather than reimplemented here. | ||
| name: Release prep | ||
| description: Bumps the version, replaces TBDs and writes the changelog, then pushes it to the release branch | ||
|
|
||
| inputs: | ||
| branch: | ||
| description: >- | ||
| The release branch to push the preparation to, e.g. `release/M26.jigglypuff`. A branch name, not | ||
| any git ref. The action does not check it out: the caller's checkout has to be on it, and the | ||
| action refuses to run when it is not. | ||
| required: true | ||
| version: | ||
| description: >- | ||
| The version to prepare: three or four numeric parts, such as `4.17.0` or `4.17.0.1`. That is | ||
| the shape the default `stellarwp` changelogger versioning takes; a custom version strategy | ||
| does not widen what the action accepts. An empty value and a version lower than the one the | ||
| branch already has are refused before anything is changed. | ||
| required: true | ||
| date: | ||
| description: >- | ||
| The release date for the changelog, as `YYYY-MM-DD`. Empty or `today` writes today's date in | ||
| UTC. | ||
| required: false | ||
| default: '' | ||
| token: | ||
| description: >- | ||
| Token the commit is pushed with. It needs `contents: write` on the repository, so the job needs | ||
| that permission too. The action pushes with it explicitly, so the checkout can use | ||
| `persist-credentials: false`. | ||
| required: false | ||
| default: ${{ github.token }} | ||
| pup-version: | ||
| description: >- | ||
| The pup release that bumps the version and replaces the TBDs: a release number, such as | ||
| 2.0.0, or 'latest' for the newest release. Versions up to 2.0.0 ship as a .phar; once pup | ||
| publishes a release without one, the action runs that release's own GitHub Action instead. | ||
| required: false | ||
| default: 'latest' | ||
|
|
||
| outputs: | ||
| version: | ||
| description: 'The version prepared.' | ||
| value: ${{ steps.prepare.outputs.version }} | ||
| date: | ||
| description: 'The date written in the changelog, as `YYYY-MM-DD`.' | ||
| value: ${{ steps.prepare.outputs.date }} | ||
| commit: | ||
| description: 'The commit pushed to the branch. Empty when nothing changed and nothing was pushed.' | ||
| value: ${{ steps.commit.outputs.commit }} | ||
| files: | ||
| description: 'The files the commit changed, one per line. Empty when nothing changed.' | ||
| value: ${{ steps.commit.outputs.files }} | ||
|
|
||
| runs: | ||
| using: 'composite' | ||
| steps: | ||
| - name: Check the inputs, bump the version and replace the TBDs | ||
| id: prepare | ||
| uses: actions/github-script@v9 | ||
| env: | ||
| # github-script's require() resolves against the workspace of the repository being built, | ||
| # not this action's directory, so the path has to be passed in. github.action_path is the | ||
| # documented expression for it. | ||
| ACTION_PATH: ${{ github.action_path }} | ||
| # Read from process.env rather than interpolated into the script, so an input is data and | ||
| # never part of the program. | ||
| INPUT_BRANCH: ${{ inputs.branch }} | ||
| INPUT_VERSION: ${{ inputs.version }} | ||
| INPUT_DATE: ${{ inputs.date }} | ||
| INPUT_PUP_VERSION: ${{ inputs.pup-version }} | ||
| # Authenticates the 'latest' lookup against the GitHub API; a pinned pup-version | ||
| # never calls the API. | ||
| INPUT_TOKEN: ${{ inputs.token }} | ||
| with: | ||
| script: | | ||
| const { runPrepare } = require(`${process.env.ACTION_PATH}/release-prep.js`) | ||
| await runPrepare({ core, exec, env: process.env }) | ||
|
|
||
| # The TypeScript line of pup publishes no .phar; its release runs through its own GitHub | ||
| # Action, called here the way the changelog step calls the changelogger. These steps run | ||
| # only when the prepare step resolved a release without a phar asset, so they stay inert | ||
| # until such a release exists — and spell out the interface the pup action is expected to | ||
| # ship. uses: takes a static ref, so the floating major tag is the moving pin. | ||
| - name: Bump the version (pup) | ||
| if: ${{ steps.prepare.outputs.distribution == 'ts' }} | ||
| uses: stellarwp/pup@v3 | ||
| with: | ||
| command: replace-version --version=${{ steps.prepare.outputs.version }} | ||
|
|
||
| - name: Replace the TBDs (pup) | ||
| if: ${{ steps.prepare.outputs.distribution == 'ts' }} | ||
| uses: stellarwp/pup@v3 | ||
| with: | ||
| command: replace-tbd --version=${{ steps.prepare.outputs.version }} | ||
|
|
||
| # The resolved date, never inputs.date: this action writes the date it is given as it is, so | ||
| # `today` would land in the changelog as the word and an empty value as an empty date. | ||
| - name: Write the changelog | ||
| uses: stellarwp/changelogger@v1 | ||
| with: | ||
| command: write | ||
| version: ${{ steps.prepare.outputs.version }} | ||
| date: ${{ steps.prepare.outputs.date }} | ||
|
|
||
| - name: Commit the preparation and push it to the release branch | ||
| id: commit | ||
| uses: actions/github-script@v9 | ||
| env: | ||
| ACTION_PATH: ${{ github.action_path }} | ||
| # The values the prepare step validated, not the raw inputs. | ||
| INPUT_BRANCH: ${{ steps.prepare.outputs.branch }} | ||
| INPUT_VERSION: ${{ steps.prepare.outputs.version }} | ||
| INPUT_TOKEN: ${{ inputs.token }} | ||
| with: | ||
| script: | | ||
| const { runCommit } = require(`${process.env.ACTION_PATH}/release-prep.js`) | ||
| await runCommit({ core, exec, env: process.env }) | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.