The generic, language-agnostic starting point for reusable-workflow
repositories in this GitHub organisation (go-workflows,
node-workflows, β¦). It carries the canonical Linux Foundation pipeline
skeletons and patterns β not a real pipeline: language-specific
steps are # TEMPLATE:-marked placeholders that instantiators
replace with real actions. python-workflows is the language-specific
reference implementation of these patterns.
| Workflow | Purpose | Trigger style |
|---|---|---|
.github/workflows/build-test.yaml |
Build, test, audit, SBOM and Grype scan skeleton | Pull request |
.github/workflows/build-test-release.yaml |
Release skeleton (Model A, tag-driven): tag validation, release artefact attachment and draft-release promotion | Tag push |
.github/workflows/merge.yaml |
Merge/publish skeleton (Model B, merge-driven): snapshot publish on every merge plus releases/ file-triggered release publish |
Merge / push to main |
All three take the same five harden_runner_* inputs, which
Egress allow-list describes.
Each pipeline runs a repository-metadata job in parallel (an
informational step that does not gate the build). After build, the
test, audit and SBOM/Grype branches run in parallel - none gates
another, so a pull request surfaces every failure at once (jobs in
{ } run concurrently; -> denotes sequence):
build -> { tests | audit | sbom -> grype }
The release skeleton wraps this with tag-validate up front and a
release-promotion chain at the end. It also defers tests until
audit and grype have both passed, so a failing audit skips the
test suite and never reaches release promotion:
tag-validate -> build -> { audit | sbom -> grype } -> tests
-> attach-artefacts -> promote-release
The merge skeleton implements the Jenkins-heritage LF/Gerrit model:
{ resolve-version | build } -> snapshot-publish
check-release -> release-publish (when a release file merged)
Repositories built from this template support both release models so consumers can adopt either β or migrate between them β without divergent behaviour:
- Model A β tag-driven (
build-test-release.yaml): the version comes from a validated, signed semver tag; a GitHub release carries the attested artefacts. Canonical for GitHub-native projects and Gerrit projects whose tags replicate to the mirror. - Model B β merge-driven (
merge.yaml): every merge publishes a snapshot (version from committed metadata such asversion.propertiesβX.Y.Z-SNAPSHOT); a release triggers from a committed release file (releases/*.yaml) in the merged change. Canonical for Jenkins-heritage LF/Gerrit projects publishing to Nexus.
Copy a template from examples/ into your project's
.github/workflows/ directory and replace the placeholder uses: SHA
with a pinned release. Each workflow ships in two forms:
github.yamlβ a plain GitHub-native caller (pull-request, tag-push or push-to-main triggered).gerrit.yamlβ a Gerrit-wrapped caller for projects where Gerrit is the source of truth (SCM), integrating withgerrit_to_platformvoting/commenting.
examples/
build-test/ { github.yaml, gerrit.yaml }
build-test-release/ { github.yaml, gerrit.yaml }
merge/ { github.yaml, gerrit.yaml }
All inputs are optional and default to the canonical behaviour; see the
inputs: block at the top of each workflow file for the full,
documented list.
The release skeleton shells out to gh, which posts usage events to
cafe.github.com unless told otherwise. The reusable workflow and the
build-test-release caller examples both set:
env:
GH_TELEMETRY: 'false'Three reasons this matters here:
- No step needs that endpoint. The GitHub CLI sends telemetry from a
separate hidden
gh send-telemetrysubcommand on a two-second timeout, so disabling it leaves everyghcommand working as normal. - The organisation runs harden-runner under
egress-policy: block, so an unnecessary call shows up as a blocked connection. That noise competes with genuine findings in the run insights. - Adding
cafe.github.comto the shared allow-list instead would widen egress across every repository in the organisation to carry analytics. Disabling the call at source keeps that policy tight.
GH_TELEMETRY takes precedence over DO_NOT_TRACK, so this one
variable covers both. A workflow-level env: block does not cross the
workflow_call boundary, which is why the reusable workflow and the
caller examples each set it.
Any caller-side job stacked after the release job β a registry publish step, for instance β inherits the value from the caller workflow.
Every job in the three skeletons loads its egress allow-list through
harden-runner-block-action
and then runs harden-runner on it. The skeletons take the same five
inputs for this and pass them to every block-action step they run.
A repository instantiated from this template keeps the whole set:
every *-workflows repository in the organisation exposes it, so a
caller can move between them without relearning the inputs.
| Input | Type | Default | Passed to block-action as |
|---|---|---|---|
harden_runner_egress |
string |
block |
harden-runner's egress-policy |
harden_runner_allowlist |
string |
the organisation list, pinned | config |
harden_runner_supplemental_allowlist |
string |
'' |
supplemental_config, with supplemental_required when set |
harden_runner_supplemental_unpinned |
boolean |
false |
supplemental_unpinned |
harden_runner_extra_endpoints |
string |
'' |
extra_endpoints |
tests/test-harden-runner-inputs.sh
fails when a workflow drops one of these inputs, changes its type,
default or description, passes it in another form, or lets a
block-action step carry on past a missing named list. It names
nothing specific to this repository, so an instantiated repository
keeps it unchanged; the harden-runner-inputs hook in
.pre-commit-config.yaml runs it whenever a workflow changes.
The organisation list covers the hosts every project needs. A project
that reaches hosts of its own, such as ONAP's registry.nordix.org,
names a supplemental list, which the action merges with the baseline
in the same coordinate grammar. An organisation that keeps its list in
its own .github repository can follow it at the default branch
rather than a pinned commit, so a host added there takes effect on the
next run without a pin bump in every caller:
with:
harden_runner_supplemental_allowlist: 'onap//'
harden_runner_supplemental_unpinned: trueonap// names the onap organisation's .github repository with no
file path, so the action searches that repository, at its default
branch, for the organisation's list. From an onap repository it
tries these files in order, and the first that exists wins:
.github/harden-runner/onap/allow_list.txt, the organisation's own list;.github/harden-runner/allow_list.txt, the repository-wide default;.github/harden-runner/<org>/allow_list.txt, when a single organisation directory carries that file (the sole-org fallback; two or more such directories match nothing).
Putting the list at the first path keeps the search unambiguous. When none of the three exists, the job fails, because the workflows require a supplemental list a caller names (see A missing list fails the job). The Search / fallback chain section of the action's README describes the search in full.
The action allows an unpinned list from the caller's own
organisation alone, so onap// unpinned works from an onap
repository and nowhere else.
A supplemental list from any other organisation must carry a ref,
and harden_runner_supplemental_unpinned stays false. The action
refuses a cross-organisation list that names no ref (or @HEAD),
which is what following the default branch means in its grammar. It
does not judge the ref itself: <org>//<path>@main passes the check
and moves with that branch. Pin a commit SHA,
<org>//<path>@<commit SHA>, so that a push to the other
organisation's repository cannot change what your runners may reach.
Whenever a caller names harden_runner_supplemental_allowlist, the
workflows also pass supplemental_required: 'true'. A caller naming a
list depends on it, so a list the action cannot find fails the job at
the block-action step, before anything runs, rather than at the first
blocked connection. This holds under harden_runner_egress: audit
too: no block-action step in the skeletons tolerates its own failure.
harden_runner_extra_endpoints adds host:port endpoints to the
allow-list inline, for example while a supplemental list waits for a
review:
with:
harden_runner_extra_endpoints: 'registry.nordix.org:443'Treat it as a stopgap. Endpoints passed inline live in one caller, so every other caller that needs the host fails until it copies them; the lasting fix is to add the host to the organisation's list or to a supplemental list. The action validates the endpoints before it fetches anything, and reports any the list already carries as redundant. The Inline extra endpoints section of the action's README gives the accepted format.
An unpinned list can change between two runs of the same commit.
harden-runner-block-action records which list it read and the
commit it resolved in its supplemental_source and supplemental_sha
outputs, which together say which revision of the list a given run
allowed.
When creating a new <lang>-workflows repository from this template:
- Replace every
# TEMPLATE:placeholder step in the three skeleton workflows with the real language build/test/audit/SBOM/publish actions, keeping the step ids and job outputs intact. The surrounding job graph, harden-runner wiring, dual checkout, Gerrit validation and Grype scan are generic β keep them as-is. - Wire real fixture/consumer repositories into
testing.yamlso the workflows run end-to-end against real projects on every pull request. - Update this README and all badge/link slugs (
workflows-templateβ your repository name). - When new work talks to new/external endpoints, follow the central
allow-list process: raise a PR against
lfreleng-actions/.githubadding the hosts/ports to the org harden-runner allow-list, get it released, then pin the new tag's commit SHA in theharden_runner_allowlistdefaults. Hosts that one project alone needs belong in that project's supplemental list, andharden_runner_extra_endpointscovers the wait for either change to land; see Egress allow-list. Reserveharden_runner_egress: 'audit'for bring-up/endpoint discovery. Keep the fiveharden_runner_*inputs andtests/test-harden-runner-inputs.shas they are, so the check stays the same across the organisation. - Keep support for both release models (Model A and Model B), factoring version resolution so the two share building blocks.
- Update the
examples/callers to reference your repository and its real input surface.
The reusable workflows are Gerrit-aware: when a caller sets the
gerrit_refspec input they check out the change with
checkout-gerrit-change-action instead of actions/checkout. Vote and
comment casting live in the gerrit.yaml caller examples (clear vote β
run β report vote for verify; comments without votes for merge), never
inside the reusable workflows.
.github/workflows/testing.yaml
exercises the build-test skeleton against fixture repositories of
different languages, calling it by self-repository path
(uses: $/.github/workflows/build-test.yaml), which resolves this
repository at the commit already running. The placeholder
steps are language-agnostic, so the self-test validates the generic
scaffolding regardless of project language β which is the point of this
repository. The matrix legs run on the pinned baseline alone, so they
keep proving the default policy. A further leg sets the allow-list
inputs on one fixture: the organisation's own list as an unpinned
supplemental list, and an inline endpoint that list already carries.
The supplemental list follows lfreleng-actions/.github at its
default branch, so that leg may allow hosts the pinned baseline does
not yet; it proves the inputs reach the action, not the baseline.