CI/CD Pipeline – Azure Container Apps
This page contains the full, drop‑in GitHub Actions workflow used to build and deploy services to Azure Container Apps.
You can copy and paste this file exactly as-is into your repository at:
.github/workflows/build-deploy-aca.yml
Full GitHub Actions Workflow (DROP‑IN)
# .github/workflows/build-deploy-aca.yml
# DROP-IN (FULL FILE) — fixes:
# 1) Checkout centralized CI actions repo (.ci-actions) BEFORE any status-step usage
# 2) Ensure deploy job also gets a token + checks out .ci-actions (works for private repo)
# 3) Make early status steps use a safe, always-available context (branch/sha) before PIPELINE_CONTEXT is set
# 4) Use CI_ACTIONS_PATH variable consistently via local path: ./.ci-actions/${{ env.CI_ACTIONS_PATH }}
name: Build and Deploy to Azure Container Apps
on:
push:
branches: [ "dev", "qa", "prod" ]
permissions:
id-token: write
contents: read
concurrency:
group: aca-${{ github.ref_name }}-${{ github.workflow }}
cancel-in-progress: true
env:
# Shared repo
SHARED_REPO: ${{ github.repository_owner }}/ts-002
# Where CommonModules lives inside ts-002 (adjust if needed)
SHARED_COMMONMODULES_PATH: CommonModules
# ✅ Centralized CI composite actions (single place to change later)
CI_ACTIONS_REPO: ts-ci-actions
CI_ACTIONS_REF: v1.0.0
CI_ACTIONS_PATH: .github/actions/status-step
jobs:
build:
name: Build & Push Image (ACR)
# ✅ dynamic runner labels (dev/qa/prod) - supported form
runs-on: ${{ fromJSON(format('["self-hosted","build","{0}","ts-gw"]', github.ref_name)) }}
outputs:
image_tag: ${{ steps.meta.outputs.image_tag }}
image_full: ${{ steps.meta.outputs.image_full }}
steps:
# --------------------------
# Token + CI actions checkout FIRST (so local action is available)
# --------------------------
- name: Create GitHub App token (for cross-repo checkout)
id: app-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.TS_CI_APP_ID }}
private-key: ${{ secrets.TS_CI_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
# ✅ include ts-ci-actions so the same token can read it
repositories: ts-002,${{ github.event.repository.name }},${{ env.CI_ACTIONS_REPO }}
- name: Checkout centralized CI actions repo
id: checkout_ci_actions
uses: actions/checkout@v4
with:
repository: ${{ github.repository_owner }}/${{ env.CI_ACTIONS_REPO }}
ref: ${{ env.CI_ACTIONS_REF }}
path: .ci-actions
token: ${{ steps.app-token.outputs.token }}
fetch-depth: 1
- name: Status - Checkout centralized CI actions repo
if: always()
uses: ./.ci-actions/.github/actions/status-step
with:
step_name: "Checkout centralized CI actions repo"
outcome: ${{ steps.checkout_ci_actions.outcome }}
# safe early context (PIPELINE_CONTEXT not set yet)
context: "env=${{ github.ref_name }} sha=${{ github.sha }}"
success_title: "🟢 CI actions checkout"
success_message: "🟢 Checked out '${{ github.repository_owner }}/${{ env.CI_ACTIONS_REPO }}@${{ env.CI_ACTIONS_REF }}' to .ci-actions"
failure_title: "🔴 CI actions checkout"
failure_message: "🔴 Failed to checkout CI actions repo."
- name: Status - Create GitHub App token
if: always()
uses: ./.ci-actions/.github/actions/status-step
with:
step_name: "Create GitHub App token"
outcome: ${{ steps.app-token.outcome }}
context: "env=${{ github.ref_name }} sha=${{ github.sha }}"
success_title: "✅ GitHub App token"
success_message: "✅ Token created (repos=ts-002,${{ github.event.repository.name }},${{ env.CI_ACTIONS_REPO }})"
failure_title: "🔴 GitHub App token"
failure_message: "🔴 Token creation failed (or was cancelled)."
# --------------------------
# Repo checkouts (service + shared)
# --------------------------
- name: Checkout service repo (this repo)
id: checkout_svc
uses: actions/checkout@v4
with:
path: svc
token: ${{ steps.app-token.outputs.token }}
- name: Status - Checkout service repo
if: always()
uses: ./.ci-actions/.github/actions/status-step
with:
step_name: "Checkout service repo"
outcome: ${{ steps.checkout_svc.outcome }}
context: "env=${{ github.ref_name }} sha=${{ github.sha }}"
success_title: "🟢 Checkout service repo"
success_message: "🟢 Checkout succeeded (repo=${{ github.repository }}, path=svc)"
failure_title: "🔴 Checkout service repo"
failure_message: "🔴 Checkout failed (or was cancelled)."
- name: Checkout shared repo (ts-002)
id: checkout_shared
uses: actions/checkout@v4
with:
repository: ${{ env.SHARED_REPO }}
path: shared
token: ${{ steps.app-token.outputs.token }}
fetch-depth: 1
- name: Status - Checkout shared repo
if: always()
uses: ./.ci-actions/.github/actions/status-step
with:
step_name: "Checkout shared repo"
outcome: ${{ steps.checkout_shared.outcome }}
context: "env=${{ github.ref_name }} sha=${{ github.sha }}"
success_title: "🟢 Checkout shared repo"
success_message: "🟢 Checkout succeeded (repo=${{ env.SHARED_REPO }}, path=shared)"
failure_title: "🔴 Checkout shared repo"
failure_message: "🔴 Checkout failed (or was cancelled)."
# --------------------------
# Resolve pipeline context (now we can use PIPELINE_CONTEXT)
# --------------------------
- name: Resolve pipeline context
id: ctx_build
shell: bash
run: |
set -euo pipefail
# Core GitHub context
ORG_NAME="${{ github.repository_owner }}"
SERVICE_NAME="${{ github.event.repository.name }}"
FULL_REPO="${{ github.repository }}"
BRANCH="${GITHUB_REF_NAME}"
REF_TYPE="${{ github.ref_type }}"
COMMIT_SHA="${GITHUB_SHA}"
SHORT_SHA="${GITHUB_SHA::7}"
RUN_ID="${{ github.run_id }}"
RUN_NUMBER="${{ github.run_number }}"
RUN_ATTEMPT="${{ github.run_attempt }}"
ACTOR="${{ github.actor }}"
DEFAULT_BRANCH="${{ github.event.repository.default_branch }}"
REPO_URL="${{ github.event.repository.html_url }}"
IS_PRIVATE="${{ github.event.repository.private }}"
# (Optional) commit message can be empty on some events; keep safe
COMMIT_MESSAGE="${{ github.event.head_commit.message }}"
COMMIT_MESSAGE_ONE_LINE="$(echo "${COMMIT_MESSAGE:-}" | tr '\n' ' ' | head -c 160)"
# Derived infra naming (your conventions)
RESOURCE_GROUP="${BRANCH}-ts-resource-group"
CONTAINER_APP_NAME="${BRANCH}-${SERVICE_NAME}-app"
ACR_NAME="${BRANCH}tsacr"
IMAGE_NAME="image_${SERVICE_NAME}"
# Resolve branch-specific Azure client id
case "${BRANCH}" in
dev) AZURE_CLIENT_ID="${{ secrets.AZURE_DEV_CLIENT_ID }}" ;;
qa) AZURE_CLIENT_ID="${{ secrets.AZURE_QA_CLIENT_ID }}" ;;
prod) AZURE_CLIENT_ID="${{ secrets.AZURE_PROD_CLIENT_ID }}" ;;
*) echo "ERROR: Unsupported branch '${BRANCH}'. Expected dev/qa/prod."; exit 1 ;;
esac
# Export to subsequent steps
echo "ORG_NAME=${ORG_NAME}" >> "$GITHUB_ENV"
echo "SERVICE_NAME=${SERVICE_NAME}" >> "$GITHUB_ENV"
echo "FULL_REPO=${FULL_REPO}" >> "$GITHUB_ENV"
echo "BRANCH=${BRANCH}" >> "$GITHUB_ENV"
echo "REF_TYPE=${REF_TYPE}" >> "$GITHUB_ENV"
echo "COMMIT_SHA=${COMMIT_SHA}" >> "$GITHUB_ENV"
echo "SHORT_SHA=${SHORT_SHA}" >> "$GITHUB_ENV"
echo "RUN_ID=${RUN_ID}" >> "$GITHUB_ENV"
echo "RUN_NUMBER=${RUN_NUMBER}" >> "$GITHUB_ENV"
echo "RUN_ATTEMPT=${RUN_ATTEMPT}" >> "$GITHUB_ENV"
echo "ACTOR=${ACTOR}" >> "$GITHUB_ENV"
echo "DEFAULT_BRANCH=${DEFAULT_BRANCH}" >> "$GITHUB_ENV"
echo "REPO_URL=${REPO_URL}" >> "$GITHUB_ENV"
echo "IS_PRIVATE=${IS_PRIVATE}" >> "$GITHUB_ENV"
echo "COMMIT_MESSAGE_ONE_LINE=${COMMIT_MESSAGE_ONE_LINE}" >> "$GITHUB_ENV"
echo "RESOURCE_GROUP=${RESOURCE_GROUP}" >> "$GITHUB_ENV"
echo "CONTAINER_APP_NAME=${CONTAINER_APP_NAME}" >> "$GITHUB_ENV"
echo "ACR_NAME=${ACR_NAME}" >> "$GITHUB_ENV"
echo "IMAGE_NAME=${IMAGE_NAME}" >> "$GITHUB_ENV"
echo "AZURE_CLIENT_ID=${AZURE_CLIENT_ID}" >> "$GITHUB_ENV"
# Standard context suffix reused across status steps
echo "PIPELINE_CONTEXT=svc=${SERVICE_NAME} env=${BRANCH} sha=${SHORT_SHA}" >> "$GITHUB_ENV"
# Loud, scannable log block
echo "✅🟢 Pipeline Context Resolved"
echo " 🏢 Org : ${ORG_NAME}"
echo " 🧬 Shared Repo : ${SHARED_REPO}"
echo " 📦 Repo : ${FULL_REPO}"
echo " 🔗 Repo URL : ${REPO_URL}"
echo " 🔒 Private : ${IS_PRIVATE}"
echo " 🌿 Branch : ${BRANCH}"
echo " 🏷️ Ref Type : ${REF_TYPE}"
echo " 🧩 Service : ${SERVICE_NAME}"
echo " 🔖 Commit (short) : ${SHORT_SHA}"
echo " 🧾 Commit (full) : ${COMMIT_SHA}"
echo " 📝 Commit msg : ${COMMIT_MESSAGE_ONE_LINE}"
echo " 👤 Actor : ${ACTOR}"
echo " 🧪 Run : #${RUN_NUMBER} (id=${RUN_ID}, attempt=${RUN_ATTEMPT})"
echo " 🌱 Default Branch : ${DEFAULT_BRANCH}"
echo " 🧱 Resource Group : ${RESOURCE_GROUP}"
echo " 🚀 Container App : ${CONTAINER_APP_NAME}"
echo " 🐳 ACR : ${ACR_NAME}"
echo " 🖼️ Image Name : ${IMAGE_NAME}"
- name: Status - Resolve pipeline context
if: always()
uses: ./.ci-actions/.github/actions/status-step
with:
step_name: "Resolve pipeline context"
outcome: ${{ steps.ctx_build.outcome }}
context: ${{ env.PIPELINE_CONTEXT }}
success_title: "✅ Resolve pipeline context"
success_message: "✅ Context resolved (rg=${{ env.RESOURCE_GROUP }}, aca=${{ env.CONTAINER_APP_NAME }}, acr=${{ env.ACR_NAME }}, image=${{ env.IMAGE_NAME }})"
failure_title: "🔴 Resolve pipeline context"
failure_message: "🔴 Context resolution failed (or was cancelled)."
- name: Validate derived names (ACR / Container App)
id: validate_names
shell: bash
run: |
set -euo pipefail
echo "🔎 Validating names..."
echo " 🐳 ACR_NAME : ${ACR_NAME}"
echo " 🚀 CONTAINER_APP_NAME: ${CONTAINER_APP_NAME}"
# ACR name rules (practical guard): lowercase letters+digits only, length 5..50
if ! [[ "${ACR_NAME}" =~ ^[a-z0-9]+$ ]]; then
echo "❌ ACR_NAME must be lowercase alphanumeric only. Got: '${ACR_NAME}'"
exit 1
fi
len=${#ACR_NAME}
if (( len < 5 || len > 50 )); then
echo "❌ ACR_NAME length must be 5..50. Got length=${len} value='${ACR_NAME}'"
exit 1
fi
echo "✅🟢 Name validation passed."
- name: Status - Validate derived names
if: always()
uses: ./.ci-actions/.github/actions/status-step
with:
step_name: "Validate derived names"
outcome: ${{ steps.validate_names.outcome }}
context: ${{ env.PIPELINE_CONTEXT }}
success_title: "🟢 Validate names"
success_message: "🟢 Name validation passed (acr=${{ env.ACR_NAME }}, aca=${{ env.CONTAINER_APP_NAME }})"
failure_title: "🔴 Validate names"
failure_message: "🔴 Name validation failed (or was cancelled)."
- name: Azure login (OIDC)
id: azure_login
uses: azure/login@v2
with:
client-id: ${{ env.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Status - Azure login (OIDC)
if: always()
uses: ./.ci-actions/.github/actions/status-step
with:
step_name: "Azure login (build)"
outcome: ${{ steps.azure_login.outcome }}
context: ${{ env.PIPELINE_CONTEXT }}
success_title: "🟢 Azure login"
success_message: "🟢 Azure login succeeded."
failure_title: "🔴 Azure login"
failure_message: "🔴 Azure login failed (or was cancelled)."
- name: ACR login (token-based)
id: acr_login
shell: bash
run: |
set -euo pipefail
TOKEN="$(az acr login -n "$ACR_NAME" --expose-token --output tsv --query accessToken)"
echo "$TOKEN" | docker login "$ACR_NAME.azurecr.io" \
--username 00000000-0000-0000-0000-000000000000 \
--password-stdin
- name: Status - ACR login (token-based)
if: always()
uses: ./.ci-actions/.github/actions/status-step
with:
step_name: "ACR login"
outcome: ${{ steps.acr_login.outcome }}
context: ${{ env.PIPELINE_CONTEXT }}
success_title: "🟢 ACR login"
success_message: "🟢 ACR login succeeded (acr=${{ env.ACR_NAME }})"
failure_title: "🔴 ACR login"
failure_message: "🔴 ACR login failed (or was cancelled)."
- name: Compute image metadata
id: meta
shell: bash
run: |
set -euo pipefail
echo "image_tag=${COMMIT_SHA}" >> "$GITHUB_OUTPUT"
echo "image_full=${ACR_NAME}.azurecr.io/${IMAGE_NAME}:${COMMIT_SHA}" >> "$GITHUB_OUTPUT"
- name: Status - Compute image metadata
if: always()
uses: ./.ci-actions/.github/actions/status-step
with:
step_name: "Compute image metadata"
outcome: ${{ steps.meta.outcome }}
context: ${{ env.PIPELINE_CONTEXT }}
success_title: "🟢 Image metadata"
success_message: "🟢 Metadata computed (image=${{ steps.meta.outputs.image_full }})"
failure_title: "🔴 Image metadata"
failure_message: "🔴 Metadata step failed (or was cancelled)."
- name: Prepare combined build context
id: prep_buildctx
shell: bash
run: |
set -euo pipefail
rm -rf buildctx
mkdir -p "buildctx/MicroServices (Web APIs)/${SERVICE_NAME}"
# Put the service repo at the exact path expected by Dockerfile COPY lines
cp -a svc/. "buildctx/MicroServices (Web APIs)/${SERVICE_NAME}/"
# Copy shared CommonModules into expected path
if [ ! -d "shared/${SHARED_COMMONMODULES_PATH}" ]; then
echo "ERROR: shared/${SHARED_COMMONMODULES_PATH} not found."
echo "Update env.SHARED_COMMONMODULES_PATH to match the real location in ts-002."
echo "Current tree under ./shared:"
ls -la shared
exit 1
fi
mkdir -p buildctx/CommonModules
cp -a "shared/${SHARED_COMMONMODULES_PATH}/." buildctx/CommonModules/
echo "Sanity check: service folder should exist at:"
ls -la "buildctx/MicroServices (Web APIs)/${SERVICE_NAME}/" || true
echo "Sanity check: common modules:"
ls -la buildctx/CommonModules || true
- name: Status - Prepare combined build context
if: always()
uses: ./.ci-actions/.github/actions/status-step
with:
step_name: "Prepare build context"
outcome: ${{ steps.prep_buildctx.outcome }}
context: ${{ env.PIPELINE_CONTEXT }}
success_title: "🟢 Build context"
success_message: "🟢 Build context prepared (svc_path=buildctx/MicroServices (Web APIs)/${{ env.SERVICE_NAME }})"
failure_title: "🔴 Build context"
failure_message: "🔴 Build context step failed (or was cancelled)."
- name: Verify Dockerfile exists
id: verify_dockerfile
shell: bash
run: |
set -euo pipefail
DOCKERFILE_PATH="buildctx/MicroServices (Web APIs)/${SERVICE_NAME}/Dockerfile"
if [ ! -f "$DOCKERFILE_PATH" ]; then
echo "ERROR: Dockerfile not found at: $DOCKERFILE_PATH"
echo "Listing expected folder:"
ls -la "buildctx/MicroServices (Web APIs)/${SERVICE_NAME}" || true
echo "Searching for Dockerfile under buildctx:"
find buildctx -maxdepth 6 -name Dockerfile -print || true
exit 1
fi
- name: Status - Verify Dockerfile exists
if: always()
uses: ./.ci-actions/.github/actions/status-step
with:
step_name: "Verify Dockerfile exists"
outcome: ${{ steps.verify_dockerfile.outcome }}
context: ${{ env.PIPELINE_CONTEXT }}
success_title: "🟢 Dockerfile"
success_message: "🟢 Dockerfile found (path=buildctx/MicroServices (Web APIs)/${{ env.SERVICE_NAME }}/Dockerfile)"
failure_title: "🔴 Dockerfile"
failure_message: "🔴 Dockerfile missing or step failed."
- name: Set up Docker Buildx
id: setup_buildx
uses: docker/setup-buildx-action@v3
- name: Status - Set up Docker Buildx
if: always()
uses: ./.ci-actions/.github/actions/status-step
with:
step_name: "Set up Docker Buildx"
outcome: ${{ steps.setup_buildx.outcome }}
context: ${{ env.PIPELINE_CONTEXT }}
success_title: "🟢 Docker Buildx"
success_message: "🟢 Buildx setup succeeded."
failure_title: "🔴 Docker Buildx"
failure_message: "🔴 Buildx setup failed (or was cancelled)."
- name: Build and push (Buildx + GHA cache)
id: buildpush
uses: docker/build-push-action@v6
with:
context: buildctx
file: buildctx/MicroServices (Web APIs)/${{ env.SERVICE_NAME }}/Dockerfile
push: true
tags: ${{ steps.meta.outputs.image_full }}
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: true
sbom: true
- name: Status - Build and push image
if: always()
uses: ./.ci-actions/.github/actions/status-step
with:
step_name: "Build & Push Image"
outcome: ${{ steps.buildpush.outcome }}
context: ${{ env.PIPELINE_CONTEXT }}
success_title: "🟢 Build & Push Image"
success_message: "🟢 Image build/push succeeded (image=${{ steps.meta.outputs.image_full }})"
failure_title: "🔴 Build & Push Image"
failure_message: "🔴 Image build/push failed (or was cancelled)."
- name: Print image digest
id: print_digest
shell: bash
run: |
set -euo pipefail
echo "✅🧾 Image pushed:"
echo " 🖼️ ${{ steps.meta.outputs.image_full }}"
echo " 🔐 Digest: ${{ steps.buildpush.outputs.digest }}"
- name: Status - Print image digest
if: always()
uses: ./.ci-actions/.github/actions/status-step
with:
step_name: "Print image digest"
outcome: ${{ steps.print_digest.outcome }}
context: ${{ env.PIPELINE_CONTEXT }}
success_title: "🟢 Print digest"
success_message: "🟢 Digest printed (digest=${{ steps.buildpush.outputs.digest }})"
failure_title: "🔴 Print digest"
failure_message: "🔴 Digest print step failed (or was cancelled)."
- name: Upload SBOM / provenance artifacts (if produced)
id: upload_supplychain
uses: actions/upload-artifact@v4
with:
name: supply-chain-${{ github.ref_name }}-${{ github.run_id }}
path: |
**/*sbom*
**/*provenance*
if-no-files-found: ignore
- name: Status - Upload supply chain artifacts
if: always()
uses: ./.ci-actions/.github/actions/status-step
with:
step_name: "Upload supply chain artifacts"
outcome: ${{ steps.upload_supplychain.outcome }}
context: ${{ env.PIPELINE_CONTEXT }}
success_title: "🟢 Upload artifacts"
success_message: "🟢 Artifact upload step completed."
failure_title: "🔴 Upload artifacts"
failure_message: "🔴 Artifact upload step failed (or was cancelled)."
deploy:
name: Deploy to Azure Container Apps (dev)
needs: build
# ✅ dynamic runner labels (dev/qa/prod) - supported form
runs-on: ${{ fromJSON(format('["self-hosted","deploy","{0}","ts-gw"]', github.ref_name)) }}
steps:
# --------------------------
# Token + CI actions checkout FIRST in this job too (fresh runner)
# --------------------------
- name: Create GitHub App token (deploy job)
id: app-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.TS_CI_APP_ID }}
private-key: ${{ secrets.TS_CI_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: ${{ github.event.repository.name }},${{ env.CI_ACTIONS_REPO }}
- name: Checkout centralized CI actions repo (deploy job)
id: checkout_ci_actions
uses: actions/checkout@v4
with:
repository: ${{ github.repository_owner }}/${{ env.CI_ACTIONS_REPO }}
ref: ${{ env.CI_ACTIONS_REF }}
path: .ci-actions
fetch-depth: 1
token: ${{ steps.app-token.outputs.token }}
- name: Status - Checkout centralized CI actions repo (deploy)
if: always()
uses: ./.ci-actions/.github/actions/status-step
with:
step_name: "Checkout centralized CI actions repo (deploy)"
outcome: ${{ steps.checkout_ci_actions.outcome }}
context: "env=${{ github.ref_name }} sha=${{ github.sha }}"
success_title: "🟢 CI actions checkout (deploy)"
success_message: "🟢 Checked out '${{ github.repository_owner }}/${{ env.CI_ACTIONS_REPO }}@${{ env.CI_ACTIONS_REF }}' to .ci-actions"
failure_title: "🔴 CI actions checkout (deploy)"
failure_message: "🔴 Failed to checkout CI actions repo."
- name: Resolve pipeline context (deploy)
id: ctx_deploy
shell: bash
run: |
set -euo pipefail
BRANCH="${GITHUB_REF_NAME}"
SERVICE_NAME="${{ github.event.repository.name }}"
RESOURCE_GROUP="${BRANCH}-ts-resource-group"
CONTAINER_APP_NAME="${BRANCH}-${SERVICE_NAME}-app"
case "${BRANCH}" in
dev) AZURE_CLIENT_ID="${{ secrets.AZURE_DEV_CLIENT_ID }}" ;;
qa) AZURE_CLIENT_ID="${{ secrets.AZURE_QA_CLIENT_ID }}" ;;
prod) AZURE_CLIENT_ID="${{ secrets.AZURE_PROD_CLIENT_ID }}" ;;
*) echo "ERROR: Unsupported branch '${BRANCH}'. Expected dev/qa/prod."; exit 1 ;;
esac
echo "BRANCH=${BRANCH}" >> "$GITHUB_ENV"
echo "SERVICE_NAME=${SERVICE_NAME}" >> "$GITHUB_ENV"
echo "RESOURCE_GROUP=${RESOURCE_GROUP}" >> "$GITHUB_ENV"
echo "CONTAINER_APP_NAME=${CONTAINER_APP_NAME}" >> "$GITHUB_ENV"
echo "AZURE_CLIENT_ID=${AZURE_CLIENT_ID}" >> "$GITHUB_ENV"
SHORT_SHA="${GITHUB_SHA::7}"
echo "PIPELINE_CONTEXT=svc=${SERVICE_NAME} env=${BRANCH} sha=${SHORT_SHA}" >> "$GITHUB_ENV"
echo "✅🟢 Deploy Context"
echo " 🌿 Branch : ${BRANCH}"
echo " 🧩 Service : ${SERVICE_NAME}"
echo " 🧱 ResourceGroup : ${RESOURCE_GROUP}"
echo " 🚀 Container App : ${CONTAINER_APP_NAME}"
- name: Status - Resolve pipeline context (deploy)
if: always()
uses: ./.ci-actions/.github/actions/status-step
with:
step_name: "Resolve pipeline context (deploy)"
outcome: ${{ steps.ctx_deploy.outcome }}
context: ${{ env.PIPELINE_CONTEXT }}
success_title: "🟢 Deploy context"
success_message: "🟢 Deploy context resolved (rg=${{ env.RESOURCE_GROUP }}, aca=${{ env.CONTAINER_APP_NAME }})"
failure_title: "🔴 Deploy context"
failure_message: "🔴 Deploy context resolution failed."
- name: Azure login (OIDC)
id: azure_login_deploy
uses: azure/login@v2
with:
client-id: ${{ env.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Status - Azure login (deploy)
if: always()
uses: ./.ci-actions/.github/actions/status-step
with:
step_name: "Azure login (deploy)"
outcome: ${{ steps.azure_login_deploy.outcome }}
context: ${{ env.PIPELINE_CONTEXT }}
success_title: "🟢 Azure login (deploy)"
success_message: "🟢 Azure login succeeded."
failure_title: "🔴 Azure login (deploy)"
failure_message: "🔴 Azure login failed (or was cancelled)."
- name: Preflight - verify Container App exists
id: preflight_containerapp
shell: bash
run: |
set -euo pipefail
echo "🔎 Preflight: checking Container App exists..."
echo " 🧱 Resource Group : ${RESOURCE_GROUP}"
echo " 🚀 Container App : ${CONTAINER_APP_NAME}"
if az containerapp show \
--resource-group "${RESOURCE_GROUP}" \
--name "${CONTAINER_APP_NAME}" \
1>/dev/null; then
echo "✅🟢 Container App exists. Proceeding to deploy."
else
echo "❌ Container App not found (or access denied)."
echo " Check these values:"
echo " - RESOURCE_GROUP=${RESOURCE_GROUP}"
echo " - CONTAINER_APP_NAME=${CONTAINER_APP_NAME}"
echo " Also confirm the app is created in Azure Container Apps for this environment."
exit 1
fi
- name: Status - Preflight Container App
if: always()
uses: ./.ci-actions/.github/actions/status-step
with:
step_name: "Preflight - verify Container App exists"
outcome: ${{ steps.preflight_containerapp.outcome }}
context: ${{ env.PIPELINE_CONTEXT }}
success_title: "🟢 Container App preflight"
success_message: "🟢 Container App exists. Proceeding to deploy (aca=${{ env.CONTAINER_APP_NAME }})"
failure_title: "🔴 Container App preflight"
failure_message: "🔴 Container App not found (or access denied)."
- name: Deploy to Azure Container Apps
id: deploy_aca
uses: azure/container-apps-deploy-action@v2
with:
resourceGroup: ${{ env.RESOURCE_GROUP }}
containerAppName: ${{ env.CONTAINER_APP_NAME }}
imageToDeploy: ${{ needs.build.outputs.image_full }}
- name: Status - Deploy to Azure Container Apps
if: always()
uses: ./.ci-actions/.github/actions/status-step
with:
step_name: "Deploy to Azure Container Apps"
outcome: ${{ steps.deploy_aca.outcome }}
context: ${{ env.PIPELINE_CONTEXT }}
success_title: "🟢 Deploy ACA"
success_message: "🟢 Deploy succeeded (image=${{ needs.build.outputs.image_full }})"
failure_title: "🔴 Deploy ACA"
failure_message: "🔴 Deploy failed (or was cancelled)."
Notes
- This workflow uses Azure OIDC authentication (no secrets stored).
- It supports dev / qa / prod branches.
- Centralized CI status actions are checked out before use.
- Designed for self-hosted runners with environment labels.