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.
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.
Knowledge check
Why did the lab record the v1 commit SHA separately from the tag?
The SHA is the immutable executable identity callers can pin; the tag is a human release label/mapping.
Why should a breaking v2 be tested before changing caller pins?
It bounds blast radius and preserves compatibility evidence before production caller state changes.
What is the least destructive fix for renaming an output used by existing callers?
Keep the old output for compatibility and add the new output, then deprecate/migrate deliberately.
Why is app-beta staying on v1 not automatically a governance failure?
A bounded, owned and expiring exception can be safer than forcing an upgrade during a freeze, provided v1 remains supported or compensating controls exist.
A team needs a trusted GPU runner. Should that be a free-form workflow input?
Usually no. Runner access is a trust/capacity governance boundary; use controlled runner groups/labels and explicit repository access rather than an arbitrary caller string.
Official references and version notes
- Reuse workflows — Current workflow_call contract, nested workflows, secret propagation and workflow-use monitoring.
- Reusing workflow configurations — Current access rules, limits, runner semantics, rerun behavior, templates and YAML reuse.
- Create workflow templates — Organization .github/workflow-templates structure and template metadata.
- Share actions and workflows with your organization — Private shared automation access and the temporary scoped download token model.
- Managing Actions settings for a repository — Repository access to shared actions/workflows and policy inheritance.
- Runner groups — Runner-group access as a security/capacity boundary.
- Choosing the runner for a job — Routing jobs to runner groups and labels.
- Enterprise Actions policies — Allow-listing actions/workflows and full-SHA action pinning policy.
- Available rules for rulesets — Ruleset workflow enforcement, status checks and plan/visibility boundaries.
- Reviewing the organization audit log — Audit data used for governance and adoption analysis where available.
- actions/checkout v7.0.1 — Pinned checkout used by executable workflow examples.
- actions/setup-python v7.0.0 — Pinned Python setup used by executable workflow examples.
- actions/upload-artifact v7.0.1 — Pinned evidence upload used by executable workflow examples.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.