Skip to content
134 changes: 125 additions & 9 deletions .github/workflows/push_gem.yml
Original file line number Diff line number Diff line change
@@ -1,41 +1,157 @@
# Publishes a new version to RubyGems.org with trusted publishing (OIDC) once
# CI passes on main, if the gemspec's version isn't on RubyGems yet, then creates
# its GitHub release. Bumping the version in singed.gemspec is the release.
# Dispatch it from main to retry a failed run: it skips whatever's already done.
# A dispatch doesn't wait for CI, so only run it when main is green; from any
# other branch it does nothing.
#
# The filename and the rubygems.org environment are what the trusted publisher
# on RubyGems.org is registered against, so don't rename either.
name: Push Gem

on:
# zizmor: ignore[dangerous-triggers] only a push to this repo's main gets past the check job, and the release job checks out main itself
workflow_run:
workflows: [CI Test]
types: [completed]
branches: [main]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: push-gem
cancel-in-progress: false

jobs:
push:
if: github.repository == 'rubyatscale/singed'
check:
name: Check whether a release is needed
# A fork's pull request can come from a branch named main, so require a push to this repo.
if: >-
github.repository == 'rubyatscale/singed' &&
(github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' ||
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.head_repository.full_name == github.repository)
runs-on: ubuntu-latest
outputs:
release: ${{ steps.version.outputs.release }}
github_release: ${{ steps.version.outputs.github_release }}
version: ${{ steps.version.outputs.version }}
sha: ${{ steps.version.outputs.sha }}
steps:
# Reads the gemspec through the API instead of checking it out and evaluating it, so no
# code from the triggering commit runs in this privileged context.
- name: Check whether this version is on RubyGems and has a GitHub release
id: version
env:
GH_TOKEN: ${{ github.token }}
SHA: ${{ github.event.workflow_run.head_sha || github.sha }}
run: |
gemspec=$(gh api "repos/$GITHUB_REPOSITORY/contents/singed.gemspec?ref=$SHA" -H 'Accept: application/vnd.github.raw')
version=$(sed -n 's/^[[:space:]]*spec\.version[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' <<<"$gemspec" | head -1)
if ! [[ "$version" =~ ^[0-9]+(\.[0-9A-Za-z]+)*$ ]]; then
echo "::error::Couldn't read a version from the spec.version line in singed.gemspec at $SHA"
exit 1
fi
status=$(curl -sS --retry 3 -o /dev/null -w '%{http_code}' "https://rubygems.org/api/v2/rubygems/singed/versions/$version.json")
case "$status" in
200) release=false; echo "singed $version is already on RubyGems." ;;
404) release=true; echo "singed $version isn't on RubyGems yet; releasing it." ;;
*) echo "::error::RubyGems returned HTTP $status for singed $version"; exit 1 ;;
esac
if lookup=$(gh api "repos/$GITHUB_REPOSITORY/releases/tags/v$version" 2>&1); then
github_release=false
elif grep -q 'HTTP 404' <<<"$lookup"; then
github_release=true
echo "v$version has no GitHub release yet."
else
echo "::error::Couldn't check for a v$version GitHub release: $lookup"
exit 1
fi
{
echo "release=$release"
echo "github_release=$github_release"
echo "version=$version"
echo "sha=$SHA"
} >> "$GITHUB_OUTPUT"

release:
name: Release
needs: check
if: needs.check.outputs.release == 'true'
runs-on: ubuntu-latest

environment:
name: rubygems.org
url: https://rubygems.org/gems/singed

permissions:
contents: write
id-token: write
contents: write # push the version tag
id-token: write # trusted publishing

steps:
# Set up
- name: Harden Runner
uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
with:
egress-policy: audit

# `rake release` pushes the current branch along with the tag, so this has to be a
# branch checkout rather than a detached HEAD at the tested commit. The push needs no
# persisted credentials: release-gem puts the token in git's credential cache for it.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: main
persist-credentials: false
- name: Confirm main is still the commit CI tested
env:
SHA: ${{ needs.check.outputs.sha }}
run: |
head=$(git rev-parse HEAD)
if [ "$head" != "$SHA" ]; then
echo "::error::main moved from $SHA to $head after CI passed. If CI passes on $head, its run will release this version; otherwise dispatch this workflow once main is green."
exit 1
fi
# No bundler cache here: a job that publishes the gem shouldn't restore one.
- name: Set up Ruby
uses: ruby/setup-ruby@e8944e80fb94b20106697132f8c20c665fab29e9 # v1.325.0
with:
ruby-version: ruby
# Not bundler-cache: a release shouldn't restore a cache other workflows can write.
- name: Install gems
run: bundle install
- run: bundle install

# Release
- uses: rubygems/release-gem@7f9650160c1a4e7989fdc9855807bdbd421d8b6b # v1.4.1

# Its own job, so a retry can still create the release after the gem has shipped.
github_release:
name: Create GitHub release
needs: [check, release]
if: >-
always() && needs.check.result == 'success' && needs.check.outputs.github_release == 'true' &&
(needs.release.result == 'success' || needs.release.result == 'skipped')
runs-on: ubuntu-latest
permissions:
contents: write # create the GitHub release
steps:
- name: Create GitHub release
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ needs.check.outputs.version }}
# RubyGems treats any version with a letter in it as a prerelease.
run: |
prerelease=()
if [[ "$VERSION" == *[A-Za-z]* ]]; then prerelease=(--prerelease); fi
gh release create "v$VERSION" --repo "$GITHUB_REPOSITORY" --verify-tag --generate-notes "${prerelease[@]}"

notify_on_failure:
name: Notify on failure
needs: [check, release, github_release]
if: failure()
runs-on: ubuntu-latest
steps:
- uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0
with:
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
text: "${{ github.repository }} Push Gem workflow FAILED\n${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
Loading