diff --git a/docs/adopting.md b/docs/adopting.md index 2f6064ca..3b2d6202 100644 --- a/docs/adopting.md +++ b/docs/adopting.md @@ -6,7 +6,7 @@ How a repository joins: its kind of project, what it adds of its own, and what t Nothing here is hard-wired to one stack. What is specific to a kind of project (its review profile, the rules its reviewers taught us, the settings -of its gates, the workflows of its battery) lives in a pack under +of its gates, the workflows of its [battery](../kinds/README.md#the-battery)) lives in a pack under [`kinds/`](../kinds/), as data, and a repository declares its kind with `kind:` in its `.github/review-policy.yml`. Every rejection a review sends back becomes one entry in the kind's `rules.yml`, with fixtures, never a new diff --git a/docs/configuration.md b/docs/configuration.md index 4931aff7..cdb44b58 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -89,7 +89,7 @@ What a level may do: `any` value; `lower-only`; `stricter` (switch off, narrow, |---|---|---|---|---|---|---| | `name`: The kind's name. | each one's own, in `kinds/*/pack.yml` | `any` | `never` | the kind's name for people | [`detect.py`](../scripts/detect.py) | #191 | | `aliases`: Other names of the kind. | each one's own, in `kinds/*/pack.yml` | `any` | `never` | names a profile may still use | [`policy.py`](../scripts/policy.py) | #191 | -| `workflows`: The workflows that make up the kind's battery. | each one's own, in `kinds/*/pack.yml` | `any` | `never` | reusable workflows of the engine | [`sync-repos.py`](../scripts/sync-repos.py) | #191 | +| `workflows`: The workflows that make up the kind's [battery](../kinds/README.md#the-battery). | each one's own, in `kinds/*/pack.yml` | `any` | `never` | reusable workflows of the engine | [`sync-repos.py`](../scripts/sync-repos.py) | #191 | | `own-checks`: What a repository of the kind requires of its own jobs. | each one's own, in `kinds/*/pack.yml` | `any` | `never` | a ruleset that exists is never rewritten | [`sync-repos.py`](../scripts/sync-repos.py) | #552 | | `pre-pr`: What a repository of the kind's `make pre-pr` runs, in words. | each one's own, in `kinds/*/pack.yml` | `any` | `never` | every check the pull request and the merge queue run | [`pre-pr.py`](../scripts/pre-pr.py) | #479 | | `pre-pr-make`: How `make pre-pr` runs them (dx.mk). | each one's own, in `kinds/*/pack.yml` | `any` | `never` | bash, one command a line | [`pre-pr.py`](../scripts/pre-pr.py), [`sync-repos.py`](../scripts/sync-repos.py) | #479 | diff --git a/docs/maintaining.md b/docs/maintaining.md index c6c2bd0f..87a99ef7 100644 --- a/docs/maintaining.md +++ b/docs/maintaining.md @@ -14,7 +14,7 @@ job after `v7` moved re-runs the old workflow. Reopen the pull request, or push to it, for a run on the new one. `v5`, `v4`, `v3`, `v2` and `v1` stay where they are. -A change to a template in [`workflow-templates/`](../workflow-templates/) reaches no repository by itself: each one has its own copy, its caller. The daily drift check lists every caller that differs from its template, with the job and the template's path (it compares what the jobs do, never their comments, so a template's comment can change without putting any repository out of step), in the issue "Repositories out of step", and every caller `sync-repos.py callers` would add to it (#671: the ones `every-repository` names and those of its kind's battery; the caller of its kind's release only when its entry in `repos.yml` gives its `slug` or its kind's place, wordpress.org for a plugin, publishes it). Before it, the same daily run opens, for each repository `repos.yml` lists whose callers fell behind, the issue and the pull request that bring it in step (`scripts/sync-repos.py callers`, #672), none while one is open; `python3 scripts/sync-repos.py callers --repo NAME` does the same for one by hand. A callers pull request closed without merging is opened again the next day: `repos.yml` `exclude` keeps a repository out. Run from a checkout that is `main` as it is now, the `org-sync` lane of [`policy/autonomy.yml`](../policy/autonomy.yml) accepts the issue, and the pull request merges on its own when every job it changes is its template's and none changes its permissions or secrets (a new job always waits); otherwise the maintainer accepts and merges them there. A new input a template asks to fill in goes into `repos.yml` (`callers`, `repository-owned`) in the same pull request, or the check would hold every repository to the template's example value, and `callers` writes no caller that would still hold it: that file is left out and named, with the repository and the input, and the others go in (#671). A template never says "Fill in": its header says where a value comes from, so a new caller is its template whole. +A change to a template in [`workflow-templates/`](../workflow-templates/) reaches no repository by itself: each one has its own copy, its caller. The daily drift check lists every caller that differs from its template, with the job and the template's path (it compares what the jobs do, never their comments, so a template's comment can change without putting any repository out of step), in the issue "Repositories out of step", and every caller `sync-repos.py callers` would add to it (#671: the ones `every-repository` names and those of its kind's [battery](../kinds/README.md#the-battery); the caller of its kind's release only when its entry in `repos.yml` gives its `slug` or its kind's place, wordpress.org for a plugin, publishes it). Before it, the same daily run opens, for each repository `repos.yml` lists whose callers fell behind, the issue and the pull request that bring it in step (`scripts/sync-repos.py callers`, #672), none while one is open; `python3 scripts/sync-repos.py callers --repo NAME` does the same for one by hand. A callers pull request closed without merging is opened again the next day: `repos.yml` `exclude` keeps a repository out. Run from a checkout that is `main` as it is now, the `org-sync` lane of [`policy/autonomy.yml`](../policy/autonomy.yml) accepts the issue, and the pull request merges on its own when every job it changes is its template's and none changes its permissions or secrets (a new job always waits); otherwise the maintainer accepts and merges them there. A new input a template asks to fill in goes into `repos.yml` (`callers`, `repository-owned`) in the same pull request, or the check would hold every repository to the template's example value, and `callers` writes no caller that would still hold it: that file is left out and named, with the repository and the input, and the others go in (#671). A template never says "Fill in": its header says where a value comes from, so a new caller is its template whole. Who signs what (#491) reaches the repositories in two steps. The release brings the rule (the `AGENTS.md` block, through `sync-repos.py files`), the conventions check at org.yml `attribution: warn` and the merge's trailers everywhere at once. Then each repository's part is a sub-issue of #491, accepted with it: remove any text of its own that contradicts the rule, give a public repository's `CONTRIBUTING.md` a "Credits" paragraph that points to [CONTRIBUTING.md, "Who signs what"](../CONTRIBUTING.md#who-signs-what), and in DiluxOne/dilux-agentic-workflow make `scripts/check_commits.py` and `ddw/rules/commits.instructions.md` accept `Assisted-by:` beside `AI-assisted:` and `AI-full:` and refuse a `Co-authored-by` only when it names a model, never a person: the cases `scripts/credits.py --test` marks C8 are the reference its tests mirror. Once every repository is in step, set `attribution: required`. On your own machine, `"includeCoAuthoredBy": false` in Claude Code's settings stops the trailer it adds by itself. diff --git a/kinds/README.md b/kinds/README.md index 6006f3a7..32b4092b 100644 --- a/kinds/README.md +++ b/kinds/README.md @@ -3,7 +3,7 @@ The organisation's workflows and scripts know nothing about WordPress, or about any other stack. What is specific to a kind of project lives in a **pack**, a directory here, as data: its review profile, its rules, its -settings, the list of workflows that make up its battery. A repository says +settings, the list of workflows that make up its [battery](#the-battery). A repository says which kind it is, once, in its `.github/review-policy.yml`: ```yaml @@ -35,7 +35,7 @@ kinds// | --- | --- | | `name` | The kind's name for people. | | `aliases` | Other names a caller's `profile:` may use for this kind (one line, `[a, b]`). `plugin-wp` is the wordpress-plugin kind's: the name of its profile before packs existed, still accepted. | -| `workflows` | The reusable workflows in `.github/workflows/` that make up the battery. | +| `workflows` | The reusable workflows in `.github/workflows/` that make up the [battery](#the-battery). | | `pre-pr` | What the repository's `make pre-pr` must run (#479, [`scripts/pre-pr.py`](../scripts/pre-pr.py)), one item per line: every check its pull request and its merge queue run, one after the other, stopping at the first failure, starting and stopping what it needs (a plugin's wp-env sites) and relying on nothing started before it. `make pre-pr` is the one check before a pull request in every repository, whatever its stack: `dx check` runs it before the local review, and so does the autopilot before it marks a pull request ready. A repository of no kind lists what its own covers in its `.github/review-policy.yml` (`pre-pr:`). The daily drift check lists a repository whose Makefile has no `pre-pr` target, unless `repos.yml` says it has none by design (`pre-pr: {NAME: false}`); with org.yml `pre-pr: required`, the conventions check fails its pull requests. | | `pre-pr-make` | How the `make pre-pr` of the kind's repositories runs (#479, `scripts/pre-pr.py render`): `run`, bash lines from the checkout, run as one script that stops at the first failure, and `finally`, run after it either way (the wp-env sites it started). `scripts/sync-repos.py pre-pr` writes it into the repository's `dx.mk`, a managed file its Makefile includes (`include dx.mk`, last, so a bare `make` keeps running the repository's own first target), so the organisation owns `pre-pr` and the repository's own targets stay its own: it adds its own checks as prerequisites (`pre-pr: my-check`), never in `dx.mk`. A Makefile whose own `pre-pr` has a recipe is left as it is, and the pull request says to make it a prerequisite. `DX_CENTRAL` is the organisation's checkout (`dx check` sets it; the autopilot's `.dx-central` is the default). The daily drift check lists a repository whose `dx.mk` is missing or differs. A kind with none: its repositories write their own `pre-pr`. | | `pre-pr-tools` | The commands the kind's `pre-pr-make` runs that a bare runner may not have, and the apt package of each (#682, `scripts/pre-pr.py tools`): each a block map, `command:` and `package:` (`- command: msgcat` and `package: gettext` under it; the PHP reader takes no flow maps), `[]` for none. The autopilot's `work` job (before the agent, so its own `check` runs on them) and its `verify` job (before `make pre-pr` in full) install the packages of the commands not on the runner's PATH, with `sudo apt-get install` when sudo is there; an install that fails fails the job, and `verify` then reads as `lost`, not as the agent's failure. A command `make pre-pr` still does not find makes its failure an engine failure, which starts no fix round: a maintainer adds it here (or to the runner's image) and continues the issue. | @@ -63,6 +63,10 @@ kind-settings: ignore-codes: [some_code_this_plugin_cannot_avoid] # added to the pack's list ``` +## The battery + +A kind's **battery** is the set of reusable workflows in `.github/workflows/` that check, test, release and publish a repository of that kind, the ones its pack lists under its `workflows` key: for wordpress-plugin, `plugin-checks-wp.yml`, `plugin-tests-wp.yml`, `plugin-release-wp.yml` and `plugin-publish-wp.yml`. It differs from the workflows every repository gets whatever its kind (`repos.yml` `callers: every-repository`, `issues.yml` and `pull-request-comments.yml`, and `org-pull-request.yml`, which the organisation's ruleset requires) in that only a repository of that kind gets it. A repository runs it through its callers: the files from [`workflow-templates/`](../workflow-templates/) that `scripts/sync-repos.py callers` writes into its `.github/workflows/` (`plugin-checks-wp.yml` and `release-wp.yml` for a plugin) and that name the battery's workflows, since GitHub cannot pick a reusable workflow from data ("What a pack cannot do", below). + ## How the pieces find the pack - [`scripts/policy.py`](../scripts/policy.py) reads `kind:` and fails on a diff --git a/policy/settings-schema.yml b/policy/settings-schema.yml index 9ecd507d..5ab8fe89 100644 --- a/policy/settings-schema.yml +++ b/policy/settings-schema.yml @@ -470,7 +470,7 @@ files: limit: reusable workflows of the engine read-by: [scripts/sync-repos.py] issue: "#191" - says: The workflows that make up the kind's battery. + says: The workflows that make up the kind's [battery](../kinds/README.md#the-battery). - key: own-checks org: any repo: never diff --git a/tests/workflows/test_battery_doc.py b/tests/workflows/test_battery_doc.py new file mode 100644 index 00000000..c36df1aa --- /dev/null +++ b/tests/workflows/test_battery_doc.py @@ -0,0 +1,74 @@ +"""What a battery is (#688): kinds/README.md defines it in one paragraph +under `## The battery`, and the first "battery" of each page that uses the +word links to that definition.""" +import re +from pathlib import Path + +import yaml + +ROOT = Path(__file__).resolve().parents[2] +KINDS = ROOT / "kinds" / "README.md" + + +def section(text, heading): + start = text.index(f"\n{heading}\n") + len(heading) + 2 + end = text.find("\n## ", start) + return text[start:] if end < 0 else text[start:end] + + +def definition(): + paragraphs = [p for p in section(KINDS.read_text(encoding="utf-8"), "## The battery").split("\n\n") if p.strip()] + assert len(paragraphs) == 1, paragraphs + return paragraphs[0] + + +def test_one_paragraph_says_what_a_battery_is(): + assert "**battery**" in definition() + + +def test_it_names_the_pack_key_it_comes_from(): + assert "`workflows` key" in definition() + + +def test_it_gives_the_wordpress_plugin_battery(): + pack = yaml.safe_load((ROOT / "kinds" / "wordpress-plugin" / "pack.yml").read_text(encoding="utf-8")) + for workflow in pack["workflows"]: + assert f"`{workflow}`" in definition(), workflow + + +def test_it_says_how_it_differs_from_what_every_repository_gets(): + text = definition() + assert "every-repository" in text + repos = yaml.safe_load((ROOT / "repos.yml").read_text(encoding="utf-8")) + for caller in repos["callers"]["every-repository"]: + assert f"`{caller}`" in text, caller + assert "`org-pull-request.yml`" in text + assert "only a repository of that kind" in text + + +def test_it_says_a_repository_runs_it_through_its_callers(): + text = definition() + assert "through its callers" in text + assert "workflow-templates/" in text + assert "sync-repos.py callers" in text + + +LINK = re.compile(r"\[battery\]\(\.\./kinds/README\.md#the-battery\)") + + +def test_the_heading_gives_the_anchor_the_links_use(): + assert "\n## The battery\n" in KINDS.read_text(encoding="utf-8") + + +def test_the_first_battery_of_each_page_links_to_the_definition(): + for page in ("configuration.md", "adopting.md", "maintaining.md"): + text = (ROOT / "docs" / page).read_text(encoding="utf-8") + first = re.search(r"battery", text, re.IGNORECASE) + assert first, page + link = LINK.search(text) + assert link and link.start() == first.start() - 1, page + + +def test_the_configuration_link_comes_from_the_schema(): + schema = (ROOT / "policy" / "settings-schema.yml").read_text(encoding="utf-8") + assert LINK.search(schema)