Chapter 17Lesson 03~190 minutes

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.

Execution modelNode 24Bundled distDocker boundaryReuse choice

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?

Why is runtime npm install usually a poor production JavaScript-action design?

Does a Docker action remove the need to review caller permissions?

What should a human-readable action tag be paired with?

Why validate action inputs at the boundary?

Next lesson

Diagnose packaging and trust failures

Lesson 4 starts from broken action evidence instead of rewriting metadata blindly.

Official references and version notes

Version and compatibility note

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.