Skip to content

Commit c2d6fc8

Browse files
authored
feat(workflows): add mcp-server-deploy reusable workflow (#11)
Canonical deploy workflow for wyre-technology/*-mcp repositories. Companion to mcp-server-ci.yml, finishing the per-repo workflow factorization (CI was already shared; deploy was still 8x copy-paste, which is how we ended up with autotask-mcp silently shipping every release into an orphaned ACA for 3+ weeks). What this workflow does: - Logs into Azure via OIDC - Deploys the caller's image to gwp-<vendor-slug> by IMMUTABLE digest - Hard-fails if the digest input is empty or not sha256:-prefixed - Sets IMAGE_VERSION env var on the new revision tying it back to release version + git SHA + workflow run ID for forensics Failure patterns this fixes for the fleet: 1. Deploying to mcpgw-prod-<vendor> instead of gwp-<vendor>. The gateway routes vendor traffic per VENDOR_URL_<VENDOR> env, all of which point to gwp-<vendor>. The mcpgw-prod-* ACAs are orphaned legacy from a prior naming convention. Releases shipped to them are silent no-ops. 2. Deploying by :latest tag. :latest is mutable and can resolve to a stale digest through GHCR edge caches or ACA's image-pull cache, making deploys "succeed" while actually rolling onto the prior image (observed 2026-05-13 with autotask-mcp PRs #95 and #96). Caller contract (release.yml): - docker job must expose `outputs.digest: ${{ steps.<push-step>.outputs.digest }}` - deploy job replaces its inline body with `uses: wyre-technology/.github/.github/workflows/mcp-server-deploy.yml@<sha>` passing vendor-slug, image-name, digest, version. Secrets propagate via `secrets: inherit` or explicit forwarding. Will be rolled out to autotask-mcp first as the reference call site, then to halopsa-mcp, datto-rmm-mcp, itglue-mcp, huntress-mcp, liongard-mcp, domotz-mcp, cipp-mcp in follow-up PRs. See related skills: - ci-docker-semantic-release-version-race - mcp-gateway-troubleshooting
1 parent cd81e46 commit c2d6fc8

1 file changed

Lines changed: 107 additions & 0 deletions

File tree

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
name: MCP Server Deploy (reusable)
2+
3+
# Canonical deploy workflow for wyre-technology/*-mcp repositories.
4+
# Called from each per-repo `.github/workflows/release.yml` `deploy` job after
5+
# semantic-release + docker push have produced a versioned image and its digest.
6+
#
7+
# What this workflow does:
8+
# - Logs into Azure via OIDC
9+
# - Deploys the image to `gwp-<vendor-slug>` in `mcp-gateway-prod` by IMMUTABLE
10+
# digest (never :latest)
11+
# - Sets an `IMAGE_VERSION` env var on the revision tying it back to release
12+
# version + git SHA + workflow run ID
13+
#
14+
# Failure patterns this prevents:
15+
# - Deploying to mcpgw-prod-<vendor> (orphaned legacy ACAs from a prior naming
16+
# convention — the gateway routes vendor traffic to gwp-<vendor> per its
17+
# VENDOR_URL_<VENDOR> env). Releases shipped to mcpgw-prod-* are silent no-ops.
18+
# - Deploying by :latest tag, which can resolve to a stale digest through GHCR
19+
# edge caches or ACA's image-pull layer — the deploy "succeeds" but the new
20+
# revision actually rolls onto the prior image (observed in autotask-mcp on
21+
# 2026-05-13, broken for ~3 weeks).
22+
#
23+
# Inputs are passed through `env:` before any shell interpolation, so no
24+
# untrusted strings reach `run:` blocks (workflow_call inputs are typed and
25+
# come from sibling repo workflows we control, but defense-in-depth is cheap).
26+
27+
on:
28+
workflow_call:
29+
inputs:
30+
vendor-slug:
31+
description: 'Vendor slug. Drives the target ACA name `gwp-<slug>` and is the source of truth for where the gateway routes traffic.'
32+
required: true
33+
type: string
34+
image-name:
35+
description: 'Full GHCR image base path (no tag/digest), e.g. `ghcr.io/wyre-technology/autotask-mcp`.'
36+
required: true
37+
type: string
38+
digest:
39+
description: 'Image digest (e.g. `sha256:abc123...`) produced by the caller''s `docker/build-push-action` step. Pass via `${{ needs.docker.outputs.digest }}`.'
40+
required: true
41+
type: string
42+
version:
43+
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.'
44+
required: true
45+
type: string
46+
resource-group:
47+
description: 'Azure resource group containing the target ACA.'
48+
required: false
49+
type: string
50+
default: 'mcp-gateway-prod'
51+
environment:
52+
description: 'GitHub Environment to run in (for env-scoped secrets and deployment protection rules).'
53+
required: false
54+
type: string
55+
default: 'production'
56+
secrets:
57+
AZURE_CLIENT_ID:
58+
required: true
59+
AZURE_TENANT_ID:
60+
required: true
61+
AZURE_SUBSCRIPTION_ID:
62+
required: true
63+
64+
jobs:
65+
deploy:
66+
name: Deploy ${{ inputs.vendor-slug }} to gwp-${{ inputs.vendor-slug }}
67+
runs-on: ubuntu-latest
68+
environment: ${{ inputs.environment }}
69+
permissions:
70+
id-token: write
71+
contents: read
72+
env:
73+
TARGET_ACA: gwp-${{ inputs.vendor-slug }}
74+
RESOURCE_GROUP: ${{ inputs.resource-group }}
75+
IMAGE_NAME: ${{ inputs.image-name }}
76+
IMAGE_DIGEST: ${{ inputs.digest }}
77+
RELEASE_VERSION: ${{ inputs.version }}
78+
steps:
79+
- name: Azure login (OIDC)
80+
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
81+
with:
82+
client-id: ${{ secrets.AZURE_CLIENT_ID }}
83+
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
84+
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
85+
86+
- name: Deploy to Azure Container Apps
87+
run: |
88+
set -euo pipefail
89+
90+
if [ -z "${IMAGE_DIGEST}" ]; then
91+
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
92+
exit 1
93+
fi
94+
if [[ "${IMAGE_DIGEST}" != sha256:* ]]; then
95+
echo "::error::Input 'digest' must start with 'sha256:' (got '${IMAGE_DIGEST}'). Refusing to deploy a non-digest reference." >&2
96+
exit 1
97+
fi
98+
99+
SHORT_SHA="${GITHUB_SHA::7}"
100+
IMAGE="${IMAGE_NAME}@${IMAGE_DIGEST}"
101+
echo "Deploying ${IMAGE} to ${TARGET_ACA} (release v${RELEASE_VERSION}, sha ${SHORT_SHA}, run ${GITHUB_RUN_ID})"
102+
103+
az containerapp update \
104+
--name "${TARGET_ACA}" \
105+
--resource-group "${RESOURCE_GROUP}" \
106+
--image "${IMAGE}" \
107+
--set-env-vars "IMAGE_VERSION=v${RELEASE_VERSION}-sha-${SHORT_SHA}-${GITHUB_RUN_ID}"

0 commit comments

Comments
 (0)