Composite Actions, JavaScript Actions, Docker Actions, and Custom Automation: Configuration, Design Patterns, and Trade-Offs
Custom action design is an execution-model decision, not a syntax preference. This lesson compares composite, JavaScript and Docker actions; local and external ownership; bundled and runtime-installed dependencies; immutable SHA references and friendly release tags; and action versus reusable-workflow boundaries.
Learning objectives
- Choose among composite, JavaScript and Docker actions using execution and trust requirements.
- Separate a local implementation convenience from an external supply-chain dependency.
- Explain bundled dependencies, Node runtime, image/base-image and runner prerequisites as release state.
- Compare immutable SHA references with tags/branches without confusing update convenience with integrity.
- Decide when repeated automation belongs in a custom action versus a reusable workflow.
1. Start with the boundary: who owns the job?
If the caller must retain the runner, job permissions, services,
matrix cell and surrounding steps, an action is usually the right
reuse boundary. If the reusable component needs to own jobs,
needs, matrices, runner selection or deployment gates,
use a reusable workflow. This decision prevents “mega-actions” that
secretly reimplement workflow orchestration.
2. Composite versus JavaScript versus Docker
| Choice | Best fit | Portability/runtime | Primary operational risk |
|---|---|---|---|
| Composite action | small orchestration of commands/actions | inherits caller runner and required shells/tools | hidden runner assumptions and unsafe shell interpolation |
| JavaScript action | structured logic, APIs, portable computation |
GitHub-provided Node runtime; package dependencies into
dist
|
runtime deprecation and stale bundled dependencies |
| Docker action | OS/toolchain environment is part of contract | Linux only; Docker required for self-hosted | image/base-image provenance, startup cost, container constraints |
None is universally “more secure.” Security depends on implementation, caller permissions, supply-chain identity and runtime assumptions. Docker isolation does not neutralize a write token or mounted workspace; a composite action is not harmless because it looks like YAML.
3. Local versus external ownership
A repository-local action can evolve in the same change as its caller. That is convenient for application-specific logic but couples release cadence to one repository. An external action lets many repositories share one reviewed implementation, but now caller trust includes another repository and its release process.
Current GitHub.com also supports same-repository
$/ references in supported contexts, which resolve
against the repository containing the file and do not use a mutable
@ref. The mandatory lab keeps the established
./ path plus checkout because it makes workspace
mechanics explicit and remains easy to understand; production
architecture should verify platform compatibility before adopting
newer reference forms.
4. Bundled dependencies versus runtime install
For JavaScript actions, installing npm dependencies at action
execution time adds registry availability, resolution state and
package mutation to every workflow run. The normal production
pattern is to lock dependencies, build a bundle, review the
generated dist artifact and commit that bundle with the
action release.
For composites, avoid assuming a random package exists globally on
the runner. Either constrain the supported runner/tool baseline or
invoke a reviewed setup action/tool installation. For Docker
actions, the image and its base-image digest become the dependency
bundle; a mutable FROM ...:latest simply moves the
version problem into Docker.
5. Tag/release versus immutable SHA
A semantic tag such as v1.2.0 is excellent for release
communication. It can also move or be deleted. A commit SHA
identifies immutable Git content. For privileged production callers,
record both: release v1.2.0 maps to reviewed SHA X,
and callers execute SHA X.
When a security update is required, publish a new release and new SHA; update callers through code review. Do not “fix” the old SHA by moving a tag and assume consumers who pinned immutably will change automatically.
6. Interface stability: expose intent, not implementation knobs
An action interface should describe caller intent:
mode: strict, not twenty flags mirroring every internal
command-line switch. Because action inputs are strings, validate
domain constraints at the edge and return a clear failure before
invoking expensive or privileged operations.
Outputs should be small non-sensitive control values. Files belong in the workspace or artifacts according to lifecycle; secrets do not belong in outputs, caches or generated release bundles.
7. Permissions belong to the caller contract
An action README should say “requires contents: read”
or “requires issues: write” when applicable, but the
workflow grants that permission. This separation keeps authority
review at the orchestration layer. An action that silently assumes
the default token has write access is brittle and unsafe.
8. Pre/post state and cleanup design
JavaScript actions can have pre, main and
post scripts under the same Node runtime. Docker
actions can have pre-entrypoint and post-entrypoint phases, but each
phase runs in a new container. When cleanup needs information from
setup/main, save only the necessary non-sensitive state and use the
documented action state mechanism.
Cleanup should be idempotent and bounded. A post step that deletes “the latest resource” or blindly revokes a shared credential creates a recovery hazard. Exact temporary resource IDs should be created, recorded and removed by the same action instance.
9. Worked decision table
| Scenario | Decision | Prerequisites | Evidence |
|---|---|---|---|
| Run three validated shell commands on caller-selected runner | Composite | documented shell/tools; no hidden privileges | action SHA/path, runner OS, step outputs |
| Parse JSON and call GitHub REST cross-platform | JavaScript | Node 24 bundle; narrow caller token permission | dist digest/source SHA, runtime, API status |
| Run a compiler requiring a custom Linux userspace | Docker | Linux runner + Docker; reviewed base/image | Dockerfile/image digest, runner/Docker version |
| Coordinate test → package → deploy jobs | Reusable workflow | workflow_call contract, runner/permission design | called workflow SHA, job graph, outputs/deployment evidence |
10. Cost, latency and maintainability
Composite startup is small but may repeat setup work. JavaScript startup is typically fast but requires runtime maintenance and bundled dependency updates. Docker can give the most controlled userspace but pays image build/pull cost. Measure the real critical path before optimizing; do not choose Docker because “containers are DevOps” or JavaScript because “it is faster” without evidence.
Knowledge check
Which abstraction owns multiple jobs: a custom action or reusable workflow?
A reusable workflow. Custom actions execute as steps inside a caller-owned job.
Why is runtime npm install usually a poor production JavaScript-action design?
It makes every workflow depend on live package resolution and registry state; bundling reviewed dependencies into dist gives a more reproducible release.
Does a Docker action remove the need to review caller permissions?
No. The container still executes within the workflow trust boundary and can receive workspace/network/token capabilities exposed by the caller.
What should a human-readable action tag be paired with?
The exact reviewed full commit SHA that callers pin for immutable execution identity.
Why validate action inputs at the boundary?
Metadata inputs are strings; validation prevents ambiguous behavior and keeps untrusted data from flowing into deeper shell/API/container operations unchecked.
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.
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.