This repository contains shared configuration and community health files for the lfreleng-actions GitHub organisation (Linux Foundation Release Engineering).
Community health files placed here are automatically inherited by all repositories in the organisation unless overridden at the repository level. Each entry below says how the rest reach repositories.
-
AGENTS.md— The default guidance document for AI coding agents working on any repository in the organisation. Covers commit format, signing and DCO, pre-commit hooks, testing gates, and pull request review cycles. It binds every contribution, including those from third parties and the agents acting on their behalf, even when the agent that produced it never read the file.Each repository carries a thin
AGENTS.mdthat points here, in the shape §12 of that document defines; a stub may add requirements, but cannot relax these. -
scripts/agents/— Reference implementations of the threeAGENTS.mdrules that are contracts with exit semantics: the AI-slop gate (aislop-gate.sh), the Change-Id preserving Gerrit amend (gerrit-amend.sh), and review-thread resolution (resolve-review-thread.sh). Each has a pytest suite underscripts/agents/tests/covering the false-pass cases review has found; the tests define what "correct" means, and agents in other repositories may run, vendor or reimplement the scripts against them.agent-scripts.yamlruns the suite on every pull request that touches them.
profile/README.md— The public-facing organisation profile displayed at github.com/lfreleng-actions. Contains a categorised directory of all actions, tools, and test fixtures in the organisation.
release-drafter.yml— Organisation-wide release-drafter configuration. Provides default categories, autolabeler rules (mapping Conventional Commits prefixes to labels), and version-resolver settings. Any repository without its own.github/release-drafter.ymlinherits this configuration automatically..github/harden-runner/— Shared harden-runner egress allow-lists. Organisation workflows run harden-runner inblockmode and loadallow_list.txtat runtime, so harden-runner denies any host the list omits. See Harden-runner egress allow-list below for the wildcard matching rules, which are easy to get wrong.
allow-list-bump.yaml— A scheduled (Monday 05:00 UTC) sweep that bumps stalestep-security/harden-runneregress allow-list pins across the*-workflowsfamily and opens a pull request per repository, authored by the organisation's bot. These pins are values ofconfig:anddefault:keys rather thanuses:references, so Dependabot cannot see them and they drift. See Allow-list bump sweep below for setup.allow-list-dns-check.yaml— A scheduled (daily, 04:30 UTC) check that every non-wildcard hostname in the harden-runner egress allow-list still resolves, on the default branch and at the latest release. harden-runner fails open when any such entry fails to resolve (it never pre-resolves wildcards, so neither does the check), so the workflow posts the offending records to#releng-scmfor removal or correction. A manual run can send a test message to verify the Slack wiring.repo-audit.yaml— Runs on a weekly schedule (Monday 10:00 UTC). Compares the current list of repositories in the organisation against the profile README and sends a Slack notification to#releng-scmwhen it finds new repositories that lack documentation or an explicit exclusion entry.zizmor.yaml— Organisation-wide static security audit of GitHub Actions workflows and composite actions using zizmor. Runs on every pull request and push, uploads results to GitHub code scanning as SARIF on pushes to the default branch after merge, and runs in advisory mode (does not block merges), and the workflow runs across the organisation as a required workflow via an organisation ruleset; see Organisation-wide zizmor audit below for the one-time org-admin configuration.zizmor-sarif-publish.yaml— A scheduled (weekday) workflow that publishes zizmor findings to each in-scope repository's default-branch code scanning. The ruleset-drivenzizmor.yamlruns on pull requests, so it never populates the default-branch alert list; this workflow closes that gap so downstream reporting reflects reality. See Organisation-wide zizmor SARIF publisher below for setup.aislop.yaml— Organisation-wide AI-slop / code-quality scan of pull request changes using aislop. Runs on every pull request across the organisation as a required workflow via an organisation ruleset, scans the files the PR changes (not the whole repository), and blocks a pull request whose changed files carry any finding, at any severity. TheAISLOP_GATE_LEVELorganisation or repository variable relaxes or tightens that; see Organisation-wide aislop scan below for the one-time org-admin configuration.aislop-sarif-publish.yaml— A scheduled (daily) workflow that runs a full-repository aislop scan against each in-scope repository, publishes the findings to its default-branch code scanning as SARIF, and renders a ranked organisation-wide score table so the repositories needing the most attention are easy to spot. See Organisation-wide aislop SARIF publisher below for setup.agent-scripts.yaml— Runs the contract tests for theAGENTS.mdreference scripts underscripts/agents/on every pull request that touches them.
excluded-repos.json— A JSON file listing repository names to exclude from the weekly audit. This covers forks of upstream actions, placeholder/template repositories not yet developed, and backup directories that are not real repositories.
When a repository in the lfreleng-actions organisation runs the
release-drafter/release-drafter action and does not have its own
.github/release-drafter.yml, GitHub automatically falls back to the
configuration in this repository. The shared configuration uses the
$OWNER/$REPOSITORY template variables so that release notes, download
badges, and issue links resolve to the correct URLs for any inheriting
repository.
Repositories that need custom categories or version-resolver rules can
override the defaults by adding their own .github/release-drafter.yml.
Organisation workflows run
harden-runner under
egress-policy: block, which denies every outbound connection the
policy does not name. The permitted endpoints live in
.github/harden-runner/lfreleng-actions/allow_list.txt,
loaded at runtime by
harden-runner-block-action.
The directory README covers the file
format and layout.
harden-runner documents none of this. It comes from the source of its
agent, step-security/agent
(matchWildcardDomain and matchAnyWildcard in dnsproxy.go,
parseEndpoints in config.go), which is the same code on amd64 and
arm64. The
allow-list probe workflow
checks it on both architectures under egress-policy: block:
| Rule | Example |
|---|---|
A *.host wildcard matches any name ending in .host, at any depth |
*.ubuntu.com covers azure.archive.ubuntu.com |
| A wildcard does not match its bare apex | sonarcloud.io:443 sits beside *.sonarcloud.io:443 |
| A wildcard opens one port: the first listed for that pattern | *.ubuntu.com:443 then *.ubuntu.com:80 opens 443 alone |
| An explicit entry overrides any wildcard for its host, and opens its own ports alone | archive.ubuntu.com:80 alone would close 443 to that host |
The agent resolves a wildcard-matched name when a job looks it up, then
opens its address on the wildcard's first port. This sorted file puts
:443 first, so a *.host:80 line never takes effect: a host reached
over plain HTTP, as apt reaches its mirrors, needs explicit entries for
every port it uses. Note that egress-policy: audit cannot show any of
this, because audit mode never enforces allowed-endpoints.
An August 2026 probe read the third rule as a label limit:
*.ubuntu.com admitted archive.ubuntu.com over HTTPS but refused
azure.archive.ubuntu.com, which apt reaches over HTTP. The same rule
later broke every arm64 job that installed packages, once
harden-runner v2.22.0 began enforcing block mode on arm64: the arm64
runners' mirror, ports.ubuntu.com, resolved through *.ubuntu.com,
and its port 80 stayed closed.
Two consequences when editing the allow-list:
- Name a host explicitly, with every port it needs, when a workflow reaches it on any port other than its wildcard's first. Deleting such an entry as "redundant" breaks the workflows that reach it.
- Choose wildcards with their reach in mind: one admits every host
below it, so
*.blob.core.windows.netwould admit any Azure storage account, anyone's included.
One gap no allow-list can close: Fedora, EPEL, CentOS Stream, Rocky, AlmaLinux, openSUSE and Arch ship repository configuration that resolves a metalink or mirrorlist, then downloads from whichever third-party mirror it returns. Those mirrors share no parent domain and change by source IP and by the hour, so a vendor wildcard admits the mirror list and then the next connection fails.
Pin the repository baseurl to the vendor-owned HTTPS host and clear
metalink/mirrorlist in the job instead. The allow-list header names
the host to use for each distribution.
The zizmor.yaml workflow performs static security analysis of GitHub
Actions workflows and composite actions across every repository in the
organisation. It uses zizmor, which detects
common security defects including template-injection vulnerabilities,
credential persistence (artipacked), excessive permission scopes,
dangerous triggers (pull_request_target), unpinned uses: references,
and more.
- Output: SARIF, uploaded to GitHub code scanning on pushes to
the default branch (i.e. after merge). PR runs skip
the SARIF upload: fork PRs cannot perform the upload because
GITHUB_TOKENlackssecurity-events: writethere, and uploading on PRs would publish unreviewed findings before reviewers approve the change. Findings appear in each repository's Security → Code scanning tab once the change lands on the default branch. - Severity floor:
low(the workflow reports findings down to the low tier; the informational and unknown tiers stay out to limit noise). - Persona:
auditor(the broadest persona; it runs the extra audit types on top of the default high-signal set). - Advisory: zizmor exits
0when emitting SARIF, so the workflow always reports success in the PR checks UI. Merge-blocking remains disabled at the workflow level on purpose. After the team triages the pre-existing finding backlog across the organisation, an org-level code-scanning ruleset can switch selected findings to merge-blocking (see Promoting findings to merge-blocking below).
Unlike release-drafter.yml (which GitHub auto-inherits from the
.github repository), workflow files in .github/workflows/ are
not automatically run for other repositories. To execute
zizmor.yaml against every repository in the organisation without
copying it into each repo, configure it as a required workflow via
an organisation ruleset:
- Go to Organisation settings → Repository → Rulesets (you must be an organisation owner).
- Click New ruleset → New branch ruleset.
- Set:
- Ruleset name:
zizmor security audit - Enforcement status:
Active - Bypass list: leave empty
- Target repositories:
All repositories(or use Dynamic list of repositories with property filters to limit scope; the initial rollout should target All repositories). - Target branches:
Default branch(andmasterif you have repositories still using that name; or useInclude by patternwithmainandmaster).
- Ruleset name:
- Under Rules, enable Require workflows to pass before merging
and click Add workflow:
- Repository:
lfreleng-actions/.github - Workflow file path:
.github/workflows/zizmor.yaml - Ref:
main
- Repository:
- For the initial advisory rollout, leave Do not require workflows to pass before merging checked so the workflow runs without blocking merges (advisory mode is also reinforced by the SARIF output, which causes zizmor to exit 0). Later, after the team clears the backlog, uncheck this option to make the workflow required before merging.
- Click Create.
After saving, every pull request opened in the organisation will
trigger a zizmor run sourced from lfreleng-actions/.github. The
checks appear in PRs as
🌈 Zizmor Scan / Audit workflows,
and findings populate the target repository's Security → Code
scanning tab once the change lands on the default branch.
The audit logic and the pinned zizmor version both live in the
zizmor-scan-action
composite action. zizmor.yaml pins that action by commit SHA, and the
action reads its zizmor==<version> pin from its own bundled
pyproject.toml at run time, so this workflow embeds no version string.
The action's own repository owns the zizmor version. Dependabot's uv
ecosystem opens a weekly PR there; merging it cuts a new action
release. Dependabot in this repository then
bumps the pinned action ref under its github-actions ecosystem
(CI(actions): Bump lfreleng-actions/zizmor-scan-action ...). After
that PR merges, every audited repository picks up the new version on
its next run. A 7-day cooldown blocks churn on releases that upstream
retracts or supersedes within days.
To upgrade manually, bump the pinned action ref in
.github/workflows/zizmor.yaml and
merge a PR through the normal review process.
After the team triages the existing backlog of findings across the
organisation (auto-fixed via zizmor --fix, suppressed via inline
# zizmor: ignore[rule] comments, or addressed in a per-repo
zizmor.yml configuration), a code-scanning ruleset can promote
individual rules — or all rules at a chosen severity — to
merge-blocking:
- Organisation settings → Repository → Rulesets → New ruleset → New code scanning ruleset.
- Add
zizmor(the SARIF category) as a tool, set the alert threshold toerror(or the desired severity), and target the default branch.
Until an org admin completes that step, zizmor operates purely as a
reporting tool.
The zizmor.yaml audit runs on pull_request events (delivered by an
organisation ruleset) and never uploads SARIF to a repository's
default-branch code scanning — organisation rulesets do not deliver a
post-merge push event to the target repositories. As a result, each
repository's default-branch Security → Code scanning alert list stays
empty even when its workflows contain zizmor findings, and anything built
on that data (e.g. github-security-report-action) under-reports zizmor
accordingly.
The zizmor-sarif-publish.yaml workflow closes that gap. On a weekday
schedule (0 6 * * 1-5, 06:00 UTC) it:
- Enumerates the in-scope repositories for the target organisation
(via the shared
repo-discovery.yamlreusable workflow). - Applies the repository scope policy from
.github/scan-scope.json— the per-organisation scan toggles andinclude/excludeglobs, plus any patterns from theZIZMOR_SCAN_INCLUDE/ZIZMOR_SCAN_EXCLUDEvariables. - Checks out each repository at its default branch, runs
zizmor-scan-action, and uploads the SARIF to that repository's code scanning viaPOST /code-scanning/sarifsagainst the default-branch HEAD (so results land in the default-branch alert list, not a PR-scoped analysis).
A final job renders the organisation posture summary to the run's step
summary. When a scheduled run fails or a maintainer cancels it, the
workflow posts a Slack alert to #releng-scm (reusing the
SLACK_BOT_TOKEN secret and SLACK_CHANNEL_ID variable documented
under Slack Setup), so a silent daily breakage never
slips past the team. Manual workflow_dispatch runs stay quiet, since
a maintainer already watches those.
Writing code-scanning results to other repositories is beyond what the
default GITHUB_TOKEN can do, so the workflow needs a dedicated PAT in
the ZIZMOR_SARIF_PAT repository secret. A classic PAT needs the
scopes:
| Scope | For |
|---|---|
repo |
check out repositories (incl. private) |
read:org |
list organisation repositories |
security_events |
upload SARIF / write code-scanning alerts |
A fine-grained PAT scoped to the organisation works too: Contents read, Metadata read, and Code scanning alerts write.
Run workflow also triggers the workflow on demand, with three optional inputs (all blank by default):
org— scan a different organisation. When blank, the workflow targets the organisation it runs in (github.repository_owner), which matches the scheduled run. The workflow reads that organisation's scope policy from.github/scan-scope.json; an organisation with no entry falls back to the default toggles and the scan variables alone.repo— restrict the run to the named repositories, for smoke-testing or to refresh results for a batch of new repositories. Give bare names, separated by commas or spaces (repo-a, repo-b repo-c). The run fails, naming each culprit, when an entry is not a bare repository name or the API cannot read the repository. The scope policy still applies: the workflow drops, with a warning, any named repository that the toggles or scope policy remove (a skipped category such as archived, fork, template, private or disabled, anexcludematch, or a repository outside a configuredincludeallow-list), and publishes nothing when that leaves none.token— override theZIZMOR_SARIF_PATsecret for an ad-hoc run. When blank, the workflow uses the secret. Caution: GitHub stores and showsworkflow_dispatchinputs in the run parameters without masking them, so prefer the secret for routine use and reserve this input for testing.
The aislop.yaml workflow scans pull request changes across every
repository in the organisation using
aislop, which detects the
hallmarks of low-quality or AI-generated "slop" code: swallowed
exceptions, narrative/meta comments, formatting drift, lint errors,
excessive complexity, and basic security issues. It scores the scanned
files 0–100 and reports each finding with severity and location.
-
Scope: the files changed on the pull request, and nothing else (via
aislop ci --changes --base <PR base branch>), so the check reports on what the PR introduces rather than pre-existing repository state. Whole-repository scanning belongs to the scheduledaislop-sarif-publish.yamlworkflow. -
Implementation: the scan itself runs through
aislop-scan-action(pinned by commit SHA), which installs the aislop CLI from its bundled, integrity-checked lock file, renders the step summary, and exposes the gate result as an output. -
Output: a step summary with the score, per-engine issue counts, severity/rule breakdowns and the top findings, plus inline PR annotations for the top findings. No SARIF upload happens on pull requests.
-
Gating: the scan step always succeeds; a separate gate step decides enforcement based on the organisation variable
AISLOP_GATE_LEVEL:any(the default when the variable is unset) — the run fails when the changed files carry any finding, at any severity: high, medium and low all block. Because the scan scope is the pull request's changed files, passing this gate means the pull request adds no findings.high— the run fails on high-severity findings alone (an aislop error-severity diagnostic). Medium and low stay advisory. This was the previous default; use it as a per-repository escape hatch while clearing an existing backlog.all— the run fails on the full aislop quality gate (score below the configured threshold or any high-severity finding).off— advisory: the workflow reports without blocking.
A repository variable of the same name overrides the organisation value, so a repository carrying a backlog can sit at
highwithout weakening the estate-wide default.For backward compatibility, when
AISLOP_GATE_LEVELis unset the legacyAISLOP_ENFORCE= truevariable still selects thealltier. Changing the level requires no workflow change. -
Reduced coverage: parts of the scan depend on services outside the runner. aislop's dependency audit queries the npm registry's advisory endpoint with the whole dependency tree, and under registry load that request can exceed aislop's 25 s audit timeout. aislop records the gap as a coverage notice (
security/dependency-audit-skipped; likewisedotnet/projects-skippedandcppcheck/chunks-skipped) rather than a finding: its own rationale is "visibility loss, not evidence of a defect". The gate leaves those three rules out of every finding count and instead raises a job-level warning titled aislop scan coverage degraded naming the underlying error, so a registry outage reads as an infrastructure condition rather than as a finding the pull request added. A clean result with reduced coverage is not conclusive; re-run the check once the service recovers. Set theAISLOP_STRICT_COVERAGEorganisation or repository variable totrueto make reduced coverage block at the enforcing gate levels. A missing engine binary (engines-ready=falsefrom the action) counts as reduced coverage in the same way. -
Version pin: the aislop CLI version pin lives in
aislop-scan-action(its bundledpackage.json/package-lock.json); bump it there via a PR and release, then update the action pin here.
Configure aislop.yaml as a required workflow via an organisation
ruleset, mirroring the zizmor audit:
- Go to Organisation settings → Repository → Rulesets (you must be an organisation owner).
- Click New ruleset → New branch ruleset.
- Set:
- Ruleset name:
aislop code quality scan - Enforcement status:
Active - Bypass list: leave empty
- Target repositories:
All repositories - Target branches:
Default branch(andmasterif you have repositories still using that name).
- Ruleset name:
- Under Rules, enable Require workflows to pass before merging
and click Add workflow:
- Repository:
lfreleng-actions/.github - Workflow file path:
.github/workflows/aislop.yaml - Ref:
main
- Repository:
- The
anygate level is the default, so the check blocks a merge whenever a pull request's changed files carry a finding of any severity once you enable Require workflows to pass before merging. To report without blocking first, either leave Do not require workflows to pass before merging checked or set theAISLOP_GATE_LEVELorganisation variable tooff;highrestores the earlier behaviour, which blocked on high severity alone, andallenforces the score threshold as well. - Click Create.
After saving, every pull request opened in the organisation will
trigger an aislop run sourced from lfreleng-actions/.github. The
checks appear in PRs as AI Slop Scan 🧹 / Audit changes.
The aislop.yaml check runs on pull_request events and scans
changed files alone, so it never populates a repository's
default-branch
code scanning. The aislop-sarif-publish.yaml workflow closes that
gap. On a daily schedule (0 7 * * *, 07:00 UTC — offset from the
zizmor publisher at 06:00) it:
- Enumerates the in-scope repositories for the target organisation
(via the shared
repo-discovery.yamlreusable workflow — the same discovery the zizmor publisher uses, so the two scanners always cover the same repository selection). - Applies the repository scope policy from
.github/scan-scope.json, plus any patterns from theAISLOP_SCAN_INCLUDE/AISLOP_SCAN_EXCLUDEvariables. - Checks out each repository at its default branch, runs a
full-repository scan with
aislop-scan-action, and uploads the SARIF to that repository's code scanning viaPOST /code-scanning/sarifsagainst the default-branch HEAD. Findings appear in each repository's Security → Code scanning tab under theaisloptool. - Aggregates per-repository score metrics into a ranked, worst-first table in the run's step summary, answering "which repositories have the biggest problems?" at a glance. A Coverage column marks repositories whose scan ran with reduced coverage (an audit or analyzer could not run, or an engine binary was missing), and the summary counts them up front, so a bad day at the npm registry reads as one infrastructure event rather than as a wave of newly clean repositories.
When a scheduled run fails or a maintainer cancels it, the workflow
posts a Slack alert to #releng-scm (reusing the SLACK_BOT_TOKEN
secret and SLACK_CHANNEL_ID variable documented under
Slack Setup), so a silent daily breakage never slips
past the team. Manual workflow_dispatch runs stay quiet, since a
maintainer already watches those.
Writing code-scanning results to other repositories is beyond what
the default GITHUB_TOKEN can do, so the workflow needs a dedicated
PAT in the AISLOP_SARIF_PAT repository secret. The aislop and
zizmor publishers use separately scoped credentials by design — they
do not share secrets. A
classic PAT needs the scopes repo, read:org and
security_events; a fine-grained PAT scoped to the organisation needs
Contents read, Metadata read, and Code scanning alerts write.
Run workflow also triggers the workflow on demand, with the same
three optional inputs as the zizmor publisher (org, repo, and
token); see
Manual dispatch inputs above for their
semantics. The token input falls back to the AISLOP_SARIF_PAT
secret when blank.
The zizmor-sarif-publish.yaml and aislop-sarif-publish.yaml
workflows read their scope (through the shared
repo-discovery.yaml
reusable workflow) from
.github/scan-scope.json, keyed
by organisation. Each organisation provides four boolean scan toggles and
two glob pattern lists:
{
"organisations": {
"example-org": {
"scan_archived": false,
"scan_forks": false,
"scan_templates": true,
"scan_private": false,
"include": [],
"exclude": ["*-sandbox"]
}
}
}The toggles opt extra repository categories into the scan. Each defaults to
false, so a bare configuration scans normal, active, public, first-party
repositories.
| Toggle | When true |
|---|---|
scan_archived |
also scan archived repositories |
scan_forks |
also scan forks |
scan_templates |
also scan template repositories |
scan_private |
also scan private repositories |
The workflow skips a repository when one of its categories has the matching toggle off. It also skips disabled repositories, because GitHub blocks their checkout.
After the toggles, include/exclude globs filter by repository name:
- Case-insensitive globs — patterns support
*(any run),?(one character), and[..]character classes; a pattern with no wildcards is an exact name. Separator-aware patterns such as*[-_./]test[-_./]*match atestpath segment without catching names likelatestorattestation. exclude— drops a repository whose name matches any exclude pattern.include— acts as an allow-list: a non-empty list keeps the repositories matching at least one include pattern (excludestill removes matches afterwards). An emptyincludelist keeps every repository.
Two optional repository variables extend the policy without editing the JSON, using the same glob syntax (values separated by commas, spaces, or newlines):
ZIZMOR_SCAN_INCLUDE— extrainclude(allow-list) patterns.ZIZMOR_SCAN_EXCLUDE— extraexcludepatterns.
The aislop-sarif-publish.yaml workflow honours the analogous
AISLOP_SCAN_INCLUDE / AISLOP_SCAN_EXCLUDE variables, so the
two scanners can diverge at run time without editing the shared JSON.
The workflow merges the variable patterns with the organisation's JSON lists before matching.
A caller with a standing scope of its own passes it as
default-include: the same glob syntax, which discovery applies when
the run names no repository and drops when it names one, so a named
repository outside that scope is still swept. The allow-list bump
sweep uses it for the *-workflows family. Discovery makes the
decision after splitting the names, so input consisting of separators
counts as naming nothing and keeps the default, rather than lifting it
and widening the run to the whole organisation.
The workflow-repo family pins the step-security/harden-runner egress
allow-list with a custom uses:-style coordinate consumed by
harden-runner-block-action:
config: '@18d9c4446bea555d0783e850f6d295f844fe8f67' # v0.1.1Because these are values of config: and default: keys rather than
uses: references, Dependabot cannot see them and they drift. A stale
pin gives no warning: it omits newly allow-listed endpoints, so
block-mode jobs fail later with confusing ECONNREFUSED errors against hosts added
to the allow-list weeks earlier.
Every Monday, allow-list-bump.yaml enumerates the in-scope
repositories, clones them side by side, runs gha-workflow-linter
once across the whole set with --multi-repo --allow-list update, and
opens a pull request wherever a pin moved. A Slack digest links every
pull request.
One sweep rather than a job per repository: a single hardened runner, one toolchain install and one linter process cover the whole family, and the job that produces the digest also sends it, rather than passing it between jobs as artefacts. A multi-repository run also shares one resolution cache, so the family costs one lookup of the allow-list host rather than one per repository.
The scheduled sweep covers the *-workflows family, and nothing
else. Those repositories host the reusable build workflows consumers
run, where a missing endpoint breaks real builds in downstream
projects. Standard GitHub plumbing elsewhere in the organisation
touches a small, stable endpoint set, so a pin some versions behind has
little practical effect, and weekly pull requests against it would
create review noise.
A manual run serves the other case: selective updates to repositories
an operator names. The repo input takes repository names (commas,
spaces or newlines; the
list-splitter-action
in discovery validates and deduplicates them), and the sweep then
covers those repositories and no others, inside the family or not.
Discovery reports every name it cannot read before it fails, so a
list with two typos needs one corrected re-run. To widen the family
for one run instead, leave repo blank and supply extra-include
globs. A manual run may also set linter-version to try a newer
linter release on that run alone (see Behaviour). The
ALLOW_LIST_BUMP_EXCLUDE variable removes repositories from every
run, named or not, and discovery warns about each named repository it
removes.
- Runs with
--action-calls report, so each pull request carries a diff confined to allow-list pins. Action version bumps belong to Dependabot, and mixing them makes the change harder to review and harder to revert. - The pinned reference and its version comment change, and nothing else: quoting style, spacing and comment position all survive.
- Every pin moves to the newest release of its host, with no cooldown.
Each repository in the family sets a seven-day Dependabot cooldown
for its action calls, and the linter would apply it to these pins
too, so the sweep passes
--cooldown 0. The allow-list grows far more often than it shrinks, an addition grants an endpoint and takes nothing away, and each change passes review and the block-mode probe in this repository before anyone tags it, so holding a release back for a week serves nothing and delays the fix for a broken build. The sweep rewrites allow-list pins and nothing else, so the override governs nothing else. Dropping the cooldown also lets the linter keep its resolution cache across the sweep, which a cooldown made it bypass. - The sweep leaves alone any pin carrying an
allow-list-pin-okdirective. - Each repository carries at most one sweep pull request, on a fixed
chore/allow-list-bumpbranch. A newer release supersedes the open proposal in place rather than opening a second one beside it. A re-run leaves the branch untouched when the open pull request already carries the identical change, so an unreviewed pull request does not reset its own review state every week; it rewrites the branch when the target version moves or a pin appears that the proposal does not yet cover. The comparison spans the files the pull request already changes and the files this run rewrites, matching the two sets and then their contents, so a commit merged into the default branch for unrelated reasons does not force a rewrite. It looks at file contents alone, so a hand-edited title or a description that has fallen out of step gets put right without a commit. Comparing the description folds out the link to the sweep run that last touched it, which would otherwise differ every week. - Each rewrite builds its commit on a staging ref at the default branch and then moves the proposal branch onto it. The proposal branch must never point at the default branch, even for an instant, because GitHub closes a pull request the moment head and base coincide, taking its number, review threads and approvals with it. Building on the default branch also keeps a workflow added there since the proposal opened from landing as an add/add conflict, and keeps an unrelated edit to a proposed file out of the diff under review.
- The default branch can move while the sweep runs, between taking the checkouts and writing. Each proposal builds on where that branch now stands, not where the checkout found it. Where it moved under a file the sweep rewrites, the checkout no longer describes that file and any proposal from it would revert whatever landed, so the sweep leaves that repository for the next run — whose checkout includes the change — and names it in the run and the digest. Retiring a proposal waits on any movement at all, since it rests entirely on the checkout showing nothing to do and no later run can reopen what it closes.
- A proposal that no longer changes anything gets retired. When the default branch reaches the selected release by some other route, or its pins already sit ahead of it, the sweep closes the open pull request, deletes the branch and says so in the digest, rather than leaving a no-op sitting in the review queue. A repository that has removed its last workflow counts here too: the linter then reports a clean result with no findings, and its proposal has nothing left to propose. A repository whose allow-list host would not resolve gets nothing done to it: with the host unknown, "nothing to fix" and "could not look" are indistinguishable, and the run fails rather than acting on the difference.
- A stale pin the linter can see but cannot rewrite — one inside a multi-line scalar, for instance — leaves the repository neither current nor fixable. The sweep names it in the run and in the digest and leaves every proposal alone, rather than retiring a pull request in a repository that is still stale. Where such a repository already carries an open proposal, the digest links and counts it as open, and notes what still needs doing by hand.
- Nearly every repository pins one allow-list host, and the pull request title names the release it moves to. A repository pinning more than one resolves more than one release, so naming a single version would describe part of the change as though it were the whole. Those pull requests carry a title without a version, list every host and its release in the body and the commit message, and the run warns that it found more than one.
- Pull requests come from the organisation's
lf-releng-botGitHub App, not from a maintainer. Code review is mandatory across the organisation and nobody may approve their own work, so a sweep raising pull requests as a person would produce changes that person could not merge. - Commits reach the branch through the GitHub API rather than a push, so
GitHub signs them. The default branch of every repository in scope
carries a
required_signaturesrule, which an unverified commit would fail. - A
dry-rundispatch input reports what would change without opening anything. It needs no bot credential: the credential check warns about a missing one instead of failing the run, the sweep mints no token, and the reads the step still makes go through the ephemeral token. - The linter installs from
uv.lock, through thesweepdependency group in this repository'spyproject.toml, matching the commit-pinned actions around it. The job checks out those two files and nothing else, without credentials, and installs withuv sync --locked, so a lock out of step withpyproject.tomlfails the run rather than re-resolving. An unpinned install would let one bad release reach every repository in the sweep at once, and pinning the top-level package alone would leave its dependencies to resolve afresh on every run; the lock fixes the whole tree by hash. Dependabot proposes the bump when the linter releases. The other dependencies keep the seven-day cooldown; the linter sits outside it, since this organisation reviews its releases and the sweep exists to run a new release the week it ships. - A
linter-versiondispatch input overrides the lock for one run, so an operator can try a new linter release against the repositories named inrepobefore anyone moves the lock. That run installs the release unlocked by design, with its dependencies resolved afresh, since the lock cannot hold a release nobody has adopted yet. It accepts a plain release version such as1.9.0and fails the job on anything else, since the value becomes part of the install specifier; blank uses the lock, and the log carries a notice whenever an override applies. The schedule always runs the lock. The override is an input rather than a repository variable because a variable would move the one tool pin in this workflow out of git, away from review, CI, diff and blame, and would keep overriding the lock until someone removed it.
| Name | Kind | Purpose |
|---|---|---|
LF_RELENG_BOT_CLIENT_ID |
Variable | Client ID of the lf-releng-bot GitHub App, shared with the other bot-authored workflows |
LF_RELENG_BOT_PRIVATE_KEY |
Secret | Private key (PEM) for that App. The sweep mints a short-lived installation token scoped to the repositories in the sweep |
ALLOW_LIST_BUMP_EXCLUDE |
Variable | Optional extra exclude globs |
SLACK_BOT_TOKEN |
Secret | Shared with the other scheduled workflows |
SLACK_CHANNEL_ID |
Variable | Shared with the other scheduled workflows |
The bot App must have write access to contents, pull requests and
workflows on every repository the sweep covers. workflows matters in
its own right: every file the sweep rewrites lives under
.github/workflows. Minting the token fails outright when a repository
in scope is missing from the installation.
The bot's token writes and does nothing else. Repository discovery and
the linter's own lookups use the ephemeral GITHUB_TOKEN: listing an
organisation's public repositories needs no more, and
.github/scan-scope.json sets scan_private to false. The lookups read
public release data right across GitHub — the allow-list host, and every
organisation whose actions these workflows call — almost none of which
lies inside the App's repository selection. A cold-cache sweep of the
whole family measured 2 REST calls and 174 GraphQL points, against a
limit of 1000 per hour for each.
All jobs run in the production environment, so environment protection
rules can guard the App key and the Slack token.
The repo-audit.yaml workflow:
- Lists all repositories in the organisation via the GitHub API
- Filters out repositories named in
excluded-repos.json - Parses the profile README for documented repository links
- Compares the two sets and identifies any undocumented repositories
- Posts a summary to the GitHub Actions job output
- Sends a Slack notification to
#releng-scmif any updates need attention
The workflow requires:
SLACK_BOT_TOKEN— A repository secret containing a Slack bot token withchat:writepermission for thelinuxfoundation.slack.comworkspaceSLACK_CHANNEL_ID— A repository variable containing the channel ID for the#releng-scmchannel
See the Slack setup instructions below.
The excluded-repos.json file contains a JSON object with an
excluded array of repository names (not full paths) to skip
during the audit. Typical exclusions include:
- Forks of upstream actions (e.g.
gh-action-pypi-publish) - Repositories still at the template/placeholder stage
- Backup or archive directories
To add a new exclusion, edit excluded-repos.json and add the
repository name to the excluded array.
The repository audit workflow, the allow-list DNS check and the
scheduled zizmor / AI slop SARIF publishers send notifications using
the official
Slack GitHub Action.
They reuse the same SLACK_BOT_TOKEN secret and SLACK_CHANNEL_ID
variable, and post to #releng-scm: the publishers when a scheduled
run fails, and the DNS check when an allow-list record no longer
resolves. Complete the following one-time setup steps:
- Go to api.slack.com/apps and sign in
to the
linuxfoundation.slack.comworkspace - Click Create New App → From scratch
- Name it
LF RelEng GitHub Notifications(or similar) - Select the
linuxfoundationworkspace
- Navigate to OAuth & Permissions in the app settings
- Under Bot Token Scopes, add:
chat:write— to post messageschat:write.customize— to customise the bot name/icon per message
- Click Install to Workspace and authorise the app
- Copy the Bot User OAuth Token (starts with
xoxb-)
In Slack, open the #releng-scm channel and run:
/invite @LF RelEng GitHub Notifications
- In Slack, right-click the
#releng-scmchannel name - Select View channel details
- The Channel ID is at the bottom of the details panel (e.g.
C0123456789)
In the .github repository settings:
- Go to Secrets and variables → Actions
- Add a Repository secret:
- Name:
SLACK_BOT_TOKEN - Value: the
xoxb-token from step 2
- Name:
- Add a Repository variable:
- Name:
SLACK_CHANNEL_ID - Value: the channel ID from step 4
- Name:
For instructions on tagging and releasing actions in this organisation, see the organisation profile README.
Changes to this repository affect all repositories in the organisation.
Please open a pull request and ensure all pre-commit hooks pass before
merging. The repository uses the standard lfreleng-actions pre-commit
configuration including yamllint, actionlint, markdownlint, REUSE/SPDX
verification, and GitHub workflow schema validation.