From ee3a5147234d57b40938041d74a6236bf02cc1cb Mon Sep 17 00:00:00 2001 From: Petr Heinz Date: Fri, 25 Sep 2026 16:59:02 +0200 Subject: [PATCH 1/6] Add the Release workflow: publish to RubyGems.org from GitHub Actions Mirrors the logtail-js and logtail-python release workflows. main requires pull requests, so the version bump stays a pull request and the workflow tests, builds, pushes the gem, tags and creates the GitHub release. Every push touching the workflow file is a dry run. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/release.yml | 180 ++++++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..173ec2e --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,180 @@ +# Publishes the gem to RubyGems.org from GitHub Actions, the way logtail-js and logtail-python +# release their packages. The publish job runs in the "rubygems" environment, so its required +# reviewers are the release gate. +# +# Authentication: the repository secret RUBYGEMS_CREDENTIALS holds the contents of a gem +# credentials file for the shared rubygems.org account, i.e. ":rubygems_api_key: rubygems_…", +# the line the manual process kept in ~/.local/share/gem/credentials. +# +# One-time setup on GitHub: create the "rubygems" environment with required reviewers and +# deployment branches limited to main. A job that references a missing environment creates it +# WITHOUT protection. +# +# Release: merge a pull request that bumps the version in lib/*/version.rb (main requires pull +# requests, so unlike logtail-js and logtail-python the workflow does not commit the bump itself), +# then Actions → Release → Run workflow from main and approve the environment prompt. The workflow +# runs the tests, builds the gem, pushes it to RubyGems.org, tags the commit vX.Y.Z and creates a +# GitHub release with auto-generated notes for the tag; edit the notes afterwards if needed. +# +# Retry: if a release failed halfway, run it again from the same commit. It skips the push when +# the version is on RubyGems.org already and creates the tag and the GitHub release if they are +# still missing. +# +# Dry run: runs the tests, builds the gem and checks the credentials secret, publishes nothing. +# Every push that touches this file is a dry run, so a change to the workflow proves itself on +# its pull request before it reaches main. A dry run can also be dispatched from any branch. +name: Release + +on: + workflow_dispatch: + inputs: + dry_run: + description: "Dry run: test, build and check the credentials, publish nothing" + type: boolean + default: false + push: + paths: + - .github/workflows/release.yml + +permissions: + contents: read + +concurrency: + group: release + cancel-in-progress: false + +env: + DRY_RUN: ${{ github.event_name == 'push' || inputs.dry_run }} + +jobs: + verify: + name: Test + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v7 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: "3" + bundler-cache: true + + - name: Run tests + run: bundle exec rspec + + build: + name: Build + needs: verify + runs-on: ubuntu-24.04 + outputs: + version: ${{ steps.version.outputs.version }} + gem: ${{ steps.build.outputs.gem }} + + steps: + - name: Releases run from main only + if: ${{ env.DRY_RUN != 'true' && github.ref != 'refs/heads/main' }} + run: | + echo "::error::Dispatch the release from main, not from $GITHUB_REF_NAME." + exit 1 + + - uses: actions/checkout@v7 + with: + fetch-depth: 0 # the version check looks at the tags + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: "3" + + - name: Check the version + id: version + run: | + version=$(ruby -e 'puts File.read(Dir["lib/**/version.rb"].fetch(0))[/VERSION = "(\d+\.\d+\.\d+)"/, 1] || abort("no VERSION constant in lib/*/version.rb")') + released_from=$(git rev-list -n 1 "v$version" 2> /dev/null || true) + if [ -n "$released_from" ] && [ "$released_from" != "$GITHUB_SHA" ]; then + if [ "$DRY_RUN" = true ]; then + echo "::warning::v$version is released from $released_from already, a release from this commit needs a version bump in lib/*/version.rb first." + else + echo "::error::v$version is released from $released_from already. Bump the version in lib/*/version.rb in a pull request first." + exit 1 + fi + fi + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "Version $version" + + - name: Build the gem + id: build + env: + VERSION: ${{ steps.version.outputs.version }} + run: | + gem build *.gemspec + gem=$(ls *.gem) + case "$gem" in + *-"$VERSION".gem) ;; + *) echo "::error::Built $gem, expected version $VERSION."; exit 1 ;; + esac + echo "gem=$gem" >> "$GITHUB_OUTPUT" + + - name: Dry run + if: ${{ env.DRY_RUN == 'true' }} + env: + RUBYGEMS_CREDENTIALS: ${{ secrets.RUBYGEMS_CREDENTIALS }} + run: | + if ! grep -q '^:rubygems_api_key: [^ ]' <<< "$RUBYGEMS_CREDENTIALS"; then + echo "::error::The RUBYGEMS_CREDENTIALS secret must hold the contents of a gem credentials file: ':rubygems_api_key: rubygems_…'" + exit 1 + fi + echo "RUBYGEMS_CREDENTIALS looks like a gem credentials file. A release pushes with it, nothing more to verify without pushing." + + - uses: actions/upload-artifact@v7 + with: + name: gem + path: "*.gem" + if-no-files-found: error + + release: + name: Publish + needs: build + if: ${{ github.event_name == 'workflow_dispatch' && !inputs.dry_run }} + runs-on: ubuntu-24.04 + environment: rubygems + permissions: + contents: write # creates the tag and the GitHub release + env: + VERSION: ${{ needs.build.outputs.version }} + GEM: ${{ needs.build.outputs.gem }} + + steps: + - uses: actions/download-artifact@v8 + with: + name: gem + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: "3" + + - name: Push to RubyGems.org + env: + RUBYGEMS_CREDENTIALS: ${{ secrets.RUBYGEMS_CREDENTIALS }} + run: | + name="${GEM%-$VERSION.gem}" + if gem specification --remote "$name" --version "$VERSION" > /dev/null 2>&1; then + echo "$name $VERSION is on rubygems.org already, finishing the release." + else + mkdir -p ~/.gem + (umask 077 && printf '%s\n' "$RUBYGEMS_CREDENTIALS" > ~/.gem/credentials) + gem push "$GEM" + fi + + - name: Tag and create the GitHub release + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: | + if gh release view "v$VERSION" > /dev/null 2>&1; then + echo "Release v$VERSION already exists." + else + # Creates the v$VERSION tag on this commit when it is still missing. + gh release create "v$VERSION" --target "$GITHUB_SHA" --generate-notes + fi From 48bba60df88f401177b0095ba01cfd7816d17698 Mon Sep 17 00:00:00 2001 From: Petr Heinz Date: Fri, 25 Sep 2026 17:06:05 +0200 Subject: [PATCH 2/6] Describe the rubygems environment as configured The environment restricts deployments to main and has no required reviewers, so anyone who can dispatch the workflow can release. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/release.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 173ec2e..027c080 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,20 +1,20 @@ # Publishes the gem to RubyGems.org from GitHub Actions, the way logtail-js and logtail-python -# release their packages. The publish job runs in the "rubygems" environment, so its required -# reviewers are the release gate. +# release their packages. The publish job runs in the "rubygems" environment, which only deploys +# from main; anyone who can dispatch the workflow can release. # # Authentication: the repository secret RUBYGEMS_CREDENTIALS holds the contents of a gem # credentials file for the shared rubygems.org account, i.e. ":rubygems_api_key: rubygems_…", # the line the manual process kept in ~/.local/share/gem/credentials. # -# One-time setup on GitHub: create the "rubygems" environment with required reviewers and -# deployment branches limited to main. A job that references a missing environment creates it -# WITHOUT protection. +# The "rubygems" environment lives in the repository settings with deployment branches limited to +# main and no required reviewers. Add reviewers there if releases should need an approval; a job +# that references a missing environment would create it WITHOUT protection. # # Release: merge a pull request that bumps the version in lib/*/version.rb (main requires pull # requests, so unlike logtail-js and logtail-python the workflow does not commit the bump itself), -# then Actions → Release → Run workflow from main and approve the environment prompt. The workflow -# runs the tests, builds the gem, pushes it to RubyGems.org, tags the commit vX.Y.Z and creates a -# GitHub release with auto-generated notes for the tag; edit the notes afterwards if needed. +# then Actions → Release → Run workflow from main. The workflow runs the tests, builds the gem, +# pushes it to RubyGems.org, tags the commit vX.Y.Z and creates a GitHub release with +# auto-generated notes for the tag; edit the notes afterwards if needed. # # Retry: if a release failed halfway, run it again from the same commit. It skips the push when # the version is on RubyGems.org already and creates the tag and the GitHub release if they are From 67ae22af954956d7bcb75a9fec03457a57886a1b Mon Sep 17 00:00:00 2001 From: Petr Heinz Date: Wed, 30 Sep 2026 17:55:15 +0200 Subject: [PATCH 3/6] Bump, commit and tag the version in the workflow The GitHub Actions app may bypass the pull request requirement of main now, so the workflow takes patch, minor and retry inputs like the logtail-js and logtail-python ones and pushes the version commit and the tag itself. Dry runs bump in place without committing. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/release.yml | 99 +++++++++++++++++++++++------------ 1 file changed, 65 insertions(+), 34 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 027c080..79b7118 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,30 +6,38 @@ # credentials file for the shared rubygems.org account, i.e. ":rubygems_api_key: rubygems_…", # the line the manual process kept in ~/.local/share/gem/credentials. # -# The "rubygems" environment lives in the repository settings with deployment branches limited to -# main and no required reviewers. Add reviewers there if releases should need an approval; a job -# that references a missing environment would create it WITHOUT protection. +# Repository settings this relies on: the "rubygems" environment with deployment branches limited +# to main and no required reviewers (add reviewers there if releases should need an approval; a +# job that references a missing environment would create it WITHOUT protection), and the GitHub +# Actions app among the actors allowed to bypass the pull request requirement of main, which the +# version commit needs. # -# Release: merge a pull request that bumps the version in lib/*/version.rb (main requires pull -# requests, so unlike logtail-js and logtail-python the workflow does not commit the bump itself), -# then Actions → Release → Run workflow from main. The workflow runs the tests, builds the gem, -# pushes it to RubyGems.org, tags the commit vX.Y.Z and creates a GitHub release with -# auto-generated notes for the tag; edit the notes afterwards if needed. +# Release: Actions → Release → Run workflow from main, pick patch or minor. The workflow runs the +# tests, bumps the version in lib/*/version.rb, builds the gem, commits "vX.Y.Z", tags it, pushes +# both, pushes the gem to RubyGems.org and creates a GitHub release with auto-generated notes for +# the tag; edit the notes afterwards if needed. # -# Retry: if a release failed halfway, run it again from the same commit. It skips the push when -# the version is on RubyGems.org already and creates the tag and the GitHub release if they are -# still missing. +# Retry: if a release failed after the version commit was pushed, run "retry" right away from +# main. It bumps nothing, rebuilds the tagged commit, pushes the gem if RubyGems.org is still +# missing it and creates the GitHub release if it is still missing. Running patch or minor again +# would release the next version instead. # -# Dry run: runs the tests, builds the gem and checks the credentials secret, publishes nothing. -# Every push that touches this file is a dry run, so a change to the workflow proves itself on -# its pull request before it reaches main. A dry run can also be dispatched from any branch. +# Dry run: bumps in place without committing, builds the gem and checks the credentials secret, +# publishes nothing. Every push that touches this file is a dry run, so a change to the workflow +# proves itself on its pull request before it reaches main. A dry run can also be dispatched from +# any branch. name: Release on: workflow_dispatch: inputs: + release: + description: "patch or minor: bump, tag and publish. retry: finish a release that failed halfway." + type: choice + options: [patch, minor, retry] + required: true dry_run: - description: "Dry run: test, build and check the credentials, publish nothing" + description: "Dry run: bump and build, check the credentials, publish nothing" type: boolean default: false push: @@ -45,6 +53,7 @@ concurrency: env: DRY_RUN: ${{ github.event_name == 'push' || inputs.dry_run }} + RELEASE: ${{ inputs.release || 'patch' }} jobs: verify: @@ -63,9 +72,11 @@ jobs: run: bundle exec rspec build: - name: Build + name: Bump and build needs: verify runs-on: ubuntu-24.04 + permissions: + contents: write # pushes the version commit and the tag outputs: version: ${{ steps.version.outputs.version }} gem: ${{ steps.build.outputs.gem }} @@ -79,28 +90,36 @@ jobs: - uses: actions/checkout@v7 with: - fetch-depth: 0 # the version check looks at the tags + fetch-depth: 0 # the version check looks at the tags on HEAD - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: "3" - - name: Check the version + - name: Bump version id: version run: | - version=$(ruby -e 'puts File.read(Dir["lib/**/version.rb"].fetch(0))[/VERSION = "(\d+\.\d+\.\d+)"/, 1] || abort("no VERSION constant in lib/*/version.rb")') - released_from=$(git rev-list -n 1 "v$version" 2> /dev/null || true) - if [ -n "$released_from" ] && [ "$released_from" != "$GITHUB_SHA" ]; then - if [ "$DRY_RUN" = true ]; then - echo "::warning::v$version is released from $released_from already, a release from this commit needs a version bump in lib/*/version.rb first." - else - echo "::error::v$version is released from $released_from already. Bump the version in lib/*/version.rb in a pull request first." - exit 1 - fi - fi - echo "version=$version" >> "$GITHUB_OUTPUT" - echo "Version $version" + ruby - <<'EOF' + release, dry_run = ENV.fetch("RELEASE"), ENV["DRY_RUN"] == "true" + path = Dir["lib/**/version.rb"].fetch(0) + source = File.read(path) + current = source[/VERSION = "(\d+\.\d+\.\d+)"/, 1] || abort("no VERSION constant in #{path}") + tagged = `git tag --points-at HEAD`.split.include?("v#{current}") + + if release == "retry" + abort("retry only finishes a release whose version commit v#{current} is HEAD") unless tagged + version = current + else + abort("HEAD is already released as v#{current}, there is nothing new to release") if tagged && !dry_run + major, minor, patch = current.split(".").map(&:to_i) + version = release == "minor" ? "#{major}.#{minor + 1}.0" : "#{major}.#{minor}.#{patch + 1}" + File.write(path, source.sub(%("#{current}"), %("#{version}"))) + end + + puts "#{current} -> #{version}" + File.open(ENV.fetch("GITHUB_OUTPUT"), "a") { |output| output.puts "version=#{version}" } + EOF - name: Build the gem id: build @@ -120,12 +139,24 @@ jobs: env: RUBYGEMS_CREDENTIALS: ${{ secrets.RUBYGEMS_CREDENTIALS }} run: | + git diff --stat if ! grep -q '^:rubygems_api_key: [^ ]' <<< "$RUBYGEMS_CREDENTIALS"; then echo "::error::The RUBYGEMS_CREDENTIALS secret must hold the contents of a gem credentials file: ':rubygems_api_key: rubygems_…'" exit 1 fi echo "RUBYGEMS_CREDENTIALS looks like a gem credentials file. A release pushes with it, nothing more to verify without pushing." + - name: Commit and tag + if: ${{ env.DRY_RUN != 'true' && env.RELEASE != 'retry' }} + env: + VERSION: ${{ steps.version.outputs.version }} + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git commit -am "v$VERSION" + git tag -a "v$VERSION" -m "v$VERSION" + git push origin main "v$VERSION" + - uses: actions/upload-artifact@v7 with: name: gem @@ -139,7 +170,7 @@ jobs: runs-on: ubuntu-24.04 environment: rubygems permissions: - contents: write # creates the tag and the GitHub release + contents: write # creates the GitHub release env: VERSION: ${{ needs.build.outputs.version }} GEM: ${{ needs.build.outputs.gem }} @@ -167,7 +198,7 @@ jobs: gem push "$GEM" fi - - name: Tag and create the GitHub release + - name: Create GitHub release env: GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} @@ -175,6 +206,6 @@ jobs: if gh release view "v$VERSION" > /dev/null 2>&1; then echo "Release v$VERSION already exists." else - # Creates the v$VERSION tag on this commit when it is still missing. - gh release create "v$VERSION" --target "$GITHUB_SHA" --generate-notes + # --verify-tag: only ever attach to the tag the build job pushed, never create one here. + gh release create "v$VERSION" --verify-tag --generate-notes fi From 7922a87db6712ebd94820eb7810aa30bd65d6ea4 Mon Sep 17 00:00:00 2001 From: Petr Heinz Date: Wed, 30 Sep 2026 18:14:34 +0200 Subject: [PATCH 4/6] Publish with trusted publishing instead of an API key The shared rubygems.org account requires a one-time code for API pushes, which no workflow can supply: the first logtail release stopped at gem push. The publish job now exchanges its OIDC token with RubyGems.org for a short-lived key, like the npm packages do. Dry runs perform the exchange too, so a workflow change proves the trusted publisher setup before it is merged. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/release.yml | 55 +++++++++++++++++------------------ 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 79b7118..4d6512d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,10 +1,11 @@ -# Publishes the gem to RubyGems.org from GitHub Actions, the way logtail-js and logtail-python -# release their packages. The publish job runs in the "rubygems" environment, which only deploys -# from main; anyone who can dispatch the workflow can release. +# Publishes the gem to RubyGems.org from GitHub Actions with trusted publishing (OIDC), the way +# logtail-js releases the @logtail/* packages to npm. No RubyGems.org API key exists anywhere: +# the gem on rubygems.org trusts exactly this workflow file, run from this repository in the +# "rubygems" environment, and the shared account's multi-factor authentication stays as it is. # -# Authentication: the repository secret RUBYGEMS_CREDENTIALS holds the contents of a gem -# credentials file for the shared rubygems.org account, i.e. ":rubygems_api_key: rubygems_…", -# the line the manual process kept in ~/.local/share/gem/credentials. +# One-time setup on rubygems.org, signed in as the gem's owner: the gem's page → Trusted publishers +# → GitHub Actions, with the repository owner "logtail", this repository's name, the workflow +# "release.yml" and the environment "rubygems". # # Repository settings this relies on: the "rubygems" environment with deployment branches limited # to main and no required reviewers (add reviewers there if releases should need an approval; a @@ -22,10 +23,11 @@ # missing it and creates the GitHub release if it is still missing. Running patch or minor again # would release the next version instead. # -# Dry run: bumps in place without committing, builds the gem and checks the credentials secret, -# publishes nothing. Every push that touches this file is a dry run, so a change to the workflow -# proves itself on its pull request before it reaches main. A dry run can also be dispatched from -# any branch. +# Dry run: bumps in place without committing, builds the gem and exchanges the workflow's OIDC +# token with RubyGems.org, which proves that the trusted publisher matches this workflow. Nothing +# is pushed. Every push that touches this file is a dry run, so a change to the workflow proves +# itself on its pull request before it reaches main. A dry run can also be dispatched from any +# branch. name: Release on: @@ -37,7 +39,7 @@ on: options: [patch, minor, retry] required: true dry_run: - description: "Dry run: bump and build, check the credentials, publish nothing" + description: "Dry run: bump and build, verify RubyGems.org accepts this workflow, publish nothing" type: boolean default: false push: @@ -126,6 +128,7 @@ jobs: env: VERSION: ${{ steps.version.outputs.version }} run: | + git diff --stat gem build *.gemspec gem=$(ls *.gem) case "$gem" in @@ -134,18 +137,6 @@ jobs: esac echo "gem=$gem" >> "$GITHUB_OUTPUT" - - name: Dry run - if: ${{ env.DRY_RUN == 'true' }} - env: - RUBYGEMS_CREDENTIALS: ${{ secrets.RUBYGEMS_CREDENTIALS }} - run: | - git diff --stat - if ! grep -q '^:rubygems_api_key: [^ ]' <<< "$RUBYGEMS_CREDENTIALS"; then - echo "::error::The RUBYGEMS_CREDENTIALS secret must hold the contents of a gem credentials file: ':rubygems_api_key: rubygems_…'" - exit 1 - fi - echo "RUBYGEMS_CREDENTIALS looks like a gem credentials file. A release pushes with it, nothing more to verify without pushing." - - name: Commit and tag if: ${{ env.DRY_RUN != 'true' && env.RELEASE != 'retry' }} env: @@ -166,11 +157,11 @@ jobs: release: name: Publish needs: build - if: ${{ github.event_name == 'workflow_dispatch' && !inputs.dry_run }} runs-on: ubuntu-24.04 environment: rubygems permissions: contents: write # creates the GitHub release + id-token: write # OIDC token exchange with RubyGems.org env: VERSION: ${{ needs.build.outputs.version }} GEM: ${{ needs.build.outputs.gem }} @@ -185,20 +176,28 @@ jobs: with: ruby-version: "3" + # Exchanges the job's OIDC token for a short-lived RubyGems.org API key and hands it to + # `gem push`. Fails when no trusted publisher on rubygems.org matches this workflow. + - uses: rubygems/configure-rubygems-credentials@v2.1.0 + + - name: Dry run + if: ${{ env.DRY_RUN == 'true' }} + run: | + ls -l "$GEM" + echo "RubyGems.org accepted the OIDC token: the trusted publisher matches this workflow. Nothing is pushed." + - name: Push to RubyGems.org - env: - RUBYGEMS_CREDENTIALS: ${{ secrets.RUBYGEMS_CREDENTIALS }} + if: ${{ env.DRY_RUN != 'true' }} run: | name="${GEM%-$VERSION.gem}" if gem specification --remote "$name" --version "$VERSION" > /dev/null 2>&1; then echo "$name $VERSION is on rubygems.org already, finishing the release." else - mkdir -p ~/.gem - (umask 077 && printf '%s\n' "$RUBYGEMS_CREDENTIALS" > ~/.gem/credentials) gem push "$GEM" fi - name: Create GitHub release + if: ${{ env.DRY_RUN != 'true' }} env: GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} From 1fd463533a23994348aa87f54599929da4549e16 Mon Sep 17 00:00:00 2001 From: Petr Heinz Date: Wed, 30 Sep 2026 18:18:53 +0200 Subject: [PATCH 5/6] Exchange the OIDC token on main only The rubygems environment admits main only, so a publish job started from another branch is rejected before its first step. Dry runs from other branches stop after the build; the push that merges a workflow change into main performs the token exchange. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/release.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4d6512d..137ef3e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,11 +23,12 @@ # missing it and creates the GitHub release if it is still missing. Running patch or minor again # would release the next version instead. # -# Dry run: bumps in place without committing, builds the gem and exchanges the workflow's OIDC -# token with RubyGems.org, which proves that the trusted publisher matches this workflow. Nothing -# is pushed. Every push that touches this file is a dry run, so a change to the workflow proves -# itself on its pull request before it reaches main. A dry run can also be dispatched from any -# branch. +# Dry run: bumps in place without committing, builds the gem and, on main, exchanges the +# workflow's OIDC token with RubyGems.org, which proves that the trusted publisher matches this +# workflow. Nothing is pushed. Every push that touches this file is a dry run, so a change to the +# workflow proves itself on its pull request before it reaches main, and the push that merges it +# performs the token exchange as well, because the "rubygems" environment only admits main. A dry +# run can also be dispatched from any branch. name: Release on: @@ -157,6 +158,8 @@ jobs: release: name: Publish needs: build + # The rubygems environment only admits main, so dry runs from other branches stop after the build + if: ${{ github.ref == 'refs/heads/main' }} runs-on: ubuntu-24.04 environment: rubygems permissions: From c5665c386f6b3f8d2304bcb60a5abd2d9bd3d264 Mon Sep 17 00:00:00 2001 From: Petr Heinz Date: Wed, 30 Sep 2026 18:28:40 +0200 Subject: [PATCH 6/6] Retry builds the tagged commit, wherever main is retry insisted that the version commit is HEAD, but main moves on as soon as anything else is merged, as happened between the failed logtail 0.1.18 release and its retry. The retry now checks out the tag of the version in lib/*/version.rb and rebuilds that commit. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/release.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 137ef3e..39cea30 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,10 +18,10 @@ # both, pushes the gem to RubyGems.org and creates a GitHub release with auto-generated notes for # the tag; edit the notes afterwards if needed. # -# Retry: if a release failed after the version commit was pushed, run "retry" right away from -# main. It bumps nothing, rebuilds the tagged commit, pushes the gem if RubyGems.org is still -# missing it and creates the GitHub release if it is still missing. Running patch or minor again -# would release the next version instead. +# Retry: if a release failed after the version commit was pushed, run "retry" from main. It bumps +# nothing, checks out the tag of the version in lib/*/version.rb and rebuilds that commit, pushes +# the gem if RubyGems.org is still missing it and creates the GitHub release if it is still +# missing. Running patch or minor again would release the next version instead. # # Dry run: bumps in place without committing, builds the gem and, on main, exchanges the # workflow's OIDC token with RubyGems.org, which proves that the trusted publisher matches this @@ -111,7 +111,9 @@ jobs: tagged = `git tag --points-at HEAD`.split.include?("v#{current}") if release == "retry" - abort("retry only finishes a release whose version commit v#{current} is HEAD") unless tagged + # main may have moved on since the version commit, so build what the tag points at + abort("retry only finishes a release whose version v#{current} is tagged") unless system("git", "rev-parse", "-q", "--verify", "refs/tags/v#{current}", out: File::NULL) + system("git", "checkout", "-q", "v#{current}") || abort("could not check out v#{current}") version = current else abort("HEAD is already released as v#{current}, there is nothing new to release") if tagged && !dry_run