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]
permissions:
contents: read
pull-requests: write # for the verdict comment
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: DominicPaaul7/wyro-check-action@v1Findings appear as annotations on the lines that caused them, as a summary on the job, and as one comment on the pull request. 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.
The comment on the pull request
The verdict is posted as a single comment and rewritten on every push, so the pull request always shows the latest result and never a stack of old ones. It leads with what this pull request added: each new finding with its file and line, then the routes and tables read and how many known findings the baseline carries. A clean pull request gets a one-line comment that says so.
Writing it needs pull-requests: write, as above. Without that permission the run notes it once and the verdict is unchanged. Pull requests from forks always get a read-only token, so on those the result stays in the job summary. A monorepo that runs the action once per service gets one comment per service. Turn the comment off with comment: false.
The README badge
On a public repository, the comment also offers a README badge until the README has one. It shows the default branch's result and links to its public report, and it updates on its own when the code changes:
[](https://wyro.in/scan/OWNER/REPO)
The badge counts every finding in the repository, including ones carried in a baseline, so it can show warnings or errors on a branch where CI is green. It reads passing only when nothing is found, and not checked when the repository could not be read.
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 you do not have to do anything first: on a pull request with no ledger committed, the check reads the same directory at the pull request's base commit and carries what was already there. Only what the pull request adds can fail it.
To carry the existing findings on pushes too, and to see them shrink over time, record them 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.
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, ornever.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 toerror, lower it towarn, or turn itoff. A disabled rule is still named in every report, so “we check for this” never quietly becomes “we used to”.policy.auth— set to"external"when nothing in the code authenticates because something in front of it does (an API gateway, mTLS between services), or because the repository is a template that leaves auth to whoever builds on it.no-auth-anywherethen becomesauth-external, a warning that still lists every open route. It applies only when no route authenticates in code, and the public/scanpage honours it too; it is the one setting there that does.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, so a repository's own policy.auth declaration is ignored and only the central policy can make it. 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.
Private repositories
The Action is free on public repositories. On a private repository it is part of Pro, priced per GitHub organisation: the same token as above is what tells the run your organisation is on Pro, and it brings central policy and run history with it.
Without a token on a private repository, nothing breaks: the check still runs, still fails a pull request that adds a new problem, and the job summary carries a note that the repository needs Pro. The CLI and the agent tool stay free on any code, private included.
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.no-auth-anywhere— nothing in the backend authenticates. Reported once, listing every route that reaches data, instead of once per route, because the fix is auth applied once at the app or router.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.rls-disabled,rls-open-write— a Supabase table the public anon key can reach because row-level security is off, or a policy lets anyone write to it.rls-open-insert— an anonymous inbox: anyone may add rows, nobody outside may read them. A warning to bound the insert, not an error.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 dependencies. It makes no network calls unless it is given a Wyro token or, on GitHub, a token for the pull-request comment, 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.