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
33 changes: 33 additions & 0 deletions .github/actions/sync/issue_bug.template.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: New issue for Reproducible Bug
description: "If you're sure it's reproducible and not just your machine: submit an issue so we can investigate."
type: "Bug"
body:
- type: checkboxes
attributes:
label: Verification
description: Verify you've followed these steps. If you can't truthfully check these boxes, open a discussion at https://github.com/orgs/Homebrew/discussions instead.
options:
- label: I searched for similar issues at https://github.com/%<repository>s/issues and found no duplicates.
required: true
- label: I did not use AI/LLM to create this issue, or I disclosed the tool and model used; I will answer maintainer questions myself without AI/LLM.
required: true
- type: textarea
attributes:
label: What were you trying to do (and why)?
validations:
required: true
- type: textarea
attributes:
label: What happened?
validations:
required: true
- type: textarea
attributes:
label: What did you expect to happen?
validations:
required: true
- type: textarea
attributes:
label: Step-by-step reproduction instructions
validations:
required: true
6 changes: 6 additions & 0 deletions .github/actions/sync/issue_config.template.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
blank_issues_enabled: false

contact_links:
- name: Get help in GitHub Discussions
url: https://github.com/orgs/Homebrew/discussions
about: Have a question? Not sure if your issue affects everyone reproducibly? The quickest way to get help is on Homebrew's GitHub Discussions!
17 changes: 17 additions & 0 deletions .github/actions/sync/pull_request.template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
-----

<!-- Only tick a checkbox once you've done it; honesty keeps reviews smooth. -->
<!-- Tick with [x] before creating, or click the boxes afterwards. -->

- [ ] Have you checked for other open [Pull Requests](https://github.com/%<repository>s/pulls) for the same change?
- [ ] Have you explained what your changes do and why you'd like them included?
- [ ] Have you written new tests where it makes sense?
- [ ] Have you successfully run this repository's tests and checks locally?

-----

- [ ] I did not use AI/LLM to create this PR, or I disclosed the tool/model below and reviewed its output; I did not attribute commits to AI and will answer maintainer questions and review comments myself without AI/LLM.

<!-- If AI was used, explain below how it was used and how you verified the changes. Non-maintainers may only have one AI-assisted PR open at a time. See https://docs.brew.sh/Responsible-AI-Usage for guidance. -->

-----
34 changes: 34 additions & 0 deletions .github/actions/sync/shared-config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,16 @@ def safe_system(*args)
codeowners_header = "# This file is synced from the `.github` repository, do not modify it directly.\n"
# GitHub uses the first of these that exists and ignores the others.
codeowners_paths = [codeowners, "CODEOWNERS", "docs/CODEOWNERS"].freeze
pull_request_template = ".github/PULL_REQUEST_TEMPLATE.md"
template_sources = {
pull_request_template => ".github/actions/sync/pull_request.template.md",
".github/ISSUE_TEMPLATE/bug.yml" => ".github/actions/sync/issue_bug.template.yml",
".github/ISSUE_TEMPLATE/config.yml" => ".github/actions/sync/issue_config.template.yml",
}.freeze
# GitHub reads templates from any of these locations, case-insensitively.
# It no longer reads single-file `ISSUE_TEMPLATE.md` issue templates, only `ISSUE_TEMPLATE/` directories.
existing_pull_request_template = %r{\A(?:\.github/|docs/)?pull_request_template(?:\.\w+)?(?:/|\z)}i
existing_issue_template = %r{\A(?:\.github/|docs/)?issue_template/}i
vale_ini = ".vale.ini"
dependabot_template_yaml = ".github/actions/sync/dependabot.template.yml"
dependabot_yaml = ".github/dependabot.yml"
Expand Down Expand Up @@ -219,6 +229,12 @@ def safe_system(*args)

tracked_files = out.split("\0")

out, err, status = Open3.capture3("git", "-C", target_directory, "ls-files", "-z", "--others", "--exclude-standard")
raise err unless status.success?

# Local syncs can run on checkouts with uncommitted template changes, so look at the files that are actually there.
working_tree_files = (tracked_files + out.split("\0")).select { |path| (target_directory_path/path).exist? }

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
swift_manifests = ["Package.swift", "Package.resolved"].freeze
licenses_enabled ||= tracked_files.any? { |path| swift_manifests.include?(File.basename(path)) }

Expand All @@ -238,6 +254,7 @@ def safe_system(*args)
rubocop_yaml,
coderabbit_config_ts,
codeowners,
*template_sources.keys,
dependabot_yaml,
*license_check_paths,
check_template_rb,
Expand Down Expand Up @@ -450,6 +467,23 @@ def safe_system(*args)
next unless target_path.exist?

git "-C", target_directory, "rm", path
when *template_sources.keys
# A template here would become the default for every repository without one, including unsynced ones.
next if repository_name == ".github"

# Install templates once and then leave them to the repository, like any template it already has.
contents = format(Pathname(template_sources.fetch(path)).read, repository: "Homebrew/#{repository_name}")
if path == pull_request_template
own_templates = working_tree_files.grep(existing_pull_request_template) - [path]
# GitHub prefers `.github/`, so remove an unedited synced template once the repository adds its own elsewhere.
FileUtils.rm_f target_path if own_templates.any? && target_path.file? && target_path.read == contents
next if own_templates.any?
elsif working_tree_files.any?(existing_issue_template)
next
end
next if target_path.exist?

target_path.write(contents)
else
next if path == target_path.to_s

Expand Down
141 changes: 141 additions & 0 deletions .github/actions/sync/shared-config_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ def git(*args, directory: target)
successful_command("git", "-C", directory.to_s, *args).chomp
end

def tracked_templates(pattern)
git("ls-files").lines(chomp: true).grep(pattern)
end

def commit_fixture(directory: target)
git("add", "--all", directory:)
git("commit", "--quiet", "--message", "Fixture", directory:)
Expand Down Expand Up @@ -162,6 +166,19 @@ def synced_contents(path)
entries = own_codeowners.lines.reject { |line| line.include?("coderabbit*") }.join
"#{entries}/.coderabbit* @Homebrew/maintainers\n"
end
let(:pull_request_template) { ".github/PULL_REQUEST_TEMPLATE.md" }
let(:expected_templates) do
{
pull_request_template => "pull_request.template.md",
".github/ISSUE_TEMPLATE/bug.yml" => "issue_bug.template.yml",
".github/ISSUE_TEMPLATE/config.yml" => "issue_config.template.yml",
}.transform_values do |source|
format((source_repository/".github/actions/sync/#{source}").read,
repository: "Homebrew/#{File.basename(repository)}")
end
end
let(:issue_templates) { expected_templates.keys - [pull_request_template] }
let(:own_template) { "- [ ] Have you run `make test`?\n" }
let(:original_gemfile) { "source \"https://rubygems.org\"\n\nruby file: \".ruby-version\"\n\ngem \"rake\"\n" }
let(:custom_gemfile) { "source \"https://rubygems.org\"\n\ngem \"rake\"\n" }
let(:expected_gemfile) do
Expand Down Expand Up @@ -315,6 +332,130 @@ def synced_contents(path)
end
end

it "installs issue and pull request templates linking to the repository, repeatedly" do
commit_fixture
sync_target
expected_templates.each { |path, contents| expect((target/path).read).to eq(contents) }
expect((target/pull_request_template).read).to include("https://github.com/Homebrew/no-ruby/pulls")
expect((target/".github/ISSUE_TEMPLATE/bug.yml").read).to include("https://github.com/Homebrew/no-ruby/issues")
expect_idempotent_sync
end

it "keeps an installed template after the repository edits it" do
commit_fixture
sync_target
write pull_request_template, own_template
commit_fixture
expect_idempotent_sync
expect((target/pull_request_template).read).to eq(own_template)
end

it "removes an unedited pull request template once the repository adds its own elsewhere" do
commit_fixture
sync_target
write "PULL_REQUEST_TEMPLATE.md", own_template
commit_fixture
sync_target
expect(target/pull_request_template).not_to exist
expect(tracked_templates(/pull_request_template/i)).to eq(["PULL_REQUEST_TEMPLATE.md"])
expect_idempotent_sync
end

it "keeps the pull request template when the repository's own one is deleted but not committed" do
commit_fixture
sync_target
write "PULL_REQUEST_TEMPLATE.md", own_template
commit_fixture
FileUtils.rm target/"PULL_REQUEST_TEMPLATE.md"
sync_target
expect((target/pull_request_template).read).to eq(expected_templates.fetch(pull_request_template))
end

it "installs issue templates when the last one is deleted but not committed" do
write ".github/ISSUE_TEMPLATE/feature.yml", own_template
commit_fixture
FileUtils.rm target/".github/ISSUE_TEMPLATE/feature.yml"
sync_target
issue_templates.each do |issue_template|
expect((target/issue_template).read).to eq(expected_templates.fetch(issue_template))
end
end

{
".github/PULL_REQUEST_TEMPLATE.md" => /pull_request_template/i,
"PULL_REQUEST_TEMPLATE.md" => /pull_request_template/i,
".github/ISSUE_TEMPLATE/feature.yml" => /issue_template/i,
}.each do |path, pattern|
it "keeps an uncommitted #{path} without adding a shared template beside it" do
commit_fixture
write path, own_template
sync_target
expect((target/path).read).to eq(own_template)
expect(tracked_templates(pattern)).to eq([path])
end
end

%w[
.github/PULL_REQUEST_TEMPLATE.md
.github/pull_request_template.md
PULL_REQUEST_TEMPLATE.md
docs/pull_request_template.md
.github/PULL_REQUEST_TEMPLATE/bug_fix.md
].each do |path|
it "keeps a repository's own pull request template at #{path} and installs issue templates" do
write path, own_template
commit_fixture
sync_target
expect((target/path).read).to eq(own_template)
expect(tracked_templates(/pull_request_template/i)).to eq([path])
issue_templates.each do |issue_template|
expect((target/issue_template).read).to eq(expected_templates.fetch(issue_template))
end
end
end

%w[
.github/ISSUE_TEMPLATE/feature.yml
.github/ISSUE_TEMPLATE/config.yml
ISSUE_TEMPLATE/bug.md
docs/ISSUE_TEMPLATE/bug.md
].each do |path|
it "keeps a repository's own issue template at #{path} and installs the pull request template" do
write path, own_template
commit_fixture
sync_target
expect((target/path).read).to eq(own_template)
expect(tracked_templates(/issue_template/i)).to eq([path])
expect((target/pull_request_template).read).to eq(expected_templates.fetch(pull_request_template))
end
end

%w[
.github/issue_template.md
ISSUE_TEMPLATE.md
docs/ISSUE_TEMPLATE.md
].each do |path|
it "installs issue templates alongside the retired single-file template at #{path}" do
write path, own_template
commit_fixture
sync_target
expect((target/path).read).to eq(own_template)
issue_templates.each do |issue_template|
expect((target/issue_template).read).to eq(expected_templates.fetch(issue_template))
end
end
end

context "with the .github repository" do
let(:repository) { "source/.github" }

it "does not install templates" do
commit_fixture
sync_target
expected_templates.each_key { |path| expect(target/path).not_to exist }
end
end

%w[
Package.swift
Package.resolved
Expand Down
Loading