Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ Review the entire release diff. Native GitHub rules require the configured revie

Merging a version increase publishes the actual merged commit through `release-publish.yaml`. There is no second release approval. The public `release.cert` stays in Git; the optional private key belongs in the Actions secret `GEM_SIGNING_KEY`, supplied by the `rubygems` environment or an organization secret. RubyGems authentication uses Trusted Publishing.

If a publishing job fails, use **Re-run all jobs** on that same workflow run, or `bundle exec bake gem:github:release:resume run=RUN_ID`. The original artifact is downloaded before another upload is attempted. Do not create a replacement version bump or rebuild an already-published version. Conflicting bytes or tags require investigation; never automatically yank a version or move a tag.
Before uploading to RubyGems, the publisher preserves the verified gem, receipt and both attestation bundles as assets of a draft GitHub release. The draft becomes public after registry verification and tag creation. Registry propagation is retried for up to one minute.

If a publishing job fails, use **Re-run all jobs** on that same workflow run, or `bundle exec bake gem:github:release:resume run=RUN_ID`. The original artifact is restored from the workflow artifact or GitHub release before another upload is attempted. Actions artifacts can disappear on rerun, so the draft release is the durable backup. Do not delete it, create a replacement version bump or rebuild an already-published version. Conflicting bytes or tags require investigation; never automatically yank a version or move a tag. An incomplete draft requires restoring the missing original files before retrying.

Verify both the downloaded gem and `release.json` with `gh attestation verify`, enforcing the publishing workflow's certificate identity and default branch ref. Then check the signed receipt's source commit and gem digest; follow the companion gem's setup guide for the complete commands. `--source-digest` checks the workflow revision, which can differ from the release commit. Release assets include the gem, RubyGems Sigstore bundle, GitHub provenance bundle, and signed source/digest receipt.

Expand Down
6 changes: 4 additions & 2 deletions context/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,11 @@ Use **Re-run all jobs** on the original publishing run, or:
bundle exec bake gem:github:release:resume run=RUN_ID
```

Rerunning keeps the original event identity. A retained artifact is downloaded and its source identity/digest checked. A matching registry version resumes tag/release finalization; different bytes or a conflicting tag stop. There is no automatic yank, retag, or rebuild of an already-published version. If failure occurs before artifact retention, rebuilding is safe because uploading has not started.
Rerunning keeps the original event identity. A retained artifact is downloaded and its source identity/digest checked. A matching registry version resumes tag/release finalization; different bytes or a conflicting tag stop. There is no automatic yank, retag, or rebuild of an already-published version. Registry propagation is retried every ten seconds for up to one minute; a digest or attestation mismatch fails immediately.

Artifacts are retained for 90 days and copied to GitHub release assets on success. If the workflow artifact expires before finalization, manual recovery from the preserved bytes is required; do not dispatch a replacement build. GitHub concurrency does not guarantee a durable FIFO queue: rerun any publishing run displaced while pending. Resume reruns all jobs, including integrity checks; it does not repeat or second-guess the native review policy or a permitted administrator bypass.
Before uploading to RubyGems, the publisher stores the verified gem, receipt and both attestation bundles in a draft GitHub release targeting the merged commit. It publishes the draft after registry verification and tag creation. Actions artifacts are also retained for 90 days, but can disappear on rerun. Recovery falls back to the draft or published release and verifies the original bytes and attestations. Keep the draft until finalization succeeds. If asset preservation was interrupted and neither backup is complete, restore the missing original files before retrying; conflicting assets are never overwritten.

GitHub concurrency does not guarantee a durable FIFO queue: rerun any publishing run displaced while pending. Resume reruns all jobs, including integrity checks; it does not repeat or second-guess the native review policy or a permitted administrator bypass. Older publishing runs execute their original code; adding this recovery support to the default branch does not change an already-triggered workflow.

## Development and current limits

Expand Down
93 changes: 93 additions & 0 deletions fixtures/bake/gem/github/recovery_publisher.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2026, by Samuel Williams.

require "bake/gem/github/publisher"

module Bake
module Gem
module GitHub
# A simulated registry and GitHub finalizer. Core content validation has its own
# real-repository integration tests; this fixture exercises interruption/retry.
class RecoveryPublisher < Publisher
attr_accessor :remote_digest, :fail_release, :fail_attestation, :fail_receipt_verification
attr_accessor :releases, :artifacts, :fail_preservation
attr_reader :commands, :stored_files

def initialize(root)
super
@commands = []
@releases = []
@artifacts = []
@stored_files = {}
@release = Object.new
def @release.resolve(reference)
"a" * 40
end
def @release.validate(**options)
{name: "example", version: "1.0.1", commit: "a" * 40}
end
end

def merged(number)
{"merge_commit_sha" => "a" * 40, "number" => 42, "html_url" => "https://github.com/socketry/example/pull/42"}
end

def system(*arguments, **options)
@commands << arguments
raise "Attestation verification failed" if @fail_attestation && arguments[0, 3] == ["gem", "exec", "sigstore-cli:0.2.3"]
if @fail_receipt_verification && arguments[0, 3] == ["gh", "attestation", "verify"] && arguments[3].end_with?("/release.json")
raise "Receipt attestation verification failed"
end
if arguments[0, 2] == ["gem", "push"]
@remote_digest = load_receipt.fetch(:sha256)
end
case arguments[0, 3]
when ["gh", "release", "create"]
@releases << {"tag_name" => arguments[3], "draft" => true, "target_commitish" => arguments[arguments.index("--target") + 1], "assets" => []}
when ["gh", "release", "upload"]
raise "Preservation failed" if @fail_preservation
file = arguments[4]
@stored_files[File.basename(file)] = File.binread(file)
@releases.first.fetch("assets") << {"name" => File.basename(file), "digest" => "sha256:#{Digest::SHA256.file(file).hexdigest}"}
when ["gh", "release", "download"], ["gh", "run", "download"]
path = arguments[arguments.index("--dir") + 1]
@stored_files.each{|name, content| File.binwrite(File.join(path, name), content)}
when ["gh", "release", "edit"]
raise "GitHub unavailable after upload" if @fail_release
@releases.first["draft"] = false
end
true
end

def readlines(*arguments, **options)
@commands << arguments
return [JSON.generate([@releases])] if arguments[0, 2] == ["gh", "api"]
[]
end

def api(path)
{"artifacts" => @artifacts}
end

private

def gem_command(*arguments)
system("gem", *arguments, chdir: @root)
end

def guard_environment
end

def registry_digest(name, version)
@remote_digest
end

def registry_get(url)
JSON.generate([{bundle: {mediaType: "test"}}])
end
end
end
end
end
6 changes: 4 additions & 2 deletions guides/getting-started/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,11 @@ Use **Re-run all jobs** on the original publishing run, or:
bundle exec bake gem:github:release:resume run=RUN_ID
```

Rerunning keeps the original event identity. A retained artifact is downloaded and its source identity/digest checked. A matching registry version resumes tag/release finalization; different bytes or a conflicting tag stop. There is no automatic yank, retag, or rebuild of an already-published version. If failure occurs before artifact retention, rebuilding is safe because uploading has not started.
Rerunning keeps the original event identity. A retained artifact is downloaded and its source identity/digest checked. A matching registry version resumes tag/release finalization; different bytes or a conflicting tag stop. There is no automatic yank, retag, or rebuild of an already-published version. Registry propagation is retried every ten seconds for up to one minute; a digest or attestation mismatch fails immediately.

Artifacts are retained for 90 days and copied to GitHub release assets on success. If the workflow artifact expires before finalization, manual recovery from the preserved bytes is required; do not dispatch a replacement build. GitHub concurrency does not guarantee a durable FIFO queue: rerun any publishing run displaced while pending. Resume reruns all jobs, including integrity checks; it does not repeat or second-guess the native review policy or a permitted administrator bypass.
Before uploading to RubyGems, the publisher stores the verified gem, receipt and both attestation bundles in a draft GitHub release targeting the merged commit. It publishes the draft after registry verification and tag creation. Actions artifacts are also retained for 90 days, but can disappear on rerun. Recovery falls back to the draft or published release and verifies the original bytes and attestations. Keep the draft until finalization succeeds. If asset preservation was interrupted and neither backup is complete, restore the missing original files before retrying; conflicting assets are never overwritten.

GitHub concurrency does not guarantee a durable FIFO queue: rerun any publishing run displaced while pending. Resume reruns all jobs, including integrity checks; it does not repeat or second-guess the native review policy or a permitted administrator bypass. Older publishing runs execute their original code; adding this recovery support to the default branch does not change an already-triggered workflow.

## Development and current limits

Expand Down
Loading
Loading