Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions src/docs/Coding-Standards/GitHub-Actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,86 @@ jobs:
PROPAGATION_TOKEN: ${{ secrets.PROPAGATION_TOKEN }} # explicit, by name
```

## Separate a reusable workflow from the actions it consumes

Choosing a reusable workflow has a consequence for the actions it runs. The
workflow is a **process** β€” it stitches jobs into an order β€” and the actions are
the **building blocks** it calls. Once the workflow is shared across
repositories, GitHub's reference resolution forces the two apart, and the split
pays off in versioning and reuse.

### A shared reusable workflow ships only its own file

When one repository calls another's reusable workflow
(`uses: OWNER/REPO/.github/workflows/name.yml@<sha>`), GitHub loads **only that
one workflow file** into the caller's run β€” the reusable workflow's repository is
*not* checked out. A `./`-relative action reference inside it therefore resolves
against the **caller's** checkout, not the workflow's own repository, and finds
the wrong action or none at all. (A same-repository caller may still name the
workflow itself by a local `./` path; the constraint is on the actions the
workflow reaches for.)

- **Reference every action from a shared reusable workflow by full path** β€”
`OWNER/REPO/path@<sha>`, which resolves the same way regardless of which
repository is checked out. Pin it by SHA like any other dependency (see
[Pin every action to a full commit SHA](#pin-every-action-to-a-full-commit-sha)).
- **A composite action may still call a sibling with `./`.** Inside a composite
action, `./` resolves within *that action's own repository at the same ref* β€”
the opposite of the workflow case β€” so co-located actions call each other with
`./` and no pin. This asymmetry is what lets a set of actions live and ship
together.
- **Self-checkout is the only escape hatch, and a poor foundation.** A reusable
workflow *can* check its own repository out at runtime β€” without caller input β€”
through the `job.workflow_repository` / `job.workflow_ref` / `job.workflow_sha`
contexts (`actions/checkout` with `repository: ${{ job.workflow_repository }}`,
`ref: ${{ job.workflow_sha }}`). But those contexts are **not available on
GitHub Enterprise Server**, the checkout is extra machinery that must not
clobber the caller's workspace, and it ties the actions to the workflow's own
commit β€” so an action others also consume gets no version line of its own.
Treat it as a last resort, not the pattern.

### Why the actions need their own versioned repository

Two lifecycle needs make a **separately versioned** action repository β€” not
co-located local actions β€” the sound choice:

- **Development must test the branch, not the last release.** While you build the
workflow, you want it to run the action versions *on the branch you are
developing*. A `./` local action does not resolve in a shared reusable workflow
at all, and pinning `OWNER/REPO/action@<released-sha>` runs stale code β€” so give
the actions their own repository and point the workflow's pin at that
repository's **branch or commit** to test branch against branch.
- **A release cannot pin its actions to itself.** The action versions a released
workflow runs must be exactly those of that release. But the workflow's tag is
cut *after* the merge to its default branch, so the workflow file can never pin
a co-located action to its own release commit β€” that commit does not exist when
the file is written. An independently versioned repository has a commit of its
own that the workflow pins by SHA, locking the release to an exact set of
actions.

### Give the actions a home that matches their reuse

A shared reusable workflow cannot reach a local action, so its actions live in
one of two cross-repository homes β€” chosen, as always, by
[single responsibility and shared lifecycle](../Ways-of-Working/Repository-Segmentation.md):

- **A standalone action repository** when the action is consumed **on its own**
by other repositories β€” its own responsibility, its own version line. This is
the promotion path in [Start local; promote when it is
reused](#start-local-promote-when-it-is-reused).
- **An action-library repository** when a reusable workflow needs **several
actions built, tested, and released together as one set**. Their lifecycle is
shared β€” changing one re-releases the set β€” so
[they belong in one repository](../Ways-of-Working/Repository-Segmentation.md#share-a-repository-only-when-the-development-lifecycle-is-shared),
not a repository each: several top-level composite actions, one test suite, one
release, one SHA to pin. It is a package of actions, consumed like any other
dependency.

Keep the workflow and the library in **separate repositories**: the workflow
repository β€” the *process* β€” pins the action-library repository β€” the *building
blocks* β€” by SHA. A change then flows outward as a deliberate bump at each hop,
so every layer upgrades on a reviewed pull request rather than silently.

## Default to PowerShell as the glue language

Between the declarative steps, a workflow always has some glue to run β€” reading a
Expand Down Expand Up @@ -500,6 +580,10 @@ appears.
[Pin every action to a full commit SHA](#pin-every-action-to-a-full-commit-sha)).
Do not reach for a separate repo preemptively β€” the cost of a shared release
surface is only worth paying once there is a second consumer.
- **A reusable workflow's actions are the exception** β€” a shared reusable
workflow cannot reach a local action, and a cohesive set is better kept in one
action library than a repository each. See [Separate a reusable workflow from
the actions it consumes](#separate-a-reusable-workflow-from-the-actions-it-consumes).

### Move the script into its own file

Expand Down