Architecture checks in CI

A check you run by hand catches what you remember to look for. A check on the merge button catches what nobody looked at — which, on a team shipping machine-written code, is most of it.

The one-liner

Add this workflow and every pull request gets an architecture check:

# .github/workflows/architecture.yml
name: architecture
on: [pull_request]

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: DominicPaaul7/wyro-check-action@v1

Findings appear as annotations on the lines that caused them, and as a summary on the job. Your source is read on your own runner and never uploaded, so this works the same on a private repository behind a firewall as it does on a public one. The action itself is thin: it downloads the checker, verifies its published SHA-256, and runs it.

Adopting it on a repository that already exists

Point the checker at an established codebase and it will find things. That number is accurate and it is not actionable — nobody fails their own build on day one over problems they did not introduce. So record what is already there, once:

curl -fsSL https://wyro.in/wyro-check.js -o /tmp/wyro-check.js
node /tmp/wyro-check.js . --update-baseline
git add .wyro/baseline.json && git commit -m "Baseline architecture findings"

From that commit on, a build fails only on findings that are not in the ledger. Existing ones are carried, counted, and shown under a fold. When one of them stops firing — because someone fixed it, or deleted the route — the check says so and invites you to bank the improvement.

Tip:The ledger is keyed on the route and the table, not on the file and line. Reformatting a file, moving it, or adding imports above a handler will not resurrect a finding you already accepted.

Configuration

Put a wyro.json at the root of the checked directory. Every setting is optional.

{
  "failOn": "error",
  "maxWarnings": 40,
  "baseline": ".wyro/baseline.json",
  "policy": {
    "rules": {
      "public-entry-guarded": "warn",
      "no-orphan-datastore": "off",
      "declared-public-read": "error"
    }
  }
}
  • failOn — error (default), warn, or never.
  • maxWarnings — a budget across the whole repository. Set it to today's count and the number can only go down.
  • policy.rules — raise a rule to error, lower it to warn, or turn it off. A disabled rule is still named in every report, so “we check for this” never quietly becomes “we used to”.
  • allowUnreadable — see below.

There is no setting that disables the gate. Switching enforcement off is an edit to the workflow file, where it shows up in a pull request and someone has to own it.

Central policy

Everything above is governed by a file inside the repository being checked — which means the repository can change it. That is fine for a team checking its own work, and it is not enforcement.

Create a token in your project's Architecture policy settings, store it as a repository secret, and the rules come from Wyro instead:

      - uses: DominicPaaul7/wyro-check-action@v1
        with:
          token: ${{ secrets.WYRO_TOKEN }}

With a token in play, wyro.json can make the build stricter and cannot make it looser. A rule the project sets to error stays an error however the repository is edited, and the job summary says which settings were overridden, so nobody spends an afternoon editing a file that is not in force.

Note:If a token is supplied and the policy cannot be fetched, the run fails — it does not fall back to the repository's own config. A gate that reverts to a local file whenever the network is unhappy can be switched off by anyone who can break the network. Teams who do not want that dependency simply do not pass a token; the check still runs and still fails builds.

Centrally enforced policy and run history are on Pro and Scale. Everything else on this page — the check, the baseline, the annotations, the CLI — is free and unmetered, on public and private repositories alike.

Exit codes

  • 0 — nothing new.
  • 1 — new findings that fail your configured threshold.
  • 2 — nothing in the directory could be read as a backend.
  • 3 — the gate itself is misconfigured and did not run.

2 and 3 are separate from 1 on purpose. A repository we could not parse produces zero findings, and zero findings must never render as a clean bill of health — so an unreadable directory fails by default, and a package that genuinely has no backend in it opts out in writing with allowUnreadable. A gate that has been broken for three weeks should not look like a gate that is catching problems.

What is checked

The same rules the canvas compiles against, run over the architecture read out of your source. They are reachability questions, which is why they live on a graph rather than in a linter: whether any path from a public endpoint reaches a table without passing through auth is not a question you can answer one file at a time.

  • auth-before-data — an endpoint that can reach a datastore without authenticating.
  • sensitive-public-read — a deliberately public read of payments, credentials or personal data.
  • no-sensitive-to-external — a path from sensitive data to a third-party service.
  • public-entry-guarded — an endpoint with no validator and no rate limiter.
  • auth-entry-public, declared-public-read, no-orphan-datastore — decisions worth seeing rather than defects.

Running it anywhere else

The action is a wrapper around a single Node script with no network calls and no dependencies, so any CI system works — and so does a laptop:

curl -fsSL https://wyro.in/wyro-check.js -o wyro-check.js

node wyro-check.js . --github        # GitHub annotations + job summary
node wyro-check.js . --json          # machine-readable, for any other CI
node wyro-check.js . --fail-on warn  # stricter
node wyro-check.js .                 # a readable report

On GitLab, Buildkite or Jenkins, run the same script and use the exit code. --json gives you the findings, the ones carried in the baseline, and the verdict in one object.

See Quickstart for the canvas side, or run a check with no install at all from a /scan page.