Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions .github/actions/release-prep/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ any, it writes nothing and accepts any version. The action's check does not depe
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 version the repository has already tagged is refused.** A release branch cut from `main` right
after a release still carries that release's version, and preparing it again would put the next
changes under a version that is already out. The tag is read through the GitHub API with `token`,
so the check works on the shallow checkout the template makes. Give the release a new version.
- **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
Expand Down Expand Up @@ -210,8 +214,9 @@ branch already has — runs on the phar path only; the TS contract does not defi
## 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 nothing, calls no API, and succeeds, so a retry of a
finished preparation is safe.
and `changelog/` is empty. The action commits nothing and succeeds, so a retry of a finished
preparation is safe, as long as the version has not been tagged in between: once the release is out,
preparing its version again is refused.

## The date

Expand Down
9 changes: 6 additions & 3 deletions .github/actions/release-prep/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ inputs:
The version to prepare. It is validated against the versioning strategy the repository's
changelogger configures: three or four numeric parts for `stellarwp`, a semver version for
`semver` or no configuration, no shape check for a custom strategy. An empty value and a
version lower than the one the branch already has are refused before anything is changed.
version lower than the one the branch already has are refused before anything is changed, and
so is a version the repository has already tagged.
required: true
date:
description: >-
Expand Down Expand Up @@ -89,12 +90,14 @@ runs:
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.
# never calls it for pup.
INPUT_TOKEN: ${{ inputs.token }}
with:
# The tag check reads the repository through github-script's own client, built from this token.
github-token: ${{ inputs.token }}
script: |
const { runPrepare } = require(`${process.env.ACTION_PATH}/release-prep.js`)
await runPrepare({ core, exec, env: process.env })
await runPrepare({ github, core, exec, env: process.env })

# 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.
Expand Down
54 changes: 50 additions & 4 deletions .github/actions/release-prep/release-prep.js
Original file line number Diff line number Diff line change
Expand Up @@ -648,15 +648,51 @@ async function runOrFail(exec, command, args, options, what) {
return result.stdout
}

/**
* Refuses a version the repository has already tagged.
*
* A release branch cut from `main` right after a release still carries that release's version, and
* preparing it again would put the next changes under a version that is already out. The tag is
* read through the API rather than from the checkout, which is shallow and fetches no tags.
*
* @param {object} options
* @param {object} options.github The Octokit github-script supplies.
* @param {string} options.owner The repository owner.
* @param {string} options.repo The repository name.
* @param {string} options.version The version to prepare, validated.
*
* @throws {Error} When the tag exists, or reading it fails for any reason but 404.
*
* @returns {Promise<void>} Resolves when the tag does not exist.
*/
async function refuseTaggedVersion({ github, owner, repo, version }) {
try {
await github.rest.git.getRef({ owner, repo, ref: `tags/${version}` })
} catch (error) {
if (error?.status === 404) {
return
}

throw new Error(`Reading tag ${version} failed (HTTP ${error.status}): ${error.message}. Nothing was changed.`)
}

throw new Error(
`The tag ${version} already exists in ${owner}/${repo}: that version is out. Preparing it again ` +
'would put new changes under a released version; the branch was cut from a commit that already ' +
'carries it. Give the release a new version. Nothing was changed.'
)
}

/**
* Validates the inputs, then bumps the version and replaces the TBDs with pup.
*
* Every check runs before anything is written, so a wrong version, date or ref, a checkout that is
* not on the release branch, a repository without a usable .puprc or with a changelogger versioning
* the changelogger does not know, or a version lower than the one the branch already has, fails
* with the branch untouched. The version is checked the way the changelog step will check it: by
* not on the release branch, a version the repository has already tagged, a repository without a
* usable .puprc or with a changelogger versioning the changelogger does not know, or a version lower
* than the one the branch already has, fails with the branch untouched. The version is checked the way the changelog step will check it: by
* the versioning strategy package.json configures.
*
* @param {object} github An Octokit, as actions/github-script supplies it, for the tag read.
* @param {object} core The @actions/core toolkit, for the log and the outputs.
* @param {object} exec The @actions/exec toolkit.
* @param {object} env The environment the action manifest put its inputs in.
Expand All @@ -668,7 +704,7 @@ async function runOrFail(exec, command, args, options, what) {
*
* @returns {Promise<void>} Resolves once pup has run and the outputs are set.
*/
async function runPrepare({ core, exec, env, cwd = process.cwd(), fetchImpl = globalThis.fetch }) {
async function runPrepare({ github, core, exec, env, cwd = process.cwd(), fetchImpl = globalThis.fetch }) {
const changelogger = readChangeloggerStrategy(readCheckoutFile(cwd, 'package.json'))
const version = validateVersion(env.INPUT_VERSION, changelogger.strategy)
const date = resolveDate(env.INPUT_DATE)
Expand Down Expand Up @@ -701,6 +737,15 @@ async function runPrepare({ core, exec, env, cwd = process.cwd(), fetchImpl = gl
)
}

const [owner, repo] = String(env.GITHUB_REPOSITORY || '').split('/')
if (!owner || !repo) {
throw new Error(
'GITHUB_REPOSITORY is not set, so the action cannot check whether the version is already ' +
'tagged. Nothing was changed.'
)
}
await refuseTaggedVersion({ github, owner, repo, version })

const { tbdDirs } = readPuprc(readCheckoutFile(cwd, '.puprc'))

if (!tbdDirs) {
Expand Down Expand Up @@ -953,6 +998,7 @@ async function runCommit({ github, core, exec, env, cwd = process.cwd() }) {

module.exports = {
runPrepare,
refuseTaggedVersion,
runCommit,
readVersion,
validateVersion,
Expand Down
Loading
Loading