Skip to main content
ship it and sleep

Cross-Repo Triggers, Versioning, and Deployment Coordination

4 min read Chapter 57 of 66

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

StrategyFormatWhen to Use
Git SHAabc123Automated deploys to staging
Semverv1.2.3Production releases with changelogs
Semver + SHAv1.2.3-abc123Traceability 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.