diff --git a/.azure/pipeline-setup.md b/.azure/pipeline-setup.md new file mode 100644 index 0000000..a870113 --- /dev/null +++ b/.azure/pipeline-setup.md @@ -0,0 +1,28 @@ +# Codeflow Azure Pipeline Setup + +The Codeflow engine deployment workflow uses GitHub Actions OIDC with a user-assigned managed identity. Do not use the legacy `AZURE_CREDENTIALS` JSON secret for the production deploy path. + +## Production Environment Values + +Create a GitHub environment named `production` and configure these environment variables: + +| Variable | Purpose | +| --- | --- | +| `AZURE_CLIENT_ID` | Client ID of the pipeline user-assigned managed identity. | +| `AZURE_TENANT_ID` | Tenant ID for the Azure subscription. | +| `AZURE_SUBSCRIPTION_ID` | Subscription containing Codeflow production resources. | +| `AZURE_RESOURCE_GROUP` | Production resource group, expected default: `pvc-prod-codeflow-rg`. | +| `AZURE_CONTAINER_APP` | Production Container App, expected default: `pvc-prod-codeflow-api`. | +| `AZURE_CONTAINER_REGISTRY` | ACR name for production images, expected default: `pvcprodcodeflowacr`. | + +The workflow deploys only to existing infrastructure. Provision or import the production Container App and ACR before running the deploy job. + +Do not apply the root `orchestration/infrastructure/terraform` stack as the runtime prerequisite without reviewing it first; that stack still reflects an older AKS-shaped deployment. Use `orchestration/infrastructure/terraform/runtime` for the production Container App runtime stack. + +## Azure Setup + +Run `scripts/setup-azure-auth-for-pipeline.ps1` after confirming the production resource group exists. The script creates a dedicated pipeline identity, assigns resource-group scoped RBAC, grants ACR push access, and adds a federated credential for the GitHub `production` environment. + +The signed-in Azure CLI identity running the script must be allowed to create role assignments on the deployment resource group or subscription. Use `Owner`, `User Access Administrator`, `Role Based Access Control Administrator`, or another role that includes `Microsoft.Authorization/roleAssignments/write`; `Contributor` alone is not sufficient for the RBAC step. + +After setup, store the script outputs in the GitHub `production` environment variables. Keep any approval gates on the environment so pushes to `master` still require explicit deployment approval. diff --git a/.github/workflows/deploy-autopr-engine.yml b/.github/workflows/deploy-autopr-engine.yml index 0dbe552..ffae465 100644 --- a/.github/workflows/deploy-autopr-engine.yml +++ b/.github/workflows/deploy-autopr-engine.yml @@ -8,17 +8,20 @@ on: - "engine/**" - ".github/workflows/deploy-autopr-engine.yml" - ".github/app-manifest.yml" + - ".azure/pipeline-setup.md" + - "scripts/setup-azure-auth-for-pipeline.ps1" pull_request: paths: - "engine/**" - ".github/workflows/deploy-autopr-engine.yml" - ".github/app-manifest.yml" - ".codeflow.yml" + - ".azure/pipeline-setup.md" + - "scripts/setup-azure-auth-for-pipeline.ps1" workflow_dispatch: env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} + IMAGE_REPOSITORY: codeflow-engine jobs: # PR validation - build and validate only, no push/deploy @@ -70,28 +73,39 @@ jobs: name: Build and Push Container Image runs-on: ubuntu-latest if: github.event_name != 'pull_request' + environment: production permissions: contents: read - packages: write + id-token: write steps: - name: Checkout code uses: actions/checkout@v4 + - name: Azure Login + uses: azure/login@v2 + with: + client-id: ${{ vars.AZURE_CLIENT_ID }} + tenant-id: ${{ vars.AZURE_TENANT_ID }} + subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} + + - name: Validate registry configuration + run: | + if [ -z "${{ vars.AZURE_CONTAINER_REGISTRY }}" ]; then + echo "::error::Configure AZURE_CONTAINER_REGISTRY in the production environment." + exit 1 + fi + + - name: Log in to Azure Container Registry + run: az acr login --name "${{ vars.AZURE_CONTAINER_REGISTRY }}" + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Log in to Container Registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Extract metadata id: meta uses: docker/metadata-action@v5 with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + images: ${{ vars.AZURE_CONTAINER_REGISTRY }}.azurecr.io/${{ env.IMAGE_REPOSITORY }} tags: | type=ref,event=branch type=ref,event=pr @@ -118,20 +132,40 @@ jobs: runs-on: ubuntu-latest needs: build-and-push if: (github.ref == 'refs/heads/master' && github.event_name == 'push') || github.event_name == 'workflow_dispatch' + environment: production + permissions: + contents: read + id-token: write steps: - name: Azure Login uses: azure/login@v2 with: - creds: ${{ secrets.AZURE_CREDENTIALS }} + client-id: ${{ vars.AZURE_CLIENT_ID }} + tenant-id: ${{ vars.AZURE_TENANT_ID }} + subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} - name: Update existing Container App + env: + RESOURCE_GROUP: ${{ vars.AZURE_RESOURCE_GROUP }} + CONTAINER_APP: ${{ vars.AZURE_CONTAINER_APP }} + IMAGE: ${{ vars.AZURE_CONTAINER_REGISTRY }}.azurecr.io/${{ env.IMAGE_REPOSITORY }}:${{ github.ref_name }} run: | - RESOURCE_GROUP="prod-rg-san-codeflow" - CONTAINER_APP="prod-codeflow-san-app" - IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}" + missing=0 + for name in RESOURCE_GROUP CONTAINER_APP IMAGE; do + if [ -z "${!name}" ]; then + echo "::error::Missing required deployment value: $name" + missing=1 + fi + done + + if [ "$missing" -ne 0 ]; then + echo "::error::Configure the production environment variables documented in .azure/pipeline-setup.md." + exit 1 + fi if ! az containerapp show -n "$CONTAINER_APP" -g "$RESOURCE_GROUP" >/dev/null; then - echo "::error::Container App $CONTAINER_APP does not exist in $RESOURCE_GROUP. Create infrastructure from phoenixvc/codeflow-infrastructure first." + echo "::error::Cannot verify Container App $CONTAINER_APP in $RESOURCE_GROUP." + echo "::error::Confirm the app exists and the pipeline identity has access before deploying this image." exit 1 fi diff --git a/.github/workflows/pr-comment-handler.yml b/.github/workflows/pr-comment-handler.yml index 709b017..faadc10 100644 --- a/.github/workflows/pr-comment-handler.yml +++ b/.github/workflows/pr-comment-handler.yml @@ -15,7 +15,7 @@ jobs: - name: Quick checkout for config uses: actions/checkout@v4 with: - sparse-checkout: configs/config.yaml + sparse-checkout: engine/configs/config.yaml sparse-checkout-cone-mode: false - name: Check if commenter is excluded bot @@ -25,7 +25,7 @@ jobs: echo "Checking if '$COMMENTER' is an excluded bot..." # Check if bot exclusions are enabled - BOT_EXCLUSIONS_ENABLED=$(yq '.comment_analyzer.bot_exclusions.enabled // false' configs/config.yaml) + BOT_EXCLUSIONS_ENABLED=$(yq '.comment_analyzer.bot_exclusions.enabled // false' engine/configs/config.yaml) if [ "$BOT_EXCLUSIONS_ENABLED" != "true" ]; then echo "Bot exclusions disabled, processing comment" echo "is_excluded=false" >> $GITHUB_OUTPUT @@ -33,7 +33,7 @@ jobs: fi # Check excluded users list - EXCLUDED_USERS=$(yq '.comment_analyzer.bot_exclusions.excluded_users[]' configs/config.yaml 2>/dev/null || echo "") + EXCLUDED_USERS=$(yq '.comment_analyzer.bot_exclusions.excluded_users[]' engine/configs/config.yaml 2>/dev/null || echo "") for user in $EXCLUDED_USERS; do # Remove quotes from YAML output user=$(echo "$user" | tr -d '"') @@ -45,7 +45,7 @@ jobs: done # Check for [bot] suffix if enabled - EXCLUDE_BOT_SUFFIX=$(yq '.comment_analyzer.bot_exclusions.exclude_bot_suffix // false' configs/config.yaml) + EXCLUDE_BOT_SUFFIX=$(yq '.comment_analyzer.bot_exclusions.exclude_bot_suffix // false' engine/configs/config.yaml) if [ "$EXCLUDE_BOT_SUFFIX" = "true" ]; then if [[ "$COMMENTER" == *"[bot]" ]]; then echo "User '$COMMENTER' has [bot] suffix, skipping" @@ -61,7 +61,7 @@ jobs: if: steps.check_bot.outputs.is_excluded != 'true' id: check_config run: | - ENABLED=$(yq '.comment_analyzer.enabled // false' configs/config.yaml) + ENABLED=$(yq '.comment_analyzer.enabled // false' engine/configs/config.yaml) echo "result=$ENABLED" >> $GITHUB_OUTPUT - name: Full checkout diff --git a/.github/workflows/validate-templates.yml b/.github/workflows/validate-templates.yml index 17d3d47..02cd576 100644 --- a/.github/workflows/validate-templates.yml +++ b/.github/workflows/validate-templates.yml @@ -5,6 +5,8 @@ name: Validate Templates on: push: + branches: + - master paths: - "engine/templates/**/*.yml" - "engine/templates/**/*.yaml" diff --git a/.gitignore b/.gitignore index de8a505..5fef519 100644 --- a/.gitignore +++ b/.gitignore @@ -69,6 +69,13 @@ Thumbs.db # Build artifacts build-artifacts/ +# Terraform local artifacts +**/.terraform/ +*.tfstate +*.tfstate.* +*.tfplan +tfplan + # Credentials and sensitive files .credentials-*.json deployment-output.json diff --git a/orchestration/infrastructure/README.md b/orchestration/infrastructure/README.md index 74f39c9..2371a3d 100644 --- a/orchestration/infrastructure/README.md +++ b/orchestration/infrastructure/README.md @@ -41,11 +41,12 @@ exception documented next to the stack. ## Naming -Current Codeflow resource names use: +Current Codeflow resource names follow the ADR-0027 style: structured identifiers with no trailing +region suffix. Region remains expressed by the resource group location. -- Resource group: `prod-rg-san-codeflow` -- Website Static Web App: `prod-stapp-san-codeflow` -- Runtime Container App: `prod-codeflow-san-app` +- Resource group: `pvc-prod-codeflow-rg` +- Website Static Web App: `pvc-prod-codeflow-swa` +- Runtime Container App: `pvc-prod-codeflow-api` -Keep future names aligned with `{env}-{resource-type}-{region}-codeflow` unless a provider -constraint requires otherwise. +Keep future names aligned with `pvc-{env}-codeflow-{type}` unless a provider constraint requires +otherwise. Storage accounts and ACR names omit dashes. diff --git a/orchestration/infrastructure/terraform/runtime/.terraform.lock.hcl b/orchestration/infrastructure/terraform/runtime/.terraform.lock.hcl new file mode 100644 index 0000000..eccae6e --- /dev/null +++ b/orchestration/infrastructure/terraform/runtime/.terraform.lock.hcl @@ -0,0 +1,22 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/azurerm" { + version = "4.81.0" + constraints = "~> 4.2" + hashes = [ + "h1:4BEgtU/crlkbMduT7tm8Sp3+C+1RBfrpUIlTBM/wtMc=", + "zh:0732e7b74264ddfa2b90ba69d01c283d3cbae9f72ed3e506c6ac92529fed7fd3", + "zh:12afb524e232fe4e3d6161927724af5dfa4831d71edd9c174917ca9b7377bfae", + "zh:169d619ae202c4145e02fb706fb7c3679445ab3e3ff722edbf89597517a8c92e", + "zh:6beb95a3ef2f2d9c76abaa48e5450e90686a3fb6a47f1cb0ff7c5e94b6960151", + "zh:705e075fb5ffc4bf66fd7cbabf1a65007a41621e80030a2c158a4c83b6046216", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:79a8d17fefe647040fcb9ee8821a4f09f395427c4fd49493489b9a93a9a1038e", + "zh:8cc3f900b3774c0ae37ae42365c4579a199cf9e5edf88e476fdf5ab1048f84ea", + "zh:dec373b9390fa95e257291acd018ed65a7d512b428645d35e22cdbe8b245a08b", + "zh:e60f1e9fb45df6defade2855ed6e68547409ea75d30655c556adb0c08579749b", + "zh:f901d12ec82f3f8b5880a27b5cbcd7bd0d97e60c9367a2d7ed82fdd1157b39ff", + "zh:facf68ea5bf0f2b8ba720e7fba5f86492e1d4c591100460bb91c3f79f391f4b6", + ] +} diff --git a/orchestration/infrastructure/terraform/runtime/README.md b/orchestration/infrastructure/terraform/runtime/README.md new file mode 100644 index 0000000..9e0f65c --- /dev/null +++ b/orchestration/infrastructure/terraform/runtime/README.md @@ -0,0 +1,15 @@ +# Codeflow Runtime Terraform + +This stack owns the Azure Container Apps runtime for Codeflow Engine. + +Names follow the ADR-0027-style convention used for live Codeflow resources: + +- Resource group: `pvc-prod-codeflow-rg` +- Container App: `pvc-prod-codeflow-api` +- Container Apps Environment: `pvc-prod-codeflow-cae` +- Log Analytics Workspace: `pvc-prod-codeflow-law` +- Container Registry: `pvcprodcodeflowacr` + +The stack expects the Codeflow image to exist in ACR. For a first-time bootstrap before the image is +available, override `initial_image` with a temporary public image, then switch it back to the ACR +image after the first build. diff --git a/orchestration/infrastructure/terraform/runtime/main.tf b/orchestration/infrastructure/terraform/runtime/main.tf new file mode 100644 index 0000000..23c73a1 --- /dev/null +++ b/orchestration/infrastructure/terraform/runtime/main.tf @@ -0,0 +1,121 @@ +data "azurerm_resource_group" "codeflow" { + name = var.resource_group_name +} + +resource "azurerm_log_analytics_workspace" "runtime" { + name = var.log_analytics_workspace_name + location = var.location + resource_group_name = data.azurerm_resource_group.codeflow.name + sku = "PerGB2018" + retention_in_days = 30 + + tags = var.tags +} + +resource "azurerm_container_registry" "runtime" { + name = var.container_registry_name + location = var.location + resource_group_name = data.azurerm_resource_group.codeflow.name + sku = "Basic" + admin_enabled = false + + tags = var.tags +} + +resource "azurerm_user_assigned_identity" "container_app" { + name = var.container_app_identity_name + location = var.location + resource_group_name = data.azurerm_resource_group.codeflow.name + + tags = var.tags +} + +resource "azurerm_role_assignment" "container_app_acr_pull" { + scope = azurerm_container_registry.runtime.id + role_definition_name = "AcrPull" + principal_id = azurerm_user_assigned_identity.container_app.principal_id +} + +resource "azurerm_container_app_environment" "runtime" { + name = var.container_app_environment_name + location = var.location + resource_group_name = data.azurerm_resource_group.codeflow.name + log_analytics_workspace_id = azurerm_log_analytics_workspace.runtime.id + + tags = var.tags +} + +resource "azurerm_container_app" "api" { + name = var.container_app_name + container_app_environment_id = azurerm_container_app_environment.runtime.id + resource_group_name = data.azurerm_resource_group.codeflow.name + revision_mode = "Single" + + identity { + type = "UserAssigned" + identity_ids = [azurerm_user_assigned_identity.container_app.id] + } + + registry { + server = azurerm_container_registry.runtime.login_server + identity = azurerm_user_assigned_identity.container_app.id + } + + ingress { + external_enabled = true + target_port = 8080 + + traffic_weight { + percentage = 100 + latest_revision = true + } + } + + template { + min_replicas = 0 + max_replicas = 1 + + container { + name = "codeflow-engine" + image = var.initial_image + cpu = 0.5 + memory = "1Gi" + + env { + name = "CODEFLOW_JSON_LOGGING" + value = "true" + } + + env { + name = "CODEFLOW_LOG_LEVEL" + value = "INFO" + } + + env { + name = "CODEFLOW_SKIP_DB_INIT" + value = "1" + } + + env { + name = "CODEFLOW_STORAGE_BACKEND" + value = "memory" + } + + env { + name = "ENVIRONMENT" + value = "staging" + } + + env { + name = "PORT" + value = "8080" + } + } + } + + depends_on = [ + azurerm_role_assignment.container_app_acr_pull + ] + + tags = var.tags +} diff --git a/orchestration/infrastructure/terraform/runtime/outputs.tf b/orchestration/infrastructure/terraform/runtime/outputs.tf new file mode 100644 index 0000000..1c5c0d9 --- /dev/null +++ b/orchestration/infrastructure/terraform/runtime/outputs.tf @@ -0,0 +1,24 @@ +output "container_app_name" { + description = "Codeflow runtime Container App name." + value = azurerm_container_app.api.name +} + +output "container_app_fqdn" { + description = "Codeflow runtime public FQDN." + value = azurerm_container_app.api.ingress[0].fqdn +} + +output "container_registry_name" { + description = "Codeflow runtime Azure Container Registry name." + value = azurerm_container_registry.runtime.name +} + +output "container_registry_login_server" { + description = "Codeflow runtime Azure Container Registry login server." + value = azurerm_container_registry.runtime.login_server +} + +output "resource_group_name" { + description = "Codeflow runtime resource group." + value = data.azurerm_resource_group.codeflow.name +} diff --git a/orchestration/infrastructure/terraform/runtime/providers.tf b/orchestration/infrastructure/terraform/runtime/providers.tf new file mode 100644 index 0000000..a6c1a20 --- /dev/null +++ b/orchestration/infrastructure/terraform/runtime/providers.tf @@ -0,0 +1,16 @@ +terraform { + required_version = ">= 1.7.0" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.2" + } + } +} + +provider "azurerm" { + features {} + subscription_id = var.subscription_id + tenant_id = var.tenant_id +} diff --git a/orchestration/infrastructure/terraform/runtime/variables.tf b/orchestration/infrastructure/terraform/runtime/variables.tf new file mode 100644 index 0000000..17ad401 --- /dev/null +++ b/orchestration/infrastructure/terraform/runtime/variables.tf @@ -0,0 +1,68 @@ +variable "subscription_id" { + description = "Azure subscription ID for Codeflow runtime infrastructure." + type = string +} + +variable "tenant_id" { + description = "Azure tenant ID for Codeflow runtime infrastructure." + type = string +} + +variable "resource_group_name" { + description = "Existing Codeflow production resource group." + type = string + default = "pvc-prod-codeflow-rg" +} + +variable "location" { + description = "Azure region for Codeflow runtime resources." + type = string + default = "southafricanorth" +} + +variable "container_app_name" { + description = "Codeflow runtime Container App name." + type = string + default = "pvc-prod-codeflow-api" +} + +variable "container_app_environment_name" { + description = "Codeflow Container Apps Environment name." + type = string + default = "pvc-prod-codeflow-cae" +} + +variable "log_analytics_workspace_name" { + description = "Log Analytics Workspace for Codeflow Container Apps logs." + type = string + default = "pvc-prod-codeflow-law" +} + +variable "container_registry_name" { + description = "Globally unique Azure Container Registry name. ACR names cannot contain dashes." + type = string + default = "pvcprodcodeflowacr" +} + +variable "container_app_identity_name" { + description = "User-assigned managed identity attached to the Codeflow Container App." + type = string + default = "pvc-prod-codeflow-api-mi" +} + +variable "initial_image" { + description = "Codeflow Engine image deployed by default. Override only for bootstrap scenarios." + type = string + default = "pvcprodcodeflowacr.azurecr.io/codeflow-engine:master" +} + +variable "tags" { + description = "Tags applied to Codeflow runtime resources." + type = map(string) + default = { + Environment = "prod" + Product = "codeflow" + Owner = "phoenixvc" + ManagedBy = "terraform" + } +} diff --git a/orchestration/infrastructure/terraform/website/.terraform.lock.hcl b/orchestration/infrastructure/terraform/website/.terraform.lock.hcl new file mode 100644 index 0000000..eccae6e --- /dev/null +++ b/orchestration/infrastructure/terraform/website/.terraform.lock.hcl @@ -0,0 +1,22 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/azurerm" { + version = "4.81.0" + constraints = "~> 4.2" + hashes = [ + "h1:4BEgtU/crlkbMduT7tm8Sp3+C+1RBfrpUIlTBM/wtMc=", + "zh:0732e7b74264ddfa2b90ba69d01c283d3cbae9f72ed3e506c6ac92529fed7fd3", + "zh:12afb524e232fe4e3d6161927724af5dfa4831d71edd9c174917ca9b7377bfae", + "zh:169d619ae202c4145e02fb706fb7c3679445ab3e3ff722edbf89597517a8c92e", + "zh:6beb95a3ef2f2d9c76abaa48e5450e90686a3fb6a47f1cb0ff7c5e94b6960151", + "zh:705e075fb5ffc4bf66fd7cbabf1a65007a41621e80030a2c158a4c83b6046216", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:79a8d17fefe647040fcb9ee8821a4f09f395427c4fd49493489b9a93a9a1038e", + "zh:8cc3f900b3774c0ae37ae42365c4579a199cf9e5edf88e476fdf5ab1048f84ea", + "zh:dec373b9390fa95e257291acd018ed65a7d512b428645d35e22cdbe8b245a08b", + "zh:e60f1e9fb45df6defade2855ed6e68547409ea75d30655c556adb0c08579749b", + "zh:f901d12ec82f3f8b5880a27b5cbcd7bd0d97e60c9367a2d7ed82fdd1157b39ff", + "zh:facf68ea5bf0f2b8ba720e7fba5f86492e1d4c591100460bb91c3f79f391f4b6", + ] +} diff --git a/orchestration/infrastructure/terraform/website/main.tf b/orchestration/infrastructure/terraform/website/main.tf index e9a3db5..9a11005 100644 --- a/orchestration/infrastructure/terraform/website/main.tf +++ b/orchestration/infrastructure/terraform/website/main.tf @@ -15,6 +15,13 @@ resource "azurerm_static_web_app" "website" { preview_environments_enabled = true public_network_access_enabled = true + lifecycle { + ignore_changes = [ + repository_branch, + repository_url + ] + } + tags = var.tags } diff --git a/orchestration/infrastructure/terraform/website/variables.tf b/orchestration/infrastructure/terraform/website/variables.tf index 84e42c5..e15ab70 100644 --- a/orchestration/infrastructure/terraform/website/variables.tf +++ b/orchestration/infrastructure/terraform/website/variables.tf @@ -11,7 +11,7 @@ variable "tenant_id" { variable "resource_group_name" { description = "Resource group for Codeflow website hosting." type = string - default = "prod-rg-san-codeflow" + default = "pvc-prod-codeflow-rg" } variable "location" { @@ -23,7 +23,7 @@ variable "location" { variable "static_web_app_name" { description = "Azure Static Web App resource name." type = string - default = "prod-stapp-san-codeflow" + default = "pvc-prod-codeflow-swa" } variable "custom_domain" { diff --git a/scripts/setup-azure-auth-for-pipeline.ps1 b/scripts/setup-azure-auth-for-pipeline.ps1 new file mode 100644 index 0000000..7f317fd --- /dev/null +++ b/scripts/setup-azure-auth-for-pipeline.ps1 @@ -0,0 +1,100 @@ +param( + [Parameter(Mandatory = $true)] + [string]$SubscriptionId, + + [Parameter(Mandatory = $true)] + [string]$TenantId, + + [string]$IdentityResourceGroup = "pvc-prod-codeflow-identity-rg", + + [string]$DeploymentResourceGroup = "pvc-prod-codeflow-rg", + + [string]$ContainerRegistryName = "pvcprodcodeflowacr", + + [string]$Location = "southafricanorth", + + [string]$IdentityName = "pvc-prod-codeflow-github-mi", + + [string]$GitHubOwner = "phoenixvc", + + [string]$GitHubRepository = "codeflow-engine", + + [string]$GitHubEnvironment = "production" +) + +$ErrorActionPreference = "Stop" + +function Invoke-Az { + param( + [Parameter(ValueFromRemainingArguments = $true)] + [string[]]$AzArgs + ) + + $output = & az @AzArgs + if ($LASTEXITCODE -ne 0) { + throw "Azure CLI failed: az $($AzArgs -join ' ')" + } + + return $output +} + +Invoke-Az account set --subscription $SubscriptionId | Out-Null + +$selectedTenantId = Invoke-Az account show --query tenantId --output tsv +if ($selectedTenantId -ne $TenantId) { + throw "Selected subscription tenant '$selectedTenantId' does not match requested tenant '$TenantId'." +} + +Invoke-Az group create ` + --name $IdentityResourceGroup ` + --location $Location ` + --tags workload=codeflow purpose=github-actions-auth environment=production | Out-Null + +$identity = Invoke-Az identity create ` + --name $IdentityName ` + --resource-group $IdentityResourceGroup ` + --location $Location ` + --query "{clientId:clientId, principalId:principalId, id:id}" ` + --output json | ConvertFrom-Json + +$deploymentRgId = Invoke-Az group show ` + --name $DeploymentResourceGroup ` + --query id ` + --output tsv + +$containerRegistryId = Invoke-Az acr show ` + --name $ContainerRegistryName ` + --resource-group $DeploymentResourceGroup ` + --query id ` + --output tsv + +Invoke-Az role assignment create ` + --assignee-object-id $identity.principalId ` + --assignee-principal-type ServicePrincipal ` + --role Contributor ` + --scope $deploymentRgId | Out-Null + +Invoke-Az role assignment create ` + --assignee-object-id $identity.principalId ` + --assignee-principal-type ServicePrincipal ` + --role AcrPush ` + --scope $containerRegistryId | Out-Null + +$subject = "repo:${GitHubOwner}/${GitHubRepository}:environment:${GitHubEnvironment}" + +Invoke-Az identity federated-credential create ` + --name "$GitHubEnvironment-github-actions" ` + --identity-name $IdentityName ` + --resource-group $IdentityResourceGroup ` + --issuer "https://token.actions.githubusercontent.com" ` + --subject $subject ` + --audiences "api://AzureADTokenExchange" | Out-Null + +[PSCustomObject]@{ + AZURE_CLIENT_ID = $identity.clientId + AZURE_TENANT_ID = $selectedTenantId + AZURE_SUBSCRIPTION_ID = $SubscriptionId + AZURE_RESOURCE_GROUP = $DeploymentResourceGroup + AZURE_CONTAINER_APP = "pvc-prod-codeflow-api" + AZURE_CONTAINER_REGISTRY = $ContainerRegistryName +} | Format-List