Inkan
↑↓ move↵ open
GitHub

CLI · ADR

0007: Check records in CI

Date

Context#

The build validates the shape of every record: a status outside the configured list or a date that is not a date fails it. It does not validate meaning. A supersedes entry can point at a record that does not exist, two files in one folder can share a number, a record can carry supersededBy while its status still says accepted, and an RFC can sit past its reviewBy date for months. None of this breaks the HTML, so none of it failed a pull request, and the build itself takes long enough that nobody runs it just to check a link.

Decision#

inkan check reads every record directly, without Astro, and runs a fixed set of rules over them. Problems that make the site wrong are errors and set the exit code to 1; problems that make it worse are warnings and only fail with --strict. The output names the file and the front matter line, so it reads like a linter. On GitHub Actions each problem is also emitted as an annotation, so it appears inline in the pull request. The CI files that inkan init creates run check before the build.

How to use it#

Run it#

Terminal window
npx inkan check
decisions/payments/adr/0003-use-stripe.md:9 error supersedes points to "payments/adr/0001-build-our-own-gateway", which does not exist, did you mean "payments/adr/0001-build-our-own-card-gateway"?
decisions/payments/adr/0003-use-stripe.md:6 warning author "jule" is not in _authors.yml, did you mean "jules"?
decisions/sre/_project.yml warning project "sre" has no owners
1 error, 2 warnings (41 records in 7 projects).

Each line is path:line level message. The path is relative to the folder you run in (or to the site root when the file is outside it), and the line points at the offending front matter key when there is one. Problems are grouped by file. With nothing to report the output is one line: No problems found (40 records in 7 projects).

init adds check to package.json, so npm run check does the same.

OptionWhat it does
--strictWarnings fail the command too (exit code 1).
--format jsonPrints a JSON report instead of text (see below).
--root <dir>Folder with inkan.config.*.

The rules#

Errors (the site would be wrong):

RuleWhat it reports
front-matterThe file has no --- block, or the block does not parse as YAML. The line of the YAML error is given when known.
titleThe front matter has no title.
duplicate-numberTwo files in the same <project>/<type>/ folder start with the same number. Both files are reported.
statusstatus is not a key of statuses, with a “did you mean” hint or the list of known keys.
datedate, reviewBy or reviewed is not a date in YYYY-MM-DD form.
review-intervalreviewEvery, on a record or in _project.yml, is not an interval such as 1 year, 6 months, 90 days or never.
reviewersAn entry of reviewers cannot be read, or its state is not pending, approved or changes-requested.
fieldA custom field has an invalid value, or a required field has none. See Custom metadata fields.
project-fieldsfields in a _project.yml is not a valid set of field definitions, or a field name has two types or units across projects.
missing-recordAn entry of supersedes, supersededBy or related names a record that does not exist, with the closest id of the same project and type as a hint.

Warnings (the site is incomplete or misleading):

RuleWhat it reports
one-sided-supersedeA supersedes entry whose target does not list this record in supersededBy, or the other way round. inkan supersede writes both sides.
superseded-statusThe record has supersededBy but its status is not superseded (only when superseded is one of your statuses).
unknown-authorA handle in authors, deciders or reviewers is not an id in _authors.yml. Only when that file exists; records from a repo source are skipped, since their handles belong to another repository.
review-blockedThe status is a current one (accepted, implemented, …) while a reviewer still has changes-requested.
review-overduereviewBy is in the past and the status is still one marked open.
review-dueA decided record’s periodic review is past due: its reviewBy, or reviewEvery after reviewed (or date), is before today. The message names the command that closes it, inkan reviewed <record>. See Review decisions on a schedule.
project-fileA project folder with records has no _project.yml.
project-owners_project.yml has an empty owners list.

Both project rules skip projects that come from sources. Reference values are matched the way the site matches them: full id, short id (sre/adr/3), reference (SRE-ADR-0003) or file path; see Link records by path.

Exit codes#

SituationExit code
No problems, or warnings only0
Warnings only, with --strict1
At least one error1
Config does not load, or a repo source cannot be fetched1, with the message on stderr

JSON output#

Terminal window
npx inkan check --format json
{
"ok": false,
"errors": 1,
"warnings": 1,
"records": 41,
"projects": 7,
"problems": [
{
"level": "error",
"rule": "missing-record",
"message": "supersedes points to \"payments/adr/0001-build-our-own-gateway\", which does not exist, did you mean \"payments/adr/0001-build-our-own-card-gateway\"?",
"file": "decisions/payments/adr/0003-use-stripe.md",
"line": 9,
"record": "payments/adr/0003-use-stripe"
}
]
}

ok is true when there are no errors, regardless of warnings. line and record are absent when they do not apply (a project-owners problem has no record). The exit code follows the same rules as the text output, so a script can read the JSON and still rely on the status.

GitHub Actions annotations#

When GITHUB_ACTIONS=true, check prints one workflow command per problem after the report:

::error file=decisions/payments/adr/0003-use-stripe.md,line=9,title=inkan check (missing-record)::supersedes points to ...

GitHub turns these into annotations on the changed files in the pull request. Paths are relative to GITHUB_WORKSPACE (or the repository root); files outside the workspace and under node_modules, such as fetched sources, get no annotation. GitLab has no equivalent; the job log and the exit code carry the result there.

In the starter CI files#

inkan init writes a GitHub workflow and a GitLab pipeline that run check before the build on every pull or merge request and on the default branch:

.github/workflows/decisions.yml
- uses: actions/checkout@v4
with:
fetch-depth: 0 # full history for "Last updated"
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm install
- run: npx inkan check # broken links, duplicate numbers, unknown statuses … as PR annotations
- run: npm run build
.gitlab-ci.yml
variables:
GIT_DEPTH: 0 # full history for "Last updated"
build:
script:
- npm install
- npx inkan check
- npm run build

A failing check stops the job before the build runs. Add --strict to the step when you want warnings to block merges too. The full history (fetch-depth: 0, GIT_DEPTH: 0) is for the dates and authors inferred from git, not for check. See GitHub and GitLab Pages.

What it does not check#

check reads the front matter and the folder layout. It does not render Markdown, so broken relative links in the body, missing images and Mermaid syntax errors are left to the build, and it does not validate inkan.config.* beyond loading it (the schema runs on load, so a bad config still fails). Content from repo sources is included: missing clones are fetched first, with progress on stderr. See Validate config and content at build time for what the build covers.

Consequences#

  • Reference and numbering mistakes fail a pull request within seconds, with the file and line, and show up inline on GitHub.
  • Errors and warnings are separate by design: a project without owners should not block a hotfix to a record, but --strict is there for repositories that want a clean log.
  • The rule set is fixed. There is no configuration to disable one rule; keep the warning level in mind when a rule does not fit your process.
  • check is a second validator next to the build, with its own messages. The two agree on what a valid status and date are, because both read the same config, but check is stricter about dates (the build accepts anything Date can parse).
  • Records in repo sources are checked but cannot be fixed here; the fix goes to the source repository.

History 1

  1. ImplementedInkan maintainers9e5e535 commit diff