class Setup
Generates reviewable repository files without changing remote settings.
Definitions
def initialize(root)
Signature
-
parameter
rootString Destination repository.
Implementation
def initialize(root)
@root = File.expand_path(root)
end
def generate(repository:, branch: "main", checks:, approvals: 2, reviewers: nil, signing: File.file?(File.join(@root, "release.cert")), ruby: "3.4")
Generate workflows, policy payloads, and configuration. Refuse conflicting existing files.
Signature
-
parameter
repositoryString The canonical GitHub owner and repository name.
-
parameter
branchString The default branch receiving release PRs.
-
parameter
checksArray(String) Required CI job names; release validation is added automatically.
-
parameter
approvalsInteger Required approvals, between one and six.
-
parameter
reviewersArray(String) | Nil Publishing environment reviewers, as user logins or organization/team names. Nil leaves environment settings unmanaged.
-
parameter
signingBoolean Whether publishing requires the certificate and matching private key.
-
parameter
rubyString The Ruby version used by release workflows.
-
returns
Array(String) Generated paths relative to the repository root.
-
raises
RuntimeError If configuration is invalid or an existing generated file differs.
Implementation
def generate(repository:, branch: "main", checks:, approvals: 2, reviewers: nil, signing: File.file?(File.join(@root, "release.cert")), ruby: "3.4")
raise "Expected owner/repository." unless repository.match?(/\A[\w.-]+\/[\w.-]+\z/)
raise "Unsupported branch name." unless branch.match?(/\A[\w.\/-]+\z/)
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",
}
config["reviewers"] = reviewers unless reviewers.nil?
files = render(config)
conflicts = files.keys.select do |name|
path = File.join(@root, name)
File.exist?(path) && File.read(path) != files[name]
end
raise "Existing files differ; review them before regenerating: #{conflicts.join(', ')}" unless conflicts.empty?
write(files)
return files.keys
end
def update
Update generated files in the working tree using the existing configuration; return changed paths.
Signature
-
returns
Array(String) Changed paths relative to the repository root.
-
raises
RuntimeError If the configuration schema is unsupported.
Implementation
def update
config = YAML.safe_load_file(File.join(@root, "config/release.yaml"))
raise "Unsupported release configuration." unless config.fetch("schema") == 1
return write(render(config))
end
def self.rules(config)
Native review/check rules allow PR-only administrator bypass; history rules have no bypass.
Signature
-
parameter
configHash Release configuration with string keys:
branch,approvals, andchecks.-
returns
Hash Ruleset payloads keyed by
reviews,checks,history, andtags.
Implementation
def self.rules(config)
conditions = {ref_name: {include: ["refs/heads/#{config.fetch('branch')}"], exclude: []}}
common = {target: "branch", enforcement: "active", conditions: conditions}
bypass = [{actor_id: 5, actor_type: "RepositoryRole", bypass_mode: "pull_request"}]
return {
"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", "rebase"],
},
}],
),
"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}},
},
}],
),
"history" => common.merge(
name: "Gem release history",
bypass_actors: [],
rules: [{type: "deletion"}, {type: "non_fast_forward"}],
),
"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
def self.validate_reviewers(reviewers)
Validate an explicit list of publishing environment reviewers.
Signature
-
parameter
reviewersArray(String) One to six user logins or organization/team names.
-
raises
ArgumentError If the list is empty, too long, or contains invalid names.
Implementation
def self.validate_reviewers(reviewers)
unless reviewers.is_a?(Array) && (1..6).include?(reviewers.size) && reviewers.all?{|name| name.is_a?(String) && name.match?(/\A[\w-]+(?:\/[\w-]+)?\z/)}
raise ArgumentError, "Specify one to six environment reviewers as user logins or organization/team names."
end
end