Praetor manages this repository's declared governance policy. This managed block records adoption state; it is not a verification certificate.
Verification: make verify-all runs the repository's configured
verification cascade.
HISS Audit: praetorctl audit enforces policy, generated-surface
integrity, and the debt ratchet.
Context Sync: praetorctl compile-context --verify verifies every
generated agent context against AGENTS.md.
Debt Baseline: .standards-baseline.json anchors the debt ratchet at
0 recorded infractions; audit forbids growth.
Reusable workflows and templates for the repositories of the Plurano framework family: the jobs every repository runs (build, test, lint, security scans, release, landing) are kept once here and called from thin callers, instead of each repository keeping its own copy.
Greek horistes, the boundary commissioner who fixes limits. Pronounced ho-RIS-tees.
The repository is plurano/horistes. The name is final with the change of
visibility (ADR-0024):
callers and preset references carry it, and the forge does not redirect a
call after a rename (ADR-0023).
Records written before 2026-10-12 keep the working name shared-ci.
Public; nothing is released yet. The repository has its own gate and the
checks every workflow added here must pass. The first reusable workflow is the
licence check; this repository is its first caller and no longer keeps a
licence job of its own. The second is the scorecard run; this repository is
its first caller (.github/workflows/scorecard-check.yml), and other public
repositories can call it. The caller a consumer keeps exists as a template for
both. The scorecard caller can be started by hand.
Consumers call this repository directly; no copy per owner is built. Its name is chosen. It turned public once its history was shown to hold no private content; the first release, 0.1.0, is proposed by release-please through the merge train; a person merges that release pull request once milestone 0.1.0 closes, and the train then creates the release. As the repository is public, public repositories can call it before that release; until then a caller pins a full commit of the default branch (ADR-0022, ADR-0024, ADR-0069).
| File | Purpose | Inputs | Check | Hosts beyond the forge |
|---|---|---|---|---|
.github/workflows/licence.yml |
every file carries its licence information (reuse lint) |
runner (Linux label, default ubuntu-26.04, the newest working image; the lint step is a container action and fails on a non-Linux runner); root (directory checked as the project root, default .) |
<calling job> / REUSE lint; with another root, <calling job> / REUSE lint (<root>) (ADR-0027) |
docker.io until 2026-12-31 (ADR-0005); the image is named by digest (ADR-0008) |
.github/workflows/secret-scan.yml |
scan of the caller's full history with Gitleaks on fingerprint-scoped exceptions that expire (.gitleaksignore), self-tested on planted keys first (ADR-0055) |
runner (label, default ubuntu-26.04, the newest working image) |
<calling job> / Secret scan |
none |
.github/workflows/workflow-check.yml |
lint of the caller's workflows with actionlint and pin check of outside actions and called workflows with zizmor (ADR-0057) | runner (label, default ubuntu-26.04, the newest working image) |
<calling job> / Workflow check |
pypi.org, files.pythonhosted.org until 2027-01-20 (ADR-0005, ADR-0057); the zizmor and PyYAML wheels are hash-pinned |
.github/workflows/scorecard.yml |
the supply-chain checks of the OpenSSF Scorecard action, reported to code scanning and kept as the artifact scorecard-result |
outside_hosts (boolean, default false: the run fails, ADR-0026); publish (boolean, default false) |
<calling job> / Public repository, <calling job> / Analysis; with outside_hosts false, Analysis is the failed refusal and the analysis job reports as Analysis (not started) |
with outside_hosts: www.bestpractices.dev, api.deps.dev, api.osv.dev, oss-fuzz-build-logs.storage.googleapis.com; with publish: api.scorecard.dev, fulcio.sigstore.dev, rekor.sigstore.dev, tuf-repo-cdn.sigstore.dev (ADR-0013) |
.github/workflows/draft-release.yml |
creates a draft release for a pushed tag from an artifact holding the release files | runner (label, default ubuntu-26.04); artifact (name of the artifact holding the files, default release-files) |
<calling job> / Store the files in a draft release |
none |
.github/workflows/release-please.yml |
proposes release pull requests and creates forge releases from Conventional Commits through a GitHub App token (ADR-0056); the release commit is written by the app, so the caller's config sets a top-level signoff to the app bot's commit address (<app-slug>[bot] <<user id>+<app-slug>[bot]@users.noreply.github.com>), else a commit-message check that requires Signed-off-by refuses the release pull request |
runner (label, default ubuntu-26.04); app_client_id (required); config_file (default release-please-config.json); manifest_file (default .release-please-manifest.json); secret app_private_key (required) |
<calling job> / Release please, never a check of a pull request |
none |
.github/workflows/hook-copy.yml |
verifies that the caller's copy of the commit-msg hook files matches the repository source at the pinned commit and passes its test suite (ADR-0056) | runner (label, default ubuntu-26.04); hooks_path (default .config/hooks) |
<calling job> / Hook copy |
none |
.github/workflows/merge-train.yml |
in every repository the merge-train app is installed on, lands one clean pull request (through the merge queue where the default branch has one) or updates the next one by merge (ADR-0029, ADR-0034, ADR-0037, ADR-0039, ADR-0040, ADR-0041, ADR-0062, ADR-0067, ADR-0068, ADR-0070, ADR-0076) | app_client_id (required); runner (required, needs python3 and openssl); switch (on or off); merge_method (SQUASH or MERGE); accept_states (UNSTABLE, HAS_HOOKS; default none); min_age_days (default 3); hold_labels, dependency_branches, person_branches, security_labels, required_status; secret app_private_key |
<calling job> / Merge train, never a check of a pull request |
none |
The licence job reads the contents and takes no secret; the caller grants
contents: read and nothing else. On a draft pull request the job stops with
an error instead of running. root must be a plain relative path (letters,
digits, dot, underscore, hyphen, slash; no ..); anything else stops the job
before the tool runs. A run with another root than . checks that folder
only: its check carries the folder in its name, so it never satisfies a
required REUSE lint, and its summary says that files outside it were not
checked (ADR-0027).
The scorecard job passes whatever it finds and takes no secret. The caller
grants contents: read, security-events: write and id-token: write. Two
of its hosts are asked with the repository's name, so a caller switches the
job on by the repository's visibility and never with a plain true, and the
caller is kept in a public repository only. A run that analyses nothing
fails: with outside_hosts false, <calling job> / Analysis is a failed job
with the reason in its text, because a skipped analysis would read as a pass
(ADR-0026). A caller that passes true for
either input in a repository that is not public gets a failed check,
<calling job> / Public repository, with the reason in its text, and the
analysis does not start
(ADR-0015).
The runner is no input: the Scorecard
service takes a published result only from a job whose label is written out,
and make check-scorecard holds the file to that and the service's other
rules.
The draft release workflow creates a draft release on a pushed tag from an
artifact holding the release files. The caller grants contents: write and
calls the workflow on tag push. artifact defaults to release-files and must
be a plain name; anything with slashes, spaces, leading hyphens or shell
metacharacters stops the job before files are fetched. When the tag contains a
hyphen, the release is created with --prerelease
(ADR-0052):
draft:
name: Draft release
needs: [build, verify]
permissions:
contents: write
uses: plurano/horistes/.github/workflows/draft-release.yml@<commit> # main
with:
artifact: release-filesThe comment is one ref and nothing else (# main or a release tag), so the
update bot moves the pin. pins.py check in the shared workflow check refuses
any other comment (ADR-0075). A pin behind main's head passes while its commit
is on main's history (ADR-0075).
The merge train runs from one caller in the fleet's operations repository
and sweeps every repository the merge-train app is installed on; the
repositories it serves keep no caller and no secret. The app's key is the
secret MERGE_TRAIN_APP_KEY of the environment merge-train there, which
only the branch main may use. The key must be RSA with at least 2048 bits,
and the train refuses a shorter one before any call. check_app_key in
tools/merge_train.py enforces this and refuses a non-RSA key too, verified by
the key tests in tests/test_merge_train.py. For each repository the
job mints a token limited to that repository, with contents: write,
pull_requests: write, workflows: write and checks: read, and revokes
it when the repository is done; only when an issue is to be closed, a
second token for that repository carries issues: write alone.
GITHUB_TOKEN gets no scope. A run lands at most one pull request per
repository: the oldest the forge reports CLEAN, through the merge call with
the head commit it read, so a red or blocked pull request is refused by the
forge itself and the app has no bypass. With none CLEAN and no armed one in
flight, it arms auto-merge on the oldest BEHIND pull request whose checks are
not red and merges the default branch into it; an armed pull request that is
BEHIND is updated first. Without required_status it updates no pull request
it has not armed. With it, the train arms none: it updates a BEHIND pull request
whose status is SUCCESS without arming it, disarming an armed one first, and
lands it itself once it is CLEAN (decision record 0076). Where the repository
allows no auto-merge, the train leaves an unarmed BEHIND pull request as it is,
even when its status is SUCCESS. It updates none where a ruleset on the default
branch demands linear history (decision record 0044). A red pull request whose red
runs failed only at the draft step gets a fresh event instead, a label added
and removed (a re-run would replay the draft payload). It then closes the open
issues that its recent merges name as closed, which the forge leaves open when
the app merges. It leaves alone drafts, forks, other bases, the hold labels,
release branches (release-please--, a release is a person's act), dependency
updates younger than the minimum age and archived repositories. After the
sweep, a second job
runs release-please with release creation and release pull request updates
split into separate steps: a repository that writes its own release skips
release creation and has its labels moved once the tag is on the merge commit,
while a pre-existing release is verified
(ADR-0031,
ADR-0040).
The app is one public app installed on every account
of the fleet; its rights are in apps/merge-train.json, and a test holds them
equal to what the train asks the forge for.
When required_status names a commit status context, the train requires that
context in state SUCCESS before it lands a pull request. Only then does the
query select status { context(name: $status) { state } } and the token carry
Commit statuses read; unset, both stay as before. While it is set the train
arms no pull request for auto-merge, disarms an armed one the status holds and
takes a held one out of the merge queue. A held dependency pull request still
gets its generated files. Opting in needs statuses: read in
apps/merge-train.json accepted by each installation, and a hard gate also
needs the context required, with the writer's app as its expected source, by a
ruleset or branch protection, and the writer reporting the context only as a
commit status on the head commit and, with a merge queue, on each merge-group
commit: the train's merge guards only the head commit, so
neither a status that changes after the train reads it nor auto-merge armed by
hand waits for the train. With a merge queue the forge judges the merge-group
commit, so a head status that changes after that commit passes may not stop
the landing, and the queue holds each pull request on its own only with its
"Only merge non-failing pull requests" setting on
(ADR-0076).
| File | Purpose | Inputs | Outputs | Hosts beyond the forge |
|---|---|---|---|---|
actions/verified-download/action.yml |
bounded, digest-verified release download over HTTPS; a failed github.com release download falls back to gh release download (ADR-0058, ADR-0071) |
url (required); sha256 (required); output (required); max-bytes (default 33554432); timeout-seconds (default 120) |
lib (absolute path of verified-download.sh) |
host of url (HTTPS only); api.github.com on the fallback |
actions/install-gitleaks/action.yml |
installs pinned Gitleaks release binary after SHA-256 verification on Linux/x86_64 (ADR-0058, ADR-0071) | version (required, no leading v); sha256 (required); install-dir (required) |
path (absolute path of installed gitleaks binary) |
github.com; api.github.com on the fallback |
| Path | Content |
|---|---|
.github/workflows/ |
reusable workflows, this repository's own gate (ci.yml), its callers (licence-check.yml) and scheduled checks (codeql.yml, image-age.yml, pins.yml) |
actions/<name>/ |
composite steps (ADR-0058) |
hooks/ |
files a consumer keeps as a copy, checked by hook-copy.yml (ADR-0056) |
templates/callers/ |
the caller a consumer keeps, one template per shared workflow (ADR-0009) |
tools/ |
the checks of the gate, the same-verdict check for a switch, the renderer of the caller templates, the release read-back, the check of action pins and runner labels, and the check of relative Markdown links |
tests/ |
tests of those checks; each rule has a planted defect that must fail |
tests/fixtures/ |
planted cases a workflow is run on: unlicensed must fail the licence check, licensed must pass it (ADR-0006); readback holds the parameter files of three read releases |
apps/ |
manifests of the forge apps the workflows use, with their computed rights (ADR-0029) |
interface.lock.json |
recorded interface of every workflow and action |
hosts.toml |
hosts and outside actions the workflows may reach; a host without a switch has a day it ends and a MEL category (ADR-0073); a host for public repositories only is marked (ADR-0013), and its workflow refuses another repository in a job of its own (ADR-0015) that runs on every call and is given the visibility and every such input (ADR-0026) |
exceptions.yaml |
exceptions to rules no other list can carry, each with its issue, a MEL category and a day it ends (ADR-0007, ADR-0073) |
docs/adr/ |
decision records |
make verify-all is the one gate, locally and in CI. It needs Python 3.12 or
newer, actionlint 1.7.12, gitleaks 8.30.1 and praetorctl on the path; the
other tools are installed into .venv from requirements/tools.txt with their
hashes.
make verifyruns this repository's own checks without the engine: YAML and Python lint, actionlint and zizmor, the interface record, the host scan, the accepted findings and exceptions, the caller templates, the Scorecard service's rules, the runner labels, the relative Markdown links and their heading anchors (tools/links.py, no host asked), the tests and the licence headers. The test run measures coverage and fails below the floorfail_underinpyproject.toml(94%), which only goes up;tests/test_coverage_floor.pyholds the floor and the coverage steps of thetestrecipe. Each coverage step readspyproject.tomlonly, so aCOVERAGE_RCFILEin the environment cannot lower the floor. Each run keeps its data files in its own temporary directory, so data an earlier or a concurrent run left behind cannot change the total. The report shows two decimals and a last step compares the total at ten decimals, so a total that only rounds up to the floor still fails.- The tests read the message of every commit between the base of the change
and
HEAD(tests/test_commit_messages.py), on every run of the gate. A message fails without aSigned-off-bytrailer, and when it credits a tool or a model: a trailer whose key is notSigned-off-byorMigration, a co-author line, or a line that opens with "generated with". The engine's own message check passes such a message; this test retires when the pinned engine refuses it. The commit-msg hook judges each message by the same rules when it is written (make check-message,tools/commit_message.py), and the squash helper runs that hook on the squash message before it sends it (ADR-0072, ADR-0074). A squash message written on the forge by hand is read only afterwards, on the run ofmain. - No tracked file holds the address this repository had under its working name
(
tests/test_name.py): the forge does not redirect a call to it. Two accepted records keep it and are named in the test (ADR-0023). - Before
make verify-all, the gate workflow scans every commit of the checkout for secrets with gitleaks and its own rules. It refuses a.gitleaks.tomlor.gitleaksignorein the tree and ignores allow comments; a finding is printed redacted (ADR-0028).tests/test_secret_scan.pyruns the step on planted repositories. make interface-snapshotrewritesinterface.lock.jsonafter an interface change. A change that breaks a caller also raises the recorded version:interface_minorwhileinterface_majoris 0,interface_majorfrom 1 on (ADR-0026).- Each line anchor that
.bestpractices.jsonlinks as evidence is derived from a pin intools/anchors.py: the source text that opens and closes the range, and the answers that must link it.tests/test_bestpractices.pyfails when an anchor matches no pin of its answer or a pin's answer does not link it.make fix-anchorsmoves anchors throughANCHORS_BASE, a revision whose files hold the source at the lines the current anchors record (for a committed change, usuallygit merge-base HEAD main);mainworks only while it still holds those lines, and nothing is written if mapping fails.fixrechecks the answers and pinned sources just before it replaces the file and refuses if they changed; the recheck is best effort. Between the recheck and replacement, an edit to.bestpractices.jsonis overwritten; an edit to a pinned source stays on disk but may leave the written anchors pointing at moved lines, sotests/test_bestpractices.pyfails. A secondmake fix-anchorsthrough the same base then refuses, because the base does not hold the lines the first run wrote. Put back the anchors from before the first run (git restore .bestpractices.jsonwhen that run made its only uncommitted change), or set each anchor thatchecknames to the pin lines it reports, then runmake fix-anchorsagain. - The floor job runs the read-back and stranger tests on Python 3.12 (ADR-0048).
A consumer's caller is rendered from templates/callers/<name>.yml.tmpl:
python3 tools/render.py render --template templates/callers/licence.yml.tmpl --context context.toml \
> .github/workflows/licence-check.ymlThe rendered licence caller skips its job on a draft and reports the draft.
context.toml holds the values of the template's fields:
RunnerTag = "ubuntu-26.04"
[Shared]
Repository = "owner/name"
Commit = "<full id of the commit of this repository the caller names>"
Version = "<the version that commit carries, written as a comment>"
[Licence]
CopyrightText = "2026 Owner <owner@example.org>"
Identifier = "EUPL-1.2"Every rendered caller starts with the consumer's own licence header, written
from .Licence, so the consumer's licence check accepts the file
(#3). The empty note inside each
field line (# SPDX-<%/* */%>...) renders as nothing and keeps this
repository's licence check from reading the line as a tag, while REUSE ignore
markers stay refused by tools/exceptions.py. .Licence.CopyrightText and
.Licence.Identifier are working names, like the .Shared fields, until the
engine renders caller templates. The consumer's LICENSES/ must hold the named
licence; reuse lint otherwise fails and names the missing licence.
The templates are written for the engine (Go text/template with the
delimiters <% and %>). The engine does not render them yet
(cordanaLLM/praetor issue 1211), so tools/render.py reads the part a caller
needs: a field, a maintainer note and the trim markers. It refuses anything
else, and a field the context does not hold; exit code 3.
make check-templates, part of the gate, renders every template with sample
values and fails unless the result calls a workflow file of this repository by
commit, with inputs that workflow declares. A secret is passed only when that
workflow declares it, by name, as ${{ secrets.NAME }}; secrets: inherit
and a secret written out are refused. The job must grant
exactly the scopes the called workflow's jobs ask for, and an input that
switches on a host for public repositories only is passed as
${{ github.event.repository.visibility == 'public' }} or as false
(ADR-0013). The scorecard
template is rendered in public repositories only: elsewhere, and with false,
its Analysis check fails (ADR-0026). It takes no
RunnerTag.
A caller that runs on pull requests or in a merge queue also holds the job
result. It carries the calling job's own name, which the engine declares as
the required check, and fails unless the call passed; the forge reports the
calling job itself only as " / "
(ADR-0030).
A repository replaces its own job by a shared workflow only after both ran side by side and decided the same (ADR-0004):
python3 tools/verdict.py --map trial.toml --live --record runs.json --markdown verdict.mdtrial.toml names the repository, the trial's branches, each pair of old and
new check with their workflow files, and the planted commits:
repository = "owner/name"
branches = ["trial/licence"]
[[pair]]
family = "licence check"
class = "R"
old = "REUSE lint"
new = "Licence / REUSE lint"
old_workflow = ".github/workflows/reuse.yml"
new_workflow = ".github/workflows/licence-check.yml"
called_workflow = ".github/workflows/licence.yml"
draft_stop = "Stop on a draft pull request"
[[planted]]
family = "licence check"
commit = "<full id of a commit both jobs must fail>"
expect = "FAIL"
marker = "reuse lint"
[[planted]]
family = "licence check"
commit = "<full id of a commit both jobs must pass>"
expect = "PASS"branches may name the branch the switch is made on from its start. A push
whose commit does not yet hold the caller's workflow file lies outside the
trial and counts for nothing; a job that is absent although the commit holds
its workflow file ends the check with exit code 3
(ADR-0012).
A planted case can also be a fixture: a folder of the repository both forms of the job are run on, in jobs of their own that exist only on the trial's branch. Such a pair must reach the named verdict on every subject and counts as a planted case of its family (ADR-0006):
[[fixture]]
family = "licence check"
class = "R"
old = "Old form, unlicensed fixture"
new = "New form, unlicensed fixture / REUSE lint (tests/fixtures/unlicensed)"
old_workflow = ".github/workflows/trial-old.yml"
new_workflow = ".github/workflows/trial-new.yml"
called_workflow = ".github/workflows/licence.yml"
draft_stop = "Stop on a draft pull request"
expect = "FAIL"
marker = "reuse lint"A job that passes whatever it finds, such as a scorecard run, is a pair of class U. Where both jobs pass, the checks and findings of the SARIF result each uploaded are compared, and the planted commits name a finding that must show in both results and a commit where it must show in neither (ADR-0011):
[[pair]]
family = "scorecard"
class = "U"
old = "Scorecard analysis"
new = "Scorecard / Analysis"
old_workflow = ".github/workflows/scorecard-old.yml"
new_workflow = ".github/workflows/scorecard-check.yml"
called_workflow = ".github/workflows/scorecard.yml"
old_artifact = "scorecard-old"
new_artifact = "scorecard-result"
[[planted]]
family = "scorecard"
commit = "<full id of a commit with the planted finding>"
expect = "FINDING"
marker = '^PinnedDependenciesID \| \.github/workflows/planted\.yml:7 '
[[planted]]
family = "scorecard"
commit = "<full id of a commit without it>"
expect = "CLEAN"
marker = '^PinnedDependenciesID \| \.github/workflows/planted\.yml:7 'A finding is one line: its rule, its places, the first line of its message
and a digest of the other lines. --record keeps the results in the recorded
file, so --runs judges them again after the artifacts expired.
--root is the consumer's checkout and --called-root the checkout of this
repository when the two differ. --runs FILE judges a recorded file instead of
the forge. Exit code 0 allows the switch; 1, 2 and 3 do not.
A published release is verified the way a stranger does it: with no login, no token and an empty home folder (ADR-0014):
python3 tools/readback.py verify --params release.toml --tag v1.2.3Before a release exists on the forge, a release workflow can read its own build files back from a local folder before creating the draft release (ADR-0033):
python3 tools/readback.py verify --params release.toml --tag v1.2.3 --local dist/The tool needs Python 3.12, git and cosign (read with v2.6.3) and
nothing else from this repository than tools/readback.py and
tools/stranger.py. A repository writes one parameter file and no script:
repository = "owner/name"
signer = ".github/workflows/release.yml" # the workflow that signs
other_workflows = [".github/workflows/ci.yml"] # must not pass in its place
unverified = ["notes-{version}.txt"] # assets nothing here verifies
images = ["ghcr.io/owner/name:{version}"]
[blobs]
checksums = "checksums.txt"
signature = "checksums.txt.sigstore.json"A file names blobs, images or both; {tag} is the tag and {version}
the tag without a leading v. python3 tools/readback.py check --params release.toml reads the file and makes no request.
- Blobs. The release holds exactly the checksums, their signature, the
listed files and the files declared
unverified. A file that is declaredunverifiedand listed in the checksums fails: the declaration is out of date and has to go. Each listed file matches its digest, the signature verifies the checksums, and for each listed file the forge serves a provenance attestation that verifies it. - Images. The signature verifies the reference, and the provenance attestation verifies the image by the digest the signature named.
- Identity. Every signature and attestation must come from the
signerworkflow of the repository, run on the tag, at the commitgit ls-remotegives for the tag. - Controls. Each accepted check is asked again with one expectation
changed (another tag, the branch
main, another repository, another commit, another issuer, another workflow, an altered file), and each of those calls must be refused. A control that gets no answer fails.
The lines read PASS accept, PASS refuse, FAIL ... and NOTE .... Exit
code 0 means every line passed, 1 that a check failed or a control was
accepted, 3 that the parameters, the tag or an answer of the forge could not
be read. The forge's API answers 60 requests an hour per address without
credentials; a release costs one request and one more for each attestation
the listed files need. The parameter files of the releases the tool was
accepted on are kept in tests/fixtures/readback/.
Every outside action and called workflow is named by a commit with its release in a comment. One tool lists, writes and checks those lines for any checkout (ADR-0016):
make list-pins PINS_ROOT=../other # uses on a tag or a branch; asks no host
make check-pins PINS_ROOT=../other # fails on any line below
make pin PINS_ROOT=../other # writes what can be fixed, then checksPINS_ROOT defaults to this repository. The tool reads
.github/workflows/*.yml and actions/*/action.yml of the checkout and runs
with the tools of this repository (.venv), never with a zizmor of the
checkout.
FAIL line |
Meaning | pin |
|---|---|---|
action is not pinned to a hash |
the use is on a tag or a branch | writes the commit the name stands for today and its release as the comment |
is pointed to by tag vX |
the comment names another release than the commit | corrects the comment, keeps the commit |
missing version comment |
a commit without a comment | adds the release |
comment does not contain a version, points to unknown ref, branch points to commit ... |
the comment is no release of that repository, or holds more than the release | left for a person |
input `x` is not declared at that ref |
a with: key the action or the called workflow does not have; the runner would ignore it |
left for a person |
no action.yml or action.yaml at that ref, or not readable |
the forge does not serve the metadata to this token | left for a person |
pin moves no pin to a newer release; updates come from Renovate. The
comment has to be the release alone (# v4.2.2). A commit that no release
names is not judged.
check-pins and pin need a token in GH_TOKEN, GITHUB_TOKEN or
ZIZMOR_GITHUB_TOKEN, or a logged-in gh. Exit code 0 means every line
passed, 1 a finding, 3 that the check could not be made: no token, a zizmor
that did not report the planted case of the run, a finding of an audit the
tool does not switch off, or an answer of the forge that cannot be read. The
last line of a pass names the zizmor version and the numbers of files, uses
and keys read. Neither target is part of make verify-all, which asks no
host.
For this repository the check runs by itself: .github/workflows/pins.yml
starts it on every pull request, once a week, and by hand
(ADR-0017,
ADR-0047).
It is a required check of main. The summary of each run names the forge requests the
check took. Other repositories run the three targets from a checkout of this
one until a reusable workflow exists.
The same tool refuses a floating label of a hosted machine (ADR-0018):
make check-runners PINS_ROOT=../other # asks no host, needs no tokenFAIL: .github/workflows/build.yml:46: `runs-on` of job `build`: `ubuntu-latest` is a floating runner label; name the release it stands for
The labels are ubuntu-latest, windows-latest, macos-latest,
macos-latest-large and macos-latest-xlarge, as a whole word and without
regard to case: ubuntu-latest-xl is a label of its own and passes.
| Read | Not read |
|---|---|
runs-on of a job: a text, a list, group with labels, the text of an expression |
a name, a description, a command, a variable, a comment |
every value under strategy of a job |
a repository variable (vars) or an output of an earlier job (needs) |
every value a job hands to a called workflow under with |
a label put together from parts (ubuntu-${{ inputs.release }}) |
default and options of an input of workflow_call and workflow_dispatch |
an action file |
The last line of a pass counts the jobs that take their label from an
expression. Exit code 0 is a pass, 1 a finding, 3 a workflow that cannot be
read. check-pins and pin run the check as well; pin rewrites no label.
The check is part of make verify-all. It has no exception list yet: a
finding is fixed.
Newer releases are proposed by Renovate (the update bot; the English verb)
from renovate.json and the preset it extends, default.json
(ADR-0019,
ADR-0020,
ADR-0036).
A proposal waits three days after the release unless it fixes a known
vulnerability, is never merged by the bot and has to pass the gate like any
other pull request.
| Read by the bot | Where | How it finds the release |
|---|---|---|
| outside actions | .github/workflows/ |
the commit and the release in the comment behind it |
| tools of the gate | requirements/tools.txt |
the compile command in the header of the file, which it reruns |
| the licence tool's image | .github/workflows/licence.yml |
the comment # release X.Y.Z on the line above uses: and the digest in it |
| actionlint and gitleaks | .github/workflows/ci.yml and Makefile |
the release tag in *_VERSION, and the checksum from the release's checksum file into *_SHA256 |
Not read: the files the Praetor engine writes (.devcontainer/ among them)
and the machine of a job.
make requirements recompiles the tool pins by hand with the same command.
Keep the header and the comment above the image line: tests/test_update_bot.py
fails when the bot could no longer read one of them.
.github/workflows/image-age.yml bounds the licence tool's image age weekly
against Docker Hub (ADR-0035).
.github/workflows/codeql.yml runs CodeQL on the workflows and the Python tools
and reports to the forge's code scanning (ADR-0043).
default.json holds what is the same in every repository
(ADR-0020):
| Value | Setting |
|---|---|
| base | config:recommended |
| commits of the bot | carry the sign-off line (:gitSignOff) |
| waiting time after a release | three days; none for a fix of a known vulnerability |
| merge by the bot | never |
| after a Go module update | go mod tidy (gomodTidy) |
| pull request body | prHeader adds the description and verification checklist; every box stays unticked. The header matches the template that praetorctl adopt ships. A repository with a different template sets its own prHeader in its renovate.json. |
| the machine a job runs on | not proposed |
A fix of a known vulnerability skips the waiting time
(ADR-0050) and is
rebased as soon as it falls behind its base branch, so it also lands where the
default branch demands linear history
(ADR-0061).
Renovate learns of the vulnerability from the OSV database
(osvVulnerabilityAlerts) and, where the repository has Dependabot alerts on,
from the forge (vulnerabilityAlerts). For the Go standard library it reads the
toolchain line of go.mod, not the go line: a Go repository without a
toolchain line receives a standard-library fix only after the waiting time.
After a Go module update Renovate runs go mod tidy (postUpdateOptions,
ADR-0051), so the proposal
carries every module hash the build needs.
A repository extends it by reference and keeps only what is its own (which managers run, which files they read):
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["github><owner>/<this repository>"]
}Without a file name Renovate reads default.json of the named repository;
in this family the reference is github>plurano/horistes. This
repository's own renovate.json extends the preset the same way and repeats
none of its values.
-
As the repository is public, any repository can extend the preset (ADR-0024).
-
From the first release on, a consumer names the release (
github><owner>/<this repository>#<release tag>) and addsrenovate-configto itsenabledManagers: Renovate then proposes the next release of the preset like any other update. A reference without a release follows the default branch. -
A repository that lints with REUSE names the configuration in its
REUSE.toml, because JSON carries no comment for a header:[[annotations]] path = ["renovate.json"] precedence = "aggregate" SPDX-FileCopyrightText = "<year> <holder>" SPDX-License-Identifier = "<licence of the repository>"
If the repository already has an
aggregatetable for files without a header, add"renovate.json"to itspathlist instead.
settings-sync.json is for a repository that holds its Rust toolchain file
equal to a source in another repository and compares it in its gate
(ADR-0021).
There a toolchain proposal of the bot changes the copy and can never pass, so
the preset switches the manager rust-toolchain off and does nothing else.
Such a repository names both presets:
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"github><owner>/<this repository>",
"github><owner>/<this repository>:settings-sync"
]
}- A repository that cannot extend the preset yet (see above) writes the one
key into its own file, in the same form as the preset holds it:
"rust-toolchain": { "enabled": false }. - A repository that names its managers in
enabledManagersand leavesrust-toolchainout needs neither. - A repository that owns its toolchain file does not extend this preset: it would receive no toolchain proposal, and nothing reports that.
Check a changed preset before it is merged, with the validator that ships in the Renovate image:
docker run --rm --network=none -v "$PWD:/data:ro" -w /data \
--entrypoint renovate-config-validator renovate/renovate:<release> \
--strict --no-global default.json settings-sync.json renovate.json--no-global makes it read the files as repository configuration. The
planted files tests/fixtures/update-bot/planted-preset.json and
tests/fixtures/update-bot/planted-settings-sync.json must each fail the
same command.
EUPL-1.2, see LICENSE.