Skip to content
Open
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
2 changes: 1 addition & 1 deletion docs/adopting.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
2 changes: 1 addition & 1 deletion docs/maintaining.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
8 changes: 6 additions & 2 deletions kinds/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -35,7 +35,7 @@ kinds/<kind>/
| --- | --- |
| `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. |
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion policy/settings-schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
74 changes: 74 additions & 0 deletions tests/workflows/test_battery_doc.py
Original file line number Diff line number Diff line change
@@ -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)
Loading