From 52ebaf9011d64b38bb2ff896465c06dccb23adab Mon Sep 17 00:00:00 2001 From: Michael Flanakin Date: Sat, 28 Feb 2026 03:43:31 -0800 Subject: [PATCH 1/4] Add per-PR deployment and cleanup workflows Creates two GitHub Actions workflows for per-PR template deployments: ftk-pr-deploy.yml: - Triggers on PRs that change src/templates/** files - Parses PR body checkboxes to determine deployment variants - Supports: ADX (managed), Fabric (manual), manual, no-data, workbooks, alerts - Each variant deploys in parallel with isolated resource groups (pr-{number}-{variant}) - Posts PR comments with deployment status and portal links - Supports fork PRs via "Needs: Deployment" label + pull_request_target - Verifies fork PRs don't modify .github/ files ftk-pr-cleanup.yml: - Triggers on PR close (merged or not) - Deletes all resource groups matching pr-{number}-* - Posts cleanup confirmation comment - Creates GitHub issue assigned to PR author if cleanup fails Co-Authored-By: Claude Opus 4.6 --- .github/workflows/ftk-pr-cleanup.yml | 94 +++++++ .github/workflows/ftk-pr-deploy.yml | 395 +++++++++++++++++++++++++++ 2 files changed, 489 insertions(+) create mode 100644 .github/workflows/ftk-pr-cleanup.yml create mode 100644 .github/workflows/ftk-pr-deploy.yml diff --git a/.github/workflows/ftk-pr-cleanup.yml b/.github/workflows/ftk-pr-cleanup.yml new file mode 100644 index 000000000..6c1f7b8ad --- /dev/null +++ b/.github/workflows/ftk-pr-cleanup.yml @@ -0,0 +1,94 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +name: 'PR Cleanup' + +on: + pull_request: + types: [closed] + +permissions: + id-token: write + contents: read + pull-requests: write + issues: write + +jobs: + cleanup: + name: Clean up PR resources + runs-on: ubuntu-latest + environment: ftk-pr + steps: + - name: Install Az modules + shell: pwsh + run: | + Set-PSRepository PSGallery -InstallationPolicy Trusted + Install-Module -Name Az.Accounts -RequiredVersion 2.19.0 -Force + Install-Module -Name Az.Resources -RequiredVersion 6.16.2 -Force + + - name: Azure login + uses: azure/login@hf_447_release + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + enable-AzPSSession: true + + - name: Delete resource groups + id: delete + shell: pwsh + run: | + $prNumber = "${{ github.event.pull_request.number }}" + $pattern = "pr-$prNumber-*" + $groups = Get-AzResourceGroup | Where-Object { $_.ResourceGroupName -like $pattern } + + if (-not $groups) + { + Write-Host "No resource groups found matching '$pattern'." + "deleted=0" >> $env:GITHUB_OUTPUT + "failed=" >> $env:GITHUB_OUTPUT + return + } + + $deleted = 0 + $failed = @() + foreach ($rg in $groups) + { + try + { + Write-Host "Deleting $($rg.ResourceGroupName)..." + Remove-AzResourceGroup -Name $rg.ResourceGroupName -Force + $deleted++ + } + catch + { + Write-Warning "Failed to delete $($rg.ResourceGroupName): $_" + $failed += $rg.ResourceGroupName + } + } + + "deleted=$deleted" >> $env:GITHUB_OUTPUT + "failed=$($failed -join ',')" >> $env:GITHUB_OUTPUT + + - name: Post cleanup comment + if: steps.delete.outputs.deleted != '0' + env: + GH_TOKEN: ${{ github.token }} + run: | + gh pr comment "${{ github.event.pull_request.number }}" \ + --repo "${{ github.repository }}" \ + --body "Test environments cleaned up." + + - name: Create issue for failed cleanups + if: steps.delete.outputs.failed != '' + env: + GH_TOKEN: ${{ github.token }} + run: | + failed="${{ steps.delete.outputs.failed }}" + pr=${{ github.event.pull_request.number }} + author=${{ github.event.pull_request.user.login }} + gh issue create \ + --repo "${{ github.repository }}" \ + --title "Cleanup failed for PR #$pr" \ + --body "The following resource groups could not be deleted: $failed. Please delete them manually." \ + --assignee "$author" diff --git a/.github/workflows/ftk-pr-deploy.yml b/.github/workflows/ftk-pr-deploy.yml new file mode 100644 index 000000000..95fbd3743 --- /dev/null +++ b/.github/workflows/ftk-pr-deploy.yml @@ -0,0 +1,395 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +name: 'PR Deploy' + +on: + pull_request: + types: [opened, synchronize, edited] + paths: + - 'src/templates/**' + - '!src/templates/**/README.md' + - '!src/templates/**/docs/**' + - '!src/templates/**/test/**' + + pull_request_target: + types: [labeled] + +concurrency: + group: ftk-pr-${{ github.event.pull_request.number }} + cancel-in-progress: true + +permissions: + id-token: write + contents: read + pull-requests: write + +jobs: + #---------------------------------------------------------------------------- + # Parse PR body checkboxes and determine what to deploy + #---------------------------------------------------------------------------- + check-options: + name: Check deployment options + runs-on: ubuntu-latest + # Skip fork PRs unless they have the deployment label + if: > + github.event.action == 'labeled' && github.event.label.name == 'Needs: Deployment' + || github.event.pull_request.head.repo.full_name == github.repository + outputs: + any-selected: ${{ steps.parse.outputs.any-selected }} + deploy-adx: ${{ steps.parse.outputs.deploy-adx }} + deploy-fabric: ${{ steps.parse.outputs.deploy-fabric }} + deploy-manual: ${{ steps.parse.outputs.deploy-manual }} + deploy-nodata: ${{ steps.parse.outputs.deploy-nodata }} + deploy-workbooks: ${{ steps.parse.outputs.deploy-workbooks }} + deploy-alerts: ${{ steps.parse.outputs.deploy-alerts }} + fabric-uri: ${{ steps.parse.outputs.fabric-uri }} + templates-changed: ${{ steps.check-files.outputs.changed }} + steps: + - name: Check for template file changes + id: check-files + env: + GH_TOKEN: ${{ github.token }} + run: | + # For 'edited' events, paths filter doesn't apply — verify manually + changed=$(gh pr diff ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --name-only \ + | grep -E '^src/templates/' \ + | grep -v -E '(README\.md|/docs/|/test/)' \ + | head -1) + if [ -n "$changed" ]; then + echo "changed=true" >> "$GITHUB_OUTPUT" + else + echo "changed=false" >> "$GITHUB_OUTPUT" + fi + + - name: Verify fork PR safety + if: github.event.pull_request.head.repo.full_name != github.repository + env: + GH_TOKEN: ${{ github.token }} + run: | + # Block fork PRs that modify workflow files + modified=$(gh pr diff ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --name-only \ + | grep -E '^\.github/' \ + | head -1) + if [ -n "$modified" ]; then + echo "::error::Fork PRs that modify .github/ files cannot use deployment CI." + exit 1 + fi + + - name: Parse deployment checkboxes + id: parse + run: | + body='${{ github.event.pull_request.body }}' + + parse_checkbox() { + local pattern="$1" + local output_name="$2" + if echo "$body" | grep -qE "\- \[x\] $pattern"; then + echo "$output_name=true" >> "$GITHUB_OUTPUT" + return 0 + else + echo "$output_name=false" >> "$GITHUB_OUTPUT" + return 1 + fi + } + + any=false + + parse_checkbox "Hubs + ADX \(managed\)" "deploy-adx" && any=true + parse_checkbox "Hubs + Fabric \(manual\)" "deploy-fabric" && any=true + parse_checkbox "Hubs \(manual\)" "deploy-manual" && any=true + parse_checkbox "Hubs \(no data\)" "deploy-nodata" && any=true + parse_checkbox "Workbooks" "deploy-workbooks" && any=true + parse_checkbox "Alerts" "deploy-alerts" && any=true + + echo "any-selected=$any" >> "$GITHUB_OUTPUT" + + # Extract Fabric URI from the checkbox line + fabric_uri=$(echo "$body" | grep -oP '(?<=Hubs + Fabric \(manual\) — URI: )\S+' || true) + echo "fabric-uri=$fabric_uri" >> "$GITHUB_OUTPUT" + + #---------------------------------------------------------------------------- + # Hub deployments + #---------------------------------------------------------------------------- + deploy-adx: + name: 'Hubs + ADX (managed)' + needs: check-options + if: needs.check-options.outputs.deploy-adx == 'true' && needs.check-options.outputs.templates-changed == 'true' + runs-on: ubuntu-latest + environment: ftk-pr + steps: + - name: Install Az modules + shell: pwsh + run: | + Set-PSRepository PSGallery -InstallationPolicy Trusted + Install-Module -Name Az.Accounts -RequiredVersion 2.19.0 -Force + Install-Module -Name Az.Resources -RequiredVersion 6.16.2 -Force + Install-Module -Name Az.Storage -RequiredVersion 6.2.0 -Force + + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Azure login + uses: azure/login@hf_447_release + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + enable-AzPSSession: true + + - name: Deploy + shell: pwsh + run: | + ./src/scripts/Deploy-Hub -PR -Name "${{ github.event.pull_request.number }}-adx" ` + -Scope "${{ secrets.CI_SCOPE }}" -ManagedExports -Build + + - name: Post PR comment + if: always() + env: + GH_TOKEN: ${{ github.token }} + run: | + pr=${{ github.event.pull_request.number }} + rg="pr-$pr-adx" + if [ "${{ job.status }}" = "success" ]; then + body="**Hubs + ADX (managed)** deployed to [\`$rg\`](https://portal.azure.com/#@/resource/subscriptions/${{ secrets.AZURE_SUBSCRIPTION_ID }}/resourceGroups/$rg/overview)." + else + body="**Hubs + ADX (managed)** deployment failed. [View logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})." + fi + gh pr comment "$pr" --repo "${{ github.repository }}" --body "$body" + + deploy-fabric: + name: 'Hubs + Fabric (manual)' + needs: check-options + if: needs.check-options.outputs.deploy-fabric == 'true' && needs.check-options.outputs.templates-changed == 'true' + runs-on: ubuntu-latest + environment: ftk-pr + steps: + - name: Install Az modules + shell: pwsh + run: | + Set-PSRepository PSGallery -InstallationPolicy Trusted + Install-Module -Name Az.Accounts -RequiredVersion 2.19.0 -Force + Install-Module -Name Az.Resources -RequiredVersion 6.16.2 -Force + Install-Module -Name Az.Storage -RequiredVersion 6.2.0 -Force + + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Azure login + uses: azure/login@hf_447_release + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + enable-AzPSSession: true + + - name: Deploy + shell: pwsh + run: | + ./src/scripts/Deploy-Hub -PR -Name "${{ github.event.pull_request.number }}-fabric" ` + -Fabric "${{ needs.check-options.outputs.fabric-uri }}" ` + -Scope "${{ secrets.CI_SCOPE }}" -Build + + - name: Post PR comment + if: always() + env: + GH_TOKEN: ${{ github.token }} + run: | + pr=${{ github.event.pull_request.number }} + rg="pr-$pr-fabric" + if [ "${{ job.status }}" = "success" ]; then + body="**Hubs + Fabric (manual)** deployed to [\`$rg\`](https://portal.azure.com/#@/resource/subscriptions/${{ secrets.AZURE_SUBSCRIPTION_ID }}/resourceGroups/$rg/overview). Fabric requires running KQL setup scripts manually." + else + body="**Hubs + Fabric (manual)** deployment failed. [View logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})." + fi + gh pr comment "$pr" --repo "${{ github.repository }}" --body "$body" + + deploy-manual: + name: 'Hubs + (manual)' + needs: check-options + if: needs.check-options.outputs.deploy-manual == 'true' && needs.check-options.outputs.templates-changed == 'true' + runs-on: ubuntu-latest + environment: ftk-pr + steps: + - name: Install Az modules + shell: pwsh + run: | + Set-PSRepository PSGallery -InstallationPolicy Trusted + Install-Module -Name Az.Accounts -RequiredVersion 2.19.0 -Force + Install-Module -Name Az.Resources -RequiredVersion 6.16.2 -Force + Install-Module -Name Az.Storage -RequiredVersion 6.2.0 -Force + + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Azure login + uses: azure/login@hf_447_release + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + enable-AzPSSession: true + + - name: Deploy + shell: pwsh + run: | + ./src/scripts/Deploy-Hub -PR -Name "${{ github.event.pull_request.number }}-manual" ` + -StorageOnly -Scope "${{ secrets.CI_SCOPE }}" -Build + + - name: Post PR comment + if: always() + env: + GH_TOKEN: ${{ github.token }} + run: | + pr=${{ github.event.pull_request.number }} + rg="pr-$pr-manual" + if [ "${{ job.status }}" = "success" ]; then + body="**Hubs (manual)** deployed to [\`$rg\`](https://portal.azure.com/#@/resource/subscriptions/${{ secrets.AZURE_SUBSCRIPTION_ID }}/resourceGroups/$rg/overview)." + else + body="**Hubs (manual)** deployment failed. [View logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})." + fi + gh pr comment "$pr" --repo "${{ github.repository }}" --body "$body" + + deploy-nodata: + name: Hubs (no data) + needs: check-options + if: needs.check-options.outputs.deploy-nodata == 'true' && needs.check-options.outputs.templates-changed == 'true' + runs-on: ubuntu-latest + environment: ftk-pr + steps: + - name: Install Az modules + shell: pwsh + run: | + Set-PSRepository PSGallery -InstallationPolicy Trusted + Install-Module -Name Az.Accounts -RequiredVersion 2.19.0 -Force + Install-Module -Name Az.Resources -RequiredVersion 6.16.2 -Force + + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Azure login + uses: azure/login@hf_447_release + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + enable-AzPSSession: true + + - name: Deploy + shell: pwsh + run: | + ./src/scripts/Deploy-Hub -PR -Name "${{ github.event.pull_request.number }}-nodata" ` + -StorageOnly -Build + + - name: Post PR comment + if: always() + env: + GH_TOKEN: ${{ github.token }} + run: | + pr=${{ github.event.pull_request.number }} + rg="pr-$pr-nodata" + if [ "${{ job.status }}" = "success" ]; then + body="**Hubs (no data)** deployed to [\`$rg\`](https://portal.azure.com/#@/resource/subscriptions/${{ secrets.AZURE_SUBSCRIPTION_ID }}/resourceGroups/$rg/overview)." + else + body="**Hubs (no data)** deployment failed. [View logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})." + fi + gh pr comment "$pr" --repo "${{ github.repository }}" --body "$body" + + #---------------------------------------------------------------------------- + # Non-hub deployments + #---------------------------------------------------------------------------- + deploy-workbooks: + name: Workbooks + needs: check-options + if: needs.check-options.outputs.deploy-workbooks == 'true' && needs.check-options.outputs.templates-changed == 'true' + runs-on: ubuntu-latest + environment: ftk-pr + steps: + - name: Install Az modules + shell: pwsh + run: | + Set-PSRepository PSGallery -InstallationPolicy Trusted + Install-Module -Name Az.Accounts -RequiredVersion 2.19.0 -Force + Install-Module -Name Az.Resources -RequiredVersion 6.16.2 -Force + + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Azure login + uses: azure/login@hf_447_release + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + enable-AzPSSession: true + + - name: Deploy + shell: pwsh + run: | + ./src/scripts/Deploy-Toolkit "finops-workbooks" ` + -ResourceGroup "pr-${{ github.event.pull_request.number }}-workbooks" -Build + + - name: Post PR comment + if: always() + env: + GH_TOKEN: ${{ github.token }} + run: | + pr=${{ github.event.pull_request.number }} + rg="pr-$pr-workbooks" + if [ "${{ job.status }}" = "success" ]; then + body="**Workbooks** deployed to [\`$rg\`](https://portal.azure.com/#@/resource/subscriptions/${{ secrets.AZURE_SUBSCRIPTION_ID }}/resourceGroups/$rg/overview)." + else + body="**Workbooks** deployment failed. [View logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})." + fi + gh pr comment "$pr" --repo "${{ github.repository }}" --body "$body" + + deploy-alerts: + name: Alerts + needs: check-options + if: needs.check-options.outputs.deploy-alerts == 'true' && needs.check-options.outputs.templates-changed == 'true' + runs-on: ubuntu-latest + environment: ftk-pr + steps: + - name: Install Az modules + shell: pwsh + run: | + Set-PSRepository PSGallery -InstallationPolicy Trusted + Install-Module -Name Az.Accounts -RequiredVersion 2.19.0 -Force + Install-Module -Name Az.Resources -RequiredVersion 6.16.2 -Force + + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Azure login + uses: azure/login@hf_447_release + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + enable-AzPSSession: true + + - name: Deploy + shell: pwsh + run: | + ./src/scripts/Deploy-Toolkit "finops-alerts" ` + -ResourceGroup "pr-${{ github.event.pull_request.number }}-alerts" -Build + + - name: Post PR comment + if: always() + env: + GH_TOKEN: ${{ github.token }} + run: | + pr=${{ github.event.pull_request.number }} + rg="pr-$pr-alerts" + if [ "${{ job.status }}" = "success" ]; then + body="**Alerts** deployed to [\`$rg\`](https://portal.azure.com/#@/resource/subscriptions/${{ secrets.AZURE_SUBSCRIPTION_ID }}/resourceGroups/$rg/overview)." + else + body="**Alerts** deployment failed. [View logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})." + fi + gh pr comment "$pr" --repo "${{ github.repository }}" --body "$body" From 7342ec44367e369a8004e51fe7c1703cd908fd38 Mon Sep 17 00:00:00 2001 From: Michael Flanakin Date: Sat, 28 Feb 2026 22:19:33 -0800 Subject: [PATCH 2/4] Fix workflow bugs found during validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix PR body script injection: use env var instead of inline interpolation - Escape literal + in regex patterns for checkbox matching - Fix azure/login: use @v2 release tag instead of branch ref - Fix job name typo: "Hubs + (manual)" → "Hubs (manual)" - Remove unused any-selected output Co-Authored-By: Claude Opus 4.6 --- .github/workflows/ftk-pr-cleanup.yml | 2 +- .github/workflows/ftk-pr-deploy.yml | 39 ++++++++++++---------------- 2 files changed, 18 insertions(+), 23 deletions(-) diff --git a/.github/workflows/ftk-pr-cleanup.yml b/.github/workflows/ftk-pr-cleanup.yml index 6c1f7b8ad..02320e9c2 100644 --- a/.github/workflows/ftk-pr-cleanup.yml +++ b/.github/workflows/ftk-pr-cleanup.yml @@ -27,7 +27,7 @@ jobs: Install-Module -Name Az.Resources -RequiredVersion 6.16.2 -Force - name: Azure login - uses: azure/login@hf_447_release + uses: azure/login@v2 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} diff --git a/.github/workflows/ftk-pr-deploy.yml b/.github/workflows/ftk-pr-deploy.yml index 95fbd3743..f57d4d3f2 100644 --- a/.github/workflows/ftk-pr-deploy.yml +++ b/.github/workflows/ftk-pr-deploy.yml @@ -36,7 +36,6 @@ jobs: github.event.action == 'labeled' && github.event.label.name == 'Needs: Deployment' || github.event.pull_request.head.repo.full_name == github.repository outputs: - any-selected: ${{ steps.parse.outputs.any-selected }} deploy-adx: ${{ steps.parse.outputs.deploy-adx }} deploy-fabric: ${{ steps.parse.outputs.deploy-fabric }} deploy-manual: ${{ steps.parse.outputs.deploy-manual }} @@ -78,13 +77,13 @@ jobs: - name: Parse deployment checkboxes id: parse + env: + PR_BODY: ${{ github.event.pull_request.body }} run: | - body='${{ github.event.pull_request.body }}' - parse_checkbox() { local pattern="$1" local output_name="$2" - if echo "$body" | grep -qE "\- \[x\] $pattern"; then + if echo "$PR_BODY" | grep -qE "\- \[x\] $pattern"; then echo "$output_name=true" >> "$GITHUB_OUTPUT" return 0 else @@ -93,19 +92,15 @@ jobs: fi } - any=false - - parse_checkbox "Hubs + ADX \(managed\)" "deploy-adx" && any=true - parse_checkbox "Hubs + Fabric \(manual\)" "deploy-fabric" && any=true - parse_checkbox "Hubs \(manual\)" "deploy-manual" && any=true - parse_checkbox "Hubs \(no data\)" "deploy-nodata" && any=true - parse_checkbox "Workbooks" "deploy-workbooks" && any=true - parse_checkbox "Alerts" "deploy-alerts" && any=true - - echo "any-selected=$any" >> "$GITHUB_OUTPUT" + parse_checkbox "Hubs \+ ADX \(managed\)" "deploy-adx" + parse_checkbox "Hubs \+ Fabric \(manual\)" "deploy-fabric" + parse_checkbox "Hubs \(manual\)" "deploy-manual" + parse_checkbox "Hubs \(no data\)" "deploy-nodata" + parse_checkbox "Workbooks" "deploy-workbooks" + parse_checkbox "Alerts" "deploy-alerts" # Extract Fabric URI from the checkbox line - fabric_uri=$(echo "$body" | grep -oP '(?<=Hubs + Fabric \(manual\) — URI: )\S+' || true) + fabric_uri=$(echo "$PR_BODY" | grep -oP '(?<=Hubs \+ Fabric \(manual\) — URI: )\S+' || true) echo "fabric-uri=$fabric_uri" >> "$GITHUB_OUTPUT" #---------------------------------------------------------------------------- @@ -131,7 +126,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - name: Azure login - uses: azure/login@hf_447_release + uses: azure/login@v2 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} @@ -178,7 +173,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - name: Azure login - uses: azure/login@hf_447_release + uses: azure/login@v2 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} @@ -207,7 +202,7 @@ jobs: gh pr comment "$pr" --repo "${{ github.repository }}" --body "$body" deploy-manual: - name: 'Hubs + (manual)' + name: 'Hubs (manual)' needs: check-options if: needs.check-options.outputs.deploy-manual == 'true' && needs.check-options.outputs.templates-changed == 'true' runs-on: ubuntu-latest @@ -226,7 +221,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - name: Azure login - uses: azure/login@hf_447_release + uses: azure/login@v2 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} @@ -272,7 +267,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - name: Azure login - uses: azure/login@hf_447_release + uses: azure/login@v2 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} @@ -321,7 +316,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - name: Azure login - uses: azure/login@hf_447_release + uses: azure/login@v2 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} @@ -367,7 +362,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - name: Azure login - uses: azure/login@hf_447_release + uses: azure/login@v2 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} From e03515b419769f513696d0a12ba45df12ac1c2c1 Mon Sep 17 00:00:00 2001 From: Michael Flanakin Date: Sun, 1 Mar 2026 01:32:03 -0800 Subject: [PATCH 3/4] Address PR #2033 feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Tighten pull_request_target condition to only fire on Needs: Deployment label - Expand fork safety check to block src/scripts/ changes - Add Fabric URI validation before deploy (fails fast as first step) - Change cleanup trigger to pull_request_target for fork PR secret access - Add -ErrorAction Stop to Remove-AzResourceGroup for proper error handling - Fix assignee fallback for external contributors on cleanup failure issues 🤖 Generated with [Claude Code](https://claude.ai/claude-code) Co-Authored-By: Claude --- .github/workflows/ftk-pr-cleanup.yml | 12 +++++++----- .github/workflows/ftk-pr-deploy.yml | 18 +++++++++++++----- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ftk-pr-cleanup.yml b/.github/workflows/ftk-pr-cleanup.yml index 02320e9c2..c7c2a0a0b 100644 --- a/.github/workflows/ftk-pr-cleanup.yml +++ b/.github/workflows/ftk-pr-cleanup.yml @@ -4,7 +4,7 @@ name: 'PR Cleanup' on: - pull_request: + pull_request_target: types: [closed] permissions: @@ -57,7 +57,7 @@ jobs: try { Write-Host "Deleting $($rg.ResourceGroupName)..." - Remove-AzResourceGroup -Name $rg.ResourceGroupName -Force + Remove-AzResourceGroup -Name $rg.ResourceGroupName -Force -ErrorAction Stop $deleted++ } catch @@ -87,8 +87,10 @@ jobs: failed="${{ steps.delete.outputs.failed }}" pr=${{ github.event.pull_request.number }} author=${{ github.event.pull_request.user.login }} - gh issue create \ + issue_url=$(gh issue create \ --repo "${{ github.repository }}" \ --title "Cleanup failed for PR #$pr" \ - --body "The following resource groups could not be deleted: $failed. Please delete them manually." \ - --assignee "$author" + --body "The following resource groups could not be deleted: $failed. Please delete them manually. See #$pr.") + # Try to assign the PR author; ignore failure for external contributors + issue_number=$(echo "$issue_url" | grep -oE '[0-9]+$') + gh issue edit "$issue_number" --repo "${{ github.repository }}" --add-assignee "$author" || true diff --git a/.github/workflows/ftk-pr-deploy.yml b/.github/workflows/ftk-pr-deploy.yml index f57d4d3f2..2020a5651 100644 --- a/.github/workflows/ftk-pr-deploy.yml +++ b/.github/workflows/ftk-pr-deploy.yml @@ -33,8 +33,8 @@ jobs: runs-on: ubuntu-latest # Skip fork PRs unless they have the deployment label if: > - github.event.action == 'labeled' && github.event.label.name == 'Needs: Deployment' - || github.event.pull_request.head.repo.full_name == github.repository + (github.event_name == 'pull_request_target' && github.event.label.name == 'Needs: Deployment') + || github.event_name == 'pull_request' outputs: deploy-adx: ${{ steps.parse.outputs.deploy-adx }} deploy-fabric: ${{ steps.parse.outputs.deploy-fabric }} @@ -66,12 +66,12 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - # Block fork PRs that modify workflow files + # Block fork PRs that modify workflow or script files modified=$(gh pr diff ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --name-only \ - | grep -E '^\.github/' \ + | grep -E '^(\.github/|src/scripts/)' \ | head -1) if [ -n "$modified" ]; then - echo "::error::Fork PRs that modify .github/ files cannot use deployment CI." + echo "::error::Fork PRs that modify .github/ or src/scripts/ files cannot use deployment CI." exit 1 fi @@ -160,6 +160,14 @@ jobs: runs-on: ubuntu-latest environment: ftk-pr steps: + - name: Validate Fabric URI + run: | + uri="${{ needs.check-options.outputs.fabric-uri }}" + if [ -z "$uri" ]; then + echo "::error::Hubs + Fabric (manual) checkbox is checked but no URI was provided. Add the eventhouse query URI to the PR body." + exit 1 + fi + - name: Install Az modules shell: pwsh run: | From 1f746b70268fd1aadbe8ad83eb30c696c69492e4 Mon Sep 17 00:00:00 2001 From: Michael Flanakin Date: Wed, 4 Mar 2026 01:21:21 -0800 Subject: [PATCH 4/4] Address PR #2033 feedback: security, perf, and parameter fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix fabric-uri script injection: pass via env var instead of direct interpolation - Add environment approval gate comment for fork PR security - Add timeout-minutes: 60 to all deployment jobs - Centralize Az module versions as workflow-level env vars - Cache Az modules in check-options, restore in deployment jobs - Update Deploy-Hub calls to use -PR {number} -Name "{variant}" 🤖 Generated with [Claude Code](https://claude.ai/claude-code) Co-Authored-By: RolandKrummenacher Co-Authored-By: Claude --- .github/workflows/ftk-pr-deploy.yml | 120 +++++++++++++++++----------- 1 file changed, 74 insertions(+), 46 deletions(-) diff --git a/.github/workflows/ftk-pr-deploy.yml b/.github/workflows/ftk-pr-deploy.yml index 2020a5651..85ae24da6 100644 --- a/.github/workflows/ftk-pr-deploy.yml +++ b/.github/workflows/ftk-pr-deploy.yml @@ -15,6 +15,11 @@ on: pull_request_target: types: [labeled] +env: + AZ_ACCOUNTS_VERSION: '2.19.0' + AZ_RESOURCES_VERSION: '6.16.2' + AZ_STORAGE_VERSION: '6.2.0' + concurrency: group: ftk-pr-${{ github.event.pull_request.number }} cancel-in-progress: true @@ -75,6 +80,20 @@ jobs: exit 1 fi + - name: Install Az modules + shell: pwsh + run: | + Set-PSRepository PSGallery -InstallationPolicy Trusted + Install-Module -Name Az.Accounts -RequiredVersion $env:AZ_ACCOUNTS_VERSION -Force + Install-Module -Name Az.Resources -RequiredVersion $env:AZ_RESOURCES_VERSION -Force + Install-Module -Name Az.Storage -RequiredVersion $env:AZ_STORAGE_VERSION -Force + + - name: Cache Az modules + uses: actions/cache/save@v4 + with: + path: /home/runner/.local/share/powershell/Modules + key: az-modules-${{ env.AZ_ACCOUNTS_VERSION }}-${{ env.AZ_RESOURCES_VERSION }}-${{ env.AZ_STORAGE_VERSION }} + - name: Parse deployment checkboxes id: parse env: @@ -106,20 +125,23 @@ jobs: #---------------------------------------------------------------------------- # Hub deployments #---------------------------------------------------------------------------- + # NOTE: The ftk-pr environment MUST have required reviewers configured in GitHub + # settings to gate fork PR deployments. Without this, fork PRs could deploy + # malicious ARM templates using CI credentials. deploy-adx: name: 'Hubs + ADX (managed)' needs: check-options if: needs.check-options.outputs.deploy-adx == 'true' && needs.check-options.outputs.templates-changed == 'true' runs-on: ubuntu-latest + timeout-minutes: 60 environment: ftk-pr steps: - - name: Install Az modules - shell: pwsh - run: | - Set-PSRepository PSGallery -InstallationPolicy Trusted - Install-Module -Name Az.Accounts -RequiredVersion 2.19.0 -Force - Install-Module -Name Az.Resources -RequiredVersion 6.16.2 -Force - Install-Module -Name Az.Storage -RequiredVersion 6.2.0 -Force + - name: Restore Az modules + uses: actions/cache/restore@v4 + with: + path: /home/runner/.local/share/powershell/Modules + key: az-modules-${{ env.AZ_ACCOUNTS_VERSION }}-${{ env.AZ_RESOURCES_VERSION }}-${{ env.AZ_STORAGE_VERSION }} + fail-on-cache-miss: true - uses: actions/checkout@v4 with: @@ -136,7 +158,7 @@ jobs: - name: Deploy shell: pwsh run: | - ./src/scripts/Deploy-Hub -PR -Name "${{ github.event.pull_request.number }}-adx" ` + ./src/scripts/Deploy-Hub -PR ${{ github.event.pull_request.number }} -Name "adx" ` -Scope "${{ secrets.CI_SCOPE }}" -ManagedExports -Build - name: Post PR comment @@ -158,23 +180,24 @@ jobs: needs: check-options if: needs.check-options.outputs.deploy-fabric == 'true' && needs.check-options.outputs.templates-changed == 'true' runs-on: ubuntu-latest + timeout-minutes: 60 environment: ftk-pr steps: - name: Validate Fabric URI + env: + FABRIC_URI: ${{ needs.check-options.outputs.fabric-uri }} run: | - uri="${{ needs.check-options.outputs.fabric-uri }}" - if [ -z "$uri" ]; then + if [ -z "$FABRIC_URI" ]; then echo "::error::Hubs + Fabric (manual) checkbox is checked but no URI was provided. Add the eventhouse query URI to the PR body." exit 1 fi - - name: Install Az modules - shell: pwsh - run: | - Set-PSRepository PSGallery -InstallationPolicy Trusted - Install-Module -Name Az.Accounts -RequiredVersion 2.19.0 -Force - Install-Module -Name Az.Resources -RequiredVersion 6.16.2 -Force - Install-Module -Name Az.Storage -RequiredVersion 6.2.0 -Force + - name: Restore Az modules + uses: actions/cache/restore@v4 + with: + path: /home/runner/.local/share/powershell/Modules + key: az-modules-${{ env.AZ_ACCOUNTS_VERSION }}-${{ env.AZ_RESOURCES_VERSION }}-${{ env.AZ_STORAGE_VERSION }} + fail-on-cache-miss: true - uses: actions/checkout@v4 with: @@ -190,9 +213,11 @@ jobs: - name: Deploy shell: pwsh + env: + FABRIC_URI: ${{ needs.check-options.outputs.fabric-uri }} run: | - ./src/scripts/Deploy-Hub -PR -Name "${{ github.event.pull_request.number }}-fabric" ` - -Fabric "${{ needs.check-options.outputs.fabric-uri }}" ` + ./src/scripts/Deploy-Hub -PR ${{ github.event.pull_request.number }} -Name "fabric" ` + -Fabric "$env:FABRIC_URI" ` -Scope "${{ secrets.CI_SCOPE }}" -Build - name: Post PR comment @@ -214,15 +239,15 @@ jobs: needs: check-options if: needs.check-options.outputs.deploy-manual == 'true' && needs.check-options.outputs.templates-changed == 'true' runs-on: ubuntu-latest + timeout-minutes: 60 environment: ftk-pr steps: - - name: Install Az modules - shell: pwsh - run: | - Set-PSRepository PSGallery -InstallationPolicy Trusted - Install-Module -Name Az.Accounts -RequiredVersion 2.19.0 -Force - Install-Module -Name Az.Resources -RequiredVersion 6.16.2 -Force - Install-Module -Name Az.Storage -RequiredVersion 6.2.0 -Force + - name: Restore Az modules + uses: actions/cache/restore@v4 + with: + path: /home/runner/.local/share/powershell/Modules + key: az-modules-${{ env.AZ_ACCOUNTS_VERSION }}-${{ env.AZ_RESOURCES_VERSION }}-${{ env.AZ_STORAGE_VERSION }} + fail-on-cache-miss: true - uses: actions/checkout@v4 with: @@ -239,7 +264,7 @@ jobs: - name: Deploy shell: pwsh run: | - ./src/scripts/Deploy-Hub -PR -Name "${{ github.event.pull_request.number }}-manual" ` + ./src/scripts/Deploy-Hub -PR ${{ github.event.pull_request.number }} -Name "manual" ` -StorageOnly -Scope "${{ secrets.CI_SCOPE }}" -Build - name: Post PR comment @@ -261,14 +286,15 @@ jobs: needs: check-options if: needs.check-options.outputs.deploy-nodata == 'true' && needs.check-options.outputs.templates-changed == 'true' runs-on: ubuntu-latest + timeout-minutes: 60 environment: ftk-pr steps: - - name: Install Az modules - shell: pwsh - run: | - Set-PSRepository PSGallery -InstallationPolicy Trusted - Install-Module -Name Az.Accounts -RequiredVersion 2.19.0 -Force - Install-Module -Name Az.Resources -RequiredVersion 6.16.2 -Force + - name: Restore Az modules + uses: actions/cache/restore@v4 + with: + path: /home/runner/.local/share/powershell/Modules + key: az-modules-${{ env.AZ_ACCOUNTS_VERSION }}-${{ env.AZ_RESOURCES_VERSION }}-${{ env.AZ_STORAGE_VERSION }} + fail-on-cache-miss: true - uses: actions/checkout@v4 with: @@ -285,7 +311,7 @@ jobs: - name: Deploy shell: pwsh run: | - ./src/scripts/Deploy-Hub -PR -Name "${{ github.event.pull_request.number }}-nodata" ` + ./src/scripts/Deploy-Hub -PR ${{ github.event.pull_request.number }} -Name "nodata" ` -StorageOnly -Build - name: Post PR comment @@ -310,14 +336,15 @@ jobs: needs: check-options if: needs.check-options.outputs.deploy-workbooks == 'true' && needs.check-options.outputs.templates-changed == 'true' runs-on: ubuntu-latest + timeout-minutes: 60 environment: ftk-pr steps: - - name: Install Az modules - shell: pwsh - run: | - Set-PSRepository PSGallery -InstallationPolicy Trusted - Install-Module -Name Az.Accounts -RequiredVersion 2.19.0 -Force - Install-Module -Name Az.Resources -RequiredVersion 6.16.2 -Force + - name: Restore Az modules + uses: actions/cache/restore@v4 + with: + path: /home/runner/.local/share/powershell/Modules + key: az-modules-${{ env.AZ_ACCOUNTS_VERSION }}-${{ env.AZ_RESOURCES_VERSION }}-${{ env.AZ_STORAGE_VERSION }} + fail-on-cache-miss: true - uses: actions/checkout@v4 with: @@ -356,14 +383,15 @@ jobs: needs: check-options if: needs.check-options.outputs.deploy-alerts == 'true' && needs.check-options.outputs.templates-changed == 'true' runs-on: ubuntu-latest + timeout-minutes: 60 environment: ftk-pr steps: - - name: Install Az modules - shell: pwsh - run: | - Set-PSRepository PSGallery -InstallationPolicy Trusted - Install-Module -Name Az.Accounts -RequiredVersion 2.19.0 -Force - Install-Module -Name Az.Resources -RequiredVersion 6.16.2 -Force + - name: Restore Az modules + uses: actions/cache/restore@v4 + with: + path: /home/runner/.local/share/powershell/Modules + key: az-modules-${{ env.AZ_ACCOUNTS_VERSION }}-${{ env.AZ_RESOURCES_VERSION }}-${{ env.AZ_STORAGE_VERSION }} + fail-on-cache-miss: true - uses: actions/checkout@v4 with: