From 827a7873e6274f1e600f714f7d3c1767fa0ce471 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Mon, 21 Sep 2026 23:21:27 +1200 Subject: [PATCH 1/3] Generate reviewable patches for release setup updates --- .github/releasing.md | 2 ++ bake/gem/github/setup.rb | 6 ++++ context/getting-started.md | 14 +++++++- guides/getting-started/readme.md | 14 +++++++- lib/bake/gem/github/setup.rb | 50 +++++++++++++++++++++++----- releases.md | 4 +++ templates/releasing.md | 2 ++ test/setup_update.rb | 57 ++++++++++++++++++++++++++++++++ 8 files changed, 138 insertions(+), 11 deletions(-) create mode 100644 test/setup_update.rb diff --git a/.github/releasing.md b/.github/releasing.md index 422d689..e2e4ed5 100644 --- a/.github/releasing.md +++ b/.github/releasing.md @@ -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`, run `bundle exec bake gem:github:setup:update` to write `pkg/release-setup.patch`. Review every hunk, including differences from local customizations, then apply the desired changes and open a PR. The task does not modify repository files or remote settings. diff --git a/bake/gem/github/setup.rb b/bake/gem/github/setup.rb index d64f7ab..6e8a08b 100644 --- a/bake/gem/github/setup.rb +++ b/bake/gem/github/setup.rb @@ -13,3 +13,9 @@ def apply require_relative "../../../lib/bake/gem/github/project" Bake::Gem::GitHub::Project.new(context.root).apply end + +# Write a reviewable update patch 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 diff --git a/context/getting-started.md b/context/getting-started.md index dcd26fc..a69e158 100644 --- a/context/getting-started.md +++ b/context/getting-started.md @@ -18,7 +18,19 @@ 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, or change policy, edit `config/release.yaml` as needed and generate an update patch: + +``` bash +bundle exec bake gem:github:setup:update +cat pkg/release-setup.patch +git apply --check pkg/release-setup.patch +git apply pkg/release-setup.patch +git diff +``` + +The task leaves repository files untouched and writes only `pkg/release-setup.patch`. The patch proposes the current templates, policy payloads, configuration formatting, and maintainer instructions. It includes any differences from repository customizations: inspect every hunk, remove unwanted hunks or apply selected changes manually, then commit the result through a PR. Repeating the task after applying all changes produces an empty patch. Intentionally retained customizations remain visible on later updates. Apply remote rulesets only after the corresponding workflows are merged and their checks 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. diff --git a/guides/getting-started/readme.md b/guides/getting-started/readme.md index dcd26fc..a69e158 100644 --- a/guides/getting-started/readme.md +++ b/guides/getting-started/readme.md @@ -18,7 +18,19 @@ 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, or change policy, edit `config/release.yaml` as needed and generate an update patch: + +``` bash +bundle exec bake gem:github:setup:update +cat pkg/release-setup.patch +git apply --check pkg/release-setup.patch +git apply pkg/release-setup.patch +git diff +``` + +The task leaves repository files untouched and writes only `pkg/release-setup.patch`. The patch proposes the current templates, policy payloads, configuration formatting, and maintainer instructions. It includes any differences from repository customizations: inspect every hunk, remove unwanted hunks or apply selected changes manually, then commit the result through a PR. Repeating the task after applying all changes produces an empty patch. Intentionally retained customizations remain visible on later updates. Apply remote rulesets only after the corresponding workflows are merged and their checks 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. diff --git a/lib/bake/gem/github/setup.rb b/lib/bake/gem/github/setup.rb index 321335a..b14664d 100644 --- a/lib/bake/gem/github/setup.rb +++ b/lib/bake/gem/github/setup.rb @@ -7,6 +7,7 @@ require "yaml" require "json" require "fileutils" +require "tempfile" module Bake module Gem @@ -25,15 +26,7 @@ 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| @@ -44,6 +37,27 @@ def generate(repository:, branch: "main", checks:, approvals: 2, signing: File.f files.keys end + # Write a patch proposing updates from the existing configuration, without replacing repository files. + def update + config = YAML.safe_load_file(File.join(@root, "config/release.yaml")) + raise "Unsupported release configuration." unless config.fetch("schema") == 1 + patch = render(config).map do |name, content| + path = File.join(@root, name) + Tempfile.create("release-update") do |file| + file.write(content) + file.flush + exists = File.exist?(path) + diff = IO.popen(["diff", "-u", "--label", exists ? "a/#{name}" : "/dev/null", "--label", "b/#{name}", exists ? path : File::NULL, file.path], &:read) + raise "Unable to compare #{name}." unless [0, 1].include?($?.exitstatus) + diff + end + end.join + path = File.join(@root, "pkg/release-setup.patch") + FileUtils.mkdir_p(File.dirname(path)) + File.write(path, patch) + path + 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: []}} @@ -56,6 +70,24 @@ 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 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 diff --git a/releases.md b/releases.md index 7da9799..7fd6a51 100644 --- a/releases.md +++ b/releases.md @@ -1,5 +1,9 @@ # Releases +## Unreleased + + - Generate reviewable setup updates with `gem:github:setup:update`. + ## v0.0.5 - Preserve and recover release files even when GitHub's release list is stale. diff --git a/templates/releasing.md b/templates/releasing.md index 422d689..e2e4ed5 100644 --- a/templates/releasing.md +++ b/templates/releasing.md @@ -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`, run `bundle exec bake gem:github:setup:update` to write `pkg/release-setup.patch`. Review every hunk, including differences from local customizations, then apply the desired changes and open a PR. The task does not modify repository files or remote settings. diff --git a/test/setup_update.rb b/test/setup_update.rb new file mode 100644 index 0000000..41800ba --- /dev/null +++ b/test/setup_update.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2026, by Samuel Williams. + +require "bake/gem/github/setup" +require "bake/context" +require "sus/fixtures/temporary_directory_context" + +describe Bake::Gem::GitHub::Setup do + include Sus::Fixtures::TemporaryDirectoryContext + + let(:setup) {subject.new(root)} + + before do + setup.generate(repository: "socketry/example", checks: ["Tests"], signing: false) + end + + it "proposes configuration changes and customizations without overwriting them" do + path = File.join(root, "config/release.yaml") + config = YAML.safe_load_file(path) + config["approvals"] = 3 + config["checks"] << "New check" + File.write(path, YAML.dump(config)) + workflow = File.join(root, ".github/workflows/release-validate.yaml") + File.write(workflow, "# Custom workflow\n" + File.read(workflow)) + before = Dir.glob("{config,.github}/**/*", File::FNM_DOTMATCH, base: root).select{|name| File.file?(File.join(root, name))}.to_h{|name| [name, File.read(File.join(root, name))]} + registry = Bake::Registry::Aggregate.new + registry.append_path(File.expand_path("..", __dir__)) + patch = Bake::Context.new(registry, root).call("gem:github:setup:update") + expect(File.read(patch)).to be(:include?, "-# Custom workflow") + expect(File.read(patch)).to be(:include?, '+ "context": "New check"') + before.each do |name, content| + expect(File.read(File.join(root, name))).to be == content + end + expect(system("git", "apply", "--check", patch, chdir: root)).to be == true + expect(system("git", "apply", patch, chdir: root)).to be == true + expect(File.read(setup.update)).to be == "" + expect(YAML.safe_load_file(path)).to be == config + end + + it "restores missing generated files through the patch" do + path = File.join(root, ".github/releasing.md") + original = File.read(path) + File.unlink(path) + patch = setup.update + expect(File.exist?(path)).to be == false + expect(system("git", "apply", patch, chdir: root)).to be == true + expect(File.read(path)).to be == original + expect(File.read(setup.update)).to be == "" + end + + it "refuses unsupported configuration schemas" do + File.write(File.join(root, "config/release.yaml"), YAML.dump("schema" => 2)) + expect{setup.update}.to raise_exception(RuntimeError, message: be =~ /Unsupported release configuration/) + end +end From 089d0950e7a13413e3c10fc07bf59fd3f613aa10 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Mon, 21 Sep 2026 23:29:27 +1200 Subject: [PATCH 2/3] Follow Sus layout for setup update tests --- test/{setup_update.rb => bake/gem/github/setup/update.rb} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename test/{setup_update.rb => bake/gem/github/setup/update.rb} (96%) diff --git a/test/setup_update.rb b/test/bake/gem/github/setup/update.rb similarity index 96% rename from test/setup_update.rb rename to test/bake/gem/github/setup/update.rb index 41800ba..b4eebd7 100644 --- a/test/setup_update.rb +++ b/test/bake/gem/github/setup/update.rb @@ -26,7 +26,7 @@ File.write(workflow, "# Custom workflow\n" + File.read(workflow)) before = Dir.glob("{config,.github}/**/*", File::FNM_DOTMATCH, base: root).select{|name| File.file?(File.join(root, name))}.to_h{|name| [name, File.read(File.join(root, name))]} registry = Bake::Registry::Aggregate.new - registry.append_path(File.expand_path("..", __dir__)) + registry.append_path(::Gem.loaded_specs.fetch("bake-gem-github").full_gem_path) patch = Bake::Context.new(registry, root).call("gem:github:setup:update") expect(File.read(patch)).to be(:include?, "-# Custom workflow") expect(File.read(patch)).to be(:include?, '+ "context": "New check"') From cc698a7b3bc569e05ceef693f6bb3643623d9d37 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Mon, 21 Sep 2026 23:40:15 +1200 Subject: [PATCH 3/3] Apply setup updates directly to the working tree --- .github/releasing.md | 2 +- bake/gem/github/setup.rb | 2 +- context/getting-started.md | 7 +-- guides/getting-started/readme.md | 7 +-- lib/bake/gem/github/setup.rb | 35 +++++------ releases.md | 2 +- templates/releasing.md | 2 +- test/bake/gem/github/setup/update.rb | 87 ++++++++++++++-------------- 8 files changed, 64 insertions(+), 80 deletions(-) diff --git a/.github/releasing.md b/.github/releasing.md index e2e4ed5..3554cd2 100644 --- a/.github/releasing.md +++ b/.github/releasing.md @@ -14,4 +14,4 @@ Verify both the downloaded gem and `release.json` with `gh attestation verify`, 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`, run `bundle exec bake gem:github:setup:update` to write `pkg/release-setup.patch`. Review every hunk, including differences from local customizations, then apply the desired changes and open a PR. The task does not modify repository files or remote settings. +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. diff --git a/bake/gem/github/setup.rb b/bake/gem/github/setup.rb index 6e8a08b..689bd58 100644 --- a/bake/gem/github/setup.rb +++ b/bake/gem/github/setup.rb @@ -14,7 +14,7 @@ def apply Bake::Gem::GitHub::Project.new(context.root).apply end -# Write a reviewable update patch using config/release.yaml and the installed templates. +# 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 diff --git a/context/getting-started.md b/context/getting-started.md index a69e158..7d874f3 100644 --- a/context/getting-started.md +++ b/context/getting-started.md @@ -20,17 +20,14 @@ 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. Setup does not replace other publishers: remove conflicting release workflows during migration. -To adopt template fixes after upgrading the gem, or change policy, edit `config/release.yaml` as needed and generate an update patch: +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 -cat pkg/release-setup.patch -git apply --check pkg/release-setup.patch -git apply pkg/release-setup.patch git diff ``` -The task leaves repository files untouched and writes only `pkg/release-setup.patch`. The patch proposes the current templates, policy payloads, configuration formatting, and maintainer instructions. It includes any differences from repository customizations: inspect every hunk, remove unwanted hunks or apply selected changes manually, then commit the result through a PR. Repeating the task after applying all changes produces an empty patch. Intentionally retained customizations remain visible on later updates. Apply remote rulesets only after the corresponding workflows are merged and their checks are running. +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. diff --git a/guides/getting-started/readme.md b/guides/getting-started/readme.md index a69e158..7d874f3 100644 --- a/guides/getting-started/readme.md +++ b/guides/getting-started/readme.md @@ -20,17 +20,14 @@ 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. Setup does not replace other publishers: remove conflicting release workflows during migration. -To adopt template fixes after upgrading the gem, or change policy, edit `config/release.yaml` as needed and generate an update patch: +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 -cat pkg/release-setup.patch -git apply --check pkg/release-setup.patch -git apply pkg/release-setup.patch git diff ``` -The task leaves repository files untouched and writes only `pkg/release-setup.patch`. The patch proposes the current templates, policy payloads, configuration formatting, and maintainer instructions. It includes any differences from repository customizations: inspect every hunk, remove unwanted hunks or apply selected changes manually, then commit the result through a PR. Repeating the task after applying all changes produces an empty patch. Intentionally retained customizations remain visible on later updates. Apply remote rulesets only after the corresponding workflows are merged and their checks are running. +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. diff --git a/lib/bake/gem/github/setup.rb b/lib/bake/gem/github/setup.rb index b14664d..2ffb40a 100644 --- a/lib/bake/gem/github/setup.rb +++ b/lib/bake/gem/github/setup.rb @@ -7,7 +7,6 @@ require "yaml" require "json" require "fileutils" -require "tempfile" module Bake module Gem @@ -29,33 +28,15 @@ def generate(repository:, branch: "main", checks:, approvals: 2, signing: File.f 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 - # Write a patch proposing updates from the existing configuration, without replacing repository files. + # 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 - patch = render(config).map do |name, content| - path = File.join(@root, name) - Tempfile.create("release-update") do |file| - file.write(content) - file.flush - exists = File.exist?(path) - diff = IO.popen(["diff", "-u", "--label", exists ? "a/#{name}" : "/dev/null", "--label", "b/#{name}", exists ? path : File::NULL, file.path], &:read) - raise "Unable to compare #{name}." unless [0, 1].include?($?.exitstatus) - diff - end - end.join - path = File.join(@root, "pkg/release-setup.patch") - FileUtils.mkdir_p(File.dirname(path)) - File.write(path, patch) - path + write(render(config)) end # Native review/check rules allow PR-only administrator bypass; history rules have no bypass. @@ -73,6 +54,16 @@ def self.rules(config) 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") diff --git a/releases.md b/releases.md index 7fd6a51..0c9389c 100644 --- a/releases.md +++ b/releases.md @@ -2,7 +2,7 @@ ## Unreleased - - Generate reviewable setup updates with `gem:github:setup:update`. + - Update generated release files in the working tree with `gem:github:setup:update`. ## v0.0.5 diff --git a/templates/releasing.md b/templates/releasing.md index e2e4ed5..3554cd2 100644 --- a/templates/releasing.md +++ b/templates/releasing.md @@ -14,4 +14,4 @@ Verify both the downloaded gem and `release.json` with `gh attestation verify`, 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`, run `bundle exec bake gem:github:setup:update` to write `pkg/release-setup.patch`. Review every hunk, including differences from local customizations, then apply the desired changes and open a PR. The task does not modify repository files or remote settings. +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. diff --git a/test/bake/gem/github/setup/update.rb b/test/bake/gem/github/setup/update.rb index b4eebd7..410489d 100644 --- a/test/bake/gem/github/setup/update.rb +++ b/test/bake/gem/github/setup/update.rb @@ -4,54 +4,53 @@ # Copyright, 2026, by Samuel Williams. require "bake/gem/github/setup" -require "bake/context" -require "sus/fixtures/temporary_directory_context" +require "bake/gem/github/repository_context" describe Bake::Gem::GitHub::Setup do - include Sus::Fixtures::TemporaryDirectoryContext + include Bake::Gem::GitHub::RepositoryContext - let(:setup) {subject.new(root)} + let(:setup) {subject.new(repository)} - before do - setup.generate(repository: "socketry/example", checks: ["Tests"], signing: false) - end - - it "proposes configuration changes and customizations without overwriting them" do - path = File.join(root, "config/release.yaml") - config = YAML.safe_load_file(path) - config["approvals"] = 3 - config["checks"] << "New check" - File.write(path, YAML.dump(config)) - workflow = File.join(root, ".github/workflows/release-validate.yaml") - File.write(workflow, "# Custom workflow\n" + File.read(workflow)) - before = Dir.glob("{config,.github}/**/*", File::FNM_DOTMATCH, base: root).select{|name| File.file?(File.join(root, name))}.to_h{|name| [name, File.read(File.join(root, name))]} - registry = Bake::Registry::Aggregate.new - registry.append_path(::Gem.loaded_specs.fetch("bake-gem-github").full_gem_path) - patch = Bake::Context.new(registry, root).call("gem:github:setup:update") - expect(File.read(patch)).to be(:include?, "-# Custom workflow") - expect(File.read(patch)).to be(:include?, '+ "context": "New check"') - before.each do |name, content| - expect(File.read(File.join(root, name))).to be == content + 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 - expect(system("git", "apply", "--check", patch, chdir: root)).to be == true - expect(system("git", "apply", patch, chdir: root)).to be == true - expect(File.read(setup.update)).to be == "" - expect(YAML.safe_load_file(path)).to be == config - end - - it "restores missing generated files through the patch" do - path = File.join(root, ".github/releasing.md") - original = File.read(path) - File.unlink(path) - patch = setup.update - expect(File.exist?(path)).to be == false - expect(system("git", "apply", patch, chdir: root)).to be == true - expect(File.read(path)).to be == original - expect(File.read(setup.update)).to be == "" - end - - it "refuses unsupported configuration schemas" do - File.write(File.join(root, "config/release.yaml"), YAML.dump("schema" => 2)) - expect{setup.update}.to raise_exception(RuntimeError, message: be =~ /Unsupported release configuration/) end end