Skip to content
Merged
Changes from all 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
107 changes: 107 additions & 0 deletions .github/workflows/mcp-server-deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
name: MCP Server Deploy (reusable)

# Canonical deploy workflow for wyre-technology/*-mcp repositories.
# Called from each per-repo `.github/workflows/release.yml` `deploy` job after
# semantic-release + docker push have produced a versioned image and its digest.
#
# What this workflow does:
# - Logs into Azure via OIDC
# - Deploys the image to `gwp-<vendor-slug>` in `mcp-gateway-prod` by IMMUTABLE
# digest (never :latest)
# - Sets an `IMAGE_VERSION` env var on the revision tying it back to release
# version + git SHA + workflow run ID
#
# Failure patterns this prevents:
# - Deploying to mcpgw-prod-<vendor> (orphaned legacy ACAs from a prior naming
# convention — the gateway routes vendor traffic to gwp-<vendor> per its
# VENDOR_URL_<VENDOR> env). Releases shipped to mcpgw-prod-* are silent no-ops.
# - Deploying by :latest tag, which can resolve to a stale digest through GHCR
# edge caches or ACA's image-pull layer — the deploy "succeeds" but the new
# revision actually rolls onto the prior image (observed in autotask-mcp on
# 2026-05-13, broken for ~3 weeks).
#
# Inputs are passed through `env:` before any shell interpolation, so no
# untrusted strings reach `run:` blocks (workflow_call inputs are typed and
# come from sibling repo workflows we control, but defense-in-depth is cheap).

on:
workflow_call:
inputs:
vendor-slug:
description: 'Vendor slug. Drives the target ACA name `gwp-<slug>` and is the source of truth for where the gateway routes traffic.'
required: true
type: string
image-name:
description: 'Full GHCR image base path (no tag/digest), e.g. `ghcr.io/wyre-technology/autotask-mcp`.'
required: true
type: string
digest:
description: 'Image digest (e.g. `sha256:abc123...`) produced by the caller''s `docker/build-push-action` step. Pass via `${{ needs.docker.outputs.digest }}`.'
required: true
type: string
version:
description: 'Semver release version with no `v` prefix (e.g. `2.25.2`). Used only for legibility in the deployed revision''s IMAGE_VERSION env var.'
required: true
type: string
resource-group:
description: 'Azure resource group containing the target ACA.'
required: false
type: string
default: 'mcp-gateway-prod'
environment:
description: 'GitHub Environment to run in (for env-scoped secrets and deployment protection rules).'
required: false
type: string
default: 'production'
secrets:
AZURE_CLIENT_ID:
required: true
AZURE_TENANT_ID:
required: true
AZURE_SUBSCRIPTION_ID:
required: true

jobs:
deploy:
name: Deploy ${{ inputs.vendor-slug }} to gwp-${{ inputs.vendor-slug }}
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
permissions:
id-token: write
contents: read
env:
TARGET_ACA: gwp-${{ inputs.vendor-slug }}
RESOURCE_GROUP: ${{ inputs.resource-group }}
IMAGE_NAME: ${{ inputs.image-name }}
IMAGE_DIGEST: ${{ inputs.digest }}
RELEASE_VERSION: ${{ inputs.version }}
steps:
- name: Azure login (OIDC)
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

- name: Deploy to Azure Container Apps
run: |
set -euo pipefail

if [ -z "${IMAGE_DIGEST}" ]; then
echo "::error::Input 'digest' is empty. Pass the docker/build-push-action digest output (steps.<id>.outputs.digest) — refusing to deploy by mutable :latest tag." >&2
exit 1
fi
if [[ "${IMAGE_DIGEST}" != sha256:* ]]; then
echo "::error::Input 'digest' must start with 'sha256:' (got '${IMAGE_DIGEST}'). Refusing to deploy a non-digest reference." >&2
exit 1
fi

SHORT_SHA="${GITHUB_SHA::7}"
IMAGE="${IMAGE_NAME}@${IMAGE_DIGEST}"
echo "Deploying ${IMAGE} to ${TARGET_ACA} (release v${RELEASE_VERSION}, sha ${SHORT_SHA}, run ${GITHUB_RUN_ID})"

az containerapp update \
--name "${TARGET_ACA}" \
--resource-group "${RESOURCE_GROUP}" \
--image "${IMAGE}" \
--set-env-vars "IMAGE_VERSION=v${RELEASE_VERSION}-sha-${SHORT_SHA}-${GITHUB_RUN_ID}"
Loading