Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions .github/extension/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Repo Radius workflow assets

This folder holds the **Repo Radius** workflow templates that are written into a user's repository so that Radius can run on a GitHub Actions runner.

These files are templates: a copy is committed into the target repository under `.github/workflows/` and dispatched there. They are not run from this repository directly.

They live here so the workflow contract has a canonical, reviewed home that any frontend (the Copilot app, the CLI, etc.) can drive. See [radius-project/radius#12118](https://github.com/radius-project/radius/issues/12118) for background.

## Credential verification (upstream)

Before this deploy workflow runs, a separate **`Radius - Verify Credentials`** workflow confirms the GitHub Environment's cloud credentials are wired up correctly. It is generated by the environment-setup flow (see the `radius-environment` skill), not by this folder, so its template is documented there rather than here. The deploy dispatcher (below) chains off it: a successful verify run auto-triggers a deploy via its `workflow_run` trigger.

The OIDC trust that the verify and deploy workflows both rely on must already exist before either can authenticate:

- **Azure:** a federated credential on the AAD app whose subject is exactly `repo:<owner>/<repo>:environment:<environment-name>`, audience `api://AzureADTokenExchange`.
- **AWS:** an IAM role trust policy that allows `sts:AssumeRoleWithWebIdentity` from `token.actions.githubusercontent.com` with audience `sts.amazonaws.com` and subject `repo:<owner>/<repo>:environment:<environment-name>`.

## `run-rad-commands.yml` (dispatcher, provider workflows, and shared actions)

The run-rad-commands workflow Radius uses to run one or more `rad` CLI commands on demand against a user's target cluster — deploying by default, but able to run any allowed command (`deploy`, `app graph`, `app delete`, and so on). It stands up an ephemeral [k3d](https://k3d.io) control plane on the runner, restores persisted state, runs the requested commands, then persists state again and tears the control plane down.

To keep the two provider paths from duplicating the ~80% of steps they share, it ships as a unified dispatcher, two thin provider workflows, and shared composite actions:

- **`run-rad-commands.yml`** — the unified **dispatcher** and the only file that is dispatched. It owns the dispatch contract (`workflow_dispatch` inputs and the `Radius - Verify Credentials` auto-trigger). A `detect` job binds the GitHub Environment, reads which provider variable is set (`AZURE_CLIENT_ID` / `AWS_ROLE_ARN`), and calls the matching provider workflow via `workflow_call` with `secrets: inherit`.
- **`run-rad-commands-azure.yml`** — a reusable (`workflow_call`) workflow with only the Azure-specific steps: Azure OIDC login, AKS connection (`az aks get-credentials`), workload-identity credential registration, and the `azure-terraform` recipe pack.
- **`run-rad-commands-aws.yml`** — a reusable (`workflow_call`) workflow with only the AWS-specific steps: AWS OIDC login, EKS connection (access entry + static token kubeconfig), IRSA credential registration, and the `aws-terraform` recipe pack.
- **`actions/*`** — composite actions holding the provider-agnostic phases both provider workflows share: [`setup-control-plane`](actions/setup-control-plane/action.yml), [`restore-state`](actions/restore-state/action.yml), [`register-resource-types`](actions/register-resource-types/action.yml), [`run-rad-commands`](actions/run-rad-commands/action.yml), and [`teardown`](actions/teardown/action.yml). The provider workflows reference them from `radius-project/radius` at a pinned ref (the `{{RADIUS_REF}}` placeholder the generator fills in), so the shared logic has a single reviewed home and is not copied into user repos.

The deploy flow (see the `radius-deploy` skill) generates the dispatcher and both provider workflows, commits them to the target repo under `.github/workflows/`, and dispatches `run-rad-commands.yml`.

### What it does

The dispatcher routes to the matching provider workflow, which runs on `ubuntu-latest`. It stands up an ephemeral [k3d](https://k3d.io) cluster to host the Radius control plane on the runner, points that control plane at the user's existing EKS/AKS cluster, and deploys the application there. The control-plane setup, state restore, resource-type registration, and run/teardown phases below run from the shared composite actions; the OIDC login, cluster connection, token projection, credential registration, and recipe-pack creation are the provider-specific steps. When a provider's identifying variable is empty, its steps are skipped and resources deploy to the ephemeral control-plane cluster instead of an external target.

1. **Authenticate via OIDC.** Runs `azure/login` (Azure) or `aws-actions/configure-aws-credentials` (AWS) so the runner exchanges its GitHub OIDC token for short-lived cloud credentials. No long-lived cloud secrets are stored.
2. **Build the target-cluster kubeconfig.** Exports `RADIUS_TARGET_KUBECONFIG` to a path under `$HOME/.kube`, then connects to the workload cluster: Azure runs `az aks get-credentials --file`; AWS ensures an EKS access entry and cluster-admin access policy for the IAM role and writes a static, token-based kubeconfig.
3. **Create the ephemeral control plane.** Installs k3d and creates the `radius-cp` cluster, then installs `oras`, the `rad` CLI (edge), and Terraform.
4. **Create the target-kubeconfig secret.** Stores the target kubeconfig as the `target-kubeconfig` secret in `radius-system` (skipped when no target kubeconfig is present).
5. **Install Radius on the control plane.** Runs `rad install kubernetes` with `database.enabled=true` (control-plane PostgreSQL for durable state), `rp.publicEndpointOverride=localhost`, `dynamicrp.buildkit.enabled=true`, and — when a target kubeconfig is present — `global.targetCluster.enabled=true`. The chart mounts the secret into `applications-rp`, `dynamic-rp`, and `bicep-de` and sets `RADIUS_TARGET_KUBECONFIG`, so recipe execution and directly-rendered resources land on the external cluster. The Terraform state backend deliberately stays on the control-plane cluster.
6. **Project cloud OIDC tokens.** Mints a GitHub OIDC token for the provider and patches it into the RP/DE pods at the fixed path each reads for the federated token exchange (AWS IRSA `/var/run/secrets/eks.amazonaws.com/serviceaccount/token`; Azure workload identity `/var/run/secrets/azure/tokens/azure-identity-token`).
7. **Refresh external target credentials.** AWS re-mints the short-lived EKS token; both providers rewrite the `target-kubeconfig` secret and restart `applications-rp`, `dynamic-rp`, and `bicep-de` so they re-read it.
8. **Configure the workspace.** Runs `rad workspace create kubernetes default` and `rad group create` / `rad group switch default`.
9. **Restore persisted state (`rad startup`).** Restores the control-plane databases and the Terraform recipe-state Secrets saved by the previous run, so `rad deploy` plans against prior state rather than an empty backend. A no-op on the first run.
10. **Register cloud credentials.** Registers the cloud identity with `rad credential register azure wi` / `aws irsa` so Radius holds the identity selector and reads the projected token at runtime.
11. **Register resource types and recipes.** Clones [resource-types-contrib](https://github.com/radius-project/resource-types-contrib), registers the `Radius.Compute/containerImages` type, builds its local Bicep extension (`radiuscompute.tgz`), writes `bicepconfig.json`, and `rad deploy`s a generated `radius-env.bicep` that defines the provider-specific Terraform recipe pack (`aws-terraform` / `azure-terraform`) and the `Radius.Core/environments` resource.
12. **Provision registry credentials on the control plane.** Creates the `ghcr-registry-creds` secret from `github.actor` and the built-in `GITHUB_TOKEN` so the containerImages recipe's in-pod BuildKit can push the application image.
13. **Run the requested rad commands.** Validates each command in `rad_commands` against the allowed-command set, then runs them in order (stopping on the first failure) and writes a combined `rad-commands-result` artifact. When `rad_commands` is empty it runs the default `rad deploy <app-file> --environment <env>`, passing the `image` parameter (the `image` input, defaulting to `github.sha`) and any application parameters from the `RADIUS_DEPLOY_PARAMS` secret. It first ensures the `radiusCompute` extension is imported into the app bicep.
14. **Persist state (`rad shutdown`).** Backs the control-plane databases and Terraform recipe-state Secrets up to the `radius-state` git orphan branch. This runs even when the deploy fails (`if: always()`), so a partially-applied Terraform run is not lost.
15. **Tear down.** Runs `rad app list`, and always deletes the ephemeral `radius-cp` cluster. On failure, Radius and application logs are collected and uploaded as the `radius-logs` artifact (three-day retention).

### Triggers and permissions

Triggers and permissions live on the **dispatcher** (`run-rad-commands.yml`); the provider workflows are `workflow_call`-only and inherit permissions and secrets from it.

- **Triggers:**
- `workflow_dispatch` with an `environment` input (the GitHub Environment name) plus optional `image` and `rad_commands` inputs. The `detect` job binds that environment via `environment: ${{ inputs.environment }}` to read the provider variables.
- `workflow_run` after the `Radius - Verify Credentials` workflow completes. The `detect` job runs only when the upstream verify run concluded `success`, so a successful credential check auto-triggers a deploy.
- **Inputs:**

| Input | Required | Description |
|---|---|---|
| `environment` | Yes | The GitHub Environment name, used as the Radius environment. |
| `image` | No | Container image for the application, passed to the default deploy as the `image` parameter. Defaults to the commit SHA (`github.sha`) when unset. |
| `rad_commands` | No | A single `rad` command string, or a JSON array of command strings run in order (the `rad` prefix omitted, e.g. `["deploy .radius/app.bicep --environment dev", "app graph my-app -o json"]`). Each command is validated against the allowed-command set. Falls back to the `RADIUS_RAD_COMMANDS` variable. When empty, the workflow runs its default `rad deploy` of the app bicep. |

- **Outputs:** a combined `rad-commands-result` artifact — a JSON document with a top-level `outcome`/`exitCode` and a `commands` array (one entry per command, in input order, with each command's exit code and output).
- **Permissions:** `id-token: write` (required for OIDC), `contents: write` (so `rad shutdown` can push the `radius-state` branch), and `packages: write` (to push the application image built by the containerImages recipe).

### Required environment variables

The workflow reads cloud and cluster configuration from GitHub Actions **variables** (`vars`). Configure the relevant provider's set on the target GitHub Environment:

| Provider | Variables |
|---|---|
| Common | `KUBERNETES_NAMESPACE` (default `default`), `RADIUS_BUILD_REGISTRY` (default `ghcr.io/<owner>/<repo>`), `RADIUS_RAD_COMMANDS` (optional fallback for `rad_commands`) |
| Azure (`run-rad-commands-azure.yml`) | `AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, `AZURE_SUBSCRIPTION_ID`, `AZURE_RESOURCE_GROUP`, `AZURE_AKS_CLUSTER_NAME` |
| AWS (`run-rad-commands-aws.yml`) | `AWS_ROLE_ARN`, `AWS_REGION`, `AWS_ACCOUNT_ID`, `AWS_EKS_CLUSTER_NAME`, `RADIUS_VPC_ID`, `RADIUS_SUBNET_IDS` |

The provider steps run only when the identifying variable (`AZURE_CLIENT_ID` or `AWS_ROLE_ARN`) is non-empty. When it is unset, resources deploy to the ephemeral control-plane cluster instead of an external target.

This workflow also reads GitHub Actions **secrets** for image push and application configuration:

| Secret | Purpose |
|---|---|
| `GITHUB_TOKEN` | Built-in. Used with `github.actor` to authenticate the containerImages recipe's image push to GHCR. |
| `RADIUS_DEPLOY_PARAMS` | Optional. A JSON object of application parameters (`{"password":"…","apiKey":"…"}`) expanded into `--parameters name=value` pairs on the default deploy. |

### State persistence (`rad startup` / `rad shutdown`)

`rad startup` and `rad shutdown` are kind-agnostic CLI commands that restore and back up all durable Radius state (control-plane PostgreSQL + Terraform recipe-state Secrets) to a `radius-state` git orphan branch. They do not manage cluster lifecycle — the workflow owns creating and destroying the ephemeral control plane around them. `rad startup` runs after the install (so `rad deploy` plans against prior state) and `rad shutdown` runs after the commands with `if: always()` (so state survives a failed deploy).

### Prerequisites

- OIDC trust for the environment (federated credential on Azure, IAM role trust policy on AWS). Run the verify workflow first to confirm the environment is wired up correctly — a successful verify run also auto-triggers this workflow.
- The target cluster (`AWS_EKS_CLUSTER_NAME` / `AZURE_AKS_CLUSTER_NAME`) must already exist and be reachable; the assumed identity needs cluster-admin-level access to it.
- The application must define its app bicep file in the target repo.

For the full deploy flow and troubleshooting, see the `radius-deploy` skill.
67 changes: 67 additions & 0 deletions .github/extension/actions/register-resource-types/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Provider-agnostic resource-type + Bicep setup shared by run-rad-commands-aws.yml
# and run-rad-commands-azure.yml. Registers the resource types from
# resource-types-contrib, builds the local containerImages Bicep extension, and
# writes the deployment bicepconfig.json.
name: Radius - Register resource types
description: Register resource types, build the local Bicep extension, and write bicepconfig.json.

inputs:
resource-types-repo:
description: resource-types-contrib git repository URL.
required: false
default: https://github.com/radius-project/resource-types-contrib.git
resource-types-ref:
description: resource-types-contrib git ref.
required: false
default: main

runs:
using: composite
steps:
- name: Clone resource-types-contrib and register resource types
shell: bash
env:
RESOURCE_TYPES_CONTRIB_REPO: ${{ inputs.resource-types-repo }}
RESOURCE_TYPES_CONTRIB_REF: ${{ inputs.resource-types-ref }}
run: |
git clone --depth 1 --branch "$RESOURCE_TYPES_CONTRIB_REF" "$RESOURCE_TYPES_CONTRIB_REPO" /tmp/resource-types-contrib

for TYPE_YAML in \
Compute/containerImages/containerImages.yaml; do
if [ -f "/tmp/resource-types-contrib/$TYPE_YAML" ]; then
echo "Registering $TYPE_YAML..."
rad resource-type create -f "/tmp/resource-types-contrib/$TYPE_YAML" || \
(echo "Retrying after 5s..." && sleep 5 && rad resource-type create -f "/tmp/resource-types-contrib/$TYPE_YAML")
else
echo "Skipping $TYPE_YAML (not found)"
fi
done
echo "✅ Resource types registered"

# Build a local Bicep extension from the containerImages type definition.
CONTAINER_IMAGES_YAML="/tmp/resource-types-contrib/Compute/containerImages/containerImages.yaml"
if [ -f "$CONTAINER_IMAGES_YAML" ]; then
echo "Building local Bicep extension for containerImages..."
rad bicep publish-extension -f "$CONTAINER_IMAGES_YAML" --target ./radiuscompute.tgz --force
echo "✅ radiuscompute.tgz built"
fi

- name: Create bicepconfig for deployment
shell: bash
run: |
cat > bicepconfig.json << 'EOF'
{
"experimentalFeaturesEnabled": {
"extensibility": true
},
"extensions": {
"radius": "br:biceptypes.azurecr.io/radius:latest",
"radiusCompute": "./radiuscompute.tgz",
"radiusData": "br:biceptypes.azurecr.io/radiusdata:latest",
"radiusSecurity": "br:biceptypes.azurecr.io/radiussecurity:latest",
"aws": "br:biceptypes.azurecr.io/aws:latest"
}
}
EOF
echo "bicepconfig.json created"
cat bicepconfig.json
50 changes: 50 additions & 0 deletions .github/extension/actions/restore-state/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Provider-agnostic state restore shared by run-rad-commands-aws.yml and
# run-rad-commands-azure.yml. Sets up the Radius workspace/resource group and
# restores durable state from the previous run via `rad startup`.
name: Radius - Restore state
description: Configure the Radius workspace and restore persisted state (rad startup).

inputs:
namespace:
description: Target Kubernetes namespace (vars.KUBERNETES_NAMESPACE, defaults to default).
required: false
default: default

runs:
using: composite
steps:
- name: Configure Radius workspace
shell: bash
env:
NAMESPACE: ${{ inputs.namespace }}
run: |
# Ensure namespace exists on target cluster before Radius deploys into it.
TARGET_KUBECONFIG="$RADIUS_TARGET_KUBECONFIG"
if [ -f "$TARGET_KUBECONFIG" ]; then
echo "Ensuring namespace $NAMESPACE exists on target cluster..."
kubectl --kubeconfig "$TARGET_KUBECONFIG" get namespace "$NAMESPACE" 2>/dev/null || \
kubectl --kubeconfig "$TARGET_KUBECONFIG" create namespace "$NAMESPACE"
fi

# The Radius.Core/environments resource (with its recipe pack and
# cloud providers) is created later via Bicep. `rad install kubernetes`
# already creates the 'default' resource group server-side, but we create
# it explicitly (idempotent) as a defensive measure against older control
# planes, then set up the local CLI workspace and point it at that group.
rad workspace create kubernetes default
rad group create default
rad group switch default

- name: Restore Radius state (rad startup)
shell: bash
run: |
# rad startup restores the control-plane PostgreSQL databases and the
# Terraform recipe-state Secrets saved by the previous run's `rad shutdown`
# from the `radius-state` git orphan branch, so commands plan against prior
# state instead of an empty backend. On the first ever run there is nothing
# to restore and it is a no-op. It needs the workspace created above and
# waits for PostgreSQL to be ready itself. Configure a git identity so it can
# manage the state branch.
git config --global user.email "radius-deploy@users.noreply.github.com"
git config --global user.name "radius-deploy"
rad startup
Loading
Loading