Generate secret connection environment variables #648
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # yaml-language-server: $schema=https://www.schemastore.org/github-workflow.json | |
| --- | |
| name: Validate Azure Recipes | |
| on: | |
| push: | |
| branches: [main] | |
| paths: | |
| - "**/azure/**" | |
| - "**/azure-*/**" | |
| - "**/test/app.bicep" | |
| - ".github/workflows/validate-azure-recipes.yaml" | |
| - ".github/scripts/**" | |
| - ".github/build/**" | |
| - "Makefile" | |
| # pull_request_target runs in the context of the base branch, providing access to secrets. | |
| # For external contributors, the approval-gate job requires manual approval before tests run. | |
| # SECURITY: We use pull_request_target but do NOT run any code from the PR until after approval. | |
| # | |
| # NOTE: No paths filter on pull_request_target. The "check-paths" job below determines | |
| # whether Azure files changed. When they haven't, the "skip-azure-validation" job | |
| # reports the required check names so branch protection is satisfied without running | |
| # expensive Azure tests. | |
| pull_request_target: | |
| branches: [main] | |
| merge_group: | |
| types: [checks_requested] | |
| workflow_dispatch: | |
| inputs: | |
| version: | |
| description: "Radius version number to use (e.g. 0.51.0, 0.51.0-rc1, edge)." | |
| required: false | |
| default: edge | |
| type: string | |
| permissions: {} | |
| # Only one Azure validation run per PR (or per ref) at a time. This keeps a single | |
| # pending approval per PR, so a reviewer can't be presented with several runs for | |
| # different head SHAs and approve the wrong one. | |
| concurrency: | |
| group: validate-azure-recipes-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| # Detect whether Azure-related files changed. For non-PR events (push, merge_group, | |
| # workflow_dispatch) the output defaults to 'true' so tests always run. | |
| check-paths: | |
| name: Check Paths | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 5 | |
| permissions: | |
| pull-requests: read | |
| outputs: | |
| azure-changed: ${{ steps.result.outputs.azure-changed }} | |
| steps: | |
| # REST API mode: no checkout needed, works with fork PRs. Only supports | |
| # pull_request* events — non-PR events skip this and default to azure-changed=true. | |
| - name: Detect Azure file changes | |
| id: filter | |
| if: github.event_name == 'pull_request_target' | |
| uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 | |
| with: | |
| use_rest_api: true | |
| files: | | |
| **/azure/** | |
| **/azure-*/** | |
| **/test/app.bicep | |
| .github/workflows/validate-azure-recipes.yaml | |
| .github/scripts/** | |
| .github/build/** | |
| Makefile | |
| - name: Set result | |
| id: result | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| ANY_MODIFIED: ${{ steps.filter.outputs.any_modified }} | |
| run: | | |
| set -euo pipefail | |
| if [ "$EVENT_NAME" = "pull_request_target" ]; then | |
| echo "azure-changed=$ANY_MODIFIED" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "Non-PR event — assuming Azure files changed" | |
| echo "azure-changed=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| # When no Azure files changed, report the required check names as successful | |
| # so that branch protection is satisfied. | |
| skip-azure-validation: | |
| needs: [check-paths] | |
| if: needs.check-paths.outputs.azure-changed != 'true' | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 5 | |
| name: Validate Azure ${{ matrix.recipe }} Recipes | |
| strategy: | |
| matrix: | |
| recipe: [bicep, terraform] | |
| permissions: {} | |
| steps: | |
| - name: Skip | |
| run: echo "No Azure files changed — skipping Azure ${{ matrix.recipe }} recipe validation" | |
| # Trust check for pull_request_target events. Determines whether the PR author | |
| # is a trusted contributor (same-repo push) or an external contributor. | |
| # | |
| # Trust is determined by: | |
| # 1. Same-repo PR (head repo == base repo): trusted (only users with write access | |
| # can push branches to the repo). | |
| # 2. Fork PR: external — routed to the approval-gated 'azure' Environment below. | |
| check-trust: | |
| name: Check Trust | |
| needs: [check-paths] | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 5 | |
| if: github.event_name == 'pull_request_target' && needs.check-paths.outputs.azure-changed == 'true' | |
| outputs: | |
| is-external: ${{ steps.check.outputs.is-external }} | |
| permissions: {} | |
| steps: | |
| - name: Determine trust level | |
| id: check | |
| env: | |
| HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }} | |
| BASE_REPO: ${{ github.event.pull_request.base.repo.full_name }} | |
| PR_AUTHOR: ${{ github.event.pull_request.user.login }} | |
| run: | | |
| # Same-repo PRs are always trusted (requires write access to push branches) | |
| if [ "${HEAD_REPO}" = "${BASE_REPO}" ]; then | |
| echo "Same-repo PR from ${PR_AUTHOR} — trusted" | |
| echo "is-external=false" >> "${GITHUB_OUTPUT}" | |
| exit 0 | |
| fi | |
| # Fork PR: treat as external, requires manual approval | |
| echo "Fork PR from ${PR_AUTHOR} — external" | |
| echo "is-external=true" >> "${GITHUB_OUTPUT}" | |
| # Approval gate for external contributors. Uses GitHub Environment protection | |
| # to require manual approval before running tests on PRs from non-members. | |
| # | |
| # NOTE: the 'azure' Environment is deliberately bound to this gate job rather than | |
| # to 'validate-azure-recipes' below. Setting 'environment:' on a job replaces the | |
| # 'ref:' segment of the GitHub OIDC subject claim with 'environment:<name>', which | |
| # no longer matches the Azure federated identity credential, so Azure Login fails | |
| # with "no matching federated identity record found". Moving the gate onto the | |
| # credential-holding job therefore requires new federated identity credentials in | |
| # Azure first. | |
| approval-gate: | |
| name: Approval Gate | |
| needs: [check-trust] | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 5 | |
| if: | | |
| needs.check-trust.outputs.is-external == 'true' | |
| environment: azure | |
| permissions: {} | |
| steps: | |
| - name: Approved | |
| run: | | |
| echo "Approving PR #${{ github.event.pull_request.number }}" | |
| echo "Head repo: ${{ github.event.pull_request.head.repo.full_name }}" | |
| echo "Head SHA: ${{ github.event.pull_request.head.sha }}" | |
| echo "Review THIS EXACT SHA before approving — approval grants it Azure credentials." | |
| validate-azure-recipes: | |
| needs: [check-paths, check-trust, approval-gate] | |
| # Fail-closed for pull_request_target: check-trust must have succeeded, and the | |
| # approval gate must have succeeded unless check-trust proved the PR is same-repo. | |
| # A rejected approval yields 'cancelled'/'failure', neither of which passes. | |
| # For push, workflow_dispatch, and merge_group, both gate jobs are skipped. | |
| if: | | |
| !cancelled() && | |
| needs.check-paths.outputs.azure-changed == 'true' && | |
| ( | |
| (github.event_name == 'pull_request_target' && | |
| needs.check-trust.result == 'success' && | |
| (needs.approval-gate.result == 'success' || | |
| (needs.approval-gate.result == 'skipped' && needs.check-trust.outputs.is-external == 'false'))) || | |
| (github.event_name != 'pull_request_target' && | |
| needs.check-trust.result == 'skipped' && | |
| needs.approval-gate.result == 'skipped') | |
| ) | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 30 | |
| name: Validate Azure ${{ matrix.recipe }} Recipes | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| recipe: [bicep, terraform] | |
| permissions: | |
| id-token: write | |
| contents: read | |
| steps: | |
| - name: Set up checkout target | |
| id: checkout-target | |
| run: | | |
| if [ "${{ github.event_name }}" = "pull_request_target" ]; then | |
| echo "ref=${{ github.event.pull_request.head.sha }}" >> "${GITHUB_OUTPUT}" | |
| else | |
| echo "ref=${{ github.ref }}" >> "${GITHUB_OUTPUT}" | |
| fi | |
| # actions/checkout v7.0.1 refuses to check out fork PR code from a | |
| # pull_request_target workflow unless this opt-in is set (see | |
| # actions/checkout#2454 and https://gh.io/securely-using-pull_request_target). | |
| # | |
| # We opt in deliberately: for fork PRs this job only runs after the | |
| # 'approval-gate' job, which requires a maintainer to approve the 'azure' | |
| # Environment before the exact head SHA below is checked out. | |
| # 'persist-credentials: false' keeps no write-capable git credential on disk, | |
| # and the job runs on an ephemeral GitHub-hosted runner against a throwaway | |
| # resource group. | |
| # | |
| # SECURITY: approval authorizes arbitrary PR-authored code (Makefile targets, | |
| # Bicep, Terraform) to execute with this repository's Azure identity. Approve | |
| # only after reading the diff, including build scripts and Terraform | |
| # provisioners — not just the recipe templates. | |
| - name: Checkout | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ steps.checkout-target.outputs.ref }} | |
| persist-credentials: false | |
| allow-unsafe-pr-checkout: true | |
| - name: Azure Login | |
| uses: azure/login@f5d393ae46f8fde4be8b75f32e3fc50e654ad0ca # v3.0.1 | |
| with: | |
| client-id: ${{ secrets.AZURE_CLIENT_ID }} | |
| tenant-id: ${{ secrets.AZURE_TENANT_ID }} | |
| subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} | |
| - name: Set Azure Test Context | |
| id: set-context | |
| run: | | |
| LOCATION="${AZURE_LOCATION:-$DEFAULT_AZURE_LOCATION}" | |
| if [ -z "$LOCATION" ]; then | |
| LOCATION="westus3" | |
| fi | |
| RG="rrttest-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.recipe }}" | |
| echo "location=$LOCATION" >> "$GITHUB_OUTPUT" | |
| { | |
| echo "AZURE_LOCATION=$LOCATION" | |
| echo "AZURE_RESOURCE_GROUP=$RG" | |
| echo "AZURE_WORKSPACE_NAME=default" | |
| echo "AZURE_ENVIRONMENT_NAME=default" | |
| } >> "$GITHUB_ENV" | |
| env: | |
| DEFAULT_AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} | |
| - name: Create Azure Resource Group | |
| env: | |
| AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} | |
| run: | | |
| set -euo pipefail | |
| RG="$AZURE_RESOURCE_GROUP" | |
| LOCATION="$AZURE_LOCATION" | |
| current_time=$(date +%s) | |
| az group create \ | |
| --only-show-errors \ | |
| --output none \ | |
| --location "$LOCATION" \ | |
| --name "$RG" \ | |
| --subscription "$AZURE_SUBSCRIPTION_ID" \ | |
| --tags "creationTime=$current_time" > /dev/null | |
| # Wait for resource group to be fully available | |
| while [[ "$(az group exists --name "$RG" --subscription "$AZURE_SUBSCRIPTION_ID")" != "true" ]]; do | |
| echo "Waiting for resource group '$RG' to be available..." | |
| sleep 5 | |
| done | |
| echo "Resource group '$RG' is ready" | |
| - name: Setup Node | |
| uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version-file: .node-version | |
| - name: Set up ORAS | |
| uses: oras-project/setup-oras@1d808f7d7f6995cc68b7bf507bfe5c5446e1dc9d # v2.0.1 | |
| with: | |
| version: "1.2.0" | |
| - name: Install Radius CLI | |
| run: make install-radius-cli RAD_VERSION="${{ inputs.version || 'edge' }}" | |
| - name: Create Radius Cluster | |
| env: | |
| AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} | |
| AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} | |
| TEST_AZURE_OIDC_JSON: ${{ secrets.TEST_AZURE_OIDC_JSON }} | |
| run: make create-radius-cluster | |
| - name: Configure Azure Provider | |
| env: | |
| AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} | |
| AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} | |
| AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} | |
| run: make configure-azure-provider | |
| - name: Build Azure Recipes | |
| run: make build-azure-recipes | |
| - name: Generate Azure Recipe Pack | |
| id: generate | |
| run: | | |
| RECIPE_PLATFORM_FILTER=azure make generate-recipe-pack PACK_NAME=azure${{ matrix.recipe }}recipepack OUTPUT_FILE=recipe-pack-azure-${{ matrix.recipe }}.bicep | |
| if [ -f "recipe-pack-azure-${{ matrix.recipe }}.bicep" ]; then | |
| echo "has_recipes=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "has_recipes=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Deploy Azure Recipe Pack | |
| if: steps.generate.outputs.has_recipes == 'true' | |
| run: make deploy-recipe-pack BICEP_FILE=recipe-pack-azure-${{ matrix.recipe }}.bicep RESOURCE_GROUP=default | |
| - name: Update Azure Environment with Recipe Pack | |
| if: steps.generate.outputs.has_recipes == 'true' | |
| run: make update-env-recipe-pack RECIPE_PACK_NAME=azure${{ matrix.recipe }}recipepack ENVIRONMENT="$AZURE_ENVIRONMENT_NAME" | |
| - name: Test Azure ${{ matrix.recipe }} Recipes | |
| if: steps.generate.outputs.has_recipes == 'true' | |
| run: RECIPE_PLATFORM_FILTER=azure make test ENVIRONMENT="$AZURE_ENVIRONMENT_NAME" RECIPE_TYPE=${{ matrix.recipe }} | |
| - name: Collect Radius pod logs | |
| if: always() | |
| run: | | |
| mkdir -p radius-pod-logs | |
| APP_POD=$(kubectl get pods -n radius-system -o json | jq -r '.items[] | select(.metadata.name | startswith("applications-rp-")) | .metadata.name' | head -n1) | |
| if [ -n "$APP_POD" ]; then | |
| kubectl logs "$APP_POD" -n radius-system --all-containers > radius-pod-logs/applications-rp.log || true | |
| else | |
| echo "applications-rp pod not found" > radius-pod-logs/applications-rp.log | |
| fi | |
| DE_POD=$(kubectl get pods -n radius-system -o json | jq -r '.items[] | select(.metadata.name | test("^(deployment-engine|bicep-de)-")) | .metadata.name' | head -n1) | |
| if [ -n "$DE_POD" ]; then | |
| kubectl logs "$DE_POD" -n radius-system --all-containers > radius-pod-logs/deployment-engine.log || true | |
| else | |
| echo "deployment engine pod not found" > radius-pod-logs/deployment-engine.log | |
| fi | |
| UCP_POD=$(kubectl get pods -n radius-system -o json | jq -r '.items[] | select(.metadata.name | startswith("ucp-")) | .metadata.name' | head -n1) | |
| if [ -n "$UCP_POD" ]; then | |
| kubectl logs "$UCP_POD" -n radius-system --all-containers > radius-pod-logs/ucp.log || true | |
| else | |
| echo "ucp pod not found" > radius-pod-logs/ucp.log | |
| fi | |
| DRP_POD=$(kubectl get pods -n radius-system -o json | jq -r '.items[] | select(.metadata.name | startswith("dynamic-rp-")) | .metadata.name' | head -n1) | |
| if [ -n "$DRP_POD" ]; then | |
| kubectl logs "$DRP_POD" -n radius-system --all-containers > radius-pod-logs/dynamic-rp.log || true | |
| else | |
| echo "dynamic-rp pod not found" > radius-pod-logs/dynamic-rp.log | |
| fi | |
| - name: Upload Radius pod logs | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: radius-pod-logs-${{ matrix.recipe }} | |
| path: radius-pod-logs | |
| if-no-files-found: warn | |
| - name: Cleanup Azure Resources | |
| if: always() | |
| run: make cleanup-azure-resources |