From a2f2c0feb44e35a89491450e9ec3ca729d9d4e12 Mon Sep 17 00:00:00 2001 From: Brian Whitman Date: Mon, 22 Jun 2026 12:44:56 -0700 Subject: [PATCH] Add automatic Arduino release on merge to main release.yml cuts a patch release on every merge to main: it bumps library.properties via an auto-opened-and-merged bump PR (main is protected, so the bump can't be pushed directly), creates the plain-tag GitHub release the Arduino Library Manager picks up, then dispatches the Godot addon build at the new tag to attach amy-godot-addon.zip. Fully GITHUB_TOKEN-based, no PAT: workflow_dispatch is the one event the built-in token can trigger, so the Godot zip still attaches without needing a PAT-pushed tag. GITHUB_TOKEN merges don't re-trigger workflows, so the bump merge can't loop; a [skip release] marker is belt-and-suspenders. Requires the org-level "Allow GitHub Actions to create and approve pull requests" setting (now enabled for shorepine). Co-Authored-By: Claude Opus 4.8 --- .github/workflows/release.yml | 134 ++++++++++++++++++++++++++++++++++ CLAUDE.md | 10 ++- 2 files changed, 140 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..3c022328 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,134 @@ +name: Release + +# Cut a GitHub release on every merge to main. The Arduino Library Manager polls +# this repo's releases and picks up each new tag automatically, so creating the +# release here is all it takes to publish a new Arduino library version. +# +# WHY THE BUMP GOES THROUGH A PR: main is a protected branch (a PR is required to +# merge, and enforce_admins is on), so the "Bump version to X" change to +# library.properties cannot be pushed straight to main — not even by an admin or +# a PAT. This workflow therefore opens a one-line bump PR and merges it, matching +# the manual claude/bump-* flow that produced every release to date. +# +# WHY THIS DOESN'T LOOP: the bump branch, PR, and merge are all done with the +# built-in GITHUB_TOKEN. Pushes and merges performed with GITHUB_TOKEN do NOT +# trigger workflow runs, so the bump merge can't re-trigger this workflow. (The +# "[skip release]" marker on the merge commit + the job `if:` below are just +# belt-and-suspenders on top of that guarantee.) +# +# WHY THE GODOT ZIP STILL ATTACHES: the Godot addon build (godot-addon.yml) runs +# on tag push and attaches amy-godot-addon.zip to the release. A tag created with +# GITHUB_TOKEN won't trigger that push event -- but workflow_dispatch is the one +# event GITHUB_TOKEN *is* allowed to trigger. So after creating the release we +# dispatch godot-addon.yml at the new tag; inside that run github.ref is +# refs/tags/, so its existing release job attaches the zip. No PAT, and +# nothing to renew. +# +# TO SKIP a release for a given merge, include "[skip release]" in the merge +# commit message. + +on: + push: + branches: [ "main" ] + +# Serialize releases so two merges landing close together can't both read the +# same current version and collide on the next tag. +concurrency: + group: release + cancel-in-progress: false + +permissions: + contents: write # push the bump branch, create the tag + release + pull-requests: write # open and merge the bump PR + actions: write # workflow_dispatch the Godot addon build + +jobs: + release: + if: ${{ !contains(github.event.head_commit.message, '[skip release]') }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: main + fetch-depth: 0 # full history + tags, needed to bump and tag + + - name: Compute next patch version + id: ver + run: | + set -euo pipefail + current=$(grep -E '^version=' library.properties | cut -d= -f2 | tr -d '[:space:]') + IFS=. read -r major minor patch <<< "$current" + next="$major.$minor.$((patch + 1))" + git fetch --tags --quiet + # If that tag already exists (e.g. a re-run), keep bumping until it's free. + while git rev-parse -q --verify "refs/tags/$next" >/dev/null; do + patch=$((patch + 1)); next="$major.$minor.$patch" + done + echo "current=$current" >> "$GITHUB_OUTPUT" + echo "next=$next" >> "$GITHUB_OUTPUT" + echo "Releasing $current -> $next" + + - name: Open and merge the version-bump PR + env: + GH_TOKEN: ${{ github.token }} + NEXT: ${{ steps.ver.outputs.next }} + run: | + set -euo pipefail + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + branch="bump-$NEXT" + git switch -c "$branch" + sed -i -E "s/^version=.*/version=$NEXT/" library.properties + git commit -am "Bump version to $NEXT" + git push -u origin "$branch" + + gh pr create --base main --head "$branch" \ + --title "Bump version to $NEXT" \ + --body "Automated release bump to $NEXT, cut on merge to main by .github/workflows/release.yml." + + # The PR was created with GITHUB_TOKEN, so it triggers no checks and is + # immediately mergeable; retry briefly while GitHub computes mergeability. + for i in $(seq 1 10); do + if gh pr merge "$branch" --merge --delete-branch \ + --subject "Bump version to $NEXT [skip release]"; then + exit 0 + fi + echo "Bump PR not mergeable yet (attempt $i/10); retrying..." + sleep 6 + done + echo "::error::Could not merge the version-bump PR for $NEXT" >&2 + exit 1 + + - name: Create the GitHub release + env: + GH_TOKEN: ${{ github.token }} + NEXT: ${{ steps.ver.outputs.next }} + run: | + set -euo pipefail + # Tag main's tip, which is now the bump merge (library.properties == + # $NEXT). Plain tag (no leading "v") -- Arduino requires this format. + gh release create "$NEXT" \ + --target main \ + --title "$NEXT" \ + --generate-notes + + - name: Build & attach the Godot addon zip + env: + GH_TOKEN: ${{ github.token }} + NEXT: ${{ steps.ver.outputs.next }} + run: | + # Dispatch the existing Godot build at the new tag. workflow_dispatch is + # the one event GITHUB_TOKEN can trigger; the run sees github.ref = + # refs/tags/$NEXT, so its release job attaches amy-godot-addon.zip here. + # The release + tag already exist, so don't fail the job over a transient + # dispatch hiccup -- just warn (it can be re-dispatched by hand). + for i in $(seq 1 5); do + if gh workflow run godot-addon.yml --ref "$NEXT"; then + echo "Dispatched godot-addon.yml at $NEXT" + exit 0 + fi + echo "Godot dispatch failed (attempt $i/5); retrying..." + sleep 5 + done + echo "::warning::Release $NEXT is published, but dispatching godot-addon.yml failed. Re-run it manually: gh workflow run godot-addon.yml --ref $NEXT" diff --git a/CLAUDE.md b/CLAUDE.md index bca9b29f..29554340 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,10 +2,12 @@ ## Releases -When creating a new release: -1. Update the version in `library.properties` to match the release tag -2. Create the GitHub release with a plain version tag (e.g. `1.2.3`, NOT `v1.2.3`) — Arduino requires this format -3. The Godot addon build workflow triggers automatically on tag push and attaches `amy-godot-addon.zip` to the release +Releases are cut **automatically on every merge to `main`** by `.github/workflows/release.yml`: +1. It bumps the **patch** number in `library.properties` (e.g. `1.2.7` → `1.2.8`). Because `main` is protected (PR required, `enforce_admins` on), the bump can't be pushed directly — the workflow opens and merges a `bump-X` PR with the built-in `GITHUB_TOKEN` (whose pushes/merges don't re-trigger workflows, so it can't loop). +2. It creates the GitHub release with a plain version tag (e.g. `1.2.8`, NOT `v1.2.8`) — Arduino requires this format — which the Arduino Library Manager picks up. +3. The Godot addon build (`godot-addon.yml`) attaches `amy-godot-addon.zip`. A `GITHUB_TOKEN`-created tag can't fire `godot-addon.yml`'s `push: tags` trigger, so the release workflow instead dispatches it via `workflow_dispatch` at the new tag (the one event `GITHUB_TOKEN` *can* trigger). No PAT is involved. + +To **skip** a release for a given merge, put `[skip release]` in the merge commit message. To cut a release manually, the same three steps work by hand (bump `library.properties` via PR, `gh release create `, then the Godot build). ## Godot GDExtension