Chapter 18Lesson 01~180 minutes

Reusable Workflows, Composite Actions, JavaScript Actions, and Automation Reuse: Concepts, Architecture, and Mental Model

Chapters 13–17 established event identity, data flow, execution topology, runners, and workflow evidence. Chapter 18 asks what happens when that automation is reused. Copy/paste creates drift; reuse creates leverage. The same component can improve dozens of repositories—or fail or exfiltrate data across all of them. The objective is therefore not merely “less YAML,” but a stable, inspectable automation interface with explicit trust and ownership.

workflow_callComposite actionsJavaScript actionsVersion & trust

Learning objectives

  • Distinguish reusable workflows, composite actions, and JavaScript actions by execution boundary, interface, runner ownership, and packaging model.
  • Trace typed inputs, named secrets, outputs, caller/callee context, and GITHUB_TOKEN permissions through a reuse chain.
  • Explain repository-local reuse, cross-repository reuse, nesting limits, accessibility rules, and why permissions may be preserved or reduced but not elevated.
  • Compare branch, tag, and full commit-SHA references and establish a review/update policy for reusable dependencies.
  • Treat internal and Marketplace automation as software supply-chain dependencies with owners, versions, tests, and blast-radius controls.

Availability: The mandatory path uses GitHub.com, GitHub Free, one disposable public personal repository, and repository-local reuse. It therefore needs no organization, Marketplace publication, private-repository sharing, enterprise policy, self-hosted runner, or paid product. Cross-private/organization/enterprise reuse is taught as an extension because access settings and policy boundaries differ.

1. The problem: copy/paste reduces dependency risk by creating configuration drift

A team often starts with two nearly identical workflow files. Copying feels safe because every repository owns its YAML, but fixes soon diverge: one repository updates a permission, another keeps an old action version, a third changes test flags, and a fourth silently stops producing the same evidence. Reuse centralizes the fix, but it also centralizes failure. A compromised or badly released shared component can affect every caller.

The design target is therefore controlled reuse: a component has a narrow purpose, a documented interface, an identifiable source revision, the minimum permissions it needs, an owner, tests, and a deliberate release/update path. Reuse is platform engineering, not YAML compression.

2. Three reuse primitives solve different problems

Primitive Called from Contains Owns runner/job topology? Typical use
Reusable workflow A job via jobs.<id>.uses One workflow file in .github/workflows Yes — may contain multiple jobs Organization CI policy, build/test/deploy pipelines
Composite action A step via steps[*].uses action.yml plus step bundle/scripts No — executes inside caller job Repeated shell/tool steps
JavaScript action A step via steps[*].uses action.yml plus packaged JS runtime code No — executes inside caller job Reusable logic needing richer control/API integration

A reusable workflow is a job-level orchestration boundary. It can select runners, create several jobs, use matrices, and expose workflow outputs. A composite action is a step-level composition boundary: it expands a bundle of steps within the already-selected caller job. A JavaScript action is also step-level, but its behavior lives in packaged JavaScript and action metadata rather than workflow steps.

Concept / workflow diagram
              flowchart TD
                C["Caller workflow"] -->|job uses + typed inputs| R["Reusable workflow"]
                R -->|selects runner + jobs| J["Called job"]
                J -->|step uses| A["Composite action"]
                J -->|step uses| JS["JavaScript action"]
                A -->|outputs| J
                JS -->|outputs| J
                J -->|job outputs| R
                R -->|workflow outputs| C
            

The caller crosses a job boundary when it invokes a reusable workflow. Inside the called job, composite/JavaScript actions are step dependencies. Outputs return through explicit mappings; neither reuse primitive should depend on invisible shared state.

3. workflow_call turns a workflow into a typed callable interface

A reusable workflow is still a normal Actions workflow file, but its on section includes workflow_call. The file must be directly inside .github/workflows; subdirectories there are not supported. Inputs must be declared as boolean, number, or string. Named secrets may also be declared. Workflow outputs map to job outputs, which map to step outputs.

name: reusable validation
on:
  workflow_call:
    inputs:
      component:
        required: true
        type: string
    outputs:
      report:
        value: ${{ jobs.validate.outputs.report }}

jobs:
  validate:
    runs-on: ubuntu-24.04
    outputs:
      report: ${{ steps.result.outputs.report }}
    steps:
      - id: result
        env:
          COMPONENT: ${{ inputs.component }}
        run: printf 'report=%s-ok\n' "$COMPONENT" >> "$GITHUB_OUTPUT"

The mapping is deliberate: step output → job output → workflow output. That apparent ceremony is useful governance because it reveals what the shared component promises to callers.

4. Caller and callee do not form a new security universe

GitHub documents that the github context in a reusable workflow is associated with the caller. GitHub-hosted runner assignment and billing are evaluated from the caller context. A called workflow from another repository does not make actions/checkout check out the workflow-library repository; it checks out the caller repository unless you explicitly fetch something else.

The reusable workflow receives a GITHUB_TOKEN for the caller execution. Across nested reusable workflows, token permissions can stay the same or become more restrictive, but they cannot increase. This is a core trust invariant: shared automation cannot grant itself authority the caller never delegated.

Authentication is not authorization: The called workflow may have a valid github.token, yet an API request still fails if the caller did not grant the required permission. Treat the resulting 403 as policy evidence, not as a reason to create a broader PAT.

5. Secrets are explicit capabilities, not implicit workflow globals

A caller can pass named secrets through jobs.<id>.secrets. For eligible same-organization/enterprise calls, secrets: inherit can pass all available secrets to the directly called workflow, but that convenience increases hidden coupling. Secrets do not automatically leap through a nested chain: A must pass to B, and B must explicitly pass what C needs.

Environment secrets have another boundary: on.workflow_call does not support the environment keyword. If the called workflow attaches a job to an environment, that job uses the environment secret associated with the called job rather than a same-named secret passed by the caller. This is a reason to make deployment ownership visible instead of assuming “secret X” always means one source.

Course pattern: The mandatory Chapter 18 lab uses no real secrets and does not use secrets: inherit. Later deployment chapters can introduce environment/OIDC boundaries with a narrower operational reason.

6. Reuse trees have depth, accessibility, and ownership limits

Current GitHub documentation allows a chain of up to ten workflow levels: the top-level caller plus nine called-workflow levels. A top-level workflow file can reference at most 50 unique reusable workflows across its entire nested tree. Loops are not allowed, and every nested workflow must be accessible to the initial caller.

Those are platform maxima, not architecture targets. A ten-deep chain is difficult to review. Production policy should normally prefer a small number of coarse, owned layers so an operator can answer “which repository and revision implemented this failed job?” without reconstructing a dependency maze.

7. action.yml is the contract for composite and JavaScript actions

Custom actions use action.yml or action.yaml metadata; GitHub currently prefers action.yml. The file declares the human description, inputs, outputs, and runs implementation. A composite action uses runs.using: composite and embeds steps. A JavaScript action selects a supported Node runtime and points to a packaged entry file.

name: Normalize component label
description: Convert a caller-provided label into a deterministic safe identifier
inputs:
  label:
    description: Label to normalize
    required: true
outputs:
  normalized:
    description: Lowercase normalized identifier
    value: ${{ steps.normalize.outputs.value }}
runs:
  using: composite
  steps:
    - name: Normalize label
      id: normalize
      shell: bash
      env:
        RAW_LABEL: ${{ inputs.label }}
      run: |
        normalized="$(printf '%s' "$RAW_LABEL" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9._-' '-' | sed 's/^-//; s/-$//')"
        if [ -z "$normalized" ]; then
          printf '%s\n' 'Input did not contain a usable identifier.' >&2
          exit 1
        fi
        printf 'value=%s\n' "$normalized" >> "$GITHUB_OUTPUT"

Notice the input is first placed in an environment variable before the shell processes it. Event or caller-provided strings are data. Do not splice untrusted values directly into generated shell source.

8. JavaScript actions add a runtime packaging responsibility

A JavaScript action can expose a clean step interface while implementing logic with the Actions Toolkit or plain JavaScript. Current GitHub tutorial examples declare runs.using: node20. The action is downloaded and executed as a complete package, so runtime dependencies must be present in the distributed action. GitHub recommends bundling dependencies into committed distribution output rather than committing a large node_modules tree.

This creates a review invariant: when src/ or dependency lockfiles change, reviewers must also verify the generated dist/ change. A stale bundle means the code users execute may not match the source reviewers just approved.

9. A reference is a supply-chain decision

Reference Reproducibility Update behavior Use
Full commit SHA Immutable Git object identity No automatic updates Production dependencies after source/release review
Release/version tag Readable but movable/deletable Maintainer can retarget tag Trusted publisher + controlled update policy
Branch Continuously mutable Every new commit changes future executions Development only or tightly controlled internal experiments
Repository-local path Uses caller commit Versioned with caller source SHA Local composite/reusable components in same repo

GitHub security guidance says full-length commit SHA pinning is currently the only immutable way to reference an action. The same stability reasoning applies to cross-repository reusable workflows: GitHub recommends a commit SHA as the safest reference. GitHub enterprise policy can require SHA pinning for actions, but do not assume that setting also forces reusable workflows to use SHAs; govern workflow references separately.

10. Marketplace and third-party components are dependencies with execution authority

A third-party action can read the workspace, environment, and whatever token/secrets its step receives. Compromising one action can compromise later job state and use GITHUB_TOKEN permissions granted to that job. “Verified creator” is a useful identity signal, not a proof that a particular release is safe.

A safe adoption sequence is: identify the repository → resolve the intended tag to a full commit SHA → verify the SHA belongs to the canonical repository → review source and release diff → inspect action metadata/runtime/dependencies → understand requested inputs/network behavior → pin the approved full SHA → schedule update review. Chapter 18 demonstrates this sequence read-only rather than executing an unnecessary external component.

11. Internal sharing adds access-token and log-visibility boundaries

Repository-local reuse is simplest. Cross-repository public components are accessible subject to Actions policy. For private repositories, GitHub can allow actions/reusable workflows to be used by other private repositories owned by the same user or organization. When a private component repository is shared this way, GitHub supplies the runner a scoped installation token with read access that automatically expires after one hour.

GitHub also warns that outside collaborators on a caller repository may indirectly access information from a private component repository through workflow logs. Organization/enterprise centralization therefore needs both repository-access policy and log/content review; “private library” does not mean its implementation is invisible to every caller participant.

12. Read-only inspection before changing reuse

REPO="OWNER/REPOSITORY"

# Inspect workflow files and the Actions policy before changing them.
gh api -H "X-GitHub-Api-Version: 2026-03-10" \
  "repos/$REPO/contents/.github/workflows" --jq '.[] | {name,path,sha}'

gh api -H "X-GitHub-Api-Version: 2026-03-10" \
  "repos/$REPO/actions/permissions"

# If selected-actions policy is in use, inspect what is allowed.
gh api -H "X-GitHub-Api-Version: 2026-03-10" \
  "repos/$REPO/actions/permissions/selected-actions" 2>/dev/null || true

# Inspect recent runs without changing anything.
gh run list -R "$REPO" --limit 10 \
  --json databaseId,name,workflowName,event,headSha,status,conclusion,url

The first question is not “can I factor this YAML?” It is “what currently runs, under which policy, and which code identities are already dependencies?” Preserve that baseline before centralization.

13. DevOps connection: reusable automation is internal platform code

A release branch may be protected by a required check that ultimately comes from a reusable workflow. Hundreds of repositories may depend on one composite action. That makes interfaces, compatibility, ownership, release notes, canary callers, rollback references, permission review, and dependency updates operational concerns. Shared automation deserves the same change-management discipline as a service or library.

14. Lesson summary

Reusable workflows package job-level orchestration; composite and JavaScript actions package step-level behavior. Interfaces consist of declared inputs, secrets, outputs, permissions, and source references. Caller context and permissions remain the outer security boundary, nested workflows cannot elevate token permissions, and version references determine reproducibility. Reuse reduces drift only when ownership and supply-chain trust are explicit.

Knowledge check

You need to standardize two jobs with different runners and a dependency between them. Composite action or reusable workflow?

A called workflow wants issues: write, but the caller grants only issues: read. Can the called workflow elevate itself?

Why can secrets: inherit be harder to govern than named secrets?

What source identity is safest for a third-party action?

What additional review obligation does a JavaScript action with dependencies create?

Next lesson

Next: Reusable Workflows, Composite Actions, JavaScript Actions, and Automation Reuse: Guided Hands-On Workflow and Core Operations

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.