Merge pull request #14207 from EtiennePerot:maintainers

PiperOrigin-RevId: 968193941
This commit is contained in:
gVisor bot
2026-08-20 19:14:47 -07:00
9 changed files with 690 additions and 0 deletions
@@ -0,0 +1,104 @@
---
name: update-gvisor-maintainers
description: >
Update the gVisor maintainer roster in governance/maintainers.yaml and regenerate
the files derived from it (.github/reviewer.json and MAINTAINERS.md).
Use when a maintainer goes on hiatus, returns from hiatus, is added, steps down to
emeritus, changes employer, or when MAINTAINERS.md / reviewer.json are out of sync.
---
# Update the gVisor maintainer roster
`governance/maintainers.yaml` is the single source of truth.
`.github/reviewer.json` and `MAINTAINERS.md` are generated from it by
`//governance/tools/maintainers:maintainers_gen`, and `maintainers_test`
byte-compares both against the checked-in copies. So: edit the YAML, regenerate,
run the test. Never hand-edit the two generated files; the test will catch you,
and rightly so.
## Schema
Each entry under `maintainers:`:
```yaml
- name: Gee Vaïzaur # Full name or nickname.
github: gvaizaur # GitHub username.
affiliation: Independent # Current employer, or "Independent".
past_affiliation: # Optional.
- affiliation: Arasaka Corporation
until: 2025-03-01
started: 2018-05-08 # First contribution. The list is sorted by this.
status: HIATUS_SINCE:2026-07-17
```
`status` is one of three, and it drives everything downstream:
Status | Reviews | Merge permissions | `reviewer.json` | `MAINTAINERS.md`
--------------------------- | ------- | ----------------- | --------------- | ----------------
`ACTIVE` | yes | yes | `true` | main table
`HIATUS_SINCE:YYYY-MM-DD` | no | yes | `false` | main table
`EMERITUS_SINCE:YYYY-MM-DD` | no | no | omitted | emeritus table
`true` in `reviewer.json` means the GitHub workflow may auto-assign reviews to
them. `false` means they are still a maintainer, just not on the receiving end
of the assignment lottery. Hiatus is about reviews only, not permissions; that
is why hiatus maintainers still show up in the main `MAINTAINERS.md` table
alongside active ones.
## Common changes
Most of these are a one-line `status:` edit on an existing entry:
- Goes on hiatus: `status: HIATUS_SINCE:<today>`.
- Comes back from hiatus: `status: ACTIVE`. Drop the date; `ACTIVE` never
carries one.
- Steps down, or hits 12 months of inactivity: `status:
EMERITUS_SINCE:<date>`.
- Comes back from emeritus: `status: ACTIVE`, but only through the normal
nomination process. See "Becoming a maintainer" in `GOVERNANCE.md`.
The two that are not:
- New maintainer: insert the entry in `started` order, not at the end of the
file.
- Changed employer: update `affiliation`, and move the old one to
`past_affiliation` with the date it ended as `until`.
Dates are real calendar dates, so go look up today's date rather than guessing
at one. If the user gave you a date, use theirs.
After any change, regenerate.
## Regenerate
```bash
make run TARGETS=//governance/tools/maintainers:maintainers_gen \
ARGS="-input governance/maintainers.yaml -format reviewer.json -output .github/reviewer.json"
make run TARGETS=//governance/tools/maintainers:maintainers_gen \
ARGS="-input governance/maintainers.yaml -format MAINTAINERS.md -output MAINTAINERS.md"
```
`-format` takes the name of the file being generated, `reviewer.json` or
`MAINTAINERS.md`. Leave off `-output` to dump to stdout instead, which is handy
for eyeballing a change before writing it.
Then:
```bash
make test TARGETS=//governance/tools/maintainers/...
```
This fails if the YAML changed and you forgot to regenerate, or if someone
edited a generated file by hand.
## Before handing back
Show the user the diff across all three files (`governance/maintainers.yaml`,
`.github/reviewer.json`, `MAINTAINERS.md`) and let them confirm the roster reads
the way they meant it to.
If a generated file contains something the generator does not emit, that content
is drift, and regenerating is what removes it. Do not teach the generator to
reproduce it without asking first: the generator decides what those files
contain, not whatever happened to be committed.
+1
View File
@@ -3,6 +3,7 @@
"carzh": true,
"etienneperot": true,
"fvoznika": true,
"kerumeto": true,
"konstantin-s-bogom": true,
"manninglucas": false,
"milantracy": true,
+2
View File
@@ -20,6 +20,8 @@ exports_files([
"README.md",
"SECURITY.md",
"GOVERNANCE.md",
"MAINTAINERS.md",
".github/reviewer.json",
])
release_files(
+50
View File
@@ -0,0 +1,50 @@
<!-- Generated from governance/maintainers.yaml by
//governance/tools/maintainers:maintainers_gen; do not edit directly. -->
The current maintainers of the gVisor project are listed below.
Name | GitHub ID | Company/Organization
-------------------- | ------------------------------------------------------------ | --------------------
Fabricio Voznika | [@fvoznika](https://github.com/fvoznika) | Google
Jamie Liu | [@nixprime](https://github.com/nixprime) | Google
Zach Koopmans | [@zkoopmans](https://github.com/zkoopmans) | Google
Nayana Bidari | [@nybidari](https://github.com/nybidari) | Google
Etienne Perot | [@etienneperot](https://github.com/etienneperot) | Google
Lucas Manning | [@manninglucas](https://github.com/manninglucas) | Google
Jing Chen | [@milantracy](https://github.com/milantracy) | Google
Konstantin Bogomolov | [@konstantin-s-bogom](https://github.com/konstantin-s-bogom) | Google
Jimmy Tran | [@trantoji](https://github.com/trantoji) | Google
Anil Altinay | [@aaltinaydev](https://github.com/aaltinaydev) | Google
Shailend Chand | [@shailend-g](https://github.com/shailend-g) | Google
Parth Sarthi | [@parth-opensrc](https://github.com/parth-opensrc) | Google
Ryan El Kochta | [@relkochta](https://github.com/relkochta) | Google
Rex Ren | [@rexren-gif](https://github.com/rexren-gif) | Google
Xin Zhong | [@xinzhong](https://github.com/xinzhong) | Google
Caroline Zhu | [@carzh](https://github.com/carzh) | Google
Alexander Cueva | [@kerumeto](https://github.com/kerumeto) | Google
## Emeritus Maintainers
Maintainers who have stepped back from active participation are recognized here.
Emeritus maintainers do not have voting rights or merge access but are
recognized for their contributions and may be consulted on project matters.
A maintainer is moved to emeritus status after 12 months of inactivity, or by
request. An emeritus maintainer may return to active status through the normal
maintainer nomination process.
Name | GitHub ID | Date moved to emeritus
-------------------- | ---------------------------------------------------- | ----------------------
Adin Scannell | [@amscanne](https://github.com/amscanne) | 2023-03-03
Nicolas Lacasse | [@nlacasse](https://github.com/nlacasse) | 2025-06-27
Bhasker Hariharan | [@hbhasker](https://github.com/hbhasker) | 2022-09-16
Ian Gudger | [@iangudger](https://github.com/iangudger) | 2020-06-26
Michael Pratt | [@prattmic](https://github.com/prattmic) | 2020-04-02
Kevin Krakauer | [@kevinGC](https://github.com/kevinGC) | 2025-01-28
Rahat Mahmood | [@mrahatm](https://github.com/mrahatm) | 2023-01-19
Andrei Vagin | [@avagin](https://github.com/avagin) | 2026-02-12
Tamir Duberstein | [@tamird](https://github.com/tamird) | 2022-07-12
Ian Lewis | [@ianlewis](https://github.com/ianlewis) | 2023-07-17
Ayush Ranjan | [@ayushr2](https://github.com/ayushr2) | 2026-06-24
Ghanan Gowripalan | [@ghananigans](https://github.com/ghananigans) | 2023-10-16
Shambhavi Srivastava | [@sshambhavi-252](https://github.com/sshambhavi-252) | 2023-10-10
+6
View File
@@ -0,0 +1,6 @@
package(
default_applicable_licenses = ["//:license"],
licenses = ["notice"],
)
exports_files(["maintainers.yaml"])
+166
View File
@@ -0,0 +1,166 @@
# gVisor maintainer roster.
#
# Maintainer fields:
# name: Full name or nickname.
# github: GitHub username.
# affiliation: Current employer or "Independent".
# past_affiliation: Optional list of { affiliation: "...", until: "YYYY-MM-DD" } objects.
# started: When the maintainer started contributing (YYYY-MM-DD). Sort by this.
# status: One of:
# - "ACTIVE" (does reviews, has merge permissions)
# - "HIATUS_SINCE:YYYY-MM-DD" (doesn't do reviews, has permissions)
# - "EMERITUS_SINCE:YYYY-MM-DD" (doesn't do reviews, no permissions)
maintainers:
- name: Adin Scannell
github: amscanne
affiliation: Google
started: 2018-04-28
status: EMERITUS_SINCE:2023-03-03
- name: Fabricio Voznika
github: fvoznika
affiliation: Google
started: 2018-04-28
status: ACTIVE
- name: Nicolas Lacasse
github: nlacasse
affiliation: Google
started: 2018-04-28
status: EMERITUS_SINCE:2025-06-27
- name: Bhasker Hariharan
github: hbhasker
affiliation: Google
started: 2018-05-01
status: EMERITUS_SINCE:2022-09-16
- name: Ian Gudger
github: iangudger
affiliation: Google
started: 2018-05-01
status: EMERITUS_SINCE:2020-06-26
- name: Michael Pratt
github: prattmic
affiliation: Google
started: 2018-05-01
status: EMERITUS_SINCE:2020-04-02
- name: Kevin Krakauer
github: kevinGC
affiliation: Google
started: 2018-05-04
status: EMERITUS_SINCE:2025-01-28
- name: Jamie Liu
github: nixprime
affiliation: Google
started: 2018-05-08
status: HIATUS_SINCE:2026-07-17
- name: Rahat Mahmood
github: mrahatm
affiliation: Google
started: 2018-05-17
status: EMERITUS_SINCE:2023-01-19
- name: Andrei Vagin
github: avagin
affiliation: Google
started: 2018-07-31
status: EMERITUS_SINCE:2026-02-12
- name: Tamir Duberstein
github: tamird
affiliation: Google
started: 2018-08-25
status: EMERITUS_SINCE:2022-07-12
- name: Ian Lewis
github: ianlewis
affiliation: Google
started: 2018-10-12
status: EMERITUS_SINCE:2023-07-17
- name: Zach Koopmans
github: zkoopmans
affiliation: Google
started: 2018-11-26
status: ACTIVE
- name: Ayush Ranjan
github: ayushr2
affiliation: Modal
past_affiliation:
- affiliation: Google
until: 2026-07-04
started: 2019-05-23
status: EMERITUS_SINCE:2026-06-24
- name: Ghanan Gowripalan
github: ghananigans
affiliation: Google
started: 2019-09-03
status: EMERITUS_SINCE:2023-10-16
- name: Nayana Bidari
github: nybidari
affiliation: Google
started: 2020-01-09
status: ACTIVE
- name: Etienne Perot
github: etienneperot
affiliation: Google
started: 2020-11-18
status: ACTIVE
- name: Lucas Manning
github: manninglucas
affiliation: Google
started: 2021-06-29
status: HIATUS_SINCE:2026-07-17
- name: Jing Chen
github: milantracy
affiliation: Google
started: 2021-10-20
status: ACTIVE
- name: Konstantin Bogomolov
github: konstantin-s-bogom
affiliation: Google
started: 2022-02-28
status: ACTIVE
- name: Shambhavi Srivastava
github: sshambhavi-252
affiliation: Google
started: 2022-04-01
status: EMERITUS_SINCE:2023-10-10
- name: Jimmy Tran
github: trantoji
affiliation: Google
started: 2025-02-07
status: ACTIVE
- name: Anil Altinay
github: aaltinaydev
affiliation: Google
started: 2025-08-05
status: ACTIVE
- name: Shailend Chand
github: shailend-g
affiliation: Google
started: 2025-08-12
status: ACTIVE
- name: Parth Sarthi
github: parth-opensrc
affiliation: Google
started: 2025-11-06
status: ACTIVE
- name: Ryan El Kochta
github: relkochta
affiliation: Google
started: 2026-05-04
status: ACTIVE
- name: Rex Ren
github: rexren-gif
affiliation: Google
started: 2026-05-07
status: ACTIVE
- name: Xin Zhong
github: xinzhong
affiliation: Google
started: 2026-05-22
status: ACTIVE
- name: Caroline Zhu
github: carzh
affiliation: Google
started: 2026-06-29
status: ACTIVE
- name: Alexander Cueva
github: kerumeto
affiliation: Google
started: 2026-08-17
status: ACTIVE
+24
View File
@@ -0,0 +1,24 @@
load("//tools:defs.bzl", "go_binary", "go_test")
package(
default_applicable_licenses = ["//:license"],
licenses = ["notice"],
)
go_binary(
name = "maintainers_gen",
srcs = ["maintainers_gen.go"],
deps = ["@in_gopkg_yaml_v3//:go_default_library"],
)
go_test(
name = "maintainers_test",
srcs = ["maintainers_test.go"],
data = [
":maintainers_gen",
"//:.github/reviewer.json",
"//:MAINTAINERS.md",
"//governance:maintainers.yaml",
],
deps = ["//pkg/test/testutil"],
)
@@ -0,0 +1,270 @@
// Copyright 2026 The gVisor Authors.
//
// 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.
// Command maintainers_gen generates the contents of .github/reviewer.json and
// MAINTAINERS.md from the maintainer roster in governance/maintainers.yaml.
package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"os"
"strings"
"time"
yaml "gopkg.in/yaml.v3"
)
var (
input = flag.String("input", "", "path to maintainers.yaml")
format = flag.String("format", "reviewer.json", `output format: "reviewer.json" or "MAINTAINERS.md"`)
output = flag.String("output", "", "path to write to; defaults to stdout")
)
// pastAffiliation is a past employer of a maintainer.
type pastAffiliation struct {
Affiliation string `yaml:"affiliation"`
Until string `yaml:"until"`
}
// maintainer is a single entry of the maintainer roster.
type maintainer struct {
Name string `yaml:"name"`
GitHub string `yaml:"github"`
Affiliation string `yaml:"affiliation"`
PastAffiliations []pastAffiliation `yaml:"past_affiliation"`
Started string `yaml:"started"`
Status string `yaml:"status"`
}
// roster is the schema of maintainers.yaml.
type roster struct {
Maintainers []maintainer `yaml:"maintainers"`
}
// parseStatus splits a status into its kind ("ACTIVE", "HIATUS_SINCE" or
// "EMERITUS_SINCE") and date, and validates both.
func parseStatus(status string) (string, string, error) {
kind, date, hasDate := strings.Cut(status, ":")
if hasDate {
if _, err := time.Parse("2006-01-02", date); err != nil {
return "", "", fmt.Errorf("invalid date in status %q", status)
}
}
switch {
case kind == "ACTIVE" && !hasDate:
case kind == "HIATUS_SINCE" && hasDate:
case kind == "EMERITUS_SINCE" && hasDate:
default:
return "", "", fmt.Errorf("invalid status %q", status)
}
return kind, date, nil
}
// parseRoster parses and validates maintainers.yaml contents.
func parseRoster(yamlData []byte) (*roster, error) {
dec := yaml.NewDecoder(bytes.NewReader(yamlData))
dec.KnownFields(true)
var r roster
if err := dec.Decode(&r); err != nil {
return nil, fmt.Errorf("cannot parse roster: %w", err)
}
seen := make(map[string]bool, len(r.Maintainers))
var prev maintainer
for _, m := range r.Maintainers {
if m.Name == "" || m.GitHub == "" || m.Affiliation == "" || m.Started == "" || m.Status == "" {
return nil, fmt.Errorf("maintainer %+v: name, github, affiliation, started and status are required", m)
}
if _, err := time.Parse("2006-01-02", m.Started); err != nil {
return nil, fmt.Errorf("maintainer %q: invalid started date %q", m.GitHub, m.Started)
}
for _, p := range m.PastAffiliations {
if p.Affiliation == "" {
return nil, fmt.Errorf("maintainer %q: past_affiliation entries require an affiliation", m.GitHub)
}
if _, err := time.Parse("2006-01-02", p.Until); err != nil {
return nil, fmt.Errorf("maintainer %q: invalid until date %q in past_affiliation", m.GitHub, p.Until)
}
}
if _, _, err := parseStatus(m.Status); err != nil {
return nil, fmt.Errorf("maintainer %q: %w", m.GitHub, err)
}
if m.Started < prev.Started {
return nil, fmt.Errorf("maintainers are not sorted by started date: %q (%s) comes after %q (%s)", m.Name, m.Started, prev.Name, prev.Started)
}
prev = m
if seen[m.GitHub] {
return nil, fmt.Errorf("duplicate maintainer %q", m.GitHub)
}
seen[m.GitHub] = true
}
return &r, nil
}
// generateReviewerJSON renders `reviewer.json` contents.
func generateReviewerJSON(r *roster) ([]byte, error) {
reviewers := make(map[string]bool, len(r.Maintainers))
for _, m := range r.Maintainers {
kind, _, _ := parseStatus(m.Status)
if kind == "EMERITUS_SINCE" {
continue
}
reviewers[m.GitHub] = kind == "ACTIVE"
}
jsonData, err := json.MarshalIndent(reviewers, "", " ")
if err != nil {
return nil, fmt.Errorf("cannot marshal reviewers: %w", err)
}
return append(jsonData, '\n'), nil
}
// formatTable renders a markdown table with columns formatted.
func formatTable(headers []string, rows [][]string) string {
numCols := len(headers)
colWidths := make([]int, numCols)
for i, h := range headers {
colWidths[i] = len(h)
}
for _, row := range rows {
for i, cell := range row {
if len(cell) > colWidths[i] {
colWidths[i] = len(cell)
}
}
}
var b strings.Builder
// Header
for i, h := range headers {
if i > 0 {
b.WriteString(" | ")
}
if i == numCols-1 {
b.WriteString(h)
} else {
b.WriteString(h)
b.WriteString(strings.Repeat(" ", colWidths[i]-len(h)))
}
}
b.WriteString("\n")
// Separator
for i := 0; i < numCols; i++ {
if i > 0 {
b.WriteString(" | ")
}
b.WriteString(strings.Repeat("-", colWidths[i]))
}
b.WriteString("\n")
// Rows
for _, row := range rows {
for i, cell := range row {
if i > 0 {
b.WriteString(" | ")
}
if i == numCols-1 {
b.WriteString(cell)
} else {
b.WriteString(cell)
b.WriteString(strings.Repeat(" ", colWidths[i]-len(cell)))
}
}
b.WriteString("\n")
}
return b.String()
}
// generateMaintainersMD renders `MAINTAINERS.md` contents.
func generateMaintainersMD(r *roster) ([]byte, error) {
var b bytes.Buffer
b.WriteString("<!-- Generated from governance/maintainers.yaml by\n")
b.WriteString(" //governance/tools/maintainers:maintainers_gen; do not edit directly. -->\n\n")
b.WriteString("The current maintainers of the gVisor project are listed below.\n\n")
var activeRows [][]string
for _, m := range r.Maintainers {
if kind, _, _ := parseStatus(m.Status); kind != "EMERITUS_SINCE" {
activeRows = append(activeRows, []string{
m.Name,
fmt.Sprintf("[@%s](https://github.com/%s)", m.GitHub, m.GitHub),
m.Affiliation,
})
}
}
b.WriteString(formatTable([]string{"Name", "GitHub ID", "Company/Organization"}, activeRows))
b.WriteString(`
## Emeritus Maintainers
Maintainers who have stepped back from active participation are recognized here.
Emeritus maintainers do not have voting rights or merge access but are
recognized for their contributions and may be consulted on project matters.
A maintainer is moved to emeritus status after 12 months of inactivity, or by
request. An emeritus maintainer may return to active status through the normal
maintainer nomination process.
`)
var emeritusRows [][]string
for _, m := range r.Maintainers {
if kind, date, _ := parseStatus(m.Status); kind == "EMERITUS_SINCE" {
emeritusRows = append(emeritusRows, []string{
m.Name,
fmt.Sprintf("[@%s](https://github.com/%s)", m.GitHub, m.GitHub),
date,
})
}
}
b.WriteString(formatTable([]string{"Name", "GitHub ID", "Date moved to emeritus"}, emeritusRows))
return b.Bytes(), nil
}
func main() {
flag.Parse()
if *input == "" {
fmt.Fprintln(os.Stderr, "must specify -input")
os.Exit(1)
}
yamlData, err := os.ReadFile(*input)
if err != nil {
fmt.Fprintf(os.Stderr, "cannot read roster: %v\n", err)
os.Exit(1)
}
r, err := parseRoster(yamlData)
if err != nil {
fmt.Fprintf(os.Stderr, "cannot parse %s: %v\n", *input, err)
os.Exit(1)
}
var outData []byte
switch *format {
case "reviewer.json":
outData, err = generateReviewerJSON(r)
case "MAINTAINERS.md":
outData, err = generateMaintainersMD(r)
default:
err = fmt.Errorf("invalid format %q", *format)
}
if err != nil {
fmt.Fprintf(os.Stderr, "cannot generate output: %v\n", err)
os.Exit(1)
}
if *output == "" {
os.Stdout.Write(outData)
} else if err := os.WriteFile(*output, outData, 0644); err != nil {
fmt.Fprintf(os.Stderr, "cannot write %s: %v\n", *output, err)
os.Exit(1)
}
}
@@ -0,0 +1,67 @@
// Copyright 2026 The gVisor Authors.
//
// 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.
package maintainers_test
import (
"bytes"
"os"
"os/exec"
"path/filepath"
"testing"
"gvisor.dev/gvisor/pkg/test/testutil"
)
// checkInSync verifies that the checked-in file at path matches what
// `maintainers_gen` generates.
func checkInSync(t *testing.T, path string) {
t.Helper()
format := filepath.Base(path)
genBin, err := testutil.FindFile("governance/tools/maintainers/maintainers_gen")
if err != nil {
t.Fatalf("cannot find maintainers_gen: %v", err)
}
yamlPath, err := testutil.FindFile("governance/maintainers.yaml")
if err != nil {
t.Fatalf("cannot find maintainers.yaml: %v", err)
}
wantPath, err := testutil.FindFile(path)
if err != nil {
t.Fatalf("cannot find %s: %v", path, err)
}
cmd := exec.Command(genBin, "-input", yamlPath, "-format", format)
cmd.Stderr = os.Stderr
got, err := cmd.Output()
if err != nil {
t.Fatalf("cannot run maintainers_gen: %v", err)
}
want, err := os.ReadFile(wantPath)
if err != nil {
t.Fatalf("cannot read %s: %v", path, err)
}
if !bytes.Equal(got, want) {
t.Errorf("%s is out of sync with governance/maintainers.yaml; regenerate with:\n"+
" make run TARGETS=//governance/tools/maintainers:maintainers_gen ARGS=\"-input governance/maintainers.yaml -format %s -output %s\"\n"+
"got:\n%swant:\n%s", path, format, path, got, want)
}
}
func TestReviewerJSONInSync(t *testing.T) {
checkInSync(t, ".github/reviewer.json")
}
func TestMaintainersMDInSync(t *testing.T) {
checkInSync(t, "MAINTAINERS.md")
}