From 3c2edfeb8a8f0d35504af1abdca14c4e3477607d Mon Sep 17 00:00:00 2001 From: Will Pfleger Date: Wed, 3 Jun 2026 15:29:09 -0400 Subject: [PATCH] feat(release): add GitHub App token, git-log release notes, and Buildkite automation The sync-release-changelog workflow now uses a GitHub App token instead of GITHUB_TOKEN so pushes to the release PR branch re-trigger CI (the repo's ruleset requires 5 status checks to pass). Also hardens PR discovery with --limit 100 --base main, and serializes concurrent runs with cancel-in-progress: false to prevent non-fast-forward push races. release.yml generates GitHub release notes from git log at release time instead of reading from CHANGELOG.md, closing the merge-before-sync race where someone merges the release PR before the changelog sync finishes. auto-tag-on-release-pr-merge.yml now triggers the internal sprout-releases Buildkite pipeline via REST API after pushing the tag, passing version metadata so the pipeline's input step is skipped. --- .../auto-tag-on-release-pr-merge.yml | 22 ++++++++++++++++ .github/workflows/release.yml | 13 +++++----- .github/workflows/sync-release-changelog.yml | 17 +++++++++--- RELEASING.md | 26 +++++++++++++------ 4 files changed, 61 insertions(+), 17 deletions(-) diff --git a/.github/workflows/auto-tag-on-release-pr-merge.yml b/.github/workflows/auto-tag-on-release-pr-merge.yml index 115a0678e7..2dd9c4d146 100644 --- a/.github/workflows/auto-tag-on-release-pr-merge.yml +++ b/.github/workflows/auto-tag-on-release-pr-merge.yml @@ -60,3 +60,25 @@ jobs: gh workflow run release.yml \ -f version="$VERSION" \ -f ref="v$VERSION" + + - name: Trigger internal release build + env: + BUILDKITE_API_TOKEN: ${{ secrets.BUILDKITE_API_TOKEN }} + VERSION: ${{ env.version }} + run: | + curl -sf -X POST \ + "https://api.buildkite.com/v2/organizations/runway/pipelines/sprout-releases/builds" \ + -H "Authorization: Bearer ${BUILDKITE_API_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{ + \"commit\": \"HEAD\", + \"branch\": \"main\", + \"message\": \"Release v${VERSION}\", + \"meta_data\": { + \"version\": \"${VERSION}\", + \"sprout_ref\": \"v${VERSION}\", + \"relay_url\": \"wss://sprout-oss.stage.blox.sqprod.co\", + \"publish_latest\": \"true\" + } + }" + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3038699dcd..af9f34cb69 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -50,6 +50,8 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: ref: ${{ github.event_name == 'push' && github.ref || inputs.ref }} + fetch-depth: 0 + fetch-tags: true persist-credentials: false - uses: cashapp/activate-hermit@e49f5cb4dd64ff0b0b659d1d8df499595451155a # v1 @@ -229,12 +231,11 @@ jobs: DMG_PATH: ${{ steps.artifacts.outputs.dmg }} run: | RELEASE_SHA=$(git rev-parse HEAD) - NOTES="" - if [[ -f CHANGELOG.md ]]; then - NOTES=$(awk "/^## v${VERSION}$/,/^## v/" CHANGELOG.md | sed '$d') - fi - if [[ -z "$NOTES" ]]; then - NOTES="Sprout Desktop v${VERSION}" + PREV_TAG=$(git describe --tags --abbrev=0 --match 'v[0-9]*' HEAD^ 2>/dev/null || echo "") + if [[ -n "$PREV_TAG" ]]; then + NOTES=$(git log "${PREV_TAG}..HEAD" --oneline --no-merges) + else + NOTES="Initial release" fi gh release create "v${VERSION}" \ --target "$RELEASE_SHA" \ diff --git a/.github/workflows/sync-release-changelog.yml b/.github/workflows/sync-release-changelog.yml index 35608a964e..ed19e20a8f 100644 --- a/.github/workflows/sync-release-changelog.yml +++ b/.github/workflows/sync-release-changelog.yml @@ -4,10 +4,10 @@ on: push: branches: [main] -# Collapse rapid successive main pushes into one sync run. +# Serialize successive main pushes — each run generates the correct changelog. concurrency: group: sync-release-changelog - cancel-in-progress: true + cancel-in-progress: false permissions: contents: write @@ -29,6 +29,8 @@ jobs: PRS=$(gh pr list \ --repo block/sprout \ --state open \ + --base main \ + --limit 100 \ --json number,headRefName,headRepository \ --jq '[.[] | select(.headRepository.nameWithOwner == "block/sprout") | @@ -59,6 +61,14 @@ jobs: echo "version=$VERSION" >> "$GITHUB_OUTPUT" echo "Found release PR #${NUMBER} (${BRANCH})" + - name: Generate GitHub App token + if: steps.find-pr.outputs.found == 'true' + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.RELEASE_APP_CLIENT_ID }} + private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} + - name: Check out release PR branch if: steps.find-pr.outputs.found == 'true' uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 @@ -66,13 +76,14 @@ jobs: ref: ${{ steps.find-pr.outputs.branch }} fetch-depth: 0 fetch-tags: true + token: ${{ steps.app-token.outputs.token }} - name: Regenerate changelog section if: steps.find-pr.outputs.found == 'true' env: VERSION: ${{ steps.find-pr.outputs.version }} PR_NUMBER: ${{ steps.find-pr.outputs.number }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} run: | set -euo pipefail diff --git a/RELEASING.md b/RELEASING.md index d759c19381..928d370f08 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -26,9 +26,9 @@ This creates a `version-bump/` PR that bumps all version manifests, reg 2. **The release PR stays in sync** — if other PRs merge to `main` while the release PR is open, the `sync-release-changelog` workflow automatically updates the changelog on the release PR branch so the final release notes reflect everything that will be in the binary. No manual action needed. -3. **Merge the PR** — the `auto-tag-on-release-pr-merge` workflow detects the `version-bump/*` branch merge and pushes a `v` tag. +3. **Merge the PR** — the `auto-tag-on-release-pr-merge` workflow detects the `version-bump/*` branch merge, pushes a `v` tag, and triggers both the OSS release build and the internal Buildkite pipeline automatically. -4. **Tag triggers `release.yml`** — the existing release workflow builds, signs, notarizes, and publishes the desktop app for macOS and Linux. +4. **Builds run in parallel** — `release.yml` builds, signs, notarizes, and publishes the OSS desktop app. The `sprout-releases` Buildkite pipeline produces Block-signed macOS and iOS builds with the `-block` version suffix. --- @@ -70,7 +70,13 @@ If the automated flow isn't suitable (e.g., building from a non-main ref): ## Internal Releases -After the OSS release ships, trigger an internal build via the **sprout-releases** Buildkite pipeline: +Internal builds are triggered automatically when the release PR merges. The `auto-tag-on-release-pr-merge` workflow calls the Buildkite REST API to start the [sprout-releases pipeline](https://buildkite.com/runway/sprout-releases) with the correct version, tag ref, relay URL, and `publish_latest=true`. No manual action needed. + +Internal desktop builds display a `-block` suffix in the version (e.g., `v0.3.0-block` in the Settings panel). This distinguishes them from OSS builds at a glance. iOS builds and GitHub release tags use the clean version (`0.3.0`) since Apple's `CFBundleShortVersionString` rejects pre-release suffixes. + +### Manual Buildkite Trigger (Fallback) + +If the automated trigger fails or you need a custom build (different relay URL, `publish_latest=false`, non-tag ref): 1. Go to the [sprout-releases pipeline](https://buildkite.com/runway/sprout-releases) and click **New Build** 2. Fill in the input fields: @@ -80,9 +86,7 @@ After the OSS release ships, trigger an internal build via the **sprout-releases | `version` | `0.3.0` | Semver, no `v` prefix | | `sprout_ref` | `v0.3.0` | The OSS git tag — use the tag, not a branch name | | `relay_url` | *(default)* | Pre-filled with the production relay; usually leave as-is | - | `publish_latest` | `true` | Updates `latest.json` on Artifactory so installed apps auto-update. Set to `false` for test builds. | - -Internal desktop builds display a `-block` suffix in the version (e.g., `v0.3.0-block` in the Settings panel). This distinguishes them from OSS builds at a glance. iOS builds and GitHub release tags use the clean version (`0.3.0`) since Apple's `CFBundleShortVersionString` rejects pre-release suffixes. + | `publish_latest` | `true` | Set to `false` for test builds | --- @@ -102,11 +106,14 @@ Each release produces two GitHub releases: - **`gh` CLI** authenticated (`gh auth status`) - The following **GitHub Actions secrets** must be configured: - | Secret | Purpose | - |--------|---------| + | Secret / Variable | Purpose | + |-------------------|---------| | `SPROUT_UPDATER_PUBLIC_KEY` | Tauri updater public key (minisign) | | `TAURI_SIGNING_PRIVATE_KEY` | Tauri updater private key | | `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` | Password for the private key | + | `RELEASE_APP_CLIENT_ID` (variable) | GitHub App client ID for release automation | + | `RELEASE_APP_PRIVATE_KEY` (secret) | GitHub App private key for release automation | + | `BUILDKITE_API_TOKEN` | Runway Buildkite API token (`write_builds` scope) | --- @@ -126,3 +133,6 @@ Verify that the `sprout-desktop-latest` release exists and contains a valid `lat ### `sync-release-changelog` workflow fails with "Multiple open release PRs" More than one `version-bump/*` PR is open. Close or merge the stale one before the sync can resume. + +### Internal Buildkite build didn't trigger after release PR merge +Check the `auto-tag-on-release-pr-merge` workflow run for errors in the "Trigger internal release build" step. Common causes: expired `BUILDKITE_API_TOKEN`, pipeline slug changed, or the token lacks `BUILD_AND_READ` access on the pipeline. Fall back to a [manual Buildkite trigger](#manual-buildkite-trigger-fallback).