From ea62413a0edb4a3c2dfb3dea438a8f1c3edccaab Mon Sep 17 00:00:00 2001 From: Seun Akanni Date: Tue, 7 Jul 2026 14:13:02 +0100 Subject: [PATCH] feat(wall): improve styling and persist contributor roster Styling: retina avatars, even 7-col grid with fixed cell widths, hover tooltips, alt text, centred header and badge. Roster: profile/wall_roster.json is merged forward on every run so contributors are never dropped when repos are archived, deleted or transferred, or when accounts and commit emails go away. Archived repos are now included in the sweep. Roster bootstrapped from the 2026-07-01 wall plus arvidsigurdsson, who was silently dropped by the July refresh. --- .github/workflows/wall-of-honour.yml | 4 +- profile/README.md | 1057 +++++++++++++------------- profile/wall_roster.json | 455 +++++++++++ scripts/generate_wall_of_honour.py | 81 +- scripts/tests/test_generate.py | 54 +- 5 files changed, 1108 insertions(+), 543 deletions(-) create mode 100644 profile/wall_roster.json diff --git a/.github/workflows/wall-of-honour.yml b/.github/workflows/wall-of-honour.yml index 43668e0..4e5a8d1 100644 --- a/.github/workflows/wall-of-honour.yml +++ b/.github/workflows/wall-of-honour.yml @@ -29,10 +29,10 @@ jobs: GITHUB_ORG: BHoM - name: Commit if changed run: | - if [[ -n "$(git status --porcelain profile/README.md)" ]]; then + if [[ -n "$(git status --porcelain profile/README.md profile/wall_roster.json)" ]]; then git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add profile/README.md + git add profile/README.md profile/wall_roster.json git commit -m "chore(wall): refresh wall of honour" git push else diff --git a/profile/README.md b/profile/README.md index 445859f..48040a9 100644 --- a/profile/README.md +++ b/profile/README.md @@ -1,526 +1,531 @@ -## Welcome to the BHoM! - -The BHoM (Buildings and Habitats object Model) is a collaborative project for the AEC (Architecture, Engineering and Construction) industry that aims to improve how we design the Built Environment and human Habitats. - -BHoM is a collection of schemas, functionality and interoperability infrastructure, developed with the aim of improving the way we work and communicate in AEC. BHoM is crafted as transdisciplinary, software-agnostic and office/region/country-invariant. BHoM is powered by an active, diverse community and a wide range of contributors. -The whole BHoM project uses an open-source model for project architecture, co-creation and planning. Please explore, experiment and contribute to both the source code and the documentation! Open-source means we can feed off and pool our disparate knowledge, experience and expertise towards a common goal – better design and a better Built Environment and Habitats for everyone. - -#### Technical philosophy of the BHoM - -The BHoM integrates concepts from different languages, coding environments and platforms. It's designed to be compatible with both visual flow-based programming (e.g. [Grasshopper](https://simplyrhino.co.uk/3d-modelling-software/grasshopper), Excel) and programming (in particular, C#). This is to integrate well in existing workflows of professionals in the AEC industry, regardless of their level of computational proficiency. BHoM aims to combine the work of professional programmers with the valuable efforts of computational designers/engineers/architects and scripters, all in the same ecosystem. - -### Contributing - -Please see https://bhom.xyz/documentation/Contributing/! 🚀 - -### Documentation and Q&A - -All documentation can be found in https://bhom.xyz/documentation/! -We also have a space for questions and discussions: https://github.com/orgs/BHoM/discussions. - -Please let us know how can we improve it! Please raise GitHub Issues in the [Documentation repo](https://github.com/BHoM/documentation/issues) or start a [Discussion](https://github.com/orgs/BHoM/discussions). - - -## Our Contributors - -![Contributors](https://img.shields.io/badge/contributors-150-brightgreen) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
AaronWagner
-
-
Adam Sobieski
-
-
Agnes Lindblom
-
-
Ahmad Sater
-
-
Ahmed Elnagar
-
-
Al Fisher
-
-
Alaa Alfakara
-
-
Albin Bergh
-
-
Alessio Lombardi
-
-
Alex Radne
-
-
Alexis Santella
-
-
Andreas Bak
-
-
Andrew Engh
-
-
Anna Stigenberg
-
-
Anthony Morehouse
-
-
Antigoni Karaiskou
-
-
Arnaud Declercq
-
-
Arne Martensen
-
-
Aurora Jensen
-
-
Bing Wang
-
-
Carmen Fan
-
-
Charlie Brooker
-
-
Chris Self
-
-
Christopher Short
-
-
Clemens Preisinger
-
-
Conor Hubert
-
-
Courtney Boulter
-
-
Daniel Imms
-
-
Daniel Jonsson
-
-
DarienC
-
-
Deesha Tech
-
-
Desai Wang
-
-
DiellzaElshani
-
-
Dimitrie Stefanescu
-
-
Djamila Mamedova
-
-
Dmitry Zelenkin
-
-
Eddy Dalton
-
-
Eduardo Pignatelli
-
-
Einar Persson
-
-
Elina Usmanova
-
-
Ellie Adebowale
-
-
Emil Svedjer
-
-
Emma Sander
-
-
EnricoAntolini
-
-
Erida Bendo
-
-
Erik Forsberg
-
-
Erik Narhi
-
-
Evangelos Pantazis
-
-
felipebandeiramm
-
-
Felix Mallinder
-
-
Fraser Greenroyd
-
-
Frédéric Bourgeon
-
-
Geir Soderin
-
-
Gideon Tamir
-
-
Giorgio Carlo Roberto Albieri
-
-
Habiba Moubarak
-
-
Haiping
-
-
Hedvig Kjellander
-
-
Isak Larborn
-
-
Isak NĂ€slund
-
-
izzy lys
-
-
Jack Trainor
-
-
Jaco Bekker
-
-
James Ramsden
-
-
Johanna Isaksson
-
-
Johanna Olin
-
-
John Sloane
-
-
Jonathan Nillius
-
-
Jose Castro Aguilar
-
-
JS
-
-
jukkasus
-
-
Junjie
-
-
Kai Chelliah
-
-
Kalle Edstroem
-
-
Kalle Thorsager
-
-
Kalle Thorsager
-
-
Karolina Prusicka
-
-
Kartheek Mylavarapu
-
-
Kasia Wodzisz
-
-
Kayleigh Houde
-
-
Kenton Phillips
-
-
Kenton Phillips
-
-
Konrad Stolarski
-
-
Lina Eriksson
-
-
linhnam-nguyen
-
-
Liv Andersson
-
-
livand
-
-
Ludvig Borgenstam
-
-
Luis E. Fraguada
-
-
Madelene Sandmer
-
-
Maja Lindroth
-
-
Manuel Chafart
-
-
Maria Glimaker
-
-
Maria Roman
-
-
Maria Tjader
-
-
Mark Cunliffe
-
-
Martin B. Henriksen
-
-
Marvin Suen
-
-
Maryanne Wachter
-
-
Maryanne Wachter
-
-
Matilda Svensson
-
-
Matteo Cominetti
-
-
Matthew Swaidan
-
-
Maya Arora-Jonsson
-
-
Meinrad Recheis
-
-
Michael Hoehn
-
-
Michal Dengusiak
-
-
Michal Pekacki
-
-
MILeach
-
-
Milo Gillot
-
-
Mish Nuh
-
-
Mozhgan Kabiri Chimeh
-
-
Nathan Brown
-
-
Nazeefa Bushra
-
-
Nicola Smithies
-
-
non-manifold
-
-
Ofek Bruhis
-
-
Oliver Stainton
-
-
Oscar Borgström
-
-
Paul Poinet
-
-
Paul Richmond
-
-
Pawel Baran
-
-
Peter Harman
-
-
Peter Heywood
-
-
Peter Nugent
-
-
PiotrBuda
-
-
Puria Safari Hesari
-
-
Rasmus Canfjorden
-
-
Rene Schulte
-
-
Richard Whitfield
-
-
Rob May
-
-
Robert Chisholm
-
-
Robin Flyman
-
-
Roly Hudson
-
-
Ross Boulton
-
-
Sally Pickard
-
-
Sam Vazquez
-
-
Samuel Basimalla
-
-
Sofia Malmsten
-
-
StephennipBH
-
-
Thomas Edward Kingstone
-
-
Tiago Del Grossi
-
-
Tom Svilans
-
-
Toste Skanberg Dahlstedt
-
-
Tristan Gerrish
-
-
Valerie Green
-
-
Viet Le
-
-
Will Pearson
-
-
wojciech buda
-
-
Ɓukasz Markowski
-
- -_Last updated: 2026-07-01_ - +## Welcome to the BHoM! + +The BHoM (Buildings and Habitats object Model) is a collaborative project for the AEC (Architecture, Engineering and Construction) industry that aims to improve how we design the Built Environment and human Habitats. + +BHoM is a collection of schemas, functionality and interoperability infrastructure, developed with the aim of improving the way we work and communicate in AEC. BHoM is crafted as transdisciplinary, software-agnostic and office/region/country-invariant. BHoM is powered by an active, diverse community and a wide range of contributors. +The whole BHoM project uses an open-source model for project architecture, co-creation and planning. Please explore, experiment and contribute to both the source code and the documentation! Open-source means we can feed off and pool our disparate knowledge, experience and expertise towards a common goal – better design and a better Built Environment and Habitats for everyone. + +#### Technical philosophy of the BHoM + +The BHoM integrates concepts from different languages, coding environments and platforms. It's designed to be compatible with both visual flow-based programming (e.g. [Grasshopper](https://simplyrhino.co.uk/3d-modelling-software/grasshopper), Excel) and programming (in particular, C#). This is to integrate well in existing workflows of professionals in the AEC industry, regardless of their level of computational proficiency. BHoM aims to combine the work of professional programmers with the valuable efforts of computational designers/engineers/architects and scripters, all in the same ecosystem. + +### Contributing + +Please see https://bhom.xyz/documentation/Contributing/! 🚀 + +### Documentation and Q&A + +All documentation can be found in https://bhom.xyz/documentation/! +We also have a space for questions and discussions: https://github.com/orgs/BHoM/discussions. + +Please let us know how can we improve it! Please raise GitHub Issues in the [Documentation repo](https://github.com/BHoM/documentation/issues) or start a [Discussion](https://github.com/orgs/BHoM/discussions). + + +

Our Contributors

+ +

Contributors

+ +

Thank you to everyone who has contributed to the BHoM.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ AaronWagner
AaronWagner
+
+ Adam Sobieski
Adam Sobieski
+
+ Agnes Lindblom
Agnes Lindblom
+
+ Ahmad Sater
Ahmad Sater
+
+ Ahmed Elnagar
Ahmed Elnagar
+
+ Al Fisher
Al Fisher
+
+ Alaa Alfakara
Alaa Alfakara
+
+ Albin Bergh
Albin Bergh
+
+ Alessio Lombardi
Alessio Lombardi
+
+ Alex Radne
Alex Radne
+
+ Alexis Santella
Alexis Santella
+
+ Andreas Bak
Andreas Bak
+
+ Andrew Engh
Andrew Engh
+
+ Anna Stigenberg
Anna Stigenberg
+
+ Anthony Morehouse
Anthony Morehouse
+
+ Antigoni Karaiskou
Antigoni Karaiskou
+
+ Arnaud Declercq
Arnaud Declercq
+
+ Arne Martensen
Arne Martensen
+
+ Arvid Sigurdsson
Arvid Sigurdsson
+
+ Aurora Jensen
Aurora Jensen
+
+ Bing Wang
Bing Wang
+
+ Carmen Fan
Carmen Fan
+
+ Charlie Brooker
Charlie Brooker
+
+ Chris Self
Chris Self
+
+ Christopher Short
Christopher Short
+
+ Clemens Preisinger
Clemens Preisinger
+
+ Conor Hubert
Conor Hubert
+
+ Courtney Boulter
Courtney Boulter
+
+ Daniel Imms
Daniel Imms
+
+ Daniel Jonsson
Daniel Jonsson
+
+ DarienC
DarienC
+
+ Deesha Tech
Deesha Tech
+
+ Desai Wang
Desai Wang
+
+ DiellzaElshani
DiellzaElshani
+
+ Dimitrie Stefanescu
Dimitrie Stefanescu
+
+ Djamila Mamedova
Djamila Mamedova
+
+ Dmitry Zelenkin
Dmitry Zelenkin
+
+ Eddy Dalton
Eddy Dalton
+
+ Eduardo Pignatelli
Eduardo Pignatelli
+
+ Einar Persson
Einar Persson
+
+ Elina Usmanova
Elina Usmanova
+
+ Ellie Adebowale
Ellie Adebowale
+
+ Emil Svedjer
Emil Svedjer
+
+ Emma Sander
Emma Sander
+
+ EnricoAntolini
EnricoAntolini
+
+ Erida Bendo
Erida Bendo
+
+ Erik Forsberg
Erik Forsberg
+
+ Erik Narhi
Erik Narhi
+
+ Evangelos Pantazis
Evangelos Pantazis
+
+ felipebandeiramm
felipebandeiramm
+
+ Felix Mallinder
Felix Mallinder
+
+ Fraser Greenroyd
Fraser Greenroyd
+
+ Frédéric Bourgeon
Frédéric Bourgeon
+
+ Geir Soderin
Geir Soderin
+
+ Gideon Tamir
Gideon Tamir
+
+ Giorgio Carlo Roberto Albieri
Giorgio Carlo Roberto Albieri
+
+ Habiba Moubarak
Habiba Moubarak
+
+ Haiping
Haiping
+
+ Hedvig Kjellander
Hedvig Kjellander
+
+ Isak Larborn
Isak Larborn
+
+ Isak NĂ€slund
Isak NĂ€slund
+
+ izzy lys
izzy lys
+
+ Jack Trainor
Jack Trainor
+
+ Jaco Bekker
Jaco Bekker
+
+ James Ramsden
James Ramsden
+
+ Johanna Isaksson
Johanna Isaksson
+
+ Johanna Olin
Johanna Olin
+
+ John Sloane
John Sloane
+
+ Jonathan Nillius
Jonathan Nillius
+
+ Jose Castro Aguilar
Jose Castro Aguilar
+
+ JS
JS
+
+ jukkasus
jukkasus
+
+ Junjie
Junjie
+
+ Kai Chelliah
Kai Chelliah
+
+ Kalle Edstroem
Kalle Edstroem
+
+ Kalle Thorsager
Kalle Thorsager
+
+ Kalle Thorsager
Kalle Thorsager
+
+ Karolina Prusicka
Karolina Prusicka
+
+ Kartheek Mylavarapu
Kartheek Mylavarapu
+
+ Kasia Wodzisz
Kasia Wodzisz
+
+ Kayleigh Houde
Kayleigh Houde
+
+ Kenton Phillips
Kenton Phillips
+
+ Kenton Phillips
Kenton Phillips
+
+ Konrad Stolarski
Konrad Stolarski
+
+ Lina Eriksson
Lina Eriksson
+
+ linhnam-nguyen
linhnam-nguyen
+
+ Liv Andersson
Liv Andersson
+
+ livand
livand
+
+ Ludvig Borgenstam
Ludvig Borgenstam
+
+ Luis E. Fraguada
Luis E. Fraguada
+
+ Madelene Sandmer
Madelene Sandmer
+
+ Maja Lindroth
Maja Lindroth
+
+ Manuel Chafart
Manuel Chafart
+
+ Maria Glimaker
Maria Glimaker
+
+ Maria Roman
Maria Roman
+
+ Maria Tjader
Maria Tjader
+
+ Mark Cunliffe
Mark Cunliffe
+
+ Martin B. Henriksen
Martin B. Henriksen
+
+ Marvin Suen
Marvin Suen
+
+ Maryanne Wachter
Maryanne Wachter
+
+ Maryanne Wachter
Maryanne Wachter
+
+ Matilda Svensson
Matilda Svensson
+
+ Matteo Cominetti
Matteo Cominetti
+
+ Matthew Swaidan
Matthew Swaidan
+
+ Maya Arora-Jonsson
Maya Arora-Jonsson
+
+ Meinrad Recheis
Meinrad Recheis
+
+ Michael Hoehn
Michael Hoehn
+
+ Michal Dengusiak
Michal Dengusiak
+
+ Michal Pekacki
Michal Pekacki
+
+ MILeach
MILeach
+
+ Milo Gillot
Milo Gillot
+
+ Mish Nuh
Mish Nuh
+
+ Mozhgan Kabiri Chimeh
Mozhgan Kabiri Chimeh
+
+ Nathan Brown
Nathan Brown
+
+ Nazeefa Bushra
Nazeefa Bushra
+
+ Nicola Smithies
Nicola Smithies
+
+ non-manifold
non-manifold
+
+ Ofek Bruhis
Ofek Bruhis
+
+ Oliver Stainton
Oliver Stainton
+
+ Oscar Borgström
Oscar Borgström
+
+ Paul Poinet
Paul Poinet
+
+ Paul Richmond
Paul Richmond
+
+ Pawel Baran
Pawel Baran
+
+ Peter Harman
Peter Harman
+
+ Peter Heywood
Peter Heywood
+
+ Peter Nugent
Peter Nugent
+
+ PiotrBuda
PiotrBuda
+
+ Puria Safari Hesari
Puria Safari Hesari
+
+ Rasmus Canfjorden
Rasmus Canfjorden
+
+ Rene Schulte
Rene Schulte
+
+ Richard Whitfield
Richard Whitfield
+
+ Rob May
Rob May
+
+ Robert Chisholm
Robert Chisholm
+
+ Robin Flyman
Robin Flyman
+
+ Roly Hudson
Roly Hudson
+
+ Ross Boulton
Ross Boulton
+
+ Sally Pickard
Sally Pickard
+
+ Sam Vazquez
Sam Vazquez
+
+ Samuel Basimalla
Samuel Basimalla
+
+ Sofia Malmsten
Sofia Malmsten
+
+ StephennipBH
StephennipBH
+
+ Thomas Edward Kingstone
Thomas Edward Kingstone
+
+ Tiago Del Grossi
Tiago Del Grossi
+
+ Tom Svilans
Tom Svilans
+
+ Toste Skanberg Dahlstedt
Toste Skanberg Dahlstedt
+
+ Tristan Gerrish
Tristan Gerrish
+
+ Valerie Green
Valerie Green
+
+ Viet Le
Viet Le
+
+ Will Pearson
Will Pearson
+
+ wojciech buda
wojciech buda
+
+ Ɓukasz Markowski
Ɓukasz Markowski
+
+ +_Last updated: 2026-07-01_ + diff --git a/profile/wall_roster.json b/profile/wall_roster.json new file mode 100644 index 0000000..63c47cb --- /dev/null +++ b/profile/wall_roster.json @@ -0,0 +1,455 @@ +{ + "AhmadSater": { + "name": "Ahmad Sater" + }, + "AhmedElnagar1": { + "name": "Ahmed Elnagar" + }, + "AlaaAlfakara": { + "name": "Alaa Alfakara" + }, + "Arne-Martensen": { + "name": "Arne Martensen" + }, + "BingWangUS": { + "name": "Bing Wang" + }, + "CKBoulter": { + "name": "Courtney Boulter" + }, + "ChrisSelf2": { + "name": "Chris Self" + }, + "Chrisshort92": { + "name": "Christopher Short" + }, + "Clemens-Preisinger": { + "name": "Clemens Preisinger" + }, + "Conor-Hubert-BH": { + "name": "Conor Hubert" + }, + "DanielJonssonBH": { + "name": "Daniel Jonsson" + }, + "DarienC": { + "name": "DarienC" + }, + "DiellzaElshani": { + "name": "DiellzaElshani" + }, + "Dmamedova": { + "name": "Djamila Mamedova" + }, + "EKAdebo": { + "name": "Ellie Adebowale" + }, + "EddyDalton": { + "name": "Eddy Dalton" + }, + "EmmaSander": { + "name": "Emma Sander" + }, + "EnricoAntolini": { + "name": "EnricoAntolini" + }, + "Erida-Bendo": { + "name": "Erida Bendo" + }, + "Felix-Mallinder": { + "name": "Felix Mallinder" + }, + "FraserGreenroyd": { + "name": "Fraser Greenroyd" + }, + "FredericBourgeon": { + "name": "Frédéric Bourgeon" + }, + "GCRA101": { + "name": "Giorgio Carlo Roberto Albieri" + }, + "IsakLarbornBH": { + "name": "Isak Larborn" + }, + "IsakNaslundBh": { + "name": "Isak NÀslund" + }, + "JohannaOlin1": { + "name": "Johanna Olin" + }, + "JohnSloane": { + "name": "John Sloane" + }, + "JonathanNillius": { + "name": "Jonathan Nillius" + }, + "JoseAguilarBH": { + "name": "Jose Castro Aguilar" + }, + "KalleEdstroem": { + "name": "Kalle Edstroem" + }, + "Kar-Myl": { + "name": "Kartheek Mylavarapu" + }, + "Kenton-Phillips": { + "name": "Kenton Phillips" + }, + "KentonPhillips-BH": { + "name": "Kenton Phillips" + }, + "KonradStolarski": { + "name": "Konrad Stolarski" + }, + "LMarkowski": { + "name": "Ɓukasz Markowski" + }, + "LivAndersson": { + "name": "Liv Andersson" + }, + "MILeach": { + "name": "MILeach" + }, + "MajaLindroth": { + "name": "Maja Lindroth" + }, + "Maria1129": { + "name": "Maria Roman" + }, + "MariaGlimaker": { + "name": "Maria Glimaker" + }, + "Martian42": { + "name": "Marvin Suen" + }, + "MartinHenriksen": { + "name": "Martin B. Henriksen" + }, + "MatildaSvenssonBH": { + "name": "Matilda Svensson" + }, + "MayaAroraJonsson": { + "name": "Maya Arora-Jonsson" + }, + "Nazeefa710": { + "name": "Nazeefa Bushra" + }, + "Oceania2018": { + "name": "Haiping" + }, + "PaulPoinet": { + "name": "Paul Poinet" + }, + "PiotrBuda": { + "name": "PiotrBuda" + }, + "RasmusCanfjorden": { + "name": "Rasmus Canfjorden" + }, + "RichardWhitfield": { + "name": "Richard Whitfield" + }, + "Robadob": { + "name": "Robert Chisholm" + }, + "SallyPickard": { + "name": "Sally Pickard" + }, + "StephennipBH": { + "name": "StephennipBH" + }, + "Tom-Kingstone": { + "name": "Thomas Edward Kingstone" + }, + "TosteSkDa": { + "name": "Toste Skanberg Dahlstedt" + }, + "Tyriar": { + "name": "Daniel Imms" + }, + "WojciechBuda00": { + "name": "wojciech buda" + }, + "adam-sobieski": { + "name": "Adam Sobieski" + }, + "adecler": { + "name": "Arnaud Declercq" + }, + "agneslindblom": { + "name": "Agnes Lindblom" + }, + "ajensen19": { + "name": "Aurora Jensen" + }, + "akaraiskou": { + "name": "Antigoni Karaiskou" + }, + "al-fisher": { + "name": "Al Fisher" + }, + "albinber": { + "name": "Albin Bergh" + }, + "alelom": { + "name": "Alessio Lombardi" + }, + "alexissantella": { + "name": "Alexis Santella" + }, + "alexradne": { + "name": "Alex Radne" + }, + "amorehouse-bh": { + "name": "Anthony Morehouse" + }, + "andrbak": { + "name": "Andreas Bak" + }, + "andrewengh": { + "name": "Andrew Engh" + }, + "annastigenberg": { + "name": "Anna Stigenberg" + }, + "arvidsigurdsson": { + "name": "Arvid Sigurdsson" + }, + "carmenfan": { + "name": "Carmen Fan" + }, + "charliebrookerBH": { + "name": "Charlie Brooker" + }, + "deepakkumar1984": { + "name": "Deesha Tech" + }, + "desaiwangBH": { + "name": "Desai Wang" + }, + "didimitrie": { + "name": "Dimitrie Stefanescu" + }, + "dmitryzelenkin": { + "name": "Dmitry Zelenkin" + }, + "eUsmanovaBH": { + "name": "Elina Usmanova" + }, + "edforsberg": { + "name": "Erik Forsberg" + }, + "emidio-piermarini": { + "name": "JS" + }, + "emilsvedjer": { + "name": "Emil Svedjer" + }, + "enarhi": { + "name": "Erik Narhi" + }, + "epersson": { + "name": "Einar Persson" + }, + "epignatelli": { + "name": "Eduardo Pignatelli" + }, + "evanPantazis": { + "name": "Evangelos Pantazis" + }, + "felipebandeiramm": { + "name": "felipebandeiramm" + }, + "fraguada": { + "name": "Luis E. Fraguada" + }, + "geirsoderin": { + "name": "Geir Soderin" + }, + "gideontamir16": { + "name": "Gideon Tamir" + }, + "harmanpa": { + "name": "Peter Harman" + }, + "hedvigkjellander": { + "name": "Hedvig Kjellander" + }, + "henon": { + "name": "Meinrad Recheis" + }, + "hmoubarak": { + "name": "Habiba Moubarak" + }, + "izzylys": { + "name": "izzy lys" + }, + "jacosoft": { + "name": "Jaco Bekker" + }, + "jamesramsden-bh": { + "name": "James Ramsden" + }, + "johannaisak": { + "name": "Johanna Isaksson" + }, + "jtrainor1": { + "name": "Jack Trainor" + }, + "jukkasus": { + "name": "jukkasus" + }, + "junjieim": { + "name": "Junjie" + }, + "kThorsager": { + "name": "Kalle Thorsager" + }, + "kaichelliah": { + "name": "Kai Chelliah" + }, + "kasiawod": { + "name": "Kasia Wodzisz" + }, + "kayleighhoude": { + "name": "Kayleigh Houde" + }, + "kprusicka": { + "name": "Karolina Prusicka" + }, + "lborgenstam": { + "name": "Ludvig Borgenstam" + }, + "linaeriksson": { + "name": "Lina Eriksson" + }, + "linhnam-nguyen": { + "name": "linhnam-nguyen" + }, + "livand": { + "name": "livand" + }, + "m-clare": { + "name": "Maryanne Wachter" + }, + "mariatjader": { + "name": "Maria Tjader" + }, + "maryannewachter": { + "name": "Maryanne Wachter" + }, + "mchaf": { + "name": "Manuel Chafart" + }, + "mcunliffe84": { + "name": "Mark Cunliffe" + }, + "michaelhoehn": { + "name": "Michael Hoehn" + }, + "michal-pekacki": { + "name": "Michal Pekacki" + }, + "michaldengusiak": { + "name": "Michal Dengusiak" + }, + "milogillot-BH": { + "name": "Milo Gillot" + }, + "mishaelnuh": { + "name": "Mish Nuh" + }, + "mondus": { + "name": "Paul Richmond" + }, + "mozhgan-kch": { + "name": "Mozhgan Kabiri Chimeh" + }, + "msandmer": { + "name": "Madelene Sandmer" + }, + "mswaidan": { + "name": "Matthew Swaidan" + }, + "ncbrown2": { + "name": "Nathan Brown" + }, + "njsmithies": { + "name": "Nicola Smithies" + }, + "non-manifold": { + "name": "non-manifold" + }, + "ofekbruhis": { + "name": "Ofek Bruhis" + }, + "oscarborgstrom": { + "name": "Oscar Borgström" + }, + "pawelbaran": { + "name": "Pawel Baran" + }, + "pearswj": { + "name": "Will Pearson" + }, + "peterjamesnugent": { + "name": "Peter Nugent" + }, + "polnischfrosch": { + "name": "AaronWagner" + }, + "ptheywood": { + "name": "Peter Heywood" + }, + "puriasafarihesari": { + "name": "Puria Safari Hesari" + }, + "rboulton-BH": { + "name": "Ross Boulton" + }, + "reneschulte": { + "name": "Rene Schulte" + }, + "robinflyman": { + "name": "Robin Flyman" + }, + "rolyhudson": { + "name": "Roly Hudson" + }, + "rwemay": { + "name": "Rob May" + }, + "samuelbasimalla": { + "name": "Samuel Basimalla" + }, + "sofmal": { + "name": "Sofia Malmsten" + }, + "staintono": { + "name": "Oliver Stainton" + }, + "svaz00": { + "name": "Sam Vazquez" + }, + "teocomi": { + "name": "Matteo Cominetti" + }, + "tg359": { + "name": "Tristan Gerrish" + }, + "theThorsager": { + "name": "Kalle Thorsager" + }, + "tiagogrossi": { + "name": "Tiago Del Grossi" + }, + "tsvilans": { + "name": "Tom Svilans" + }, + "vgreen-BH": { + "name": "Valerie Green" + }, + "vietle-bh": { + "name": "Viet Le" + } +} diff --git a/scripts/generate_wall_of_honour.py b/scripts/generate_wall_of_honour.py index 703f2f7..157db66 100644 --- a/scripts/generate_wall_of_honour.py +++ b/scripts/generate_wall_of_honour.py @@ -1,6 +1,7 @@ """Generate BHoM Wall of Honour and splice into profile/README.md.""" from __future__ import annotations +import json import os import re import sys @@ -15,10 +16,13 @@ def list_org_repos(session: requests.Session, org: str) -> list[dict[str, Any]]: - """List non-archived repos in `org`, excluding the org's `.github` repo.""" + """List repos in `org`, excluding the org's `.github` repo. + + Archived repos are included so their contributors stay on the wall. + """ url = f"{GITHUB_API}/orgs/{org}/repos" all_repos = paginated_get(session, url, params={"per_page": 100, "type": "all"}) - return [r for r in all_repos if not r["archived"] and r["name"] != ".github"] + return [r for r in all_repos if r["name"] != ".github"] def fetch_repo_contributors(session: requests.Session, org: str, repo: str) -> list[dict[str, Any]]: @@ -86,7 +90,11 @@ def enrich_display_names( def _contributors_badge(count: int) -> str: color = "lightgrey" if count == 0 else "brightgreen" - return f"![Contributors](https://img.shields.io/badge/contributors-{count}-{color})" + # HTML img rather than markdown ![]() so it renders inside the

wrapper + return ( + f"\"Contributors\"" + ) def render_wall(contributors: dict[str, dict[str, Any]], last_updated: str) -> str: @@ -96,8 +104,8 @@ def render_wall(contributors: dict[str, dict[str, Any]], last_updated: str) -> s if not contributors: return ( "\n" - "## Our Contributors\n\n" - f"{badge}\n\n" + "

Our Contributors

\n\n" + f"

{badge}

\n\n" "Wall coming soon. No contributors yet.\n\n" f"_Last updated: {last_updated}_\n" "" @@ -116,8 +124,9 @@ def render_wall(contributors: dict[str, dict[str, Any]], last_updated: str) -> s return ( "\n" - "## Our Contributors\n\n" - f"{badge}\n\n" + "

Our Contributors

\n\n" + f"

{badge}

\n\n" + "

Thank you to everyone who has contributed to the BHoM.

\n\n" "\n" + "\n".join(rows) + "\n
\n\n" @@ -128,16 +137,57 @@ def render_wall(contributors: dict[str, dict[str, Any]], last_updated: str) -> s def _render_cell(login: str, info: dict[str, Any]) -> str: name = info["name"] + cell_width = f"{100 / GRID_COLS:.2f}%" + # Request the avatar at 2x display size so it stays sharp on hi-DPI screens return ( - " \n" - f" " - f"
" + f" \n" + f"
" + f"\"{name}\"
" f"{name}
\n" " " ) +DEFAULT_ROSTER_PATH = "profile/wall_roster.json" + + +def load_roster(roster_path: str) -> dict[str, dict[str, Any]]: + """Load the persisted roster; missing file means an empty roster.""" + path = Path(roster_path) + if not path.exists(): + return {} + return json.loads(path.read_text(encoding="utf-8")) + + +def save_roster(roster_path: str, roster: dict[str, dict[str, Any]]) -> None: + """Write the roster as stable, diff-friendly JSON.""" + path = Path(roster_path) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text( + json.dumps(roster, indent=2, sort_keys=True, ensure_ascii=False) + "\n", + encoding="utf-8", + ) + + +def merge_into_roster( + roster: dict[str, dict[str, Any]], + live: dict[str, dict[str, Any]], +) -> dict[str, dict[str, Any]]: + """Union live contributors into the roster. Live data wins; absent logins persist. + + Denylisted logins are dropped even if a past run added them. + """ + merged = { + login: {"name": info["name"]} + for login, info in roster.items() + if login not in DENYLISTED_LOGINS + } + for login, info in live.items(): + merged[login] = {"name": info["name"]} + return merged + + WALL_MARKER_START = "" WALL_MARKER_END = "" _WALL_BLOCK_RE = re.compile( @@ -184,6 +234,7 @@ def main() -> int: token = os.environ["GITHUB_TOKEN"] org = os.environ.get("GITHUB_ORG", "BHoM") readme_path = os.environ.get("README_PATH", DEFAULT_README_PATH) + roster_path = os.environ.get("ROSTER_PATH", DEFAULT_ROSTER_PATH) session = make_session(token) @@ -202,8 +253,14 @@ def main() -> int: print(f"Aggregated to {len(aggregated)} unique contributors. Enriching display names...") enriched = enrich_display_names(session, aggregated) + + roster = load_roster(roster_path) + merged = merge_into_roster(roster, enriched) + save_roster(roster_path, merged) + print(f"Roster: {len(roster)} known, {len(merged)} after merge.") + today = date.today().isoformat() - wall_md = render_wall(enriched, today) + wall_md = render_wall(merged, today) changed = splice_into_readme(readme_path, wall_md) if changed: diff --git a/scripts/tests/test_generate.py b/scripts/tests/test_generate.py index 608cbf6..1ec97cc 100644 --- a/scripts/tests/test_generate.py +++ b/scripts/tests/test_generate.py @@ -7,7 +7,8 @@ @responses.activate -def test_list_org_repos_filters_archived_and_dot_github(): +def test_list_org_repos_includes_archived_excludes_dot_github(): + # Archived repos are kept so their contributors stay on the wall. responses.add( responses.GET, "https://api.github.com/orgs/BHoM/repos", @@ -22,7 +23,7 @@ def test_list_org_repos_filters_archived_and_dot_github(): session = make_session("fake-token") result = list_org_repos(session, "BHoM") names = [r["name"] for r in result] - assert names == ["BHoM", "BHoM_Engine"] + assert names == ["BHoM", "old_repo", "BHoM_Engine"] @responses.activate @@ -229,6 +230,42 @@ def test_render_wall_sort_is_case_insensitive_and_unicode(): assert names_in_order == sorted(names_in_order) +from scripts.generate_wall_of_honour import load_roster, merge_into_roster, save_roster + + +def test_load_roster_missing_file_returns_empty(): + with tempfile.TemporaryDirectory() as tmp: + assert load_roster(str(Path(tmp) / "wall_roster.json")) == {} + + +def test_roster_save_load_roundtrip(): + with tempfile.TemporaryDirectory() as tmp: + path = str(Path(tmp) / "wall_roster.json") + roster = {"alice": {"name": "Alice Example"}, "Ă„ngström": {"name": "Ångström"}} + save_roster(path, roster) + assert load_roster(path) == roster + + +def test_merge_preserves_absent_logins_and_refreshes_live(): + roster = { + "gone": {"name": "Deleted Account"}, + "alice": {"name": "Old Name"}, + } + live = {"alice": {"name": "Alice Example", "avatar_url": "https://x/a", "contributions": 10}} + merged = merge_into_roster(roster, live) + assert merged == { + "gone": {"name": "Deleted Account"}, + "alice": {"name": "Alice Example"}, + } + + +def test_merge_drops_denylisted_roster_entries(): + # A login denylisted after a past run added it must not linger in the roster. + roster = {"BHoMBot": {"name": "BHoMBot"}, "alice": {"name": "Alice Example"}} + merged = merge_into_roster(roster, {}) + assert merged == {"alice": {"name": "Alice Example"}} + + from scripts.generate_wall_of_honour import splice_into_readme @@ -313,14 +350,25 @@ def test_main_end_to_end(monkeypatch, tmp_path): ) readme = tmp_path / "profile" / "README.md" + roster_path = tmp_path / "profile" / "wall_roster.json" + # Pre-seed the roster with a login no longer returned by the API + roster_path.parent.mkdir(parents=True, exist_ok=True) + roster_path.write_text('{"gone": {"name": "Departed Contributor"}}', encoding="utf-8") monkeypatch.setenv("GITHUB_TOKEN", "fake-token") monkeypatch.setenv("GITHUB_ORG", "BHoM") monkeypatch.setenv("README_PATH", str(readme)) + monkeypatch.setenv("ROSTER_PATH", str(roster_path)) exit_code = main() assert exit_code == 0 assert readme.exists() content = readme.read_text(encoding="utf-8") assert "Alice Example" in content - assert "img.shields.io/badge/contributors-1-brightgreen" in content # Only alice; bot filtered + assert "Departed Contributor" in content # roster-only login stays on the wall + assert "img.shields.io/badge/contributors-2-brightgreen" in content # alice + gone; bot filtered assert "dependabot" not in content + roster = load_roster(str(roster_path)) + assert roster == { + "alice": {"name": "Alice Example"}, + "gone": {"name": "Departed Contributor"}, + }