Checkpoint Lab — Composite Actions, JavaScript Actions, Docker Actions, and Custom Automation
The checkpoint implements one local composite action as a documented string contract, deliberately sends an unsupported value so validation fails, preserves that run, repairs only the caller input, and produces a release plan that maps a human version to the exact commit SHA callers should pin.
Learning objectives
- Implement a local composite action with documented enum/integer-like inputs and non-sensitive outputs.
- Run a failing Revision A and preserve the action validation evidence before any repair.
- Repair only the incompatible caller input and verify Revision B independently.
- Record action metadata, runner context, action/workflow SHA and release-provenance mapping.
- Produce a caller strategy that uses a full immutable commit SHA for external consumption.
1. Checkpoint scenario and safety boundary
Use a throwaway repository named gha-ch17-checkpoint.
The lab creates no secret, package, release object, registry image,
environment, cloud resource or self-hosted runner. It uses one local
composite action and one SHA-pinned checkout action on
ubuntu-24.04.
The “typed” contract is intentionally implemented as documented
string semantics because custom-action metadata does not support
workflow_call-style type declarations.
mode is an enum string and repeat is an
integer-like string validated before use.
2. Preflight and predictions
- Confirm the repository is disposable and contains no proprietary source.
-
Confirm Actions is enabled and
ubuntu-24.04is available. -
Record the exact commit containing
action.ymland the workflow before each run. - Predict Revision A: checkout succeeds, local action starts, mode validation exits 64, output verification never runs.
-
Predict Revision B: the same action implementation accepts
strict, returns two outputs and produces no external side effect.
3. The action contract
# .github/actions/ch17-contract/action.yml
name: Chapter 17 checkpoint contract
description: Validate caller strings and return a non-sensitive summary
inputs:
mode:
description: "Enum string: strict or relaxed"
required: true
repeat:
description: "Integer string from 1 through 3"
required: false
default: "1"
outputs:
summary:
description: Normalized summary
value: ${{ steps.validate.outputs.summary }}
contract_kind:
description: Fixed classification
value: ${{ steps.validate.outputs.contract_kind }}
runs:
using: composite
steps:
- id: validate
shell: bash
env:
MODE: ${{ inputs.mode }}
REPEAT: ${{ inputs.repeat }}
run: |
set -euo pipefail
case "$MODE" in
strict|relaxed) ;;
*) echo "unsupported mode" >&2; exit 64 ;;
esac
[[ "$REPEAT" =~ ^[1-3]$ ]] || {
echo "repeat must be 1, 2, or 3" >&2
exit 65
}
printf 'summary=mode-%s-repeat-%s
' "$MODE" "$REPEAT" >> "$GITHUB_OUTPUT"
printf 'contract_kind=validated-string-contract
' >> "$GITHUB_OUTPUT"
4. Workflow Revision A — preserve a real validation failure
# .github/workflows/ch17-checkpoint.yml — Revision A
name: chapter17-checkpoint
on: workflow_dispatch
permissions: {}
jobs:
validate:
runs-on: ubuntu-24.04
steps:
- name: Record bounded run evidence
shell: bash
run: |
echo "repository=$GITHUB_REPOSITORY"
echo "sha=$GITHUB_SHA"
echo "run=$GITHUB_RUN_ID attempt=$GITHUB_RUN_ATTEMPT"
echo "runner_os=$RUNNER_OS arch=$RUNNER_ARCH"
- name: Checkout exact caller revision
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- id: contract
name: Execute contract
uses: ./.github/actions/ch17-contract
with:
mode: unsupported
repeat: '2'
- name: Verify outputs
shell: bash
env:
SUMMARY: ${{ steps.contract.outputs.summary }}
run: test "$SUMMARY" = "mode-strict-repeat-2"
Commit Revision A and dispatch it. Preserve the run ID, attempt and
source SHA. The expected first failure is the
Execute contract step with exit 64 and the generic
unsupported mode message. The verification step is
skipped because the prior step failed.
5. Interpret before repairing
| Observed state | Expected Revision A evidence | Meaning |
|---|---|---|
| Workflow/run | manual run exists at recorded SHA | workflow syntax and trigger were valid |
| Checkout | success at pinned v7.0.1 action SHA | repository-local action files are present |
| Action metadata | runs.using: composite |
correct execution model selected |
| Validation | exit 64, unsupported mode | caller value violates documented enum contract |
| Outputs | not consumed | failed action did not produce a valid contract result |
| External state | none | failure is bounded to the run/workspace |
Do not modify action code, runner, permissions or checkout when this evidence matches the prediction. Those layers are healthy.
6. Workflow Revision B — least destructive repair
# Change exactly one caller value.
with:
mode: strict
repeat: '2'
Commit only that change and dispatch a new run. Expected output is
mode-strict-repeat-2. Record the new source SHA and
compare it with Revision A. The action metadata can be byte-for-byte
identical across the two commits if only the workflow input changed.
7. Build an immutable release strategy without publishing anything
After Revision B succeeds, identify the reviewed commit that
contains the action implementation you would release. Record a human
version such as v1.0.0 and map it to the exact
40-character commit SHA. In a real action repository, you may
publish that tag/release, but mandatory completion stops at the
release plan.
Release record
name: v1.0.0
action path: .github/actions/ch17-contract
source commit: <REVIEWED_40_CHARACTER_SHA>
runtime: composite
required permissions: none
supported runner baseline: caller must provide Bash; lab verified ubuntu-24.04
inputs: mode=strict|relaxed; repeat=1|2|3
outputs: summary; contract_kind
external side effects: none
# Production caller pattern after release — pin the exact recorded commit.
- uses: OWNER/REPO/.github/actions/ch17-contract@REVIEWED_40_CHARACTER_SHA
with:
mode: strict
repeat: '2'
A release tag can point reviewers to documentation, but callers that require immutable supply-chain identity should execute the full SHA.
8. Required evidence packet
| Evidence field | Record |
|---|---|
| Event/revision | workflow_dispatch; Revision A and B source SHAs |
| Run identity | run ID and attempt for broken and repaired runs |
| Action identity | local path plus exact source commit containing action.yml |
| Metadata |
input/output names and runs.using: composite
|
| Runtime/tooling | ubuntu-24.04; Bash; checkout v7.0.1 at pinned SHA |
| Permissions |
permissions: {}; no secret or token capability
required
|
| First failure | Execute contract; exit 64; unsupported mode |
| Repair | caller changes only mode unsupported → strict |
| Outputs | mode-strict-repeat-2 and validated-string-contract |
| Release provenance | v1.0.0 → reviewed 40-character action commit SHA |
| External state | none |
| Limitations | composite relies on documented Bash-compatible runner; JS/Docker alternatives were design comparisons only |
9. Verification checklist
- ZIP/course example contains no real token, key or registry credential.
- Revision A exists as an independent failed run and was not overwritten.
- The first failure is action input validation, not checkout/path/syntax.
- Revision B changes only the caller mode value and succeeds.
- Output values are non-sensitive and deterministic.
-
No
write-all, secret dump,pull_request_target, Docker socket or privileged execution is used. - Release plan records both human version and full immutable source SHA.
-
External callers are instructed to pin the full SHA, not
@mainor only@v1.
10. Cleanup and rollback
There is no external infrastructure to roll back. Preserve non-sensitive screenshots/run IDs if desired, then delete the throwaway repository. If you optionally created a real release/tag for practice, remove only the exact disposable release/tag that you created after recording its SHA mapping.
11. What Chapter 17 adds to the operating model
You can now treat custom actions as versioned executable dependencies with an explicit step-level contract: metadata defines the surface, the execution model defines portability/runtime assumptions, the caller owns permissions, the implementation validates untrusted input, outputs remain small and non-sensitive, and release provenance binds human versions to immutable code. Chapter 18 expands reuse from individual actions and reusable workflows into Workflow Templates, Organization Standards, YAML Anchors, and Reuse Architecture.
Knowledge check
Why does the checkpoint call its inputs typed/documented rather than adding type: string to action.yml?
Custom-action metadata inputs do not use workflow_call-style type declarations. The action documents expected semantics and validates the incoming strings.
What is the causal repair for Revision A?
Change only the caller mode from unsupported to strict. The action correctly rejected the original input, so widening permissions or editing runtime state would be unrelated.
Why keep the failed Revision A run after Revision B succeeds?
It preserves the original source SHA, first-failure step and validation evidence so the repair can be compared independently.
What should an external caller pin after v1.0.0 is released?
The full reviewed commit SHA corresponding to v1.0.0; the tag is the human mapping, not the immutable execution identity.
What does Chapter 18 add beyond Chapter 17?
Organization-level reuse architecture: templates, standards, YAML reuse and governance around how workflows/actions are adopted across repositories.
Official references and version notes
- GitHub Docs — About custom actions — execution-model and portability comparison.
-
GitHub Docs — Metadata syntax reference
— current
action.ymlinputs, outputs andrunsmodels. -
GitHub Docs — Create a composite action
— composite steps and
GITHUB_ACTION_PATH. - GitHub Docs — Create a JavaScript action — JavaScript packaging and runtime model.
- GitHub Docs — Create a Docker container action — Docker metadata, inputs and outputs.
- GitHub Docs — Dockerfile support for Actions — workspace, USER, ENTRYPOINT and CMD constraints.
- GitHub Docs — Managing custom actions — release management and immutable SHA references.
- actions/toolkit — official JavaScript action helper packages.
- GitHub Changelog — Node 20 deprecation — current Node 24 migration/removal schedule.
Version-sensitive behavior was rechecked on
2026-09-10 for GitHub.com. New JavaScript actions
should target runs.using: node24. GitHub moved
JavaScript actions to Node 24 by default in 2026; Node 20 is in
its final deprecation window and is scheduled for removal from
Actions runners on 2026-09-23. Current
@actions/core source reports version
3.0.1 and is ESM-only. Action metadata inputs do
not provide reusable-workflow-style typed
schemas; document the expected type/enum and validate the string
value in the action. Composite actions access declared values
through the inputs context and should use
GITHUB_ACTION_PATH for action-relative scripts.
JavaScript actions can define
pre/main/post; Docker
actions can define
pre-entrypoint/entrypoint/post-entrypoint, with
pre/main/post Docker phases running in distinct containers. Docker
container actions run only on Linux runners; self-hosted runners
also need Docker. Production references to external actions should
use a reviewed full commit SHA and record the release/tag that
maps to it. The mandatory lab uses
actions/checkout v7.0.1 pinned to
3d3c42e5aac5ba805825da76410c181273ba90b1; every other
executable action is repository-local. The checkpoint
intentionally does not publish a release or external action. It
creates the exact release-provenance record a publisher would
need, while keeping mandatory learning free, local to a disposable
repository and side-effect bounded.
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.