From 1b863042d37595dcc65f51fc0a308d9e37b35b79 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Wed, 17 Jun 2026 12:18:23 +0200 Subject: [PATCH 1/2] Publish via trusted publishing without rubygems/release-gem Replace the rubygems/release-gem action in the publish job with a minimal flow that we fully control: - configure-rubygems-credentials performs the OIDC token exchange for trusted publishing and exposes the short-lived key via GEM_HOST_API_KEY - a vendored patch makes `gem push` sign each gem with sigstore-cli and attach the attestation, keeping provenance attestations - rubygems-await waits for the pushed gems to become available on RubyGems.org before the job completes - sigstore-cli and rubygems-await are installed explicitly and pinned, rather than being resolved implicitly at run time This drops release-gem's git identity/tagging, credential cache, tag fetch and rake release wrapper -- none of which this repo needs (it does not tag) -- and removes the octo-sts token, which only existed to authenticate those git operations. --- .github/scripts/rubygems-attestation-patch.rb | 45 +++++++++++++++++++ .github/workflows/build.yml | 28 +++++++----- 2 files changed, 62 insertions(+), 11 deletions(-) create mode 100644 .github/scripts/rubygems-attestation-patch.rb diff --git a/.github/scripts/rubygems-attestation-patch.rb b/.github/scripts/rubygems-attestation-patch.rb new file mode 100644 index 0000000..ccd6d7d --- /dev/null +++ b/.github/scripts/rubygems-attestation-patch.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +# Attach a Sigstore attestation to `gem push` uploads to rubygems.org. +# +# Vendored from rubygems/release-gem@6317d8d1f7e28c24d28f6eff169ea854948bd9f7 +# (rubygems-attestation-patch.rb, MIT). Intentional differences from upstream: +# * invoke the explicitly-installed `sigstore-cli` binary instead of +# `gem exec sigstore-cli:`, so the signing tool is a declared, +# pinned dependency rather than an implicit install at push time; +# * no silent "retry without attestation" fallback -- a signing failure must +# fail the push loudly. + +return if RUBY_ENGINE == "jruby" +return unless defined?(Gem) + +require "rubygems/commands/push_command" + +push_command_with_attestation = Module.new do + def send_push_request(name, args) + return super if Array(options[:attestations]).any? || @host != "https://rubygems.org" + + attestation = attest!(name) + rubygems_api_request(*args, scope: get_push_scope) do |request| + request.set_form( + [ + ["gem", Gem.read_binary(name), {filename: name, content_type: "application/octet-stream"}], + ["attestations", "[#{Gem.read_binary(attestation)}]", {content_type: "application/json"}] + ], + "multipart/form-data" + ) + request.add_field "Authorization", api_key + end + end + + def attest!(name) + require "open3" + bundle = "#{name}.sigstore.json" + output, status = Open3.capture2e("sigstore-cli", "sign", name, "--bundle", bundle) + raise Gem::Exception, "Failed to sign #{name} with sigstore-cli:\n\n#{output}" unless status.success? + + bundle + end +end + +Gem::Commands::PushCommand.prepend(push_command_with_attestation) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3bc9e1d..9e4eaaa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -303,7 +303,7 @@ jobs: environment: rubygems.org concurrency: publish permissions: - id-token: write + id-token: write # trusted-publishing OIDC exchange AND sigstore keyless signing steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: @@ -312,19 +312,25 @@ jobs: uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # 1.310.0 with: ruby-version: "4.0" - bundler-cache: true - - run: bundle install + - name: Install pinned publishing tools + run: | + gem install sigstore-cli -v 0.2.3 + gem install rubygems-await -v 0.5.4 - name: Download pre-built gems into pkg/ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: gem-* merge-multiple: true path: pkg - - uses: DataDog/dd-octo-sts-action@96a25462dbcb10ebf0bfd6e2ccc917d2ab235b9a # v1.0.4 - id: octo-sts - with: - scope: DataDog/libdatadog-rb - policy: self.publish - - uses: rubygems/release-gem@6317d8d1f7e28c24d28f6eff169ea854948bd9f7 # v1.2.0 - with: - token: ${{ steps.octo-sts.outputs.token }} + # Exchange the GitHub Actions OIDC token for a short-lived RubyGems API + # key (trusted publishing) and expose it via GEM_HOST_API_KEY. + - uses: rubygems/configure-rubygems-credentials@bc6dd217f8a4f919d6835fcfefd470ef821f5c44 # v1.0.0 + - name: Push gems with attestation + # The patch makes `gem push` sign each gem with sigstore-cli and attach + # the attestation to the upload. + env: + RUBYOPT: "-r${{ github.workspace }}/.github/scripts/rubygems-attestation-patch.rb" + run: | + for gem in pkg/*.gem; do gem push "$gem"; done + - name: Wait for the gems to be available on RubyGems.org + run: rubygems-await pkg/*.gem From 8a8514686abb83bf0ab5802694f7ea452be63091 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Wed, 17 Jun 2026 12:50:15 +0200 Subject: [PATCH 2/2] Run the publish gem tooling inside a container Run the gem install, push and await steps inside a pinned, runtime-only ghcr.io/datadog/images-rb/engines/ruby:4.0-musl image, spawned with `docker run --detach` and driven with `docker exec`, mirroring the build and validate jobs. This pins the Ruby/RubyGems used for publishing instead of relying on the host toolchain. The workspace is mounted at the same path so the vendored attestation patch and the pkg/ gems resolve identically inside the container. The trusted-publishing credential exchange stays on the host (it is a JavaScript action); its GEM_HOST_API_KEY, along with the OIDC request variables sigstore-cli needs, are passed into the container. None of the publishing dependencies have native extensions, so the runtime-only image (no compiler) is sufficient. --- .github/workflows/build.yml | 47 ++++++++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9e4eaaa..958c574 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -308,14 +308,6 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - name: Set up Ruby - uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # 1.310.0 - with: - ruby-version: "4.0" - - name: Install pinned publishing tools - run: | - gem install sigstore-cli -v 0.2.3 - gem install rubygems-await -v 0.5.4 - name: Download pre-built gems into pkg/ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -323,14 +315,41 @@ jobs: merge-multiple: true path: pkg # Exchange the GitHub Actions OIDC token for a short-lived RubyGems API - # key (trusted publishing) and expose it via GEM_HOST_API_KEY. + # key (trusted publishing) and expose it via GEM_HOST_API_KEY. This runs + # on the host; the key is passed into the container below. - uses: rubygems/configure-rubygems-credentials@bc6dd217f8a4f919d6835fcfefd470ef821f5c44 # v1.0.0 + # Run the gem tooling inside a pinned, runtime-only image rather than on + # the host. The workspace is mounted at the same path so the vendored + # patch and the pkg/ gems resolve identically inside the container. + - name: Start container + run: docker run --rm --detach --name publish --volume "${PWD}:${PWD}" -w "${PWD}" ghcr.io/datadog/images-rb/engines/ruby:4.0-musl sleep 86400 + - name: Install pinned publishing tools + run: | + docker exec publish sh -c ' + gem install sigstore-cli -v 0.2.3 + gem install rubygems-await -v 0.5.4 + ' - name: Push gems with attestation # The patch makes `gem push` sign each gem with sigstore-cli and attach - # the attestation to the upload. - env: - RUBYOPT: "-r${{ github.workspace }}/.github/scripts/rubygems-attestation-patch.rb" + # the attestation to the upload. GEM_HOST_API_KEY authorizes the push; + # the ACTIONS_ID_TOKEN_REQUEST_* vars let sigstore-cli mint its OIDC + # signing identity from inside the container. run: | - for gem in pkg/*.gem; do gem push "$gem"; done + docker exec \ + --env GEM_HOST_API_KEY \ + --env ACTIONS_ID_TOKEN_REQUEST_URL \ + --env ACTIONS_ID_TOKEN_REQUEST_TOKEN \ + --env RUBYOPT="-r${PWD}/.github/scripts/rubygems-attestation-patch.rb" \ + publish sh -c ' + export PATH="$(ruby -e "print Gem.bindir"):$PATH" + for gem in pkg/*.gem; do gem push "$gem"; done + ' - name: Wait for the gems to be available on RubyGems.org - run: rubygems-await pkg/*.gem + run: | + docker exec publish sh -c ' + export PATH="$(ruby -e "print Gem.bindir"):$PATH" + rubygems-await pkg/*.gem + ' + - name: Stop container + if: always() + run: docker stop publish || true