Skip to content

Add Repo Radius deploy workflow #10653

Add Repo Radius deploy workflow

Add Repo Radius deploy workflow #10653

# ------------------------------------------------------------
# Copyright 2023 The Radius Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ------------------------------------------------------------
# yaml-language-server: $schema=https://www.schemastore.org/github-workflow.json
---
name: Functional Tests (with Cloud Resources)
on:
# Enable manual trigger
workflow_dispatch:
inputs:
branch:
description: Branch to run the workflow on
required: true
default: main
schedule:
# Run three times a day at 6 AM PDT (1 PM UTC), 12 PM PDT (7 PM UTC), and 6 PM PDT (1 AM UTC next day)
- cron: 0 1,13,19 * * *
# Dispatch on external events
repository_dispatch:
types: [deployment-engine.run-functional-tests]
# Expected client_payload:
# src_image: Source image in ACR (e.g. radiusdeploymentengine.azurecr.io/deployment-engine)
# dest_image: Destination image in GHCR (e.g. ghcr.io/radius-project/deployment-engine)
# tag: Tag for the image (e.g. latest, 0.1, 0.1.0-rc1, pr-123)
# 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.
pull_request_target:
branches:
- main
- features/*
- release/*
# Revalidate the combined changes when a PR is queued in the merge queue.
# Scoped to main only so the release branch flow is never affected. In the
# merge queue the code already lives in the base repo (trusted context), so
# the check-trust / approval-gate jobs are skipped and setup proceeds.
merge_group:
branches:
- main
permissions: {}
# Reduce self-contention on the shared Azure ACI 'StandardCores' quota (the
# bottleneck behind Test_ACI's ContainerGroupQuotaReached flakes): serialize cloud
# functional-test runs per pull request so a single PR pushing rapid commits does
# not run several ACI-provisioning runs at once. Keyed by PR number because under
# pull_request_target github.ref is the base ref, which would otherwise collapse
# every PR into one group and cancel unrelated runs. cancel-in-progress is limited
# to pull_request_target so the merge queue and scheduled runs are never cancelled.
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request_target' }}
env:
GOPROXY: https://proxy.golang.org
# yq version
YQ_VERSION: v4.44.3
YQ_LINUX_AMD64_SHA256: a2c097180dd884a8d50c956ee16a9cec070f30a7947cf4ebf87d5f36213e9ed7
# Helm version
HELM_VER: v4.2.2
# KinD cluster version
KIND_VER: v0.29.0
# Kubectl version
KUBECTL_VER: v1.25.0
# Azure Keyvault CSI driver chart version
AZURE_KEYVAULT_CSI_DRIVER_VER: 1.4.2
# Azure workload identity webhook chart version
AZURE_WORKLOAD_IDENTITY_WEBHOOK_VER: 1.3.0
# Container registry for storing container images
CONTAINER_REGISTRY: ${{ vars.FUNCTIONAL_TEST_CONTAINER_REGISTRY }}
# Container registry for storing Bicep recipe artifacts
BICEP_RECIPE_REGISTRY: ${{ vars.FUNCTIONAL_TEST_BICEP_RECIPE_REGISTRY }}
# The radius functional test timeout
FUNCTIONALTEST_TIMEOUT: 60m
# The Azure Location to store test resources
AZURE_LOCATION: ${{ vars.AZURE_LOCATION }}
# The base directory for storing test logs
RADIUS_CONTAINER_LOG_BASE: dist/container_logs
# The Radius helm chart location.
RADIUS_CHART_LOCATION: deploy/Chart/
# The region for AWS resources
AWS_REGION: ${{ vars.AWS_REGION }}
# The current GitHub action link
ACTION_LINK: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
# Server where terraform test modules are deployed
TF_RECIPE_MODULE_SERVER_URL: http://tf-module-server.radius-test-tf-module-server.svc.cluster.local
# Private Git repository where terraform module for testing is stored.
TF_RECIPE_PRIVATE_GIT_SOURCE: git::https://github.com/radius-project/terraform-private-modules//kubernetes-redis
# bicep-types ACR url for pulling latest Radius Bicep types (AWS)
BICEP_TYPES_REGISTRY: biceptypes.azurecr.io
# bicep-types ACR url for uploading test Radius Bicep types
TEST_BICEP_TYPES_REGISTRY: ${{ vars.TEST_BICEP_TYPES_REGISTRY }}
# Kubernetes client QPS and Burst settings for high-concurrency CI environments
RADIUS_QPS_AND_BURST: "800"
jobs:
# Trust check for pull_request_target events. Determines whether the PR author
# is a trusted contributor (org member or 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 + org member: trusted (checked via GitHub API using app token).
# 3. Fork PR + non-member: external (requires approval).
#
# NOTE: We do NOT rely on github.event.pull_request.author_association because
# webhook payloads report incorrect values for org members with private
# membership visibility (returns CONTRIBUTOR instead of MEMBER).
check-trust:
name: Check Trust
runs-on: ubuntu-24.04
timeout-minutes: 5
if: github.event_name == 'pull_request_target'
outputs:
is-external: ${{ steps.check.outputs.is-external }}
permissions: {}
steps:
- name: Generate App Token
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ secrets.FUNCTIONAL_TEST_CLIENT_ID }}
private-key: ${{ secrets.FUNCTIONAL_TEST_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: radius
permission-members: read
- name: Determine trust level
id: check
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
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 }}
ORG: ${{ github.repository_owner }}
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: check if the author is an org member via GitHub API.
# Uses app token which can read org membership regardless of visibility settings.
# gh api returns exit code 0 for 204 (member) and non-zero for 404/302 (not a member).
if gh api "orgs/${ORG}/members/${PR_AUTHOR}" --silent 2>/dev/null; then
echo "Fork PR from org member ${PR_AUTHOR} — trusted"
echo "is-external=false" >> "${GITHUB_OUTPUT}"
else
echo "Fork PR from ${PR_AUTHOR} — external"
echo "is-external=true" >> "${GITHUB_OUTPUT}"
fi
# Approval gate for external contributors. Uses GitHub Environment protection
# to require manual approval before running tests on PRs from non-members.
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: external-contributor-approval
permissions: {}
steps:
- name: Approved
run: echo "Tests approved to run"
setup:
name: Setup
needs: [check-trust, approval-gate]
# Run for all events. For PRs:
# - check-trust determines if the author is external
# - approval-gate runs only for external contributors and requires manual approval
# - If check-trust or approval-gate are skipped (non-PR events), setup proceeds
# For pull_request_target, require approval-gate to be 'success' or 'skipped' — block
# on 'cancelled' (rejected approval) to prevent running PR code with secrets.
if: |
!cancelled() &&
(needs.check-trust.result == 'success' || needs.check-trust.result == 'skipped') &&
(needs.approval-gate.result == 'success' || needs.approval-gate.result == 'skipped') &&
(github.event_name != 'schedule' || github.repository == vars.RADIUS_REPOSITORY)
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
contents: read # Required for listing the commits
pull-requests: read # Required for fetching PR details
env:
DE_IMAGE: ghcr.io/radius-project/deployment-engine
DE_TAG: latest
outputs:
REL_VERSION: ${{ steps.gen-id.outputs.REL_VERSION }}
UNIQUE_ID: ${{ steps.gen-id.outputs.UNIQUE_ID }}
PR_NUMBER: ${{ steps.gen-id.outputs.PR_NUMBER }}
CHECKOUT_REPO: ${{ steps.gen-id.outputs.CHECKOUT_REPO }}
CHECKOUT_REF: ${{ steps.gen-id.outputs.CHECKOUT_REF }}
RAD_CLI_ARTIFACT_NAME: ${{ steps.gen-id.outputs.RAD_CLI_ARTIFACT_NAME }}
DE_IMAGE: ${{ steps.gen-id.outputs.DE_IMAGE }}
DE_TAG: ${{ steps.gen-id.outputs.DE_TAG }}
BASE_SHA: ${{ steps.gen-id.outputs.BASE_SHA }}
steps:
- name: Log Event Information
env:
PR_AUTHOR_ASSOCIATION: ${{ github.event.pull_request.author_association }}
PR_HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
PR_BASE_REPO: ${{ github.event.pull_request.base.repo.full_name }}
IS_FORK: ${{ github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name }}
run: |
echo "Event Name: ${GITHUB_EVENT_NAME}"
echo "Actor: ${GITHUB_ACTOR}"
echo "Author Association: ${PR_AUTHOR_ASSOCIATION}"
echo "PR Head Repo: ${PR_HEAD_REPO}"
echo "PR Base Repo: ${PR_BASE_REPO}"
echo "Is Fork: ${IS_FORK}"
- name: Set up checkout target (scheduled)
if: github.event_name == 'schedule'
run: |
echo "CHECKOUT_REPO=${GITHUB_REPOSITORY}" >> $GITHUB_ENV
echo "CHECKOUT_REF=refs/heads/main" >> $GITHUB_ENV
- name: Set up checkout target (repository_dispatch)
if: github.event_name == 'repository_dispatch'
run: |
echo "CHECKOUT_REPO=${GITHUB_REPOSITORY}" >> $GITHUB_ENV
echo "CHECKOUT_REF=refs/heads/main" >> $GITHUB_ENV
- name: Set up checkout target (pull_request_target)
if: github.event_name == 'pull_request_target'
env:
PR_HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
{
echo "CHECKOUT_REPO=${PR_HEAD_REPO}"
echo "CHECKOUT_REF=${PR_HEAD_SHA}"
echo "PR_NUMBER=${PR_NUMBER}"
echo "BASE_SHA=${PR_BASE_SHA}"
} >> "${GITHUB_ENV}"
- name: Set up checkout target (workflow_dispatch)
if: github.event_name == 'workflow_dispatch'
env:
INPUT_BRANCH: ${{ github.event.inputs.branch }}
run: |
{
echo "CHECKOUT_REPO=${GITHUB_REPOSITORY}"
echo "CHECKOUT_REF=refs/heads/${INPUT_BRANCH}"
} >> "${GITHUB_ENV}"
- name: Set up checkout target (merge_group)
if: github.event_name == 'merge_group'
run: |
# GITHUB_SHA is the merge queue's combined commit (base + queued PRs).
# Using it as the checkout ref and the status-check sha ensures the
# required check is reported on the commit the merge queue validates.
{
echo "CHECKOUT_REPO=${GITHUB_REPOSITORY}"
echo "CHECKOUT_REF=${GITHUB_SHA}"
} >> "${GITHUB_ENV}"
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Set DE image and tag (repository_dispatch from deployment-engine.run-functional-tests)
if: github.event_name == 'repository_dispatch'
shell: bash
env:
DEST_IMAGE: ${{ github.event.client_payload.dest_image }}
TAG: ${{ github.event.client_payload.tag }}
run: |
{
echo "DE_IMAGE=${DEST_IMAGE}"
echo "DE_TAG=${TAG}"
} >> "$GITHUB_ENV"
- name: Generate ID for release
id: gen-id
run: |
BASE_STR="RADIUS|${GITHUB_SHA}|${GITHUB_SERVER_URL}|${GITHUB_REPOSITORY}|${GITHUB_RUN_ID}|${GITHUB_RUN_ATTEMPT}"
if [ "$GITHUB_EVENT_NAME" == "schedule" ]; then
# Add run number to randomize unique id for scheduled runs.
BASE_STR="${GITHUB_RUN_NUMBER}|${BASE_STR}"
fi
UNIQUE_ID=func$(echo $BASE_STR | sha1sum | head -c 10)
echo "REL_VERSION=pr-${UNIQUE_ID}" >> $GITHUB_ENV
# Set output variables to be used in the other jobs
{
echo "REL_VERSION=pr-${UNIQUE_ID}"
echo "UNIQUE_ID=${UNIQUE_ID}"
echo "CHECKOUT_REPO=${CHECKOUT_REPO}"
echo "CHECKOUT_REF=${CHECKOUT_REF}"
echo "RAD_CLI_ARTIFACT_NAME=rad_cli_linux_amd64"
echo "PR_NUMBER=${PR_NUMBER}"
echo "DE_IMAGE=${DE_IMAGE}"
echo "DE_TAG=${DE_TAG}"
echo "BASE_SHA=${BASE_SHA}"
} >> "${GITHUB_OUTPUT}"
changes:
name: Changes
needs: setup
if: always() && needs.setup.result == 'success'
uses: ./.github/workflows/__changes.yml
with:
ref: ${{ needs.setup.outputs.CHECKOUT_REF }}
repository: ${{ needs.setup.outputs.CHECKOUT_REPO }}
base_sha: ${{ needs.setup.outputs.BASE_SHA }}
permissions:
contents: read
pull-requests: read
build:
name: Build Radius for test
needs: [setup, changes]
# Skip if only docs/markdown changed
if: always() && needs.changes.outputs.only_changed != 'true'
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
contents: read # Required for listing the commits
packages: write # Required for uploading the package
pull-requests: write # Required for updating pull requests
id-token: write # Required for azure/login
env:
REL_VERSION: ${{ needs.setup.outputs.REL_VERSION }}
UNIQUE_ID: ${{ needs.setup.outputs.UNIQUE_ID }}
PR_NUMBER: ${{ needs.setup.outputs.PR_NUMBER }}
CHECKOUT_REPO: ${{ needs.setup.outputs.CHECKOUT_REPO }}
CHECKOUT_REF: ${{ needs.setup.outputs.CHECKOUT_REF }}
RAD_CLI_ARTIFACT_NAME: ${{ needs.setup.outputs.RAD_CLI_ARTIFACT_NAME }}
DE_IMAGE: ${{ needs.setup.outputs.DE_IMAGE }}
DE_TAG: ${{ needs.setup.outputs.DE_TAG }}
DAPR_VER: 1.14.4
ACTION_LINK: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
steps:
- name: Get GitHub app token
if: github.repository == vars.RADIUS_REPOSITORY
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
id: get_installation_token
with:
client-id: ${{ secrets.FUNCTIONAL_TEST_CLIENT_ID }}
private-key: ${{ secrets.FUNCTIONAL_TEST_APP_PRIVATE_KEY }}
permission-pull-requests: write
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
repository: ${{ env.CHECKOUT_REPO }}
ref: ${{ env.CHECKOUT_REF }}
persist-credentials: false
- name: Setup Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
cache-dependency-path: go.sum
cache: true
- uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4
if: env.PR_NUMBER != ''
continue-on-error: true
with:
GITHUB_TOKEN: ${{ steps.get_installation_token.outputs.token }}
header: teststatus
number: ${{ env.PR_NUMBER }}
recreate: true
message: |
## Radius functional test overview
:mag: **[Go to test action run](${{ env.ACTION_LINK }})**
<details>
<summary> Click here to see the test run details</summary>
| Name | Value |
|------|-------|
|**Repository** | ${{ env.CHECKOUT_REPO }} |
|**Commit ref** | ${{ env.CHECKOUT_REF }} |
|**Unique ID** | ${{ env.UNIQUE_ID }} |
|**Image tag** | ${{ env.REL_VERSION }} |
* KinD: ${{ env.KIND_VER }}
* Dapr: ${{ env.DAPR_VER }}
* Azure KeyVault CSI driver: ${{ env.AZURE_KEYVAULT_CSI_DRIVER_VER }}
* Azure Workload identity webhook: ${{ env.AZURE_WORKLOAD_IDENTITY_WEBHOOK_VER }}
* Bicep recipe location `${{ env.BICEP_RECIPE_REGISTRY }}/test/testrecipes/test-bicep-recipes/<name>:${{ env.REL_VERSION }}`
* Terraform recipe location `${{ env.TF_RECIPE_MODULE_SERVER_URL }}/<name>.zip` (in cluster)
* applications-rp test image location: `${{ env.CONTAINER_REGISTRY }}/applications-rp:${{ env.REL_VERSION }}`
* dynamic-rp test image location: `${{ env.CONTAINER_REGISTRY }}/dynamic-rp:${{ env.REL_VERSION }}`
* controller test image location: `${{ env.CONTAINER_REGISTRY }}/controller:${{ env.REL_VERSION }}`
* ucp test image location: `${{ env.CONTAINER_REGISTRY }}/ucpd:${{ env.REL_VERSION }}`
* deployment-engine test image location: `${{ env.DE_IMAGE }}:${{ env.DE_TAG }}`
</details>
## Test Status
- name: Login to GitHub Container Registry
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4
if: env.PR_NUMBER != ''
continue-on-error: true
with:
GITHUB_TOKEN: ${{ steps.get_installation_token.outputs.token }}
header: teststatus
number: ${{ env.PR_NUMBER }}
append: true
message: |
:hourglass: Building Radius and pushing container images for functional tests...
- name: Build and Push container images
run: |
make build && make docker-build && make docker-push
env:
DOCKER_REGISTRY: ${{ env.CONTAINER_REGISTRY }}
DOCKER_TAG_VERSION: ${{ env.REL_VERSION }}
- name: Upload CLI binary
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ env.RAD_CLI_ARTIFACT_NAME }}
path: |
./dist/linux_amd64/release/rad
- uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4
if: success() && env.PR_NUMBER != ''
continue-on-error: true
with:
GITHUB_TOKEN: ${{ steps.get_installation_token.outputs.token }}
header: teststatus
number: ${{ env.PR_NUMBER }}
append: true
message: |
:white_check_mark: Container images build succeeded
- uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4
if: failure() && env.PR_NUMBER != ''
continue-on-error: true
with:
GITHUB_TOKEN: ${{ steps.get_installation_token.outputs.token }}
header: teststatus
number: ${{ env.PR_NUMBER }}
append: true
message: |
:x: Container images build failed
- uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4
if: env.PR_NUMBER != ''
continue-on-error: true
with:
GITHUB_TOKEN: ${{ steps.get_installation_token.outputs.token }}
header: teststatus
number: ${{ env.PR_NUMBER }}
append: true
message: |
:hourglass: Publishing Bicep Recipes for functional tests...
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: .node-version
- name: Install yq
# Required by make generate-bicep-types-contrib to parse defaults.yaml.
run: |
mkdir -p "${RUNNER_TEMP}/bin"
curl -fsSL "https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/yq_linux_amd64" -o "${RUNNER_TEMP}/bin/yq"
echo "${YQ_LINUX_AMD64_SHA256} ${RUNNER_TEMP}/bin/yq" | sha256sum -c -
chmod +x "${RUNNER_TEMP}/bin/yq"
echo "${RUNNER_TEMP}/bin" >> "${GITHUB_PATH}"
- name: Generate Bicep extensibility types from OpenAPI specs
env:
BICEP_TYPES_VERSION: ${{ env.REL_VERSION == 'edge' && 'latest' || env.REL_VERSION }}
run: |
make generate-bicep-types VERSION="${BICEP_TYPES_VERSION}"
- name: Setup and verify bicep CLI
run: |
curl -Lo bicep https://github.com/Azure/bicep/releases/latest/download/bicep-linux-x64
chmod +x ./bicep
sudo mv ./bicep /usr/local/bin/bicep
bicep --version
- name: Login to Azure (for private test bicep-types ACR)
uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0
with:
client-id: ${{ secrets.AZURE_SP_TESTS_APPID }}
tenant-id: ${{ secrets.AZURE_SP_TESTS_TENANTID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTIONID_TESTS }}
- name: Publish Radius test bicep types
env:
BICEP_TYPES_VERSION: ${{ env.REL_VERSION == 'edge' && 'latest' || env.REL_VERSION }}
run: |
bicep publish-extension ./hack/bicep-types-radius/generated/index.json --target "br:${TEST_BICEP_TYPES_REGISTRY}/test/radius:${BICEP_TYPES_VERSION}" --force
- name: Generate test bicepconfig.json
run: |
if [[ "${REL_VERSION}" == "edge" ]]; then
RADIUS_VERSION="latest"
else
RADIUS_VERSION="${REL_VERSION}"
fi
cat <<EOF > ./test/bicepconfig.json
{
"extensions": {
"radius": "br:${TEST_BICEP_TYPES_REGISTRY}/test/radius:$RADIUS_VERSION",
"aws": "br:${BICEP_TYPES_REGISTRY}/aws:latest"
}
}
EOF
- name: Publish Bicep Test Recipes
run: |
mkdir ./bin
cp ./dist/linux_amd64/release/rad ./bin/rad
chmod +x ./bin/rad
export PATH=$GITHUB_WORKSPACE/bin:$PATH
which rad || { echo "cannot find rad"; exit 1; }
rad bicep download
rad version
make publish-test-bicep-recipes
env:
BICEP_RECIPE_REGISTRY: ${{ env.BICEP_RECIPE_REGISTRY }}
BICEP_RECIPE_TAG_VERSION: ${{ env.REL_VERSION }}
- uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4
if: success() && env.PR_NUMBER != ''
continue-on-error: true
with:
GITHUB_TOKEN: ${{ steps.get_installation_token.outputs.token }}
header: teststatus
number: ${{ env.PR_NUMBER }}
append: true
message: |
:white_check_mark: Recipe publishing succeeded
- uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4
if: failure() && env.PR_NUMBER != ''
continue-on-error: true
with:
GITHUB_TOKEN: ${{ steps.get_installation_token.outputs.token }}
header: teststatus
number: ${{ env.PR_NUMBER }}
append: true
message: |
:x: Test recipe publishing failed
skip-tests:
name: Skip Functional Tests
needs: [setup, changes]
if: always() && needs.changes.outputs.only_changed == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
contents: read # Required for listing the commits
steps:
- name: Get GitHub app token
if: github.repository == vars.RADIUS_REPOSITORY
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
id: get_installation_token
with:
client-id: ${{ secrets.FUNCTIONAL_TEST_CLIENT_ID }}
private-key: ${{ secrets.FUNCTIONAL_TEST_APP_PRIVATE_KEY }}
permission-checks: write
- uses: LouisBrunner/checks-action@937cbbcde3259005b50746dc91cde29098aac2ff # v3.1.0
with:
token: ${{ steps.get_installation_token.outputs.token }}
name: Functional Test Run
status: completed
conclusion: success
repo: ${{ github.repository }}
sha: ${{ needs.setup.outputs.CHECKOUT_REF }}
output: |
{"summary":"Skipped functional tests"}
tests:
name: Run ${{ matrix.name }} functional tests
needs: [setup, build]
if: always() && needs.setup.result == 'success' && needs.build.result == 'success'
# Approval gate (via environment protection) ensures external contributors are approved before reaching here
strategy:
# Keep matrix legs independent. With fail-fast: true, a single failing
# (often flaky) leg cancels all in-progress siblings. GitHub's
# "Re-run failed jobs" reliably re-runs only failure-concluded jobs, so
# cancelled siblings can stay stale on the PR and the checks never fully
# update to green even after a successful re-run.
fail-fast: false
matrix:
os: [ubuntu-24.04]
name: [corerp-cloud, ucp-cloud]
runs-on: ${{ matrix.os }}
timeout-minutes: 60
permissions:
id-token: write # Required for requesting the JWT
contents: read # Required for listing the commits
checks: write # Required for publishing test results
packages: read # Required for pulling images from ghcr.io inside KinD
pull-requests: write # Required for posting a PR comment
env:
UNIQUE_ID: ${{ needs.setup.outputs.UNIQUE_ID }}
REL_VERSION: ${{ needs.setup.outputs.REL_VERSION }}
CHECKOUT_REPO: ${{ needs.setup.outputs.CHECKOUT_REPO }}
CHECKOUT_REF: ${{ needs.setup.outputs.CHECKOUT_REF }}
PR_NUMBER: ${{ needs.setup.outputs.PR_NUMBER }}
# Include run_attempt so each re-run uses a distinct resource group.
# UNIQUE_ID is generated in the separate `setup` job, which is NOT
# re-executed by "Re-run failed jobs", so it stays constant across
# attempts. The delete step uses `az group delete --no-wait`, so a
# re-run would otherwise try to recreate the same resource group while
# the previous attempt's deletion is still in progress, failing with
# "ResourceGroupBeingDeleted". The radtest- prefix is preserved so the
# purge workflow still cleans up any orphaned groups.
AZURE_TEST_RESOURCE_GROUP: radtest-${{ needs.setup.outputs.UNIQUE_ID }}-${{ matrix.name }}-${{ github.run_attempt }}
RAD_CLI_ARTIFACT_NAME: ${{ needs.setup.outputs.RAD_CLI_ARTIFACT_NAME }}
BICEP_RECIPE_TAG_VERSION: ${{ needs.setup.outputs.REL_VERSION }}
DE_IMAGE: ${{ needs.setup.outputs.DE_IMAGE }}
DE_TAG: ${{ needs.setup.outputs.DE_TAG }}
steps:
- name: Get GitHub app token
if: github.repository == vars.RADIUS_REPOSITORY
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
id: get_installation_token
with:
client-id: ${{ secrets.FUNCTIONAL_TEST_CLIENT_ID }}
private-key: ${{ secrets.FUNCTIONAL_TEST_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: |
radius
terraform-private-modules
permission-checks: write
permission-pull-requests: write
permission-contents: read
- uses: LouisBrunner/checks-action@937cbbcde3259005b50746dc91cde29098aac2ff # v3.1.0
if: always()
with:
token: ${{ steps.get_installation_token.outputs.token }}
name: Functional Test Run
status: in_progress
repo: ${{ github.repository }}
sha: ${{ env.CHECKOUT_REF }}
details_url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
- name: Checkout Radius repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
repository: ${{ env.CHECKOUT_REPO }}
ref: ${{ env.CHECKOUT_REF }}
persist-credentials: false
- name: Checkout Samples repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
if: matrix.name == 'samples'
with:
repository: radius-project/samples
ref: refs/heads/edge
path: samples
persist-credentials: false
- name: Setup Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
cache-dependency-path: go.sum
cache: true
- name: Download rad CLI
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ${{ env.RAD_CLI_ARTIFACT_NAME }}
path: ${{ runner.temp }}/rad-cli-artifact
- name: Copy rad CLI to bin
run: |
mkdir -p ./bin
cp "${RUNNER_TEMP}/rad-cli-artifact/rad" ./bin/rad
chmod +x ./bin/rad
# Verify it is the expected binary
file ./bin/rad | grep -q "ELF" || { echo "Downloaded rad binary is not a valid ELF executable"; exit 1; }
# Verify it is the expected rad CLI binary by running a harmless command
./bin/rad version >/dev/null 2>&1 || { echo "Downloaded rad binary is not a working rad CLI executable"; exit 1; }
- name: Login to Azure
uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0
with:
client-id: ${{ secrets.AZURE_SP_TESTS_APPID }}
tenant-id: ${{ secrets.AZURE_SP_TESTS_TENANTID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTIONID_TESTS }}
- uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4
continue-on-error: true
with:
GITHUB_TOKEN: ${{ steps.get_installation_token.outputs.token }}
header: teststatus
number: ${{ env.PR_NUMBER }}
append: true
message: |
:hourglass: Starting ${{ matrix.name }} functional tests...
- name: Create azure resource group - ${{ env.AZURE_TEST_RESOURCE_GROUP }}
run: |
current_time=$(date +%s)
az group create \
--location "${AZURE_LOCATION}" \
--name "${RESOURCE_GROUP}" \
--subscription "${AZURE_SUBSCRIPTIONID_TESTS}" \
--tags creationTime=$current_time
while [ $(az group exists --name "${RESOURCE_GROUP}") = false ]; do sleep 2; done
env:
RESOURCE_GROUP: ${{ env.AZURE_TEST_RESOURCE_GROUP }}
AZURE_SUBSCRIPTIONID_TESTS: ${{ secrets.AZURE_SUBSCRIPTIONID_TESTS }}
- uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5.0.0
with:
version: ${{ env.HELM_VER }}
# The role-to-assume is the role that the github action will assume to execute aws commands and
# construct cloud control client in test code.
- name: configure aws credentials using assumed role
uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0
with:
role-to-assume: ${{ secrets.AWS_GH_ACTIONS_ROLE }}
role-session-name: GitHub_to_AWS_via_FederatedOIDC
aws-region: ${{ env.AWS_REGION }}
# create kind cluster with OIDC provider.
- name: Create KinD cluster
env:
FUNCTEST_AZURE_OIDC_JSON: ${{ secrets.FUNCTEST_AZURE_OIDC_JSON }}
GITHUB_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
curl -sSLo "kind" "https://github.com/kubernetes-sigs/kind/releases/download/${KIND_VER}/kind-linux-amd64"
chmod +x ./kind
# Parse and validate Azure workload identity values from secret JSON.
OIDC_JSON="${FUNCTEST_AZURE_OIDC_JSON}"
if ! echo "${OIDC_JSON}" | jq -e . >/dev/null; then
echo "FUNCTEST_AZURE_OIDC_JSON is not valid JSON."
echo "Expected keys: AZURE_OIDC_ISSUER, AZURE_OIDC_ISSUER_PUBLIC_KEY, AZURE_OIDC_ISSUER_PRIVATE_KEY"
exit 1
fi
AZURE_OIDC_ISSUER="$(echo "${OIDC_JSON}" | jq -er '.AZURE_OIDC_ISSUER')"
AZURE_OIDC_ISSUER_PUBLIC_KEY="$(echo "${OIDC_JSON}" | jq -er '.AZURE_OIDC_ISSUER_PUBLIC_KEY')"
AZURE_OIDC_ISSUER_PRIVATE_KEY="$(echo "${OIDC_JSON}" | jq -er '.AZURE_OIDC_ISSUER_PRIVATE_KEY')"
AUTHKEY=$(printf '%s' "${GITHUB_ACTOR}:${GITHUB_TOKEN}" | base64)
echo "{\"auths\":{\"ghcr.io\":{\"auth\":\"${AUTHKEY}\"}}}" > "./ghcr_secret.json"
# Create KinD cluster with OIDC Issuer keys
printf '%s' "${AZURE_OIDC_ISSUER_PUBLIC_KEY}" | base64 -d > sa.pub
printf '%s' "${AZURE_OIDC_ISSUER_PRIVATE_KEY}" | base64 -d > sa.key
openssl pkey -pubin -in sa.pub -noout >/dev/null
openssl pkey -in sa.key -check -noout >/dev/null
cat <<EOF | ./kind create cluster --name radius --config=-
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
extraMounts:
- hostPath: ./sa.pub
containerPath: /etc/kubernetes/pki/sa.pub
- hostPath: ./sa.key
containerPath: /etc/kubernetes/pki/sa.key
- hostPath: ./ghcr_secret.json
containerPath: /var/lib/kubelet/config.json
kubeadmConfigPatches:
- |
kind: ClusterConfiguration
apiServer:
extraArgs:
service-account-issuer: $AZURE_OIDC_ISSUER
service-account-key-file: /etc/kubernetes/pki/sa.pub
service-account-signing-key-file: /etc/kubernetes/pki/sa.key
controllerManager:
extraArgs:
service-account-private-key-file: /etc/kubernetes/pki/sa.key
EOF
- name: Install Azure Keyvault CSI driver chart
run: |
helm repo add csi-secrets-store-provider-azure https://azure.github.io/secrets-store-csi-driver-provider-azure/charts
helm install csi csi-secrets-store-provider-azure/csi-secrets-store-provider-azure --version "${AZURE_KEYVAULT_CSI_DRIVER_VER}"
- name: Install azure workload identity webhook chart
env:
AZURE_SP_TESTS_TENANTID: ${{ secrets.AZURE_SP_TESTS_TENANTID }}
run: |
helm repo add azure-workload-identity https://azure.github.io/azure-workload-identity/charts
helm install workload-identity-webhook azure-workload-identity/workload-identity-webhook --namespace radius-default --create-namespace --version "${AZURE_WORKLOAD_IDENTITY_WEBHOOK_VER}" --set "azureTenantID=${AZURE_SP_TESTS_TENANTID}"
- name: Login to GitHub Container Registry
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Download Bicep
run: |
export PATH=$GITHUB_WORKSPACE/bin:$PATH
which rad || { echo "cannot find rad"; exit 1; }
rad bicep download
rad version
- uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4
if: failure() && env.PR_NUMBER != ''
continue-on-error: true
with:
GITHUB_TOKEN: ${{ steps.get_installation_token.outputs.token }}
header: teststatus
number: ${{ env.PR_NUMBER }}
append: true
message: |
:x: Test tool installation for ${{ matrix.name }} failed. Please check [the logs](${{ env.ACTION_LINK }}) for more details
- name: Install Radius
env:
AZURE_SUBSCRIPTIONID_TESTS: ${{ secrets.AZURE_SUBSCRIPTIONID_TESTS }}
AZURE_SP_TESTS_APPID: ${{ secrets.AZURE_SP_TESTS_APPID }}
AZURE_SP_TESTS_TENANTID: ${{ secrets.AZURE_SP_TESTS_TENANTID }}
FUNCTEST_AWS_ACCOUNT_ID: ${{ secrets.FUNCTEST_AWS_ACCOUNT_ID }}
FUNC_TEST_RAD_IRSA_ROLE: ${{ secrets.FUNC_TEST_RAD_IRSA_ROLE }}
run: |
export PATH=$GITHUB_WORKSPACE/bin:$PATH
which rad || { echo "cannot find rad"; exit 1; }
echo "*** Installing Radius to Kubernetes ***"
rad install kubernetes \
--chart "${RADIUS_CHART_LOCATION}" \
--set "rp.image=${CONTAINER_REGISTRY}/applications-rp,rp.tag=${REL_VERSION}" \
--set "dynamicrp.image=${CONTAINER_REGISTRY}/dynamic-rp,dynamicrp.tag=${REL_VERSION}" \
--set "controller.image=${CONTAINER_REGISTRY}/controller,controller.tag=${REL_VERSION}" \
--set "ucp.image=${CONTAINER_REGISTRY}/ucpd,ucp.tag=${REL_VERSION}" \
--set "de.image=${DE_IMAGE},de.tag=${DE_TAG}" \
--set "bicep.image=${CONTAINER_REGISTRY}/bicep,bicep.tag=${REL_VERSION}" \
--set global.azureWorkloadIdentity.enabled=true \
--set global.aws.irsa.enabled=true
echo "*** Verify manifests are registered ***"
rm -f registermanifest_logs.txt
# Find the pod with container "ucp"
POD_NAME=$(
kubectl get pods -n radius-system \
-o jsonpath='{range .items[*]}{.metadata.name}{" "}{.spec.containers[*].name}{"\n"}{end}' \
| grep "ucp" \
| head -n1 \
| cut -d" " -f1
)
echo "Found ucp pod: $POD_NAME"
if [ -z "$POD_NAME" ]; then
echo "No pod with container 'ucp' found in namespace radius-system."
exit 1
fi
# Poll logs for up to iterations, 30 seconds each (upto 3 minutes total)
for i in {1..6}; do
kubectl logs "$POD_NAME" -n radius-system | tee registermanifest_logs.txt > /dev/null
# Exit on error
if grep -qi "Service initializer terminated with error" registermanifest_logs.txt; then
echo "Error found in ucp logs:"
grep -i "Service initializer terminated with error" registermanifest_logs.txt
exit 1
fi
# Check for success
if grep -q "Successfully registered manifests" registermanifest_logs.txt; then
echo "Successfully registered manifests - message found."
break
fi
echo "Logs not ready, waiting 30 seconds..."
sleep 30
done
# Final check to ensure success message was found
if ! grep -q "Successfully registered manifests" registermanifest_logs.txt; then
echo "Manifests not registered after 3 minutes."
exit 1
fi
echo "*** Create workspace, group and environment for test ***"
rad workspace create kubernetes
rad group create kind-radius
rad group switch kind-radius
# The functional test is designed to use default namespace. So you must create the environment for default namespace.
rad env create kind-radius --namespace default
rad env switch kind-radius
echo "*** Configuring Azure provider ***"
rad env update kind-radius --azure-subscription-id "${AZURE_SUBSCRIPTIONID_TESTS}" \
--azure-resource-group "${AZURE_TEST_RESOURCE_GROUP}"
rad credential register azure wi \
--client-id "${AZURE_SP_TESTS_APPID}" \
--tenant-id "${AZURE_SP_TESTS_TENANTID}"
echo "*** Configuring AWS provider ***"
rad env update kind-radius --aws-region "${AWS_REGION}" --aws-account-id "${FUNCTEST_AWS_ACCOUNT_ID}"
rad credential register aws irsa \
--iam-role "${FUNC_TEST_RAD_IRSA_ROLE}"
- uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4
if: failure() && env.PR_NUMBER != ''
continue-on-error: true
with:
GITHUB_TOKEN: ${{ steps.get_installation_token.outputs.token }}
header: teststatus
number: ${{ env.PR_NUMBER }}
append: true
message: |
:x: Failed to install Radius for ${{ matrix.name }} functional test. Please check [the logs](${{ env.ACTION_LINK }}) for more details
- name: Publish Terraform test recipes
run: |
make publish-test-terraform-recipes
- name: Generate test bicepconfig.json
run: |
if [[ "${REL_VERSION}" == "edge" ]]; then
RADIUS_VERSION="latest"
else
RADIUS_VERSION="${REL_VERSION}"
fi
cat <<EOF > ./test/bicepconfig.json
{
"extensions": {
"radius": "br:${TEST_BICEP_TYPES_REGISTRY}/test/radius:$RADIUS_VERSION",
"aws": "br:${BICEP_TYPES_REGISTRY}/aws:latest"
},
"cloud": {
"credentialPrecedence": ["AzureCLI", "Environment"]
}
}
EOF
- name: Restore Bicep artifacts before running functional tests
# The exact files chosen to run the command can be changed, but we need 1 that uses the Radius extension and 1 that uses the AWS extension so we can restore both Bicep artifacts before the tests start
run: |
# Restore Radius Bicep types
bicep restore ./test/functional-portable/corerp/cloud/resources/testdata/corerp-azure-connection-database-service.bicep --force
# Restore AWS Bicep types
bicep restore ./test/functional-portable/corerp/cloud/resources/testdata/aws-logs-loggroup.bicep --force
- name: Run functional tests
run: |
set -euo pipefail
# Ensure rad cli is in path before running tests.
export PATH=$GITHUB_WORKSPACE/bin:$PATH
# Make directory to capture functional test results
mkdir -p ./dist/functional_test
cd $GITHUB_WORKSPACE
which rad || { echo "cannot find rad"; exit 1; }
# Populate the following test environment variables from JSON secret.
# AZURE_COSMOS_MONGODB_ACCOUNT_ID
# AZURE_MSSQL_RESOURCE_ID
# AZURE_MSSQL_USERNAME
# AZURE_MSSQL_PASSWORD
PREPROVISIONED_JSON="${FUNCTEST_PREPROVISIONED_RESOURCE_JSON}"
if ! echo "${PREPROVISIONED_JSON}" | jq -e . >/dev/null; then
echo "FUNCTEST_PREPROVISIONED_RESOURCE_JSON is not valid JSON."
echo "Expected keys: AZURE_COSMOS_MONGODB_ACCOUNT_ID, AZURE_MSSQL_RESOURCE_ID, AZURE_MSSQL_USERNAME, AZURE_MSSQL_PASSWORD"
exit 1
fi
export AZURE_COSMOS_MONGODB_ACCOUNT_ID="$(echo "${PREPROVISIONED_JSON}" | jq -er '.AZURE_COSMOS_MONGODB_ACCOUNT_ID')"
export AZURE_MSSQL_RESOURCE_ID="$(echo "${PREPROVISIONED_JSON}" | jq -er '.AZURE_MSSQL_RESOURCE_ID')"
export AZURE_MSSQL_USERNAME="$(echo "${PREPROVISIONED_JSON}" | jq -er '.AZURE_MSSQL_USERNAME')"
export AZURE_MSSQL_PASSWORD="$(echo "${PREPROVISIONED_JSON}" | jq -er '.AZURE_MSSQL_PASSWORD')"
make "test-functional-${MATRIX_NAME}"
env:
DOCKER_REGISTRY: ${{ env.CONTAINER_REGISTRY }}
TEST_TIMEOUT: ${{ env.FUNCTIONALTEST_TIMEOUT }}
RADIUS_CONTAINER_LOG_PATH: ${{ github.workspace }}/${{ env.RADIUS_CONTAINER_LOG_BASE }}
AWS_REGION: ${{ env.AWS_REGION }}
AWS_ACCOUNT_ID: ${{ secrets.FUNCTEST_AWS_ACCOUNT_ID }}
RADIUS_SAMPLES_REPO_ROOT: ${{ github.workspace }}/samples
# Test_MongoDB_Recipe_Parameters is using the following environment variable.
INTEGRATION_TEST_RESOURCE_GROUP_NAME: ${{ env.AZURE_TEST_RESOURCE_GROUP }}
# Enable Azure resource location verification in tests
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTIONID_TESTS }}
BICEP_RECIPE_REGISTRY: ${{ env.BICEP_RECIPE_REGISTRY }}
BICEP_RECIPE_TAG_VERSION: ${{ env.BICEP_RECIPE_TAG_VERSION }}
GH_TOKEN: ${{ steps.get_installation_token.outputs.token }}
GOTESTSUM_OPTS: --junitfile ./dist/functional_test/results.xml
# Initiate resource deletion without blocking on completion (matches the
# non-cloud suite). Test assertions for deploy/create still run in full;
# only the synchronous teardown wait is skipped, and the per-run resource
# group is force-deleted afterward. This removes the slow Test_ACI teardown
# (~9 min) from the critical path without reducing validation scope.
RADIUS_TEST_FAST_CLEANUP: true
MATRIX_NAME: ${{ matrix.name }}
FUNCTEST_PREPROVISIONED_RESOURCE_JSON: ${{ secrets.FUNCTEST_PREPROVISIONED_RESOURCE_JSON }}
- name: Process Functional Test Results
uses: ./.github/actions/process-test-results
# Run on success and failure so a passing "Re-run failed jobs" refreshes the
# "Functional Tests - <name>" check published by EnricoMi. Gating this on
# failure() left that check stuck red after a green re-run, because the step
# was skipped and the previous attempt's failed check was never updated.
# comment_mode: failures keeps PR comments limited to actual failures, and
# the failure artifacts are still uploaded for post-mortem.
if: always() && github.repository == vars.RADIUS_REPOSITORY
with:
test_group_name: Functional Tests - ${{ matrix.name }}
artifact_name: functional_test_results_${{ matrix.name }}
result_directory: dist/functional_test/
comment_mode: failures
- name: Collect Pod details
if: always()
# Diagnostic-only snapshot of cluster-wide pod state for post-mortem. A
# `kubectl describe pods -A` can race pod teardown and return a transient
# NotFound, so never let it fail the job.
continue-on-error: true
env:
MATRIX_NAME: ${{ matrix.name }}
run: |
POD_STATE_LOG_FILENAME="${RADIUS_CONTAINER_LOG_BASE}/${MATRIX_NAME}-tests-pod-states.log"
mkdir -p "$(dirname "${POD_STATE_LOG_FILENAME}")"
{
echo "kubectl get pods -A"
kubectl get pods -A
echo "kubectl describe pods -A"
kubectl describe pods -A
} >> "${POD_STATE_LOG_FILENAME}"
- name: Upload container logs
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ matrix.name }}_container_logs
path: ./${{ env.RADIUS_CONTAINER_LOG_BASE }}
- name: Get Terraform recipe publishing logs
if: always()
run: |
# Create pod-logs directory
mkdir -p recipes/pod-logs
# Get pod logs and save to file
namespace="radius-test-tf-module-server"
label="app.kubernetes.io/name=tf-module-server"
pod_names=($(kubectl get pods -l $label -n $namespace -o jsonpath='{.items[*].metadata.name}'))
for pod_name in "${pod_names[@]}"; do
kubectl logs $pod_name -n $namespace > recipes/pod-logs/${pod_name}.txt
done
echo "Pod logs saved to recipes/pod-logs/"
# Get kubernetes events and save to file
kubectl get events -n $namespace > recipes/pod-logs/events.txt
- name: Upload Terraform recipe publishing logs
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: ${{ matrix.name }}_recipes-pod-logs
path: recipes/pod-logs
if-no-files-found: error
- uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4
if: success() && env.PR_NUMBER != ''
continue-on-error: true
with:
GITHUB_TOKEN: ${{ steps.get_installation_token.outputs.token }}
header: teststatus
number: ${{ env.PR_NUMBER }}
append: true
message: |
:white_check_mark: ${{ matrix.name }} functional tests succeeded
- uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4
if: failure() && env.PR_NUMBER != ''
continue-on-error: true
with:
GITHUB_TOKEN: ${{ steps.get_installation_token.outputs.token }}
header: teststatus
number: ${{ env.PR_NUMBER }}
append: true
message: |
:x: ${{ matrix.name }} functional test failed. Please check [the logs](${{ env.ACTION_LINK }}) for more details
- uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4
if: cancelled() && env.PR_NUMBER != ''
continue-on-error: true
with:
GITHUB_TOKEN: ${{ steps.get_installation_token.outputs.token }}
header: teststatus
number: ${{ env.PR_NUMBER }}
append: true
message: |
:x: ${{ matrix.name }} functional test cancelled. Please check [the logs](${{ env.ACTION_LINK }}) for more details
- name: Release ACI quota before async resource group delete
if: always()
env:
AZURE_SUBSCRIPTIONID_TESTS: ${{ secrets.AZURE_SUBSCRIPTIONID_TESTS }}
run: |
# The regional Azure Container Instances 'StandardCores' quota is shared
# across all concurrent cloud functional-test runs and is the bottleneck
# behind Test_ACI (deploys fail with ContainerGroupQuotaReached). The
# resource group is deleted with --no-wait below because the full delete is
# slow for gateways/VNets (see #12044), so its ACI resources would otherwise
# linger for minutes and keep holding quota that other runs need. Delete the
# ACI resources synchronously here to release the quota promptly. Best-effort:
# the resource group delete and the purge workflow still clean up the rest.
set -uo pipefail
sub="${AZURE_SUBSCRIPTIONID_TESTS}"
rg="${AZURE_TEST_RESOURCE_GROUP}"
# Radius creates the ACI nGroups/containerGroupProfiles with this API version
# (pkg/sdk/v20241101preview). Delete with it explicitly: a plain delete lets
# ARM pick the provider's latest advertised version (e.g. 2026-06-01-preview),
# which the RP rejects ("api-version not supported"), leaving the nGroups -
# and therefore the whole resource group - undeletable.
aci_api_version="2024-11-01-preview"
if ! az group show --subscription "${sub}" --name "${rg}" >/dev/null 2>&1; then
echo "Resource group ${rg} not found; nothing to release."
exit 0
fi
aci_ids=$(az resource list \
--subscription "${sub}" \
--resource-group "${rg}" \
--query "[?starts_with(type, 'Microsoft.ContainerInstance/')].id" \
--output tsv 2>/dev/null || true)
if [ -z "${aci_ids}" ]; then
echo "No Azure Container Instances resources in ${rg}."
exit 0
fi
echo "Releasing ACI quota by deleting:"
echo "${aci_ids}"
# nGroups (container scale sets) back the quota-holding container groups, so
# delete them first (in parallel) and wait, then delete the remaining ACI
# resources (profiles). Parallelizing keeps the synchronous teardown short
# while preserving the nGroups-before-profiles order (nGroups reference the
# profiles).
for id in $(echo "${aci_ids}" | grep -i '/nGroups/' || true); do
az resource delete --subscription "${sub}" --ids "${id}" --api-version "${aci_api_version}" --verbose || true &
done
wait
for id in $(echo "${aci_ids}" | grep -iv '/nGroups/' || true); do
az resource delete --subscription "${sub}" --ids "${id}" --api-version "${aci_api_version}" --verbose || true &
done
wait
- name: Delete azure resource group - ${{ env.AZURE_TEST_RESOURCE_GROUP }}
if: always()
env:
AZURE_SUBSCRIPTIONID_TESTS: ${{ secrets.AZURE_SUBSCRIPTIONID_TESTS }}
run: |
# if deletion fails, purge workflow will purge the resource group and its resources later.
az group delete \
--subscription "${AZURE_SUBSCRIPTIONID_TESTS}" \
--name "${AZURE_TEST_RESOURCE_GROUP}" \
--yes \
--verbose \
--no-wait
report-test-results:
# Report final test status. Runs after all tests complete (or are skipped).
if: always() && github.repository == vars.RADIUS_REPOSITORY
name: Report test results
needs: [setup, build, tests]
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
contents: read
env:
CHECKOUT_REF: ${{ needs.setup.outputs.CHECKOUT_REF }}
steps:
- name: Get GitHub app token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
id: get_installation_token
with:
client-id: ${{ secrets.FUNCTIONAL_TEST_CLIENT_ID }}
private-key: ${{ secrets.FUNCTIONAL_TEST_APP_PRIVATE_KEY }}
permission-checks: write
- name: Aggregate functional test status
id: get_test_status
run: |
# Derive the overall status from the needs context instead of querying the
# Actions API. `tests` is a matrix job, so needs.tests.result already collapses
# all legs (success only if every leg passed). A docs-only run skips build+tests
# with setup successful, which yields success and matches the skip-tests job.
# failure takes precedence over cancelled.
echo "setup=${SETUP_RESULT} build=${BUILD_RESULT} tests=${TESTS_RESULT}"
TEST_STATUS="success"
for result in "${SETUP_RESULT}" "${BUILD_RESULT}" "${TESTS_RESULT}"; do
if [[ "${result}" == "failure" ]]; then
TEST_STATUS="failure"
break
elif [[ "${result}" == "cancelled" ]]; then
TEST_STATUS="cancelled"
fi
done
echo "Functional Test Run status: ${TEST_STATUS}"
echo "test_status=${TEST_STATUS}" >> "${GITHUB_OUTPUT}"
env:
SETUP_RESULT: ${{ needs.setup.result }}
BUILD_RESULT: ${{ needs.build.result }}
TESTS_RESULT: ${{ needs.tests.result }}
- uses: LouisBrunner/checks-action@937cbbcde3259005b50746dc91cde29098aac2ff # v3.1.0
if: always()
with:
token: ${{ steps.get_installation_token.outputs.token }}
name: Functional Test Run
repo: ${{ github.repository }}
sha: ${{ env.CHECKOUT_REF }}
status: completed
conclusion: ${{ steps.get_test_status.outputs.test_status }}
output: |
{"summary":"Functional Test run completed. See links for more information.","title":"Functional Test Run"}
details_url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
report-failure:
name: Report test failure
needs: [build, tests]
runs-on: ubuntu-24.04
timeout-minutes: 5
if: failure() && github.event_name == 'schedule' && github.repository == vars.RADIUS_REPOSITORY
permissions:
issues: write # Required to create an issue when the scheduled run fails
steps:
- name: Create failure issue for failing scheduled run
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ github.token }}
script: |
github.rest.issues.create({
...context.repo,
title: `Scheduled functional test failed - Run ID: ${context.runId}`,
labels: ['test-failure'],
body: `## Bug information \n\nThis issue is automatically generated if the scheduled functional test fails. The Radius functional test operates on a schedule of every 4 hours during weekdays and every 12 hours over the weekend. It's important to understand that the test may fail due to workflow infrastructure issues, like network problems, rather than the flakiness of the test itself. For the further investigation, please visit [here](${process.env.ACTION_LINK}).`
})