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
13 changes: 13 additions & 0 deletions lib/ghb/repository_configurator.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true

require 'json'
require 'psych'
require 'uri'

require_relative 'github_api_client'
Expand Down Expand Up @@ -50,6 +51,18 @@ def configure_branch_protection(github_client, repo_url, current_protection, pro
# Add Vercel check if Next.js project
@required_status_checks << 'Vercel' if File.exist?('package.json') && File.read('package.json').include?('"next"')

# Add checks for a hand-maintained .github/workflows/smoke.yml. That
# workflow is intentionally NOT generated (e.g. ci-actions smoke-tests
# its own composite actions), so its jobs are absent from
# @required_status_checks; discover them here so they stay required
# across regenerations. Job names are read dynamically so renaming a
# smoke job needs no generator change.
smoke_workflow = '.github/workflows/smoke.yml'
if File.exist?(smoke_workflow)
smoke_jobs = (Psych.safe_load(File.read(smoke_workflow)) || {})['jobs'] || {}
smoke_jobs.each { |job_id, job| @required_status_checks << ((job && job['name']) || job_id.to_s) }
end

# Check for CodeQL default setup
codeql_response = github_client.get("#{repo_url}/code-scanning/default-setup", expected_codes: nil)

Expand Down
78 changes: 78 additions & 0 deletions spec/ghb/repository_configurator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
allow(Dir).to(receive(:pwd).and_return("/home/user/#{repository}"))
allow(GHB::GitHubAPIClient).to(receive(:new).with(github_token).and_return(github_client))
allow(File).to(receive(:exist?).with('package.json').and_return(false))
allow(File).to(receive(:exist?).with('.github/workflows/smoke.yml').and_return(false))
allow(Dir).to(receive(:exist?).with('ci_scripts').and_return(false))
end

Expand Down Expand Up @@ -599,6 +600,83 @@
end
end

context 'when .github/workflows/smoke.yml exists (smoke-test detection)' do # rubocop:disable RSpec/MultipleMemoizedHelpers
let(:repo_info_response) do
instance_double(HTTParty::Response, code: 200, body: { private: false }.to_json)
end

let(:protection_response) do
instance_double(HTTParty::Response, code: 404, body: '{"message":"Not Found"}')
end

let(:codeql_default_setup_response) do
instance_double(HTTParty::Response, code: 200, body: { state: 'not-configured' }.to_json)
end

let(:codeql_get_response) do
instance_double(HTTParty::Response, code: 200, body: { state: 'not-configured' }.to_json)
end

let(:ok_response) do
instance_double(HTTParty::Response, code: 200, body: '{}')
end

let(:accepted_response) do
instance_double(HTTParty::Response, code: 202, body: '{}')
end

let(:smoke_yaml) do
<<~YAML
---
name: Smoke
'on': [push]
jobs:
action-contracts:
name: Action contracts (all 28)
linters-smoke:
name: Linter actions (disabled path)
variables-smoke:
YAML
end

before do
allow(File).to(receive(:exist?).with('.github/workflows/smoke.yml').and_return(true))
allow(File).to(receive(:read).with('.github/workflows/smoke.yml').and_return(smoke_yaml))

allow(github_client).to(receive(:get).with(repo_url).and_return(repo_info_response))
allow(github_client).to(receive(:get).with("#{repo_url}/branches/#{default_branch}/protection", expected_codes: [200, 404]).and_return(protection_response))
allow(github_client).to(receive(:get).with("#{repo_url}/code-scanning/default-setup", expected_codes: nil).and_return(codeql_default_setup_response))
allow(github_client).to(receive(:put).with("#{repo_url}/branches/#{default_branch}/protection", body: anything).and_return(ok_response))
allow(github_client).to(receive(:post).with("#{repo_url}/branches/#{default_branch}/protection/required_signatures", expected_codes: [200, 204]).and_return(ok_response))
allow(github_client).to(receive(:put).with("#{repo_url}/vulnerability-alerts", expected_codes: [200, 204]).and_return(ok_response))
allow(github_client).to(receive(:put).with("#{repo_url}/automated-security-fixes", expected_codes: [200, 204]).and_return(ok_response))
allow(github_client).to(receive(:patch).with(repo_url, body: anything).and_return(ok_response))
allow(github_client).to(receive(:get).with("#{repo_url}/code-scanning/default-setup").and_return(codeql_get_response))
allow(github_client).to(receive(:patch).with("#{repo_url}/code-scanning/default-setup", body: anything, expected_codes: [200, 202]).and_return(accepted_response))
end

it 'adds each smoke job (by name, falling back to job id) to the expected checks' do # rubocop:disable RSpec/ExampleLength
configurator.configure

expect(github_client).to(
have_received(:put).with(
"#{repo_url}/branches/#{default_branch}/protection",
body: hash_including(
required_status_checks: hash_including(
checks: [
{ context: 'Build', app_id: nil },
{ context: 'Lint', app_id: nil },
{ context: 'Action contracts (all 28)', app_id: nil },
{ context: 'Linter actions (disabled path)', app_id: nil },
{ context: 'variables-smoke', app_id: nil }
]
)
)
)
)
end
end

context 'when ci_scripts directory exists with no existing protection' do # rubocop:disable RSpec/MultipleMemoizedHelpers
let(:repo_info_response) do
instance_double(HTTParty::Response, code: 200, body: { private: false }.to_json)
Expand Down
Loading