From b8c192665f943933794488afd395a23adba96511 Mon Sep 17 00:00:00 2001 From: Michael Flanakin Date: Wed, 28 Oct 2026 17:11:47 +0000 Subject: [PATCH 01/11] docs: list external contributors first in release notes Co-Authored-By: Claude Sonnet 5.5 --- .claude/commands/release.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude/commands/release.md b/.claude/commands/release.md index 86cdfb623..e2a83f338 100644 --- a/.claude/commands/release.md +++ b/.claude/commands/release.md @@ -191,7 +191,7 @@ A draft is only visible to maintainers and doesn't create the tag, so it is safe 2. Write the body from the previous release's body (`gh release view v{N-1} --json body --jq .body`) and this version's changelog section: - **New in {Month yyyy}**: one line per tool for new features, each with the tool emoji and a link to its page (don't link individual features). - **Updated**: one line per tool for changes and fixes, in the same tool order as the previous release. Keep it to the notable items; don't list every change. - - **Contributors**: code contributors come from `gh api --paginate repos/microsoft/finops-toolkit/compare/v{N-1}...dev --jq '.commits[].author.login'` (drop bots; list `@MSBrett` and `@flanakin` last). Supporters are issue reporters from the milestone (`gh issue list --milestone v{N} --state all --json author`) who aren't contributors, sorted alphabetically. Tell the user these lists miss reviewers and discussion participants, so they need a final review. + - **Contributors**: code contributors come from `gh api --paginate repos/microsoft/finops-toolkit/compare/v{N-1}...dev --jq '.commits[].author.login'` (drop bots). List external contributors first, then the regular team members (`@RolandKrummenacher`, then `@MSBrett` and `@flanakin` last), so one-time and occasional contributors stand out. Cross-check against merged PR authors (`gh pr list --state merged --search "milestone:v{N}" --json author`), because a few commits have no GitHub login. Supporters are issue reporters from the milestone (`gh issue list --milestone v{N} --state all --json author`) who aren't contributors, sorted alphabetically. Tell the user these lists miss reviewers and discussion participants, so they need a final review. - **Footer**: copy the previous footer with the version updated. Keep the blog and download links commented out until the release is published. 3. Set the body with `gh api --method PATCH repos/microsoft/finops-toolkit/releases/{id} -F body=@{file}`. Find `{id}` with `gh api repos/microsoft/finops-toolkit/releases --jq '.[] | select(.draft and .tag_name=="v{N}") | .id'`. 4. After Power BI packaging, upload the remaining files with `gh release upload`. `gh` can't find a draft by its `v{N}` tag, so use the `untagged-โ€ฆ` name from the draft's URL: `gh release upload untagged-{hash} release/PowerBI-demo.zip --repo microsoft/finops-toolkit`. From 390ed077d8a8d9dc4086974b4c53f04457d01020 Mon Sep 17 00:00:00 2001 From: Michael Flanakin Date: Wed, 28 Oct 2026 17:42:18 +0000 Subject: [PATCH 02/11] docs: credit v15 issue reporters as contributors for bug Adds Christopherloof, haflidif, josesarsa, sharmilamusunuru, and xan-archera, and adds bug for JacobStruiksma. Documents the contrib command in the release instructions. Co-Authored-By: Claude Sonnet 5.5 --- .all-contributorsrc | 49 ++++++++++++++++++++++++++++++++++++- .claude/commands/release.md | 2 +- README.md | 9 ++++++- docs/README.md | 9 ++++++- 4 files changed, 65 insertions(+), 4 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 1935bcec9..c4fd05910 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -824,7 +824,8 @@ "avatar_url": "https://avatars.githubusercontent.com/u/9788612?v=4", "profile": "https://github.com/JacobStruiksma", "contributions": [ - "code" + "code", + "bug" ] }, { @@ -998,6 +999,51 @@ "contributions": [ "bug" ] + }, + { + "login": "Christopherloof", + "name": "Christopherloof", + "avatar_url": "https://avatars.githubusercontent.com/u/119003550?v=4", + "profile": "https://github.com/Christopherloof", + "contributions": [ + "bug" + ] + }, + { + "login": "haflidif", + "name": "Haflidi Fridthjofsson", + "avatar_url": "https://avatars.githubusercontent.com/u/26624010?v=4", + "profile": "https://linktr.ee/haflidif", + "contributions": [ + "bug" + ] + }, + { + "login": "josesarsa", + "name": "josearsa", + "avatar_url": "https://avatars.githubusercontent.com/u/89536192?v=4", + "profile": "https://github.com/josesarsa", + "contributions": [ + "bug" + ] + }, + { + "login": "sharmilamusunuru", + "name": "sharmilamusunuru", + "avatar_url": "https://avatars.githubusercontent.com/u/70754330?v=4", + "profile": "https://github.com/sharmilamusunuru", + "contributions": [ + "bug" + ] + }, + { + "login": "xan-archera", + "name": "xan-archera", + "avatar_url": "https://avatars.githubusercontent.com/u/248541667?v=4", + "profile": "https://github.com/xan-archera", + "contributions": [ + "bug" + ] } ], "commitType": "docs", @@ -1009,3 +1055,4 @@ } } } + diff --git a/.claude/commands/release.md b/.claude/commands/release.md index e2a83f338..6740e00a6 100644 --- a/.claude/commands/release.md +++ b/.claude/commands/release.md @@ -191,7 +191,7 @@ A draft is only visible to maintainers and doesn't create the tag, so it is safe 2. Write the body from the previous release's body (`gh release view v{N-1} --json body --jq .body`) and this version's changelog section: - **New in {Month yyyy}**: one line per tool for new features, each with the tool emoji and a link to its page (don't link individual features). - **Updated**: one line per tool for changes and fixes, in the same tool order as the previous release. Keep it to the notable items; don't list every change. - - **Contributors**: code contributors come from `gh api --paginate repos/microsoft/finops-toolkit/compare/v{N-1}...dev --jq '.commits[].author.login'` (drop bots). List external contributors first, then the regular team members (`@RolandKrummenacher`, then `@MSBrett` and `@flanakin` last), so one-time and occasional contributors stand out. Cross-check against merged PR authors (`gh pr list --state merged --search "milestone:v{N}" --json author`), because a few commits have no GitHub login. Supporters are issue reporters from the milestone (`gh issue list --milestone v{N} --state all --json author`) who aren't contributors, sorted alphabetically. Tell the user these lists miss reviewers and discussion participants, so they need a final review. + - **Contributors**: code contributors come from `gh api --paginate repos/microsoft/finops-toolkit/compare/v{N-1}...dev --jq '.commits[].author.login'` (drop bots). List external contributors first, then the regular team members (`@RolandKrummenacher`, then `@MSBrett` and `@flanakin` last), so one-time and occasional contributors stand out. Cross-check against merged PR authors (`gh pr list --state merged --search "milestone:v{N}" --json author`), because a few commits have no GitHub login. Supporters are issue reporters from the milestone (`gh issue list --milestone v{N} --state all --json author`) who aren't contributors, sorted alphabetically. Credit each supporter who reported a bug in `.all-contributorsrc` with `npm ci --ignore-scripts` once, then `npm run contrib -- {login} bug` (the same result as an `@allcontributors please add @{login} for bug` comment) and commit the changed `.all-contributorsrc`, `README.md`, and `docs/README.md`. Tell the user these lists miss reviewers and discussion participants, so they need a final review. - **Footer**: copy the previous footer with the version updated. Keep the blog and download links commented out until the release is published. 3. Set the body with `gh api --method PATCH repos/microsoft/finops-toolkit/releases/{id} -F body=@{file}`. Find `{id}` with `gh api repos/microsoft/finops-toolkit/releases --jq '.[] | select(.draft and .tag_name=="v{N}") | .id'`. 4. After Power BI packaging, upload the remaining files with `gh release upload`. `gh` can't find a draft by its `v{N}` tag, so use the `untagged-โ€ฆ` name from the draft's URL: `gh release upload untagged-{hash} release/PowerBI-demo.zip --repo microsoft/finops-toolkit`. diff --git a/README.md b/README.md index 71c297e71..7366632a1 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ There are many ways to participate. From reporting bugs and requesting features NicholasBrand
NicholasBrand

๐Ÿ’ป ๐Ÿ› - Jacob Struiksma
Jacob Struiksma

๐Ÿ’ป + Jacob Struiksma
Jacob Struiksma

๐Ÿ’ป ๐Ÿ› Adeel Aziz
Adeel Aziz

๐Ÿ’ป shasulin
shasulin

๐Ÿ‘€ Kasim Rehman
Kasim Rehman

๐Ÿ’ป @@ -163,6 +163,13 @@ There are many ways to participate. From reporting bugs and requesting features TakuSe
TakuSe

๐Ÿ’ป sandhyakumili
sandhyakumili

๐Ÿ› + Christopherloof
Christopherloof

๐Ÿ› + Haflidi Fridthjofsson
Haflidi Fridthjofsson

๐Ÿ› + josearsa
josearsa

๐Ÿ› + sharmilamusunuru
sharmilamusunuru

๐Ÿ› + + + xan-archera
xan-archera

๐Ÿ› diff --git a/docs/README.md b/docs/README.md index 229a28a1c..cc5b7db5b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -221,7 +221,7 @@ Whether you're looking for a little assistance or are interested in contributing NicholasBrand
NicholasBrand

๐Ÿ’ป ๐Ÿ› - Jacob Struiksma
Jacob Struiksma

๐Ÿ’ป + Jacob Struiksma
Jacob Struiksma

๐Ÿ’ป ๐Ÿ› Adeel Aziz
Adeel Aziz

๐Ÿ’ป shasulin
shasulin

๐Ÿ‘€ Kasim Rehman
Kasim Rehman

๐Ÿ’ป @@ -247,6 +247,13 @@ Whether you're looking for a little assistance or are interested in contributing TakuSe
TakuSe

๐Ÿ’ป sandhyakumili
sandhyakumili

๐Ÿ› + Christopherloof
Christopherloof

๐Ÿ› + Haflidi Fridthjofsson
Haflidi Fridthjofsson

๐Ÿ› + josearsa
josearsa

๐Ÿ› + sharmilamusunuru
sharmilamusunuru

๐Ÿ› + + + xan-archera
xan-archera

๐Ÿ› From fcbd2db5d27e671278450ff3d5082c7d0013582c Mon Sep 17 00:00:00 2001 From: Michael Flanakin Date: Wed, 28 Oct 2026 18:13:21 +0000 Subject: [PATCH 03/11] docs: credit code contributors and document contribution types Adds bpurchell and danfiedler-msft for code, documents every contribution type in CONTRIBUTING.md, and updates the release guidance to credit everyone named in the release notes. Co-Authored-By: Claude Sonnet 5.5 --- .all-contributorsrc | 18 ++++++++++++++++++ .claude/commands/release.md | 2 +- CONTRIBUTING.md | 20 ++++++++++++++++++++ README.md | 2 ++ docs/README.md | 2 ++ 5 files changed, 43 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index c4fd05910..6714faf44 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1044,6 +1044,24 @@ "contributions": [ "bug" ] + }, + { + "login": "bpurchell", + "name": "Brian Purchell", + "avatar_url": "https://avatars.githubusercontent.com/u/40211715?v=4", + "profile": "https://github.com/bpurchell", + "contributions": [ + "code" + ] + }, + { + "login": "danfiedler-msft", + "name": "Dan Fiedler", + "avatar_url": "https://avatars.githubusercontent.com/u/151573964?v=4", + "profile": "https://github.com/danfiedler-msft", + "contributions": [ + "code" + ] } ], "commitType": "docs", diff --git a/.claude/commands/release.md b/.claude/commands/release.md index 6740e00a6..a8d6a3305 100644 --- a/.claude/commands/release.md +++ b/.claude/commands/release.md @@ -191,7 +191,7 @@ A draft is only visible to maintainers and doesn't create the tag, so it is safe 2. Write the body from the previous release's body (`gh release view v{N-1} --json body --jq .body`) and this version's changelog section: - **New in {Month yyyy}**: one line per tool for new features, each with the tool emoji and a link to its page (don't link individual features). - **Updated**: one line per tool for changes and fixes, in the same tool order as the previous release. Keep it to the notable items; don't list every change. - - **Contributors**: code contributors come from `gh api --paginate repos/microsoft/finops-toolkit/compare/v{N-1}...dev --jq '.commits[].author.login'` (drop bots). List external contributors first, then the regular team members (`@RolandKrummenacher`, then `@MSBrett` and `@flanakin` last), so one-time and occasional contributors stand out. Cross-check against merged PR authors (`gh pr list --state merged --search "milestone:v{N}" --json author`), because a few commits have no GitHub login. Supporters are issue reporters from the milestone (`gh issue list --milestone v{N} --state all --json author`) who aren't contributors, sorted alphabetically. Credit each supporter who reported a bug in `.all-contributorsrc` with `npm ci --ignore-scripts` once, then `npm run contrib -- {login} bug` (the same result as an `@allcontributors please add @{login} for bug` comment) and commit the changed `.all-contributorsrc`, `README.md`, and `docs/README.md`. Tell the user these lists miss reviewers and discussion participants, so they need a final review. + - **Contributors**: code contributors come from `gh api --paginate repos/microsoft/finops-toolkit/compare/v{N-1}...dev --jq '.commits[].author.login'` (drop bots). List external contributors first, then the regular team members (`@RolandKrummenacher`, then `@MSBrett` and `@flanakin` last), so one-time and occasional contributors stand out. Cross-check against merged PR authors (`gh pr list --state merged --search "milestone:v{N}" --json author`), because a few commits have no GitHub login. Supporters are issue reporters from the milestone (`gh issue list --milestone v{N} --state all --json author`) who aren't contributors, sorted alphabetically. Everyone named in either list must also be in `.all-contributorsrc` with the right contribution types, so check each login and credit what they did, treating every type equally (see "Recognizing contributors" in `CONTRIBUTING.md`): `code` for code PRs, `doc` for documentation PRs, `bug` for issue reporters, `ideas` for feature requesters, `review` for reviewers, `question` for people who answered questions, and any other type that fits. Add missing credits with `npm ci --ignore-scripts` once, then `npm run contrib -- {login} {type} [{type}...]` (the same result as an `@allcontributors please add @{login} for {type}` comment), then commit the changed `.all-contributorsrc`, `README.md`, and `docs/README.md`. The CLI rewrites `.all-contributorsrc` with short arrays on one line, so put the arrays back on multiple lines to keep the diff to the added entries. Don't skip someone because their type is unclear; credit `code` or `doc` for a merged PR and ask the user about the rest. Tell the user these lists miss reviewers and discussion participants, so they need a final review. - **Footer**: copy the previous footer with the version updated. Keep the blog and download links commented out until the release is published. 3. Set the body with `gh api --method PATCH repos/microsoft/finops-toolkit/releases/{id} -F body=@{file}`. Find `{id}` with `gh api repos/microsoft/finops-toolkit/releases --jq '.[] | select(.draft and .tag_name=="v{N}") | .id'`. 4. After Power BI packaging, upload the remaining files with `gh release upload`. `gh` can't find a draft by its `v{N}` tag, so use the `untagged-โ€ฆ` name from the draft's URL: `gh release upload untagged-{hash} release/PowerBI-demo.zip --repo microsoft/finops-toolkit`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8f21334b1..6edbecd88 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -62,6 +62,26 @@ Want to stay connected with the team? Join our [weekly contributor sync](https:/ For details about contributing code changes and submitting pull requests, see [How to contribute](../../wiki/How-to-contribute). --> +## ๐Ÿ… Recognizing contributors + +We recognize every kind of contribution equally with the [all-contributors](https://allcontributors.org) list in the [README](./README.md#-contributors). Anyone who contributes in any of the ways below should be listed. If we missed you, let us know. + +| Type | Emoji | Credit for | +| ---- | ----- | ---------- | +| `code` | ๐Ÿ’ป | Code changes in a merged pull request | +| `doc` | ๐Ÿ“– | Documentation changes in a merged pull request | +| `bug` | ๐Ÿ› | Reporting a bug that was confirmed or fixed | +| `ideas` | ๐Ÿค” | Suggesting a feature or idea that shaped the product | +| `review` | ๐Ÿ‘€ | Reviewing pull requests | +| `question` | ๐Ÿ’ฌ | Answering questions in issues and discussions | +| `design` | ๐ŸŽจ | Design work, like report layouts and visuals | +| `security` | ๐Ÿ›ก๏ธ | Finding or fixing security issues | +| `mentoring` | ๐Ÿง‘โ€๐Ÿซ | Mentoring other contributors | +| `promotion` | ๐Ÿ“ฃ | Promoting the toolkit in blogs, talks, and events | +| `leader` | ๐ŸŒŸ | Serving on the governing board | + +To add someone, comment `@allcontributors please add @username for code, doc` on any issue or pull request in this repo, or run `npm ci --ignore-scripts` once and then `npm run contrib -- username code doc`. +
# ๐Ÿ™ Thank you! diff --git a/README.md b/README.md index 7366632a1..b9d538ced 100644 --- a/README.md +++ b/README.md @@ -170,6 +170,8 @@ There are many ways to participate. From reporting bugs and requesting features xan-archera
xan-archera

๐Ÿ› + Brian Purchell
Brian Purchell

๐Ÿ’ป + Dan Fiedler
Dan Fiedler

๐Ÿ’ป diff --git a/docs/README.md b/docs/README.md index cc5b7db5b..ffbcf2b0d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -254,6 +254,8 @@ Whether you're looking for a little assistance or are interested in contributing xan-archera
xan-archera

๐Ÿ› + Brian Purchell
Brian Purchell

๐Ÿ’ป + Dan Fiedler
Dan Fiedler

๐Ÿ’ป From 4ca1b0390ad57b207258faf123ab0b9b49fd8360 Mon Sep 17 00:00:00 2001 From: Michael Flanakin Date: Wed, 28 Oct 2026 18:43:15 +0000 Subject: [PATCH 04/11] chore: Automate contributor recognition Add Update-Contributors.ps1, which finds merged PR authors, PR reviewers, and reporters of fixed or accepted bugs and feature requests who are missing from .all-contributorsrc, and adds them with all-contributors-cli while keeping the diff small. Add a weekly update-contributors workflow that opens a PR with the result, unit tests, and docs. Co-Authored-By: Claude Sonnet 5.5 --- .claude/commands/release.md | 2 +- .github/workflows/update-contributors.yml | 121 +++ CONTRIBUTING.md | 2 +- .../Unit/Action.UpdateContributors.Tests.ps1 | 61 ++ .../Tests/Unit/Update-Contributors.Tests.ps1 | 329 +++++++ src/scripts/README.md | 31 + src/scripts/Update-Contributors.ps1 | 862 ++++++++++++++++++ 7 files changed, 1406 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/update-contributors.yml create mode 100644 src/powershell/Tests/Unit/Action.UpdateContributors.Tests.ps1 create mode 100644 src/powershell/Tests/Unit/Update-Contributors.Tests.ps1 create mode 100644 src/scripts/Update-Contributors.ps1 diff --git a/.claude/commands/release.md b/.claude/commands/release.md index a8d6a3305..88e131e51 100644 --- a/.claude/commands/release.md +++ b/.claude/commands/release.md @@ -191,7 +191,7 @@ A draft is only visible to maintainers and doesn't create the tag, so it is safe 2. Write the body from the previous release's body (`gh release view v{N-1} --json body --jq .body`) and this version's changelog section: - **New in {Month yyyy}**: one line per tool for new features, each with the tool emoji and a link to its page (don't link individual features). - **Updated**: one line per tool for changes and fixes, in the same tool order as the previous release. Keep it to the notable items; don't list every change. - - **Contributors**: code contributors come from `gh api --paginate repos/microsoft/finops-toolkit/compare/v{N-1}...dev --jq '.commits[].author.login'` (drop bots). List external contributors first, then the regular team members (`@RolandKrummenacher`, then `@MSBrett` and `@flanakin` last), so one-time and occasional contributors stand out. Cross-check against merged PR authors (`gh pr list --state merged --search "milestone:v{N}" --json author`), because a few commits have no GitHub login. Supporters are issue reporters from the milestone (`gh issue list --milestone v{N} --state all --json author`) who aren't contributors, sorted alphabetically. Everyone named in either list must also be in `.all-contributorsrc` with the right contribution types, so check each login and credit what they did, treating every type equally (see "Recognizing contributors" in `CONTRIBUTING.md`): `code` for code PRs, `doc` for documentation PRs, `bug` for issue reporters, `ideas` for feature requesters, `review` for reviewers, `question` for people who answered questions, and any other type that fits. Add missing credits with `npm ci --ignore-scripts` once, then `npm run contrib -- {login} {type} [{type}...]` (the same result as an `@allcontributors please add @{login} for {type}` comment), then commit the changed `.all-contributorsrc`, `README.md`, and `docs/README.md`. The CLI rewrites `.all-contributorsrc` with short arrays on one line, so put the arrays back on multiple lines to keep the diff to the added entries. Don't skip someone because their type is unclear; credit `code` or `doc` for a merged PR and ask the user about the rest. Tell the user these lists miss reviewers and discussion participants, so they need a final review. + - **Contributors**: code contributors come from `gh api --paginate repos/microsoft/finops-toolkit/compare/v{N-1}...dev --jq '.commits[].author.login'` (drop bots). List external contributors first, then the regular team members (`@RolandKrummenacher`, then `@MSBrett` and `@flanakin` last), so one-time and occasional contributors stand out. Cross-check against merged PR authors (`gh pr list --state merged --search "milestone:v{N}" --json author`), because a few commits have no GitHub login. Supporters are issue reporters from the milestone (`gh issue list --milestone v{N} --state all --json author`) who aren't contributors, sorted alphabetically. Everyone named in either list must also be in `.all-contributorsrc` with the right contribution types, so check each login and credit what they did, treating every type equally (see "Recognizing contributors" in `CONTRIBUTING.md`): `code` for code PRs, `doc` for documentation PRs, `bug` for issue reporters, `ideas` for feature requesters, `review` for reviewers, `question` for people who answered questions, and any other type that fits. Find missing credits with `./src/scripts/Update-Contributors.ps1 -Milestone v{N}` (a dry run that infers `code`, `doc`, `review`, `bug`, and `ideas` from merged PRs, reviews, and issues), then run `npm ci --ignore-scripts` once and rerun it with `-Apply`, which also keeps the `.all-contributorsrc` diff to the added entries. Commit the changed `.all-contributorsrc`, `README.md`, and `docs/README.md`. The weekly `update-contributors` workflow opens a PR with the same changes, so check for `automation/update-contributors` first. For types the script can't infer, or issue authors it lists as needing a human, add them with `npm run contrib -- {login} {type} [{type}...]` and put the arrays in `.all-contributorsrc` back on multiple lines. Don't skip someone because their type is unclear; credit `code` or `doc` for a merged PR and ask the user about the rest. Tell the user these lists miss reviewers and discussion participants, so they need a final review. - **Footer**: copy the previous footer with the version updated. Keep the blog and download links commented out until the release is published. 3. Set the body with `gh api --method PATCH repos/microsoft/finops-toolkit/releases/{id} -F body=@{file}`. Find `{id}` with `gh api repos/microsoft/finops-toolkit/releases --jq '.[] | select(.draft and .tag_name=="v{N}") | .id'`. 4. After Power BI packaging, upload the remaining files with `gh release upload`. `gh` can't find a draft by its `v{N}` tag, so use the `untagged-โ€ฆ` name from the draft's URL: `gh release upload untagged-{hash} release/PowerBI-demo.zip --repo microsoft/finops-toolkit`. diff --git a/.github/workflows/update-contributors.yml b/.github/workflows/update-contributors.yml new file mode 100644 index 000000000..6caebc1b4 --- /dev/null +++ b/.github/workflows/update-contributors.yml @@ -0,0 +1,121 @@ +name: Update contributors + +# Finds contributors who are missing from .all-contributorsrc (merged PR authors, PR reviewers, and the reporters of +# fixed or accepted bugs and feature requests) and opens a pull request that adds them. Only contribution types that +# GitHub data can prove are handled; everything else (question, design, security, mentoring, promotion, leader) is +# left to humans. See "Recognizing contributors" in CONTRIBUTING.md and src/scripts/Update-Contributors.ps1. +# +# Requires the repo setting "Allow GitHub Actions to create and approve pull requests" (Settings > Actions > +# General). If it is off, or the org blocks it, the branch is still pushed and the job summary links to the +# "create pull request" page, like the open data workflows. +# +# Pull requests opened with GITHUB_TOKEN don't start other workflows, so close and reopen the PR to run its checks. + +on: + schedule: + - cron: '0 7 * * 1' # Every Monday at 07:00 UTC + workflow_dispatch: + inputs: + since: + description: 'Credit contributions since this date (YYYY-MM-DD). Default is 60 days ago.' + required: false + type: string + +permissions: + contents: write + pull-requests: write + +concurrency: + group: update-contributors + cancel-in-progress: false + +jobs: + update: + name: Credit new contributors + runs-on: ubuntu-latest + timeout-minutes: 30 + # Scheduled workflows also run in forks, where there is nothing to credit and no permission to push. Only run in + # the upstream repo, from dev, so manual runs from other branches can't push their commits into the PR. + if: github.repository == 'microsoft/finops-toolkit' && github.ref == 'refs/heads/dev' + env: + GH_TOKEN: ${{ github.token }} + BRANCH: automation/update-contributors + steps: + - name: Checkout dev + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + # The repo's own devDependency (all-contributors-cli). Scripts are not run during install. + - name: Install all-contributors-cli + run: npm ci --ignore-scripts + + # The default window is long on purpose: the script only adds what is missing, so re-checking recent work is + # harmless, and an unmerged PR from a previous week keeps its entries when the branch is rebuilt from dev. + - name: Add missing contributors + id: update + shell: pwsh + env: + SINCE_INPUT: ${{ inputs.since }} + run: | + $since = if ($env:SINCE_INPUT) { $env:SINCE_INPUT } else { (Get-Date).AddDays(-60).ToString('yyyy-MM-dd') } + "since=$since" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + ./src/scripts/Update-Contributors.ps1 -Since $since -Apply -SummaryPath "$env:RUNNER_TEMP/contributors.md" + # A failure for one login shouldn't lose the credits that worked. The last step fails the job afterwards. + continue-on-error: true + + - name: Check for changes + id: changes + run: | + if git diff --quiet -- .all-contributorsrc README.md docs/README.md; then + echo "changed=false" >> "$GITHUB_OUTPUT" + echo "No missing contributors." + else + echo "changed=true" >> "$GITHUB_OUTPUT" + git diff --stat -- .all-contributorsrc README.md docs/README.md + fi + + - name: Push branch and open pull request + if: steps.changes.outputs.changed == 'true' + env: + SINCE: ${{ steps.update.outputs.since }} + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -b "$BRANCH" + git add .all-contributorsrc README.md docs/README.md + git commit -m "docs: Credit new contributors" + + # The branch is rebuilt from dev every run, so an open PR is updated in place + git push --force origin "$BRANCH" + + { + echo "Adds contributor credits found by [\`Update-Contributors.ps1\`](https://github.com/${GITHUB_REPOSITORY}/blob/dev/src/scripts/Update-Contributors.ps1) for work since $SINCE." + echo "" + cat "$RUNNER_TEMP/contributors.md" + echo "" + echo "Review before merging: credits come from GitHub data only (merged PRs, reviews, and fixed or accepted bugs and feature requests). Other types, like question, design, security, mentoring, and promotion, are added by hand. See [Recognizing contributors](https://github.com/${GITHUB_REPOSITORY}/blob/dev/CONTRIBUTING.md#-recognizing-contributors)." + echo "" + echo "_Opened by the [Update contributors](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}) workflow. Close and reopen this PR to run its checks._" + } > "$RUNNER_TEMP/pr-body.md" + cat "$RUNNER_TEMP/pr-body.md" >> "$GITHUB_STEP_SUMMARY" + + PR_NUMBER=$(gh pr list --head "$BRANCH" --base dev --state open --json number --jq '.[0].number // empty') + if [ -n "$PR_NUMBER" ]; then + gh pr edit "$PR_NUMBER" --body-file "$RUNNER_TEMP/pr-body.md" + echo "Updated PR #$PR_NUMBER" + elif ! gh pr create --base dev --head "$BRANCH" --title "[Docs] Credit new contributors" --body-file "$RUNNER_TEMP/pr-body.md"; then + # Creating PRs from Actions is a repo/org setting. Don't lose the work if it is off. + PR_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/compare/dev...${BRANCH}?expand=1" + echo "::warning::Could not open the pull request (is \"Allow GitHub Actions to create and approve pull requests\" off?). The branch $BRANCH was pushed." + { + echo "" + echo "GitHub Actions could not open the pull request. Branch \`$BRANCH\` was pushed, so open it manually:" + echo "" + echo "[**Create pull request**]($PR_URL)" + } >> "$GITHUB_STEP_SUMMARY" + fi + + - name: Fail if credits could not be added + if: steps.update.outcome == 'failure' + run: | + echo "::error::Update-Contributors.ps1 failed. See the 'Add missing contributors' step." + exit 1 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6edbecd88..0bbd52102 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -80,7 +80,7 @@ We recognize every kind of contribution equally with the [all-contributors](http | `promotion` | ๐Ÿ“ฃ | Promoting the toolkit in blogs, talks, and events | | `leader` | ๐ŸŒŸ | Serving on the governing board | -To add someone, comment `@allcontributors please add @username for code, doc` on any issue or pull request in this repo, or run `npm ci --ignore-scripts` once and then `npm run contrib -- username code doc`. +The `code`, `doc`, `review`, `bug`, and `ideas` credits are found automatically. The weekly [update-contributors](.github/workflows/update-contributors.yml) workflow opens a pull request with whatever is missing, and you can check any time with `./src/scripts/Update-Contributors.ps1 -Since 2026-01-01` (add `-Apply` to update the list). Other types are added by hand: comment `@allcontributors please add @username for question, design` on any issue or pull request in this repo, or run `npm ci --ignore-scripts` once and then `npm run contrib -- username question design`.
diff --git a/src/powershell/Tests/Unit/Action.UpdateContributors.Tests.ps1 b/src/powershell/Tests/Unit/Action.UpdateContributors.Tests.ps1 new file mode 100644 index 000000000..66419dc47 --- /dev/null +++ b/src/powershell/Tests/Unit/Action.UpdateContributors.Tests.ps1 @@ -0,0 +1,61 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe 'update-contributors GitHub Action' { + BeforeAll { + $repoRoot = (Get-Item -Path $PSScriptRoot).Parent.Parent.Parent.Parent.FullName + $workflowPath = Join-Path $repoRoot '.github/workflows/update-contributors.yml' + $workflowContent = Get-Content -Path $workflowPath -Raw + } + + It 'Should exist' { + Test-Path $workflowPath | Should -BeTrue + } + + It 'Should run weekly and on demand with a since input' { + $workflowContent | Should -Match 'schedule:\s*\r?\n\s+- cron:' + $workflowContent | Should -Match 'workflow_dispatch:\s*\r?\n\s+inputs:\s*\r?\n\s+since:' + } + + It 'Should not run on push or pull_request events' { + $workflowContent | Should -Not -Match '\n\s+(push|pull_request|pull_request_target):' + } + + It 'Should use only the minimal permissions' { + $workflowContent | Should -Match 'permissions:\s*\r?\n\s+contents:\s*write\s*\r?\n\s+pull-requests:\s*write\s*\r?\n\r?\n' + } + + It 'Should only run in the upstream repo, from dev' { + $workflowContent | Should -Match "if:\s*github\.repository == 'microsoft/finops-toolkit' && github\.ref == 'refs/heads/dev'" + } + + It 'Should pin every action to a full commit SHA' { + $uses = [regex]::Matches($workflowContent, '(?m)^\s*(?:-\s*)?uses:\s*(\S+)') | ForEach-Object { $_.Groups[1].Value } + $uses | Should -Not -BeNullOrEmpty + foreach ($u in $uses) { $u | Should -Match '@[0-9a-f]{40}$' } + } + + It 'Should install only the declared dev dependencies without running install scripts' { + $workflowContent | Should -Match 'npm ci --ignore-scripts' + } + + It 'Should run the script in apply mode' { + $workflowContent | Should -Match 'Update-Contributors\.ps1 -Since \$since -Apply' + } + + It 'Should not put workflow inputs directly in a script' { + # Inputs reach scripts through env vars so they can't inject commands + $workflowContent | Should -Not -Match 'run:[^\n]*\$\{\{\s*(inputs|github\.event)\.' + $workflowContent | Should -Match 'SINCE_INPUT:\s*\$\{\{\s*inputs\.since\s*\}\}' + } + + It 'Should push a fixed automation branch and open or update a PR to dev' { + $workflowContent | Should -Match 'BRANCH:\s*automation/update-contributors' + $workflowContent | Should -Match 'gh pr create --base dev' + $workflowContent | Should -Match 'gh pr edit' + } + + It 'Should only stage the contributor files' { + $workflowContent | Should -Match 'git add \.all-contributorsrc README\.md docs/README\.md' + } +} diff --git a/src/powershell/Tests/Unit/Update-Contributors.Tests.ps1 b/src/powershell/Tests/Unit/Update-Contributors.Tests.ps1 new file mode 100644 index 000000000..ad1e1f810 --- /dev/null +++ b/src/powershell/Tests/Unit/Update-Contributors.Tests.ps1 @@ -0,0 +1,329 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe 'Update-Contributors' { + BeforeAll { + # Update-Contributors.ps1 has top-level script logic that calls GitHub, so load only its functions via + # the PowerShell AST. They are pure (no gh calls) except the ones under "GitHub access", which are not + # exercised here. + $scriptPath = Join-Path (Get-Item -Path $PSScriptRoot).Parent.Parent.Parent.FullName 'scripts/Update-Contributors.ps1' + $ast = [System.Management.Automation.Language.Parser]::ParseFile($scriptPath, [ref]$null, [ref]$null) + $functions = $ast.FindAll({ param($node) $node -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $false) + if (-not $functions) { throw "No functions found in $scriptPath" } + foreach ($function in $functions) { . ([scriptblock]::Create($function.Extent.Text)) } + + $repoRoot = (Get-Item -Path $PSScriptRoot).Parent.Parent.Parent.Parent.FullName + $configText = Get-Content -Path (Join-Path $repoRoot '.all-contributorsrc') -Raw + + # Simulates the prettier pass in all-contributors-cli, which puts arrays of strings on one line + function ConvertTo-CollapsedArray([string]$Text) + { + [regex]::Replace($Text, '\[\s*\r?\n((?:\s*"[^"]*",?\s*\r?\n)+)\s*\]', { + param($m) + $items = [regex]::Matches($m.Groups[1].Value, '"[^"]*"') | ForEach-Object { $_.Value } + '[' + ($items -join ', ') + ']' + }) + } + + function Join-ConfigLine([string[]]$Lines) { ($Lines -join "`n") + "`n" } + } + + Context 'Test-BotLogin' { + It 'Skips ' -ForEach @( + @{ login = 'app/dependabot' } + @{ login = 'app/copilot-swe-agent' } + @{ login = 'dependabot[bot]' } + @{ login = 'copilot-pull-request-reviewer[bot]' } + @{ login = 'github-actions' } + @{ login = 'allcontributors' } + @{ login = 'allcontributors[bot]' } + @{ login = 'OssSecurityBot' } + @{ login = 'Copilot' } + @{ login = 'some-new-app[bot]' } + @{ login = '' } + ) { + Test-BotLogin -Login $login | Should -BeTrue + } + + It 'Skips accounts flagged as bots even when the name looks human' { + Test-BotLogin -Login 'someone' -IsBot $true | Should -BeTrue + } + + It 'Keeps ' -ForEach @( + @{ login = 'flanakin' } + @{ login = 'MSBrett' } + @{ login = 'RolandKrummenacher' } + @{ login = 'robot-fan' } + @{ login = 'apple' } + ) { + Test-BotLogin -Login $login -IsBot $false | Should -BeFalse + } + } + + Context 'Get-FileContributionType' { + It 'Returns code for files outside the docs paths' { + Get-FileContributionType -Path 'src/templates/finops-hub/main.bicep', 'src/powershell/Public/Get-FinOpsHub.ps1' | Should -Be @('code') + } + + It 'Returns doc for ' -ForEach @( + @{ path = 'docs-mslearn/toolkit/hubs/overview.md' } + @{ path = 'docs-wiki/Home.md' } + @{ path = 'docs/_layouts/default.html' } + @{ path = 'README.md' } + @{ path = 'src/templates/finops-hub/README.md' } + ) { + Get-FileContributionType -Path $path | Should -Be @('doc') + } + + It 'Returns both code and doc, code first, when a PR touches both' { + Get-FileContributionType -Path 'docs-mslearn/toolkit/hubs/overview.md', 'src/scripts/Build-Toolkit.ps1' | Should -Be @('code', 'doc') + } + + It 'Accepts Windows path separators and leading slashes' { + Get-FileContributionType -Path 'docs-wiki\Home.md', '/docs/index.md' | Should -Be @('doc') + } + + It 'Ignores changelog entries, which almost every code PR has' { + Get-FileContributionType -Path 'docs-mslearn/toolkit/changelog.md', 'src/templates/finops-hub/main.bicep' | Should -Be @('code') + Get-FileContributionType -Path 'docs-mslearn/toolkit/changelog.md' | Should -BeNullOrEmpty + } + + It 'Ignores the contributor list itself' { + Get-FileContributionType -Path '.all-contributorsrc' | Should -BeNullOrEmpty + } + + It 'Returns nothing for no files' { + Get-FileContributionType -Path @() | Should -BeNullOrEmpty + } + } + + Context 'Get-IssueContributionType' { + It 'Maps Type: Bug to bug and Type: Feature to ideas' { + Get-IssueContributionType -Label 'Type: Bug ๐Ÿ›', 'Tool: FinOps hubs' | Should -Be @('bug') + Get-IssueContributionType -Label 'Type: Feature ๐Ÿ’Ž' | Should -Be @('ideas') + } + + It 'Returns nothing for other labels' { + Get-IssueContributionType -Label 'Type: Cleanup', 'Type: Documentation ๐Ÿ“ƒ' | Should -BeNullOrEmpty + Get-IssueContributionType -Label @() | Should -BeNullOrEmpty + } + } + + Context 'Test-IssueCredit' { + It 'Credits issues closed as completed' { + Test-IssueCredit -Label 'Type: Bug ๐Ÿ›' -StateReason 'COMPLETED' | Should -BeTrue + } + + It 'Credits open issues that are in a milestone' { + Test-IssueCredit -Label 'Type: Bug ๐Ÿ›' -StateReason '' -HasMilestone $true | Should -BeTrue + } + + It 'Does not credit open, unplanned issues' { + Test-IssueCredit -Label 'Type: Bug ๐Ÿ›', 'Needs: Attention ๐Ÿ‘‹' -StateReason '' | Should -BeFalse + } + + It 'Does not credit issues closed as ' -ForEach @( + @{ reason = 'NOT_PLANNED' } + @{ reason = 'DUPLICATE' } + ) { + Test-IssueCredit -Label 'Type: Bug ๐Ÿ›' -StateReason $reason -HasMilestone $true | Should -BeFalse + } + + It 'Does not credit issues labeled