Skip to content

Commit

Permalink
projects with no maintainers now cause script to exit with an error (#3)
Browse files Browse the repository at this point in the history
* adds check for projects with no maintainers.
* adds check for projects with unknown maintainers if alias map given.
  • Loading branch information
lsh-0 authored May 30, 2023
1 parent fecc5e2 commit c6c35eb
Show file tree
Hide file tree
Showing 2 changed files with 82 additions and 26 deletions.
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
# maintainers.txt

Script that parses `maintainers.txt` files in elife repositories and prints a simple report of *what* is maintained by *whom*.
Parses `maintainers.txt` files in eLife Github repositories, printing a simple report of *what* is maintained by *whom*.

Accepts an optional input file mapping a `maintainer=>alias`. This will replace the name of the maintainer output with
If any repository has no maintainers, the script will exit with a failure.

Accepts an optional input file mapping a `maintainer => alias`. This will replace the name of the maintainer output with
something else (like an email address).

If an alias map was given and any repository has a maintainer not present in the map, the script will exit with a failure.

## requisites

* Go 1.20+
* A Github Personal Access Token the `repo` scope.
* A Github Personal Access Token the `repo` scope and access to private repositories.
- See: https://docs.github.com/en/rest/dependabot/alerts#list-dependabot-alerts-for-an-organization

## Installation
Expand All @@ -28,7 +32,7 @@ or
and `alias-map.json` might look like:

```json
{"lsh-0": "l.skibinski@example.org"}
{"jdoe": "john.doe@example.org"}
```

## Licence
Expand Down
96 changes: 74 additions & 22 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ import (
"golang.org/x/oauth2"
)

type Maintainer = string
type Alias = string
type Project = string

// --- utils

// write templated message `tem` with `args` to stderr
func stderr(tem string, args ...interface{}) {
fmt.Fprintln(os.Stderr, fmt.Sprintf(tem, args...))
Expand All @@ -52,14 +58,6 @@ func panicOnErr(err error, action string) {
}
}

// pulls a Github personal access token (PAT) out of an envvar `GITHUB_TOKEN`
// panics if token does not exist.
func github_token() string {
token, present := os.LookupEnv("GITHUB_TOKEN")
ensure(present, "envvar GITHUB_TOKEN not set.")
return token
}

// converts most data to a JSON string with sorted keys.
func as_json(thing interface{}) string {
json_blob_bytes, err := json.Marshal(thing)
Expand Down Expand Up @@ -124,17 +122,38 @@ func spit(contents string, path string) {
}
}

// ---

// pulls a Github personal access token (PAT) out of an envvar `GITHUB_TOKEN`
// panics if token does not exist.
func github_token() string {
token, present := os.LookupEnv("GITHUB_TOKEN")
ensure(present, "envvar GITHUB_TOKEN not set.")
return token
}

// returns `true` if the given `maintainer` looks like a slack channel
// slack channels are supported by eLife CI but I (personally) think it mixes
// purposes: project ownership and CI notifications.
func slack_channel(maintainer string) bool {
return maintainer != "" && maintainer[0] == '#'
}

// parses the raw contents of a maintainers.txt file,
// replacing the maintainer name with an alias from `maintainer_alias_map`.
// returns a list of maintainers/aliases.
func parse_maintainers_txt_file(contents string, maintainer_alias_map map[string]string) []string {
maintainer_list := []string{}
func parse_maintainers_txt_file(contents string, maintainer_alias_map map[Maintainer]Alias) []Alias {
maintainer_list := []Alias{}
contents = strings.TrimSpace(contents)
if contents == "" {
return maintainer_list
}
for _, maintainer := range strings.Split(contents, "\n") {
alias, present := maintainer_alias_map[maintainer] // foo => [email protected]
if slack_channel(maintainer) {
stderr("skipping slack channel: %s", maintainer)
continue
}
alias, present := maintainer_alias_map[maintainer] // jdoe => [email protected]
if !present {
alias = maintainer
}
Expand All @@ -144,15 +163,15 @@ func parse_maintainers_txt_file(contents string, maintainer_alias_map map[string
}

// parses the optional JSON input file of maintainer IDs to an alias.
// input is a simple JSON map: {"foo": "f.bar@elifesciences.org"}
// returns a map of `maintainer=>alias`.
func parse_maintainers_alias_file(path string) map[string]string {
// input is a simple JSON map: {"jdoe": "john.doe@example.org"}
// returns a map of `maintainer => alias`.
func parse_maintainers_alias_file(path string) map[Maintainer]Alias {
ensure(file_exists(path), "file does not exist: "+path)

json_blob := slurp(path)
ensure(json_blob != "", "file is empty: "+path)

alias_map := map[string]string{}
alias_map := map[Maintainer]Alias{}
err := json.Unmarshal([]byte(json_blob), &alias_map)
panicOnErr(err, "deserialising JSON into a map of string=>string")

Expand Down Expand Up @@ -198,14 +217,21 @@ func main() {
token := github_token()
org_name := "elifesciences"

maintainer_alias_map := map[string]string{}
// "jdoe" => "[email protected]"
maintainer_alias_map := map[Maintainer]Alias{}
if len(args) > 0 {
maintainer_alias_map = parse_maintainers_alias_file(args[0])
}
// "jdoe" => "[email protected]" becomes "[email protected]" => "jdoe"
reverse_maintainer_alias_map := map[Alias]Maintainer{}
for maintainer, alias := range maintainer_alias_map {
reverse_maintainer_alias_map[alias] = maintainer
}

// step 1, slurp all the maintainers.txt files we can and cache their contents on disk.
// step 1, slurp all the maintainers.txt files from all repositories.
// we cache their contents on disk for development only.

raw_maintainers := map[string]string{}
raw_maintainers := map[Project]string{}
for _, repo := range fetch_repos(org_name, token) {
if repo.GetArchived() {
continue
Expand All @@ -231,10 +257,36 @@ func main() {
// step 2, parse that raw maintainers.txt content into a map of project=>maintainer-list

// we want a datastructure like: {project1: [maintainer1, maintainer2], project2: [...], ...}
maintainers := map[string][]string{}
for repo, maintainers_file_contents := range raw_maintainers {
maintainers[repo] = parse_maintainers_txt_file(maintainers_file_contents, maintainer_alias_map)
project_maintainers := map[Project][]Maintainer{}
for project, maintainers_file_contents := range raw_maintainers {
project_maintainers[project] = parse_maintainers_txt_file(maintainers_file_contents, maintainer_alias_map)
}

fmt.Println(as_json(maintainers))
// step 3, final checks.
// 1. projects with no maintainers.txt files should cause script to fail.
// 2. projects with a maintainer not present in the given alias map (if any)
// should cause script to fail.
fail := false
for project, maintainer_alias_list := range project_maintainers {
if len(maintainer_alias_list) == 0 {
stderr("project has no maintainers: %s", project)
fail = true
}
if len(maintainer_alias_map) > 0 {
for _, maintainer_alias := range maintainer_alias_list {
_, present := reverse_maintainer_alias_map[maintainer_alias]
if !present {
// "project 'foo' has an unknown maintainer: john"
stderr("project '%s' has an unknown maintainer: %s", project, maintainer_alias)
fail = true
}
}
}
}

fmt.Println(as_json(project_maintainers))

if fail {
os.Exit(1)
}
}

0 comments on commit c6c35eb

Please sign in to comment.