Skip to content
pluranoPublic

About

Reusable CI workflows and templates of the Plurano family.

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

170 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Horistes

HISS Adopted

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.

Name

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.

Status

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).

Workflows

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-files

The 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).

Actions

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

Layout

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

Gate

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 verify runs 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 floor fail_under in pyproject.toml (94%), which only goes up; tests/test_coverage_floor.py holds the floor and the coverage steps of the test recipe. Each coverage step reads pyproject.toml only, so a COVERAGE_RCFILE in 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 a Signed-off-by trailer, and when it credits a tool or a model: a trailer whose key is not Signed-off-by or Migration, 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 of main.
  • 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.toml or .gitleaksignore in the tree and ignores allow comments; a finding is printed redacted (ADR-0028). tests/test_secret_scan.py runs the step on planted repositories.
  • make interface-snapshot rewrites interface.lock.json after an interface change. A change that breaks a caller also raises the recorded version: interface_minor while interface_major is 0, interface_major from 1 on (ADR-0026).
  • Each line anchor that .bestpractices.json links as evidence is derived from a pin in tools/anchors.py: the source text that opens and closes the range, and the answers that must link it. tests/test_bestpractices.py fails when an anchor matches no pin of its answer or a pin's answer does not link it. make fix-anchors moves anchors through ANCHORS_BASE, a revision whose files hold the source at the lines the current anchors record (for a committed change, usually git merge-base HEAD main); main works only while it still holds those lines, and nothing is written if mapping fails. fix rechecks 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.json is overwritten; an edit to a pinned source stays on disk but may leave the written anchors pointing at moved lines, so tests/test_bestpractices.py fails. A second make fix-anchors through 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.json when that run made its only uncommitted change), or set each anchor that check names to the pin lines it reports, then run make fix-anchors again.
  • The floor job runs the read-back and stranger tests on Python 3.12 (ADR-0048).

Caller templates

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.yml

The 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).

Switching a consumer

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.md

trial.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.

Reading a release back

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.3

Before 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 declared unverified and 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 signer workflow of the repository, run on the tag, at the commit git ls-remote gives 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/.

Action pins

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 checks

PINS_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.

Runner labels

The same tool refuses a floating label of a hosted machine (ADR-0018):

make check-runners PINS_ROOT=../other   # asks no host, needs no token
FAIL: .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.

Updates

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).

The preset other repositories extend

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 adds renovate-config to its enabledManagers: 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 aggregate table for files without a header, add "renovate.json" to its path list instead.

The preset for a repository under settings sync

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 enabledManagers and leaves rust-toolchain out 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.

Licence

EUPL-1.2, see LICENSE.

About

Reusable CI workflows and templates of the Plurano family.

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages