From bea8db0ebd53319cbab8872e37b7f30bc6e9719b Mon Sep 17 00:00:00 2001 From: Sam Calder-Mason Date: Fri, 17 Jul 2026 16:50:11 +1000 Subject: [PATCH 1/3] feat(ci): one-click releases that auto-trigger lab-backend Add a cut-release workflow that computes the next version and publishes the GitHub release, and make the release workflow dispatch a frontend-release event to lab-backend once assets are uploaded, so the backend release no longer needs to be run by hand afterwards. Claude-Session: https://claude.ai/code/session_01MW57Wwhb2PA4rjwP7PBv1b --- .github/workflows/cut-release.yaml | 63 ++++++++++++++++++++++++++++++ .github/workflows/release.yaml | 11 ++++++ RELEASING.md | 19 +++++++++ 3 files changed, 93 insertions(+) create mode 100644 .github/workflows/cut-release.yaml create mode 100644 RELEASING.md diff --git a/.github/workflows/cut-release.yaml b/.github/workflows/cut-release.yaml new file mode 100644 index 000000000..3bde00454 --- /dev/null +++ b/.github/workflows/cut-release.yaml @@ -0,0 +1,63 @@ +name: Cut Release + +# Computes the next version tag and publishes a GitHub release. +# Publishing the release triggers the Release workflow, which builds the +# frontend, attaches the archives, and dispatches a lab-backend release. + +on: + workflow_dispatch: + inputs: + bump: + description: 'Version bump type' + required: true + default: 'patch' + type: choice + options: + - patch + - minor + - major + +permissions: + contents: write + +jobs: + cut-release: + name: Tag and Publish Release + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + + - name: Compute next version + id: version + run: | + LATEST_TAG=$(git tag -l 'v[0-9]*.[0-9]*.[0-9]*' | sort -V | tail -n 1) + + if [ -z "$LATEST_TAG" ]; then + NEW_VERSION="0.0.1" + else + VERSION="${LATEST_TAG#v}" + IFS='.' read -r major minor patch <<< "$VERSION" + case "${{ inputs.bump }}" in + major) NEW_VERSION="$((major + 1)).0.0" ;; + minor) NEW_VERSION="${major}.$((minor + 1)).0" ;; + *) NEW_VERSION="${major}.${minor}.$((patch + 1))" ;; + esac + fi + + echo "Latest tag: ${LATEST_TAG:-none}, new tag: v${NEW_VERSION}" + echo "new_tag=v${NEW_VERSION}" >> $GITHUB_OUTPUT + + # EPOBOT_TOKEN (not GITHUB_TOKEN) so the release publish event + # triggers the Release workflow. + - name: Create GitHub release + env: + GH_TOKEN: ${{ secrets.EPOBOT_TOKEN }} + run: | + gh release create "${{ steps.version.outputs.new_tag }}" \ + --repo "${{ github.repository }}" \ + --target "${{ github.sha }}" \ + --title "${{ steps.version.outputs.new_tag }}" \ + --generate-notes diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index e67e930ef..98b5d22c8 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -42,3 +42,14 @@ jobs: files: | ./lab-${{ github.event.release.tag_name }}.tar.gz ./lab-${{ github.event.release.tag_name }}.zip + + # Assets are uploaded, so lab-backend can now cut a release that + # embeds this frontend version. + - name: Trigger lab-backend release + if: ${{ !github.event.release.prerelease }} + uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4.0.1 + with: + token: ${{ secrets.EPOBOT_TOKEN }} + repository: ethpandaops/lab-backend + event-type: frontend-release + client-payload: '{"frontend_tag": "${{ github.event.release.tag_name }}"}' diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 000000000..f06edef62 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,19 @@ +# Releasing + +## Stable release + +Run the [Cut Release workflow](../../actions/workflows/cut-release.yaml) (or `gh workflow run cut-release.yaml`), picking a `patch`/`minor`/`major` bump. Everything else is automatic: + +1. **Cut Release** computes the next `vX.Y.Z` tag and publishes a GitHub release from `master`. +2. **Release** builds the frontend and attaches `lab-.tar.gz` / `lab-.zip` to the release. +3. It then dispatches a `frontend-release` event to [lab-backend](https://github.com/ethpandaops/lab-backend), which tags its own next release and builds Docker images embedding this exact frontend version. + +End to end this takes ~10 minutes; the lab-backend release notes and `/lab-backend/version` output record which frontend tag was embedded. + +Publishing a release by hand through the GitHub UI also works — step 2 onwards is triggered by the release publish event, not by the Cut Release workflow. + +Deploying the resulting image is still a manual bump of `image.tag` in the [platform](https://github.com/ethpandaops/platform) lab application values. + +## Alpha releases + +Pushing to a `release/` branch auto-tags and publishes a prerelease named `-vX.Y.Z` with the same archives attached. Prereleases do **not** trigger a lab-backend release; to build a backend image against one, run the lab-backend `release` workflow manually with `frontend_tag: -vX.Y.Z`. From bb1e063b4684bfa5b06ff1c717d57cc28fa98eeb Mon Sep 17 00:00:00 2001 From: Sam Calder-Mason Date: Fri, 17 Jul 2026 17:09:10 +1000 Subject: [PATCH 2/3] fix(ci): only consider plain semver tags when computing next version Claude-Session: https://claude.ai/code/session_01MW57Wwhb2PA4rjwP7PBv1b --- .github/workflows/cut-release.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cut-release.yaml b/.github/workflows/cut-release.yaml index 3bde00454..0b2ab942a 100644 --- a/.github/workflows/cut-release.yaml +++ b/.github/workflows/cut-release.yaml @@ -33,7 +33,7 @@ jobs: - name: Compute next version id: version run: | - LATEST_TAG=$(git tag -l 'v[0-9]*.[0-9]*.[0-9]*' | sort -V | tail -n 1) + LATEST_TAG=$(git tag -l 'v*' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -n 1) if [ -z "$LATEST_TAG" ]; then NEW_VERSION="0.0.1" From 6b705712bfd6c563cba08e3b3a1929fa8bf6aecd Mon Sep 17 00:00:00 2001 From: Sam Calder-Mason Date: Fri, 17 Jul 2026 17:22:57 +1000 Subject: [PATCH 3/3] refactor(ci): fold cut-release into the release workflow One workflow now handles both entry points: workflow_dispatch computes the next tag and creates the release itself (with GITHUB_TOKEN, since nothing needs to trigger off the publish event anymore), and a manually published release picks up at the build step as before. Claude-Session: https://claude.ai/code/session_01MW57Wwhb2PA4rjwP7PBv1b --- .github/workflows/cut-release.yaml | 63 ---------------------- .github/workflows/release.yaml | 85 +++++++++++++++++++++++++----- RELEASING.md | 8 +-- 3 files changed, 77 insertions(+), 79 deletions(-) delete mode 100644 .github/workflows/cut-release.yaml diff --git a/.github/workflows/cut-release.yaml b/.github/workflows/cut-release.yaml deleted file mode 100644 index 0b2ab942a..000000000 --- a/.github/workflows/cut-release.yaml +++ /dev/null @@ -1,63 +0,0 @@ -name: Cut Release - -# Computes the next version tag and publishes a GitHub release. -# Publishing the release triggers the Release workflow, which builds the -# frontend, attaches the archives, and dispatches a lab-backend release. - -on: - workflow_dispatch: - inputs: - bump: - description: 'Version bump type' - required: true - default: 'patch' - type: choice - options: - - patch - - minor - - major - -permissions: - contents: write - -jobs: - cut-release: - name: Tag and Publish Release - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - fetch-depth: 0 - - - name: Compute next version - id: version - run: | - LATEST_TAG=$(git tag -l 'v*' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -n 1) - - if [ -z "$LATEST_TAG" ]; then - NEW_VERSION="0.0.1" - else - VERSION="${LATEST_TAG#v}" - IFS='.' read -r major minor patch <<< "$VERSION" - case "${{ inputs.bump }}" in - major) NEW_VERSION="$((major + 1)).0.0" ;; - minor) NEW_VERSION="${major}.$((minor + 1)).0" ;; - *) NEW_VERSION="${major}.${minor}.$((patch + 1))" ;; - esac - fi - - echo "Latest tag: ${LATEST_TAG:-none}, new tag: v${NEW_VERSION}" - echo "new_tag=v${NEW_VERSION}" >> $GITHUB_OUTPUT - - # EPOBOT_TOKEN (not GITHUB_TOKEN) so the release publish event - # triggers the Release workflow. - - name: Create GitHub release - env: - GH_TOKEN: ${{ secrets.EPOBOT_TOKEN }} - run: | - gh release create "${{ steps.version.outputs.new_tag }}" \ - --repo "${{ github.repository }}" \ - --target "${{ github.sha }}" \ - --title "${{ steps.version.outputs.new_tag }}" \ - --generate-notes diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 98b5d22c8..9943bcddd 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,19 +1,77 @@ name: Release +# Builds the frontend, attaches it to a GitHub release, and dispatches a +# lab-backend release embedding this version. +# +# Run manually (workflow_dispatch) to cut the next release from master, or +# publish a release by hand and the release event picks up from there. + on: release: types: [published] + workflow_dispatch: + inputs: + bump: + description: 'Version bump type' + required: true + default: 'patch' + type: choice + options: + - patch + - minor + - major permissions: contents: write +# Serialized so concurrent runs cannot compute the same next tag. +concurrency: + group: release + jobs: - build: - name: Build and Upload Release Assets + release: + name: Build and Publish Release runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + + # A release created here with GITHUB_TOKEN emits no workflow events, + # so this run does not re-trigger itself via the release trigger. + - name: Resolve release tag + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + if [ "${{ github.event_name }}" = "release" ]; then + TAG_NAME="${{ github.event.release.tag_name }}" + else + LATEST_TAG=$(git tag -l 'v*' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -n 1) + + if [ -z "$LATEST_TAG" ]; then + NEW_VERSION="0.0.1" + else + VERSION="${LATEST_TAG#v}" + IFS='.' read -r major minor patch <<< "$VERSION" + case "${{ inputs.bump }}" in + major) NEW_VERSION="$((major + 1)).0.0" ;; + minor) NEW_VERSION="${major}.$((minor + 1)).0" ;; + *) NEW_VERSION="${major}.${minor}.$((patch + 1))" ;; + esac + fi + + TAG_NAME="v${NEW_VERSION}" + echo "Latest tag: ${LATEST_TAG:-none}, new tag: $TAG_NAME" + + gh release create "$TAG_NAME" \ + --repo "${{ github.repository }}" \ + --target "${{ github.sha }}" \ + --title "$TAG_NAME" \ + --generate-notes + fi + + echo "TAG_NAME=$TAG_NAME" >> $GITHUB_ENV - name: Set up pnpm uses: pnpm/action-setup@v4 @@ -31,25 +89,28 @@ jobs: - name: Build project run: pnpm build - - name: Create build archive + - name: Create build archives run: | - tar -czf lab-${{ github.event.release.tag_name }}.tar.gz -C dist . - cd dist && zip -r ../lab-${{ github.event.release.tag_name }}.zip . && cd .. + tar -czf lab-${TAG_NAME}.tar.gz -C dist . + cd dist && zip -r ../lab-${TAG_NAME}.zip . && cd .. - name: Upload release assets - uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1 - with: - files: | - ./lab-${{ github.event.release.tag_name }}.tar.gz - ./lab-${{ github.event.release.tag_name }}.zip + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release upload "$TAG_NAME" \ + --repo "${{ github.repository }}" \ + --clobber \ + "lab-${TAG_NAME}.tar.gz" \ + "lab-${TAG_NAME}.zip" # Assets are uploaded, so lab-backend can now cut a release that # embeds this frontend version. - name: Trigger lab-backend release - if: ${{ !github.event.release.prerelease }} + if: github.event_name == 'workflow_dispatch' || !github.event.release.prerelease uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4.0.1 with: token: ${{ secrets.EPOBOT_TOKEN }} repository: ethpandaops/lab-backend event-type: frontend-release - client-payload: '{"frontend_tag": "${{ github.event.release.tag_name }}"}' + client-payload: '{"frontend_tag": "${{ env.TAG_NAME }}"}' diff --git a/RELEASING.md b/RELEASING.md index f06edef62..3460510a4 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -2,15 +2,15 @@ ## Stable release -Run the [Cut Release workflow](../../actions/workflows/cut-release.yaml) (or `gh workflow run cut-release.yaml`), picking a `patch`/`minor`/`major` bump. Everything else is automatic: +Run the [Release workflow](../../actions/workflows/release.yaml) (or `gh workflow run release.yaml`), picking a `patch`/`minor`/`major` bump. Everything else is automatic: -1. **Cut Release** computes the next `vX.Y.Z` tag and publishes a GitHub release from `master`. -2. **Release** builds the frontend and attaches `lab-.tar.gz` / `lab-.zip` to the release. +1. The workflow computes the next `vX.Y.Z` tag and publishes a GitHub release from `master`. +2. It builds the frontend and attaches `lab-.tar.gz` / `lab-.zip` to the release. 3. It then dispatches a `frontend-release` event to [lab-backend](https://github.com/ethpandaops/lab-backend), which tags its own next release and builds Docker images embedding this exact frontend version. End to end this takes ~10 minutes; the lab-backend release notes and `/lab-backend/version` output record which frontend tag was embedded. -Publishing a release by hand through the GitHub UI also works — step 2 onwards is triggered by the release publish event, not by the Cut Release workflow. +Publishing a release by hand through the GitHub UI also works — the release publish event runs the same workflow from step 2 onwards. Deploying the resulting image is still a manual bump of `image.tag` in the [platform](https://github.com/ethpandaops/platform) lab application values.