diff --git a/.github/tests/community_health_contract_test.py b/.github/tests/community_health_contract_test.py
index d62de0f..1ceab2e 100644
--- a/.github/tests/community_health_contract_test.py
+++ b/.github/tests/community_health_contract_test.py
@@ -183,6 +183,17 @@ def test_standards_workflow_pins_actions_and_runs_every_contract(self):
):
self.assertIn(expected, workflow)
+ def test_local_validation_runs_every_ci_contract_suite(self):
+ pattern = r"(?m)^[ \t]*python3[ \t]+\.github/tests/([A-Za-z0-9_]+_test\.py)[ \t]*$"
+ ci = set(re.findall(
+ pattern, self.read_text(".github/workflows/standards-validation.yml"),
+ ))
+ local = set(re.findall(pattern, self.read_text("scripts/validate.sh")))
+ present = {path.name for path in (ROOT / ".github/tests").glob("*_test.py")}
+
+ self.assertEqual(ci, local)
+ self.assertEqual(present, ci)
+
def test_zizmor_action_owns_its_supported_cli_version(self):
workflow = self.read_text(".github/workflows/standards-validation.yml")
zizmor_step = workflow.split(
diff --git a/.github/tests/tracked_validation_test.py b/.github/tests/tracked_validation_test.py
index b657ab1..7154d67 100644
--- a/.github/tests/tracked_validation_test.py
+++ b/.github/tests/tracked_validation_test.py
@@ -1,4 +1,5 @@
from pathlib import Path
+import os
import subprocess
import sys
import tempfile
@@ -9,6 +10,13 @@
VALIDATOR = ROOT / "scripts/validate_tracked_files.py"
+def git_env():
+ names = subprocess.check_output(
+ ["git", "rev-parse", "--local-env-vars"], text=True,
+ ).split()
+ return {key: value for key, value in os.environ.items() if key not in names}
+
+
class TrackedValidationTest(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory(prefix="tracked-validation-")
@@ -16,7 +24,7 @@ def setUp(self):
self.repo = Path(self.temp.name)
subprocess.run(
["git", "init", "--quiet", str(self.repo)], check=True,
- capture_output=True,
+ capture_output=True, env=git_env(),
)
self.write(".gitignore", ".planning/\n.claude/\n__pycache__/\n", tracked=True)
@@ -27,13 +35,13 @@ def write(self, name, content, tracked=False):
if tracked:
subprocess.run(
["git", "add", "--", name], cwd=self.repo, check=True,
- capture_output=True,
+ capture_output=True, env=git_env(),
)
- def run_validation(self, kind):
+ def run_validation(self, kind, env=None):
return subprocess.run(
[sys.executable, str(VALIDATOR), kind], cwd=self.repo,
- capture_output=True, text=True, timeout=60,
+ capture_output=True, text=True, timeout=60, env=env,
)
def assert_valid(self, kind):
@@ -92,6 +100,15 @@ def test_profile_keeps_its_separate_rules(self):
self.write("profile/README.md", "
Profile
\n", tracked=True)
self.assert_invalid("markdown", "MD009")
+ def test_repo_local_git_environment_is_ignored(self):
+ self.write(".github/bad.json", "{broken", tracked=True)
+ env = {**os.environ, "GIT_INDEX_FILE": str(self.repo / "bogus-index")}
+ result = self.run_validation("data", env=env)
+ self.assertNotEqual(0, result.returncode, result.stdout + result.stderr)
+ self.assertIn("JSONDecodeError", result.stdout + result.stderr)
+ self.write(".github/bad.json", '{"ok": true}\n', tracked=True)
+ self.assertEqual(0, self.run_validation("data", env=env).returncode)
+
def test_empty_tracked_input_sets_succeed(self):
for kind in ("python", "data", "markdown"):
with self.subTest(kind=kind):
diff --git a/.github/workflows/main-is-released.yml b/.github/workflows/main-is-released.yml
index 361b473..0c55677 100644
--- a/.github/workflows/main-is-released.yml
+++ b/.github/workflows/main-is-released.yml
@@ -14,6 +14,8 @@ name: Main Is Released
# permissions: {}
# jobs:
# released:
+# permissions:
+# contents: read # A reusable workflow cannot raise its caller's grant.
# uses: CodesWhat/.github/.github/workflows/main-is-released.yml@
#
# Continuously deployed repositories that never tag (a website, a meta repo)
diff --git a/AGENTS.md b/AGENTS.md
index 956f396..acd5c33 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -25,21 +25,26 @@ contract tests that pin the shapes other repositories depend on.
with safe defaults, never repurpose an existing input, and let consumers
adopt by rolling their pin deliberately.
- PRs target `dev/repository-standards`. `main` advances only through
- promotion PRs. Reconcile before promoting:
+ promotion PRs. Before reconciling, account for released content:
+ `git log --oneline origin/dev/repository-standards..origin/main` should list
+ only the previous promotion's squash commit. Any other commit is content that
+ landed on `main` directly; forward-port it to dev through a normal reviewed PR
+ first, because the `ours` merge keeps dev's tree and silently drops it. Then
+ reconcile:
`git merge -s ours origin/main -m "chore(sync): reconcile main before promotion"`.
Verify promotions with tree equality
(`git diff --quiet origin/main origin/dev/repository-standards`), never
commit ancestry.
-- Admit new hosted review/heavy CI work through the shared capacity roster.
- Keep preparation local while queued. After admission, add `review-ready` for
- the first CodeRabbit review; request subsequent fix-batch reviews explicitly.
+- There is no shared admission queue; pace hosted review and heavy CI with
+ the maintainer directly. Add `review-ready` for a PR's first CodeRabbit
+ review and request subsequent fix-batch reviews explicitly.
Freeze pushes while review runs, read its output and verify the covered head.
`.coderabbit.yaml` keeps default/dev targets but disables automatic review on
ordinary pushes. Required review and protection remain effective.
-- Approve ordinary dependency updates in the Renovate dashboard only after
- admission. This repo's wrapper limits ordinary PRs/branches to one at a time
- and one new PR per hour. The shared preset is unchanged; urgent vulnerability
- updates keep their separate handling.
+- Approve ordinary dependency updates in the Renovate dashboard one at a time.
+ This repo's wrapper targets `dev/repository-standards` and limits ordinary
+ PRs/branches to one at a time and one new PR per hour. The shared preset is
+ unchanged; urgent vulnerability updates keep their separate handling.
- Commits are plain Conventional Commits, `(scope): `,
no emoji, no AI attribution trailers.
- Never weaken branch protection or rulesets to land a change.
diff --git a/REPOSITORY_ONBOARDING.md b/REPOSITORY_ONBOARDING.md
index 06068c5..a95300b 100644
--- a/REPOSITORY_ONBOARDING.md
+++ b/REPOSITORY_ONBOARDING.md
@@ -10,14 +10,22 @@ This document defines the target state for a repository being onboarded. Its
presence in the standards repository does not certify that an existing
repository has already passed the checklist.
+Each item is either a requirement, which applies when its trigger does and
+protects a named outcome, or a recommended recipe. A repository that already
+meets the outcome another way keeps its choice: effective existing alternatives
+are accepted without a waiver. Established products keep their current release
+and verification contracts until a scoped change updates them.
+
## 1. Classify the repository
- [ ] Record its purpose, owner, visibility, and lifecycle: public product,
public infrastructure, permanently private, or pre-public.
-- [ ] Set `main` as the default branch and create one active development or
- integration branch. Ordinary changes merge from a feature or fix branch into
- that branch. `main` advances only through a reviewed PR from the active
- branch, never from an independent commit or feature branch.
+- [ ] Set `main` as the default branch and record the repository's branch and
+ release model in `AGENTS.md`. Recommended recipe for a tagged product: one
+ active development or integration branch that ordinary changes merge into,
+ with `main` advancing only through a reviewed promotion PR. Trunk development
+ with immutable release tags, or a site that verifies its deployed commit, is
+ equally acceptable when it fits the artifact.
- [ ] Decide whether it publishes any artifact: a binary, package, container,
website, documentation site, Homebrew formula or cask, or GitHub release.
- [ ] Inventory its languages, package managers, generated files, external
@@ -88,7 +96,9 @@ repositories. Language-specific tests do not.
### Dependency updates and automated review
-- [ ] Add `renovate.json` with the organization preset:
+- [ ] Give the repository an automated dependency-update path that includes
+ the lockfile changes. Recommended recipe: `renovate.json` with the
+ organization preset:
```json
{
@@ -97,16 +107,18 @@ repositories. Language-specific tests do not.
}
```
-- [ ] If the repository uses an integration branch, add exactly one
- `baseBranchPatterns` entry for the active branch. Rotate it when the next
+ A working Dependabot or equivalent setup is an accepted alternative.
+
+- [ ] With Renovate on a repository that uses an integration branch, add exactly
+ one `baseBranchPatterns` entry for the active branch. Rotate it when the next
integration branch is cut. Do not use a pattern that matches old and current
branches because Renovate will open a duplicate PR set against every match.
- [ ] Add only repository-specific Renovate overrides that have a written
reason. Keep lockfile updates enabled and verify dependency PRs contain the
lockfile changes required by a clean install.
-- [ ] Confirm the Renovate GitHub app can access the repository. App access is
- managed at the organization level; do not widen a public-only installation
- to private repositories as an onboarding shortcut.
+- [ ] When using Renovate, confirm its GitHub app can access the repository. App
+ access is managed at the organization level; do not widen a public-only
+ installation to private repositories as an onboarding shortcut.
- [ ] Where CodeRabbit is adopted, use a root `.coderabbit.yaml` appropriate to
the repository. Adopt intentional admission rather than automatically spending
review capacity on every in-progress push. Scope opt-in and incremental-review
@@ -130,77 +142,78 @@ repositories. Language-specific tests do not.
review every PR directly. Inspect the provider's resolved configuration and
delivered review rather than assuming a legacy file or summon proves coverage.
-### Capacity and queued development
-
-Public projects use queue-first development, with adoption work recorded in
-their private internal roadmaps. Begin with one newly admitted hosted-review or
-heavy-CI candidate across public projects for a seven-day measured pilot. Finish
-existing in-flight work safely. A named coordinator records the candidate's
-owner, head/base, dependencies, local verification, services, review-pass count,
-blocked reason and next eligible time before new expensive work starts.
-
-Keep one active change per repo and at most one small dependent successor local.
-Pilot two-layer stacks for genuine dependencies; verify trunk, bot and merge
-compatibility. Draft and stacked PRs can still run CI and deploys. Keep diffs
-small, aiming around 25 changed files and within the actual reviewer limit;
-batching many unrelated items into one oversized PR is not admission control.
-
-Freeze pushes during review, then submit one batch of fixes per pass. Prioritize
-finishing admitted work over opening another PR. Keep the three-pass cap, all
-required checks and valid approvals. Record cooldowns and wait for recovery
-without repeated review summons, workflow reruns or indefinite polling. Review
-capacity after the pilot using actual service pools before expanding concurrency.
-
-Admit one new deployment-producing web candidate per shared deployment pool
-initially. Pilot ordinary dependency arrivals at one concurrent PR and one new
-PR/hour per repo, with staggered schedules or dashboard admission and urgent
-vulnerability handling retained. Retain the monthly Qlty reporting policy below.
-Provider settings, bot controls and required CI changes need scoped reviewed
-rollouts; documentation alone does not configure them. Metadata-only projects
-record applicable controls without installing irrelevant tools or paid services.
-
-This shared repository pilots the controls in its own configuration, without
-changing the live Renovate preset for consumers. The coordinator approves an
-ordinary dependency update in the dashboard only after admission; the local
-wrapper permits one ordinary PR/branch and one new PR per hour. Urgent
-vulnerability handling remains separate. After admitting a stable review
-candidate, apply `review-ready` for its first CodeRabbit review. Later fix
-batches use an explicit review request; pushes do not automatically start
-another review. Verify the provider's resolved configuration and delivered head
-after landing these settings before claiming the pilot is operational.
+### Capacity and pacing
+
+There is no shared admission queue. Pace hosted review, heavy CI and
+deployments so one project does not starve the shared pools, and coordinate
+timing with the maintainer directly. Keep queued development in the project's
+private internal roadmap.
+
+Keep one active change per repository and at most one small dependent successor
+local. Keep diffs small, aiming around 25 changed files and within the actual
+reviewer limit; batching many unrelated items into one oversized PR is not
+pacing.
+
+Freeze pushes during review, then submit one batch of fixes per pass. Finish
+open work before opening another PR. Keep the three-pass cap, all required
+checks and valid approvals. When a provider is in cooldown, wait for recovery
+without repeated review summons, workflow reruns or indefinite polling.
+
+Ordinary dependency arrivals run at one concurrent PR and one new PR per hour
+per repository, through staggered schedules or dashboard approval, with urgent
+vulnerability handling kept separate. Provider settings, bot controls and
+required CI changes need scoped reviewed rollouts; documentation alone does not
+configure them. Metadata-only projects record applicable controls without
+installing irrelevant tools or paid services. The Qlty reporting policy is in
+section 4.
+
+This repository runs those controls in its own configuration without changing
+the Renovate preset that consumers extend. Its wrapper targets the integration
+branch and allows one ordinary PR and branch at a time and one new PR per hour,
+and dashboard updates are approved one at a time. Apply `review-ready` for a
+PR's first CodeRabbit review. Later fix batches use an explicit review request,
+because pushes do not start another review.
### Local gates
-- [ ] Add a root `lefthook.yml` and document how contributors install Lefthook.
-- [ ] Add a `commit-msg` command that enforces plain Conventional Commits:
+Run the checks CI enforces locally before pushing. Recommended recipe: a root
+`lefthook.yml` with the hooks below, with install steps in `AGENTS.md`. An
+existing hook runner such as Husky or pre-commit, or a documented validation
+script, is accepted when it covers the same checks.
+
+- [ ] Enforce the repository's commit convention at `commit-msg`. Without an
+ existing convention, use plain Conventional Commits:
`(scope): `. Allowed types are `feat`, `fix`, `docs`,
`style`, `refactor`, `perf`, `test`, `build`, `ci`, `chore`, and `revert`.
Do not weaken or bypass an existing commit-message hook.
-- [ ] Make `pre-push` sequential and fail-fast. Start with a clean-tree check,
- then run the same formatter/linter, tests, build, and workflow-security checks
- that CI enforces. A missing optional local tool may be reported and left to
- CI, but a tool that is installed must not have its failure swallowed.
+- [ ] Make the pre-push check sequential and fail-fast. Start with a clean-tree
+ check, then run the same formatter/linter, tests, build, and workflow-security
+ checks that CI enforces. A missing optional local tool may be reported and
+ left to CI, but a tool that is installed must not have its failure swallowed.
- [ ] Add language formatters to `pre-commit` only when they can safely operate
on staged files. Verify the hook does not rewrite unrelated work.
-Qlty spans local and CI gates; the full baseline, including why Qlty Cloud
-checks stay non-required, is in the Qlty subsection of section 4.
+Qlty is optional. Its recommended use across local and CI gates, including why
+Qlty Cloud checks stay non-required, is in the Qlty subsection of section 4.
## 3. Protect `main`
-`main` is the released version, not the newest work. It always equals what
-users currently have: the GA tag on a versioned product, production on a
-continuously deployed site. It is never the GA tag plus merged work that has
-not shipped. The next version lives on its own branch, `dev/vX.Y`, which is
-the integration target for every feature pull request, and prereleases are
-tagged there rather than on `main`.
+The recommended model for a tagged product makes `main` the released
+version, not the newest work. It always equals what users currently have, the
+GA tag, and is never the GA tag plus merged work that has not shipped. In that
+recipe the next version lives on its own branch, `dev/vX.Y`, which is the
+integration target for feature pull requests, and prereleases are tagged there
+rather than on `main`. A continuously deployed site verifies its deployed commit
+instead, and a trunk model identifies releases through immutable tags and
+artifacts. The branch ruleset below applies to every model.
The reason is measurement accuracy rather than tidiness. OpenSSF Scorecard,
CodeQL default-branch analysis, Dependabot alerts, README badges, and every
published vulnerability report describe the default branch and nothing else.
Unshipped work on `main` makes all of them describe software no user runs.
-One invariant covers it, and it is mechanical rather than a judgement call:
+For a released-`main` repository, one invariant covers it, and it is
+mechanical rather than a judgement call:
```sh
git describe --exact-match origin/main
@@ -218,13 +231,13 @@ drifts, and an incorrect README is a defect in the released version like any
other. Fix it on a hotfix branch and cut a patch release. For that to stay
honest, a documentation-only release must skip artifact publication —
GoReleaser, signing, deb/rpm, Homebrew, npm — or maintainers will route around
-the rule. Generated assets committed to the repository, such as a star-history
-chart, regenerate at the release cut rather than on a schedule that commits to
-a branch, so they cannot change underneath a tag.
+the rule. Generated assets committed to the repository, such as a chart
+SVG, regenerate at the release cut rather than on a schedule that commits to a
+branch, so they cannot change underneath a tag.
Adopting this on a repository whose `main` is already ahead of its newest tag
-means either cutting a release or resetting `main` to the last released tag.
-That is the maintainer's decision, because it changes what every user sees.
+means cutting a release at the next appropriate boundary. Never move published
+history backward to adopt the model.
Create an active branch ruleset named `Main branch protection`, targeting only
the default branch. Its baseline is:
@@ -299,38 +312,45 @@ Add these only when the behavior exists:
with a clear enforced or advisory threshold.
- [ ] Translation synchronization only for a repository with a translation
source of truth and configured provider credentials.
-- [ ] Release-invariant enforcement for a repository that tags releases. Call
- this repository's `main-is-released.yml` at a pinned full commit SHA from a
- thin caller on `schedule` plus `push` to `main` plus `workflow_dispatch`. The
- called workflow declares `contents: read` itself, and a reusable workflow can
- only narrow what the caller grants, so the caller needs no `permissions`
- block beyond the top-level `permissions: {}` — a job-level grant would only
- widen the ceiling it runs under. It asserts the section 3 invariant: an
+- [ ] Release-invariant enforcement for a repository that defines `main` as its
+ tagged release. Call this repository's `main-is-released.yml` at a pinned
+ full commit SHA from a thin caller on `schedule` plus `push` to `main` plus
+ `workflow_dispatch`. A reusable workflow cannot raise the permissions its
+ caller grants, so a caller with top-level `permissions: {}` must grant
+ `contents: read` on the calling job:
+
+ ```yaml
+ on:
+ schedule: [{cron: "23 7 * * *"}]
+ push:
+ branches: [main]
+ workflow_dispatch:
+ permissions: {}
+ jobs:
+ released:
+ permissions:
+ contents: read
+ uses: CodesWhat/.github/.github/workflows/main-is-released.yml@
+ ```
+
+ It asserts the section 3 invariant: an
untagged `main` head fails, and so does a prerelease tag unless the caller
passes `allow-prerelease: true`. A repository that never tags, such as a
continuously deployed site or a meta repository, should not call this at all
rather than call it with a carve-out; for those, "`main` equals production"
is enforced by the deploy.
-- [ ] Star-history chart refresh for a public repository whose README carries a
- Star History section. Call this repository's `starchart-refresh.yml` at a
- pinned full commit SHA from a thin caller on `release: [published]` plus
- `workflow_dispatch`, granting the job `contents: write` and passing the
- active integration branch as `branch` and the repository's logo colour as
- `accent`. It regenerates a first-party SVG pair from GitHub's stargazer
- timestamps and commits only when the chart actually changed. The trigger is
- the release cut rather than a schedule because a committed artifact
- refreshed on a cron mutates underneath a tag, which section 3 forbids. The
- chart is a committed artifact rather than a live route or a third-party
- embed on purpose: it needs no secret and makes no request at render time, so
- a stale one is visible and a missing one is a visibly broken image, where a
- route that loses its credential serves a plausible placeholder at HTTP 200
- indefinitely. Two files ship, `star-history.svg` and `star-history-dark.svg`,
- because GitHub's theme toggle does not reach a media query inside an
- `
`-embedded SVG but does drive a `` element in the README, so
- the markup chooses and the `
` stays the fallback for anything that does
- not understand ``. Do not embed `star-history.com` or
- `warpchart.dev`; both are retired organization-wide, and adopting the chart
- means removing what it replaced in the same change.
+- [ ] A star chart is optional. A public repository that shows one uses Star
+ History: open it for the actual owner and repository, copy its current README
+ snippet, and verify the rendered README in both themes while signed out.
+ Generate a fresh snippet for that repository and never paste a raw GitHub
+ credential into the README. A hosted chart is live presentation rather than
+ a release artifact, so it needs no refresh workflow.
+- [ ] A repository that still commits a first-party star-history SVG pair keeps
+ its pinned `starchart-refresh.yml` caller, on `release: [published]` plus
+ `workflow_dispatch` with the job granted `contents: write`, until it moves to
+ Star History through its own reviewed change. Trace other consumers of the
+ committed SVGs, such as a website, before removing them or the caller. The
+ reusable workflow keeps its interface for existing pins.
### Qlty
@@ -419,10 +439,11 @@ and [CLI versus Cloud](https://docs.qlty.sh/cli-vs-cloud) documentation.
Actions workflows are present.
- [ ] Install dependencies from a clean checkout, run the full local hook
pipeline, and run the production build.
-- [ ] Open a feature-to-active-branch PR, then an active-branch-to-`main` PR.
- Use the branch selected in section 1. Wait for all CI and an explicit
- CodeRabbit review on both, address every actionable finding, and obtain the
- required non-author and code-owner approvals.
+- [ ] Open a feature PR into the repository's integration target and, when it
+ promotes to `main`, a promotion PR. Use the model recorded in section 1. Wait
+ for all CI and the delivered review the repository requires on each, address
+ every actionable finding, and obtain the required non-author and code-owner
+ approvals.
- [ ] Confirm the PR cannot merge while a required job is pending or failing.
Never weaken branch protection to perform this test or to clear
`REVIEW_REQUIRED`.
diff --git a/renovate.json b/renovate.json
index 3e6d303..976ab53 100644
--- a/renovate.json
+++ b/renovate.json
@@ -1,6 +1,7 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["local>CodesWhat/.github:renovate-config"],
+ "baseBranchPatterns": ["dev/repository-standards"],
"dependencyDashboardApproval": true,
"prConcurrentLimit": 1,
"prHourlyLimit": 1,
diff --git a/scripts/validate_tracked_files.py b/scripts/validate_tracked_files.py
index 347903d..ee904ff 100644
--- a/scripts/validate_tracked_files.py
+++ b/scripts/validate_tracked_files.py
@@ -7,18 +7,27 @@
import sys
+def git_env():
+ """Environment without git's repo-local variables (GIT_DIR, GIT_INDEX_FILE, ...)."""
+ names = subprocess.check_output(
+ ["git", "rev-parse", "--local-env-vars"], text=True, env=os.environ.copy(),
+ ).split()
+ return {key: value for key, value in os.environ.items() if key not in names}
+
+
def main():
parser = argparse.ArgumentParser()
parser.add_argument("kind", choices=("python", "data", "markdown"))
kind = parser.parse_args().kind
+ env = git_env()
root = subprocess.check_output(
- ["git", "rev-parse", "--show-toplevel"], text=True,
+ ["git", "rev-parse", "--show-toplevel"], text=True, env=env,
).strip()
os.chdir(root)
# NUL delimiters preserve filenames containing spaces or newlines.
paths = [
Path(os.fsdecode(path))
- for path in subprocess.check_output(["git", "ls-files", "-z"]).split(b"\0")
+ for path in subprocess.check_output(["git", "ls-files", "-z"], env=env).split(b"\0")
if path
]