diff --git a/.agents/skills/quarterly-ladder-analysis/references/bots.txt b/.agents/skills/quarterly-ladder-analysis/references/bots.txt index 8c9f084..e72b8f9 100644 --- a/.agents/skills/quarterly-ladder-analysis/references/bots.txt +++ b/.agents/skills/quarterly-ladder-analysis/references/bots.txt @@ -35,6 +35,7 @@ buildbot agentfu kenzie-agent reidbaker-agent +renzo-olivares-agent google-admin google-admin-automation-1 google-admin-automation-2 diff --git a/.agents/skills/quarterly-ladder-analysis/references/known_maintainers.txt b/.agents/skills/quarterly-ladder-analysis/references/known_maintainers.txt new file mode 100644 index 0000000..ab06217 --- /dev/null +++ b/.agents/skills/quarterly-ladder-analysis/references/known_maintainers.txt @@ -0,0 +1,8 @@ +# These are known maintainers, listed here by exception due to a sync issue with internal tooling and GitHub. +CarlosMendonca +LouiseHsu +abdallahshaban557 +andywolff +gaaclarke +gmackall +jesswrd diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index e01b250..6716ecf 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -39,6 +39,12 @@ jobs: - name: Analyze Dart code run: dart analyze --fatal-infos + - name: Validate contributors.yaml + run: dart run tool/validate_roster.dart + + - name: Verify ROSTER.md and EMERITUS.md are in sync + run: dart run tool/generate_roster.dart --check + - name: Run unit test suite run: dart test diff --git a/EMERITUS.md b/EMERITUS.md new file mode 100644 index 0000000..6d1e161 --- /dev/null +++ b/EMERITUS.md @@ -0,0 +1,65 @@ +# Dasher Emeritus + +> [!IMPORTANT] +> **Pending Q4 Audit:** No contributor roles or organization memberships have been changed yet. The Emeritus transitions listed below are pending the Q4 audit, which will be subject to a 30-day community review period before being finalized. + +This directory honors **Dasher Emeritus** alumni—former Flutter Reviewers, Committers, and Maintainers who previously held roles on the contributor ladder and have stepped back from active contribution (< 10 contributions over the preceding 12 months). + +To maintain repository security, Dasher Emeriti do not remain members of the `flutter` GitHub organization. Instead, their contributions are recognized here, and they retain access to Flutter contributor channels on Discord (including `#hidden-chat`) and the announcement-only `flutter-contributors` Google Group (for project announcements and calendar invites to our recurring Flutter Contributor Roundtable and the Dash Forum, where contributors can present and get feedback on their [RFCs](https://github.com/flutter/rfc); the registration form is pinned in `#hidden-chat` on Discord) so they can stay in touch and more easily ramp back up onto the project if and when they choose to return. + +Returning Emeriti who resume contributing are automatically identified during quarterly reviews (where they can reply directly on the public tracking issue). For off-cycle reinstatement, because creating new issues and pull requests in `flutter/contributors` is restricted to organization members (`collaborators`), returning Emeriti can reach out in `#hidden-chat` on Discord (or to an active organization member) to have an active org member sponsor and open a [Dasher Emeritus Reinstatement request](.github/ISSUE_TEMPLATE/2_emeritus_reinstatement.yml) on their behalf (where they can also share an updated Discord username if needed). See [docs/LADDER.md](docs/LADDER.md) for details. + +> [!NOTE] +> This file is automatically generated from `data/contributors.yaml`. +> To update, edit `data/contributors.yaml` and run `dart run tool/generate_roster.dart`. + +## Honored Alumni + +[@0xZOne](https://github.com/0xZOne) • [@324mao](https://github.com/324mao) • [@a14n](https://github.com/a14n) • [@aaron-sf](https://github.com/aaron-sf) +[@adarshprabhu](https://github.com/adarshprabhu) • [@adicahyono](https://github.com/adicahyono) • [@akrwb](https://github.com/akrwb) • [@albertodev01](https://github.com/albertodev01) +[@alestiago](https://github.com/alestiago) • [@AlexV525](https://github.com/AlexV525) • [@allisonryan0002](https://github.com/allisonryan0002) • [@almasryalyoum](https://github.com/almasryalyoum) +[@alxp1982](https://github.com/alxp1982) • [@AnnaPS](https://github.com/AnnaPS) • [@antonellof](https://github.com/antonellof) • [@ANTONINEUTRON](https://github.com/ANTONINEUTRON) +[@AntonyKiragu](https://github.com/AntonyKiragu) • [@atrope](https://github.com/atrope) • [@AustinGTI](https://github.com/AustinGTI) • [@AyushBherwani1998](https://github.com/AyushBherwani1998) +[@azchohfi](https://github.com/azchohfi) • [@bartekpacia](https://github.com/bartekpacia) • [@BerkSpar](https://github.com/BerkSpar) • [@bernaferrari](https://github.com/bernaferrari) +[@brianegan](https://github.com/brianegan) • [@bruno-garcia](https://github.com/bruno-garcia) • [@bselwe](https://github.com/bselwe) • [@btobin](https://github.com/btobin) +[@budikhafiKLY](https://github.com/budikhafiKLY) • [@burakkasikci](https://github.com/burakkasikci) • [@CareF](https://github.com/CareF) • [@chenglu](https://github.com/chenglu) +[@chinmoy12c](https://github.com/chinmoy12c) • [@chriscraws](https://github.com/chriscraws) • [@cityxsuperapp](https://github.com/cityxsuperapp) • [@CoffeeDevUK](https://github.com/CoffeeDevUK) +[@ColdPaleLight](https://github.com/ColdPaleLight) • [@collinjackson](https://github.com/collinjackson) • [@dam-ease](https://github.com/dam-ease) • [@danagbemava-nc](https://github.com/danagbemava-nc) +[@darshankawar](https://github.com/darshankawar) • [@dclark-disney](https://github.com/dclark-disney) • [@diarmaidlindsay](https://github.com/diarmaidlindsay) • [@diegogawenda](https://github.com/diegogawenda) +[@diegoveloper](https://github.com/diegoveloper) • [@djol](https://github.com/djol) • [@dominikIncibit](https://github.com/dominikIncibit) • [@eda-nagao](https://github.com/eda-nagao) +[@eggfly](https://github.com/eggfly) • [@egoe](https://github.com/egoe) • [@Ehesp](https://github.com/Ehesp) • [@emma-roudabush-disney](https://github.com/emma-roudabush-disney) +[@emrebuyukozkan](https://github.com/emrebuyukozkan) • [@ened](https://github.com/ened) • [@enzoftware](https://github.com/enzoftware) • [@erickzanardo](https://github.com/erickzanardo) +[@eseidel](https://github.com/eseidel) • [@exaby73](https://github.com/exaby73) • [@farshad1489](https://github.com/farshad1489) • [@felangel](https://github.com/felangel) +[@FirentisTFW](https://github.com/FirentisTFW) • [@FlutterIssues](https://github.com/FlutterIssues) • [@FXschwartz](https://github.com/FXschwartz) • [@gastonliz](https://github.com/gastonliz) +[@gianfrancopapa](https://github.com/gianfrancopapa) • [@gnprice](https://github.com/gnprice) • [@guoguo338](https://github.com/guoguo338) • [@hamdikahloun](https://github.com/hamdikahloun) +[@harri35](https://github.com/harri35) • [@hbatagelo](https://github.com/hbatagelo) • [@helinanever](https://github.com/helinanever) • [@hgraceb](https://github.com/hgraceb) +[@HidenoriMatsubayashi](https://github.com/HidenoriMatsubayashi) • [@hillelcoren](https://github.com/hillelcoren) • [@Hiroaki-Kanai](https://github.com/Hiroaki-Kanai) • [@horiaciu](https://github.com/horiaciu) +[@IanSimpson](https://github.com/IanSimpson) • [@ihorkozar](https://github.com/ihorkozar) • [@irmasantoso](https://github.com/irmasantoso) • [@itsjen](https://github.com/itsjen) +[@ja-dfir](https://github.com/ja-dfir) • [@Jan-Stepien](https://github.com/Jan-Stepien) • [@jcollins-disney](https://github.com/jcollins-disney) • [@JErazo7](https://github.com/JErazo7) +[@jeroen-meijer](https://github.com/jeroen-meijer) • [@Jhon-Aires](https://github.com/Jhon-Aires) • [@jingshao-code](https://github.com/jingshao-code) • [@jnschulze](https://github.com/jnschulze) +[@jomblocoder](https://github.com/jomblocoder) • [@jorgecoca](https://github.com/jorgecoca) • [@jpnurmi](https://github.com/jpnurmi) • [@JsouLiang](https://github.com/JsouLiang) +[@Justian-SopraSteria](https://github.com/Justian-SopraSteria) • [@kaiceyd](https://github.com/kaiceyd) • [@kenvandine](https://github.com/kenvandine) • [@kevinburns426](https://github.com/kevinburns426) +[@kinla004](https://github.com/kinla004) • [@Kirpal](https://github.com/Kirpal) • [@kmillikin](https://github.com/kmillikin) • [@kokoro-team](https://github.com/kokoro-team) +[@kondoakio](https://github.com/kondoakio) • [@KristinBi](https://github.com/KristinBi) • [@kwg4](https://github.com/kwg4) • [@laramartin](https://github.com/laramartin) +[@leoafarias](https://github.com/leoafarias) • [@linxuebin1990](https://github.com/linxuebin1990) • [@LinXunFeng](https://github.com/LinXunFeng) • [@lucavenir](https://github.com/lucavenir) +[@luckysmg](https://github.com/luckysmg) • [@MacarenaSantos](https://github.com/MacarenaSantos) • [@magicianA](https://github.com/magicianA) • [@maheshj01](https://github.com/maheshj01) +[@martinduva](https://github.com/martinduva) • [@MaruQuarky](https://github.com/MaruQuarky) • [@marwfair](https://github.com/marwfair) • [@matiasleyba](https://github.com/matiasleyba) +[@matsuda-shota](https://github.com/matsuda-shota) • [@matthew-carroll](https://github.com/matthew-carroll) • [@matthewshahi](https://github.com/matthewshahi) • [@mattmartelnz](https://github.com/mattmartelnz) +[@MauriPastorini](https://github.com/MauriPastorini) • [@maxp89](https://github.com/maxp89) • [@mchudy](https://github.com/mchudy) • [@Menshoz](https://github.com/Menshoz) +[@mhadaily](https://github.com/mhadaily) • [@milezabaleta](https://github.com/milezabaleta) • [@miquelbeltran](https://github.com/miquelbeltran) • [@moffatman](https://github.com/moffatman) +[@MrUnfunny](https://github.com/MrUnfunny) • [@msangalli](https://github.com/msangalli) • [@mvanbeusekom](https://github.com/mvanbeusekom) • [@nikosportolos](https://github.com/nikosportolos) +[@nirunirosha401-personal](https://github.com/nirunirosha401-personal) • [@noboru-i](https://github.com/noboru-i) • [@nt4f04uNd](https://github.com/nt4f04uNd) • [@okursoftware](https://github.com/okursoftware) +[@omartinma](https://github.com/omartinma) • [@omatt](https://github.com/omatt) • [@pedromassango](https://github.com/pedromassango) • [@pingbird](https://github.com/pingbird) +[@PoojaB26](https://github.com/PoojaB26) • [@QMAshfaqAli](https://github.com/QMAshfaqAli) • [@rahulan](https://github.com/rahulan) • [@rajayogan](https://github.com/rajayogan) +[@renefloor](https://github.com/renefloor) • [@Rexios80](https://github.com/Rexios80) • [@rzambroni](https://github.com/rzambroni) • [@Salakar](https://github.com/Salakar) +[@scarletteliza](https://github.com/scarletteliza) • [@scutlight](https://github.com/scutlight) • [@SenseiMinimal](https://github.com/SenseiMinimal) • [@shirbayat](https://github.com/shirbayat) +[@simpson-peter](https://github.com/simpson-peter) • [@slightfoot](https://github.com/slightfoot) • [@SofiaRey](https://github.com/SofiaRey) • [@spideythewebhead](https://github.com/spideythewebhead) +[@spjass](https://github.com/spjass) • [@spratt-disney](https://github.com/spratt-disney) • [@ssmitienko](https://github.com/ssmitienko) • [@sushanthmshetty](https://github.com/sushanthmshetty) +[@swavkulinski](https://github.com/swavkulinski) • [@Swiftaxe](https://github.com/Swiftaxe) • [@tanercc](https://github.com/tanercc) • [@tcannon-disney](https://github.com/tcannon-disney) +[@tfabacher-dp](https://github.com/tfabacher-dp) • [@tgucio](https://github.com/tgucio) • [@thekorn](https://github.com/thekorn) • [@TimoPieti](https://github.com/TimoPieti) +[@tomgilder](https://github.com/tomgilder) • [@tpiaggio](https://github.com/tpiaggio) • [@trevorplant](https://github.com/trevorplant) • [@tsurumotokdi](https://github.com/tsurumotokdi) +[@udayon](https://github.com/udayon) • [@vaind](https://github.com/vaind) • [@valentinallavayol](https://github.com/valentinallavayol) • [@Vanethos](https://github.com/Vanethos) +[@vickyduran](https://github.com/vickyduran) • [@wakita181009](https://github.com/wakita181009) • [@wangying3426](https://github.com/wangying3426) • [@werainkhatri](https://github.com/werainkhatri) +[@whesse](https://github.com/whesse) • [@workerbee22](https://github.com/workerbee22) • [@wprater](https://github.com/wprater) • [@xm0128](https://github.com/xm0128) +[@xu-baolin](https://github.com/xu-baolin) • [@YeungKC](https://github.com/YeungKC) • [@yiiim](https://github.com/yiiim) • [@ymgn-dev](https://github.com/ymgn-dev) +[@zhongwuzw](https://github.com/zhongwuzw) • [@Zudah228](https://github.com/Zudah228) diff --git a/ROSTER.md b/ROSTER.md new file mode 100644 index 0000000..d3624c6 --- /dev/null +++ b/ROSTER.md @@ -0,0 +1,110 @@ +# Contributor Roster + +> [!IMPORTANT] +> **Pending Q4 Audit:** No contributor roles or permissions have been changed yet. The placements listed below are initial baseline assignments pending the Q4 audit, which will be subject to a 30-day community review period before being finalized. + +This directory lists active Flutter Contributor Ladder members. +Honorary alumni who have stepped back from active contribution and exited the GitHub organization are listed in [EMERITUS.md](EMERITUS.md). +Governance details and tier qualifications are specified in [docs/LADDER.md](docs/LADDER.md). + +> [!NOTE] +> This file is automatically generated from `data/contributors.yaml`. +> To update, edit `data/contributors.yaml` and run `dart run tool/generate_roster.dart`. + +## Maintainers (`@flutter/maintainers`) + +Maintainers hold GitHub Write permissions and organization moderation privileges, and belong to `@flutter/maintainers` (a child team of `@flutter/flutter-hackers`). + +[@aam](https://github.com/aam) • [@abdallahshaban557](https://github.com/abdallahshaban557) • [@alexmarkov](https://github.com/alexmarkov) • [@amcasari](https://github.com/amcasari) +[@ananci](https://github.com/ananci) • [@anderdobo](https://github.com/anderdobo) • [@andrewkolos](https://github.com/andrewkolos) • [@andywolff](https://github.com/andywolff) +[@another-rex](https://github.com/another-rex) • [@apwilson](https://github.com/apwilson) • [@arbreng](https://github.com/arbreng) • [@athomas](https://github.com/athomas) +[@b-luk](https://github.com/b-luk) • [@bdero](https://github.com/bdero) • [@biggs0125](https://github.com/biggs0125) • [@bkonyi](https://github.com/bkonyi) +[@bparrishMines](https://github.com/bparrishMines) • [@brianquinlan](https://github.com/brianquinlan) • [@bwilkerson](https://github.com/bwilkerson) • [@camsim99](https://github.com/camsim99) +[@CarlosMendonca](https://github.com/CarlosMendonca) • [@caroqliu](https://github.com/caroqliu) • [@catharp](https://github.com/catharp) • [@cbracken](https://github.com/cbracken) +[@chandarrengoog](https://github.com/chandarrengoog) • [@chaselatta](https://github.com/chaselatta) • [@chingjun](https://github.com/chingjun) • [@chloestefantsova](https://github.com/chloestefantsova) +[@chrisraygill](https://github.com/chrisraygill) • [@ChristianEdwardPadilla](https://github.com/ChristianEdwardPadilla) • [@chunhtai](https://github.com/chunhtai) • [@cj-radcliff](https://github.com/cj-radcliff) +[@cligh](https://github.com/cligh) • [@conooi](https://github.com/conooi) • [@cpsloal](https://github.com/cpsloal) • [@craiglabenz](https://github.com/craiglabenz) +[@danielleon-cmd](https://github.com/danielleon-cmd) • [@DanTup](https://github.com/DanTup) • [@danxorzum](https://github.com/danxorzum) • [@davidmorgan](https://github.com/davidmorgan) +[@DBowen33](https://github.com/DBowen33) • [@dcharkes](https://github.com/dcharkes) • [@di](https://github.com/di) • [@Dipakkumarjena](https://github.com/Dipakkumarjena) +[@ditman](https://github.com/ditman) • [@dkbharti-glitch](https://github.com/dkbharti-glitch) • [@dkwingsmt](https://github.com/dkwingsmt) • [@domesticmouse](https://github.com/domesticmouse) +[@dsmcl](https://github.com/dsmcl) • [@dustingreen](https://github.com/dustingreen) • [@eieio](https://github.com/eieio) • [@elliette](https://github.com/elliette) +[@emckean](https://github.com/emckean) • [@ericwindmill](https://github.com/ericwindmill) • [@essjay05](https://github.com/essjay05) • [@ethanalee-work](https://github.com/ethanalee-work) +[@everyplace](https://github.com/everyplace) • [@eyebrowsoffire](https://github.com/eyebrowsoffire) • [@flar](https://github.com/flar) • [@flutter-zl](https://github.com/flutter-zl) +[@gaaclarke](https://github.com/gaaclarke) • [@GemmaJAy](https://github.com/GemmaJAy) • [@gitrey](https://github.com/gitrey) • [@gmackall](https://github.com/gmackall) +[@goderbauer](https://github.com/goderbauer) • [@grouma](https://github.com/grouma) • [@gspencergoog](https://github.com/gspencergoog) • [@guidezpl](https://github.com/guidezpl) +[@h9jiang](https://github.com/h9jiang) • [@hannah-hyj](https://github.com/hannah-hyj) • [@harryterkelsen](https://github.com/harryterkelsen) • [@helin24](https://github.com/helin24) +[@hellohuanlin](https://github.com/hellohuanlin) • [@Hixie](https://github.com/Hixie) • [@ievdokdm](https://github.com/ievdokdm) • [@iinozemtsev](https://github.com/iinozemtsev) +[@InMatrix](https://github.com/InMatrix) • [@itsjustkevin](https://github.com/itsjustkevin) • [@jacob314](https://github.com/jacob314) • [@jacobsimionato](https://github.com/jacobsimionato) +[@jaeheon](https://github.com/jaeheon) • [@jakemac53](https://github.com/jakemac53) • [@jalseth](https://github.com/jalseth) • [@jensjoha](https://github.com/jensjoha) +[@jesskuras](https://github.com/jesskuras) • [@jesswrd](https://github.com/jesswrd) • [@jiahaog](https://github.com/jiahaog) • [@jmagman](https://github.com/jmagman) +[@joefspiro](https://github.com/joefspiro) • [@johnmccutchan](https://github.com/johnmccutchan) • [@johnniwinther](https://github.com/johnniwinther) • [@johnpryan](https://github.com/johnpryan) +[@johnsonmh](https://github.com/johnsonmh) • [@jokerttu](https://github.com/jokerttu) • [@jonasfj](https://github.com/jonasfj) • [@jonathanmetzman](https://github.com/jonathanmetzman) +[@josemontespg](https://github.com/josemontespg) • [@joshualitt](https://github.com/joshualitt) • [@jq129398](https://github.com/jq129398) • [@jrwang](https://github.com/jrwang) +[@jtmcdole](https://github.com/jtmcdole) • [@juan-mora-google](https://github.com/juan-mora-google) • [@justinmc](https://github.com/justinmc) • [@jwill](https://github.com/jwill) +[@jwren](https://github.com/jwren) • [@jyameo](https://github.com/jyameo) • [@kallentu](https://github.com/kallentu) • [@keertip](https://github.com/keertip) +[@kentnguyen99](https://github.com/kentnguyen99) • [@kenzieschmoll](https://github.com/kenzieschmoll) • [@kevmoo](https://github.com/kevmoo) • [@kevya-google](https://github.com/kevya-google) +[@khanhnwin](https://github.com/khanhnwin) • [@kkboateng](https://github.com/kkboateng) • [@knopp](https://github.com/knopp) • [@lamek](https://github.com/lamek) +[@leafpetersen](https://github.com/leafpetersen) • [@liamappelbe](https://github.com/liamappelbe) • [@loic-sharma](https://github.com/loic-sharma) • [@LongCatIsLooong](https://github.com/LongCatIsLooong) +[@LouiseHsu](https://github.com/LouiseHsu) • [@mariamhas](https://github.com/mariamhas) • [@Markzipan](https://github.com/Markzipan) • [@MaryaBelanger](https://github.com/MaryaBelanger) +[@maryx](https://github.com/maryx) • [@mattkae](https://github.com/mattkae) • [@mbcorona](https://github.com/mbcorona) • [@mboetger](https://github.com/mboetger) +[@mbrase](https://github.com/mbrase) • [@mdebbar](https://github.com/mdebbar) • [@michaelkedar](https://github.com/michaelkedar) • [@mikaelkomu](https://github.com/mikaelkomu) +[@mit-mit](https://github.com/mit-mit) • [@mkustermann](https://github.com/mkustermann) • [@mmohankrishna-lab](https://github.com/mmohankrishna-lab) • [@mosuem](https://github.com/mosuem) +[@mraleph](https://github.com/mraleph) • [@munificent](https://github.com/munificent) • [@muralikrishmt](https://github.com/muralikrishmt) • [@MuthuGCodes](https://github.com/MuthuGCodes) +[@nan-yu](https://github.com/nan-yu) • [@natebosch](https://github.com/natebosch) • [@nickyringland](https://github.com/nickyringland) • [@nshahan](https://github.com/nshahan) +[@nuritzi](https://github.com/nuritzi) • [@okorohelijah](https://github.com/okorohelijah) • [@oliverchang](https://github.com/oliverchang) • [@omerlevran46](https://github.com/omerlevran46) +[@parlough](https://github.com/parlough) • [@paul-geoghegan](https://github.com/paul-geoghegan) • [@peixinli](https://github.com/peixinli) • [@pennzht](https://github.com/pennzht) +[@Piinks](https://github.com/Piinks) • [@pjkammer](https://github.com/pjkammer) • [@polina-c](https://github.com/polina-c) • [@pq](https://github.com/pq) +[@precision](https://github.com/precision) • [@preethi0228-git](https://github.com/preethi0228-git) • [@puneetkukreja98](https://github.com/puneetkukreja98) • [@QuncCccccc](https://github.com/QuncCccccc) +[@ranbeuer](https://github.com/ranbeuer) • [@redbrogdon](https://github.com/redbrogdon) • [@reidbaker](https://github.com/reidbaker) • [@renaudd](https://github.com/renaudd) +[@renyou](https://github.com/renyou) • [@Renzo-Olivares](https://github.com/Renzo-Olivares) • [@rmacnak-google](https://github.com/rmacnak-google) • [@robert-ancell](https://github.com/robert-ancell) +[@rodydavis](https://github.com/rodydavis) • [@ronit-cloudsufi](https://github.com/ronit-cloudsufi) • [@rspier](https://github.com/rspier) • [@Rusino](https://github.com/Rusino) +[@samuli-codemate](https://github.com/samuli-codemate) • [@saurabh-mirajkar](https://github.com/saurabh-mirajkar) • [@Saviq](https://github.com/Saviq) • [@scheglov](https://github.com/scheglov) +[@schultek](https://github.com/schultek) • [@sethladd](https://github.com/sethladd) • [@sfshaza2](https://github.com/sfshaza2) • [@shanecglass](https://github.com/shanecglass) +[@sigmundch](https://github.com/sigmundch) • [@sigurdm](https://github.com/sigurdm) • [@snikiche](https://github.com/snikiche) • [@sokoloff06](https://github.com/sokoloff06) +[@srawlins](https://github.com/srawlins) • [@srujzs](https://github.com/srujzs) • [@sstrickl](https://github.com/sstrickl) • [@stef-luo](https://github.com/stef-luo) +[@stereotype441](https://github.com/stereotype441) • [@stevemessick](https://github.com/stevemessick) • [@studgeek](https://github.com/studgeek) • [@suertreus](https://github.com/suertreus) +[@suryaKommana2662](https://github.com/suryaKommana2662) • [@szakarias](https://github.com/szakarias) • [@tarobins](https://github.com/tarobins) • [@tarrinneal](https://github.com/tarrinneal) +[@tasnim-khan](https://github.com/tasnim-khan) • [@thagomizer](https://github.com/thagomizer) • [@thkim1011](https://github.com/thkim1011) • [@timothyhoang-google](https://github.com/timothyhoang-google) +[@twerske](https://github.com/twerske) • [@utopicnarwhal](https://github.com/utopicnarwhal) • [@uysalere](https://github.com/uysalere) • [@vashworth](https://github.com/vashworth) +[@victogomez-cs](https://github.com/victogomez-cs) • [@victorsanni](https://github.com/victorsanni) • [@vivahome2](https://github.com/vivahome2) • [@vsmenon](https://github.com/vsmenon) +[@walley892](https://github.com/walley892) • [@xster](https://github.com/xster) • [@yjbanov](https://github.com/yjbanov) • [@zanderso](https://github.com/zanderso) +[@zhenqiu1101](https://github.com/zhenqiu1101) • [@zijiehe-google-com](https://github.com/zijiehe-google-com) • [@zoeyfan](https://github.com/zoeyfan) + +## Committers (`@flutter/committers`) + +Committers hold GitHub Write permissions (merge rights) and provide authoritative code reviews. + +| Contributor | GitHub profile | 12m merged PRs | 12m total activity | +| :--- | :--- | :--- | :--- | +| @AbdeMohlbi | [github.com/AbdeMohlbi](https://github.com/AbdeMohlbi) | 138 | 236 | +| @huycozy | [github.com/huycozy](https://github.com/huycozy) | 11 | 33 | +| @JavadAsadi | [github.com/JavadAsadi](https://github.com/JavadAsadi) | 42 | 48 | +| @ksokolovskyi | [github.com/ksokolovskyi](https://github.com/ksokolovskyi) | 21 | 34 | +| @Mairramer | [github.com/Mairramer](https://github.com/Mairramer) | 28 | 43 | +| @navaronbracke | [github.com/navaronbracke](https://github.com/navaronbracke) | 43 | 334 | +| @rkishan516 | [github.com/rkishan516](https://github.com/rkishan516) | 50 | 76 | +| @ValentinVignal | [github.com/ValentinVignal](https://github.com/ValentinVignal) | 30 | 45 | + +## Reviewers (`@flutter/reviewers`) + +Reviewers hold GitHub Triage permissions, triage issues, and conduct first-pass code reviews. + +| Contributor | GitHub profile | 12m merged PRs | 12m total activity | +| :--- | :--- | :--- | :--- | +| @bleroux | [github.com/bleroux](https://github.com/bleroux) | 15 | 23 | +| @csells | [github.com/csells](https://github.com/csells) | 11 | 21 | +| @davidhicks980 | [github.com/davidhicks980](https://github.com/davidhicks980) | 12 | 24 | +| @filiph | [github.com/filiph](https://github.com/filiph) | 0 | 10 | +| @Hari-07 | [github.com/Hari-07](https://github.com/Hari-07) | 9 | 30 | +| @htrolle-epam | [github.com/htrolle-epam](https://github.com/htrolle-epam) | 15 | 15 | +| @jwinarske | [github.com/jwinarske](https://github.com/jwinarske) | 4 | 23 | +| @piedcipher | [github.com/piedcipher](https://github.com/piedcipher) | 6 | 15 | +| @RobertOdrowaz | [github.com/RobertOdrowaz](https://github.com/RobertOdrowaz) | 7 | 12 | +| @stuartmorgan | [github.com/stuartmorgan](https://github.com/stuartmorgan) | 4 | 13 | +| @tirth-patel-nc | [github.com/tirth-patel-nc](https://github.com/tirth-patel-nc) | 0 | 10 | +| @wolfenrain | [github.com/wolfenrain](https://github.com/wolfenrain) | 0 | 16 | +| @zemanux | [github.com/zemanux](https://github.com/zemanux) | 6 | 24 | + +## Dasher Emeritus + +Former ladder members who have stepped back from active contribution do not remain in the GitHub organization and are honored in [EMERITUS.md](EMERITUS.md). diff --git a/data/contributors.yaml b/data/contributors.yaml new file mode 100644 index 0000000..a8fc989 --- /dev/null +++ b/data/contributors.yaml @@ -0,0 +1,902 @@ +# Flutter Contributor Ladder Roster +# Source of truth for Flutter contributor roles and permissions. +# Last updated: 2026-10-27 + +maintainers: + # Maintainers on the Flutter team (@flutter/googlers and @flutter/partners) + # Granted Write access + Org Moderation + googlers: + - username: aam + role: maintainer + - username: abdallahshaban557 + role: maintainer + - username: alexmarkov + role: maintainer + - username: amcasari + role: maintainer + - username: ananci + role: maintainer + - username: anderdobo + role: maintainer + - username: andrewkolos + role: maintainer + - username: andywolff + role: maintainer + - username: another-rex + role: maintainer + - username: apwilson + role: maintainer + - username: arbreng + role: maintainer + - username: athomas + role: maintainer + - username: b-luk + role: maintainer + - username: biggs0125 + role: maintainer + - username: bkonyi + role: maintainer + - username: bparrishMines + role: maintainer + - username: brianquinlan + role: maintainer + - username: bwilkerson + role: maintainer + - username: camsim99 + role: maintainer + - username: CarlosMendonca + role: maintainer + - username: caroqliu + role: maintainer + - username: catharp + role: maintainer + - username: cbracken + role: maintainer + - username: chandarrengoog + role: maintainer + - username: chaselatta + role: maintainer + - username: chingjun + role: maintainer + - username: chloestefantsova + role: maintainer + - username: chrisraygill + role: maintainer + - username: ChristianEdwardPadilla + role: maintainer + - username: chunhtai + role: maintainer + - username: cj-radcliff + role: maintainer + - username: cligh + role: maintainer + - username: conooi + role: maintainer + - username: cpsloal + role: maintainer + - username: craiglabenz + role: maintainer + - username: davidmorgan + role: maintainer + - username: DBowen33 + role: maintainer + - username: dcharkes + role: maintainer + - username: di + role: maintainer + - username: ditman + role: maintainer + - username: dkwingsmt + role: maintainer + - username: domesticmouse + role: maintainer + - username: dsmcl + role: maintainer + - username: dustingreen + role: maintainer + - username: eieio + role: maintainer + - username: elliette + role: maintainer + - username: emckean + role: maintainer + - username: ericwindmill + role: maintainer + - username: essjay05 + role: maintainer + - username: ethanalee-work + role: maintainer + - username: everyplace + role: maintainer + - username: eyebrowsoffire + role: maintainer + - username: flar + role: maintainer + - username: flutter-zl + role: maintainer + - username: gaaclarke + role: maintainer + - username: GemmaJAy + role: maintainer + - username: gitrey + role: maintainer + - username: gmackall + role: maintainer + - username: goderbauer + role: maintainer + - username: grouma + role: maintainer + - username: gspencergoog + role: maintainer + - username: guidezpl + role: maintainer + - username: h9jiang + role: maintainer + - username: hannah-hyj + role: maintainer + - username: harryterkelsen + role: maintainer + - username: helin24 + role: maintainer + - username: hellohuanlin + role: maintainer + - username: ievdokdm + role: maintainer + - username: iinozemtsev + role: maintainer + - username: InMatrix + role: maintainer + - username: itsjustkevin + role: maintainer + - username: jacob314 + role: maintainer + - username: jacobsimionato + role: maintainer + - username: jaeheon + role: maintainer + - username: jakemac53 + role: maintainer + - username: jalseth + role: maintainer + - username: jensjoha + role: maintainer + - username: jesskuras + role: maintainer + - username: jesswrd + role: maintainer + - username: jiahaog + role: maintainer + - username: jmagman + role: maintainer + - username: joefspiro + role: maintainer + - username: johnmccutchan + role: maintainer + - username: johnniwinther + role: maintainer + - username: johnpryan + role: maintainer + - username: johnsonmh + role: maintainer + - username: jonasfj + role: maintainer + - username: jonathanmetzman + role: maintainer + - username: josemontespg + role: maintainer + - username: joshualitt + role: maintainer + - username: jq129398 + role: maintainer + - username: jrwang + role: maintainer + - username: jtmcdole + role: maintainer + - username: justinmc + role: maintainer + - username: jwill + role: maintainer + - username: jwren + role: maintainer + - username: jyameo + role: maintainer + - username: kallentu + role: maintainer + - username: keertip + role: maintainer + - username: kentnguyen99 + role: maintainer + - username: kenzieschmoll + role: maintainer + - username: kevmoo + role: maintainer + - username: kevya-google + role: maintainer + - username: khanhnwin + role: maintainer + - username: kkboateng + role: maintainer + - username: lamek + role: maintainer + - username: leafpetersen + role: maintainer + - username: liamappelbe + role: maintainer + - username: loic-sharma + role: maintainer + - username: LongCatIsLooong + role: maintainer + - username: LouiseHsu + role: maintainer + - username: mariamhas + role: maintainer + - username: Markzipan + role: maintainer + - username: MaryaBelanger + role: maintainer + - username: maryx + role: maintainer + - username: mboetger + role: maintainer + - username: mbrase + role: maintainer + - username: mdebbar + role: maintainer + - username: michaelkedar + role: maintainer + - username: mit-mit + role: maintainer + - username: mkustermann + role: maintainer + - username: mosuem + role: maintainer + - username: mraleph + role: maintainer + - username: munificent + role: maintainer + - username: MuthuGCodes + role: maintainer + - username: nan-yu + role: maintainer + - username: natebosch + role: maintainer + - username: nickyringland + role: maintainer + - username: nshahan + role: maintainer + - username: nuritzi + role: maintainer + - username: okorohelijah + role: maintainer + - username: oliverchang + role: maintainer + - username: omerlevran46 + role: maintainer + - username: peixinli + role: maintainer + - username: pennzht + role: maintainer + - username: Piinks + role: maintainer + - username: pjkammer + role: maintainer + - username: polina-c + role: maintainer + - username: pq + role: maintainer + - username: precision + role: maintainer + - username: QuncCccccc + role: maintainer + - username: redbrogdon + role: maintainer + - username: reidbaker + role: maintainer + - username: renaudd + role: maintainer + - username: renyou + role: maintainer + - username: Renzo-Olivares + role: maintainer + - username: rmacnak-google + role: maintainer + - username: rodydavis + role: maintainer + - username: rspier + role: maintainer + - username: Rusino + role: maintainer + - username: scheglov + role: maintainer + - username: sethladd + role: maintainer + - username: sfshaza2 + role: maintainer + - username: shanecglass + role: maintainer + - username: sigmundch + role: maintainer + - username: sigurdm + role: maintainer + - username: snikiche + role: maintainer + - username: sokoloff06 + role: maintainer + - username: srawlins + role: maintainer + - username: srujzs + role: maintainer + - username: sstrickl + role: maintainer + - username: stef-luo + role: maintainer + - username: stereotype441 + role: maintainer + - username: stevemessick + role: maintainer + - username: studgeek + role: maintainer + - username: suertreus + role: maintainer + - username: szakarias + role: maintainer + - username: tarobins + role: maintainer + - username: tarrinneal + role: maintainer + - username: tasnim-khan + role: maintainer + - username: thagomizer + role: maintainer + - username: thkim1011 + role: maintainer + - username: timothyhoang-google + role: maintainer + - username: twerske + role: maintainer + - username: uysalere + role: maintainer + - username: vashworth + role: maintainer + - username: victorsanni + role: maintainer + - username: vivahome2 + role: maintainer + - username: vsmenon + role: maintainer + - username: walley892 + role: maintainer + - username: xster + role: maintainer + - username: yjbanov + role: maintainer + - username: zanderso + role: maintainer + - username: zhenqiu1101 + role: maintainer + - username: zijiehe-google-com + role: maintainer + - username: zoeyfan + role: maintainer + partners: + - username: bdero + team: "@flutter/flutter_gpu" + role: maintainer + - username: danielleon-cmd + team: "@flutter/cloudsufi" + role: maintainer + - username: DanTup + team: "@flutter/partners" + role: maintainer + - username: danxorzum + team: "@flutter/virtusa" + role: maintainer + - username: Dipakkumarjena + team: "@flutter/techm" + role: maintainer + - username: dkbharti-glitch + team: "@flutter/techm" + role: maintainer + - username: Hixie + team: "@flutter/partners" + role: maintainer + - username: jokerttu + team: "@flutter/codemate" + role: maintainer + - username: juan-mora-google + team: "@flutter/virtusa" + role: maintainer + - username: knopp + team: "@flutter/canonical" + role: maintainer + - username: mattkae + team: "@flutter/canonical" + role: maintainer + - username: mbcorona + team: "@flutter/tcs" + role: maintainer + - username: mikaelkomu + team: "@flutter/codemate" + role: maintainer + - username: mmohankrishna-lab + team: "@flutter/techm" + role: maintainer + - username: muralikrishmt + team: "@flutter/techm" + role: maintainer + - username: parlough + team: "@flutter/partners" + role: maintainer + - username: paul-geoghegan + team: "@flutter/canonical" + role: maintainer + - username: preethi0228-git + team: "@flutter/tcs" + role: maintainer + - username: puneetkukreja98 + team: "@flutter/tcs" + role: maintainer + - username: ranbeuer + team: "@flutter/virtusa" + role: maintainer + - username: robert-ancell + team: "@flutter/canonical" + role: maintainer + - username: ronit-cloudsufi + team: "@flutter/cloudsufi" + role: maintainer + - username: samuli-codemate + team: "@flutter/codemate" + role: maintainer + - username: saurabh-mirajkar + team: "@flutter/tcs" + role: maintainer + - username: Saviq + team: "@flutter/canonical" + role: maintainer + - username: schultek + team: "@flutter/partners" + role: maintainer + - username: suryaKommana2662 + team: "@flutter/tcs" + role: maintainer + - username: utopicnarwhal + team: "@flutter/codemate" + role: maintainer + - username: victogomez-cs + team: "@flutter/cloudsufi" + role: maintainer + +committers: + # Members of @flutter/committers (Child of @flutter/flutter-hackers) + # Granted GitHub Write role / merge access + - username: AbdeMohlbi + role: committer + - username: huycozy + role: committer + - username: JavadAsadi + role: committer + - username: ksokolovskyi + role: committer + - username: Mairramer + role: committer + - username: navaronbracke + role: committer + - username: rkishan516 + role: committer + - username: ValentinVignal + role: committer + +reviewers: + # Members of @flutter/reviewers (Child of @flutter/flutter-hackers) + # Granted GitHub Triage role + - username: bleroux + role: reviewer + - username: csells + role: reviewer + - username: davidhicks980 + role: reviewer + - username: filiph + role: reviewer + - username: Hari-07 + role: reviewer + - username: htrolle-epam + grace_until: "2027-10-14" + role: reviewer + - username: jwinarske + role: reviewer + - username: piedcipher + role: reviewer + - username: RobertOdrowaz + role: reviewer + - username: stuartmorgan + role: reviewer + - username: tirth-patel-nc + role: reviewer + - username: wolfenrain + role: reviewer + - username: zemanux + role: reviewer + +dash_emeritus: + # Honored alumni (External, non-org members on GitHub) + - username: 0xZOne + role: emeritus + - username: 324mao + role: emeritus + - username: a14n + role: emeritus + - username: aaron-sf + role: emeritus + - username: adarshprabhu + role: emeritus + - username: adicahyono + role: emeritus + - username: akrwb + role: emeritus + - username: albertodev01 + role: emeritus + - username: alestiago + role: emeritus + - username: AlexV525 + role: emeritus + - username: allisonryan0002 + role: emeritus + - username: almasryalyoum + role: emeritus + - username: alxp1982 + role: emeritus + - username: AnnaPS + role: emeritus + - username: antonellof + role: emeritus + - username: ANTONINEUTRON + role: emeritus + - username: AntonyKiragu + role: emeritus + - username: atrope + role: emeritus + - username: AustinGTI + role: emeritus + - username: AyushBherwani1998 + role: emeritus + - username: azchohfi + role: emeritus + - username: bartekpacia + role: emeritus + - username: BerkSpar + role: emeritus + - username: bernaferrari + role: emeritus + - username: brianegan + role: emeritus + - username: bruno-garcia + role: emeritus + - username: bselwe + role: emeritus + - username: btobin + role: emeritus + - username: budikhafiKLY + role: emeritus + - username: burakkasikci + role: emeritus + - username: CareF + role: emeritus + - username: chenglu + role: emeritus + - username: chinmoy12c + role: emeritus + - username: chriscraws + role: emeritus + - username: cityxsuperapp + role: emeritus + - username: CoffeeDevUK + role: emeritus + - username: ColdPaleLight + role: emeritus + - username: collinjackson + role: emeritus + - username: dam-ease + role: emeritus + - username: danagbemava-nc + role: emeritus + - username: darshankawar + role: emeritus + - username: dclark-disney + role: emeritus + - username: diarmaidlindsay + role: emeritus + - username: diegogawenda + role: emeritus + - username: diegoveloper + role: emeritus + - username: djol + role: emeritus + - username: dominikIncibit + role: emeritus + - username: eda-nagao + role: emeritus + - username: eggfly + role: emeritus + - username: egoe + role: emeritus + - username: Ehesp + role: emeritus + - username: emma-roudabush-disney + role: emeritus + - username: emrebuyukozkan + role: emeritus + - username: ened + role: emeritus + - username: enzoftware + role: emeritus + - username: erickzanardo + role: emeritus + - username: eseidel + role: emeritus + - username: exaby73 + role: emeritus + - username: farshad1489 + role: emeritus + - username: felangel + role: emeritus + - username: FirentisTFW + role: emeritus + - username: FlutterIssues + role: emeritus + - username: FXschwartz + role: emeritus + - username: gastonliz + role: emeritus + - username: gianfrancopapa + role: emeritus + - username: gnprice + role: emeritus + - username: guoguo338 + role: emeritus + - username: hamdikahloun + role: emeritus + - username: harri35 + role: emeritus + - username: hbatagelo + role: emeritus + - username: helinanever + role: emeritus + - username: hgraceb + role: emeritus + - username: HidenoriMatsubayashi + role: emeritus + - username: hillelcoren + role: emeritus + - username: Hiroaki-Kanai + role: emeritus + - username: horiaciu + role: emeritus + - username: IanSimpson + role: emeritus + - username: ihorkozar + role: emeritus + - username: irmasantoso + role: emeritus + - username: itsjen + role: emeritus + - username: ja-dfir + role: emeritus + - username: Jan-Stepien + role: emeritus + - username: jcollins-disney + role: emeritus + - username: JErazo7 + role: emeritus + - username: jeroen-meijer + role: emeritus + - username: Jhon-Aires + role: emeritus + - username: jingshao-code + role: emeritus + - username: jnschulze + role: emeritus + - username: jomblocoder + role: emeritus + - username: jorgecoca + role: emeritus + - username: jpnurmi + role: emeritus + - username: JsouLiang + role: emeritus + - username: Justian-SopraSteria + role: emeritus + - username: kaiceyd + role: emeritus + - username: kenvandine + role: emeritus + - username: kevinburns426 + role: emeritus + - username: kinla004 + role: emeritus + - username: Kirpal + role: emeritus + - username: kmillikin + role: emeritus + - username: kokoro-team + role: emeritus + - username: kondoakio + role: emeritus + - username: KristinBi + role: emeritus + - username: kwg4 + role: emeritus + - username: laramartin + role: emeritus + - username: leoafarias + role: emeritus + - username: linxuebin1990 + role: emeritus + - username: LinXunFeng + role: emeritus + - username: lucavenir + role: emeritus + - username: luckysmg + role: emeritus + - username: MacarenaSantos + role: emeritus + - username: magicianA + role: emeritus + - username: maheshj01 + role: emeritus + - username: martinduva + role: emeritus + - username: MaruQuarky + role: emeritus + - username: marwfair + role: emeritus + - username: matiasleyba + role: emeritus + - username: matsuda-shota + role: emeritus + - username: matthew-carroll + role: emeritus + - username: matthewshahi + role: emeritus + - username: mattmartelnz + role: emeritus + - username: MauriPastorini + role: emeritus + - username: maxp89 + role: emeritus + - username: mchudy + role: emeritus + - username: Menshoz + role: emeritus + - username: mhadaily + role: emeritus + - username: milezabaleta + role: emeritus + - username: miquelbeltran + role: emeritus + - username: moffatman + role: emeritus + - username: MrUnfunny + role: emeritus + - username: msangalli + role: emeritus + - username: mvanbeusekom + role: emeritus + - username: nikosportolos + role: emeritus + - username: nirunirosha401-personal + role: emeritus + - username: noboru-i + role: emeritus + - username: nt4f04uNd + role: emeritus + - username: okursoftware + role: emeritus + - username: omartinma + role: emeritus + - username: omatt + role: emeritus + - username: pedromassango + role: emeritus + - username: pingbird + role: emeritus + - username: PoojaB26 + role: emeritus + - username: QMAshfaqAli + role: emeritus + - username: rahulan + role: emeritus + - username: rajayogan + role: emeritus + - username: renefloor + role: emeritus + - username: Rexios80 + role: emeritus + - username: rzambroni + role: emeritus + - username: Salakar + role: emeritus + - username: scarletteliza + role: emeritus + - username: scutlight + role: emeritus + - username: SenseiMinimal + role: emeritus + - username: shirbayat + role: emeritus + - username: simpson-peter + role: emeritus + - username: slightfoot + role: emeritus + - username: SofiaRey + role: emeritus + - username: spideythewebhead + role: emeritus + - username: spjass + role: emeritus + - username: spratt-disney + role: emeritus + - username: ssmitienko + role: emeritus + - username: sushanthmshetty + role: emeritus + - username: swavkulinski + role: emeritus + - username: Swiftaxe + role: emeritus + - username: tanercc + role: emeritus + - username: tcannon-disney + role: emeritus + - username: tfabacher-dp + role: emeritus + - username: tgucio + role: emeritus + - username: thekorn + role: emeritus + - username: TimoPieti + role: emeritus + - username: tomgilder + role: emeritus + - username: tpiaggio + role: emeritus + - username: trevorplant + role: emeritus + - username: tsurumotokdi + role: emeritus + - username: udayon + role: emeritus + - username: vaind + role: emeritus + - username: valentinallavayol + role: emeritus + - username: Vanethos + role: emeritus + - username: vickyduran + role: emeritus + - username: wakita181009 + role: emeritus + - username: wangying3426 + role: emeritus + - username: werainkhatri + role: emeritus + - username: whesse + role: emeritus + - username: workerbee22 + role: emeritus + - username: wprater + role: emeritus + - username: xm0128 + role: emeritus + - username: xu-baolin + role: emeritus + - username: YeungKC + role: emeritus + - username: yiiim + role: emeritus + - username: ymgn-dev + role: emeritus + - username: zhongwuzw + role: emeritus + - username: Zudah228 + role: emeritus diff --git a/docs/TRANSITION.md b/docs/TRANSITION.md index 74d19cb..4755df4 100644 --- a/docs/TRANSITION.md +++ b/docs/TRANSITION.md @@ -27,12 +27,12 @@ Inactive historical `flutter-hackers` ($< 10$ contributions in the preceding 12 ## Organization audit -To ensure all existing GitHub organization members are categorized, the initial bootstrap tool (`tool/seed_initial_data.dart`) reviews all members of the `flutter` GitHub organization (`gh api orgs/flutter/members`) and cross-references each account against `@flutter/googlers`, `@flutter/partners`, `@flutter/flutter-hackers`, and 12-month activity data. Untracked active contributors are placed into their qualifying tier (`Committer` or `Reviewer`), while inactive organization members and former `flutter-hackers` ($< 10$ contributions in the preceding 12 months) transition to Dasher Emeritus, are removed from the GitHub organization, and are listed in [EMERITUS.md](../EMERITUS.md). +To ensure all existing GitHub organization members are categorized, the initial audit reviews all members of the `flutter` GitHub organization (`gh api orgs/flutter/members`) and cross-references each account against `@flutter/googlers`, `@flutter/partners`, `@flutter/flutter-hackers`, and 12-month activity data to populate `data/contributors.yaml`. Untracked active contributors are placed into their qualifying tier (`Committer` or `Reviewer`), while inactive organization members and former `flutter-hackers` ($< 10$ contributions in the preceding 12 months) transition to Dasher Emeritus, are removed from the GitHub organization, and are listed in [EMERITUS.md](../EMERITUS.md). ## Retirement of static AUTHORS files Historically, repository-level `AUTHORS` files (`flutter/flutter/AUTHORS`, `flutter/packages/AUTHORS`, etc.) tracked individual contributors. Over time, these files became static and difficult to maintain. `flutter/contributors` now serves as the single source of truth for contributor roles and permissions, while historical `AUTHORS` files are preserved under `legacy_authors/`. The active ladder roster is published in [ROSTER.md](../ROSTER.md) and Dasher Emeriti are listed in [EMERITUS.md](../EMERITUS.md), both generated from `data/contributors.yaml`. -## Data seeding +## Rolling activity dataset -`tool/seed_initial_data.dart` creates `data/activity_summary.json` seeded with the baseline Year 0 analysis, and daily syncs update the current ISO week activity in `data/activity/YYYY-Wxx.json`. Over the first 12 months, the rolling 52-week dataset becomes self-sustaining. +`data/activity_summary.json` aggregates rolling 12-month contribution counts across the 52 weekly snapshots in `data/activity/YYYY-Wxx.json`, and daily syncs (`tool/sync_activity.dart`) update the current ISO week activity and recompute the summary automatically. diff --git a/test/bots_test.dart b/test/bots_test.dart index c0342de..a009ba3 100644 --- a/test/bots_test.dart +++ b/test/bots_test.dart @@ -31,6 +31,7 @@ void main() { expect(isBot('renovate[bot]'), isTrue); expect(isBot('DartDevtoolWorkflowBot'), isTrue); expect(isBot('reidbaker-agent'), isTrue); + expect(isBot('renzo-olivares-agent'), isTrue); expect(isBot('gemini-code-assist'), isTrue); expect(isBot('gemini-code-assist[bot]'), isTrue); expect(isBot('buildbot'), isTrue); @@ -39,6 +40,7 @@ void main() { test('identifies pattern-based bot usernames', () { expect(isBot('some-user-bot'), isTrue); expect(isBot('some_user_bot'), isTrue); + expect(isBot('some-user-agent'), isTrue); expect(isBot('service[bot]'), isTrue); expect(isBot('flutter-autoroll'), isTrue); expect(isBot('custom-autoroll-service'), isTrue); diff --git a/test/roster_validation_test.dart b/test/roster_validation_test.dart new file mode 100644 index 0000000..96b3a95 --- /dev/null +++ b/test/roster_validation_test.dart @@ -0,0 +1,304 @@ +// Copyright 2026 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; +import 'package:test/test.dart'; +import 'package:yaml/yaml.dart'; +import '../tool/generate_roster.dart'; +import '../tool/validate_roster.dart'; + +void main() { + group('Roster Schema & Integrity Tests', () { + late String yamlContent; + late YamlMap doc; + + setUpAll(() { + final file = File('data/contributors.yaml'); + expect(file.existsSync(), isTrue, + reason: 'data/contributors.yaml must exist'); + yamlContent = file.readAsStringSync(); + final loaded = loadYaml(yamlContent); + expect(loaded, isA()); + doc = loaded as YamlMap; + }); + + test('contributors.yaml contains all required root sections', () { + expect(doc.containsKey('maintainers'), isTrue); + expect(doc.containsKey('committers'), isTrue); + expect(doc.containsKey('reviewers'), isTrue); + expect(doc.containsKey('dash_emeritus'), isTrue); + }); + + test('maintainers contains strictly googlers and partners', () { + final maintainers = doc['maintainers'] as YamlMap; + expect(maintainers.keys.toSet(), equals({'googlers', 'partners'})); + + final googlers = maintainers['googlers'] as YamlList; + final partners = maintainers['partners'] as YamlList; + + expect(googlers, isNotEmpty); + expect(partners, isNotEmpty); + }); + + test('all member handles across the entire roster are unique', () { + final seen = {}; + + void checkUser(dynamic entry, String section) { + expect(entry, isA()); + final username = (entry as YamlMap)['username'] as String?; + expect(username, isNotNull); + expect(username!.trim(), isNotEmpty); + + final lower = username.toLowerCase(); + expect(seen.containsKey(lower), isFalse, + reason: + 'Duplicate username "@$username" in $section (already in ${seen[lower]})'); + seen[lower] = section; + } + + final maintainers = doc['maintainers'] as YamlMap; + for (final g in maintainers['googlers'] as YamlList) { + checkUser(g, 'maintainers.googlers'); + } + for (final p in maintainers['partners'] as YamlList) { + checkUser(p, 'maintainers.partners'); + } + for (final c in doc['committers'] as YamlList) { + checkUser(c, 'committers'); + } + for (final r in doc['reviewers'] as YamlList) { + checkUser(r, 'reviewers'); + } + for (final e in doc['dash_emeritus'] as YamlList) { + checkUser(e, 'dash_emeritus'); + } + }); + + test('all roles belong to the allowed set of ladder roles', () { + const allowedRoles = {'maintainer', 'committer', 'reviewer', 'emeritus'}; + + void verifyRole(dynamic entry) { + final map = entry as YamlMap; + final role = map['role'] as String? ?? map['suggested_role'] as String?; + if (role != null) { + expect(allowedRoles.contains(role.toLowerCase()), isTrue, + reason: 'Invalid role "$role" for user ${map['username']}'); + } + } + + final maintainers = doc['maintainers'] as YamlMap; + (maintainers['googlers'] as YamlList).forEach(verifyRole); + (maintainers['partners'] as YamlList).forEach(verifyRole); + (doc['committers'] as YamlList).forEach(verifyRole); + (doc['reviewers'] as YamlList).forEach(verifyRole); + (doc['dash_emeritus'] as YamlList).forEach(verifyRole); + }); + + test('validateContributorsYaml passes on production data/contributors.yaml', + () { + expect(validateContributorsYaml(doc), isEmpty); + }); + + test( + 'validateContributorsYaml detects invalid schemas, duplicate users, invalid roles, and invalid maintainer groups', + () { + expect( + validateContributorsYaml(loadYaml('- not_a_map')), + contains(contains('root must be a YAML mapping')), + ); + + final invalidDoc = loadYaml(''' +maintainers: + googlers: + - username: alice + role: maintainer + discord: "alice_discord" + partners: + - username: bob + role: maintainer + community: + - username: invalidGroupUser + role: maintainer +committers: + - "not_a_map_entry" + - role: committer + - username: ALICE + role: committer + - username: charlie + role: superadmin + - username: dave + role: committer + discord: " " +reviewers: [] +dash_emeritus: [] +'''); + + final errors = validateContributorsYaml(invalidDoc); + expect( + errors, + contains( + contains('invalid group "community"'))); // non-googler/partner + expect(errors, contains(contains('entry is not a map'))); + expect(errors, contains(contains('missing username'))); + expect(errors, + contains(contains('duplicate username "@ALICE" in committers'))); + expect(errors, contains(contains('invalid role "superadmin"'))); + expect( + errors, + contains( + contains('invalid or empty discord username for user @dave'))); + }); + + test( + 'known_maintainers.txt single source of truth enforces Maintainer placement', + () { + final known = loadKnownExceptionMaintainers(); + expect( + known, + containsAll([ + 'CarlosMendonca', + 'LouiseHsu', + 'abdallahshaban557', + 'andywolff', + 'gaaclarke', + 'gmackall', + 'jesswrd', + ])); + + final badPlacementDoc = loadYaml(''' +maintainers: + googlers: + - username: Piinks + role: maintainer + partners: [] +committers: + - username: gaaclarke + role: committer +reviewers: [] +dash_emeritus: + - username: CarlosMendonca + role: emeritus +'''); + final errors = validateContributorsYaml(badPlacementDoc); + expect( + errors, + contains(contains( + '"@gaaclarke" is a known Maintainer and must be listed under maintainers.googlers'))); + expect( + errors, + contains(contains( + '"@CarlosMendonca" is a known Maintainer and must be listed under maintainers.googlers'))); + }); + + test( + 'buildRosterMarkdown, buildEmeritusMarkdown, and generate_roster CLI --check', + () { + final tempDir = Directory.systemTemp.createTempSync('roster_gen_test_'); + try { + final yamlFile = File('${tempDir.path}/contributors.yaml'); + final summaryFile = File('${tempDir.path}/activity_summary.json'); + final rosterFile = File('${tempDir.path}/ROSTER.md'); + final emeritusFile = File('${tempDir.path}/EMERITUS.md'); + + yamlFile.writeAsStringSync(''' +maintainers: + googlers: + - username: Piinks + role: maintainer + partners: + - username: robert-ancell + team: "@flutter/canonical" + role: maintainer + +committers: + - username: alice + role: committer + +reviewers: + - username: bob + role: reviewer + +dash_emeritus: + - username: formerDasher + role: emeritus +'''); + summaryFile.writeAsStringSync( + '{"alice": {"merged_prs": 15, "total": 40}, "bob": {"merged_prs": 3, "total": 12}}'); + + final parsedYaml = loadYaml(yamlFile.readAsStringSync()) as YamlMap; + expect(validateContributorsYaml(parsedYaml), isEmpty); + expect(buildRosterMarkdown(parsedYaml, {}), + contains('## Committers (`@flutter/committers`)')); + expect( + buildEmeritusMarkdown(parsedYaml), contains('# Dasher Emeritus')); + + // --check should fail before ROSTER.md and EMERITUS.md exist + final checkBefore = Process.runSync('dart', [ + 'run', + 'tool/generate_roster.dart', + '--check', + '--contributors', + yamlFile.path, + '--summary', + summaryFile.path, + '--roster', + rosterFile.path, + '--emeritus', + emeritusFile.path, + ]); + expect(checkBefore.exitCode, equals(1)); + + // Generate ROSTER.md and EMERITUS.md + final genResult = Process.runSync('dart', [ + 'run', + 'tool/generate_roster.dart', + '--contributors', + yamlFile.path, + '--summary', + summaryFile.path, + '--roster', + rosterFile.path, + '--emeritus', + emeritusFile.path, + ]); + expect(genResult.exitCode, equals(0)); + expect(rosterFile.existsSync(), isTrue); + expect(emeritusFile.existsSync(), isTrue); + + final rosterMd = rosterFile.readAsStringSync(); + expect(rosterMd, contains('[@Piinks](https://github.com/Piinks)')); + expect(rosterMd, + contains('[@robert-ancell](https://github.com/robert-ancell)')); + expect( + rosterMd, + contains( + '| @alice | [github.com/alice](https://github.com/alice) | 15 | 40 |')); + expect(rosterMd, isNot(contains('Prospective new joiners'))); + + final emeritusMd = emeritusFile.readAsStringSync(); + expect(emeritusMd, + contains('[@formerDasher](https://github.com/formerDasher)')); + expect(emeritusMd, contains('https://github.com/flutter/rfc')); + + // --check should now pass + final checkAfter = Process.runSync('dart', [ + 'run', + 'tool/generate_roster.dart', + '--check', + '--contributors', + yamlFile.path, + '--summary', + summaryFile.path, + '--roster', + rosterFile.path, + '--emeritus', + emeritusFile.path, + ]); + expect(checkAfter.exitCode, equals(0)); + } finally { + tempDir.deleteSync(recursive: true); + } + }); + }); +} diff --git a/tool/bots.dart b/tool/bots.dart index 33010ab..6cc121d 100644 --- a/tool/bots.dart +++ b/tool/bots.dart @@ -60,6 +60,7 @@ const Set knownBots = { 'agentfu', 'kenzie-agent', 'reidbaker-agent', + 'renzo-olivares-agent', 'google-admin', 'google-admin-automation-1', 'google-admin-automation-2', @@ -96,6 +97,7 @@ bool isBot(String username) { if (lower.endsWith('[bot]') || lower.endsWith('-bot') || lower.endsWith('_bot') || + lower.endsWith('-agent') || lower.contains('autoroll')) { return true; } diff --git a/tool/generate_roster.dart b/tool/generate_roster.dart new file mode 100644 index 0000000..18800cc --- /dev/null +++ b/tool/generate_roster.dart @@ -0,0 +1,200 @@ +// Copyright 2026 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; +import 'dart:io'; +import 'package:args/args.dart'; +import 'package:yaml/yaml.dart'; + +void main(List args) { + final parser = ArgParser() + ..addFlag('check', + help: + 'Verify that ROSTER.md and EMERITUS.md match data/contributors.yaml without modifying files', + defaultsTo: false) + ..addOption('contributors', + help: 'Path to contributors.yaml', defaultsTo: 'data/contributors.yaml') + ..addOption('summary', + help: 'Path to activity_summary.json', + defaultsTo: 'data/activity_summary.json') + ..addOption('roster', help: 'Path to ROSTER.md', defaultsTo: 'ROSTER.md') + ..addOption('emeritus', + help: 'Path to EMERITUS.md', defaultsTo: 'EMERITUS.md'); + + final results = parser.parse(args); + final isCheck = results['check'] as bool; + final contributorsPath = results['contributors'] as String; + final summaryPath = results['summary'] as String; + final rosterPath = results['roster'] as String; + final emeritusPath = results['emeritus'] as String; + + final yamlFile = File(contributorsPath); + if (!yamlFile.existsSync()) { + print('Error: $contributorsPath not found.'); + exit(1); + } + + final doc = loadYaml(yamlFile.readAsStringSync()) as YamlMap; + + final summaryFile = File(summaryPath); + var summary = {}; + if (summaryFile.existsSync()) { + try { + summary = + jsonDecode(summaryFile.readAsStringSync()) as Map; + } catch (_) {} + } + + String preservePendingAuditBanner(File targetFile, String generated) { + if (!targetFile.existsSync()) return generated; + final existing = targetFile.readAsStringSync(); + final match = RegExp( + r'\n(> \[!IMPORTANT\]\n> \*\*Pending Q4 Audit:\*\*[^\n]*\n)', + ).firstMatch(existing); + if (match == null) return generated; + final firstNewline = generated.indexOf('\n'); + if (firstNewline == -1) return generated; + return '${generated.substring(0, firstNewline + 1)}${match.group(0)}${generated.substring(firstNewline + 1)}'; + } + + final rosterFile = File(rosterPath); + final emeritusFile = File(emeritusPath); + final rosterContent = + preservePendingAuditBanner(rosterFile, buildRosterMarkdown(doc, summary)); + final emeritusContent = + preservePendingAuditBanner(emeritusFile, buildEmeritusMarkdown(doc)); + + if (isCheck) { + final rosterOutOfSync = !rosterFile.existsSync() || + rosterFile.readAsStringSync() != rosterContent; + final emeritusOutOfSync = !emeritusFile.existsSync() || + emeritusFile.readAsStringSync() != emeritusContent; + if (rosterOutOfSync || emeritusOutOfSync) { + print( + 'Error: ROSTER.md or EMERITUS.md is out of sync with data/contributors.yaml.'); + print('Run `dart run tool/generate_roster.dart` to update them.'); + exit(1); + } + print('ROSTER.md and EMERITUS.md are in sync with data/contributors.yaml.'); + } else { + rosterFile.writeAsStringSync(rosterContent); + emeritusFile.writeAsStringSync(emeritusContent); + print( + 'Generated ${rosterFile.path} and ${emeritusFile.path} successfully.'); + } +} + +List _formatGridRows(List usernames, {int columns = 4}) { + final rows = []; + for (var i = 0; i < usernames.length; i += columns) { + final row = [ + for (var j = 0; j < columns && i + j < usernames.length; j++) + '[@${usernames[i + j]}](https://github.com/${usernames[i + j]})', + ]; + rows.add(row.join(' • ')); + } + return rows; +} + +String buildRosterMarkdown(YamlMap doc, Map summary) { + final maintainers = doc['maintainers'] as YamlMap?; + final allMaintainers = { + if (maintainers != null) ...[ + for (final g in maintainers['googlers'] as YamlList? ?? []) + g['username'] as String, + for (final p in maintainers['partners'] as YamlList? ?? []) + p['username'] as String, + ], + }; + final sortedMaintainers = allMaintainers.toList() + ..sort((a, b) => a.toLowerCase().compareTo(b.toLowerCase())); + final maintainerRows = _formatGridRows(sortedMaintainers); + + final committers = doc['committers'] as YamlList? ?? []; + final committerRows = [ + for (final c in committers) + () { + final username = c['username'] as String; + final stats = summary[username] as Map?; + final prs = stats?['merged_prs'] ?? '-'; + final total = stats?['total'] ?? '-'; + return '| @$username | [github.com/$username](https://github.com/$username) | $prs | $total |'; + }(), + ]; + + final reviewers = doc['reviewers'] as YamlList? ?? []; + final reviewerRows = [ + for (final r in reviewers) + () { + final username = r['username'] as String; + final stats = summary[username] as Map?; + final prs = stats?['merged_prs'] ?? '-'; + final total = stats?['total'] ?? '-'; + return '| @$username | [github.com/$username](https://github.com/$username) | $prs | $total |'; + }(), + ]; + + return ''' +# Contributor Roster + +This directory lists active Flutter Contributor Ladder members. +Honorary alumni who have stepped back from active contribution and exited the GitHub organization are listed in [EMERITUS.md](EMERITUS.md). +Governance details and tier qualifications are specified in [docs/LADDER.md](docs/LADDER.md). + +> [!NOTE] +> This file is automatically generated from `data/contributors.yaml`. +> To update, edit `data/contributors.yaml` and run `dart run tool/generate_roster.dart`. + +## Maintainers (`@flutter/maintainers`) + +Maintainers hold GitHub Write permissions and organization moderation privileges, and belong to `@flutter/maintainers` (a child team of `@flutter/flutter-hackers`). + +${maintainerRows.join('\n')} + +## Committers (`@flutter/committers`) + +Committers hold GitHub Write permissions (merge rights) and provide authoritative code reviews. + +| Contributor | GitHub profile | 12m merged PRs | 12m total activity | +| :--- | :--- | :--- | :--- | +${committerRows.join('\n')} + +## Reviewers (`@flutter/reviewers`) + +Reviewers hold GitHub Triage permissions, triage issues, and conduct first-pass code reviews. + +| Contributor | GitHub profile | 12m merged PRs | 12m total activity | +| :--- | :--- | :--- | :--- | +${reviewerRows.join('\n')} + +## Dasher Emeritus + +Former ladder members who have stepped back from active contribution do not remain in the GitHub organization and are honored in [EMERITUS.md](EMERITUS.md). +'''; +} + +String buildEmeritusMarkdown(YamlMap doc) { + final emeriti = doc['dash_emeritus'] as YamlList? ?? []; + final sortedEmeriti = emeriti.map((e) => e['username'] as String).toList() + ..sort((a, b) => a.toLowerCase().compareTo(b.toLowerCase())); + final emeritusRows = _formatGridRows(sortedEmeriti); + + return ''' +# Dasher Emeritus + +This directory honors **Dasher Emeritus** alumni—former Flutter Reviewers, Committers, and Maintainers who previously held roles on the contributor ladder and have stepped back from active contribution (< 10 contributions over the preceding 12 months). + +To maintain repository security, Dasher Emeriti do not remain members of the `flutter` GitHub organization. Instead, their contributions are recognized here, and they retain access to Flutter contributor channels on Discord (including `#hidden-chat`) and the announcement-only `flutter-contributors` Google Group (for project announcements and calendar invites to our recurring Flutter Contributor Roundtable and the Dash Forum, where contributors can present and get feedback on their [RFCs](https://github.com/flutter/rfc); the registration form is pinned in `#hidden-chat` on Discord) so they can stay in touch and more easily ramp back up onto the project if and when they choose to return. + +Returning Emeriti who resume contributing are automatically identified during quarterly reviews (where they can reply directly on the public tracking issue). For off-cycle reinstatement, because creating new issues and pull requests in `flutter/contributors` is restricted to organization members (`collaborators`), returning Emeriti can reach out in `#hidden-chat` on Discord (or to an active organization member) to have an active org member sponsor and open a [Dasher Emeritus Reinstatement request](.github/ISSUE_TEMPLATE/2_emeritus_reinstatement.yml) on their behalf (where they can also share an updated Discord username if needed). See [docs/LADDER.md](docs/LADDER.md) for details. + +> [!NOTE] +> This file is automatically generated from `data/contributors.yaml`. +> To update, edit `data/contributors.yaml` and run `dart run tool/generate_roster.dart`. + +## Honored Alumni + +${emeritusRows.join('\n')} +'''; +} diff --git a/tool/validate_roster.dart b/tool/validate_roster.dart new file mode 100644 index 0000000..d9de0eb --- /dev/null +++ b/tool/validate_roster.dart @@ -0,0 +1,195 @@ +// Copyright 2026 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; +import 'package:yaml/yaml.dart'; +import 'bots.dart'; + +const validRoles = {'maintainer', 'committer', 'reviewer', 'emeritus'}; + +const knownMaintainersPath = + '.agents/skills/quarterly-ladder-analysis/references/known_maintainers.txt'; +const knownBotsPath = + '.agents/skills/quarterly-ladder-analysis/references/bots.txt'; + +/// Loads known bot/automation accounts from [knownBotsPath]. +Set loadKnownBotReferences({String path = knownBotsPath}) { + final file = File(path); + if (!file.existsSync()) return {}; + final result = {}; + for (final line in file.readAsLinesSync()) { + final trimmed = line.trim(); + if (trimmed.isNotEmpty && !trimmed.startsWith('#')) { + result.add(trimmed.toLowerCase()); + } + } + return result; +} + +/// Loads known exception Maintainers from the single source of truth file +/// ([knownMaintainersPath]). +Set loadKnownExceptionMaintainers( + {String path = knownMaintainersPath}) { + final file = File(path); + if (!file.existsSync()) return {}; + final result = {}; + for (final line in file.readAsLinesSync()) { + final trimmed = line.trim(); + if (trimmed.isNotEmpty && !trimmed.startsWith('#')) { + result.add(trimmed); + } + } + return result; +} + +/// Returns true if [username] is listed in [knownMaintainersPath]. +bool isKnownExceptionMaintainer(String username, + {Set? knownMaintainers}) { + final lower = username.toLowerCase().trim(); + final set = knownMaintainers ?? loadKnownExceptionMaintainers(); + return set.any((m) => m.toLowerCase() == lower); +} + +/// Validates a parsed `contributors.yaml` document and returns a list of +/// human-readable error strings (empty if valid). +List validateContributorsYaml(dynamic doc) { + if (doc is! YamlMap) { + return ['Error: data/contributors.yaml root must be a YAML mapping.']; + } + + final seenUsers = {}; + final errors = []; + final knownExceptionMaintainers = loadKnownExceptionMaintainers(); + final knownExceptionLower = + knownExceptionMaintainers.map((u) => u.toLowerCase()).toSet(); + final knownBotsLower = loadKnownBotReferences(); + + void validateUser(dynamic entry, String section, + {bool isMaintainer = false}) { + if (entry is! YamlMap) { + errors.add('Error in $section: entry is not a map: $entry'); + return; + } + + final username = entry['username'] as String?; + if (username == null || username.trim().isEmpty) { + errors.add('Error in $section: missing username: $entry'); + return; + } + + final lowerUser = username.toLowerCase(); + if (isExcludedAccount(username) || knownBotsLower.contains(lowerUser)) { + errors.add( + 'Error in $section: "@$username" is a bot or excluded automation account and must not be listed in contributors.yaml.'); + } + if (seenUsers.containsKey(lowerUser)) { + errors.add( + 'Error: duplicate username "@$username" in $section (already defined in ${seenUsers[lowerUser]})'); + } else { + seenUsers[lowerUser] = section; + } + + if (!isMaintainer && knownExceptionLower.contains(lowerUser)) { + errors.add( + 'Error in $section: "@$username" is a known Maintainer and must be listed under maintainers.googlers.'); + } + + final role = entry['role'] as String? ?? entry['suggested_role'] as String?; + if (role != null && !validRoles.contains(role.toLowerCase())) { + errors.add( + 'Error in $section: invalid role "$role" for user @$username. Valid roles: $validRoles'); + } + + if (entry.containsKey('discord')) { + final discord = entry['discord']; + if (discord is! String || discord.trim().isEmpty) { + errors.add( + 'Error in $section: invalid or empty discord username for user @$username.'); + } + } + + if (entry.containsKey('grace_until')) { + final graceUntil = entry['grace_until']; + if (graceUntil is! String || + DateTime.tryParse(graceUntil.trim()) == null) { + errors.add( + 'Error in $section: invalid grace_until date "$graceUntil" for user @$username (expected YYYY-MM-DD).'); + } + } + } + + // Validate Maintainers (must strictly be googlers or partners) + final maintainers = doc['maintainers']; + if (maintainers is YamlMap) { + for (final key in maintainers.keys) { + if (key != 'googlers' && key != 'partners') { + errors.add( + 'Error in maintainers: invalid group "$key". All Maintainers must be in "googlers" or "partners".'); + } + } + final googlers = maintainers['googlers']; + if (googlers is YamlList) { + for (final g in googlers) { + validateUser(g, 'maintainers.googlers', isMaintainer: true); + } + } + final partners = maintainers['partners']; + if (partners is YamlList) { + for (final p in partners) { + validateUser(p, 'maintainers.partners', isMaintainer: true); + } + } + } + + // Validate Committers + final committers = doc['committers']; + if (committers is YamlList) { + for (final c in committers) { + validateUser(c, 'committers'); + } + } + + // Validate Reviewers + final reviewers = doc['reviewers']; + if (reviewers is YamlList) { + for (final r in reviewers) { + validateUser(r, 'reviewers'); + } + } + + // Validate Dasher Emeritus + final emeriti = doc['dash_emeritus']; + if (emeriti is YamlList) { + for (final e in emeriti) { + validateUser(e, 'dash_emeritus'); + } + } + + return errors; +} + +void main(List args) { + final filePath = args.isNotEmpty ? args.first : 'data/contributors.yaml'; + final file = File(filePath); + if (!file.existsSync()) { + print('Error: $filePath does not exist.'); + exit(1); + } + + print('Validating $filePath...'); + final content = file.readAsStringSync(); + final dynamic doc = loadYaml(content); + + final errors = validateContributorsYaml(doc); + for (final err in errors) { + print(err); + } + + if (errors.isNotEmpty) { + print('\nValidation FAILED with ${errors.length} errors.'); + exit(1); + } + + print('Validation SUCCESS: $filePath is valid.'); +}