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
38 changes: 32 additions & 6 deletions docs/repository-standards.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ note allows.
| `.opx/config.json` | `.opx/config.json` | Routes JS, JSON, & Markdown to one ESLint config; `opx enable release` adds `"release": true` |
| `.husky/post-commit` | `.husky/post-commit` | Runs `opx` hooks & lint; never blocks a commit. A repository with its own hooks (`pre-commit`, `commit-msg`) keeps them |
| `.env.example` | `.env.example` | Lists every variable the repo reads; required only where it reads some |
| `.github/CODEOWNERS` | `.github/CODEOWNERS` | One line, `* @castastrophe`; the ruleset requires code owner review |
| `.github/renovate.json` | `renovate.json` | One-month release age, monthly lockfile refresh |
| `.github/release.yml` | `release.yml` | Changelog categories keyed on `type:` labels |
| `.gitignore` | `.gitignore` | Add `_site` for repos that build an Eleventy site, & `coverage` for every repo with tests |
Expand Down Expand Up @@ -179,7 +180,7 @@ on the same files where it is not adopted yet.

| File | Rule |
| ----------------------------- | -------------------------------------------------------------------------------------------- |
| `.github/workflows/ci.yml` | On `pull_request` & pushes to `main`. `permissions: contents: read`. Steps: checkout with `persist-credentials: false`, `corepack enable`, `setup-node` with `node-version: ${{ matrix.node }}` & `cache: yarn`, `yarn install --immutable`, `yarn lint`, `yarn build`, `yarn typecheck`, `yarn test` (`yarn coverage` on the Node 26 job, with the coverage steps below). `permissions` add `pull-requests: write` for the coverage comment. One job named `CI (node${{ matrix.node }})` over `node: [26, 24]`, so the checks are `CI (node26)` & `CI (node24)`; both are required |
| `.github/workflows/ci.yml` | On `pull_request`, `merge_group`, & pushes to `main`. `permissions: contents: read`. Steps: checkout with `persist-credentials: false`, `corepack enable`, `setup-node` with `node-version: ${{ matrix.node }}` & `cache: yarn`, `yarn install --immutable`, `yarn lint`, `yarn build`, `yarn typecheck`, `yarn test` (`yarn coverage` on the Node 26 job, with the coverage steps below). `permissions` add `pull-requests: write` for the coverage comment. One job named `CI (node${{ matrix.node }})` over `node: [26, 24]`, so the checks are `CI (node26)` & `CI (node24)`; both are required |
| `.github/workflows/pages.yml` | Calls the org's reusable `pages.yml@main` with `output-dir`; grants `contents: write` & `pull-requests: write` |
| `.github/renovate.json` | The baseline above, with `reviewers` naming the maintainer |

Expand Down Expand Up @@ -247,11 +248,34 @@ job only; see [Coverage](testing.md#coverage) for the tools & the floor.
The Commands table in `AGENTS.md` lists only commands that run from the repository root &
that have been run.

## Not covered
## Branch protection

GitHub-side settings (branch protection, required checks, merge options, Pages source)
are not files. The one rule that depends on them: `CI (node26)` & `CI (node24)` are required
status checks. Add the rest here once confirmed.
Every repository carries one repository-level ruleset, named `Default`, that targets the
default branch. Org-level rulesets are not used: the merge queue is available only on
repository rulesets. Import it from the repository's Settings → Rules → Rulesets.

| Rule | Setting |
| ------------------------ | --------------------------------------------------------------------------------------- |
| Deletion, force push | Blocked (`deletion`, `non_fast_forward`) |
| Creation | Restricted to the bypass actors (`creation`) |
| History | Linear (`required_linear_history`) |
| Commit signatures | Required (`required_signatures`); queue & bot commits show "Verified" |
| Pull request | 1 approval, code owner review, stale approvals dismissed on push, last push approved, threads resolved, squash merge only |
| Required status checks | `CI (node26)` & `CI (node24)`, plus any check the repository's own workflows add; checks are not strict, so a branch need not be current |
| Merge queue | Squash, all green, up to 5 entries, 60 minute check timeout |
| Bypass | Organization admins, for pull requests only |

The merge queue runs each required check on a `gh-readonly-queue/*` branch, so every
workflow that provides a required check lists `merge_group:` under `on:`. Without it, a
queued pull request waits for checks that never start. A workflow that provides no
required check, such as a release or a Pages workflow, leaves it out.

Changing a job's name changes the check's name: rename the job in a pull request, then
update the ruleset's required checks, never the reverse, or the queue waits on a check
that no longer exists.

The ruleset & the secrets are GitHub settings, not files, so the matrix tracks them as
manual rows. Pages source & merge options outside the ruleset are not covered yet.

## Adopting the standard

Expand All @@ -263,4 +287,6 @@ status checks. Add the rest here once confirmed.
6. If the repo publishes, add `@allons-y/opx-release` & run `opx enable release`.
7. Add `ci.yml` from the snippet above, which gives the `CI (node26)` & `CI (node24)` jobs, & `pages.yml`.
8. Add `AGENTS.md` & link [testing.md](testing.md) from its Testing section.
9. Run `yarn lint`, `yarn build`, `yarn typecheck`, & `yarn test`.
9. Add `.github/CODEOWNERS` & `merge_group:` to every workflow that provides a required check.
10. Run `yarn lint`, `yarn build`, `yarn typecheck`, & `yarn test`.
11. Import the `Default` ruleset, then set `CI (node26)` & `CI (node24)` as required checks.
204 changes: 198 additions & 6 deletions skills/standards-matrix/data/manual.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,209 @@
{
"area": "Settings",
"tool": "Required checks",
"std": "Both Node jobs are required status checks.",
"src": "GitHub branch protection",
"std": "CI (node26) & CI (node24) are required status checks.",
"src": "Repository ruleset",
"repos": {
".github": {
"status": "you",
"state": "Not verified: a GitHub setting.",
"status": "ok",
"state": "Requires CI (node26) & CI (node24).",
"verified": "2026-11-06"
},
"actions-file-diff": {
"status": "gap",
"state": "Requires test, not the Node jobs; rename the jobs first.",
"verified": "2026-11-06"
},
"actions-weaver": {
"status": "gap",
"state": "Requires test, not the Node jobs; rename the jobs first.",
"verified": "2026-11-06"
},
"agent-skills": {
"status": "gap",
"state": "Requires Build docs site; Lint; Test, not the Node jobs; rename the jobs first.",
"verified": "2026-11-06"
},
"allonsy-studio.github.io": {
"status": "gap",
"state": "Requires no checks.",
"verified": "2026-11-06"
},
"course-kit": {
"status": "gap",
"state": "Requires test, not the Node jobs; rename the jobs first.",
"verified": "2026-11-06"
},
"dashly": {
"status": "gap",
"state": "Requires Lint; Test, not the Node jobs; rename the jobs first.",
"verified": "2026-11-06"
},
"eleventy-plugin-netlify-data-store": {
"status": "gap",
"state": "Requires Lint; Test \u2014 Node 22 on macos-latest; Test \u2014 Node 22 on ubuntu-latest; & more, not the Node jobs; rename the jobs first.",
"verified": "2026-11-06"
},
"envoy": {
"status": "ok",
"state": "Requires CI (node26) & CI (node24).",
"verified": "2026-11-06"
},
"glob-concat-cli": {
"status": "gap",
"state": "Requires coverage (24); lint (24); test (22); & more, not the Node jobs; rename the jobs first.",
"verified": "2026-11-06"
},
"link-checker": {
"status": "gap",
"state": "Requires test (24); test (lts/*), not the Node jobs; rename the jobs first.",
"verified": "2026-11-06"
},
"markdown-it-anchor-sections": {
"status": "gap",
"state": "Requires build-validation, not the Node jobs; rename the jobs first.",
"verified": "2026-11-06"
},
"opx": {
"status": "gap",
"state": "Requires build; lint; smoke; & more, not the Node jobs; rename the jobs first.",
"verified": "2026-11-06"
},
"postcss-licensing": {
"status": "gap",
"state": "Requires test, not the Node jobs; rename the jobs first.",
"verified": "2026-11-06"
},
"stylelint-header": {
"status": "gap",
"state": "Requires Coverage reporting; Stylelint v17 | macos-latest | node v24; Stylelint v17 | ubuntu-latest | node v24; & more, not the Node jobs; rename the jobs first.",
"verified": "2026-11-06"
},
"template-actions": {
"status": "gap",
"state": "Requires test, not the Node jobs; rename the jobs first.",
"verified": "2026-11-06"
},
"webc-docs": {
"status": "you",
"state": "Not verified: a GitHub setting.",
"status": "ok",
"state": "Requires CI (node26) & CI (node24).",
"verified": "2026-11-06"
},
"webc-resources": {
"status": "gap",
"state": "Requires Lint, test, pack (Node 22); Lint, test, pack (Node 24), not the Node jobs; rename the jobs first.",
"verified": "2026-11-06"
},
"website": {
"status": "gap",
"state": "Requires Browser tests (Cypress \u2014 a11y + internal links); Unit tests (Vitest); build-validation, not the Node jobs; rename the jobs first.",
"verified": "2026-11-06"
}
},
"default": {
"status": "you",
"state": "Not verified: a GitHub setting."
}
},
{
"area": "Settings",
"tool": "Merge queue",
"std": "The Default ruleset has the merge queue on.",
"src": "Repository ruleset",
"repos": {
".github": {
"status": "ok",
"state": "Merge queue on; signed commits not required yet, pending the queue signing test.",
"verified": "2026-11-06"
},
"actions-file-diff": {
"status": "gap",
"state": "No merge queue; signed commits not required yet, pending the queue signing test.",
"verified": "2026-11-06"
},
"actions-weaver": {
"status": "ok",
"state": "Merge queue on; signed commits not required yet, pending the queue signing test.",
"verified": "2026-11-06"
},
"agent-skills": {
"status": "ok",
"state": "Merge queue on; signed commits not required yet, pending the queue signing test.",
"verified": "2026-11-06"
},
"allonsy-studio.github.io": {
"status": "ok",
"state": "Merge queue on; signed commits not required yet, pending the queue signing test.",
"verified": "2026-11-06"
},
"course-kit": {
"status": "ok",
"state": "Merge queue on; signed commits not required yet, pending the queue signing test.",
"verified": "2026-11-06"
},
"dashly": {
"status": "ok",
"state": "Merge queue on; signed commits not required yet, pending the queue signing test.",
"verified": "2026-11-06"
},
"eleventy-plugin-netlify-data-store": {
"status": "ok",
"state": "Merge queue on; signed commits not required yet, pending the queue signing test.",
"verified": "2026-11-06"
},
"envoy": {
"status": "ok",
"state": "Merge queue on; signed commits not required yet, pending the queue signing test.",
"verified": "2026-11-06"
},
"glob-concat-cli": {
"status": "ok",
"state": "Merge queue on; signed commits not required yet, pending the queue signing test.",
"verified": "2026-11-06"
},
"link-checker": {
"status": "ok",
"state": "Merge queue on; signed commits not required yet, pending the queue signing test.",
"verified": "2026-11-06"
},
"markdown-it-anchor-sections": {
"status": "ok",
"state": "Merge queue on; signed commits not required yet, pending the queue signing test.",
"verified": "2026-11-06"
},
"opx": {
"status": "ok",
"state": "Merge queue on; signed commits not required yet, pending the queue signing test.",
"verified": "2026-11-06"
},
"postcss-licensing": {
"status": "ok",
"state": "Merge queue on; signed commits not required yet, pending the queue signing test.",
"verified": "2026-11-06"
},
"stylelint-header": {
"status": "ok",
"state": "Merge queue on; signed commits not required yet, pending the queue signing test.",
"verified": "2026-11-06"
},
"template-actions": {
"status": "ok",
"state": "Merge queue on; signed commits not required yet, pending the queue signing test.",
"verified": "2026-11-06"
},
"webc-docs": {
"status": "ok",
"state": "Merge queue on; signed commits not required yet, pending the queue signing test.",
"verified": "2026-11-06"
},
"webc-resources": {
"status": "ok",
"state": "Merge queue on; signed commits not required yet, pending the queue signing test.",
"verified": "2026-11-06"
},
"website": {
"status": "gap",
"state": "No merge queue; signed commits not required yet, pending the queue signing test.",
"verified": "2026-11-06"
}
},
Expand Down
2 changes: 2 additions & 0 deletions skills/standards-matrix/references/checks.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ Each row of the matrix comes from one check in `scripts/lib/checks.js`. A check
| `husky` | Husky | The repository has no `.husky` hooks. The baseline `post-commit` & a repository's own hooks both pass; the state names which | never |
| `opx-release` | opx release | A publishing repository lacks the plugin, `"release": true`, or the `changeset`, `version`, & `release` scripts | Nothing is published |
| `ci` | ci.yml | There is no workflow that runs tests, the workflow is not named `ci.yml`, no workflow runs lint, test, (build & typecheck for TypeScript), or the CI workflow lacks node 26 & node 24 jobs named `CI (node26)` & `CI (node24)` | never |
| `merge-group` | merge_group trigger | A `ci`, `test`, `tests`, `testing`, or `build` workflow runs on `pull_request` but not on `merge_group` | No such workflow runs on `pull_request` |
| `codeowners` | CODEOWNERS | There is no `.github/CODEOWNERS` (or root, or `docs/`) file, or it has no default `*` owner | never |
| `pages` | Pages | A site exists & `pages.yml` does not call the shared workflow | It hosts the workflow, or there is no site |
| `renovate` | Renovate | `renovate.json` is missing or differs from the baseline | never |
| `agents` | AGENTS.md | Missing (a lone `CLAUDE.md` is noted), or (outside `.github`) missing the org link or a standard section | never |
Expand Down
46 changes: 40 additions & 6 deletions skills/standards-matrix/scripts/lib/checks.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ const ORG_ROOT_NAME = "@allons-y/core";
/** Variables every environment provides, so `.env.example` need not list them. */
const AMBIENT_ENV = new Set(["CI", "NODE_ENV", "HOME", "PATH", "TMPDIR", "TEMP", "TMP", "USER", "SHELL", "NO_COLOR", "FORCE_COLOR", "NODE_OPTIONS", "NODE_PATH", "PWD", "DEBUG"]);

/** Workflow files that provide the required checks. */
const CI_WORKFLOW = /^(ci|test|tests|testing|build)\.ya?ml$/;

/** Dependencies that provide coverage: c8, Vitest's v8 provider, & Jest's built-in reporter. */
const COVERAGE_TOOLS = ["c8", "@vitest/coverage-v8", "jest"];

Expand All @@ -36,6 +39,11 @@ const hasTypeScript = (repo) => tsSourceFiles(repo).length > 0;
const tsPackages = (repo) => repo.packages().filter((p) => repo.has(p.dir === "." ? "tsconfig.json" : `${p.dir}/tsconfig.json`));
const workflow = (repo, name) => repo.read(`.github/workflows/${name}`) ?? "";
const workflowNames = (repo) => repo.files().filter((f) => /^\.github\/workflows\/[^/]+\.ya?ml$/.test(f)).map((f) => f.split("/").pop());
/** The CI workflow: `ci.yml`, else the first of the usual test workflow names. */
const ciWorkflowName = (repo) => {
const names = workflowNames(repo);
return names.includes("ci.yml") ? "ci.yml" : names.find((n) => /^(ci|test|tests|testing)\.ya?ml$/.test(n));
};
const allWorkflows = (repo) => workflowNames(repo).map((name) => workflow(repo, name)).join("\n");
/** Node majors a workflow runs: a matrix list (`node: [26, 24]`) or explicit `node-version` values. */
function nodeMajors(text) {
Expand Down Expand Up @@ -79,7 +87,7 @@ export const CHECKS = [
run(repo) {
const nvmrc = repo.read(".nvmrc")?.trim();
const engines = rootPkg(repo).engines?.node;
const ci = workflow(repo, "ci.yml");
const ci = workflow(repo, ciWorkflowName(repo) ?? "ci.yml");
const problems = [];
if (nvmrc !== "26") problems.push(`.nvmrc is ${nvmrc ?? "missing"}`);
if (!/^>=\s*24(\.\d+){0,2}$/.test(engines ?? "")) problems.push(`engines.node is ${engines ?? "missing"}`);
Expand Down Expand Up @@ -380,11 +388,8 @@ export const CHECKS = [
src: ".github/workflows/ci.yml",
run(repo) {
const names = workflowNames(repo);
let file = "ci.yml";
if (!names.includes(file)) {
file = names.find((n) => /^(ci|test|tests|testing)\.ya?ml$/.test(n));
if (!file) return gap(names.length ? `No CI workflow that runs tests (workflows: ${joinList(names, 5)}).` : "No workflows.");
}
const file = ciWorkflowName(repo);
if (!file) return gap(names.length ? `No CI workflow that runs tests (workflows: ${joinList(names, 5)}).` : "No workflows.");
const ci = workflow(repo, file);
const everything = allWorkflows(repo);
const ts = hasTypeScript(repo);
Expand All @@ -401,6 +406,35 @@ export const CHECKS = [
return ok(ts ? "Lint, build, typecheck, & test on both Node versions." : "Lint & test on both Node versions; build & typecheck are omitted because nothing builds.");
},
},
{
id: "merge-group",
area: "CI",
tool: "merge_group trigger",
std: "Each workflow that provides a required check also runs on merge_group.",
src: ".github/workflows/",
run(repo) {
const files = workflowNames(repo).filter((n) => CI_WORKFLOW.test(n));
const pr = files.filter((n) => /^\s*pull_request(_target)?:/m.test(workflow(repo, n)));
if (!pr.length) return na("No pull request workflow provides a required check.");
const missing = pr.filter((n) => !/^\s*merge_group:/m.test(workflow(repo, n)));
if (missing.length) return gap(`${joinList(missing)} ${missing.length > 1 ? "lack" : "lacks"} a merge_group trigger, so a queued pull request never gets its checks.`);
return ok(`${joinList(pr)} run${pr.length > 1 ? "" : "s"} on merge_group.`);
},
},
{
id: "codeowners",
area: "Config",
tool: "CODEOWNERS",
std: "A default owner (`* @castastrophe`) routes review.",
src: ".github/CODEOWNERS",
run(repo) {
const rel = [".github/CODEOWNERS", "CODEOWNERS", "docs/CODEOWNERS"].find((f) => repo.has(f));
if (!rel) return gap("No CODEOWNERS file.");
const owner = (repo.read(rel) ?? "").split("\n").map((l) => l.trim()).find((l) => /^\*\s+@/.test(l));
if (!owner) return gap(`${rel} has no default \`*\` owner.`);
return ok(`${rel}: ${owner.replace(/\s+/g, " ")}.`);
},
},
{
id: "pages",
area: "CI",
Expand Down
Loading
Loading