Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ New to the project? See the "Your first PR" section in CONTRIBUTING.md.

## Checklist

- [ ] Commits are **DCO signed off** (`git commit -s`). No CLA is required.
- [ ] Commits are **DCO signed off** (`git commit -s`). No CLA is required. This is
enforced by the `dco` check; to sign commits you already made, see
[CONTRIBUTING.md](../CONTRIBUTING.md#sign-off-is-required).
- [ ] If this changes a recorded decision in `docs/adr/`, an ADR is **added or
supersedes** the affected record — with the argument, not just a status flip.
- [ ] If the schema changed, I edited the Zod source
Expand Down
34 changes: 34 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,37 @@ jobs:
exit 0
fi
bun run adr check "${changed[@]}"

dco:
# ADR-0006 chose a DCO over a CLA. CONTRIBUTING.md and the PR template have
# required sign-off since the repository went public, and nothing checked it
# — an unsigned commit would have merged. This is that check (issue #130).
#
# A repository script rather than the DCO GitHub App, so the gate stays
# inside the surface ADR-0007 keeps mechanical and self-contained. It imports
# only Node builtins and therefore runs with no `bun install`: a broken
# dependency graph cannot take the sign-off gate down with it.
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: Set up Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: 1.3.14
- name: Verify every commit carries a DCO sign-off
env:
# Explicit SHAs, not `origin/$BASE_REF..HEAD`. A ref resolves to
# whatever it points at now, so a branch that moved mid-run silently
# changes which commits were checked — the stale-read failure
# ADR-0016 records under "report what was examined". The default
# checkout is the synthetic merge ref, and this range excludes it.
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: bun run check:dco "$BASE_SHA..$HEAD_SHA"
42 changes: 40 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,42 @@ Until `1.0.0`, minor releases may include breaking changes

### Added

- **DCO sign-off is enforced, not just documented** ([ADR-0006](docs/adr/0006-license-apache-2-and-single-monorepo.md)
action item 2, #130). A `dco` job checks every commit a pull request adds and is a
required status check on `main`. Sign-off was practiced by every contributor and
required by both `CONTRIBUTING.md` and the PR template, but no ruleset check
enforced it — the control was honor-system, and an unsigned commit would have
merged.

**A repository script (`scripts/check-dco.ts`), not the [DCO app](https://github.com/apps/dco),**
so the gate stays inside the surface [ADR-0007](docs/adr/0007-adapter-isolation-and-public-surface-build.md)
keeps mechanical and self-contained rather than adding a third-party app to the IP
boundary. It imports only Node builtins and runs with no `bun install`, so a broken
dependency graph cannot take the sign-off gate down with it. Accept/reject semantics
track the app's, because that is the contract contributors already know, with two
deliberate differences: a sign-off's **name and address must come from one identity**
(the app takes the name from either the author or the committer and the address from
either, so a web-UI commit signed `Jane Doe <noreply@github.com>` passes there), and
a **bot still has to sign** — app accounts are exempt from the *address* half only,
because Dependabot signs from `support@github.com` and cannot equal its own author
address by construction, but the trailer must still name the bot. Every exemption is
named in the job output, so a commit is never skipped silently.

**The squash-merge body setting moved from `BLANK` to `COMMIT_MESSAGES`.** A
pull-request check certifies the *contributor*, which is what the DCO is for, but a
blank squash body discards every trailer at merge: `main`'s own head (`f74c089`)
carried no sign-off while every commit proposed to it carried one. ADR-0006 traded
away commercial leverage for provenance, and provenance that is verified and then
thrown away at merge is not provenance.

Observed rejecting a real unsigned commit in a real repository before it counted as
coverage ([ADR-0016](docs/adr/0016-require-every-check-to-be-observed-failing-before-it-counts-as-coverage.md)).
The negative cases are permanent in `scripts/check-dco.test.ts`, including the one
that matters most for this class of check: **an empty commit range is an error, not
a pass.** An unfetched or misspelled base ref makes `git log` return nothing and
drives every count in the report to zero, which renders identically to a clean run —
the exact fail-quiet shape ADR-0016 exists to prevent.

- **Badges — corpus size and ARB queue depth — as recipes over output adrkit
already produces.** A new [badges guide](https://adrkit.dev/badges/) documents two
snippets, both rendering a number through shields.io from JSON your own repository
Expand Down Expand Up @@ -72,8 +108,10 @@ Until `1.0.0`, minor releases may include breaking changes
trail says when the decision was taken and by whom rather than leaving `status`
to carry it alone. Stale action-item checkboxes were corrected to match verified
reality; items that are genuinely open were left unchecked, including ADR-0006's
DCO bot, which is documented in CONTRIBUTING.md but which no ruleset check
enforces, ADR-0008's re-import pull request, which is unbuilt because non-MADR
DCO bot, which CONTRIBUTING.md required but which no ruleset check enforced at the
time of that ratification — closed later in this same release by the DCO gate
described above — ADR-0008's re-import pull request, which is unbuilt because
non-MADR
re-import is unbuilt, and ADR-0009's catalog port item — the port type exists but
no adapter implementation ships, since `catalog-backstage` is placement and
dependency boundary only. Ratification rests on the resolution semantics, which
Expand Down
20 changes: 20 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,26 @@ why.
git commit -s -m "your message"
```

**This is enforced.** The `dco` job checks every commit your pull request adds and
is a required status check, so an unsigned commit cannot merge. If you forgot, sign
the whole branch at once and force-push:

```
git rebase --signoff origin/main
git push --force-with-lease
```

The trailer must name you: `Signed-off-by: Your Name <your@email>`, matching the
commit's author or committer exactly. Merge commits are exempt — the commits they
merge carry the certification. Bot accounts are exempt from the address half only,
because they sign from a service address; their trailer must still name them.

**Editing in the browser?** A commit made through the GitHub web editor carries no
sign-off, and you cannot add one from the browser. Clone the branch, run the rebase
above, and force-push — or make the change locally with `git commit -s` to begin
with. This catches docs-only contributions in particular, so it is worth knowing
before you start rather than after the check goes red.

## Two hard rules

These are enforced in CI. A PR that violates either will fail, and the fix is to
Expand Down
2 changes: 1 addition & 1 deletion MANIFEST.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ adrkit/
│ └── adapters/
│ └── spec-kit/ @adrkit/spec-kit — independently versioned
│ (ADR-0007); ships no dist, no dependencies
├── scripts/ release pack/publish, dep boundary + audit gates
├── scripts/ release pack/publish, dep boundary + audit + DCO gates
├── site/ Astro Starlight docs site; hosts the schema at its $id
├── specs/ 001–009, one spec-kit feature per phase/spike
└── docs/
Expand Down
29 changes: 28 additions & 1 deletion docs/adr/0006-license-apache-2-and-single-monorepo.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@ affects:
pattern: "LICENSE"
- type: path
pattern: "CONTRIBUTING.md"
assertions:
- id: dco-signoff-required
description: >-
Every commit a pull request adds must carry a Signed-off-by trailer naming
its author or its committer. Merge commits are exempt; app accounts are
exempt from the address half only, because they sign from a service
address, and their trailer must still name them.
engine: custom
expression: dco-signoff-required
input: source
severity: error
provenance:
authoredBy: agent-drafted
ratifiedBy: "@mbeacom"
Expand Down Expand Up @@ -147,7 +158,23 @@ sense if standardization is genuinely the goal — which ADR-0002 asserts it is.
## Action items

1. [x] LICENSE, NOTICE, per-package `license` fields
2. [ ] DCO bot enabled on the repository
2. [x] DCO sign-off enforced on the repository. **Done 2026-08-12** (#130) as the
`dco` job in `.github/workflows/ci.yml`, a required check on the `main`
ruleset, backed by `scripts/check-dco.ts`. A repository script rather than
the [DCO app](https://github.com/apps/dco), so the gate stays inside the
surface [ADR-0007](0007-adapter-isolation-and-public-surface-build.md)
keeps mechanical and self-contained; it imports only Node builtins, so a
broken dependency graph cannot take the sign-off gate down with it.
Observed rejecting a real unsigned commit before it counted as coverage
([ADR-0016](0016-require-every-check-to-be-observed-failing-before-it-counts-as-coverage.md)),
with the negative cases kept in `scripts/check-dco.test.ts`.

The repository's squash-merge body setting moved from `BLANK` to
`COMMIT_MESSAGES` in the same change. A pull-request check certifies the
*contributor*, which is what the DCO is for, but a blank squash body
discards every trailer at merge — so `main` carried unsigned commits
(`f74c089`) while every commit proposed to it was signed. Provenance that
is verified and then thrown away is not provenance.
3. [x] `schema/LICENSE` (CC0) with the carve-out stated plainly in the README
4. [x] SECURITY.md and CODE_OF_CONDUCT.md before the repository goes public
5. [ ] Resolve external participation obligations before first public push
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"check:changelog": "bun run scripts/check-changelog.ts",
"check:freeze-hashes": "bun run scripts/check-freeze-hashes.ts",
"check:doc-pins": "bun run scripts/check-doc-cli-versions.ts",
"check:dco": "bun run scripts/check-dco.ts",
"audit:gate": "bun run scripts/audit-gate.ts",
"adr": "bun packages/cli/src/index.ts",
"site:queue": "bun packages/cli/src/index.ts queue --format json > site/public/queue.json",
Expand Down
Loading