Skip to content

Generate secret connection environment variables #648

Generate secret connection environment variables

Generate secret connection environment variables #648

# 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