From 00a99557bcc93cbc57320fbc1db7722bd7ce0bcd Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Tue, 22 Sep 2026 13:58:17 -0400 Subject: [PATCH] Add release script and workflow to check release versions The 3.0.2 bump commit was tagged and released but never merged into trunk. release.sh refuses to release unless trunk matches origin/trunk and getVersion() matches the new version, then creates the tag and GitHub release. The Release check workflow fails if a release tag is not on trunk or does not match getVersion(), catching manual releases. Document the process in a new Releasing section of the README. --- .github/workflows/release-check.yml | 45 ++++++++++++++ README.md | 12 ++++ release.sh | 95 +++++++++++++++++++++++++++++ 3 files changed, 152 insertions(+) create mode 100644 .github/workflows/release-check.yml create mode 100755 release.sh diff --git a/.github/workflows/release-check.yml b/.github/workflows/release-check.yml new file mode 100644 index 0000000..779e696 --- /dev/null +++ b/.github/workflows/release-check.yml @@ -0,0 +1,45 @@ +name: Release check + +# Fails if a release tag points at a commit that is not on trunk, or at a +# commit whose getVersion() does not match the tag. This cannot block the +# release, but a failed run makes the mistake visible right away. Releases +# made with ./release.sh already run these checks before tagging. + +on: + push: + tags: + - 'v*' + release: + types: [published] + +jobs: + check: + name: Tag matches version on trunk + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + coverage: none + + - name: Check that the tagged commit is on trunk + run: | + if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/trunk; then + echo "::error::Tag ${GITHUB_REF_NAME} points at ${GITHUB_SHA}, which is not on trunk. Merge the release commit into trunk." + exit 1 + fi + + - name: Check that getVersion() matches the tag + run: | + CODE_VERSION="$(php -r 'require "PhpcsChanged/functions.php"; echo PhpcsChanged\getVersion();')" + if [ "v${CODE_VERSION}" != "${GITHUB_REF_NAME}" ]; then + echo "::error::Tag ${GITHUB_REF_NAME} does not match getVersion() in PhpcsChanged/functions.php (${CODE_VERSION}). Bump the version before releasing." + exit 1 + fi diff --git a/README.md b/README.md index 90e04a0..89b5d08 100644 --- a/README.md +++ b/README.md @@ -264,3 +264,15 @@ If something isn't working the way you expect, use the `--debug` option. This wi ## Inspiration This was inspired by the amazing work in https://github.com/Automattic/phpcs-diff + +## Releasing + +Releases are made from `trunk` using the `release.sh` script, which requires the [gh CLI](https://cli.github.com/). + +1. Open and merge a PR that updates `getVersion()` in `PhpcsChanged/functions.php` to the new version. Use a minor version bump (eg: `3.1.0`) for new features or options and a patch version bump (eg: `3.0.2`) for bug fixes only. +2. Check out `trunk` and pull the merged PR. +3. Run `./release.sh ` (eg: `./release.sh 3.1.0`). Add `--dry-run` to run only the checks. + +The script refuses to release unless `trunk` is clean and matches `origin/trunk`, `getVersion()` returns the new version, and the new version is higher than the latest release tag. It then creates the tag and a GitHub release with generated notes at the current `trunk` commit. + +If you make a release another way, the "Release check" GitHub workflow will fail when the release tag points at a commit that isn't on `trunk` or whose `getVersion()` doesn't match the tag. diff --git a/release.sh b/release.sh new file mode 100755 index 0000000..b7d0cbf --- /dev/null +++ b/release.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +# release.sh — tag and publish a new phpcs-changed release on GitHub +# +# Usage: +# ./release.sh eg: ./release.sh 3.1.0 +# ./release.sh --dry-run run all checks but do not create the release +# +# Before running this, merge a PR that sets getVersion() in +# PhpcsChanged/functions.php to . The script refuses to release unless: +# - you are on trunk with no uncommitted changes to tracked files +# - local trunk matches origin/trunk, so the release commit is on the remote +# - getVersion() returns exactly +# - the tag v does not already exist +# - is higher than the latest existing release tag +# +# It then creates the v tag and GitHub release (with generated notes) +# at the current origin/trunk commit using the gh CLI. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +fail() { + echo "Error: $*" >&2 + exit 1 +} + +DRY_RUN=false +if [[ "${1:-}" == "--dry-run" ]]; then + DRY_RUN=true + shift +fi + +VERSION="${1:-}" +if [[ -z "$VERSION" ]]; then + fail "Usage: $0 [--dry-run] (eg: $0 3.1.0)" +fi +VERSION="${VERSION#v}" +if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + fail "Version '$VERSION' must look like X.Y.Z" +fi +TAG="v$VERSION" + +command -v gh >/dev/null || fail "The gh CLI is required: https://cli.github.com/" +command -v php >/dev/null || fail "php is required to read the version from PhpcsChanged/functions.php" + +BRANCH="$(git branch --show-current)" +[[ "$BRANCH" == "trunk" ]] || fail "You must be on trunk to release (currently on '$BRANCH')" + +if [[ -n "$(git status --porcelain --untracked-files=no)" ]]; then + fail "You have uncommitted changes; commit or stash them first" +fi + +echo "Fetching origin..." +git fetch --quiet --tags origin trunk + +LOCAL_SHA="$(git rev-parse HEAD)" +REMOTE_SHA="$(git rev-parse origin/trunk)" +if [[ "$LOCAL_SHA" != "$REMOTE_SHA" ]]; then + fail "Local trunk ($(git rev-parse --short HEAD)) does not match origin/trunk ($(git rev-parse --short origin/trunk)). Merge the version bump via a PR and pull before releasing." +fi + +CODE_VERSION="$(php -r 'require "PhpcsChanged/functions.php"; echo PhpcsChanged\getVersion();')" +if [[ "$CODE_VERSION" != "$VERSION" ]]; then + fail "getVersion() in PhpcsChanged/functions.php returns '$CODE_VERSION', not '$VERSION'. Merge a PR bumping the version first." +fi + +if git rev-parse --quiet --verify "refs/tags/$TAG" >/dev/null; then + fail "Tag $TAG already exists" +fi + +LATEST_TAG="$(git tag --list 'v*' --sort=-v:refname | head -n 1)" +if [[ -n "$LATEST_TAG" ]]; then + if ! php -r 'exit(version_compare($argv[1], $argv[2], ">") ? 0 : 1);' "$VERSION" "${LATEST_TAG#v}"; then + fail "Version $VERSION is not higher than the latest release tag $LATEST_TAG" + fi +fi + +echo "All checks passed: releasing $TAG at $(git rev-parse --short "$REMOTE_SHA") (previous release: ${LATEST_TAG:-none})" + +if [[ "$DRY_RUN" == "true" ]]; then + echo "Dry run; not creating the release." + exit 0 +fi + +read -r -p "Create tag and GitHub release $TAG? [y/N] " CONFIRM +if [[ "$CONFIRM" != "y" && "$CONFIRM" != "Y" ]]; then + echo "Aborted." + exit 1 +fi + +gh release create "$TAG" --target "$REMOTE_SHA" --title "$TAG" --generate-notes +git fetch --quiet --tags origin +echo "Released $TAG."