Cross-Repo Triggers, Versioning, and Deployment Coordination
Cross-Repo Triggers, Versioning, and Deployment Coordination
The Failure
The checkout service introduced a new field in its API response. The frontend-shell expected the new field. Both services were deployed independently. The frontend deployed first. For 12 minutes, the frontend tried to render data from a field that the checkout service did not yet return. Users saw blank product prices.
Coordinated deployments ensure dependent services deploy in the correct order.
The Mechanism
Deployment Ordering
1. Database migrations (if any)
2. Backend services (upstream first)
a. catalog-service (no deps)
b. inventory-service (no deps)
c. checkout-service (depends on inventory)
d. payments-service (depends on checkout)
3. Frontend services
e. frontend-shell (depends on all backends)
Versioning Strategy
| Strategy | Format | When to Use |
|---|---|---|
| Git SHA | abc123 | Automated deploys to staging |
| Semver | v1.2.3 | Production releases with changelogs |
| Semver + SHA | v1.2.3-abc123 | Traceability in all environments |
The Implementation
Repository Dispatch with Dependencies
# checkout-service/.github/workflows/ci.yml
# HARDENED: Include dependency info in dispatch payload
- name: Trigger deploy
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.INFRA_REPO_TOKEN }}
repository: acme/ecommerce-infra
event-type: deploy-service
client-payload: |
{
"service": "checkout-service",
"image": "ghcr.io/acme/checkout-service",
"tag": "${{ github.sha }}",
"version": "${{ steps.version.outputs.semver }}",
"environment": "staging",
"requires": ["inventory-service"],
"requiredBy": ["payments-service", "frontend-shell"]
}
Coordinated Deployment Workflow
# ecommerce-infra/.github/workflows/coordinated-deploy.yml
# HARDENED: Deploy services in dependency order
name: Coordinated Deploy
on:
workflow_dispatch:
inputs:
services:
description: "Comma-separated services to deploy"
required: true
environment:
description: "Target environment"
required: true
type: choice
options: [staging, production]
jobs:
plan:
runs-on: ubuntu-latest
outputs:
order: ${{ steps.order.outputs.result }}
steps:
- uses: actions/checkout@v4
- name: Compute deployment order
id: order
uses: actions/github-script@v7
with:
script: |
const deps = {
'catalog-service': [],
'inventory-service': [],
'checkout-service': ['inventory-service'],
'payments-service': ['checkout-service'],
'frontend-shell': ['catalog-service', 'checkout-service', 'payments-service'],
};
const requested = '${{ inputs.services }}'.split(',').map(s => s.trim());
// Topological sort
const order = [];
const visited = new Set();
function visit(svc) {
if (visited.has(svc)) return;
visited.add(svc);
for (const dep of (deps[svc] || [])) {
if (requested.includes(dep)) visit(dep);
}
order.push(svc);
}
requested.forEach(visit);
return JSON.stringify(order);
deploy:
needs: plan
runs-on: ubuntu-latest
strategy:
max-parallel: 1
matrix:
service: ${{ fromJson(needs.plan.outputs.order) }}
steps:
- uses: actions/checkout@v4
- name: Deploy ${{ matrix.service }}
run: |
echo "Deploying ${{ matrix.service }} to ${{ inputs.environment }}"
cd apps/${{ matrix.service }}/overlays/${{ inputs.environment }}
# ArgoCD sync
argocd app sync ${{ matrix.service }}-${{ inputs.environment }} --wait
- name: Verify health
run: |
argocd app wait ${{ matrix.service }}-${{ inputs.environment }} \
--health --timeout 300
Version Tracking
# ecommerce-infra/versions.yaml
# HARDENED: Track deployed versions per environment
staging:
catalog-service: "v2.1.0-abc123"
inventory-service: "v1.8.3-def456"
checkout-service: "v3.0.1-ghi789"
payments-service: "v2.5.0-jkl012"
frontend-shell: "v4.2.1-mno345"
production:
catalog-service: "v2.1.0-abc123"
inventory-service: "v1.8.2-xyz999"
checkout-service: "v3.0.0-aaa111"
payments-service: "v2.5.0-jkl012"
frontend-shell: "v4.2.0-bbb222"
Contract Testing Between Services
# checkout-service/.github/workflows/contract.yml
# HARDENED: Verify API contracts before deploy
name: Contract Tests
on:
pull_request:
branches: [main]
jobs:
verify-contracts:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run contract tests
run: |
# Verify checkout-service fulfills its provider contracts
go test ./contracts/... -tags=contract
- name: Publish pact
run: |
pact-broker publish pacts/ \
--consumer-app-version ${{ github.sha }} \
--broker-base-url ${{ secrets.PACT_BROKER_URL }}
The Gate
Contract tests are the gate for API-breaking changes. If checkout changes its response format, the contract test with frontend-shell fails before the change merges. No broken APIs reach production.
The Recovery
Deployment order causes cascading timeout: If the first service in the chain takes too long to become healthy, all subsequent deployments queue. Set per-service timeouts and fail fast.
Circular dependency detected: The topological sort will loop infinitely. Add cycle detection. If services have circular dependencies, they need simultaneous deployment (blue-green at the service-mesh level).
Version file conflicts: Multiple deployments update versions.yaml simultaneously. Use atomic commits with git pull --rebase retry logic.