Skip to content

Appendix G: A Technical Guide for Repository and Organization Administrators

Introduction

As Salesforce organisations scale their use of GitHub, CI/CD pipelines multiply. Individual project teams duplicate workflow logic, authentication steps drift out of sync, and a security policy change that should affect every repository ends up requiring dozens of separate pull requests. The answer is centralisation: defining automation once, then composing and calling it across every repository in the organization.

GitHub Actions provides three distinct mechanisms for code reuse: reusable workflows, composite actions, and starter workflow templates. It also provides a special organizational repository (.github) that acts as the canonical source for sharing them. Each mechanism solves a different problem, and understanding when to reach for each one is the first step toward a clean, maintainable CI/CD architecture.

This appendix targets GitHub organization administrators and senior contributors managing Salesforce DX (SFDX) projects. Code examples assume familiarity with the Salesforce CLI (sf) and standard SFDX project layouts.

Action version note: every uses: reference on this page names the version that was current at the time of writing. Where a page references the organisation's own reusable workflows and composite actions by tag, the tag is an immutable release tag; third-party actions are shown at their floating major tag, a choice many organisations make for highly trusted actions such as actions/checkout (the trade-off is explained in Appendix G). Newer major versions may introduce breaking changes: review the release notes for an action before upgrading, and validate in a non-production pipeline before promoting to protected branches. Dependabot and Renovate (Section 7) open PRs when new versions are published, providing a structured upgrade path.

The GitHub Actions Component Landscape

Before diving into implementation, it is worth establishing a clear mental model of the components and how they relate to one another.

┌───────────────────────────────────────────────────────────┐
│                   Calling Repository                      │
│                                                           │
│  .github/workflows/pr-validation.yml  ← Caller Workflow   │
│       │                                                   │
│       ├── uses: org/.github/.github/workflows/deploy.yml  │  ← Reusable Workflow (a job)
│       │                                                   │
│       └── steps:                                          │
│             - uses: org/.github/.github/actions/sf-auth   │  ← Composite Action (a step in a job)
└───────────────────────────────────────────────────────────┘

┌───────────────────────────────────────────────────────────┐
│              org/.github (Shared Repository)              │
│                                                           │
│  .github/workflows/        ← Reusable Workflows           │
│  .github/actions/          ← Composite Actions            │
│  workflow-templates/       ← Starter Workflow Templates   │
└───────────────────────────────────────────────────────────┘

The three building blocks:

  • Reusable workflows are complete workflow files that can be called as an entire job. They define their own runners and job structure. Think of them as pipeline templates.
  • Composite actions are collections of steps bundled into a single action and invoked as a single step within a job. Think of them as shared task functions.
  • Starter workflow templates are pre-built workflow files that GitHub surfaces in the "Actions" tab of any repository in the organization, giving developers a governed starting point rather than a blank page.

The Organizational .github Repository

GitHub treats a repository named .github within your organization as a special shared resource. It is the central hub for all cross-repository automation assets.

Configuration Repository Structure (Subset of full list)

<my-org>/.github/
├── .github/
│   ├── workflows/                # Reusable workflows (called with `uses:`)
│   │   ├── sf-prettier-verify.yml
│   │   ├── sf-code-analyzer.yml
│   │   ├── sf-apex-test-selection.yml
│   │   ├── sf-validate-deployment.yml
│   │   └── …                     # (see Appendix A.2 for the full list)
│   └── actions/                  # Composite actions (called as steps)
│       ├── setup-sf/
│       │   └── action.yml
│       ├── sf-auth-jwt/
│       │   └── action.yml
│       ├── sf-auth-sfdx-url/
│       │   └── action.yml
│       └── post-pr-comment/
│           └── action.yml
└── workflow-templates/           # Starter templates (shown in the Actions UI)
    ├── sf-pr-checks.yml
    └── sf-pr-checks.properties.json

Note: The distinction between workflow-templates/ (at the root) and .github/workflows/ (inside the .github subdirectory) often causes confusion. Templates live at the repository root and are surfaced in the UI; reusable workflows live inside .github/workflows/ and are called programmatically from other workflows.

Access Control

The visibility of the .github repository determines who can use assets from it:

  • Public .github repository: Reusable workflows and composite actions are callable from any repository (including those outside your organization).
  • Internal .github repository (GitHub Enterprise): Accessible from any internal or private repository within the enterprise.
  • Private .github repository: Accessible only from repositories within the same organization. This is the recommended setting for most enterprise Salesforce teams.

Additionally, at the organization level, administrators can restrict Actions to only allow workflows and reusable workflows from within the organization. This prevents developers from accidentally pulling in unvetted third-party actions (GitHub Docs: Disabling or limiting GitHub Actions for your organization).

Reusable Workflows

A reusable workflow is a standard GitHub Actions YAML file that exposes a workflow_call trigger. Any other workflow in the organization can invoke it as a complete job, passing in typed inputs and secrets.

Anatomy of a Reusable Workflow

The excerpt below is abbreviated from the validation workflow, shown in full in Appendix A.2.12.

# <my-org>/.github/.github/workflows/sf-validate-deployment.yml
# (abbreviated)

name: Metadata Validation and Apex Tests

on:
  workflow_call:
    inputs:
      test-level:
        description: "Test level for validation run"
        required: true
        type: string
      wait-minutes:
        description: "Minutes to wait for validation to complete"
        required: false
        type: string
        default: "60"
      artifact-name:
        description: "Name of the delta package artifact to download"
        required: false
        type: string
        default: "delta-package"
      sf-username:
        description: "Username of the integration user in the validation org"
        required: true
        type: string
      sf-instance-url:
        description: "Instance URL of the validation org"
        required: true
        type: string
    secrets:
      SF_JWT_KEY:
        description: "JWT private key for the validation org"
        required: true
      SF_CLIENT_ID:
        description: "Client ID of the External Client App in the validation org"
        required: true

jobs:
  validate-deploy:
    name: Metadata Validation
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write
    steps:
      - uses: actions/checkout@v7

      - name: Set up Salesforce environment
        uses: ./.github/actions/setup-sf
        with:
          install-code-analyzer: "false"
          install-sfdx-git-delta: "false"

      - name: Authenticate to validation org
        uses: ./.github/actions/sf-auth-jwt
        with:
          jwt-key: ${{ secrets.SF_JWT_KEY }}
          client-id: ${{ secrets.SF_CLIENT_ID }}
          username: ${{ inputs.sf-username }}
          instance-url: ${{ inputs.sf-instance-url }}
          alias: validation-org

      - name: Download delta package
        uses: actions/download-artifact@v8
        with:
          name: ${{ inputs.artifact-name }}
          path: delta/

      - name: Validate delta deployment
        run: |
          sf project deploy start \
            --dry-run \
            --target-org validation-org \
            --manifest delta/package/package.xml \
            --test-level ${{ inputs.test-level }} \
            --wait ${{ inputs.wait-minutes }}

The key elements of a reusable workflow definition:

  • on.workflow_call: the trigger that makes this file callable from other workflows.
  • inputs: typed parameters (string, boolean, number) with optional defaults.
  • secrets: explicitly declared secrets the called workflow needs. These are separate from inputs and cannot be referenced in with: expressions on the caller side.
  • outputs: values surfaced back to the caller, mapped from job outputs.

Calling a Reusable Workflow

PR Validation
# any-salesforce-repo/.github/workflows/pr-validation.yml

name: PR Validation

on:
  pull_request:
    branches: [main]

jobs:
  validate:
    uses: <my-org>/.github/.github/workflows/sf-validate-deployment.yml@v2.3.1
    with:
      test-level: RunRelevantTests
      sf-username: ${{ vars.SF_USERNAME_PROD }}
      sf-instance-url: ${{ vars.SF_INSTANCE_URL_PROD }}
    secrets:
      SF_JWT_KEY: ${{ secrets.SF_JWT_KEY_PROD }}
      SF_CLIENT_ID: ${{ secrets.SF_CLIENT_ID_PROD }}

The uses: key references the reusable workflow using the format {owner}/{repo}/{path}@{ref}. The @{ref} component can be a branch name, tag, or commit SHA. In production, pin it to an immutable release tag (@v2.3.1): a tag that is never moved fixes the behaviour of the called workflow and prevents unexpected breakage from upstream changes. The versioning strategy below explains the trade-off against a floating major tag.

Passing Secrets: Explicit vs. inherit

GitHub provides two mechanisms for forwarding secrets to a called workflow.

Explicit passing (recommended for clarity):

jobs:
  validate-deploy:
    uses: <my-org>/.github/.github/workflows/sf-validate-deployment.yml@v2.3.1
    secrets:
      SF_JWT_KEY: ${{ secrets.SF_JWT_KEY_PROD }}
      SF_CLIENT_ID: ${{ secrets.SF_CLIENT_ID_PROD }}

Implicit inheritance (convenient, use with caution):

jobs:
  validate-deploy:
    uses: <my-org>/.github/.github/workflows/sf-validate-deployment.yml@v2.3.1
    secrets: inherit

With secrets: inherit, every secret available to the caller is automatically forwarded to the called workflow. This is convenient in same-organization calls, but it means the called workflow has access to secrets it may not need; this is a relevant consideration in least-privilege security models.

Important: Secrets are only passed to directly called workflows. In a chain A → B → C, workflow C receives secrets from A only if B explicitly forwards them. secrets: inherit at each hop is required for full propagation.

Nesting and Limits

As of late 2025, GitHub raised the nesting limit to 10 levels of reusable workflow calls, and a single workflow run can call up to 50 reusable workflows in total (GitHub Changelog: New releases for GitHub Actions, November 2025). In practice, deep nesting should be treated as an architectural smell; two or three levels is typically the maximum useful depth.

A calling job that uses uses: to invoke a reusable workflow cannot contain additional steps:. The entire job is delegated to the called workflow. If you need steps before or after the delegated logic within the same job, use a composite action instead.

Composite Actions

A composite action bundles multiple steps into a single unit that is consumed as a uses: step within a job, not as a job itself. Where a reusable workflow replaces a job, a composite action replaces an entire step.

Anatomy of a Composite Action

Each composite action lives in its own directory and is defined in an action.yml file. The excerpt below is the start of the sf-auth-jwt action, shown in full in Appendix A.3.2.

# <my-org>/.github/.github/actions/sf-auth-jwt/action.yml
# (abbreviated)

name: "Authenticate to Salesforce (JWT)"
description: "Authenticates to a Salesforce org using the JWT bearer flow."

inputs:
  jwt-key:
    description: "The JWT private key (PEM format)."
    required: true
  client-id:
    description: "The External Client App consumer key (client ID)."
    required: true
  # ... username and instance-url, both required, and alias, with a default

runs:
  using: composite
  steps:
    - name: Write JWT key to temporary file
      shell: bash
      env:
        JWT_KEY: ${{ inputs.jwt-key }}
      run: |
        umask 077
        printf '%s\n' "$JWT_KEY" > /tmp/sf-jwt-key.key

    # ... run sf org login jwt, then remove the key file
    # with if: always() so the key never survives a failed login

Key structural requirements:

  • runs.using: 'composite' - declares this as a composite action (as opposed to a JavaScript or Docker action).
  • Every run step within a composite action must declare a shell. This is a requirement unique to composite actions.
  • Inputs are referenced via ${{ inputs.<name> }}. In a run step, pass an input to the script through env:, as the excerpt does, rather than writing ${{ inputs.<name> }} in the script, so that a quote or a $ in a value cannot change the command. Unlike reusable workflows, inputs to composite actions are strings only: they have no typed boolean or number variants. Callers must pass 'true' or 'false' as strings and handle coercion in the action's shell logic. The setup-sf action in the reference implementation (Appendix A.3.1) is a concrete example: its install-code-analyzer and install-sfdx-git-delta inputs are compared as strings (if: inputs.install-code-analyzer == 'true').

Calling a Composite Action

# any-salesforce-repo/.github/workflows/validate.yml

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7

      - name: Authenticate to sandbox
        uses: <my-org>/.github/.github/actions/sf-auth-jwt@v2.3.1
        with:
          jwt-key: ${{ secrets.SF_JWT_KEY_STAGING }}
          client-id: ${{ secrets.SF_CLIENT_ID_STAGING }}
          username: ${{ vars.SF_USERNAME_STAGING }}
          instance-url: ${{ vars.SF_INSTANCE_URL_STAGING }}
          alias: "ci-sandbox"

      - name: Validate
        run: |
          sf project deploy start \
            --dry-run \
            --source-dir force-app \
            --target-org ci-sandbox \
            --test-level RunLocalTests \
            --wait 30

Notice that the composite action is a step, and additional steps freely appear before and after it in the same job. This is the key ergonomic difference from reusable workflows.

Secrets in Composite Actions

Composite actions do not have an on.workflow_call.secrets block. Secrets must be passed as inputs. This is an intentional design: the calling workflow holds the secret context and passes only what the action needs. The action receives it as an input string and must handle it accordingly (e.g., writing to a temp file, not echoing to logs).

- uses: <my-org>/.github/.github/actions/sf-auth-jwt@v2.3.1
  with:
    jwt-key: ${{ secrets.SF_JWT_KEY_STAGING }} # Secret passed as input
    client-id: ${{ secrets.SF_CLIENT_ID_STAGING }} # Secret passed as input
    username: ${{ vars.SF_USERNAME_STAGING }}
    instance-url: ${{ vars.SF_INSTANCE_URL_STAGING }}
    alias: 'my-sandbox'

Starter Workflow Templates

Starter workflow templates solve a different problem: discoverability and governance at the moment a developer creates a new repository. Instead of starting from scratch, developers see your organization's approved templates in the GitHub "Actions" tab.

This functionality may be irrelevant at strictly-governed organisations, such as federal agencies, where all workflows must be approved, even to create a workflow.

In a slightly relaxed situation, template workflows may be pre-approved initially and able to be installed by any repository editor in a feature branch, and then any edits to those files would require organization administrator approval.

Creating a Starter Template

Templates live in the workflow-templates/ directory at the root of the org .github repository (not inside .github/workflows/). Each template requires two files: the workflow YAML and a .properties.json metadata file with the same base name.

org/.github/
└── workflow-templates/
    ├── salesforce-pr-validation.yml
    ├── salesforce-pr-validation.properties.json
    ├── salesforce-deploy.yml
    └── salesforce-deploy.properties.json

The workflow file:

Salesforce PR Validation
# workflow-templates/salesforce-pr-validation.yml

name: Salesforce PR Validation

on:
  pull_request:
    branches: [main]

jobs:
  validate:
    uses: <my-org>/.github/.github/workflows/sf-validate-deployment.yml@v2.3.1
    with:
      test-level: RunRelevantTests
      sf-username: ${{ vars.SF_USERNAME_PROD }}
      sf-instance-url: ${{ vars.SF_INSTANCE_URL_PROD }}
    secrets:
      SF_JWT_KEY: ${{ secrets.SF_JWT_KEY_PROD }}
      SF_CLIENT_ID: ${{ secrets.SF_CLIENT_ID_PROD }}

The properties file:

{
    "name": "Salesforce PR Validation",
    "description": "Validates a Salesforce DX pull request against the CI sandbox using a check-only deployment.",
    "iconName": "salesforce",
    "categories": ["Salesforce", "Deployment"]
}

Templates Calling Reusable Workflows

As illustrated above, a starter template can itself call a reusable workflow using uses:. This is a powerful composition pattern: the template gives every new repository a working pipeline on day one, while the actual logic lives in a single, centrally maintained reusable workflow. When the deployment logic needs to change (e.g., to add a new test level flag), you update the reusable workflow in one place, and every repository automatically picks up the change on their next run.

Non-Public Template Support (2025)

Prior to late 2025, starter workflow templates required the .github repository to be public. GitHub now supports templates from non-public repositories (GitHub Changelog: YAML anchors and non-public workflow templates). If the .github repository is internal, all internal and private repositories in the enterprise can see and use the templates. If it is private, only repositories within the same organization can use them. This is a significant improvement for enterprise teams that cannot expose their pipeline logic publicly.

Combining the Components: An Organization-Wide Strategy

The three components are not mutually exclusive; they are designed to be layered. A well-structured organization uses all three in concert:

Developer creates new Salesforce repo
           │
           ▼
  Selects "Salesforce PR Validation" starter template
  from Actions tab (Starter Workflow Template)
           │
           ▼
  Template calls the org's canonical validate workflow
  uses: <my-org>/.github/.github/workflows/sf-validate-deployment.yml@v2.3.1
  (Reusable Workflow)
           │
           ▼
  Reusable workflow uses composite actions for shared steps
  uses: ./.github/actions/sf-auth-jwt
  (Composite Action)

Repository Versioning Strategy

When the .github repository stores both reusable workflows and composite actions, a single git tag covers all of them. This simplifies versioning but means a change to one action bumps the version for everything in the repo.

Callers pin to an immutable release tag (@v2.3.1), never to a branch, and take updates through Renovate PRs (Section 8). A tag that is never moved gives a reviewed, reproducible pipeline: the workflow that ran yesterday is the workflow that runs today, and an upgrade is a visible diff in the consuming repository rather than a silent change upstream.

# Tag the release; the tag is never moved afterwards
git tag v2.3.1
git push origin v2.3.1

Callers targeting org-wide standards reference the release tag:

uses: <my-org>/.github/.github/workflows/sf-validate-deployment.yml@v2.3.1

Many organisations also keep a floating major tag (v2) that moves to each non-breaking release, so that a caller using @v2 receives updates without a PR. That is a reasonable choice for highly trusted actions such as actions/checkout, where the review saved outweighs the reproducibility lost, and the samples in these standards show third-party actions at their floating major tag for that reason. For the organisation's own workflows and actions the immutable tag is the standard. Renovate can be configured to keep the two styles side by side in one workflow file: it moves an immutable tag to the next release and leaves a floating major tag alone until the next major version.

Organization Policy Enforcement

GitHub organization administrators can enforce that all repositories only use actions and reusable workflows from within the organization. This is configured at Organization Settings → Actions → General → Allow actions and reusable workflows. Selecting "Allow actions created by GitHub" plus your own organization prevents any external action from running without explicit approval, which is an important control for regulated Salesforce environments handling financial or healthcare data.

Salesforce SFDX: Putting It Into Practice

The following example represents a realistic multi-repository Salesforce organization using all three components.

Shared Repository Structure (<my-org>/.github)

The full tree, with every caller workflow, reusable workflow and composite action in place, is the recommended directory structure in Appendix D; the subset earlier on this page shows the shape. The inner .github/workflows/ directory holds both the caller workflows (A.1) and the reusable workflow implementations they invoke (A.2). The inner .github/actions/ directory holds the composite actions that the reusable workflows call as steps (A.3).

The delta package in this architecture is produced by the sf-generate-delta.yml reusable workflow (A.2.8), not a composite action. That workflow runs sf sgd source delta as a job, uploads the result as an artifact, and exposes an artifact-uploaded output. Downstream gates (sf-code-analyzer.yml, sf-apex-test-selection.yml, sf-jest-tests.yml, sf-validate-deployment.yml) each download the artifact by name rather than regenerating the delta independently. Keeping delta generation in a job rather than a composite action lets multiple downstream jobs consume the same delta in parallel without re-running the underlying git diff.

Stitching the Pieces Together

The next four code blocks walk through how a caller workflow invokes a reusable workflow, how the reusable workflow calls composite actions as steps, and how a project repository consumes the whole chain.

The caller workflow is the trigger-bound entry point. In a project repository, it responds to a GitHub event and composes reusable workflows from the organization .github repository. From the reference implementation (pr-prod.yml, A.1.3), a simplified excerpt shows the pattern:

# salesforce-project-repo/.github/workflows/pr-prod.yml

name: CI on PR (prod)

on:
  pull_request:
    branches: [main]
    types: [opened, synchronize, ready_for_review]

env:
  APEX_COVERAGE_THRESHOLD: 85 # Apex classes below this coverage are listed

jobs:
  variables:
    name: Variables
    runs-on: ubuntu-latest
    outputs:
      apex-coverage-threshold: ${{ steps.set.outputs.apex-coverage-threshold }}
    steps:
      - id: set
        run: echo "apex-coverage-threshold=$APEX_COVERAGE_THRESHOLD" >> "$GITHUB_OUTPUT"

  generate-delta:
    name: Delta Package
    uses: <my-org>/.github/.github/workflows/sf-generate-delta.yml@v2.3.1

  apex-tests:
    name: Apex Test Selection
    needs: generate-delta
    if: needs.generate-delta.outputs.artifact-uploaded == 'true'
    uses: <my-org>/.github/.github/workflows/sf-apex-test-selection.yml@v2.3.1
    with:
      organization-run-relevant-tests: ${{ vars.APEX_RUN_RELEVANT_TESTS_ORGANIZATION }}
      run-relevant-tests: ${{ vars.APEX_RUN_RELEVANT_TESTS }}

  validate-deploy:
    name: Validation
    needs: [variables, generate-delta, apex-tests]
    if: needs.generate-delta.outputs.artifact-uploaded == 'true' && !failure() && !cancelled()
    uses: <my-org>/.github/.github/workflows/sf-validate-deployment.yml@v2.3.1
    with:
      test-level: ${{ needs.apex-tests.outputs.test-level }}
      tests: ${{ needs.apex-tests.outputs.tests }}
      coverage-threshold: ${{ needs.variables.outputs.apex-coverage-threshold }}
      sf-username: ${{ vars.SF_USERNAME_PROD }}
      sf-instance-url: ${{ vars.SF_INSTANCE_URL_PROD }}
    secrets:
      SF_JWT_KEY: ${{ secrets.SF_JWT_KEY_PROD }}
      SF_CLIENT_ID: ${{ secrets.SF_CLIENT_ID_PROD }}

Three layers are visible: the caller composes jobs, each job delegates to a reusable workflow via uses:, and the reusable workflows consume secrets passed explicitly from the caller. One job also hands its outputs to the next: the test selection chooses a test level and a list of tests, and the validation takes them as inputs. The needs: and if: conditions express dependency ordering and short-circuit on an empty delta so gates skip cleanly when no Salesforce metadata changed.

The reusable workflow implements a single quality gate. Its job calls composite actions for repeated setup steps. From sf-validate-deployment.yml (A.2.12), the job steps collapse to:

# <my-org>/.github/.github/workflows/sf-validate-deployment.yml

jobs:
  validate-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7

      - name: Set up Salesforce environment
        uses: ./.github/actions/setup-sf # A.3.1
        with:
          install-code-analyzer: "false"
          install-sfdx-git-delta: "false"

      - name: Authenticate to validation org
        uses: ./.github/actions/sf-auth-jwt # A.3.2
        with:
          jwt-key: ${{ secrets.SF_JWT_KEY }}
          client-id: ${{ secrets.SF_CLIENT_ID }}
          username: ${{ inputs.sf-username }}
          instance-url: ${{ inputs.sf-instance-url }}
          alias: validation-org

      - name: Download delta package
        uses: actions/download-artifact@v8
        with:
          name: delta-package
          path: delta/

      # ... sf project deploy start --dry-run with the selected tests, post-pr-comment ...

Every step that wraps more than one command is a composite action. setup-sf replaces what would otherwise be five steps (Node, npm ci, CLI install, Code Analyzer plugin, sfdx-git-delta plugin) with one invocation that accepts toggles for the optional plugins. sf-auth-jwt replaces the three-step pattern of writing the private key to a temp file, authenticating, and cleaning up. The net effect: every reusable workflow that connects to an org has the same shape, and authentication behaviour changes in one place when it changes at all.

The composite action is the leaf of this call graph. It contains steps, not jobs, and runs in the caller job's runner context. The sf-auth-jwt action shown in full in Appendix A.3.2 is a representative example: three run steps handle file write, CLI login, and cleanup, with an if: always() on the cleanup step to ensure the secret file never survives a failed login.

Why This Split Matters

Dividing the system into caller workflows, reusable workflows, and composite actions is not organisational hygiene for its own sake: each layer enables a specific kind of change without disturbing the others.

  • Adding a new quality gate is a reusable-workflow change. Create sf-<gate>.yml in the organization .github repository, add a job to pr-prod.yml and pr-pipeline.yml that calls it, and every consuming project inherits the gate on its next pipeline run.
  • Onboarding a new environment (e.g. a pre-production hotfix branch) is a caller-workflow change. Add pr-hotfix.yml that reuses the same gate chain with the JWT credentials of the hotfix org: the secrets SF_JWT_KEY_HOTFIX and SF_CLIENT_ID_HOTFIX and the variables SF_USERNAME_HOTFIX and SF_INSTANCE_URL_HOTFIX. The reusable workflows do not change.
  • Switching authentication mechanisms (e.g. from the JWT bearer flow to GitHub OIDC, which Section 7 calls an emerging pattern) is a composite-action change. Replace sf-auth-jwt in the reusable workflows that use it, update the consuming caller secrets, and the gate logic is untouched.

Each layer has a distinct owner, a distinct change cadence, and a distinct blast radius. The combination is what makes the architecture sustainable at scale.

Key Differences at a Glance

Dimension Reusable Workflow Composite Action Starter Template
Defined in .github/workflows/*.yml with on: workflow_call action.yml with runs.using: composite workflow-templates/*.yml
Called with uses: in a job uses: in a step Selected from the Actions UI
Can contain Multiple jobs, multiple runners Steps only, single runner context Any valid workflow syntax
Secrets Declared in on.workflow_call.secrets or secrets: inherit Passed as inputs (string) Resolved at call time in calling workflow
Outputs Declared in on.workflow_call.outputs Declared in outputs: N/A
Max nesting 10 levels (as of late 2025) 10 levels 1 level (the template itself)
Can call reusable workflows? Yes No Yes
Versioning @{ref} in uses: @{ref} in uses: Copied into the target repo at creation
Runner defined by The called workflow The calling job The template itself

References