Skip to content
Open
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
5 changes: 4 additions & 1 deletion app/commands/github/team_member/create.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ class Github::TeamMember::Create

def call
user.github_team_memberships.find_or_create_by!(team_name:).tap do |team_member|
User::UpdateMaintainer.(user) if team_member.previously_new_record? && team_member.track_id
next unless team_member.previously_new_record? && team_member.track_id

User::UpdateMaintainer.(user)
Track::UpdateGithubMaintenanceStatus.defer(team_member.track)
end
end
end
5 changes: 4 additions & 1 deletion app/commands/github/team_member/destroy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ class Github::TeamMember::Destroy

def call
team_member.delete
User::UpdateMaintainer.(team_member.user) if team_member.track_id
return unless team_member.track_id

User::UpdateMaintainer.(team_member.user)
Track::UpdateGithubMaintenanceStatus.defer(team_member.track)
end
end
43 changes: 43 additions & 0 deletions app/commands/track/update_github_maintenance_status.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Keeps a track repo's GitHub `maintained`/`unmaintained` topic in sync with
# whether the track has any maintainers on its GitHub team. Those topics drive
# the automated "this repo is unmaintained" PR comments (see the
# ping-cross-track-maintainers-team workflow), so promoting a track the moment
# it gains a maintainer silences them without any manual intervention.
#
# We only ever toggle the plain `maintained` <-> `unmaintained` pair. The
# nuanced categories (`wip-track`, `maintained-autonomous`, `maintained-solitary`)
# are deliberate admin choices, so we leave them untouched.
class Track::UpdateGithubMaintenanceStatus
include Mandate

initialize_with :track

def call
return if Rails.env.development?
return unless new_topic

Exercism.octokit_client.replace_all_topics(repo, (current_topics - TOGGLEABLE) + [new_topic])
end

private
memoize
def new_topic
if maintained?
MAINTAINED if current_topics.include?(UNMAINTAINED)
elsif current_topics.include?(MAINTAINED)
UNMAINTAINED
end
end

def maintained? = track.github_team_members.exists?

memoize
def current_topics = Exercism.octokit_client.topics(repo).names

def repo = "exercism/#{track.slug}"

MAINTAINED = "maintained".freeze
UNMAINTAINED = "unmaintained".freeze
TOGGLEABLE = [MAINTAINED, UNMAINTAINED].freeze
private_constant :MAINTAINED, :UNMAINTAINED, :TOGGLEABLE
end
7 changes: 5 additions & 2 deletions test/commands/github/team_member/create_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ class Github::TeamMember::CreateTest < ActiveSupport::TestCase
assert_equal team_name, team_name_member.team_name
end

test "update maintainer role" do
test "update maintainer role and sync maintenance status" do
github_uid = '137131'
team_name = 'fsharp'

create(:track, slug: team_name)
track = create(:track, slug: team_name)
user = create(:user, uid: github_uid)
User::UpdateMaintainer.expects(:call).with(user).once
Track::UpdateGithubMaintenanceStatus.expects(:defer).with(track).once

team_name_member = Github::TeamMember::Create.(user, team_name)

Expand All @@ -34,6 +35,7 @@ class Github::TeamMember::CreateTest < ActiveSupport::TestCase
user = create(:user, uid: github_uid)

User::UpdateMaintainer.expects(:call).with(user).never
Track::UpdateGithubMaintenanceStatus.expects(:defer).never

Github::TeamMember::Create.(user, team_name)
end
Expand All @@ -45,6 +47,7 @@ class Github::TeamMember::CreateTest < ActiveSupport::TestCase
create(:github_team_member, team_name:, user:)

User::UpdateMaintainer.expects(:call).with(user).never
Track::UpdateGithubMaintenanceStatus.expects(:defer).never

Github::TeamMember::Create.(user, team_name)
end
Expand Down
6 changes: 4 additions & 2 deletions test/commands/github/team_member/destroy_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,16 @@ class Github::TeamMember::DestroyTest < ActiveSupport::TestCase
refute Github::TeamMember.where(user:, team_name:).exists?
end

test "update maintainer role when track team" do
test "update maintainer role and sync maintenance status when track team" do
github_uid = '137131'
team_name = 'fsharp'

user = create(:user, uid: github_uid)
create(:track, slug: team_name)
track = create(:track, slug: team_name)
team_member = create(:github_team_member, team_name:, user:)

User::UpdateMaintainer.expects(:call).with(user).once
Track::UpdateGithubMaintenanceStatus.expects(:defer).with(track).once

Github::TeamMember::Destroy.(team_member)
end
Expand All @@ -34,6 +35,7 @@ class Github::TeamMember::DestroyTest < ActiveSupport::TestCase
team_member = create(:github_team_member, team_name:, user:)

User::UpdateMaintainer.expects(:call).never
Track::UpdateGithubMaintenanceStatus.expects(:defer).never

Github::TeamMember::Destroy.(team_member)
end
Expand Down
79 changes: 79 additions & 0 deletions test/commands/track/update_github_maintenance_status_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
require "test_helper"

class Track::UpdateGithubMaintenanceStatusTest < ActiveSupport::TestCase
test "promotes unmaintained repo to maintained when track has a maintainer" do
track = create(:track, slug: 'fortran')
create(:github_team_member, team_name: 'fortran')

stub_topics('fortran', %w[exercism-track community-contributions-paused unmaintained])
put = stub_replace_topics('fortran', %w[exercism-track community-contributions-paused maintained])

Track::UpdateGithubMaintenanceStatus.(track)

assert_requested put
end

test "demotes maintained repo to unmaintained when track has no maintainers" do
track = create(:track, slug: 'fortran')

stub_topics('fortran', %w[exercism-track maintained])
put = stub_replace_topics('fortran', %w[exercism-track unmaintained])

Track::UpdateGithubMaintenanceStatus.(track)

assert_requested put
end

test "does nothing when already maintained and has a maintainer" do
track = create(:track, slug: 'fortran')
create(:github_team_member, team_name: 'fortran')

stub_topics('fortran', %w[exercism-track maintained])

Track::UpdateGithubMaintenanceStatus.(track)
# No PUT stubbed, so WebMock raises if replace_all_topics is called
end

test "does nothing when already unmaintained and has no maintainers" do
track = create(:track, slug: 'fortran')

stub_topics('fortran', %w[exercism-track unmaintained])

Track::UpdateGithubMaintenanceStatus.(track)
end

test "leaves nuanced maintained categories untouched" do
track = create(:track, slug: 'fortran')
# No team members, but the track is deliberately labelled solitary/autonomous
%w[maintained-solitary maintained-autonomous wip-track].each do |category|
stub_topics('fortran', ['exercism-track', category])

Track::UpdateGithubMaintenanceStatus.(track)
# No PUT stubbed for any of these, so WebMock raises on a write
end
end

test "does nothing in development" do
track = create(:track, slug: 'fortran')
Rails.env.stubs(:development?).returns(true)

Track::UpdateGithubMaintenanceStatus.(track)
# No API calls stubbed - WebMock raises if any are made
end

private
def stub_topics(slug, names)
stub_request(:get, %r{https://api\.github\.com/repos/exercism/#{slug}/topics}).
to_return(
status: 200,
body: { names: }.to_json,
headers: { 'Content-Type': 'application/json' }
)
end

def stub_replace_topics(slug, expected_names)
stub_request(:put, "https://api.github.com/repos/exercism/#{slug}/topics").
with { |request| JSON.parse(request.body)["names"] == expected_names }.
to_return(status: 200, body: { names: expected_names }.to_json, headers: { 'Content-Type': 'application/json' })
end
end
Loading