diff --git a/.all-contributorsrc b/.all-contributorsrc index 1935bcec9..e7007454c 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,69 @@ "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" + ] + }, + { + "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 86cdfb623..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 `@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. 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..fe76fb5fd --- /dev/null +++ b/.github/workflows/update-contributors.yml @@ -0,0 +1,198 @@ +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. +# +# Two ways to run: +# - On every pull request from this repo, the author (code, doc) and reviewers (review) of that PR are credited with a +# commit on the PR branch, like the ms.date workflow. Pull requests from forks can't be pushed to, so they are +# skipped here. +# - Weekly (and on demand), everything from the last 60 days is checked, which catches fork PRs, reviews added after +# the PR was last updated, and fixed or accepted bugs and feature requests. + +on: + pull_request: + types: [opened, synchronize, reopened] + 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 + +# Each job asks for only what it needs +permissions: {} + +jobs: + credit-pr: + name: Credit pull request contributors + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: write + # Same-repo pull requests only (forks have a read-only token). Skip the weekly PR, which is already the credit. + if: >- + github.event_name == 'pull_request' + && github.event.pull_request.head.repo.full_name == github.repository + && github.head_ref != 'automation/update-contributors' + concurrency: + group: update-contributors-pr-${{ github.event.pull_request.number }} + cancel-in-progress: true + env: + GH_TOKEN: ${{ github.token }} + steps: + - name: Checkout PR branch + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.head_ref }} + token: ${{ secrets.GITHUB_TOKEN }} + + # all-contributors-cli needs Node 22.22 or later + - name: Set up Node.js + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version: '22.22' + + - name: Install all-contributors-cli + run: npm ci --ignore-scripts + + - name: Add missing credits for this pull request + shell: pwsh + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + run: ./src/scripts/Update-Contributors.ps1 -PullRequest ([int]$env:PR_NUMBER) -Apply + + - name: Commit and push changes + env: + HEAD_REF: ${{ github.head_ref }} + run: | + if git diff --quiet -- .all-contributorsrc README.md docs/README.md; then + echo "No missing credits." + exit 0 + fi + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git add .all-contributorsrc README.md docs/README.md + git commit -m "docs: Credit contributors" + + # Other workflows (like ms.date) push to the same branch on the same event, so rebase onto theirs and retry + for attempt in 1 2 3 4; do + if git push; then exit 0; fi + echo "Push failed (attempt $attempt). Rebasing onto the remote branch and retrying." + sleep $((attempt * 5)) + git pull --rebase origin "$HEAD_REF" + done + echo "::error::Could not push the contributor credits after 4 attempts." + exit 1 + + update: + name: Credit new contributors + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: write + pull-requests: write + concurrency: + group: update-contributors + cancel-in-progress: false + # 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. + # all-contributors-cli needs Node 22.22 or later + - name: Set up Node.js + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version: '22.22' + + - 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 8f21334b1..78ac723b5 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#contributing). 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 | + +The `code`, `doc`, `review`, `bug`, and `ideas` credits are found automatically. The [update-contributors](.github/workflows/update-contributors.yml) workflow adds a commit crediting the author and reviewers to every pull request from this repo, and a weekly run opens a pull request with whatever else is missing (fork pull requests, bugs, and feature requests), 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`. +
# πŸ™ Thank you! diff --git a/README.md b/README.md index 71c297e71..b9d538ced 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,15 @@ 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

πŸ› + Brian Purchell
Brian Purchell

πŸ’» + Dan Fiedler
Dan Fiedler

πŸ’» diff --git a/docs/README.md b/docs/README.md index 229a28a1c..ffbcf2b0d 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,15 @@ 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

πŸ› + Brian Purchell
Brian Purchell

πŸ’» + Dan Fiedler
Dan Fiedler

πŸ’» 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..a44b1ab27 --- /dev/null +++ b/src/powershell/Tests/Unit/Action.UpdateContributors.Tests.ps1 @@ -0,0 +1,88 @@ +# 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 run on pull requests but never on push or pull_request_target' { + # pull_request_target would give fork PRs a write token + $workflowContent | Should -Match 'on:\s*\r?\n\s+pull_request:\s*\r?\n\s+types:\s*\[opened, synchronize, reopened\]' + $workflowContent | Should -Not -Match '\n\s+(push|pull_request_target):' + } + + It 'Should only credit same-repo pull requests and skip its own automation branch' { + $workflowContent | Should -Match 'github\.event\.pull_request\.head\.repo\.full_name == github\.repository' + $workflowContent | Should -Match "github\.head_ref != 'automation/update-contributors'" + } + + It 'Should credit the pull request with -PullRequest' { + $workflowContent | Should -Match 'Update-Contributors\.ps1 -PullRequest \(\[int\]\$env:PR_NUMBER\) -Apply' + $workflowContent | Should -Match 'PR_NUMBER:\s*\$\{\{\s*github\.event\.pull_request\.number\s*\}\}' + } + + It 'Should grant no permissions by default and only what each job needs' { + $workflowContent | Should -Match '(?m)^permissions:\s*\{\}' + # credit-pr only pushes a commit; update also opens the pull request + $workflowContent | Should -Match 'credit-pr:[\s\S]*?permissions:\s*\r?\n\s+contents:\s*write\s*\r?\n\s+# ' + $workflowContent | Should -Match 'update:[\s\S]*?permissions:\s*\r?\n\s+contents:\s*write\s*\r?\n\s+pull-requests:\s*write' + } + + It 'Should set up Node.js 22.22 or later before installing the CLI' { + $workflowContent | Should -Match "node-version:\s*'22\.22'" + } + + It 'Should retry the push after rebasing onto the PR branch' { + $workflowContent | Should -Match 'git pull --rebase origin' + } + + It 'Should only run the weekly job 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 + # (single-line values and | or > blocks) + $runs = [regex]::Matches($workflowContent, '(?m)^( *)(?:- )?run:[ \t]*(?:[|>][-+]?[ \t]*\r?\n((?:\1 +.*\r?\n|[ \t]*\r?\n)+)|(.*))') | + ForEach-Object { $_.Groups[2].Value + $_.Groups[3].Value } + $runs | Should -Not -BeNullOrEmpty + foreach ($run in $runs) { $run | Should -Not -Match '\$\{\{' } + $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..7e77a4101 --- /dev/null +++ b/src/powershell/Tests/Unit/Update-Contributors.Tests.ps1 @@ -0,0 +1,335 @@ +# 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 files the contributor CLI rewrites' { + Get-FileContributionType -Path 'README.md', 'docs/README.md' -IgnorePath 'README.md', 'docs/README.md' | Should -BeNullOrEmpty + Get-FileContributionType -Path 'README.md', 'src/templates/finops-hub/main.bicep' -IgnorePath 'README.md' | Should -Be @('code') + Get-FileContributionType -Path 'src/templates/finops-hub/README.md' -IgnorePath 'README.md' | Should -Be @('doc') + } + + 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