From 1e00e582ebeb46cf81fd8e08198fa28e34046bbf Mon Sep 17 00:00:00 2001 From: Nathen Harvey Date: Fri, 30 Oct 2015 09:38:21 -0400 Subject: [PATCH] A MAINTAINERS file lists the maintainers of the prject * MAINTAINERS.toml is the source of record * MAINTAINERS.md is generated using a new rake task * New rake task added for generating the maintainers markdown file * New rake task for synchronizing the github team --- MAINTAINERS.md | 28 ++++++ MAINTAINERS.toml | 42 +++++++++ Rakefile | 1 + tasks/maintainers.rb | 212 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 283 insertions(+) create mode 100644 MAINTAINERS.md create mode 100644 MAINTAINERS.toml create mode 100644 tasks/maintainers.rb diff --git a/MAINTAINERS.md b/MAINTAINERS.md new file mode 100644 index 000000000..10c7c15c0 --- /dev/null +++ b/MAINTAINERS.md @@ -0,0 +1,28 @@ + + + + +# Maintainers + +This file lists how the InSpec project is maintained. When making changes to the +system, this file tells you who needs to review your patch - you need at least +two maintainers to provide a :+1: on your pull request. Additionally, you need +to not receive a veto from a Lieutenant or the Project Lead. +Check out [How Chef is Maintained](https://github.com/opscode/chef-rfc/blob/master/rfc030-maintenance-policy.md#how-the-project-is-maintained) +for details on the process, how to become a maintainer, lieutenant, or the +project lead. + +## Components + +## InSpec + + Handles the [InSpec](https://github.com/chef/inspec) toolset. + +### Lieutenant + +* [Dominik Richter](https://github.com/arlimus) + +### Maintainers + +* [Christoph Hartmann](https://github.com/chris-rock) +* [Jason Reed](https://github.com/jcreedcmu) diff --git a/MAINTAINERS.toml b/MAINTAINERS.toml new file mode 100644 index 000000000..056540cba --- /dev/null +++ b/MAINTAINERS.toml @@ -0,0 +1,42 @@ +[Preamble] + title = "Maintainers" + text = """ +This file lists how the InSpec project is maintained. When making changes to the +system, this file tells you who needs to review your patch - you need at least +two maintainers to provide a :+1: on your pull request. Additionally, you need +to not receive a veto from a Lieutenant or the Project Lead. +Check out [How Chef is Maintained](https://github.com/opscode/chef-rfc/blob/master/rfc030-maintenance-policy.md#how-the-project-is-maintained) +for details on the process, how to become a maintainer, lieutenant, or the +project lead. +""" + +[Org] + [Org.Components] + title = "Components" + + [Org.Components.InSpec] + title = "InSpec" + text = """ + Handles the [InSpec](https://github.com/chef/inspec) toolset. + """ + team = "inspec-maintainers" + + lieutenant = "arlimus" + + maintainers = [ + "chris-rock", + "jcreedcmu" + ] + +[people] + [people.arlimus] + Name = "Dominik Richter" + GitHub = "arlimus" + + [people.chris-rock] + Name = "Christoph Hartmann" + GitHub = "chris-rock" + + [people.jcreedcmu] + Name = "Jason Reed" + GitHub = "jcreedcmu" diff --git a/Rakefile b/Rakefile index 55f588612..38af8a262 100644 --- a/Rakefile +++ b/Rakefile @@ -3,6 +3,7 @@ require 'rake/testtask' require 'rubocop/rake_task' +require_relative 'tasks/maintainers' # Rubocop desc 'Run Rubocop lint checks' diff --git a/tasks/maintainers.rb b/tasks/maintainers.rb new file mode 100644 index 000000000..e8f3d7c5e --- /dev/null +++ b/tasks/maintainers.rb @@ -0,0 +1,212 @@ +# +# Copyright:: Copyright (c) 2015 Chef Software, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'rake' + +SOURCE = File.join(File.dirname(__FILE__), "..", "MAINTAINERS.toml") +TARGET = File.join(File.dirname(__FILE__), "..", "MAINTAINERS.md") + +# The list of repositories that teams should own +REPOSITORIES = ["chef/inspec"] + + +begin + require 'tomlrb' + require 'octokit' + require 'pp' + task :default => :generate + + namespace :maintainers do + desc "Generate MarkDown version of MAINTAINERS file" + task :generate do + maintainers = Tomlrb.load_file SOURCE + out = "\n\n" + out << "\n\n" + out << "# " + maintainers["Preamble"]["title"] + "\n\n" + out << maintainers["Preamble"]["text"] + "\n" + out << components(maintainers["people"], maintainers["Org"]["Components"]) + File.open(TARGET, "w") { |fn| + fn.write out + } + end + + desc "Synchronize GitHub teams" + task :synchronize do + Octokit.auto_paginate = true + get_github_teams + prepare_teams(source["Org"]["Components"].dup) + sync_teams! + end + end + + def github + @github ||= Octokit::Client.new(:netrc => true) + end + + def source + @source ||= Tomlrb.load_file SOURCE + end + + def teams + @teams ||= {"inspec-maintainers" => {"title" => "Maintainers of the InSpec toolset"}} + end + + def add_members(team, name) + teams["inspec-maintainers"]["members"] ||= [] + teams["inspec-maintainers"]["members"] << name + teams[team] ||= {} + teams[team]["members"] ||= [] + teams[team]["members"] << name + end + + def set_team_title(team, title) + teams[team] ||= {} + teams[team]["title"] = title + end + + def gh_teams + @gh_teams ||= {} + end + + # we have to resolve team names to ids. While we're at it, we can get the privacy + # setting, so we know whether we need to update it + def get_github_teams + github.org_teams("chef").each do |team| + gh_teams[team[:slug]] = {"id" => team[:id], "privacy" => team[:privacy]} + end + end + + def get_github_team(team) + github.team_members(gh_teams[team]["id"]).map do |member| + member[:login] + end.sort.uniq.map(&:downcase) + rescue + [] + end + + def create_team(team) + puts "creating new github team: #{team} with title: #{teams[team]["title"]} " + t = github.create_team("chef", name: team, description: teams[team]["title"], + privacy: "closed", repo_names: REPOSITORIES, + accept: "application/vnd.github.ironman-preview+json") + gh_teams[team] = { "id" => t[:id], "privacy" => t[:privacy] } + end + + def compare_teams(current, desired) + # additions are the subtraction of the current state from the desired state + # deletions are the subtraction of the desired state from the current state + [desired - current, current - desired] + end + + def prepare_teams(cmp) + %w(text paths).each { |k| cmp.delete(k) } + if cmp.key?("team") + team = cmp.delete("team") + add_members(team, cmp.delete("lieutenant")) if cmp.key?("lieutenant") + add_members(team, cmp.delete("maintainers")) if cmp.key?("maintainers") + set_team_title(team, cmp.delete("title")) + else + %w(maintainers lieutenant title).each { |k| cmp.delete(k) } + end + cmp.each { |_k, v| prepare_teams(v) } + end + + def update_team(team, additions, deletions) + create_team(team) unless gh_teams.key?(team) + update_team_privacy(team) + add_team_members(team, additions) + remove_team_members(team, deletions) + rescue + puts "failed for #{team}" + end + + def update_team_privacy(team) + return + return if gh_teams[team]["privacy"] == "closed" + puts "Setting #{team} privacy to closed from #{gh_teams[team]["privacy"]}" + github.update_team(gh_teams[team]["id"], privacy: "closed", + accept: "application/vnd.github.ironman-preview+json") + end + + def add_team_members(team, additions) + additions.each do |member| + puts "Adding #{member} to #{team}" + github.add_team_membership(gh_teams[team]["id"], member, role: "member", + accept: "application/vnd.github.ironman-preview+json") + end + end + + def remove_team_members(team, deletions) + deletions.each do |member| + puts "Removing #{member} from #{team}" + github.remove_team_membership(gh_teams[team]["id"], member, + accept: "application/vnd.github.ironman-preview+json") + end + end + + def sync_teams! + teams.each do |name, details| + current = get_github_team(name) + desired = details["members"].flatten.sort.uniq.map(&:downcase) + additions, deletions = compare_teams(current, desired) + update_team(name, additions, deletions) + end + end + + def get_person(person) + source["people"][person] + end + + def components(list, cmp) + out = "## " + cmp.delete("title") + "\n\n" + out << cmp.delete("text") + "\n" if cmp.has_key?("text") + out << "To mention the team, use @chef/#{cmp.delete("team")}\n\n" if cmp.has_key?("team") + if cmp.has_key?("lieutenant") + out << "### Lieutenant\n\n" + out << person(list, cmp.delete("lieutenant")) + "\n\n" + end + out << maintainers(list, cmp.delete("maintainers")) + "\n" if cmp.has_key?("maintainers") + cmp.delete("paths") + cmp.each {|k,v| out << components(list, v) } + out + end + + def maintainers(list, people) + o = "### Maintainers\n\n" + people.each do |p| + o << person(list, p) + "\n" + end + o + end + + def person(list, person) + if list[person].has_key?("GitHub") + out = "* [#{list[person]["Name"]}](https://github.com/#{list[person]["GitHub"]})" + else + out = "* #{list[person]["Name"]}" + end + out << "\n * IRC - #{list[person]["IRC"]}" if list[person].has_key?("IRC") + out << "\n * [@#{list[person]["Twitter"]}](https://twitter.com/#{list[person]["Twitter"]})" if list[person].has_key?("Twitter") + out << "\n * [#{list[person]["email"]}](mailto:#{list[person]["email"]})" if list[person].has_key?("email") + out << "\n * #{list[person]["phone"]}" if list[person].has_key?("phone") + out << "\n * [ServerFault](#{list[person]["ServerFault"]})" if list[person].has_key?("ServerFault") + out + end + +rescue LoadError + STDERR.puts "\n*** TomlRb not available.\n\n" +end