Reusable GitHub Actions quality gates for all ForkHorizon projects. The gate logic lives here once; each project only carries a thin caller workflow. Use a commit SHA for production callers so a gate rollout is reproducible.
| Workflow | What it checks |
|---|---|
code-linter.yml |
Dependency-free structure checks for 21 mapped language/config families plus .gitignore: file ≤ 300 lines, function ≤ 50 lines, control-flow nesting ≤ 4, parameters ≤ 5, prose comment block ≤ 5 lines, doc-comment block ≤ 50 lines, top-level types per file ≤ 2, plus syntax and lexical block-balance checks. YAML and .gitignore receive syntax-only policy checks; YAML mappings also reject duplicate keys. Blank lines inside a comment run do not reset its limit; recognized SPDX/license headers have a bounded allowance of 30 lines under the default policy. |
swift-compile.yml |
Project compiles; fails on critical warnings (Swift 6 concurrency, Sendable, data races). |
swift-quality.yml |
Build, swift-format lint --strict, dead code via Periphery. |
web-quality.yml |
TS/JS: tsc --noEmit, ESLint (if the repo has a config), dead code + unused deps via knip, copy-paste via jscpd. |
python-quality.yml |
Ruff lint (strict fallback config in configs/ruff-strict.toml) and ruff format --check. |
go-quality.yml |
go vet, gofmt -l (fails on unformatted files), golangci-lint run. Does not run go test — test execution stays with the project's own CI. |
unity-quality.yml |
Unity C#: dotnet build with Microsoft.Unity.Analyzers (fails on first-party warnings), jscpd for C#. Uses a persistent per-repo workspace cache under ~/Library/Caches/ci-gates — no project checkout, incremental fetch + Library reuse. |
slop-review.yml |
Advisory, non-blocking. Sends each changed file's diff to a local Ollama model to flag semantic AI-slop that linters miss (swallowed errors, fake tests, misleading names, insecure string-built queries, dead-end code), with a 3-vote adversarial refutation pass. Posts ::warning annotations + a job-summary table and a calibration journal; never affects the merge decision. |
All jobs target self-hosted macOS ARM64 runners by default. The active routing
contract is runner-group plus runner-labels/runner-label; runs-on is
retained as a legacy caller input for compatibility. Keep Slop Review on a
dedicated ci-scope-ai runner label with exactly one registered runner.
Add a caller workflow to the project:
# .github/workflows/quality.yml
name: Quality
on:
pull_request:
merge_group:
types: [checks_requested]
permissions:
contents: read
jobs:
code-linter:
uses: ForkHorizon/ci-gates/.github/workflows/code-linter.yml@main
swift-compile:
uses: ForkHorizon/ci-gates/.github/workflows/swift-compile.yml@main
swift-quality:
uses: ForkHorizon/ci-gates/.github/workflows/swift-quality.yml@main
with:
run-build: false # compile gate already buildsPer-repo tuning stays in the project via config files:
.code-linter.json, .swift-compile-gate.json,
.swift-quality-gate.json. See each script's DEFAULT_CONFIG in
scripts/ for the available keys.
The Code Linter uses .code-linter.json by default and fails if that file is
missing. Its config is strictly validated. Unsupported
extensions, unknown keys, empty extension lists, non-positive limits, and
malformed language overrides fail the job. Limits are bounded and blanket
source ignores such as *.py are rejected. Changing the config or a caller
workflow forces an all-files structure scan. The gate does not replace a
compiler, type checker, security scanner, or tests; use the language-specific
quality workflows alongside it.
Coverage gaps are accounted for separately. The default coverage_mode is
report: tracked files in ignored source directories, excluded supported
extensions, and recognized unsupported code/config surfaces are listed as
GitHub warnings instead of disappearing silently. Set "coverage_mode": "strict" to fail on any unapproved gap. Intentional exclusions for surfaces
that still need a dedicated analyzer must be documented with a pattern and
reason, for example:
{
"coverage_mode": "strict",
"coverage_exceptions": [
{
"pattern": "vendor/",
"reason": "third-party dependency mirrored from upstream"
},
{
"pattern": ".github/workflows/",
"reason": "validated by actionlint in the workflow gate"
}
]
}The coverage inventory recognizes common C/C++, Objective-C, Dart, Scala,
shell, SQL, build, web, serialization, and workflow/configuration surfaces.
C/C++, Objective-C, Dart, Scala, and Groovy/Gradle now use the existing
brace-based structural checks; JSON and TOML use standard-library syntax
parsers, Bash-compatible shell files use native bash -n plus structural
checks, and YAML workflows/configuration use dependency-free lexical checks.
.gitignore receives policy-pattern checks. Other recognized surfaces remain
explicit gaps, and unknown UTF-8 text files are reported too; binary files and
clearly documentary files such as Markdown are excluded from that inventory.
Strict mode never approves an ignored or excluded extension that already has
structural support; this prevents a self-declared generated/vendor exception
from hiding handwritten code. Such files must be scanned or removed from the
policy gap before release.
Function detection is dependency-free and best-effort lexical analysis, not a full parser for every supported language. Keep language-specific quality gates enabled when complete grammar coverage is required.
This repository also checks itself on every pull request, merge-queue run, and
push to main. The self-check executes the scripts from the exact revision
being reviewed, runs the full unit-test suite, applies the default Code Linter
policy, checks Ruff lint/format, and validates all workflow files. The public
scripts/code-linter.py entry point remains stable; its implementation is split
across the small modules in scripts/code_linter/.
The manual CI Scope v2 Canary workflow runs two same-label v2 jobs against
the ci-scope-v2-canary runner group with isolated routing validation.
Release provenance and manifest enforcement are validated fail-closed via
scripts/release_enforcement.py and scripts/release_manifest.py.
Common to all workflows:
-
runs-on— legacy JSON array of runner labels. New callers should userunner-groupplusrunner-labels/runner-labelbelow. -
runner-group— optional GitHub-hosted runner-group name. v1 defaults keep the existing placement; v2 requires the dedicated trusted group. -
runner-labels— JSON array used by the v2 routing contract. It must be non-empty and is validated together withrouting-generation. -
routing-generation—v1(default) orv2; unknown generations fail closed and v1/v2 routing fields cannot be mixed. -
workflow-contract-version— workflow-facing contract version,v1by default until a consumer is migrated. -
trust-fixture-mode— empty by default; only canary workflows may enable a named trust-policy fixture. -
config— path to the gate's JSON config in the calling repo; the Code Linter defaults to.code-linter.json. -
coverage-mode— optionalreportorstrictoverride; when omitted, the repository config'scoverage_modeis used. -
gates-ref— which ref of this repo to fetch scripts from where supported. It defaults tomainfor backward compatibility. Production release manifests must override it with a full commit SHA. -
explain-model— Ollama model used by the failure explainer (defaultqwen3-coder:30b-a3b-q4_K_M); set to''to disable.
When a gate fails, scripts/explain-failure.py sends the log tail and diff
summary to the local Ollama on the runner and writes a "Why this failed"
analysis to the job summary. Advisory only — it never changes the gate
verdict, and it silently skips if Ollama is unreachable.
code-linter.yml and swift-quality.yml also take mode
(auto/all/changed; auto scans changed files on pull_request and
merge_group, everything otherwise). swift-quality.yml takes run-build
to skip its build stage when the compile gate already builds the project.
web-quality.yml, python-quality.yml, and go-quality.yml take
working-directory; web-quality.yml also takes duplication-threshold
(max % of duplicated code, default 2). go-quality.yml also takes
bootstrap-command, an optional shell command run before vet/lint —
e.g. to satisfy a go:embed target that needs at least one file present
on a fresh checkout.
Projects reference @main, so a push here rolls out everywhere at once. If a
rollout misbehaves, revert the commit. For a breaking change in gate behavior,
cut a tag and migrate callers deliberately.