Skip to content

Commit d3779ec

Browse files
Follow Sus test layout and add standard coverage workflows
1 parent 5b37729 commit d3779ec

14 files changed

Lines changed: 311 additions & 108 deletions

File tree

‎.github/release-rules/checks.json‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,13 @@
3737
"context": "RuboCop"
3838
},
3939
{
40-
"context": "Documentation coverage"
40+
"context": "ruby on ubuntu"
41+
},
42+
{
43+
"context": "ruby on macos"
44+
},
45+
{
46+
"context": "validate"
4147
},
4248
{
4349
"context": "Release validation"
Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
1-
name: Documentation coverage
1+
name: Documentation Coverage
22

33
on: [push, pull_request]
44

55
permissions:
66
contents: read
77

88
env:
9+
COVERAGE: PartialSummary
910
BUNDLE_WITH: maintenance
1011

1112
jobs:
1213
validate:
13-
name: Documentation coverage
1414
runs-on: ubuntu-latest
15+
1516
steps:
16-
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
17-
- uses: ruby/setup-ruby@a0102e0972be65f351c307e2d64b9314a57c8073 # v1
18-
with:
19-
ruby-version: "3.4"
20-
bundler-cache: true
21-
- name: Check library documentation
22-
timeout-minutes: 5
23-
run: bundle exec bake decode:index:coverage lib
17+
- uses: actions/checkout@v7
18+
- uses: ruby/setup-ruby@v1
19+
with:
20+
ruby-version: ruby
21+
bundler-cache: true
22+
23+
- name: Validate coverage
24+
timeout-minutes: 5
25+
run: bundle exec bake decode:index:coverage lib
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
name: Test Coverage
2+
3+
on: [push, pull_request]
4+
5+
permissions:
6+
contents: read
7+
8+
env:
9+
COVERAGE: PartialSummary
10+
11+
jobs:
12+
test:
13+
name: ${{matrix.ruby}} on ${{matrix.os}}
14+
runs-on: ${{matrix.os}}-latest
15+
16+
strategy:
17+
matrix:
18+
os:
19+
- ubuntu
20+
- macos
21+
22+
ruby:
23+
- ruby
24+
25+
steps:
26+
- uses: actions/checkout@v7
27+
- uses: ruby/setup-ruby@v1
28+
with:
29+
ruby-version: ${{matrix.ruby}}
30+
bundler-cache: true
31+
32+
- name: Run tests
33+
timeout-minutes: 5
34+
run: bundle exec bake test
35+
36+
- uses: actions/upload-artifact@v7
37+
with:
38+
include-hidden-files: true
39+
if-no-files-found: error
40+
name: coverage-${{matrix.os}}-${{matrix.ruby}}
41+
path: .covered.db
42+
43+
validate:
44+
needs: test
45+
runs-on: ubuntu-latest
46+
47+
steps:
48+
- uses: actions/checkout@v7
49+
- uses: ruby/setup-ruby@v1
50+
with:
51+
ruby-version: ruby
52+
bundler-cache: true
53+
54+
- uses: actions/download-artifact@v8
55+
56+
- name: Validate coverage
57+
timeout-minutes: 5
58+
run: bundle exec bake covered:validate --paths */.covered.db \;

‎config/release.yaml‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ checks:
77
- Test Ruby 3.4
88
- Test Ruby 4.0
99
- RuboCop
10-
- Documentation coverage
10+
- ruby on ubuntu
11+
- ruby on macos
12+
- validate
1113
- Release validation
1214
approvals: 2
1315
signing: true

‎fixtures/bake/gem/github/repository_context.rb‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33
# Released under the MIT License.
44
# Copyright, 2026, by Samuel Williams.
55

6+
require "sus/shared"
67
require "sus/fixtures/temporary_directory_context"
78
require "sus/fixtures/isolated_ruby_context"
89
require "bake/gem/github/setup"
910

1011
module Bake
1112
module Gem
1213
module GitHub
13-
module RepositoryContext
14+
RepositoryContext = Sus::Shared("a gem repository") do
1415
include Sus::Fixtures::TemporaryDirectoryContext
1516
include Sus::Fixtures::IsolatedRubyContext
1617

@@ -24,8 +25,7 @@ def git(*arguments, chdir: repository)
2425
output.strip
2526
end
2627

27-
def before
28-
super
28+
before do
2929
FileUtils.mkdir_p(File.join(repository, "lib/example"))
3030
File.write(File.join(repository, "lib/example/version.rb"), "module Example; VERSION = \"1.0.0\"; end\n")
3131
File.write(File.join(repository, "example.gemspec"), <<~RUBY)

‎gems.rb‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
gemspec
66

77
group :test do
8+
gem "bake-test"
89
gem "covered"
910
gem "sus"
1011
gem "rubocop"
Lines changed: 55 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -9,56 +9,58 @@
99
describe Bake::Gem::GitHub::Project do
1010
include Bake::Gem::GitHub::RepositoryContext
1111

12-
it "prepares, validates, and pushes a release before opening its PR" do
13-
result = isolated_project(<<~'RUBY')
14-
project = Bake::Gem::GitHub::ProjectClient.new(Dir.pwd)
15-
url = project.prepare(Bake::Context.load(Dir.pwd), "patch")
16-
{url: url, body: project.writes.first}
17-
RUBY
18-
expect(result[:url]).to be == "https://github.com/socketry/example/pull/42"
19-
expect(result[:body]).to be(:include?, "Release example 1.0.1.")
20-
expect(git("branch", "--show-current")).to be == "releases/v1.0.1"
21-
expect(git("rev-parse", "HEAD")).to be == git("rev-parse", "refs/heads/releases/v1.0.1", chdir: File.join(root, "remote"))
22-
expect(File.read(File.join(repository, "releases.md"))).to be(:include?, "## v1.0.1")
23-
expect(git("tag", "--list")).to be == ""
24-
git("checkout", "--quiet", "main")
25-
git("merge", "--quiet", "--no-ff", "releases/v1.0.1", "-m", "Merge release")
26-
git("push", "--quiet", "origin", "main")
27-
commit = git("rev-parse", "HEAD")
28-
project = Bake::Gem::GitHub::ProjectClient.new(repository)
29-
project.responses["repos/socketry/example/pulls/42"] = {
30-
"number" => 42, "merged" => true, "base" => {"ref" => "main", "repo" => {"full_name" => "socketry/example"}},
31-
"merge_commit_sha" => commit, "merged_by" => {"login" => "maintainer"}, "html_url" => result[:url]
32-
}
33-
evidence = project.inspect_release(42)
34-
expect(evidence).to have_keys(name: be == "example", version: be == "1.0.1", commit: be == commit, merged_by: be == "maintainer")
35-
end
36-
37-
it "reports an existing release PR without creating a branch" do
38-
url = isolated_project(<<~'RUBY')
39-
project = Bake::Gem::GitHub::ProjectClient.new(Dir.pwd)
40-
project.pulls = [{"headRefName" => "releases/v1.0.1", "url" => "existing"}]
41-
project.prepare(Bake::Context.load(Dir.pwd), "patch")
42-
RUBY
43-
expect(url).to be == "existing"
44-
expect(git("branch", "--show-current")).to be == "main"
45-
end
46-
47-
it "refuses preparation from another branch" do
48-
git("checkout", "--quiet", "-b", "feature")
49-
expect do
50-
isolated_project('Bake::Gem::GitHub::ProjectClient.new(Dir.pwd).prepare(Bake::Context.load(Dir.pwd), "patch")')
51-
end.to raise_exception(RuntimeError, message: be =~ /Prepare releases from main/)
52-
end
53-
54-
it "refuses preparation when the local default branch differs from origin" do
55-
git("commit", "--quiet", "--allow-empty", "-m", "Local change")
56-
expect do
57-
isolated_project('Bake::Gem::GitHub::ProjectClient.new(Dir.pwd).prepare(Bake::Context.load(Dir.pwd), "patch")')
58-
end.to raise_exception(RuntimeError, message: be =~ /differs from origin/)
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
5961
end
6062

61-
with "merged source verification" do
63+
with "#merged and #inspect_release" do
6264
let(:project) {Bake::Gem::GitHub::ProjectClient.new(repository)}
6365
let(:pull) {{"merged" => true, "base" => {"ref" => "main", "repo" => {"full_name" => "socketry/example"}}, "merge_commit_sha" => git("rev-parse", "HEAD")}}
6466

@@ -94,7 +96,7 @@
9496
end
9597
end
9698

97-
with "repository policy" do
99+
with "#doctor and #apply" do
98100
let(:project) {Bake::Gem::GitHub::ProjectClient.new(repository)}
99101
let(:rules) {[]}
100102

@@ -104,7 +106,8 @@
104106
end
105107

106108
it "reports desired and observed settings without writing" do
107-
result = project.doctor
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")
108111
expect(result[:desired_rules].keys).to be == %w[reviews checks history tags]
109112
expect(result[:existing_rules]).to be == []
110113
expect(result[:trusted_publisher][:workflow_filename]).to be == "release-publish.yaml"
@@ -113,7 +116,8 @@
113116

114117
it "creates missing managed rulesets while preserving unrelated rulesets" do
115118
rules << {"id" => 123, "name" => "Unrelated policy"}
116-
project.apply
119+
expect(Bake::Gem::GitHub::Project).to receive(:new).with(repository).and_return(project)
120+
Bake::Context.load(repository).call("gem:github:setup:apply")
117121
expect(project.writes.map{|rule| rule.fetch("name")}).to be == ["Gem release reviews", "Gem release checks", "Gem release history", "Gem release tags"]
118122
expect(project.requests.drop(1).all?{|request| request.include?("POST")}).to be == true
119123
end
Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# Released under the MIT License.
44
# Copyright, 2026, by Samuel Williams.
55

6-
require_relative "../lib/bake/gem/github/publisher"
6+
require "bake/gem/github/publisher"
77
require "open3"
88
require "sus/fixtures/temporary_directory_context"
99
require "sus/fixtures/isolated_ruby_context"
@@ -12,8 +12,11 @@
1212
include Sus::Fixtures::TemporaryDirectoryContext
1313
include Sus::Fixtures::IsolatedRubyContext
1414

15+
let(:publisher) {subject.new(root)}
16+
let(:commit) {git("rev-parse", "HEAD")}
17+
1518
def git(*arguments)
16-
output, status = Open3.capture2e("git", *arguments, chdir: @root)
19+
output, status = Open3.capture2e("git", *arguments, chdir: root)
1720
raise output unless status.success?
1821
output.strip
1922
end
@@ -26,41 +29,40 @@ def git(*arguments)
2629
git("config", "user.email", "test@example.com")
2730
git("add", "--all")
2831
git("commit", "-m", "Initial source")
29-
@commit = git("rev-parse", "HEAD")
30-
@publisher = subject.new(root)
3132
end
3233

3334
it "detects a changed retained artifact" do
34-
FileUtils.mkdir_p(File.join(@root, "pkg"))
35-
File.write(File.join(@root, "pkg", "example-1.0.1.gem"), "Original")
35+
FileUtils.mkdir_p(File.join(root, "pkg"))
36+
File.write(File.join(root, "pkg", "example-1.0.1.gem"), "Original")
3637
receipt = {file: "example-1.0.1.gem", sha256: Digest::SHA256.hexdigest("Original")}
37-
File.write(File.join(@root, "pkg", "release.json"), JSON.generate(receipt))
38-
expect(@publisher.load_receipt).to be == receipt
39-
File.write(File.join(@root, "pkg", "example-1.0.1.gem"), "Changed")
40-
expect{@publisher.load_receipt}.to raise_exception(RuntimeError, message: be =~ /digest mismatch/)
38+
File.write(File.join(root, "pkg", "release.json"), JSON.generate(receipt))
39+
expect(publisher.load_receipt).to be == receipt
40+
File.write(File.join(root, "pkg", "example-1.0.1.gem"), "Changed")
41+
expect{publisher.load_receipt}.to raise_exception(RuntimeError, message: be =~ /digest mismatch/)
4142
end
4243

4344
it "rejects an artifact path outside pkg" do
44-
FileUtils.mkdir_p(File.join(@root, "pkg"))
45-
File.write(File.join(@root, "pkg", "release.json"), JSON.generate(file: "../example.gem"))
46-
expect{@publisher.load_receipt}.to raise_exception(RuntimeError, message: be =~ /filename/)
45+
FileUtils.mkdir_p(File.join(root, "pkg"))
46+
File.write(File.join(root, "pkg", "release.json"), JSON.generate(file: "../example.gem"))
47+
expect{publisher.load_receipt}.to raise_exception(RuntimeError, message: be =~ /filename/)
4748
end
4849

4950
it "rejects existing tags which name a different commit" do
51+
commit
5052
git("tag", "v1.0.1")
5153
git("commit", "--allow-empty", "-m", "Later source")
52-
expect{@publisher.guard_tag("v1.0.1", git("rev-parse", "HEAD"))}.to raise_exception(RuntimeError, message: be =~ /another commit/)
53-
expect(git("rev-parse", "v1.0.1")).to be == @commit
54+
expect{publisher.guard_tag("v1.0.1", git("rev-parse", "HEAD"))}.to raise_exception(RuntimeError, message: be =~ /another commit/)
55+
expect(git("rev-parse", "v1.0.1")).to be == commit
5456
end
5557

5658
it "rejects unmerged PRs before any git or publishing command" do
57-
expect(@publisher).to receive(:api).with("pulls/42").and_return({"merged" => false})
58-
expect{@publisher.merged(42)}.to raise_exception(RuntimeError, message: be =~ /must be merged/)
59+
expect(publisher).to receive(:api).with("pulls/42").and_return({"merged" => false})
60+
expect{publisher.merged(42)}.to raise_exception(RuntimeError, message: be =~ /must be merged/)
5961
end
6062

6163
it "rejects a PR merged into another branch" do
62-
expect(@publisher).to receive(:api).with("pulls/42").and_return({"merged" => true, "base" => {"ref" => "other"}})
63-
expect{@publisher.merged(42)}.to raise_exception(RuntimeError, message: be =~ /configured branch/)
64+
expect(publisher).to receive(:api).with("pulls/42").and_return({"merged" => true, "base" => {"ref" => "other"}})
65+
expect{publisher.merged(42)}.to raise_exception(RuntimeError, message: be =~ /configured branch/)
6466
end
6567

6668
it "certificate-signs committed source and excludes dirty checkout content" do
@@ -73,9 +75,9 @@ def git(*arguments)
7375
certificate.not_before = Time.now - 60
7476
certificate.not_after = Time.now + 3600
7577
certificate.sign(key, OpenSSL::Digest.new("SHA256"))
76-
File.write(File.join(@root, "release.cert"), certificate.to_pem)
77-
File.write(File.join(@root, "example.rb"), "ORIGINAL")
78-
File.write(File.join(@root, "example.gemspec"), <<~RUBY)
78+
File.write(File.join(root, "release.cert"), certificate.to_pem)
79+
File.write(File.join(root, "example.rb"), "ORIGINAL")
80+
File.write(File.join(root, "example.gemspec"), <<~RUBY)
7981
Gem::Specification.new do |spec|
8082
spec.name = "example"
8183
spec.version = "1.0.1"
@@ -87,7 +89,7 @@ def git(*arguments)
8789
RUBY
8890
git("add", "--all")
8991
git("commit", "-m", "Gem source and public certificate")
90-
File.write(File.join(@root, "example.rb"), "DIRTY")
92+
File.write(File.join(root, "example.rb"), "DIRTY")
9193
result = isolated_ruby(<<~'RUBY', chdir: root, env: {"GEM_SIGNING_KEY" => key.to_pem}, requires: ["bundler/setup", "bake/gem/github/publisher"])
9294
publisher = Bake::Gem::GitHub::Publisher.new(Dir.pwd)
9395
publisher.config["signing"] = true

0 commit comments

Comments
 (0)