|
| 1 | +# frozen_string_literal: true |
| 2 | + |
| 3 | +# Released under the MIT License. |
| 4 | +# Copyright, 2026, by Samuel Williams. |
| 5 | + |
| 6 | +require "bake/gem/github/repository_context" |
| 7 | +require "bake/gem/github/project_client" |
| 8 | + |
| 9 | +describe Bake::Gem::GitHub::Project do |
| 10 | + include Bake::Gem::GitHub::RepositoryContext |
| 11 | + |
| 12 | + with "#prepare" do |
| 13 | + it "prepares, validates, and pushes a release before opening its PR" do |
| 14 | + result = isolated_project(<<~'RUBY') |
| 15 | + project = Bake::Gem::GitHub::ProjectClient.new(Dir.pwd) |
| 16 | + url = project.prepare(Bake::Context.load(Dir.pwd), "patch") |
| 17 | + {url: url, body: project.writes.first} |
| 18 | + RUBY |
| 19 | + expect(result[:url]).to be == "https://github.com/socketry/example/pull/42" |
| 20 | + expect(result[:body]).to be(:include?, "Release example 1.0.1.") |
| 21 | + expect(git("branch", "--show-current")).to be == "releases/v1.0.1" |
| 22 | + expect(git("rev-parse", "HEAD")).to be == git("rev-parse", "refs/heads/releases/v1.0.1", chdir: File.join(root, "remote")) |
| 23 | + expect(File.read(File.join(repository, "releases.md"))).to be(:include?, "## v1.0.1") |
| 24 | + expect(git("tag", "--list")).to be == "" |
| 25 | + git("checkout", "--quiet", "main") |
| 26 | + git("merge", "--quiet", "--no-ff", "releases/v1.0.1", "-m", "Merge release") |
| 27 | + git("push", "--quiet", "origin", "main") |
| 28 | + commit = git("rev-parse", "HEAD") |
| 29 | + project = Bake::Gem::GitHub::ProjectClient.new(repository) |
| 30 | + project.responses["repos/socketry/example/pulls/42"] = { |
| 31 | + "number" => 42, "merged" => true, "base" => {"ref" => "main", "repo" => {"full_name" => "socketry/example"}}, |
| 32 | + "merge_commit_sha" => commit, "merged_by" => {"login" => "maintainer"}, "html_url" => result[:url] |
| 33 | + } |
| 34 | + evidence = project.inspect_release(42) |
| 35 | + expect(evidence).to have_keys(name: be == "example", version: be == "1.0.1", commit: be == commit, merged_by: be == "maintainer") |
| 36 | + end |
| 37 | + |
| 38 | + it "reports an existing release PR without creating a branch" do |
| 39 | + url = isolated_project(<<~'RUBY') |
| 40 | + project = Bake::Gem::GitHub::ProjectClient.new(Dir.pwd) |
| 41 | + project.pulls = [{"headRefName" => "releases/v1.0.1", "url" => "existing"}] |
| 42 | + project.prepare(Bake::Context.load(Dir.pwd), "patch") |
| 43 | + RUBY |
| 44 | + expect(url).to be == "existing" |
| 45 | + expect(git("branch", "--show-current")).to be == "main" |
| 46 | + end |
| 47 | + |
| 48 | + it "refuses preparation from another branch" do |
| 49 | + git("checkout", "--quiet", "-b", "feature") |
| 50 | + expect do |
| 51 | + isolated_project('Bake::Gem::GitHub::ProjectClient.new(Dir.pwd).prepare(Bake::Context.load(Dir.pwd), "patch")') |
| 52 | + end.to raise_exception(RuntimeError, message: be =~ /Prepare releases from main/) |
| 53 | + end |
| 54 | + |
| 55 | + it "refuses preparation when the local default branch differs from origin" do |
| 56 | + git("commit", "--quiet", "--allow-empty", "-m", "Local change") |
| 57 | + expect do |
| 58 | + isolated_project('Bake::Gem::GitHub::ProjectClient.new(Dir.pwd).prepare(Bake::Context.load(Dir.pwd), "patch")') |
| 59 | + end.to raise_exception(RuntimeError, message: be =~ /differs from origin/) |
| 60 | + end |
| 61 | + end |
| 62 | + |
| 63 | + with "#merged and #inspect_release" do |
| 64 | + let(:project) {Bake::Gem::GitHub::ProjectClient.new(repository)} |
| 65 | + let(:pull) {{"merged" => true, "base" => {"ref" => "main", "repo" => {"full_name" => "socketry/example"}}, "merge_commit_sha" => git("rev-parse", "HEAD")}} |
| 66 | + |
| 67 | + before do |
| 68 | + project.responses["repos/socketry/example/pulls/42"] = pull |
| 69 | + end |
| 70 | + |
| 71 | + it "accepts a merged commit in the remote default branch history" do |
| 72 | + git("commit", "--quiet", "--allow-empty", "-m", "Later development") |
| 73 | + git("push", "--quiet", "origin", "main") |
| 74 | + expect(project.merged(42)).to be == pull |
| 75 | + end |
| 76 | + |
| 77 | + it "rejects a commit outside the remote default branch history" do |
| 78 | + git("checkout", "--quiet", "-b", "unmerged") |
| 79 | + git("commit", "--quiet", "--allow-empty", "-m", "Unmerged change") |
| 80 | + pull["merge_commit_sha"] = git("rev-parse", "HEAD") |
| 81 | + expect{project.merged(42)}.to raise_exception(Bake::Gem::CommandExecutionError) |
| 82 | + end |
| 83 | + |
| 84 | + it "rejects invalid commit identifiers" do |
| 85 | + pull["merge_commit_sha"] = "--help" |
| 86 | + expect{project.merged(42)}.to raise_exception(RuntimeError, message: be =~ /Invalid merged commit/) |
| 87 | + end |
| 88 | + |
| 89 | + it "does not publish ordinary merged changes" do |
| 90 | + File.write(File.join(repository, "readme.md"), "An ordinary change.\n") |
| 91 | + git("add", "readme.md") |
| 92 | + git("commit", "--quiet", "-m", "Documentation") |
| 93 | + git("push", "--quiet", "origin", "main") |
| 94 | + pull["merge_commit_sha"] = git("rev-parse", "HEAD") |
| 95 | + expect(project.inspect_release(42)).to be_nil |
| 96 | + end |
| 97 | + end |
| 98 | + |
| 99 | + with "#doctor and #apply" do |
| 100 | + let(:project) {Bake::Gem::GitHub::ProjectClient.new(repository)} |
| 101 | + let(:rules) {[]} |
| 102 | + |
| 103 | + before do |
| 104 | + project.responses["repos/socketry/example/rulesets?per_page=100"] = rules |
| 105 | + project.responses["repos/socketry/example/environments"] = {"environments" => []} |
| 106 | + end |
| 107 | + |
| 108 | + it "reports desired and observed settings without writing" do |
| 109 | + expect(Bake::Gem::GitHub::Project).to receive(:new).with(repository).and_return(project) |
| 110 | + result = Bake::Context.load(repository).call("gem:github:setup:plan") |
| 111 | + expect(result[:desired_rules].keys).to be == %w[reviews checks history tags] |
| 112 | + expect(result[:existing_rules]).to be == [] |
| 113 | + expect(result[:trusted_publisher][:workflow_filename]).to be == "release-publish.yaml" |
| 114 | + expect(project.writes).to be == [] |
| 115 | + end |
| 116 | + |
| 117 | + it "creates missing managed rulesets while preserving unrelated rulesets" do |
| 118 | + rules << {"id" => 123, "name" => "Unrelated policy"} |
| 119 | + expect(Bake::Gem::GitHub::Project).to receive(:new).with(repository).and_return(project) |
| 120 | + Bake::Context.load(repository).call("gem:github:setup:apply") |
| 121 | + expect(project.writes.map{|rule| rule.fetch("name")}).to be == ["Gem release reviews", "Gem release checks", "Gem release history", "Gem release tags"] |
| 122 | + expect(project.requests.drop(1).all?{|request| request.include?("POST")}).to be == true |
| 123 | + end |
| 124 | + |
| 125 | + it "updates existing managed rulesets by ID" do |
| 126 | + Bake::Gem::GitHub::Setup.rules(project.config).values.each_with_index do |rule, index| |
| 127 | + rules << {"id" => index + 1, "name" => rule.fetch(:name)} |
| 128 | + end |
| 129 | + project.apply |
| 130 | + expect(project.requests.drop(1).map{|request| request[2]}).to be == (1..4).map{|id| "repos/socketry/example/rulesets/#{id}"} |
| 131 | + expect(project.requests.drop(1).all?{|request| request.include?("PUT")}).to be == true |
| 132 | + end |
| 133 | + |
| 134 | + it "refuses ambiguous managed rulesets" do |
| 135 | + rules.concat([{"id" => 1, "name" => "Gem release reviews"}, {"id" => 2, "name" => "Gem release reviews"}]) |
| 136 | + expect{project.apply}.to raise_exception(RuntimeError, message: be =~ /Multiple rulesets/) |
| 137 | + expect(project.writes).to be == [] |
| 138 | + end |
| 139 | + end |
| 140 | +end |
0 commit comments