Chapter 18Lesson 04~190 minutes

Reusable Workflows, Composite Actions, JavaScript Actions, and Automation Reuse: Diagnostics, Failure Modes, Security, and Performance

Reuse compresses configuration while expanding dependency depth. A failure may originate in the caller, the called workflow, a composite action, a JavaScript bundle, a nested dependency, or an Actions policy. The diagnostic discipline is to preserve the exact caller run, resolve every reused component to code identity, inspect effective permissions and passed data, and change the smallest layer that is actually wrong.

DiagnosticsPermission failureDependency driftBundle security

Learning objectives

  • Diagnose reusable-workflow failures by separating caller configuration, callee interface, nested dependency, permissions, and runtime component identity.
  • Interpret missing secret/permission failures without responding with broad PATs or blanket write permissions.
  • Detect mutable-reference drift and prove which commit actually executed.
  • Identify stale/vulnerable JavaScript action bundles and mismatches between reviewed source and distributed code.
  • Use a preserve → scope → inspect → least-destructive correction → verify sequence for shared automation incidents.

Safety: The live failure in this chapter uses a disposable issue-write permission probe only. It does not rewrite refs, bypass protections, expose secrets, register runners, delete packages, or run untrusted pull-request code. Permission changes are temporary and restored after evidence is captured.

1. Diagnostic sequence for shared automation

  1. Preserve evidence: run ID/attempt, head SHA, caller workflow file, resolved dependency refs, logs, annotations, API error.
  2. Scope: caller repository, called workflow repository/ref, action repository/ref, job, token permission, secret/input/output.
  3. Inspect: workflow syntax, accessibility policy, effective permissions, run jobs/logs, component metadata and source revision.
  4. Correct the smallest layer: caller input, permission grant, component release, pin, or bundle—not a blanket privilege increase.
  5. Verify: new run at known SHA plus comparison to preserved failed attempt.

Do not begin by editing the central workflow. Shared automation failures are often caller-contract failures, and a central “fix” can break every healthy consumer.

2. Intentionally broken example: the callee tries a mutation the caller did not authorize

Use the Chapter 18 repository from Lesson 2. The caller grants issues: read. Dispatch with the probe enabled:

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')"

# Expected non-zero result; preserve it.
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

The permission-probe step issues an authenticated POST to create an issue. Authentication succeeds, but authorization should fail because the caller supplied only issues: read. Expect an HTTP 403-style API failure rather than “token missing.” This is the exact distinction established in Chapter 2 and Chapter 12.

Wrong repair: Do not create a classic PAT, use blanket write permissions, or put a broad secret into the reusable workflow. The failure proves the caller contract is narrower than the attempted operation.

3. Missing secret: distinguish interface declaration from runtime availability

A reusable workflow can declare a required named secret. The caller must pass a matching secret, unless a supported same-organization/enterprise call deliberately uses secrets: inherit. A secret defined in some other repository or environment does not become visible merely because the workflow file references its name.

# Callee interface
on:
  workflow_call:
    secrets:
      registry_token:
        required: true

# Caller must pass it explicitly
jobs:
  publish:
    uses: platform/automation/.github/workflows/publish.yml@FULL_SHA
    secrets:
      registry_token: ${{ secrets.RELEASE_REGISTRY_TOKEN }}

If validation says the secret was not supplied, fix the caller mapping or redesign the capability. Do not make the callee silently search for alternate credential names.

4. Deep or inaccessible nesting obscures ownership

A chain can currently reach ten workflow levels, but accessibility must hold for every workflow in the tree. If A can call B but B calls C in a repository A cannot access, the run fails. Even when access is valid, excessive nesting hides the location of runner selection, permissions, and failures.

Preserve the top-level caller run, then inspect each uses edge and resolve its ref. Build a small dependency map: caller → workflow repo/path/SHA → actions repo/path/SHA. If the tree cannot be explained in one operational view, simplify ownership rather than adding another indirection layer.

5. Mutable tag drift: “same YAML” can run different code tomorrow

Suppose a caller contains uses: vendor/action@v3. The caller commit has not changed, yet the tag may now resolve to a different commit. Preserve both the caller SHA and the current resolved action SHA before re-running. If the incident concerns a historical run, use run logs/dependency evidence rather than assuming the tag’s current target is what executed then.

ACTION_REPO="docker/setup-buildx-action"
REF="v3"
CURRENT="$(gh api "repos/$ACTION_REPO/commits/$REF" --jq .sha)"
printf 'current_resolved_sha=%s\n' "$CURRENT"

# Compare with the SHA recorded in your approved dependency inventory/run evidence.
# If different, review the release/source diff before changing any workflow.

Least-destructive correction is usually to pin an approved known-good SHA or update deliberately to a reviewed fixed SHA. Do not force-move someone else’s tag or rewrite caller history.

6. JavaScript bundle drift: reviewed source may not equal executed distribution code

A custom JavaScript action may have src/index.js, package-lock.json, and committed dist/index.js. If a dependency update changes the lockfile but dist was not regenerated, users still execute the old bundle. Conversely, an unexplained dist change can hide code not represented by the visible source diff.

Production repositories should have a deterministic build/check that rebuilds the distribution artifact and fails when the committed bundle differs. Dependabot/security alerts should be resolved by updating source dependencies, rebuilding, testing, reviewing the generated diff, and releasing a new immutable commit—not by manually editing minified output.

7. A reusable workflow “quietly gains privileges” only if callers or policy grant them

A central workflow author may add an API call that requires write permission. If callers already passed a broad write token or inherited broad secrets, the new release might immediately gain the capability. If callers use narrow explicit permissions, the changed workflow fails until the caller reviews and grants the new permission. Failure is safer than silent authority expansion.

This is why least privilege and pinned versions reinforce each other: pinning prevents code drift; narrow permissions limit what even an approved-but-buggy component can do.

8. Performance and cost failures: reuse can multiply expensive topology

A reusable workflow can hide matrices, large runner selection, artifact uploads, or repeated dependency installs. One caller may look cheap; 100 callers magnify every design choice. Measure queue time, runner time, cache behavior, and artifact size at the shared component level. Do not optimize by collapsing trust boundaries or sharing persistent state between unrelated callers.

Prefer stable interfaces that let callers choose only legitimate dimensions. Avoid user-controlled free-form runner labels or arbitrary shell fragments as inputs; those turn a reuse interface into an execution escape hatch.

9. Structured evidence for a failed reuse run

gh run view "$BROKEN" -R "$REPO" \
  --json databaseId,attempt,headSha,event,status,conclusion,jobs,url

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,status,conclusion}]}'

# Preserve the caller and reusable files at the failed run SHA.
FAILED_SHA="$(gh run view "$BROKEN" -R "$REPO" --json headSha --jq .headSha)"
for path in .github/workflows/c18-caller.yml .github/workflows/c18-reusable.yml; do
  gh api -H "X-GitHub-Api-Version: 2026-03-10" \
    "repos/$REPO/contents/$path?ref=$FAILED_SHA" --jq '{path,sha,size}'
done

Do not overwrite or delete the failed workflow before capturing these identities. The failure is evidence of the permission contract and the source revision that enforced it.

10. Security-sensitive corrections and what this chapter avoids

Runner registration, token creation, secret changes, policy bypass, force-updating tags/branches, deleting packages/releases, and rewriting history are all security-sensitive. None is required here. The only privilege change in the checkpoint is a temporary issues: write grant in a disposable repository, followed by issue closure and permission restoration.

If a real shared action is suspected compromised, freeze updates, identify consumers and exact SHAs, revoke/rotate any exposed credentials first, then replace pins or disable the component. Removing a malicious reference does not undo a credential already exfiltrated.

11. Lesson summary

Reuse incidents are dependency-graph incidents. Preserve caller run and source identity, resolve each reused component, inspect permission/secret contracts, and correct the smallest faulty layer. A 403 can be healthy evidence of least privilege; a mutable tag can create code drift without caller changes; a JavaScript bundle can diverge from reviewed source. Reuse is reliable only when execution identity and capability flow are observable.

Knowledge check

The permission probe returns 403 with a valid GITHUB_TOKEN. What does that prove?

A workflow uses vendor/action@v3 and suddenly changes behavior without caller commits. What identity should you inspect first?

Why is broad secrets: inherit dangerous for centrally updated workflows?

What makes a JavaScript dist mismatch a supply-chain problem?

Why preserve the failed run instead of immediately re-running after a fix?

Next lesson

Next: Checkpoint Lab — Reusable Workflows, Composite Actions, JavaScript Actions, and Automation Reuse

Further reading — current official GitHub sources

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
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.