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.
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
distfor 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;
inheritrequires 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: writeexisted 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?
The token was authenticated but had only the permissions granted
by the caller job; issue creation required
issues: write.
Why was changing only the caller permission the correct repair?
The called workflow behavior matched the requested probe; the contract failure was insufficient caller authorization. Fixing the callee or adding a PAT would solve the wrong layer.
Why restore issues: read after the successful
probe?
Least privilege is the normal operating state. Temporary capability used for a test should not become permanent authority.
What should a production policy require before updating a third-party action pin?
Resolve/review the new canonical commit and release/source/dependency changes, test it, and merge the exact SHA through normal review.
What is the most important difference between a reusable workflow and a composite action in the checkpoint?
The reusable workflow owns the job-level orchestration and caller permission boundary; the composite action is only a step bundle inside that called job.
What does Chapter 19 add next?
Deployment environments, protection/approval rules, concurrency, and rollback—privileged controls that build on the version and permission contracts established here.
Further reading — current official GitHub sources
- GitHub Docs — Reuse workflows
- GitHub Docs — Reusing workflow configurations
- GitHub Docs — Workflow syntax for reusable workflows
- GitHub Docs — About custom actions
- GitHub Docs — Metadata syntax for actions
- GitHub Docs — Creating a composite action
- GitHub Docs — Creating a JavaScript action
- GitHub Docs — Managing custom actions
- GitHub Docs — Releasing and maintaining actions
- GitHub Docs — Secure use reference
- GitHub Docs — Sharing actions/workflows with an organization
- GitHub Docs — Sharing across private repositories
- GitHub REST — Actions permissions
- GitHub CLI — gh workflow run
- GitHub CLI — gh run view
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.