Chapter 29Lesson 02~250 minutes

Reusable Platform Pipelines, Golden Paths, and Organization-Wide Delivery Design: Guided Hands-On Workflow

Compose a disposable golden path, onboard two sample repositories, release v1 and a breaking v2, then perform a compatibility-safe migration with evidence.

Hands-onTwo callersv1 → v2.1CompatibilityExceptions

Learning objectives

  • Build a small golden path from a template plus reusable CI and deployment contracts.
  • Onboard two disposable callers without sharing real secrets or production runners.
  • Create immutable v1/v2 identities and record the caller→platform mapping.
  • Detect a breaking v2 contract before broad caller migration.
  • Release a compatibility-preserving revision and document one controlled exception.

1. Lab scenario and boundaries

Use three disposable repositories or three local Git repositories: delivery-platform, app-alpha and app-beta. The platform exposes one CI workflow and one deployment simulation. The two apps contain tiny Python tests. No package registry, cloud account, production environment, secret or self-hosted runner is needed.

The mandatory path is local and faithful: Git gives every platform release a real commit SHA, a contract test checks caller compatibility, and caller manifests record what they would pin on GitHub. If you want to execute the reusable workflows end to end, mirror the three disposable repositories to GitHub under an account/organization you control and substitute the real owner/repository plus the generated 40-character platform SHA.

2. Preflight: prove current state before creating anything

git --version
python --version
mkdir -p gha-platform-lab && cd gha-platform-lab
pwd
# The directory must contain no production checkout or credential file.
find . -maxdepth 2 -type f -print

For GitHub execution, the examples assume ubuntu-24.04, Python 3.13, checkout v7.0.1 at 3d3c42e5aac5ba805825da76410c181273ba90b1, setup-python v7.0.0 at 5fda3b95a4ea91299a34e894583c3862153e4b97 and upload-artifact v7.0.1 at 043fb46d1a93c77aae656e7c1c64a875d1fc6a0a. Re-check these assumptions when running the lab later.

3. Publish the v1 CI and deployment contracts

The CI workflow owns multi-job policy and evidence shape; callers choose only bounded inputs. Event-derived or caller-provided values enter shell through environment variables. The deployment workflow is intentionally a simulation: it validates an artifact digest and writes a deployment record, but it does not call a cloud or cluster API.

name: Platform CI v1
on:
  workflow_call:
    inputs:
      python-version:
        type: string
        default: "3.13"
        required: false
      working-directory:
        type: string
        default: "."
        required: false
    outputs:
      evidence-digest:
        value: ${{ jobs.ci.outputs.evidence-digest }}
permissions: {}
jobs:
  ci:
    runs-on: ubuntu-24.04
    permissions:
      contents: read
    outputs:
      evidence-digest: ${{ steps.evidence.outputs.digest }}
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
        with:
          python-version: ${{ inputs.python-version }}
      - name: Validate bounded working directory
        shell: bash
        env:
          WORKDIR: ${{ inputs.working-directory }}
        run: |
          set -euo pipefail
          case "$WORKDIR" in
            .|src|app) ;;
            *) echo "unsupported working-directory" >&2; exit 2 ;;
          esac
      - name: Run standardized unit suite
        shell: bash
        env:
          WORKDIR: ${{ inputs.working-directory }}
        run: python -m unittest discover -s "$WORKDIR/tests" -v
      - id: evidence
        shell: bash
        run: |
          set -euo pipefail
          printf '%s\n' "caller_sha=${GITHUB_SHA}" "run_id=${GITHUB_RUN_ID}" "attempt=${GITHUB_RUN_ATTEMPT}" > platform-ci-evidence.txt
          DIGEST="$(sha256sum platform-ci-evidence.txt | awk '{print $1}')"
          printf 'digest=%s\n' "$DIGEST" >> "$GITHUB_OUTPUT"
      - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          name: platform-ci-evidence-${{ github.run_id }}-${{ github.run_attempt }}
          path: platform-ci-evidence.txt
          retention-days: 7
name: Platform deploy simulation v1
on:
  workflow_call:
    inputs:
      service:
        type: string
        required: true
      environment-name:
        type: string
        required: true
      artifact-digest:
        type: string
        required: true
    outputs:
      deployment-record-digest:
        value: ${{ jobs.deploy.outputs.record-digest }}
permissions: {}
jobs:
  deploy:
    runs-on: ubuntu-24.04
    permissions: {}
    environment: ${{ inputs.environment-name }}
    outputs:
      record-digest: ${{ steps.record.outputs.digest }}
    steps:
      - name: Validate provider-neutral deployment contract
        shell: bash
        env:
          SERVICE: ${{ inputs.service }}
          ENVIRONMENT_NAME: ${{ inputs.environment-name }}
          ARTIFACT_DIGEST: ${{ inputs.artifact-digest }}
        run: |
          set -euo pipefail
          [[ "$SERVICE" =~ ^[a-z0-9-]{1,32}$ ]]
          case "$ENVIRONMENT_NAME" in lab|staging) ;; *) exit 2 ;; esac
          [[ "$ARTIFACT_DIGEST" =~ ^[0-9a-f]{64}$ ]]
      - id: record
        shell: bash
        env:
          SERVICE: ${{ inputs.service }}
          ENVIRONMENT_NAME: ${{ inputs.environment-name }}
          ARTIFACT_DIGEST: ${{ inputs.artifact-digest }}
        run: |
          set -euo pipefail
          printf '%s\n' "service=$SERVICE" "environment=$ENVIRONMENT_NAME" "artifact_digest=$ARTIFACT_DIGEST" "run_id=$GITHUB_RUN_ID" > deployment-record.txt
          DIGEST="$(sha256sum deployment-record.txt | awk '{print $1}')"
          printf 'digest=%s\n' "$DIGEST" >> "$GITHUB_OUTPUT"

Initialize the platform repository, commit these files under .github/workflows/ci.yml and deploy.yml, then create a release tag. The commit SHA—not the tag text—is what consumers will pin in the production pattern.

4. Capture the immutable v1 identity

cd delivery-platform
git init -b main
git add .
git -c user.name='Platform Lab' -c user.email='platform@example.invalid' commit -m 'platform v1'
git tag platform-v1
PLATFORM_V1_SHA="$(git rev-parse platform-v1^{commit})"
printf 'platform-v1=%s
' "$PLATFORM_V1_SHA" | tee release-map.txt

Do not invent a SHA in the caller. Copy the value printed by git rev-parse. If the repositories are mirrored to GitHub, push the exact commit first and verify the remote object resolves to the same SHA before using it in uses:.

5. The template is a thin caller, not the platform implementation

The organization template should remain small enough that a repository owner can understand its triggers, permissions and platform pins during review. Replace PLATFORM_V1_COMMIT_SHA below with the 40-character SHA produced in the previous step before using it on GitHub.

name: Golden path
on:
  pull_request:
  push:
    branches: [main]
permissions: {}
jobs:
  ci:
    uses: acme-lab/delivery-platform/.github/workflows/ci.yml@PLATFORM_V1_COMMIT_SHA
    with:
      python-version: "3.13"
      working-directory: "."
    permissions:
      contents: read
  deploy-lab:
    if: ${{ github.event_name == 'push' }}
    needs: ci
    uses: acme-lab/delivery-platform/.github/workflows/deploy.yml@PLATFORM_V1_COMMIT_SHA
    with:
      service: sample-app
      environment-name: lab
      artifact-digest: ${{ needs.ci.outputs.evidence-digest }}
    permissions: {}

Because workflow templates are copied, the consumer owns this file after onboarding. Updating the organization template later does not mutate existing caller repositories. That is why the upgrade channel must open an explicit change to the caller pin.

6. Onboard app-alpha and app-beta

for app in app-alpha app-beta; do
  mkdir -p "$app/tests" "$app/.github/workflows"
  cat > "$app/tests/test_smoke.py" <<'PYTEST'
import unittest
class Smoke(unittest.TestCase):
    def test_platform_contract(self):
        self.assertEqual(2 + 2, 4)
PYTEST
  (cd "$app" && git init -b main && git add . &&     git -c user.name='Caller Lab' -c user.email='caller@example.invalid' commit -m 'app baseline')
done

For the local path, store a platform-lock.json in each app with the v1 SHA and expected workflow inputs/outputs. For a GitHub mirror, create the caller YAML from the template and replace the synthetic owner plus v1 SHA with your disposable platform repository identity.

7. Add machine-checkable caller contracts

{
  "workflows": {
    "ci": {
      "inputs": ["python-version", "working-directory"],
      "outputs": ["evidence-digest"]
    },
    "deploy": {
      "inputs": ["service", "environment-name", "artifact-digest"],
      "outputs": ["deployment-record-digest"]
    }
  }
}
import json, pathlib, sys
platform = json.loads(pathlib.Path(sys.argv[1]).read_text())
caller = json.loads(pathlib.Path(sys.argv[2]).read_text())
errors=[]
for wf, contract in caller["workflows"].items():
    published=platform["workflows"].get(wf)
    if not published:
        errors.append(f"missing workflow: {wf}")
        continue
    for name in contract.get("inputs", []):
        if name not in published.get("inputs", []): errors.append(f"{wf}: missing input {name}")
    for name in contract.get("outputs", []):
        if name not in published.get("outputs", []): errors.append(f"{wf}: missing output {name}")
print(json.dumps({"compatible": not errors, "errors": errors}, indent=2))
sys.exit(1 if errors else 0)

This compatibility test is intentionally small. It does not prove workflow semantics, but it gives the platform a contract-level guard against removing an input/output that an existing caller still consumes. Production platform tests should add fixture repositories and end-to-end runs for important behavior.

8. Create a deliberately breaking v2 and preserve its failure

In v2, rename evidence-digest to evidence-sha256 without retaining the old output. Update contracts/platform.json, commit and tag it platform-v2-broken. Do not migrate callers yet.

{
  "workflows": {
    "ci": {
      "inputs": ["python-version", "working-directory"],
      "outputs": ["evidence-sha256"]
    },
    "deploy": {
      "inputs": ["service", "environment-name", "artifact-digest"],
      "outputs": ["deployment-record-digest"]
    }
  }
}
python delivery-platform/tools/check_contract.py delivery-platform/contracts/platform-v2.json app-alpha/platform-contract.json   | tee app-alpha-v2-compatibility.json
# Expected exit != 0 and error: ci: missing output evidence-digest

Preserve the failing compatibility output and the v2 SHA. This is platform first-failure evidence: the release exists, but the rollout is blocked before either caller pin changes.

9. Repair with a compatibility-preserving v2.1

The least destructive repair is additive. Keep evidence-digest and add evidence-sha256 as an alias or new output. New callers can adopt the clearer name later while v1 callers continue to function. Commit this as platform-v2.1 and run the compatibility test against both apps.

for app in app-alpha app-beta; do
  python delivery-platform/tools/check_contract.py     delivery-platform/contracts/platform-v2.1.json     "$app/platform-contract.json"     | tee "$app-v2.1-compatibility.json"
done
# Both must report compatible:true before rollout.

10. Controlled migration and exception record

Migrate app-alpha to the v2.1 commit SHA through a normal review. Leave app-beta on v1 for one cycle to model a repository with a release freeze. The exception must be explicit rather than inferred from an old pin.

{
  "repository": "app-beta",
  "current_platform": "platform-v1",
  "target_platform": "platform-v2.1",
  "reason": "release freeze for lab scenario",
  "owner": "app-beta-maintainer",
  "approved_by": "platform-lab-owner",
  "expires": "2026-10-10",
  "compensating_control": "v1 remains supported and its security updates are backported"
}

11. Evidence and rollback

Evidence Expected value
Platform releases v1 SHA, broken-v2 SHA, v2.1 SHA + release labels.
Caller lock alpha → v2.1 SHA; beta → v1 SHA under exception.
Compatibility broken v2 fails; v2.1 passes both caller contracts.
Permissions CI contents:read; deploy simulation no write token.
Runner ubuntu-24.04 in GitHub path; local simulation records local tool versions.
Artifacts CI evidence artifact ID/digest only when GitHub path is run.
Rollback previous v1 SHA retained; caller can revert the pin without rebuilding platform code.

If app-alpha later fails under v2.1, preserve the caller run ID/attempt and platform SHA, then revert only the caller pin to v1. That is a rollback of executable platform dependency state, not a rebuild of the failed application artifact.

12. Challenge: choose the correct layer

A team asks for a custom GPU runner, a different deployment approval rule and one additional CI report. Decide which request belongs in the reusable workflow contract, which belongs in runner-group/environment governance, and which should be an exception instead of a new global knob. Your answer must name the state changed and the evidence that would prove the change.

13. Guided workflow summary

You now have a small platform product: thin onboarding scaffolding, immutable reusable-workflow releases, two callers, a compatibility gate, a safe additive upgrade path and a time-bounded exception. The next lesson turns those implementation choices into explicit architecture trade-offs.

Next lesson

Reusable Platform Pipelines, Golden Paths, and Organization-Wide Delivery Design: Configuration, Design Patterns, and Trade-Offs

Continue with the next lesson to build on the current concepts, evidence, security boundaries, and operational practices.

Knowledge check

Why did the lab record the v1 commit SHA separately from the tag?

Why should a breaking v2 be tested before changing caller pins?

What is the least destructive fix for renaming an output used by existing callers?

Why is app-beta staying on v1 not automatically a governance failure?

A team needs a trusted GPU runner. Should that be a free-form workflow input?

Official references and version notes

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.