Reusable GitHub Actions workflows and a merge-train script shared by the Contrast3D repositories.
Status: in progress. The design is in specs/next-ci-reliability of
naindev-contrast3d/contrast3d-platform. Licensed under MIT.
-
preflight(available): checks required tools, a reachable Docker daemon, memory and disk on the runner before any build job, and labels a failureinfra. Inputs:runs-on,required-tools,min-memory-mb,min-disk-mb. -
gate(available): one aggregate job to make the only required check. It fails if any job it depends on failed, was cancelled or was skipped without being declared inallowed-skipped. Inputs:needs-json(the caller'stoJSON(needs)),allowed-skipped,runs-on. The caller job needsif: ${{ always() }}. -
supply-chain(available): the scan every repository runs, in one place. zizmor audits the workflow files online with the job's read-only token and gitleaks scans the history of the commit for secrets, both from images pinned by digest, so raising a digest is one change here and a caller gets it by raising its pin. Inputs:runs-on(a JSON label or array, default"ubuntu-latest") andpaths(what zizmor audits, default.github/). The caller job needspermissions: contents: read, and the runner needs Docker.tests/supply-chain.test.shchecks that every image is pinned by digest and that the flags that decide what is scanned stay. -
changes(available): changed-file detection inside the workflow, withoutpaths:filters. Inputs:filters(area -> glob patterns),global-patterns(default.github/**),runs-on. Outputs:areas(area -> true/false) andreason. When the diff cannot be known with certainty (no base commit, base not fetchable, empty diff, global pattern matched) every area is reported as changed. Skipped jobs must be declared in the gate'sallowed-skipped. -
train/automerge.py(available): the merge train. It is not a workflow: a scheduled task on the owner's desktop runs it withgh. One cycle goes over every repository of a JSON list (train/repos.example.json), and per repository merges at most one pull request labelledautomergewhose gate check is green (plus theci-ok/<sha>seal whenrequire_sealis set) and whose head contains the base branch. A stale head gets the base merged in through the API, without force-push; a conflict getsneeds-humanand losesautomerge.merge_method(merge,squashorrebase, defaultmerge) follows what each repository allows. Each cycle it also logs once any workflow run that has been queued longer thanqueue_alert_minutes(default 30): a runner that is offline or saturated otherwise holds jobs for up to 24 hours before they fail. The runs already reported are remembered inqueue-alerts.jsonbeside the configuration. After every merge it comments<!-- merge-train:merged -->on the pull request, so a merge made around the train can be counted, and it writes aheartbeat:line (repositories, labelled pull requests, version of the script) at most once an hour as proof of life.--dry-runchanges nothing. Tests:python3 -m unittest discover -s tests. -
train/install.ps1(available): installs the train as the scheduled taskCI train, every two minutes, fromD:\Development\Tools\ci-train(a copy ofautomerge.py, arepos.jsoncreated from the example only if missing, andtrain.log). Run it from a normal PowerShell window of the owner's session;-Removeretires the task. -
report/ci_report.py(available): the CI health report. Per repository and period it prints the failure rate of conclusive runs, the share of failures caused by the infrastructure (a job that never reached its steps, or the shared preflight) and not the code, the median duration and the median queue time, followed by a snapshot of today: runners online, offline and busy, queued runs and open pull requests. Use it to plan from the current state, not from memory:py -3 report/ci_report.py --config train/repos.json --repo naindev-contrast3d/contrast-hub --days 7. For the repositories of--config(the shared train) the snapshot also counts the pull requests merged outside the train since--train-since, and--train-dir D:\Development\Tools\ci-trainadds a section on the installed train: age of its log (silent after 90 minutes) and whether its script differs from the one inci-sharedmain. -
report/weekly_report.py(available): the weekly report to a file. It runsci_report.pyover every private repository of the organisation that is not archived and writesci-report-<date>.md(andlatest.md) to--out-dir. When the week deviates, with a failure rate above 25 % over at least five runs, a p90 wait for a runner above five minutes, or more than a fifth of the failures caused by the infrastructure, the deviations go at the top of the report and one toast is raised through--notify; a healthy week raises nothing. The thresholds are options.report/install-weekly-report.ps1registers it as the taskCI weekly report(Mondays 08:00), from a normal PowerShell window of the owner's session. -
report/runner_watch.py(available): the runner watchdog. The report says how the queue behaved over a period; this says that it is wrong now. Run from a scheduled task every few minutes (py -3 report/runner_watch.py --org naindev-contrast3d --notify <path of notify.ps1>), it reads the runners of the organisation and of each private repository, plus the queued runs, and raises one toast per finding: a runner offline for--offline-minminutes (default 10; GitHub does not say since when, so the first sighting is remembered in a state file) or a run queued for--queue-min(default 15). A finding is reported once and again only after it has cleared;--ignore NAMEmutes a runner that is offline on purpose; an unreadable answer is skipped, never reported. Exit code 1 when it reported something.report/install-runner-watch.ps1registers it as the scheduled taskCI runner watch(every five minutes, hidden, the owner'sghlogin, toasts throughworkstation-maintenance\notify.ps1); run it from a normal PowerShell window of the owner's session, with-Ignore <runner>for a runner that is offline on purpose, and-Removeto retire it.--until 2026-10-14T11:00ends the period in the past, so a baseline can be reproduced after later changes;--limitraises the number of runs read per repository (default 300). -
standards/audit.py(available): audits every repository of the organisation againststandards/standards.jsonand prints each deviation aserror,warningorinfo: branch deletion after merge, Dependabot, CODEOWNERS, secret scanning and a recognised license on public repositories, a CI that calls the shared gate pinned by a full SHA with a# vX.Y.Zcomment, too many leftover branches, the organisation's two-factor requirement, Dependabot alerts that are off or open past their deadline (critical 7, high 30, medium 90 days, only when a fix exists), writable deploy keys, webhooks without a secret, an unprotected default branch on public repositories, and an organisation that does not restrict actions to a list pinned by SHA. The audit only reports; the owner changes keys, webhooks and two-factor. An exception names a repository and a rule, gives a reason and expires; an expired one is itself an error.py -3 standards/audit.py [--fail-on error], orreport/ci_report.py --auditto append it to the weekly report. -
standards/bootstrap.py(available): applies the organisation's repository settings to a new repository: branch deletion after merge, secret scanning and push protection where they are free, Dependabot alerts and security updates, and theautomergeandneeds-humanlabels the merge train uses. A dry run by default;--applymakes the changes.py -3 standards/bootstrap.py owner/name [--apply].
- No secrets, no business logic and no repository-specific values: everything comes in as an input.
- Callers pin these workflows by full commit SHA, never by branch or moving tag.
- Every change passes
actionlint,zizmorandgitleaksbefore merge.