From 46a380a0144c5a4ec1eb12461ddaebe93151ddc8 Mon Sep 17 00:00:00 2001 From: Jared Rickert Date: Mon, 27 Apr 2026 23:25:21 -0500 Subject: [PATCH 1/3] ci(release): split release into PR and publish workflows Replace the single release.yml with two workflows so dev stays in sync with main automatically: - release-pr.yml (workflow_dispatch): commit "chore: release vX.Y.Z" on dev, cut release/$VERSION from dev's tip, open PR into main. The commit lands on dev first, so a --rebase merge fast-forwards main to dev's SHA in steady state and the two branches stay aligned. - release-publish.yml (push to main, guarded by 'chore: release v'): parse version from the head commit subject, tag main HEAD, run goreleaser. Tag becomes a side-effect of publish, not the trigger. The previous workflow created the release commit on a release branch only; after the PR merged, dev never received it, causing dev/main divergence after every release. Splitting also gives the release PR a real review gate before any artifact ships. Untrusted external inputs (cliff output, workflow input, head commit subject) are read via env vars or git log to avoid run-block interpolation. --- .../workflows/{release.yml => release-pr.yml} | 69 +++++++------------ .github/workflows/release-publish.yml | 64 +++++++++++++++++ 2 files changed, 90 insertions(+), 43 deletions(-) rename .github/workflows/{release.yml => release-pr.yml} (70%) create mode 100644 .github/workflows/release-publish.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release-pr.yml similarity index 70% rename from .github/workflows/release.yml rename to .github/workflows/release-pr.yml index 16b872e..3267d80 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release-pr.yml @@ -1,11 +1,11 @@ -name: Release +name: Release PR on: workflow_dispatch: inputs: version: - description: 'Version override (e.g. v1.2.3). Leave empty to auto-detect via git-cliff.' + description: "Version override (e.g. v1.2.3). Leave empty to auto-detect via git-cliff." required: false - default: '' + default: "" permissions: contents: write pull-requests: write @@ -14,17 +14,20 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 + with: + ref: dev - uses: actions/setup-go@v6 with: go-version-file: go.mod - run: go test ./... - release: + open-pr: needs: test runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 with: + ref: dev fetch-depth: 0 fetch-tags: true - uses: actions/setup-go@v6 @@ -43,8 +46,11 @@ jobs: - name: Resolve release version id: version + env: + CLIFF_OUTPUT: ${{ steps.cliff.outputs.content }} + INPUT_VERSION: ${{ inputs.version }} run: | - DETECTED_VERSION="$(echo "${{ steps.cliff.outputs.content }}" | tr -d '[:space:]')" + DETECTED_VERSION="$(echo "$CLIFF_OUTPUT" | tr -d '[:space:]')" if [ -z "$DETECTED_VERSION" ]; then LAST_TAG="$(git tag --list 'v*' --sort=-version:refname | head -n1)" if [[ -z "$LAST_TAG" ]]; then @@ -62,7 +68,7 @@ jobs: fi fi - VERSION="${{ inputs.version }}" + VERSION="$INPUT_VERSION" if [ -z "$VERSION" ]; then VERSION="$DETECTED_VERSION" fi @@ -70,6 +76,10 @@ jobs: echo "Error: could not determine version. git-cliff returned nothing (no prior tags?). Use the version override input." >&2 exit 1 fi + if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Error: resolved version '$VERSION' is not semver (vX.Y.Z)." >&2 + exit 1 + fi if git rev-parse -q --verify "refs/tags/$VERSION" >/dev/null; then echo "Error: tag '$VERSION' already exists. Set workflow input 'version' to a new value." >&2 exit 1 @@ -89,58 +99,31 @@ jobs: TAPPER_PLUGIN_VERSION: ${{ steps.version.outputs.version }} run: go run ./cmd/render-integrations - - name: Build release commit on release branch + - name: Commit release on dev and cut release branch env: VERSION: ${{ steps.version.outputs.version }} run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - BRANCH="release/$VERSION" - git checkout -b "$BRANCH" git add CHANGELOG.md integrations/rendered if git diff --staged --quiet; then - echo "No release artifacts to commit; release branch tracks dispatch ref" - else - git commit -m "chore: release $VERSION" + echo "Error: no release artifacts staged. Expected CHANGELOG and rendered integrations to change." >&2 + exit 1 fi + git commit -m "chore: release $VERSION" + git push origin dev + BRANCH="release/$VERSION" + git checkout -b "$BRANCH" git push origin "$BRANCH" - - name: Open and merge release PR into main + - name: Open release PR into main env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} VERSION: ${{ steps.version.outputs.version }} run: | BRANCH="release/$VERSION" - PR_URL=$(gh pr create \ + gh pr create \ --base main \ --head "$BRANCH" \ --title "chore: release $VERSION" \ - --body "Automated release PR for $VERSION.") - gh pr merge "$PR_URL" --rebase --delete-branch - - - name: Tag release on main - env: - VERSION: ${{ steps.version.outputs.version }} - run: | - git fetch origin main --tags - git checkout main - git reset --hard origin/main - git tag -a "$VERSION" -m "$VERSION" - git push origin "refs/tags/$VERSION" - - - name: Ensure clean git state for goreleaser - run: | - git clean -ffdx - if [ -n "$(git status --porcelain)" ]; then - echo "Error: working tree is dirty before goreleaser." >&2 - git status --porcelain - exit 1 - fi - - - uses: goreleaser/goreleaser-action@v6 - with: - version: latest - args: release --clean - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} + --body "Automated release PR for $VERSION." diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml new file mode 100644 index 0000000..8e4e69c --- /dev/null +++ b/.github/workflows/release-publish.yml @@ -0,0 +1,64 @@ +name: Release Publish +on: + push: + branches: [main] +permissions: + contents: write +jobs: + publish: + if: "startsWith(github.event.head_commit.message, 'chore: release v')" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + fetch-tags: true + - uses: actions/setup-go@v6 + with: + go-version-file: go.mod + + - name: Run tests + run: go test ./... + + - name: Parse version from release commit + id: version + run: | + FIRST_LINE="$(git log -1 --pretty=%s HEAD)" + if [[ ! "$FIRST_LINE" =~ ^chore:\ release\ (v[0-9]+\.[0-9]+\.[0-9]+)$ ]]; then + echo "Error: head commit subject does not match 'chore: release vX.Y.Z'." >&2 + echo "Subject was: $FIRST_LINE" >&2 + exit 1 + fi + VERSION="${BASH_REMATCH[1]}" + echo "Publishing $VERSION" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + - name: Tag release if not already tagged + env: + VERSION: ${{ steps.version.outputs.version }} + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + if git rev-parse -q --verify "refs/tags/$VERSION" >/dev/null; then + echo "Tag $VERSION already exists; skipping tag creation" + else + git tag -a "$VERSION" -m "$VERSION" + git push origin "refs/tags/$VERSION" + fi + + - name: Ensure clean git state for goreleaser + run: | + git clean -ffdx + if [ -n "$(git status --porcelain)" ]; then + echo "Error: working tree is dirty before goreleaser." >&2 + git status --porcelain + exit 1 + fi + + - uses: goreleaser/goreleaser-action@v6 + with: + version: latest + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} From 63bd9686b11a68cd85734424b20d26c954e1609d Mon Sep 17 00:00:00 2001 From: Jared Rickert Date: Tue, 28 Apr 2026 23:30:13 -0500 Subject: [PATCH 2/3] fix(release-pr): tolerate git-cliff failure on first release --- .github/workflows/release-pr.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index 3267d80..d6e5569 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -39,6 +39,7 @@ jobs: - name: Determine version via git-cliff id: cliff + continue-on-error: true uses: orhun/git-cliff-action@v4 with: config: cliff.toml From 5b7ea46972ea20166f96c6b868b04192b8d4b276 Mon Sep 17 00:00:00 2001 From: Jared Rickert Date: Wed, 29 Apr 2026 17:12:25 -0500 Subject: [PATCH 3/3] =?UTF-8?q?ci(release):=20rework=20to=20manual=20dev?= =?UTF-8?q?=E2=86=92main=20PR=20flow=20+=20tag-push=20publish?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the workflow-creates-PR release pattern with one where the human opens the dev→main PR, merges it, then triggers the release action on main directly. - Rename release-pr.yml → release.yml. Trigger swaps from workflow_dispatch on dev to workflow_dispatch on main. The workflow no longer opens a PR or cuts a release branch — humans do those manually. Action steps: checkout main, run tests, resolve version via git-cliff (with continue-on-error + script-level fallback for defense in depth), generate CHANGELOG, re-render integrations with TAPPER_PLUGIN_VERSION embedded (preserved verbatim — load-bearing for plugin.json version coherence), commit chore: release on main, push main, tag and push the tag. - Restructure release-publish.yml. Trigger swaps from push: branches: [main] with chore-commit-subject sniffing to push: tags: ['v*']. Drop the chore-subject job guard, the duplicate test step, the tag-create step (tag exists by definition), and the parse-from-commit-subject step (replaced by parse-from-tag using $GITHUB_REF). Goreleaser action with HOMEBREW_TAP_GITHUB_TOKEN preserved verbatim. - Add main to ci.yml push and pull_request triggers (was dev-only). Daily work stays on dev; dev → main PRs and post-release main commits now also get CI. cliff.toml unchanged. --- .github/workflows/ci.yml | 4 +-- .github/workflows/release-publish.yml | 33 ++++-------------- .../workflows/{release-pr.yml => release.yml} | 34 ++++++++----------- 3 files changed, 23 insertions(+), 48 deletions(-) rename .github/workflows/{release-pr.yml => release.yml} (86%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5d5bb10..e9217a2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,9 +1,9 @@ name: CI on: push: - branches: [dev] + branches: [dev, main] pull_request: - branches: [dev] + branches: [dev, main] jobs: test: runs-on: ubuntu-latest diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index 8e4e69c..3b108fd 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -1,12 +1,11 @@ name: Release Publish on: push: - branches: [main] + tags: ["v*"] permissions: contents: write jobs: publish: - if: "startsWith(github.event.head_commit.message, 'chore: release v')" runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 @@ -17,34 +16,16 @@ jobs: with: go-version-file: go.mod - - name: Run tests - run: go test ./... - - - name: Parse version from release commit + - name: Parse version from tag id: version run: | - FIRST_LINE="$(git log -1 --pretty=%s HEAD)" - if [[ ! "$FIRST_LINE" =~ ^chore:\ release\ (v[0-9]+\.[0-9]+\.[0-9]+)$ ]]; then - echo "Error: head commit subject does not match 'chore: release vX.Y.Z'." >&2 - echo "Subject was: $FIRST_LINE" >&2 + TAG="${GITHUB_REF#refs/tags/}" + if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Error: tag '$TAG' is not semver (vX.Y.Z); skipping publish." >&2 exit 1 fi - VERSION="${BASH_REMATCH[1]}" - echo "Publishing $VERSION" - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - - - name: Tag release if not already tagged - env: - VERSION: ${{ steps.version.outputs.version }} - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - if git rev-parse -q --verify "refs/tags/$VERSION" >/dev/null; then - echo "Tag $VERSION already exists; skipping tag creation" - else - git tag -a "$VERSION" -m "$VERSION" - git push origin "refs/tags/$VERSION" - fi + echo "Publishing $TAG" + echo "version=$TAG" >> "$GITHUB_OUTPUT" - name: Ensure clean git state for goreleaser run: | diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release.yml similarity index 86% rename from .github/workflows/release-pr.yml rename to .github/workflows/release.yml index d6e5569..785d3a0 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -name: Release PR +name: Release on: workflow_dispatch: inputs: @@ -8,26 +8,25 @@ on: default: "" permissions: contents: write - pull-requests: write jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 with: - ref: dev + ref: main - uses: actions/setup-go@v6 with: go-version-file: go.mod - run: go test ./... - open-pr: + release: needs: test runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 with: - ref: dev + ref: main fetch-depth: 0 fetch-tags: true - uses: actions/setup-go@v6 @@ -37,6 +36,11 @@ jobs: - name: Ensure tags are available run: git fetch --force --tags + - name: Configure git identity + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + - name: Determine version via git-cliff id: cliff continue-on-error: true @@ -100,31 +104,21 @@ jobs: TAPPER_PLUGIN_VERSION: ${{ steps.version.outputs.version }} run: go run ./cmd/render-integrations - - name: Commit release on dev and cut release branch + - name: Commit release on main env: VERSION: ${{ steps.version.outputs.version }} run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" git add CHANGELOG.md integrations/rendered if git diff --staged --quiet; then echo "Error: no release artifacts staged. Expected CHANGELOG and rendered integrations to change." >&2 exit 1 fi git commit -m "chore: release $VERSION" - git push origin dev - BRANCH="release/$VERSION" - git checkout -b "$BRANCH" - git push origin "$BRANCH" + git push origin main - - name: Open release PR into main + - name: Tag and push env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} VERSION: ${{ steps.version.outputs.version }} run: | - BRANCH="release/$VERSION" - gh pr create \ - --base main \ - --head "$BRANCH" \ - --title "chore: release $VERSION" \ - --body "Automated release PR for $VERSION." + git tag -a "$VERSION" -m "$VERSION" + git push origin "refs/tags/$VERSION"