bake-gem-github 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,66 +18,149 @@ module Bake
18
18
  @root = File.expand_path(root)
19
19
  end
20
20
 
21
- # Generate workflows, policy payloads, and maintainer instructions. Refuse conflicting existing files.
22
- def generate(repository:, branch: "main", checks:, approvals: 2, signing: File.file?(File.join(@root, "release.cert")), ruby: "3.4")
21
+ # Generate workflows, policy payloads, and configuration. Refuse conflicting existing files.
22
+ # @parameter repository [String] The canonical GitHub owner and repository name.
23
+ # @parameter branch [String] The default branch receiving release PRs.
24
+ # @parameter checks [Array(String)] Required CI job names; release validation is added automatically.
25
+ # @parameter approvals [Integer] Required approvals, between one and six.
26
+ # @parameter reviewers [Array(String) | Nil] Publishing environment reviewers, as user logins or organization/team names. Nil leaves environment settings unmanaged.
27
+ # @parameter signing [Boolean] Whether publishing requires the certificate and matching private key.
28
+ # @parameter ruby [String] The Ruby version used by release workflows.
29
+ # @returns [Array(String)] Generated paths relative to the repository root.
30
+ # @raises [RuntimeError] If configuration is invalid or an existing generated file differs.
31
+ def generate(repository:, branch: "main", checks:, approvals: 2, reviewers: nil, signing: File.file?(File.join(@root, "release.cert")), ruby: "3.4")
23
32
  raise "Expected owner/repository." unless repository.match?(/\A[\w.-]+\/[\w.-]+\z/)
24
33
  raise "Unsupported branch name." unless branch.match?(/\A[\w.\/-]+\z/)
25
34
  raise "Select the required CI check names." if checks.empty?
26
35
  raise "Review count must be between 1 and 6." unless (1..6).include?(approvals)
27
- config = {"schema" => 1, "repository" => repository, "branch" => branch, "checks" => (checks + ["Release validation"]).uniq, "approvals" => approvals, "signing" => signing, "ruby" => ruby, "environment" => "rubygems"}
36
+
37
+ config = {
38
+ "schema" => 1,
39
+ "repository" => repository,
40
+ "branch" => branch,
41
+ "checks" => (checks + ["Release validation"]).uniq,
42
+ "approvals" => approvals,
43
+ "signing" => signing,
44
+ "ruby" => ruby,
45
+ "environment" => "rubygems",
46
+ }
47
+ config["reviewers"] = reviewers unless reviewers.nil?
48
+
28
49
  files = render(config)
29
- conflicts = files.keys.select{|name| File.exist?(File.join(@root, name)) && File.read(File.join(@root, name)) != files[name]}
50
+ conflicts = files.keys.select do |name|
51
+ path = File.join(@root, name)
52
+ File.exist?(path) && File.read(path) != files[name]
53
+ end
54
+
30
55
  raise "Existing files differ; review them before regenerating: #{conflicts.join(', ')}" unless conflicts.empty?
56
+
31
57
  write(files)
32
- files.keys
58
+
59
+ return files.keys
33
60
  end
34
61
 
35
62
  # Update generated files in the working tree using the existing configuration; return changed paths.
63
+ # @returns [Array(String)] Changed paths relative to the repository root.
64
+ # @raises [RuntimeError] If the configuration schema is unsupported.
36
65
  def update
37
66
  config = YAML.safe_load_file(File.join(@root, "config/release.yaml"))
38
67
  raise "Unsupported release configuration." unless config.fetch("schema") == 1
39
- write(render(config))
68
+
69
+ return write(render(config))
40
70
  end
41
71
 
42
72
  # Native review/check rules allow PR-only administrator bypass; history rules have no bypass.
73
+ # @parameter config [Hash] Release configuration with string keys: `branch`, `approvals`, and `checks`.
74
+ # @returns [Hash] Ruleset payloads keyed by `reviews`, `checks`, `history`, and `tags`.
43
75
  def self.rules(config)
44
76
  conditions = {ref_name: {include: ["refs/heads/#{config.fetch('branch')}"], exclude: []}}
45
77
  common = {target: "branch", enforcement: "active", conditions: conditions}
46
78
  bypass = [{actor_id: 5, actor_type: "RepositoryRole", bypass_mode: "pull_request"}]
47
- {
48
- "reviews" => common.merge(name: "Gem release reviews", bypass_actors: bypass, rules: [{type: "pull_request", parameters: {required_approving_review_count: config.fetch("approvals"), dismiss_stale_reviews_on_push: true, require_last_push_approval: true, required_review_thread_resolution: true, require_code_owner_review: false, allowed_merge_methods: ["merge", "squash"]}}]),
49
- "checks" => common.merge(name: "Gem release checks", bypass_actors: bypass, rules: [{type: "required_status_checks", parameters: {strict_required_status_checks_policy: true, do_not_enforce_on_create: false, required_status_checks: config.fetch("checks").map{|name| {context: name}}}}]),
50
- "history" => common.merge(name: "Gem release history", bypass_actors: [], rules: [{type: "deletion"}, {type: "non_fast_forward"}]),
51
- "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"}]}
79
+
80
+ return {
81
+ "reviews" => common.merge(
82
+ name: "Gem release reviews",
83
+ bypass_actors: bypass,
84
+ rules: [{
85
+ type: "pull_request",
86
+ parameters: {
87
+ required_approving_review_count: config.fetch("approvals"),
88
+ dismiss_stale_reviews_on_push: true,
89
+ require_last_push_approval: true,
90
+ required_review_thread_resolution: true,
91
+ require_code_owner_review: false,
92
+ allowed_merge_methods: ["merge", "squash"],
93
+ },
94
+ }],
95
+ ),
96
+ "checks" => common.merge(
97
+ name: "Gem release checks",
98
+ bypass_actors: bypass,
99
+ rules: [{
100
+ type: "required_status_checks",
101
+ parameters: {
102
+ strict_required_status_checks_policy: true,
103
+ do_not_enforce_on_create: false,
104
+ required_status_checks: config.fetch("checks").map{|name| {context: name}},
105
+ },
106
+ }],
107
+ ),
108
+ "history" => common.merge(
109
+ name: "Gem release history",
110
+ bypass_actors: [],
111
+ rules: [{type: "deletion"}, {type: "non_fast_forward"}],
112
+ ),
113
+ "tags" => {
114
+ name: "Gem release tags",
115
+ target: "tag",
116
+ enforcement: "active",
117
+ bypass_actors: [],
118
+ conditions: {ref_name: {include: ["refs/tags/v*"], exclude: []}},
119
+ rules: [{type: "deletion"}, {type: "non_fast_forward"}],
120
+ },
52
121
  }
53
122
  end
54
123
 
124
+ # Validate an explicit list of publishing environment reviewers.
125
+ # @parameter reviewers [Array(String)] One to six user logins or organization/team names.
126
+ # @raises [ArgumentError] If the list is empty, too long, or contains invalid names.
127
+ def self.validate_reviewers(reviewers)
128
+ unless reviewers.is_a?(Array) && (1..6).include?(reviewers.size) && reviewers.all?{|name| name.is_a?(String) && name.match?(/\A[\w-]+(?:\/[\w-]+)?\z/)}
129
+ raise ArgumentError, "Specify one to six environment reviewers as user logins or organization/team names."
130
+ end
131
+ end
132
+
55
133
  private
56
134
 
57
135
  def write(files)
58
136
  files.filter_map do |name, content|
59
137
  path = File.join(@root, name)
60
138
  next if File.exist?(path) && File.read(path) == content
139
+
61
140
  FileUtils.mkdir_p(File.dirname(path))
62
141
  File.write(path, content)
142
+
63
143
  name
64
144
  end
65
145
  end
66
146
 
67
147
  def render(config)
148
+ self.class.validate_reviewers(config["reviewers"]) if config.key?("reviewers")
68
149
  branch = config.fetch("branch")
69
150
  ruby = config.fetch("ruby")
70
151
  signing = config.fetch("signing")
152
+
71
153
  templates = File.expand_path("../../../../templates", __dir__)
72
154
  files = {"config/release.yaml" => YAML.dump(config)}
73
155
  Dir.glob("*.erb", base: templates).each do |name|
74
156
  files[".github/workflows/#{name.delete_suffix('.erb')}"] = ERB.new(File.read(File.join(templates, name)), trim_mode: "-").result(binding)
75
157
  end
158
+
76
159
  self.class.rules(config).each do |name, rule|
77
160
  files[".github/release-rules/#{name}.json"] = JSON.pretty_generate(rule) + "\n"
78
161
  end
79
- files[".github/releasing.md"] = File.read(File.join(templates, "releasing.md"))
80
- files
162
+
163
+ return files
81
164
  end
82
165
  end
83
166
  end
@@ -9,7 +9,7 @@ module Bake
9
9
  module Gem
10
10
  # GitHub pull request and release orchestration for bake-gem.
11
11
  module GitHub
12
- VERSION = "0.2.0"
12
+ VERSION = "0.3.0"
13
13
  end
14
14
  end
15
15
  end
data/readme.md CHANGED
@@ -2,11 +2,89 @@
2
2
 
3
3
  Reviewed GitHub releases for Ruby gems, using `bake-gem` for branch preparation, independent content validation, and clean builds.
4
4
 
5
- - `gem:github:release:patch` / `minor` / `major`: prepare, push and open a release PR.
6
- - `gem:github:setup`: generate the three workflows and native review/CI policy.
7
- - `gem:github:setup:plan` / `apply`: inspect and apply the managed GitHub rulesets.
8
- - `gem:github:release:resume run=ID`: retry with the original artifact.
5
+ [![Development Status](https://github.com/socketry/bake-gem-github/workflows/Test/badge.svg)](https://github.com/socketry/bake-gem-github/actions?workflow=Test)
9
6
 
10
- Read [the setup, release and recovery guide](guides/getting-started/readme.md) before enabling publishing. Context is distributed through `agent-context`. This initial implementation requires `bake-gem` 0.15 or later and a live pilot before wider rollout.
7
+ ## Motivation
11
8
 
12
- For this gem's releases, follow [the maintainer instructions](.github/releasing.md). Release workflows use the source checkout through Bundler's `gemspec` dependency, so the first release does not require an already published copy of `bake-gem-github`.
9
+ Maintainers need a shared release process that they can run locally or through GitHub. This gem prepares release pull requests for review, validates their generated content, and publishes the merged release through GitHub Actions using RubyGems Trusted Publishing. Retained artifacts and attestations allow interrupted releases to resume using the original verified bytes.
10
+
11
+ ## Usage
12
+
13
+ Please see the [project documentation](https://socketry.github.io/bake-gem-github/) for more details.
14
+
15
+ - [Getting Started](https://socketry.github.io/bake-gem-github/guides/getting-started/index) - This guide explains how to configure reviewed Ruby gem releases and prepare the first release PR with `bake-gem-github`.
16
+
17
+ - [Preparing Releases](https://socketry.github.io/bake-gem-github/guides/preparing-releases/index) - This guide explains how to request a release PR, resume interrupted preparation, and refresh generated content when the default branch changes.
18
+
19
+ - [Verifying Releases](https://socketry.github.io/bake-gem-github/guides/verifying-releases/index) - This guide explains how publishing binds a gem to its reviewed source and how to verify the downloaded artifact and attestations.
20
+
21
+ - [Recovering Releases](https://socketry.github.io/bake-gem-github/guides/recovering-releases/index) - This guide explains how to resume an interrupted publishing workflow using the original gem and its verification evidence.
22
+
23
+ ## Releases
24
+
25
+ Please see the [project releases](https://socketry.github.io/bake-gem-github/releases/index) for all releases.
26
+
27
+ ### v0.3.0
28
+
29
+ - Configure publishing environment reviewers through release setup while preserving existing environment protections.
30
+ - Stop generating `.github/releasing.md`; release instructions are maintained in the shared guide and agent context.
31
+ - Resume interrupted release preparation and explicitly refresh stale release PRs while preserving their previous commits.
32
+ - Preserve all release files in one archive before individual asset uploads, so reruns can recover interrupted drafts.
33
+
34
+ ### v0.2.0
35
+
36
+ - Use only the version tag for GitHub release titles.
37
+
38
+ ### v0.1.0
39
+
40
+ - Include the version's release notes in GitHub releases using `bake-releases`.
41
+ - Update generated release files in the working tree with `gem:github:setup:update`.
42
+
43
+ ### v0.0.5
44
+
45
+ - Preserve and recover release files even when GitHub's release list is stale.
46
+
47
+ ### v0.0.4
48
+
49
+ - Preserve verified release files in a draft GitHub release before uploading to RubyGems, so reruns can recover when Actions artifacts disappear.
50
+ - Wait for RubyGems registry propagation before finalizing releases.
51
+
52
+ ### v0.0.1
53
+
54
+ - Initial implementation.
55
+
56
+ ## Contributing
57
+
58
+ We welcome contributions to this project.
59
+
60
+ 1. Fork the repository.
61
+ 2. Create your feature branch (`git checkout -b my-new-feature`).
62
+ 3. Commit your changes (`git commit -am 'Add some feature.'`).
63
+ 4. Push to the branch (`git push origin my-new-feature`).
64
+ 5. Create a new pull request.
65
+
66
+ ### Running Tests
67
+
68
+ To run the test suite:
69
+
70
+ ``` bash
71
+ $ bundle exec sus
72
+ ```
73
+
74
+ ### Making Releases
75
+
76
+ To prepare a release branch and open a pull request from an up-to-date `main`:
77
+
78
+ ``` bash
79
+ $ bundle exec bake gem:github:release:patch # or minor or major
80
+ ```
81
+
82
+ See [bake-gem-github](https://github.com/socketry/bake-gem-github) for setup, remote releases, and recovery.
83
+
84
+ ### Developer Certificate of Origin
85
+
86
+ In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed.
87
+
88
+ ### Community Guidelines
89
+
90
+ This project is best served by a collaborative and respectful environment. Treat each other professionally, respect differing viewpoints, and engage constructively. Harassment, discrimination, or harmful behavior is not tolerated. Communicate clearly, listen actively, and support one another. If any issues arise, please inform the project maintainers.
data/releases.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Releases
2
2
 
3
+ ## v0.3.0
4
+
5
+ - Configure publishing environment reviewers through release setup while preserving existing environment protections.
6
+ - Stop generating `.github/releasing.md`; release instructions are maintained in the shared guide and agent context.
7
+ - Resume interrupted release preparation and explicitly refresh stale release PRs while preserving their previous commits.
8
+ - Preserve all release files in one archive before individual asset uploads, so reruns can recover interrupted drafts.
9
+
3
10
  ## v0.2.0
4
11
 
5
12
  - Use only the version tag for GitHub release titles.
@@ -8,6 +8,10 @@ on:
8
8
  required: true
9
9
  type: choice
10
10
  options: [patch, minor, major]
11
+ refresh:
12
+ description: Preserve and regenerate an existing release branch
13
+ type: boolean
14
+ default: false
11
15
 
12
16
  permissions:
13
17
  contents: write
@@ -38,10 +42,11 @@ jobs:
38
42
  env:
39
43
  GH_TOKEN: ${{ github.token }}
40
44
  BUMP: ${{ inputs.bump }}
45
+ REFRESH: ${{ inputs.refresh }}
41
46
  run: |
42
47
  # GitHub.com's shared Actions bot ID; this identity is not for GitHub Enterprise Server.
43
48
  # https://github.com/actions/checkout#push-a-commit-using-the-built-in-token
44
49
  git config user.name 'github-actions[bot]'
45
50
  git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
46
51
  case "$BUMP" in patch|minor|major) ;; *) exit 1 ;; esac
47
- bundle exec bake "gem:github:release:$BUMP"
52
+ bundle exec bake "gem:github:release:$BUMP" "refresh=$REFRESH"
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bake-gem-github
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -94,14 +94,18 @@ executables: []
94
94
  extensions: []
95
95
  extra_rdoc_files: []
96
96
  files:
97
- - agents.md
98
97
  - bake/gem/github.rb
99
98
  - bake/gem/github/release.rb
100
99
  - bake/gem/github/setup.rb
101
100
  - context/getting-started.md
102
101
  - context/index.yaml
102
+ - context/preparing-releases.md
103
+ - context/recovering-releases.md
104
+ - context/verifying-releases.md
105
+ - lib/bake/gem/github/backup.rb
103
106
  - lib/bake/gem/github/project.rb
104
107
  - lib/bake/gem/github/publisher.rb
108
+ - lib/bake/gem/github/registry.rb
105
109
  - lib/bake/gem/github/setup.rb
106
110
  - lib/bake/gem/github/version.rb
107
111
  - license.md
@@ -111,11 +115,14 @@ files:
111
115
  - templates/release-prepare.yaml.erb
112
116
  - templates/release-publish.yaml.erb
113
117
  - templates/release-validate.yaml.erb
114
- - templates/releasing.md
115
118
  homepage: https://github.com/socketry/bake-gem-github
116
119
  licenses:
117
120
  - MIT
118
- metadata: {}
121
+ metadata:
122
+ documentation_uri: https://socketry.github.io/bake-gem-github/
123
+ bug_tracker_uri: https://github.com/socketry/bake-gem-github/issues
124
+ changelog_uri: https://github.com/socketry/bake-gem-github/blob/main/releases.md
125
+ source_code_uri: https://github.com/socketry/bake-gem-github.git
119
126
  rdoc_options: []
120
127
  require_paths:
121
128
  - lib
metadata.gz.sig CHANGED
Binary file