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..9df292b --- /dev/null +++ b/EMERITUS.md @@ -0,0 +1,68 @@ +# 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) • [@allisonryan0002](https://github.com/allisonryan0002) • [@almasryalyoum](https://github.com/almasryalyoum) • [@alxp1982](https://github.com/alxp1982) +[@anderdobo](https://github.com/anderdobo) • [@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) • [@chrisraygill](https://github.com/chrisraygill) • [@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) +[@darshankawar](https://github.com/darshankawar) • [@DBowen33](https://github.com/DBowen33) • [@dclark-disney](https://github.com/dclark-disney) • [@diarmaidlindsay](https://github.com/diarmaidlindsay) +[@DiegoGW](https://github.com/DiegoGW) • [@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) +[@eieio](https://github.com/eieio) • [@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) • [@filiph](https://github.com/filiph) • [@FirentisTFW](https://github.com/FirentisTFW) +[@FlutterIssues](https://github.com/FlutterIssues) • [@fma-goog](https://github.com/fma-goog) • [@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) • [@h9jiang](https://github.com/h9jiang) +[@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) • [@jesskuras](https://github.com/jesskuras) • [@jingshao-code](https://github.com/jingshao-code) +[@jnschulze](https://github.com/jnschulze) • [@jomblocoder](https://github.com/jomblocoder) • [@jorgecoca](https://github.com/jorgecoca) • [@josemontespg](https://github.com/josemontespg) +[@jpnurmi](https://github.com/jpnurmi) • [@JsouLiang](https://github.com/JsouLiang) • [@Justian-SopraSteria](https://github.com/Justian-SopraSteria) • [@jwinarske](https://github.com/jwinarske) +[@kaiceyd](https://github.com/kaiceyd) • [@kenvandine](https://github.com/kenvandine) • [@kevinburns426](https://github.com/kevinburns426) • [@kevya-google](https://github.com/kevya-google) +[@kinla004](https://github.com/kinla004) • [@Kirpal](https://github.com/Kirpal) • [@kkboateng](https://github.com/kkboateng) • [@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) • [@Markzipan](https://github.com/Markzipan) • [@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) • [@michaelkedar](https://github.com/michaelkedar) +[@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) • [@MuthuGCodes](https://github.com/MuthuGCodes) • [@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) • [@snikiche](https://github.com/snikiche) • [@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) +[@thkim1011](https://github.com/thkim1011) • [@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) • [@wakita181009](https://github.com/wakita181009) • [@wangying3426](https://github.com/wangying3426) +[@werainkhatri](https://github.com/werainkhatri) • [@whesse](https://github.com/whesse) • [@wolfenrain](https://github.com/wolfenrain) • [@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..eb2e925 --- /dev/null +++ b/ROSTER.md @@ -0,0 +1,104 @@ +# 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) • [@andrewkolos](https://github.com/andrewkolos) • [@andywolff](https://github.com/andywolff) • [@another-rex](https://github.com/another-rex) +[@antfitch](https://github.com/antfitch) • [@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) +[@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) • [@DanTup](https://github.com/DanTup) +[@danxorzum](https://github.com/danxorzum) • [@dcharkes](https://github.com/dcharkes) • [@di](https://github.com/di) • [@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) • [@elliette](https://github.com/elliette) • [@emckean](https://github.com/emckean) • [@ericwindmill](https://github.com/ericwindmill) +[@essjay05](https://github.com/essjay05) • [@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) • [@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) +[@inferno-chromium](https://github.com/inferno-chromium) • [@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) +[@jason-simmons](https://github.com/jason-simmons) • [@jensjoha](https://github.com/jensjoha) • [@jesswrd](https://github.com/jesswrd) • [@jfschmakeit](https://github.com/jfschmakeit) +[@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) • [@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) • [@kenzieschmoll](https://github.com/kenzieschmoll) • [@kevmoo](https://github.com/kevmoo) • [@khanhnwin](https://github.com/khanhnwin) +[@kjlubick](https://github.com/kjlubick) • [@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) • [@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) +[@miguelfrde](https://github.com/miguelfrde) • [@mikaelkomu](https://github.com/mikaelkomu) • [@mit-mit](https://github.com/mit-mit) • [@mkustermann](https://github.com/mkustermann) +[@mosuem](https://github.com/mosuem) • [@mraleph](https://github.com/mraleph) • [@munificent](https://github.com/munificent) • [@muralikrishmt](https://github.com/muralikrishmt) +[@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) • [@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) +[@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) • [@sgnagnarella](https://github.com/sgnagnarella) • [@shanecglass](https://github.com/shanecglass) • [@sigmundch](https://github.com/sigmundch) +[@sigurdm](https://github.com/sigurdm) • [@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) +[@stuartmorgan-g](https://github.com/stuartmorgan-g) • [@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) • [@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) • [@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) | 143 | 241 | +| @ValentinVignal | [github.com/ValentinVignal](https://github.com/ValentinVignal) | 29 | 46 | +| @bleroux | [github.com/bleroux](https://github.com/bleroux) | 20 | 31 | +| @davidhicks980 | [github.com/davidhicks980](https://github.com/davidhicks980) | 12 | 25 | +| @huycozy | [github.com/huycozy](https://github.com/huycozy) | 11 | 33 | +| @ksokolovskyi | [github.com/ksokolovskyi](https://github.com/ksokolovskyi) | 23 | 36 | +| @navaronbracke | [github.com/navaronbracke](https://github.com/navaronbracke) | 43 | 330 | +| @rkishan516 | [github.com/rkishan516](https://github.com/rkishan516) | 51 | 77 | +| @vickyduran | [github.com/vickyduran](https://github.com/vickyduran) | - | - | + +## 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 | +| :--- | :--- | :--- | :--- | +| @AlexV525 | [github.com/AlexV525](https://github.com/AlexV525) | 3 | 6 | +| @Hari-07 | [github.com/Hari-07](https://github.com/Hari-07) | 9 | 29 | +| @Jhon-Aires | [github.com/Jhon-Aires](https://github.com/Jhon-Aires) | - | - | +| @RobertOdrowaz | [github.com/RobertOdrowaz](https://github.com/RobertOdrowaz) | 7 | 12 | +| @csells | [github.com/csells](https://github.com/csells) | 11 | 22 | +| @danagbemava-nc | [github.com/danagbemava-nc](https://github.com/danagbemava-nc) | 0 | 8 | +| @htrolle-epam | [github.com/htrolle-epam](https://github.com/htrolle-epam) | 15 | 15 | +| @kentnguyen99 | [github.com/kentnguyen99](https://github.com/kentnguyen99) | 0 | 527 | +| @piedcipher | [github.com/piedcipher](https://github.com/piedcipher) | 7 | 16 | +| @tirth-patel-nc | [github.com/tirth-patel-nc](https://github.com/tirth-patel-nc) | 0 | 10 | + +## 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..fb3be21 --- /dev/null +++ b/data/contributors.yaml @@ -0,0 +1,891 @@ +# Flutter Contributor Ladder Roster +# Source of truth for Flutter contributor roles and permissions. +# Last updated: 2026-07-30 + +maintainers: + # Maintainers on the Flutter team (@flutter/googlers and @flutter/partners) + # Granted Write access + Org Moderation + googlers: + - username: CarlosMendonca + role: maintainer + - username: ChristianEdwardPadilla + role: maintainer + - username: GemmaJAy + role: maintainer + - username: InMatrix + role: maintainer + - username: LongCatIsLooong + role: maintainer + - username: LouiseHsu + role: maintainer + - username: MaryaBelanger + role: maintainer + - username: Piinks + role: maintainer + - username: QuncCccccc + role: maintainer + - username: Renzo-Olivares + role: maintainer + - username: Rusino + role: maintainer + - username: aam + role: maintainer + - username: abdallahshaban557 + role: maintainer + - username: alexmarkov + role: maintainer + - username: amcasari + role: maintainer + - username: ananci + role: maintainer + - username: andrewkolos + role: maintainer + - username: andywolff + role: maintainer + - username: another-rex + role: maintainer + - username: antfitch + 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: 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: 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: 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: elliette + role: maintainer + - username: emckean + role: maintainer + - username: ericwindmill + role: maintainer + - username: essjay05 + role: maintainer + - username: everyplace + role: maintainer + - username: eyebrowsoffire + role: maintainer + - username: flar + role: maintainer + - username: flutter-zl + role: maintainer + - username: gaaclarke + 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: 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: inferno-chromium + 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: jason-simmons + role: maintainer + - username: jensjoha + role: maintainer + - username: jesswrd + role: maintainer + - username: jfschmakeit + 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: 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: kenzieschmoll + role: maintainer + - username: kevmoo + role: maintainer + - username: khanhnwin + role: maintainer + - username: kjlubick + role: maintainer + - username: lamek + role: maintainer + - username: leafpetersen + role: maintainer + - username: liamappelbe + role: maintainer + - username: loic-sharma + role: maintainer + - username: mariamhas + role: maintainer + - username: maryx + role: maintainer + - username: mboetger + role: maintainer + - username: mbrase + role: maintainer + - username: mdebbar + role: maintainer + - username: miguelfrde + role: maintainer + - username: mit-mit + role: maintainer + - username: mkustermann + role: maintainer + - username: mosuem + role: maintainer + - username: mraleph + role: maintainer + - username: munificent + 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: polina-c + role: maintainer + - username: pq + role: maintainer + - username: precision + role: maintainer + - username: redbrogdon + role: maintainer + - username: reidbaker + role: maintainer + - username: renaudd + role: maintainer + - username: renyou + role: maintainer + - username: rmacnak-google + role: maintainer + - username: rodydavis + role: maintainer + - username: rspier + role: maintainer + - username: scheglov + role: maintainer + - username: sethladd + role: maintainer + - username: sfshaza2 + role: maintainer + - username: sgnagnarella + role: maintainer + - username: shanecglass + role: maintainer + - username: sigmundch + role: maintainer + - username: sigurdm + 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: stuartmorgan-g + 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: 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: DanTup + team: "@flutter/partners" + role: maintainer + - username: Hixie + team: "@flutter/partners" + role: maintainer + - username: Saviq + team: "@flutter/canonical" + role: maintainer + - username: bdero + team: "@flutter/flutter_gpu" + role: maintainer + - username: danxorzum + team: "@flutter/virtusa" + role: maintainer + - username: dkbharti-glitch + team: "@flutter/techm" + 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: 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: samuli-codemate + team: "@flutter/codemate" + role: maintainer + - username: saurabh-mirajkar + team: "@flutter/tcs" + role: maintainer + - username: schultek + team: "@flutter/partners" + role: maintainer + - username: suryaKommana2662 + team: "@flutter/tcs" + role: maintainer + - username: utopicnarwhal + team: "@flutter/codemate" + role: maintainer + +committers: + # Members of @flutter/committers (Child of @flutter/flutter-hackers) + # Granted GitHub Write role / merge access + - username: AbdeMohlbi + role: committer + - username: ValentinVignal + role: committer + - username: bleroux + role: committer + - username: davidhicks980 + role: committer + - username: huycozy + role: committer + - username: ksokolovskyi + role: committer + - username: navaronbracke + role: committer + - username: rkishan516 + role: committer + - username: vickyduran + role: committer + +reviewers: + # Members of @flutter/reviewers (Child of @flutter/flutter-hackers) + # Granted GitHub Triage role + - username: AlexV525 + role: reviewer + - username: Hari-07 + role: reviewer + - username: Jhon-Aires + role: reviewer + - username: RobertOdrowaz + role: reviewer + - username: csells + role: reviewer + - username: danagbemava-nc + role: reviewer + - username: htrolle-epam + grace_until: "2027-10-14" + role: reviewer + - username: kentnguyen99 + role: reviewer + - username: piedcipher + role: reviewer + - username: tirth-patel-nc + role: reviewer + +dash_emeritus: + # Honored alumni (External, non-org members on GitHub) + - username: 0xZOne + role: emeritus + - username: 324mao + role: emeritus + - username: ANTONINEUTRON + role: emeritus + - username: AnnaPS + role: emeritus + - username: AntonyKiragu + role: emeritus + - username: AustinGTI + role: emeritus + - username: AyushBherwani1998 + role: emeritus + - username: BerkSpar + role: emeritus + - username: CareF + role: emeritus + - username: CoffeeDevUK + role: emeritus + - username: ColdPaleLight + role: emeritus + - username: DBowen33 + role: emeritus + - username: DiegoGW + role: emeritus + - username: Ehesp + role: emeritus + - username: FXschwartz + role: emeritus + - username: FirentisTFW + role: emeritus + - username: FlutterIssues + role: emeritus + - username: HidenoriMatsubayashi + role: emeritus + - username: Hiroaki-Kanai + role: emeritus + - username: IanSimpson + role: emeritus + - username: JErazo7 + role: emeritus + - username: Jan-Stepien + role: emeritus + - username: JsouLiang + role: emeritus + - username: Justian-SopraSteria + role: emeritus + - username: Kirpal + role: emeritus + - username: KristinBi + role: emeritus + - username: LinXunFeng + role: emeritus + - username: MacarenaSantos + role: emeritus + - username: Markzipan + role: emeritus + - username: MaruQuarky + role: emeritus + - username: MauriPastorini + role: emeritus + - username: Menshoz + role: emeritus + - username: MrUnfunny + role: emeritus + - username: MuthuGCodes + role: emeritus + - username: PoojaB26 + role: emeritus + - username: QMAshfaqAli + role: emeritus + - username: Rexios80 + role: emeritus + - username: Salakar + role: emeritus + - username: SenseiMinimal + role: emeritus + - username: SofiaRey + role: emeritus + - username: Swiftaxe + role: emeritus + - username: TimoPieti + role: emeritus + - username: Vanethos + role: emeritus + - username: YeungKC + role: emeritus + - username: Zudah228 + 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: allisonryan0002 + role: emeritus + - username: almasryalyoum + role: emeritus + - username: alxp1982 + role: emeritus + - username: anderdobo + role: emeritus + - username: antonellof + role: emeritus + - username: atrope + role: emeritus + - username: azchohfi + role: emeritus + - username: bartekpacia + 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: chenglu + role: emeritus + - username: chinmoy12c + role: emeritus + - username: chriscraws + role: emeritus + - username: chrisraygill + role: emeritus + - username: cityxsuperapp + role: emeritus + - username: collinjackson + role: emeritus + - username: dam-ease + role: emeritus + - username: darshankawar + role: emeritus + - username: dclark-disney + role: emeritus + - username: diarmaidlindsay + 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: eieio + 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: filiph + role: emeritus + - username: fma-goog + role: emeritus + - username: gastonliz + role: emeritus + - username: gianfrancopapa + role: emeritus + - username: gnprice + role: emeritus + - username: guoguo338 + role: emeritus + - username: h9jiang + role: emeritus + - username: hamdikahloun + role: emeritus + - username: harri35 + role: emeritus + - username: hbatagelo + role: emeritus + - username: helinanever + role: emeritus + - username: hgraceb + role: emeritus + - username: hillelcoren + role: emeritus + - username: horiaciu + role: emeritus + - username: ihorkozar + role: emeritus + - username: irmasantoso + role: emeritus + - username: itsjen + role: emeritus + - username: ja-dfir + role: emeritus + - username: jcollins-disney + role: emeritus + - username: jeroen-meijer + role: emeritus + - username: jesskuras + role: emeritus + - username: jingshao-code + role: emeritus + - username: jnschulze + role: emeritus + - username: jomblocoder + role: emeritus + - username: jorgecoca + role: emeritus + - username: josemontespg + role: emeritus + - username: jpnurmi + role: emeritus + - username: jwinarske + role: emeritus + - username: kaiceyd + role: emeritus + - username: kenvandine + role: emeritus + - username: kevinburns426 + role: emeritus + - username: kevya-google + role: emeritus + - username: kinla004 + role: emeritus + - username: kkboateng + role: emeritus + - username: kmillikin + role: emeritus + - username: kokoro-team + role: emeritus + - username: kondoakio + role: emeritus + - username: kwg4 + role: emeritus + - username: laramartin + role: emeritus + - username: leoafarias + role: emeritus + - username: linxuebin1990 + role: emeritus + - username: lucavenir + role: emeritus + - username: luckysmg + role: emeritus + - username: magicianA + role: emeritus + - username: maheshj01 + role: emeritus + - username: martinduva + 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: maxp89 + role: emeritus + - username: mchudy + role: emeritus + - username: mhadaily + role: emeritus + - username: michaelkedar + role: emeritus + - username: milezabaleta + role: emeritus + - username: miquelbeltran + role: emeritus + - username: moffatman + 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: rahulan + role: emeritus + - username: rajayogan + role: emeritus + - username: renefloor + role: emeritus + - username: rzambroni + role: emeritus + - username: scarletteliza + role: emeritus + - username: scutlight + role: emeritus + - username: shirbayat + role: emeritus + - username: simpson-peter + role: emeritus + - username: slightfoot + role: emeritus + - username: snikiche + 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: tanercc + role: emeritus + - username: tcannon-disney + role: emeritus + - username: tfabacher-dp + role: emeritus + - username: tgucio + role: emeritus + - username: thekorn + role: emeritus + - username: thkim1011 + 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: wakita181009 + role: emeritus + - username: wangying3426 + role: emeritus + - username: werainkhatri + role: emeritus + - username: whesse + role: emeritus + - username: wolfenrain + role: emeritus + - username: workerbee22 + role: emeritus + - username: wprater + role: emeritus + - username: xm0128 + role: emeritus + - username: xu-baolin + role: emeritus + - username: yiiim + role: emeritus + - username: ymgn-dev + role: emeritus + - username: zhongwuzw + role: emeritus diff --git a/test/roster_validation_test.dart b/test/roster_validation_test.dart new file mode 100644 index 0000000..ab93f76 --- /dev/null +++ b/test/roster_validation_test.dart @@ -0,0 +1,292 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// 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/seed_initial_data.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'); + + writeContributorsYaml( + googlers: ['Piinks'], + partners: ['robert-ancell'], + partnerTeams: {'robert-ancell': '@flutter/canonical'}, + committers: ['alice'], + reviewers: ['bob'], + emeriti: ['formerDasher'], + outputFile: yamlFile, + ); + 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/generate_roster.dart b/tool/generate_roster.dart new file mode 100644 index 0000000..494ecb1 --- /dev/null +++ b/tool/generate_roster.dart @@ -0,0 +1,200 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// 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/seed_initial_data.dart b/tool/seed_initial_data.dart new file mode 100644 index 0000000..d4c031b --- /dev/null +++ b/tool/seed_initial_data.dart @@ -0,0 +1,480 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// 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 'bots.dart'; +import 'github.dart'; +import 'validate_roster.dart'; + +void main(List args) async { + final parser = ArgParser() + ..addFlag('live', + help: + 'Fetch live org membership from GitHub API instead of using historical baseline', + defaultsTo: false) + ..addOption('token', help: 'GitHub personal access token'); + + final results = parser.parse(args); + final isLive = results['live'] as bool; + final token = results['token'] as String?; + + print('=== Flutter Contributor Ladder Bootstrap & Seeding Tool ==='); + + final dataDir = Directory('data'); + if (!dataDir.existsSync()) { + dataDir.createSync(recursive: true); + } + final activityDir = Directory('data/activity'); + if (!activityDir.existsSync()) { + activityDir.createSync(recursive: true); + } + + if (isLive) { + await _runLiveBootstrap(token); + } else { + _runHistoricalBootstrap(); + } + + print( + '\nBootstrap complete! Run `dart run tool/generate_roster.dart` to generate ROSTER.md and EMERITUS.md.'); +} + +/// Runs bootstrap against live GitHub organization data. +Future _runLiveBootstrap(String? token) async { + final client = GitHubClient(token: token); + print('Querying Flutter GitHub organization members...'); + + try { + await loadRobotTeams(client); + // 1. Fetch all members of flutter org + final allOrgMembers = await client.getOrgMembers('flutter'); + print( + 'Found ${allOrgMembers.length} total members in flutter organization.'); + + // 2. Fetch Googlers and Partners teams (merged with known exception Maintainers) + print('Querying @flutter/googlers and @flutter/partners teams...'); + final googlers = { + ...await client.getTeamMembers('flutter', 'googlers'), + ...loadKnownExceptionMaintainers(), + }; + final partners = await client.getTeamMembers('flutter', 'partners'); + print('Found ${googlers.length} Googlers and ${partners.length} Partners.'); + + // Query child teams of @flutter/partners to resolve exact child team / POC handles + print('Resolving child teams of @flutter/partners...'); + final partnerTeamMap = {}; + try { + final childTeamSlugs = await client.getChildTeams('flutter', 'partners'); + for (final childSlug in childTeamSlugs) { + final childMembers = await client.getTeamMembers('flutter', childSlug); + for (final m in childMembers) { + partnerTeamMap[m] = '@flutter/$childSlug'; + } + } + print( + 'Resolved child teams for ${partnerTeamMap.length} partner members.'); + } catch (e) { + print('Notice: Could not resolve child teams: $e'); + } + + // 3. Separate Maintainers from Community Candidates + final maintainers = {...googlers, ...partners}; + final communityCandidates = allOrgMembers.difference(maintainers).toList() + ..sort(); + + print('Maintainers (Googlers + Partners): ${maintainers.length}'); + print( + 'Non-Googler/Non-Partner org members to evaluate: ${communityCandidates.length}'); + print( + 'API Optimization: Skipping activity queries for all ${maintainers.length} Maintainers!'); + + final now = DateTime.now().toUtc(); + final oneYearAgo = now.subtract(const Duration(days: 365)); + + print( + '\nEvaluating 12-month rolling activity (${oneYearAgo.toIso8601String().split('T').first} to ${now.toIso8601String().split('T').first}) using batched GraphQL queries...'); + + final activitySummary = await client.getBatchUserActivity( + communityCandidates, + since: oneYearAgo, + until: now); + + final committers = []; + final reviewers = []; + final emeriti = []; + + for (final member in communityCandidates) { + if (isExcludedAccount(member)) continue; + final activity = activitySummary[member] ?? + {'total': 0, 'merged_prs': 0, 'issues': 0, 'reviews': 0}; + final total = activity['total'] ?? 0; + final prs = activity['merged_prs'] ?? 0; + + if (total >= 30 && prs >= 10) { + committers.add(member); + } else if (total >= 10) { + reviewers.add(member); + } else { + emeriti.add(member); + } + } + + print('Results for current org members:'); + print(' • Maintainers: ${maintainers.length}'); + print(' • Committers: ${committers.length}'); + print(' • Reviewers: ${reviewers.length}'); + print(' • Dasher Emeritus (Inactive): ${emeriti.length}'); + + // 4. External candidate discovery (non-org contributors with high activity) + print('\nDiscovering active external community candidates...'); + final prospective = []; + try { + final externalList = [ + 'Mairramer', + 'koji-1009', + ]; + final externalStats = await client.getBatchUserActivity(externalList, + since: oneYearAgo, until: now); + for (final entry in externalStats.entries) { + activitySummary[entry.key] = entry.value; + if ((entry.value['total'] ?? 0) >= 10) { + prospective.add(entry.key); + } + } + print( + 'Discovered ${prospective.length} active external candidates for invitation: $prospective'); + } catch (e) { + print('Notice: External discovery completed with defaults: $e'); + } + + // 5. Save data/activity_summary.json + final summaryFile = File('data/activity_summary.json'); + summaryFile.writeAsStringSync( + const JsonEncoder.withIndent(' ').convert(activitySummary)); + print('Saved ${summaryFile.path}'); + + // 6. Write data/contributors.yaml + writeContributorsYaml( + googlers: googlers.toList()..sort(), + partners: partners.toList()..sort(), + partnerTeams: partnerTeamMap, + committers: committers..sort(), + reviewers: reviewers..sort(), + emeriti: emeriti..sort(), + ); + } finally { + client.close(); + } +} + +/// Runs bootstrap using the vetted historical analysis from the proposal. +void _runHistoricalBootstrap() { + print( + 'Seeding Day 1 roster and baseline metrics from approved proposal data...'); + + // Canonical engineers / Partners + final partners = [ + 'kenvandine', + 'robert-ancell', + ]; + + // Active Committers from proposal analysis (>= 30 total, >= 10 PRs) + final committers = [ + 'AbdeMohlbi', + 'AlexV525', + 'DanTup', + 'Jhon-Aires', + 'RobertOdrowaz', + 'ValentinVignal', + 'bleroux', + 'csells', + 'davidhicks980', + 'huycozy', + 'knopp', + 'ksokolovskyi', + 'mattkae', + 'navaronbracke', + 'parlough', + 'rkishan516', + 'schultek', + 'vickyduran', + ]; + + // Active Reviewers from proposal analysis (>= 10 total) + final reviewers = [ + 'bdero', + 'danagbemava-nc', + 'filiph', + 'gnprice', + 'nt4f04uNd', + 'pedromassango', + 'piedcipher', + 'tirth-patel-nc', + 'yiiim', + ]; + + // Dasher Emeriti (< 10 total in 12m historical analysis, bots filtered out) + final emeriti = [ + '0xZOne', + 'AnnaPS', + 'AyushBherwani1998', + 'CareF', + 'ColdPaleLight', + 'DiegoGW', + 'Ehesp', + 'FXschwartz', + 'FirentisTFW', + 'Hari-07', + 'HidenoriMatsubayashi', + 'Hixie', + 'Jan-Stepien', + 'JsouLiang', + 'Kirpal', + 'KristinBi', + 'LinXunFeng', + 'MacarenaSantos', + 'Rexios80', + 'Salakar', + 'SofiaRey', + 'Swiftaxe', + 'Vanethos', + 'YeungKC', + 'a14n', + 'alestiago', + 'allisonryan0002', + 'atrope', + 'azchohfi', + 'bartekpacia', + 'bernaferrari', + 'brianegan', + 'bruno-garcia', + 'bselwe', + 'btobin', + 'chenglu', + 'chinmoy12c', + 'chriscraws', + 'collinjackson', + 'dam-ease', + 'darshankawar', + 'eggfly', + 'ened', + 'erickzanardo', + 'eseidel', + 'exaby73', + 'felangel', + 'fluttercerberus', + 'gastonliz', + 'guoguo338', + 'hamdikahloun', + 'harri35', + 'hbatagelo', + 'helinanever', + 'hgraceb', + 'horiaciu', + 'jingshao-code', + 'jnschulze', + 'jokerttu', + 'jpnurmi', + 'jwinarske', + 'kaiceyd', + 'kevinburns426', + 'kmillikin', + 'linxuebin1990', + 'luckysmg', + 'magicianA', + 'martinduva', + 'marwfair', + 'matiasleyba', + 'matthew-carroll', + 'mbcorona', + 'mchudy', + 'milezabaleta', + 'miquelbeltran', + 'moffatman', + 'mvanbeusekom', + 'omartinma', + 'pingbird', + 'puneetkukreja98', + 'renefloor', + 'rzambroni', + 'saurabh-mirajkar', + 'scarletteliza', + 'scutlight', + 'simpson-peter', + 'slightfoot', + 'tgucio', + 'tpiaggio', + 'vaind', + 'valentinallavayol', + 'wangying3426', + 'werainkhatri', + 'whesse', + 'wolfenrain', + 'xu-baolin', + 'zhongwuzw', + ]..removeWhere(isBot); + + // Googlers placeholder list for baseline (managed via @flutter/googlers on GitHub) + final googlers = [ + 'Piinks', + ]; + + // Seed baseline metrics dictionary + final activitySummary = >{ + 'AbdeMohlbi': { + 'merged_prs': 158, + 'issues': 10, + 'reviews': 12, + 'total': 180 + }, + 'parlough': { + 'merged_prs': 223, + 'issues': 155, + 'reviews': 300, + 'total': 678 + }, + 'bleroux': {'merged_prs': 78, 'issues': 20, 'reviews': 27, 'total': 125}, + 'ksokolovskyi': { + 'merged_prs': 54, + 'issues': 10, + 'reviews': 21, + 'total': 85 + }, + 'robert-ancell': { + 'merged_prs': 57, + 'issues': 12, + 'reviews': 15, + 'total': 84 + }, + 'ValentinVignal': { + 'merged_prs': 53, + 'issues': 10, + 'reviews': 20, + 'total': 83 + }, + 'navaronbracke': { + 'merged_prs': 24, + 'issues': 20, + 'reviews': 35, + 'total': 79 + }, + 'rkishan516': {'merged_prs': 69, 'issues': 2, 'reviews': 2, 'total': 73}, + 'DanTup': {'merged_prs': 16, 'issues': 20, 'reviews': 36, 'total': 72}, + 'huycozy': {'merged_prs': 34, 'issues': 15, 'reviews': 20, 'total': 69}, + 'csells': {'merged_prs': 29, 'issues': 16, 'reviews': 20, 'total': 65}, + 'schultek': {'merged_prs': 46, 'issues': 8, 'reviews': 10, 'total': 64}, + 'Jhon-Aires': {'merged_prs': 47, 'issues': 5, 'reviews': 4, 'total': 56}, + 'RobertOdrowaz': {'merged_prs': 35, 'issues': 5, 'reviews': 5, 'total': 45}, + 'AlexV525': {'merged_prs': 18, 'issues': 10, 'reviews': 10, 'total': 38}, + 'knopp': {'merged_prs': 22, 'issues': 6, 'reviews': 10, 'total': 38}, + 'davidhicks980': { + 'merged_prs': 11, + 'issues': 10, + 'reviews': 12, + 'total': 33 + }, + 'vickyduran': {'merged_prs': 24, 'issues': 4, 'reviews': 5, 'total': 33}, + 'mattkae': {'merged_prs': 29, 'issues': 30, 'reviews': 36, 'total': 95}, + 'tirth-patel-nc': { + 'merged_prs': 5, + 'issues': 15, + 'reviews': 10, + 'total': 30 + }, + 'gnprice': {'merged_prs': 5, 'issues': 12, 'reviews': 10, 'total': 27}, + 'danagbemava-nc': { + 'merged_prs': 0, + 'issues': 15, + 'reviews': 8, + 'total': 23 + }, + 'Mairramer': {'merged_prs': 13, 'issues': 4, 'reviews': 4, 'total': 21}, + 'piedcipher': {'merged_prs': 9, 'issues': 5, 'reviews': 5, 'total': 19}, + 'yiiim': {'merged_prs': 14, 'issues': 1, 'reviews': 2, 'total': 17}, + 'filiph': {'merged_prs': 5, 'issues': 6, 'reviews': 5, 'total': 16}, + 'koji-1009': {'merged_prs': 13, 'issues': 1, 'reviews': 1, 'total': 15}, + 'nate-thegrate': {'merged_prs': 5, 'issues': 4, 'reviews': 4, 'total': 13}, + 'nt4f04uNd': {'merged_prs': 1, 'issues': 5, 'reviews': 5, 'total': 11}, + 'pedromassango': {'merged_prs': 6, 'issues': 3, 'reviews': 2, 'total': 11}, + 'bdero': {'merged_prs': 5, 'issues': 3, 'reviews': 2, 'total': 10}, + }; + + final summaryFile = File('data/activity_summary.json'); + summaryFile.writeAsStringSync( + const JsonEncoder.withIndent(' ').convert(activitySummary)); + print('Wrote ${summaryFile.path}'); + + writeContributorsYaml( + googlers: googlers, + partners: partners, + committers: committers, + reviewers: reviewers, + emeriti: emeriti, + ); +} + +void writeContributorsYaml({ + required List googlers, + required List partners, + Map? partnerTeams, + required List committers, + required List reviewers, + required List emeriti, + File? outputFile, +}) { + final today = DateTime.now().toUtc().toIso8601String().split('T').first; + final googlerEntries = [ + for (final g in googlers) ' - username: $g\n role: maintainer', + ].join('\n'); + final partnerEntries = [ + for (final p in partners) + ' - username: $p\n' + ' team: "${partnerTeams?[p] ?? '@flutter/partners'}"\n' + ' role: maintainer', + ].join('\n'); + final committerEntries = [ + for (final c in committers) ' - username: $c\n role: committer', + ].join('\n'); + final reviewerEntries = [ + for (final r in reviewers) ' - username: $r\n role: reviewer', + ].join('\n'); + final emeritusEntries = [ + for (final e in emeriti) ' - username: $e\n role: emeritus', + ].join('\n'); + + final content = ''' +# Flutter Contributor Ladder Roster +# Source of truth for Flutter contributor roles and permissions. +# Last updated: $today + +maintainers: + # Maintainers on the Flutter team (@flutter/googlers and @flutter/partners) + # Granted Write access + Org Moderation + googlers: +$googlerEntries + partners: +$partnerEntries + +committers: + # Members of @flutter/committers (Child of @flutter/flutter-hackers) + # Granted GitHub Write role / merge access +$committerEntries + +reviewers: + # Members of @flutter/reviewers (Child of @flutter/flutter-hackers) + # Granted GitHub Triage role +$reviewerEntries + +dash_emeritus: + # Honored alumni (External, non-org members on GitHub) +$emeritusEntries +'''; + + final file = outputFile ?? File('data/contributors.yaml'); + file.writeAsStringSync(content); + print( + 'Wrote ${file.path} (${committers.length} Committers, ${reviewers.length} Reviewers, ${emeriti.length} Emeriti).'); +} diff --git a/tool/validate_roster.dart b/tool/validate_roster.dart new file mode 100644 index 0000000..f9aeda7 --- /dev/null +++ b/tool/validate_roster.dart @@ -0,0 +1,195 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// 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.'); +}