diff --git a/.github/workflows/release-prepare.yml b/.github/workflows/release-prepare.yml deleted file mode 100644 index ff16c47c..00000000 --- a/.github/workflows/release-prepare.yml +++ /dev/null @@ -1,155 +0,0 @@ -name: Prepare release - -on: - release: - types: [created, edited] - -concurrency: - group: release-prepare-${{ github.event.release.tag_name }} - cancel-in-progress: false - -jobs: - prepare-release: - name: Prepare release PR and build plugin zip - if: >- - github.repository == 'WordPress/sqlite-database-integration' - && github.event.release.draft == true - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - ref: trunk - - - name: Extract version and changelog - id: release_info - env: - RELEASE_TAG: ${{ github.event.release.tag_name }} - RELEASE_BODY: ${{ github.event.release.body }} - run: | - # Extract version from tag (strip leading "v" if present). - VERSION="${RELEASE_TAG#v}" - if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then - echo "::error::Invalid version format: '$VERSION' (expected semver like '1.2.3')" - exit 1 - fi - echo "version=$VERSION" >> $GITHUB_OUTPUT - - # Extract changelog, converting markdown list items to WP readme format. - CHANGELOG=$(echo "$RELEASE_BODY" | sed 's/^- /* /' | sed 's/^[[:space:]]*- /* /') - { - echo "changelog<> $GITHUB_OUTPUT - - # Branch name for the release PR. - echo "branch=release/v$VERSION" >> $GITHUB_OUTPUT - - - name: Create release branch - env: - BRANCH: ${{ steps.release_info.outputs.branch }} - run: | - git checkout -B "$BRANCH" - - - name: Bump version numbers - env: - VERSION: ${{ steps.release_info.outputs.version }} - run: | - # Update version.php - sed -i "s/define( 'SQLITE_DRIVER_VERSION', '.*' );/define( 'SQLITE_DRIVER_VERSION', '$VERSION' );/" \ - packages/mysql-on-sqlite/src/version.php - - # Update plugin header - sed -i "s/^\( \* Version:\).*/\1 $VERSION/" \ - packages/plugin-sqlite-database-integration/load.php - - # Update readme.txt stable tag - sed -i "s/^Stable tag:.*/Stable tag: $VERSION/" \ - packages/plugin-sqlite-database-integration/readme.txt - - - name: Update changelog in readme.txt - env: - VERSION: ${{ steps.release_info.outputs.version }} - CHANGELOG: ${{ steps.release_info.outputs.changelog }} - run: | - README="packages/plugin-sqlite-database-integration/readme.txt" - - # Build the new changelog entry. - ENTRY=$(printf "= %s =\n\n%s\n" "$VERSION" "$CHANGELOG") - - if grep -q "^== Changelog ==" "$README"; then - # Insert the new entry after the == Changelog == header. - awk -v entry="$ENTRY" ' - /^== Changelog ==/ { print; print ""; print entry; next } - { print } - ' "$README" > "$README.tmp" && mv "$README.tmp" "$README" - else - # Add a changelog section at the end. - printf "\n== Changelog ==\n\n%s\n" "$ENTRY" >> "$README" - fi - - - name: Commit version bump - env: - VERSION: ${{ steps.release_info.outputs.version }} - BRANCH: ${{ steps.release_info.outputs.branch }} - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add -A - git commit -m "Bump version to $VERSION" - git push -f origin "$BRANCH" - - - name: Build plugin zip - run: composer run build-sqlite-plugin-zip - - - name: Upload zip to draft release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RELEASE_TAG: ${{ github.event.release.tag_name }} - run: | - # Remove any previously uploaded zip. - gh release delete-asset "$RELEASE_TAG" "plugin-sqlite-database-integration.zip" --yes 2>/dev/null || true - # Upload the new zip. - gh release upload "$RELEASE_TAG" "build/plugin-sqlite-database-integration.zip" - - - name: Create or update pull request - id: pr - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - VERSION: ${{ steps.release_info.outputs.version }} - BRANCH: ${{ steps.release_info.outputs.branch }} - run: | - # Check if a PR already exists for this branch. - EXISTING_PR=$(gh pr list --head "$BRANCH" --base trunk --json number --jq '.[0].number // empty') - - if [ -n "$EXISTING_PR" ]; then - PR_URL=$(gh pr view "$EXISTING_PR" --json url --jq '.url') - echo "pr_url=$PR_URL" >> $GITHUB_OUTPUT - else - PR_URL=$(gh pr create \ - --base trunk \ - --head "$BRANCH" \ - --title "Release $VERSION" \ - --body "Version bump and changelog update for release $VERSION.") - echo "pr_url=$PR_URL" >> $GITHUB_OUTPUT - fi - - - name: Add PR link to draft release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RELEASE_TAG: ${{ github.event.release.tag_name }} - RELEASE_BODY: ${{ github.event.release.body }} - PR_URL: ${{ steps.pr.outputs.pr_url }} - run: | - PR_NOTE="> To publish the release, review and merge: $PR_URL" - - # Remove any existing PR note, then append the new one. - CLEAN_BODY=$(echo "$RELEASE_BODY" | grep -v "^> To publish the release, review and merge:") - NEW_BODY=$(printf "%s\n\n%s" "$CLEAN_BODY" "$PR_NOTE") - - gh release edit "$RELEASE_TAG" --notes "$NEW_BODY" diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index e6b7aa02..aab24bd3 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -5,9 +5,13 @@ on: types: [closed] branches: [trunk] +concurrency: + group: release-publish + cancel-in-progress: false + jobs: publish-release: - name: Publish draft release + name: Build plugin and create GitHub release if: >- github.repository == 'WordPress/sqlite-database-integration' && github.event.pull_request.merged == true @@ -30,21 +34,59 @@ jobs: echo "version=$VERSION" >> $GITHUB_OUTPUT echo "tag=v$VERSION" >> $GITHUB_OUTPUT - - name: Publish draft release + # Versions with a hyphen (e.g., 2.3.0-beta.1) are prereleases. + if [[ "$VERSION" == *-* ]]; then + echo "prerelease=true" >> $GITHUB_OUTPUT + else + echo "prerelease=false" >> $GITHUB_OUTPUT + fi + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + + - name: Build plugin zip + run: composer run build-sqlite-plugin-zip + + - name: Extract changelog from readme.txt + id: changelog + env: + VERSION: ${{ steps.version.outputs.version }} + run: | + README="packages/plugin-sqlite-database-integration/readme.txt" + + # Extract the changelog entry for this version. + CHANGELOG=$(awk -v version="$VERSION" ' + $0 == "= " version " =" { found = 1; next } + found && /^= / { exit } + found { print } + ' "$README" | sed -e '/./,$!d' -e :a -e '/^\s*$/{ $d; N; ba; }') # Trim leading/trailing blank lines. + + { + echo "body<> $GITHUB_OUTPUT + + - name: Create GitHub release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAG: ${{ steps.version.outputs.tag }} MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }} + BODY: ${{ steps.changelog.outputs.body }} run: | - # Get the current release body and remove the PR note. - BODY=$(gh release view "$TAG" --json body --jq '.body' 2>/dev/null || echo "") - CLEAN_BODY=$(echo "$BODY" | grep -v "^> To publish the release, review and merge:") + PRERELEASE_FLAG="" + if [[ "${{ steps.version.outputs.prerelease }}" == "true" ]]; then + PRERELEASE_FLAG="--prerelease" + fi - # Publish the release, targeting the merge commit. - gh release edit "$TAG" \ - --draft=false \ + gh release create "$TAG" \ --target "$MERGE_SHA" \ - --notes "$CLEAN_BODY" + --title "$TAG" \ + --notes "$BODY" \ + $PRERELEASE_FLAG \ + "build/sqlite-database-integration.zip" - name: Delete release branch env: diff --git a/.github/workflows/release-wporg.yml b/.github/workflows/release-wporg.yml index 8f832d2b..98128679 100644 --- a/.github/workflows/release-wporg.yml +++ b/.github/workflows/release-wporg.yml @@ -11,7 +11,9 @@ concurrency: jobs: deploy: name: Deploy plugin to WordPress.org - if: github.repository == 'WordPress/sqlite-database-integration' + if: >- + github.repository == 'WordPress/sqlite-database-integration' + && github.event.release.prerelease == false runs-on: ubuntu-latest environment: WordPress.org permissions: diff --git a/AGENTS.md b/AGENTS.md index 21a52656..19d2f674 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,6 +46,7 @@ composer install # Install dependencies composer run check-cs # Check coding standards (PHPCS) composer run fix-cs # Auto-fix coding standards (PHPCBF) composer run build-sqlite-plugin-zip # Build the plugin zip +composer run prepare-release # Prepare a new release # SQLite driver tests (under packages/mysql-on-sqlite) cd packages/mysql-on-sqlite @@ -69,18 +70,25 @@ composer run wp-test-clean # Clean up WordPress environment (Docker ``` ## Release workflow -Release is automated with GitHub Actions. It requires only two manual steps: - -1. **Create a draft release with a new tag and changelog.** - The `release-prepare` workflow will automatically: - - Use the draft tag to bump versions in `version.php`, `load.php`, and `readme.txt`. - - Add the draft release changelog entry to `readme.txt`. - - Build the plugin ZIP and attach it to the draft release. - - Create a PR (`release/` → `trunk`) and link it in the draft release body. -2. **Review and merge the PR.** - The `release-publish` workflow will automatically: - - Publish the draft release. - - Publish the release artifact to WordPress.org. +Release is streamlined with a local preparation script and GitHub Actions: + +1. **Run the release preparation script locally.** + ```bash + composer run prepare-release -- + ``` + The script will: + - Bump version numbers and generate a changelog from merged PRs. + - Create a `release/` branch with a preparation commit. + - Push the branch and create a PR. + +2. **Review the PR.** + Edit the changelog or push additional changes to the release branch. + +3. **Mark as ready and merge the PR.** + The `release-publish` workflow will automatically: + - Build the plugin ZIP. + - Create and publish a GitHub release with the ZIP attached. + - Deploy the release to WordPress.org. ## Architecture The project consists of multiple components providing different APIs that funnel diff --git a/README.md b/README.md index f0e878cf..0c1de90d 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ composer install # Install dependencies composer run check-cs # Check coding standards (PHPCS) composer run fix-cs # Auto-fix coding standards (PHPCBF) composer run build-sqlite-plugin-zip # Build the plugin zip +composer run prepare-release # Prepare a new release # SQLite driver tests (under packages/mysql-on-sqlite) cd packages/mysql-on-sqlite @@ -58,18 +59,25 @@ composer run wp-test-clean # Clean up WordPress environment (Docker ``` ## Release workflow -Release is automated with GitHub Actions. It requires only two manual steps: - -1. **Create a draft release with a new tag and changelog.** - The `release-prepare` workflow will automatically: - - Use the draft tag to bump versions in `version.php`, `load.php`, and `readme.txt`. - - Add the draft release changelog entry to `readme.txt`. - - Build the plugin ZIP and attach it to the draft release. - - Create a PR (`release/` → `trunk`) and link it in the draft release body. -2. **Review and merge the PR.** - The `release-publish` workflow will automatically: - - Publish the draft release. - - Publish the release artifact to WordPress.org. +Release is streamlined with a local preparation script and GitHub Actions: + +1. **Run the release preparation script locally.** + ```bash + composer run prepare-release -- + ``` + The script will: + - Bump version numbers and generate a changelog from merged PRs. + - Create a `release/` branch with a preparation commit. + - Push the branch and create a PR. + +2. **Review the PR.** + Edit the changelog or push additional changes to the release branch. + +3. **Mark as ready and merge the PR.** + The `release-publish` workflow will automatically: + - Build the plugin ZIP. + - Create and publish a GitHub release with the ZIP attached. + - Deploy the release to WordPress.org. ## Architecture The project consists of multiple components providing different APIs that funnel diff --git a/bin/prepare-release.sh b/bin/prepare-release.sh new file mode 100755 index 00000000..6b908d70 --- /dev/null +++ b/bin/prepare-release.sh @@ -0,0 +1,148 @@ +#!/usr/bin/env bash +# +# Prepare a new release of the SQLite Database Integration plugin. +# +# Usage: +# ./bin/prepare-release.sh +# +# This script: +# 1. Verifies prerequisites (clean tree, trunk branch, up to date). +# 2. Creates a release branch and bumps version numbers. +# 3. Generates a changelog from merged PRs since the last release. +# 4. Commits the changes and creates a pull request. + +set -euo pipefail +cd "$(dirname "$0")/.." + +fail() { + printf '\033[0;31mError: %s\033[0m\n' "$1" >&2 + exit 1 +} + +REPO_URL="https://github.com/WordPress/sqlite-database-integration" +VERSION_PHP="packages/mysql-on-sqlite/src/version.php" +LOAD_PHP="packages/plugin-sqlite-database-integration/load.php" +README_TXT="packages/plugin-sqlite-database-integration/readme.txt" +CURRENT_VERSION="$(sed -n "s/.*SQLITE_DRIVER_VERSION', '\(.*\)'.*/\1/p" "$VERSION_PHP")" + +# 1. VALIDATE ARGUMENTS +NEW_VERSION="${1:-}" +NEW_VERSION="${NEW_VERSION#v}" + +if [ -z "$NEW_VERSION" ]; then + echo "Usage: $0 " + echo "" + echo "Current version: $CURRENT_VERSION" + exit 1 +fi + +if ! [[ "$NEW_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-.+)?$ ]]; then + fail "Invalid version format: $NEW_VERSION" +fi + +PRERELEASE=false +[[ "$NEW_VERSION" == *-* ]] && PRERELEASE=true + +# 2. VERIFY PREREQUISITES +command -v git >/dev/null 2>&1 || fail "git is not installed." +command -v gh >/dev/null 2>&1 || fail "gh CLI is not installed." + +[ -z "$(git status --porcelain)" ] || fail "Working tree is not clean." + +# BRANCH="$(git rev-parse --abbrev-ref HEAD)" +# [ "$BRANCH" = "trunk" ] || fail "Not on trunk branch (current: $BRANCH)." +# +# git pull --ff-only origin trunk --quiet || fail "trunk is not up to date with origin/trunk." + +# 3. GENERATE CHANGELOG +LATEST_TAG="v$CURRENT_VERSION" + +echo "Generating changelog from merged PRs since $LATEST_TAG..." +TAG_DATE="$(git log -1 --format='%aI' "$LATEST_TAG" 2>/dev/null | cut -d'T' -f1 || true)" +CHANGELOG="" + +if [ -n "$TAG_DATE" ]; then + CHANGELOG="$(gh pr list \ + --state merged \ + --base trunk \ + --search "merged:>=$TAG_DATE" \ + --limit 100 \ + --json number,title \ + --jq ".[] | \"* \\(.title) ([#\\(.number)]($REPO_URL/pull/\\(.number)))\"" 2>/dev/null \ + | grep -v "^\* Release $CURRENT_VERSION " || true)" +fi + +if [ -z "$CHANGELOG" ]; then + CHANGELOG="* (no changes listed)" +fi + +# 4. PREPARE RELEASE BRANCH +RELEASE_BRANCH="release/v$NEW_VERSION" +echo "Creating branch $RELEASE_BRANCH..." +git checkout -q -b "$RELEASE_BRANCH" + +echo "Bumping version numbers..." +sed -i.bak "s/define( 'SQLITE_DRIVER_VERSION', '.*' );/define( 'SQLITE_DRIVER_VERSION', '$NEW_VERSION' );/" "$VERSION_PHP" +sed -i.bak "s/^ \* Version: .*/ * Version: $NEW_VERSION/" "$LOAD_PHP" +sed -i.bak "s/^Stable tag:[[:space:]]*.*/Stable tag: $NEW_VERSION/" "$README_TXT" + +echo "Updating changelog..." +if ! grep -Fxq '== Changelog ==' "$README_TXT"; then + printf '\n== Changelog ==\n' >> "$README_TXT" +fi +TMPFILE="$(mktemp)" +trap 'rm -f "$TMPFILE"' EXIT +printf '\n= %s =\n\n%s\n' "$NEW_VERSION" "$CHANGELOG" > "$TMPFILE" +sed -i.bak "/^== Changelog ==$/r $TMPFILE" "$README_TXT" + +rm -f "$VERSION_PHP.bak" "$LOAD_PHP.bak" "$README_TXT.bak" + +# 5. CREATE A PULL REQUEST +echo "Committing changes..." +git add "$VERSION_PHP" "$LOAD_PHP" "$README_TXT" +git commit -q -m "Prepare release $NEW_VERSION" + +echo "Pushing to origin..." +PUSH_OUTPUT="$(git push -q origin "$RELEASE_BRANCH" 2>&1)" \ + || { echo "$PUSH_OUTPUT" >&2; fail "Failed to push $RELEASE_BRANCH."; } + +if [ "$PRERELEASE" = true ]; then + MERGE_NOTE="Merging will automatically build the plugin ZIP and create a [GitHub release]($REPO_URL/releases). + +> [!NOTE] +> This is a **pre-release**. It will not be deployed to [WordPress.org](https://wordpress.org/plugins/sqlite-database-integration/)." +else + MERGE_NOTE="Merging will automatically build the plugin ZIP, create a [GitHub release]($REPO_URL/releases), and deploy to [WordPress.org](https://wordpress.org/plugins/sqlite-database-integration/)." +fi + +echo "Creating pull request..." +PR_URL="$(gh pr create \ + --base trunk \ + --head "$RELEASE_BRANCH" \ + --title "Release $NEW_VERSION" \ + --assignee @me \ + --reviewer "$(gh api user --jq '.login')" \ + --body "$(cat <