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
2 changes: 2 additions & 0 deletions .github/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ If a publishing job fails, use **Re-run all jobs** on that same workflow run, or
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.

The initial implementation supports public repositories, a single gemspec, stable patch/minor/major versions, and merge/squash commits. Keep rebase merges and merge queues disabled. Generation hooks must be repeatable from the same source and version. Run setup and account recovery checks before enabling publishing; see the companion gem's setup guide.

After upgrading `bake-gem-github`, start from a clean working tree and run `bundle exec bake gem:github:setup:update`. It updates generated files without staging or committing them. Review `git diff`, selectively keep changes and restore customizations as needed, then open a PR.
6 changes: 6 additions & 0 deletions bake/gem/github/setup.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,9 @@ def apply
require_relative "../../../lib/bake/gem/github/project"
Bake::Gem::GitHub::Project.new(context.root).apply
end

# Update generated files in the working tree using config/release.yaml and the installed templates.
def update
require_relative "../../../lib/bake/gem/github/setup"
Bake::Gem::GitHub::Setup.new(context.root).update
end
11 changes: 10 additions & 1 deletion context/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,16 @@ bundle exec bake agent:context:install
bundle exec bake gem:github:setup:plan
```

Setup adds three pinned workflows, `config/release.yaml`, native ruleset payloads, and `.github/releasing.md`. Identical reruns do nothing; conflicting existing files stop before any file is written. For later policy changes, edit the configuration and review the generated workflow/policy changes explicitly. Setup does not replace other publishers: remove conflicting release workflows during migration.
Setup adds three pinned workflows, `config/release.yaml`, native ruleset payloads, and `.github/releasing.md`. Identical reruns do nothing; conflicting existing files stop before any file is written. Setup does not replace other publishers: remove conflicting release workflows during migration.

To adopt template fixes after upgrading the gem, start from a clean working tree, edit `config/release.yaml` as needed, and regenerate:

``` bash
bundle exec bake gem:github:setup:update
git diff
```

The task updates the managed workflows, policy payloads, configuration formatting, and maintainer instructions directly in the working tree and returns the changed paths. It does not stage, commit, or change remote settings. An agent or maintainer can review the diff and selectively keep changes, restoring repository-specific customizations from Git where needed. Commit or stash existing edits first: generated files are replaced by the current templates. Repeating an update produces no further changes; intentionally retained customizations will appear in later update diffs. Apply remote rulesets after the corresponding workflows are running.

The default is two approvals with explicit administrator bypass, dismissed stale reviews, approval of the last push, strict up-to-date CI, and immutable default-branch history/release tags. These branch rules affect **all PRs** into the default branch. Ordinary administrator reviews count as one review. A human who dispatches a bot-authored PR is not its author under GitHub's native rules.

Expand Down
11 changes: 10 additions & 1 deletion guides/getting-started/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,16 @@ bundle exec bake agent:context:install
bundle exec bake gem:github:setup:plan
```

Setup adds three pinned workflows, `config/release.yaml`, native ruleset payloads, and `.github/releasing.md`. Identical reruns do nothing; conflicting existing files stop before any file is written. For later policy changes, edit the configuration and review the generated workflow/policy changes explicitly. Setup does not replace other publishers: remove conflicting release workflows during migration.
Setup adds three pinned workflows, `config/release.yaml`, native ruleset payloads, and `.github/releasing.md`. Identical reruns do nothing; conflicting existing files stop before any file is written. Setup does not replace other publishers: remove conflicting release workflows during migration.

To adopt template fixes after upgrading the gem, start from a clean working tree, edit `config/release.yaml` as needed, and regenerate:

``` bash
bundle exec bake gem:github:setup:update
git diff
```

The task updates the managed workflows, policy payloads, configuration formatting, and maintainer instructions directly in the working tree and returns the changed paths. It does not stage, commit, or change remote settings. An agent or maintainer can review the diff and selectively keep changes, restoring repository-specific customizations from Git where needed. Commit or stash existing edits first: generated files are replaced by the current templates. Repeating an update produces no further changes; intentionally retained customizations will appear in later update diffs. Apply remote rulesets after the corresponding workflows are running.

The default is two approvals with explicit administrator bypass, dismissed stale reviews, approval of the last push, strict up-to-date CI, and immutable default-branch history/release tags. These branch rules affect **all PRs** into the default branch. Ordinary administrator reviews count as one review. A human who dispatches a bot-authored PR is not its author under GitHub's native rules.

Expand Down
51 changes: 37 additions & 14 deletions lib/bake/gem/github/setup.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,20 @@ def generate(repository:, branch: "main", checks:, approvals: 2, signing: File.f
raise "Select the required CI check names." if checks.empty?
raise "Review count must be between 1 and 6." unless (1..6).include?(approvals)
config = {"schema" => 1, "repository" => repository, "branch" => branch, "checks" => (checks + ["Release validation"]).uniq, "approvals" => approvals, "signing" => signing, "ruby" => ruby, "environment" => "rubygems"}
templates = File.expand_path("../../../../templates", __dir__)
files = {"config/release.yaml" => YAML.dump(config)}
Dir.glob("*.erb", base: templates).each do |name|
files[".github/workflows/#{name.delete_suffix('.erb')}"] = ERB.new(File.read(File.join(templates, name)), trim_mode: "-").result(binding)
end
self.class.rules(config).each do |name, rule|
files[".github/release-rules/#{name}.json"] = JSON.pretty_generate(rule) + "\n"
end
files[".github/releasing.md"] = File.read(File.join(templates, "releasing.md"))
files = render(config)
conflicts = files.keys.select{|name| File.exist?(File.join(@root, name)) && File.read(File.join(@root, name)) != files[name]}
raise "Existing files differ; review them before regenerating: #{conflicts.join(', ')}" unless conflicts.empty?
files.each do |name, content|
path = File.join(@root, name)
FileUtils.mkdir_p(File.dirname(path))
File.write(path, content) unless File.exist?(path)
end
write(files)
files.keys
end

# Update generated files in the working tree using the existing configuration; return changed paths.
def update
config = YAML.safe_load_file(File.join(@root, "config/release.yaml"))
raise "Unsupported release configuration." unless config.fetch("schema") == 1
write(render(config))
end

# Native review/check rules allow PR-only administrator bypass; history rules have no bypass.
def self.rules(config)
conditions = {ref_name: {include: ["refs/heads/#{config.fetch('branch')}"], exclude: []}}
Expand All @@ -56,6 +51,34 @@ def self.rules(config)
"tags" => {name: "Gem release tags", target: "tag", enforcement: "active", bypass_actors: [], conditions: {ref_name: {include: ["refs/tags/v*"], exclude: []}}, rules: [{type: "deletion"}, {type: "non_fast_forward"}]}
}
end

private

def write(files)
files.filter_map do |name, content|
path = File.join(@root, name)
next if File.exist?(path) && File.read(path) == content
FileUtils.mkdir_p(File.dirname(path))
File.write(path, content)
name
end
end

def render(config)
branch = config.fetch("branch")
ruby = config.fetch("ruby")
signing = config.fetch("signing")
templates = File.expand_path("../../../../templates", __dir__)
files = {"config/release.yaml" => YAML.dump(config)}
Dir.glob("*.erb", base: templates).each do |name|
files[".github/workflows/#{name.delete_suffix('.erb')}"] = ERB.new(File.read(File.join(templates, name)), trim_mode: "-").result(binding)
end
self.class.rules(config).each do |name, rule|
files[".github/release-rules/#{name}.json"] = JSON.pretty_generate(rule) + "\n"
end
files[".github/releasing.md"] = File.read(File.join(templates, "releasing.md"))
files
end
end
end
end
Expand Down
4 changes: 4 additions & 0 deletions releases.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Releases

## Unreleased

- Update generated release files in the working tree with `gem:github:setup:update`.

## v0.0.5

- Preserve and recover release files even when GitHub's release list is stale.
Expand Down
2 changes: 2 additions & 0 deletions templates/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ If a publishing job fails, use **Re-run all jobs** on that same workflow run, or
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.

The initial implementation supports public repositories, a single gemspec, stable patch/minor/major versions, and merge/squash commits. Keep rebase merges and merge queues disabled. Generation hooks must be repeatable from the same source and version. Run setup and account recovery checks before enabling publishing; see the companion gem's setup guide.

After upgrading `bake-gem-github`, start from a clean working tree and run `bundle exec bake gem:github:setup:update`. It updates generated files without staging or committing them. Review `git diff`, selectively keep changes and restore customizations as needed, then open a PR.
56 changes: 56 additions & 0 deletions test/bake/gem/github/setup/update.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# frozen_string_literal: true

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

require "bake/gem/github/setup"
require "bake/gem/github/repository_context"

describe Bake::Gem::GitHub::Setup do
include Bake::Gem::GitHub::RepositoryContext

let(:setup) {subject.new(repository)}

with "#update" do
it "leaves generated updates in the working tree for selective review" do
workflow = File.join(repository, ".github/workflows/release-validate.yaml")
File.write(workflow, "# Custom workflow\n" + File.read(workflow))
File.write(File.join(repository, ".github/workflows/custom.yaml"), "Custom workflow\n")
git("add", "--all")
git("commit", "--quiet", "-m", "Repository customizations")
original = git("rev-parse", "HEAD")
path = File.join(repository, "config/release.yaml")
config = YAML.safe_load_file(path)
config["approvals"] = 3
config["checks"] << "New check"
File.write(path, YAML.dump(config))
registry = Bake::Registry::Aggregate.new
registry.append_path(::Gem.loaded_specs.fetch("bake-gem-github").full_gem_path)
changed = Bake::Context.new(registry, repository).call("gem:github:setup:update")
expect(changed.sort).to be == [".github/release-rules/checks.json", ".github/release-rules/reviews.json", ".github/workflows/release-validate.yaml"]
diff = git("diff")
expect(diff).to be(:include?, "-# Custom workflow")
expect(diff).to be(:include?, '+ "context": "New check"')
expect(git("rev-parse", "HEAD")).to be == original
expect(git("diff", "--cached")).to be == ""
expect(File.read(File.join(repository, ".github/workflows/custom.yaml"))).to be == "Custom workflow\n"
expect(setup.update).to be == []
expect(git("diff")).to be == diff
expect(YAML.safe_load_file(path)).to be == config
end

it "restores missing generated files" do
path = File.join(repository, ".github/releasing.md")
original = File.read(path)
File.unlink(path)
expect(setup.update).to be == [".github/releasing.md"]
expect(File.read(path)).to be == original
expect(setup.update).to be == []
end

it "refuses unsupported configuration schemas" do
File.write(File.join(repository, "config/release.yaml"), YAML.dump("schema" => 2))
expect{setup.update}.to raise_exception(RuntimeError, message: be =~ /Unsupported release configuration/)
end
end
end
Loading