Chapter 18Lesson 05~230 minutes

Checkpoint Lab — Reusable Workflows, Composite Actions, JavaScript Actions, and Automation Reuse

The checkpoint turns reuse into an operating contract. You will predict how a caller, reusable workflow, and composite action exchange inputs, outputs, permissions, and source identity; verify a healthy run; deliberately violate the permission contract; preserve the 403 evidence; recover with a temporary least-required grant; then restore the normal read-only policy and document how future shared automation is versioned and reviewed.

Checkpoint labPermission contractVersioning policySafe reuse

Learning objectives

  • Run a complete caller → reusable workflow → composite action contract in a disposable repository and verify exact source/output identity.
  • Predict and verify how job-level permissions flow into the called workflow.
  • Preserve an intentional permission failure, repair only the caller capability, verify the mutation, and restore least privilege.
  • Create a production versioning/review policy for reusable workflows, composite actions, JavaScript actions, and third-party dependencies.
  • Close Chapter 18 with an auditable reuse operating model and hand off to environments/deployment protection in Chapter 19.

Checkpoint assumptions: GitHub.com, GitHub Free, one disposable public personal repository, standard GitHub-hosted Ubuntu runner, repository owner/write access, GitHub CLI authenticated. No real secret, PAT, organization, Marketplace purchase, external cloud, private shared component, or self-hosted runner is required.

1. Setup and preflight

Continue from c18-reuse-lab. If you are recreating the lab, use the exact composite/reusable/caller files from Lesson 2 with issues: read in the caller. Before any run, inspect the repository and confirm no secrets are present.

gh auth status
REPO="$(gh repo view --json nameWithOwner --jq .nameWithOwner)"
DEFAULT_BRANCH="$(gh repo view "$REPO" --json defaultBranchRef --jq .defaultBranchRef.name)"

gh repo view "$REPO" --json nameWithOwner,visibility,defaultBranchRef,isArchived,url
gh secret list -R "$REPO" --json name --jq 'length'
gh workflow list -R "$REPO" --all

git fetch origin
git switch "$DEFAULT_BRANCH"
git pull --ff-only
BASE_SHA="$(git rev-parse HEAD)"
printf 'base_sha=%s\n' "$BASE_SHA"

If the secret count is nonzero because you reused another repository, stop and create a fresh disposable repository. This checkpoint needs no secrets.

2. Write predictions before execution

Record at least these predictions in C18_PREDICTIONS.md:

Prediction Expected change Independent evidence
P1 — healthy call Caller creates a run; reusable workflow executes at the same caller source SHA; composite action returns payments-api Run head SHA + logs + downstream output
P2 — permission probe with read Issue POST fails; no issue is created; downstream job is skipped because called workflow failed 403 in log + issue search + job graph
P3 — temporary write repair Changing only caller call-job permission to issues: write allows one probe issue Git diff + successful run + issue API/list
P4 — rollback Closing probe issue and restoring issues: read returns normal policy to read-only Caller file at final SHA + issue state + final normal run

Also predict what will not change: no PAT is created, no repository setting is bypassed, no tag is force-moved, and the reusable workflow file itself does not need privilege edits.

3. Healthy run — verify the interface before breaking it

gh workflow run c18-caller.yml -R "$REPO" --ref "$DEFAULT_BRANCH" \
  -f component='Payments API' \
  -f permission_probe=false
sleep 3
HEALTHY="$(gh run list -R "$REPO" --workflow c18-caller.yml \
  --event workflow_dispatch --limit 1 --json databaseId --jq '.[0].databaseId')"
gh run watch "$HEALTHY" -R "$REPO" --exit-status

gh run view "$HEALTHY" -R "$REPO" \
  --json headSha,status,conclusion,jobs,url
gh run view "$HEALTHY" -R "$REPO" --log

Verify P1: normalized=payments-api and a report containing the source SHA. Confirm the called job used the local composite action. Because the probe was false, no issue mutation should occur.

4. Intentionally break the permission contract — preserve P2 evidence

gh workflow run c18-caller.yml -R "$REPO" --ref "$DEFAULT_BRANCH" \
  -f component='Payments API' \
  -f permission_probe=true
sleep 3
BROKEN="$(gh run list -R "$REPO" --workflow c18-caller.yml \
  --event workflow_dispatch --limit 1 --json databaseId --jq '.[0].databaseId')"

# Failure is expected. Keep the non-zero result visible without aborting the lab.
gh run watch "$BROKEN" -R "$REPO" || true
gh run view "$BROKEN" -R "$REPO" \
  --json headSha,status,conclusion,jobs,url
gh run view "$BROKEN" -R "$REPO" --log

gh issue list -R "$REPO" --state all --search '"Chapter 18 permission probe" in:title' \
  --json number,title,state,url

Verify P2: the issue creation request is rejected because the caller granted only read access. No probe issue should exist. Preserve the failed run ID and its log; do not “fix” it by suppressing the error.

5. Prove exactly which caller and callee definitions failed

FAILED_SHA="$(gh run view "$BROKEN" -R "$REPO" --json headSha --jq .headSha)"
printf 'failed_sha=%s\n' "$FAILED_SHA"

for path in \
  .github/workflows/c18-caller.yml \
  .github/workflows/c18-reusable.yml \
  .github/actions/c18-normalize/action.yml
do
  gh api -H "X-GitHub-Api-Version: 2026-03-10" \
    "repos/$REPO/contents/$path?ref=$FAILED_SHA" \
    --jq '{path,sha,size}'
done

gh api -H "X-GitHub-Api-Version: 2026-03-10" \
  "repos/$REPO/actions/runs/$BROKEN/jobs?per_page=100" \
  --jq '.jobs[] | {id,name,status,conclusion,steps:[.steps[]|{name,conclusion}]}'

This step separates evidence from memory. The reusable workflow did what its input requested; the caller intentionally supplied insufficient authorization. The correction belongs at the caller permission contract.

6. Least-destructive repair — temporarily grant exactly issue write

Edit only .github/workflows/c18-caller.yml: change the call job from issues: read to issues: write. Do not change contents: read, do not add a PAT, and do not modify the reusable workflow.

python - <<'PYEDIT'
from pathlib import Path
p=Path('.github/workflows/c18-caller.yml')
s=p.read_text()
old='      issues: read\n'
new='      issues: write\n'
if s.count(old) != 1:
    raise SystemExit('Expected exactly one issues: read permission')
p.write_text(s.replace(old,new,1))
PYEDIT

git diff --check
git diff -- .github/workflows/c18-caller.yml
git add .github/workflows/c18-caller.yml
git commit -m "test: temporarily allow Chapter 18 permission probe"
git push origin "$DEFAULT_BRANCH"
WRITE_SHA="$(git rev-parse HEAD)"

This is a security-sensitive workflow-permission change, but it is scoped to one disposable repository and one explicit resource permission. Review the diff before pushing.

7. Re-run the probe and verify P3 independently

gh workflow run c18-caller.yml -R "$REPO" --ref "$DEFAULT_BRANCH" \
  -f component='Payments API' \
  -f permission_probe=true
sleep 3
RECOVERED="$(gh run list -R "$REPO" --workflow c18-caller.yml \
  --event workflow_dispatch --limit 1 --json databaseId --jq '.[0].databaseId')"
gh run watch "$RECOVERED" -R "$REPO" --exit-status

gh run view "$RECOVERED" -R "$REPO" --log
PROBE_NUMBER="$(gh issue list -R "$REPO" --state open \
  --search '"Chapter 18 permission probe" in:title' --limit 1 \
  --json number --jq '.[0].number')"
gh issue view "$PROBE_NUMBER" -R "$REPO" --json number,title,state,author,url

The successful issue creation proves P3. The called workflow did not change; only the caller’s capability grant changed. This is the practical meaning of caller-controlled permission ceilings.

8. Roll back the temporary capability and close the probe issue

gh issue close "$PROBE_NUMBER" -R "$REPO" \
  --comment 'Closing disposable Chapter 18 permission-contract probe.'

python - <<'PYEDIT'
from pathlib import Path
p=Path('.github/workflows/c18-caller.yml')
s=p.read_text()
old='      issues: write\n'
new='      issues: read\n'
if s.count(old) != 1:
    raise SystemExit('Expected exactly one issues: write permission')
p.write_text(s.replace(old,new,1))
PYEDIT

git diff --check
git diff -- .github/workflows/c18-caller.yml
git add .github/workflows/c18-caller.yml
git commit -m "test: restore read-only Chapter 18 permission contract"
git push origin "$DEFAULT_BRANCH"
FINAL_SHA="$(git rev-parse HEAD)"

gh issue view "$PROBE_NUMBER" -R "$REPO" --json number,title,state,url
printf 'final_sha=%s\n' "$FINAL_SHA"

Verify P4: the issue is closed and the normal caller is read-only again. The temporary write-capability commit remains in Git history, which is useful audit evidence of the controlled exercise.

9. Final normal run proves cleanup did not break reuse

gh workflow run c18-caller.yml -R "$REPO" --ref "$DEFAULT_BRANCH" \
  -f component='Payments API' \
  -f permission_probe=false
sleep 3
FINAL_RUN="$(gh run list -R "$REPO" --workflow c18-caller.yml \
  --event workflow_dispatch --limit 1 --json databaseId --jq '.[0].databaseId')"
gh run watch "$FINAL_RUN" -R "$REPO" --exit-status
gh run view "$FINAL_RUN" -R "$REPO" --json headSha,conclusion,jobs,url

The final run should succeed at FINAL_SHA with the reusable output contract intact and no issue mutation.

10. Write the shared-automation versioning and review policy

Create SHARED_AUTOMATION_POLICY.md that answers:

  • Classification: which logic belongs in reusable workflows, composite actions, JavaScript actions, or caller workflows.
  • Interfaces: input/output types, required permissions, named secret/capability list, runner/network assumptions, compatibility guarantees.
  • References: full SHA required for external actions; cross-repository reusable workflow pin strategy; repository-local components version with caller commit.
  • Review: CODEOWNERS, source/release diff, dependency lockfiles, generated dist for JS actions, permission changes, untrusted-input handling.
  • Release: immutable commit identity, release notes, compatibility/deprecation window, canary callers, rollback pin.
  • Inventory: owner and consumer list for every central component/version.
  • Secrets: named passing by default; inherit requires explicit exception; prefer OIDC/short-lived capabilities when relevant.
  • Incident response: freeze/move callers to known-good pins; rotate exposed credentials first; preserve affected run/source identities.

11. Reuse decision matrix for production review

Need Primitive Version model Primary control
Multi-job CI policy Reusable workflow Cross-repo full SHA or controlled internal release Caller permission ceiling + stable typed interface
Repeated shell/tool recipe Composite action Local caller commit or pinned action repo SHA Safe input handling + small step scope
Complex portable program logic JavaScript action Pinned action repo SHA/release Bundled dependency review + tests
External Marketplace capability Third-party action Reviewed full SHA Canonical source + permission/network review + update PRs

12. Final verification checklist

  • Healthy and final runs completed at known caller SHAs.
  • Composite action output propagated through job/workflow output mappings.
  • Broken run retained the original 403 permission evidence.
  • No PAT or real secret was created, passed, or logged.
  • Temporary issues: write existed only in the disposable caller and was restored to read.
  • Probe issue is closed; repository content remains intact.
  • Reusable workflow did not gain permission by editing itself.
  • Third-party action review used tag → full SHA resolution without executing it.
  • Policy documents versioning, owners, permissions, secrets, generated bundles, consumer inventory, canaries, and rollback.

13. Cleanup / rollback

Disable the caller workflow and archive the disposable repository. Archiving preserves the healthy, failed, repaired, and rollback evidence while preventing casual mutation.

gh workflow disable c18-caller.yml -R "$REPO"
gh workflow list -R "$REPO" --all --json name,path,state

gh repo archive "$REPO" --yes
gh repo view "$REPO" --json nameWithOwner,isArchived,url

Destructive cleanup not required: Permanent repository deletion, force-moving tags/branches, deleting run history, and token revocation are unnecessary because the lab created no external credential and can be preserved safely as an archived disposable repository.

14. What Chapter 18 adds to the production GitHub operating model

Chapter 18 adds the automation dependency plane. Workflows are no longer isolated files: callers consume reusable workflows and actions with version identities, typed interfaces, permission ceilings, capability inputs, release processes, and consumer blast radius. You can now explain which component owns a failed job, which revision executed, what authority it had, and how to roll it back without broadening trust.

Chapter 19 applies this model to deployments. Environments, approvals, concurrency, protection rules, and rollback introduce another privileged boundary; reusable deployment workflows are safe only when the permission and version contracts established here remain explicit.

15. Checkpoint summary

You factored duplicated automation into a reusable workflow and composite action, verified the output path, deliberately exercised insufficient authorization, preserved and explained the 403, repaired only the caller capability, restored read-only policy, and wrote a release/review policy. Reuse is now governed internal platform code rather than hidden YAML inheritance.

Knowledge check

Why did the broken permission run fail even though the reusable workflow received github.token?

Why was changing only the caller permission the correct repair?

Why restore issues: read after the successful probe?

What should a production policy require before updating a third-party action pin?

What is the most important difference between a reusable workflow and a composite action in the checkpoint?

What does Chapter 19 add next?

Next lesson

Next: Environments, Deployment Protection Rules, Approvals, Concurrency, and Rollbacks: Concepts, Architecture, and Mental Model

Further reading — current official GitHub sources

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
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.