Chapter 17Lesson 04~200 minutes

Composite Actions, JavaScript Actions, Docker Actions, and Custom Automation: Diagnostics, Failure Modes, and Production Practices

Custom-action failures can originate in metadata, packaging, runner capability, shell quoting, runtime selection, hidden permission assumptions or mutable supply-chain identity. This lesson preserves first-failure evidence, identifies the causal boundary and repairs only that boundary.

First failurePackagingUnsafe inputPermissionsProvenance

Learning objectives

  • Diagnose metadata, packaging, runtime, runner and permission failures as separate causal layers.
  • Preserve the original run/attempt, action identity and first failing step before repair.
  • Recognize unsafe shell input handling and hidden capability assumptions in custom actions.
  • Differentiate dependency packaging failures from mutable-reference and release-provenance failures.
  • Apply the least destructive correction and rerun only the smallest equivalent scope.

1. Evidence-first diagnostic sequence

When an action fails, do not begin by changing YAML or rerunning. Preserve the run ID, attempt, source SHA, action reference, runner OS/arch, metadata and first failing log. Then walk the layers: caller event/revision → job permissions/runner → action identity → metadata/runtime → implementation/dependencies → outputs/files → external side effects. Repair the first proven mismatch.

2. Broken JavaScript packaging: source exists, dependency does not

A common failure occurs when action.yml points to src/index.js, the source imports @actions/core, but node_modules is neither shipped nor bundled. The runner is not a development checkout with your npm dependencies. The first failure will be module resolution, not a token or runner-capacity problem.

Error [ERR_MODULE_NOT_FOUND]: Cannot find package '@actions/core'
imported from /home/runner/work/_actions/example/action/<sha>/src/index.js

The repair is release packaging: lock dependencies, create a reviewed bundle such as dist/index.js, point runs.main there, and publish a new immutable action commit. Do not add npm install to every caller as a troubleshooting shortcut.

3. Hidden runner-global dependency

A composite action works on one hosted image because a CLI happens to be preinstalled, then fails on another OS or after an image update. Evidence should compare runner image/tool versions and the action's documented prerequisites. The fix is to make the dependency explicit—support a constrained runner baseline or install/setup the tool through a versioned mechanism.

4. Hidden permission requirement

An action that calls the Issues API may fail with 403/404 under permissions: {}. Do not change to write-all. Inspect the action documentation/source and API requirement, then grant only issues: write to the specific caller job if the side effect is intentional. If the action is supposed to be read-only, a write requirement is an implementation defect.

5. Unsafe untrusted input interpolation

# Broken: expression result becomes shell program text.
- shell: bash
  run: echo "processing ${{ inputs.name }}"

If the caller controls name, special shell syntax can alter execution. The repair is to place the expression into an environment variable and quote the variable as data.

- shell: bash
  env:
    NAME: ${{ inputs.name }}
  run: printf 'processing %s
' "$NAME"

For higher-risk operations, validate an allow-list or use a structured API rather than a shell.

6. Mutable tag without release provenance

Suppose two runs both say uses: vendor/action@v1 but behave differently a week apart. The workflow text is identical while the dependency identity moved. Capture the resolved action SHA from logs/metadata where available and compare release history. The production fix is a reviewed full SHA plus a release mapping.

7. Custom action that should have been a reusable workflow

If an action starts launching its own background services, simulating multiple independent jobs, or encoding deployment gates, the abstraction is fighting Actions itself. Move job-graph concerns to a reusable workflow and keep custom actions focused on step-level execution.

8. Docker-specific failure layers

A Docker action on Windows or macOS will not run because Docker container actions require Linux. A self-hosted Linux runner without Docker has a different failure. An executable entrypoint permission error differs again. Preserve the exact runner and daemon evidence before changing the image or runner label.

Do not fix a Dockerfile workspace-access problem by granting privileged host access. GitHub's Docker action model has specific constraints, including default-user expectations for GITHUB_WORKSPACE.

9. Intentionally broken checkpoint fragment

permissions: {}
jobs:
  validate:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
      - uses: ./.github/actions/ch17-contract
        with:
          mode: unsupported
          repeat: '2'

The expected failure is exit 64 in the composite validation step. If the run instead fails before that—checkout failure, path-not-found, YAML validation—diagnose that earlier layer first. The intended repair is one caller value: mode: strict. Do not edit permissions, checkout SHA or action code when the action is correctly rejecting the caller input.

10. Recovery rule: smallest equivalent rerun

After preserving the first failure, commit the minimal repair and dispatch a new run. Keep the old run. A rerun of the old attempt would still use its old workflow/action revision in ways that can obscure what changed; a new source SHA makes the repair explicit and reviewable.

Knowledge check

A JavaScript action cannot find @actions/core. What layer should you inspect first?

Why is permissions: write-all not a valid fix for an API 403?

How should a composite action handle caller-controlled text passed to Bash?

Two runs use @v1 but execute different action code. What state was missing?

Why preserve the broken run before a new commit?

Next lesson

Prove the contract and release identity

Lesson 5 combines validation failure, repair and immutable release planning into one evidence packet.

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.