Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
30bebb1
Add custom recipe pack support to run-rad-commands workflows
sk593 Jul 10, 2026
70071f6
Register custom types and use fixed recipe pack filename
sk593 Jul 10, 2026
afc92dd
Add delete application/environment workflows and fix env update --pre…
sk593 Jul 13, 2026
b8f1dfe
Wire OCI/GHCR state archive into delete workflows
sk593 Jul 20, 2026
117885e
Grant packages: write on delete dispatchers
sk593 Jul 20, 2026
b6eb91a
update docs with preview flag
sk593 Jul 21, 2026
e17dfe5
update docs with preview flag
sk593 Jul 21, 2026
00a974c
Merge branch 'main' into sk593-custom-types-recipe-packs
sk593 Jul 21, 2026
e72379e
Address PR review comments on custom recipe pack and delete actions
sk593 Jul 22, 2026
b67a303
Merge branch 'main' into sk593-custom-types-recipe-packs
sk593 Jul 23, 2026
89fcc89
Merge branch 'main' into sk593-custom-types-recipe-packs
sk593 Jul 23, 2026
ebec18e
Address PR review: scope recipe-pack attach and harden delete script
sk593 Jul 23, 2026
ed7b385
Address PR review: sanitize delete logs and correct state-archive docs
sk593 Jul 23, 2026
9711d81
Address PR review: use rad env show --preview for recipePacks
sk593 Jul 23, 2026
ebfb4e0
Merge branch 'main' into sk593-custom-types-recipe-packs
sk593 Jul 23, 2026
993708c
Address PR review: surface EKS access errors and fix stale action com…
sk593 Jul 23, 2026
61b2640
Address PR review: embed delete output via jq --rawfile
sk593 Jul 23, 2026
a232ccc
Address PR review: add apply-custom-recipe-packs to workflow diagram
sk593 Jul 23, 2026
d7347fd
Move radius-delete skill to ai-extensions repo
sk593 Jul 23, 2026
8b6bd4b
Address PR review: pass --environment to custom recipe pack deploy
sk593 Jul 24, 2026
2212d53
Merge branch 'main' into sk593-custom-types-recipe-packs
sk593 Jul 24, 2026
284958c
Merge branch 'main' into sk593-custom-types-recipe-packs
sk593 Jul 24, 2026
fdf712f
Potential fix for pull request finding
sk593 Jul 24, 2026
f444a25
Potential fix for pull request finding
sk593 Jul 24, 2026
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
24 changes: 18 additions & 6 deletions .github/extension/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,21 @@ To keep the two provider paths from duplicating the ~80% of steps they share, it
- **`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-avm` recipe pack (Azure Verified Modules) downloaded from [resource-types-contrib](https://github.com/radius-project/resource-types-contrib).
- **`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), [`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. Third-party actions in these workflows are pinned to full commit SHAs (with a `# vX` comment); only the first-party Radius composite actions are referenced by ref.
- **`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), [`apply-custom-recipe-packs`](actions/apply-custom-recipe-packs/action.yml), [`run-rad-commands`](actions/run-rad-commands/action.yml), [`delete-resource`](actions/delete-resource/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. Third-party actions in these workflows are pinned to full commit SHAs (with a `# vX` comment); only the first-party Radius composite actions are referenced by ref.

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

## `delete-application.yml` / `delete-environment.yml` (delete dispatchers and provider workflows)

Radius deletes a deployed application or an environment with the same ephemeral-control-plane model as the deploy flow. Because deleting recipe-backed resources runs the recipes' delete path (e.g. `terraform destroy`) against the target cluster and cloud, the delete workflows restore the persisted Radius state first, run the delete, then persist the updated state again — so subsequent runs plan against the post-delete state.

- **`delete-application.yml`** — dispatcher to delete one application. `workflow_dispatch` inputs: `environment` (GitHub Environment name) and `application` (application name). A `detect` job binds the environment, reads the provider variable, and calls the matching provider delete workflow with `resource_type: application`.
- **`delete-environment.yml`** — dispatcher to delete one environment. `workflow_dispatch` inputs: `environment` (GitHub Environment name) and optional `environment_name` (the Radius environment name, defaulting to the GitHub Environment name, since the deploy flow names the Radius environment after it). Calls the provider delete workflow with `resource_type: environment`.
- **`delete-azure.yml`** / **`delete-aws.yml`** — reusable (`workflow_call`) workflows with the provider-specific steps (OIDC login, cluster connection, cloud OIDC token projection, and credential registration) shared with the deploy provider workflows. They reuse the `setup-control-plane`, `restore-state`, [`delete-resource`](actions/delete-resource/action.yml), and `teardown` composite actions. Like the deploy provider workflows they log in to GHCR and set the `RADIUS_STATE_*` variables so `rad startup`/`rad shutdown` can open the OCI-backed state archive. Unlike the deploy provider workflows they do **not** create the environment, recipe pack, or the in-pod image-push registry credentials — the environment and its recipes are restored from state, and deleting builds no images.

The `delete-resource` composite action runs `rad app delete <name> --yes --preview` or `rad env delete <name> --yes --preview` (`--preview` selects the Radius.Core surface the deploy flow provisions) and writes a `rad-delete-result` artifact — a JSON document with `outcome`, `exitCode`, `resourceType`, `name`, and the command `output`.


### 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, 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.
Expand All @@ -82,9 +93,10 @@ The dispatcher routes to the matching provider workflow, which runs on `ubuntu-l
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. **Create the Radius environment and recipe pack.** `rad deploy`s a `radius-env.bicep` that defines a `Radius.Core/recipePacks` resource and the `Radius.Core/environments` resource that references it. Azure downloads the `azure-avm` pack (Azure Verified Modules) from [resource-types-contrib](https://github.com/radius-project/resource-types-contrib); AWS generates an inline `aws-terraform` pack. `radius-env.bicep` is written to the app file's directory (e.g. `.radius/`) and deployed from there, so `rad deploy` resolves the repo's own `bicepconfig.json` (which declares the `radius` extension) — bicep resolves the config nearest the `.bicep` file. The `Radius.Compute/containerImages` type ships with the Radius extension, so no separate resource-type registration is needed.
12. **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`), any application parameters from the `RADIUS_DEPLOY_PARAMS` secret, and the registry push/pull credentials as `registryUsername` (`github.actor`) and `registryPassword` (the built-in `GITHUB_TOKEN`). Those feed the app's `Radius.Security/secrets` resource (`radius-ghcr-registry-creds`), which materializes the registry Secret on the target cluster so the containerImages recipe's in-pod BuildKit can push the application image. The secret value is passed via an argv array and never written into the recorded command string.
13. **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.
14. **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).
12. **Register custom types and apply custom recipe pack.** When the app's `.radius/` folder carries a `custom-types.yaml` file, the shared `apply-custom-recipe-packs` action registers those resource types with `rad resource-type create --from-file` (skipped when absent). When it carries a `custom-recipe-pack.bicep` file, the action snapshots the recipe-pack IDs before and after `rad deploy`ing that pack to identify the newly-created pack(s), reads the environment's existing `recipePacks` with `rad env show --preview`, and runs `rad env update <env> --recipe-packs <existing ∪ new> --preview` so the environment keeps the default provider pack and gains the custom pack — without pulling in unrelated packs the control plane may know about (skipped when absent). When neither file exists this step is a no-op and the default pack stays in place.
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`), any application parameters from the `RADIUS_DEPLOY_PARAMS` secret, and the registry push/pull credentials as `registryUsername` (`github.actor`) and `registryPassword` (the built-in `GITHUB_TOKEN`). Those feed the app's `Radius.Security/secrets` resource (`radius-ghcr-registry-creds`), which materializes the registry Secret on the target cluster so the containerImages recipe's in-pod BuildKit can push the application image. The secret value is passed via an argv array and never written into the recorded command string.
14. **Persist state (`rad shutdown`).** Backs the control-plane databases and Terraform recipe-state Secrets up to the state archive — the OCI-backed archive by default (pushed to GHCR, selected by the `RADIUS_STATE_*` variables), or the `radius-state` git orphan branch when `RADIUS_STATE_BACKEND=git`. 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

Expand All @@ -102,7 +114,7 @@ Triggers and permissions live on the **dispatcher** (`run-rad-commands.yml`); th
| `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).
- **Permissions:** `id-token: write` (required for OIDC), `contents: write` (so `rad shutdown` can push the `radius-state` branch when the git state backend is selected), and `packages: write` (to push the OCI-backed state archive to GHCR and the application image built by the containerImages recipe).

### Required environment variables

Expand All @@ -125,7 +137,7 @@ This workflow also reads GitHub Actions **secrets** for image push and applicati

### 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).
`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). These workflows use the OCI-backed state archive by default — the `RADIUS_STATE_*` variables select an OCI repository and the workflow logs in to GHCR before `rad startup`/`rad shutdown` — and fall back to the `radius-state` git orphan branch only when `RADIUS_STATE_BACKEND=git`. 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

Expand Down
112 changes: 112 additions & 0 deletions .github/extension/actions/apply-custom-recipe-packs/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Provider-agnostic custom resource-type / recipe-pack apply shared by
# run-rad-commands-aws.yml and run-rad-commands-azure.yml. Runs after the
# provider-specific "Create Radius environment and recipe pack" step. When the
# app's `.radius/` folder (alongside `.radius/app.bicep`) carries custom resource
# types, this:
# 1. registers those types from `.radius/custom-types.yaml` via
# `rad resource-type create --from-file` (skipped when the file is absent), and
# 2. deploys `.radius/custom-recipe-pack.bicep`, then attaches only the pack(s)
# newly created by that deploy to the environment -- preserving the
# environment's existing recipe packs (e.g. the default provider pack) and
# adding the custom pack, without pulling in unrelated packs the control
# plane may know about (skipped when the file is absent).
# When neither file exists the action is a no-op, leaving the default pack in place.
name: Radius - Apply custom recipe packs
description: Register the repo's custom resource types and recipe pack (if present) and attach the new pack to the environment alongside its existing packs.

inputs:
environment:
description: Radius environment name to update with the full recipe pack list.
required: true
app-file:
description: Application bicep file. Its directory (e.g. .radius/) is searched for custom-types.yaml and custom-recipe-pack.bicep.
required: true

runs:
using: composite
steps:
- name: Register custom types and apply custom recipe pack
shell: bash
env:
ENVIRONMENT: ${{ inputs.environment }}
APP_FILE: ${{ inputs.app-file }}
run: |
set -euo pipefail

# Custom type / recipe-pack files live next to the app file (e.g. .radius/).
# Deploying from that directory lets `rad deploy` resolve the repo's own
# .radius/bicepconfig.json (which declares the `radius` extension) -- bicep
# resolves the config nearest the .bicep file.
APP_DIR=$(dirname "$APP_FILE")
CUSTOM_TYPES_YAML="$APP_DIR/custom-types.yaml"
RECIPE_PACK_BICEP="$APP_DIR/custom-recipe-pack.bicep"

# 1. Register custom resource types with Radius. --from-file registers every
# type defined in the manifest. Must run before deploying the recipe pack,
# which references these types. Absent file => nothing to register.
if [ -f "$CUSTOM_TYPES_YAML" ]; then
echo "Registering custom resource types from $CUSTOM_TYPES_YAML..."
rad resource-type create --from-file "$CUSTOM_TYPES_YAML"
echo "✅ Custom resource types registered."
else
echo "No custom resource types at $CUSTOM_TYPES_YAML; skipping type registration."
fi

# 2. Deploy the custom recipe pack and attach it to the environment. Absent
# file => nothing to deploy, keep the default pack.
if [ ! -f "$RECIPE_PACK_BICEP" ]; then
echo "No custom recipe pack at $RECIPE_PACK_BICEP; keeping the default recipe pack."
exit 0
fi

# Capture the set of recipe-pack IDs before deploying so we can identify
# exactly which pack(s) this step creates. `rad recipe-pack list` returns
# every pack across scopes (including ones restored from prior state), so we
# must not blindly attach all of them -- that could pull unrelated packs into
# the environment and trip server-side recipe-pack conflict validation.
# ids_json emits a compact JSON array of the non-empty string IDs.
ids_json() {
rad recipe-pack list -o json \
| jq -c '(if type == "array" then . else [.] end) | map(.id) | map(select(type == "string" and . != ""))'
}

echo "Recording existing recipe packs..."
PACKS_BEFORE=$(ids_json)

# Pass --environment explicitly: the recipe pack bicep only creates a
# Radius.Core/recipePacks resource (no environment), so rad deploy would
# otherwise require a default environment to be set. The environment was
# created by the preceding step.
echo "Deploying custom recipe pack from $RECIPE_PACK_BICEP..."
rad deploy "$RECIPE_PACK_BICEP" --environment "$ENVIRONMENT"

PACKS_AFTER=$(ids_json)

# New pack(s) = those present after the deploy but not before.
NEW_PACKS=$(jq -nc --argjson before "$PACKS_BEFORE" --argjson after "$PACKS_AFTER" '$after - $before')

# Preserve the environment's current recipe packs (e.g. the default provider
# pack attached when the environment was created) and add only the new pack(s).
# `rad env update --recipe-packs` REPLACES the list, so we compute the full
# desired set here rather than passing every pack the control plane knows.
# --preview reads the Radius.Core/environments resource (which carries
# properties.recipePacks); without it env show hits the legacy surface and
# can't see recipe packs. That command prints the resource first followed by
# optional provider/recipe JSON docs, so jq -s slurps the stream and takes the
# environment resource ([0]) rather than choking on the trailing documents.
EXISTING_PACKS=$(rad env show "$ENVIRONMENT" --preview -o json | jq -sc '((.[0].properties.recipePacks) // []) | map(select(type == "string" and . != ""))')

PACK_IDS=$(jq -nr --argjson existing "$EXISTING_PACKS" --argjson new "$NEW_PACKS" '($existing + $new) | unique | join(",")')

if [ -z "$PACK_IDS" ]; then
echo "No recipe packs found to attach to environment '$ENVIRONMENT'." >&2
exit 1
fi

# Replace the environment's recipe pack list with the preserved + new set so
# the custom types resolve alongside the default provider recipes. --preview
# selects the Radius.Core implementation; without it `rad env update`
# dispatches to the legacy command, which does not understand --recipe-packs.
echo "Attaching recipe packs to environment '$ENVIRONMENT': $PACK_IDS"
rad env update "$ENVIRONMENT" --recipe-packs "$PACK_IDS" --preview
echo "✅ Environment '$ENVIRONMENT' updated with recipe packs."
Loading
Loading