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
12 changes: 12 additions & 0 deletions .github/actionlint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# This is the actionlint config.
# The paths.<glob>.ignore key filters specific error messages by regex, scoped to the file that needs it, rather than disabling the rule fleet-wide.
#
# The job.workflow_sha and job.workflow_repository properties are documented GitHub Actions context fields (GitHub Docs, "Contexts", the job context).
# They exist specifically so a reusable workflow can check out its own repository at the exact commit its caller pinned.
# The actionlint context schema has not caught up to them yet, so it reports a false property-not-defined finding here.
# Drop this entry once a released actionlint recognizes both properties.
paths:
'.github/workflows/validate-task.yml':
ignore:
- 'property "workflow_repository" is not defined in object type'
- 'property "workflow_sha" is not defined in object type'
12 changes: 12 additions & 0 deletions .github/actions/validate-default/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# The hub default for the validate hook, used when a caller carries no .github/actions/validate/action.yml of its own.
# A repo with no domain checks of its own (no ESPHome compile, no Hugo build, no KiCad ERC, no codegen drift, no PowerShell tests) has nothing for this hook to run.
# The fleet doc-lint block plus the generic unit-test job already cover everything else validate-task.yml checks.
name: Validate repository (default)
description: No-op default for the validate hook.

runs:
using: composite
steps:
- name: No repository-specific validation step
shell: bash
run: echo "no repository-specific validate hook, nothing to run"
50 changes: 50 additions & 0 deletions .github/actions/validate/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# The hub's own validate hook, run by validate-task.yml's validate job for this repo.
# The fleet doc-lint block and the generic unit-test job in validate-task.yml do not cover the hub's own machinery.
# Carrying this hook makes the hub exercise the override path on every hub pull request, where a repo with no hook of its own exercises the hub default at .github/actions/validate-default.
name: Validate repository (hub)
description: Registry and spec validation, the script self-test suite, the fleet-skills freshness check, and the unclassified-character report.

runs:
using: composite
steps:

- name: Validate registry and spec step
shell: bash
run: |
set -Eeuo pipefail
for f in registry/*.json spec/*.json repo-config/*.json; do
jq empty "$f"
done
python3 spec/validate.py

- name: Setup uv step
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
python-version: "3.13"

# Each gate here is proven by a case that reintroduces the fault it catches, wherever it lives.
# A test sits with the layer that owns it rather than beside the file it names, so a read-only gate over the spec and the host tooling lives in scripts/tests/ even where its subject is shell.
# The agent-safety kit carries its own tests because it ships as a unit.
# Every run goes through coverage with --append, so one report covers the whole self-test surface, informational with no threshold adopted.
- name: Run script self-tests step
shell: bash
run: |
set -Eeuo pipefail
uvx coverage@latest run --source=scripts,spec,host-setup -m unittest discover -s scripts/tests
uvx coverage@latest run --source=scripts,spec,host-setup --append spec/audit.py --selftest
uvx coverage@latest run --source=scripts,spec,host-setup --append spec/workflow_reuse.py --selftest
uvx coverage@latest run --source=scripts,spec,host-setup --append host-setup/agent-safety/gh-write-guard.py --selftest
uvx coverage@latest run --source=scripts,spec,host-setup --append host-setup/agent-safety/test_install.py
uvx coverage@latest report

# Read-only: fails if .claude-plugin/fleet-skills/ was not regenerated from .agents/skills/.
- name: Check fleet skills are current step
shell: bash
run: python3 scripts/build_dist.py --check

# Warn-only, and visible rather than absent: an unrun check is one nobody acts on.
# A finding here names a character no tier covers, and classifying it is a fleet-law edit rather than a prose fix.
- name: Report unclassified characters step
shell: bash
continue-on-error: true
run: python3 scripts/prose_lint.py . --check charset-unknown --summary
286 changes: 214 additions & 72 deletions .github/workflows/validate-task.yml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion GOVERNANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ CI runs the full lint set, but run the linters locally before pushing to catch i

**Each surface runs the lint with the tool that fits it, all from the same config files** (`.markdownlint-cli2.jsonc`, `cspell.json`, `.editorconfig`):

- **CI (authoritative)** runs **markdownlint-cli2**, **cspell**, and **actionlint** as pinned action wrappers (Dependabot bumps them), plus **editorconfig-checker** via Docker `:latest` (its action only installs the CLI, so the Docker one-liner is what actually runs the check), and, **for a repo that carries `.ps1` files**, **PSScriptAnalyzer** the same way (it has no action either). markdownlint covers all `**/*.md`, and **cspell is scoped to `README.md` + `HISTORY.md`** (see [CODESTYLE.md](./CODESTYLE.md) "Markdown and Spelling" for why), matching the cspell one-liner below.
- **CI (authoritative)** runs **markdownlint-cli2**, **cspell**, and **actionlint** as pinned action wrappers (Dependabot bumps them), plus **editorconfig-checker** via Docker `:latest` (its action only installs the CLI, so the Docker one-liner is what actually runs the check), **shellcheck** the same way for a repo that carries `.sh` files, and, **for a repo that carries `.ps1` files**, **PSScriptAnalyzer** the same way (neither one has an action). markdownlint covers all `**/*.md`, and **cspell is scoped to `README.md` + `HISTORY.md`** (see [CODESTYLE.md](./CODESTYLE.md) "Markdown and Spelling" for why), matching the cspell one-liner below. This whole block is the hub's `validate-task.yml` reusable workflow, per [`docs/reusable-workflows.md`](./docs/reusable-workflows.md), so a fleet repo reaches it rather than carrying a copy of these steps.
- **The `.husky/pre-commit` hook** runs **language formatting** and the **diff-scoped doc gates**, never Docker and never a network call, so it stays fast. The formatting half is whatever the repo's own language needs, CSharpier and `dotnet format` for .NET or ruff for Python, via native tooling. A repo adds each half once its tree passes that half, since a gate that fails on the corpus it guards blocks every commit from the moment it lands, so a hook running one half is a repo mid-convergence rather than a repo out of conformance. The doc half runs each gate at the scope that fits it. The prose gate is scoped to what the commit changes rather than swept over the tree, which is the difference between about 2.2 seconds and about 0.13 and is what makes it affordable in a hook at all. A whole-repo check belongs there too when it is already fast and takes no file list, which the line-ending consistency check is, so scope is a property of the gate rather than a rule the hook applies to all of them. `repo_gate.py --check sha-pin` stays out, since it resolves a same-owner pin against the GitHub API and a hook that needs a network fails offline. A repo enables the hook per clone with `git config core.hooksPath .husky`, and CI remains the authoritative run either way.
- **The VS Code Lint tasks** run the full doc-lint set via Docker `:latest` on demand, the local surface for Markdown, spelling, workflow, and EditorConfig checks.

Expand Down
9 changes: 5 additions & 4 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,14 +239,15 @@ One pull request per stage moving a standard workflow out of every repo and into

**State** `ready` for the gates, `blocked` on the gates for everything after. **Touches** the hub's `.github/workflows/`, [`spec/files.json`][files], [`catalog/snippets/workflows/`][workflows], and [`WORKFLOW.md`][workflow] where a guarantee names a copied job. **Cost** one hub edit per stage plus an adoption per repo on its next visit, and no re-vendor beyond the stub each stage introduces.

- **Host the gates: `validate-task.yml` with a `validate` hook, and `test-pull-request-task.yml` with the fixed aggregator.** The hub owns the per-type doc-lint block once, the hook carries a repo's own tests, and the stub carries the trigger shape, operational or release. This stage is where the hook fallback is first proven live, on the hub for the default and on a pilot for the override.
- **Host the gates: `validate-task.yml` with a `validate` hook.** The hub owns the fleet doc-lint block, the language lint by tree detection, the prose gate and the repo gate in a lint job, a generic unit-test job, and the validate hook for a repo's own domain checks. There is no `test-pull-request-task.yml`: the stub shapes carrying the trigger, operational or release, live in [`docs/reusable-workflows.md`][reusable-workflows-doc] "Adopting the Gates" instead.
- **Blocked by** - Nothing.
- **Issue** - None filed. [#585][issue-585] and [#729][issue-729] are settled inside this stage, the first by the operational stub's trigger and the second by the one place the hub validate task pins or floats its `uvx` tools.
- **Issue** - None filed. [#585][issue-585] and [#729][issue-729] are settled by design in this stage, the first by the stub's trigger shape and the second by the one place the hub validate task pins or floats its `uvx` tools.
- **Checked** - `develop` at `7c67328` on 2026-08-15, where the report counts 20 copies of `test-pull-request.yml` in 13 variants and 13 copies of `validate-task.yml` in 11, and the doc-lint block (markdownlint, cspell, actionlint, editorconfig-checker) repeats in every one.
- **Open** - Whether the per-type lint steps are selected by an input the stub sets or read from the repo's registry entry through a hub checkout at `github.job_workflow_sha`, since the second needs no per-repo input and the first needs no network read.
- **Open** - Whether a `validate` hook that runs a domain compile (an ESPHome build, a KiCad ERC) is one hook or several, given the two repos carrying such a step run it as a separate job today.
- **Settled** - Pilots are PhotoCleaner, which piloted the merge-bot stub in ptr727/PhotoCleaner#53 on 2026-08-15 as a release-model repo with Dependabot, C#, executable and Docker targets, then HomeAutomation-Config for the operational trigger shape, so both shapes are exercised before the sweep.
- **Settled** - The step gated on `hashFiles('.github/actions/validate/action.yml') != ''` runs the caller's hook from its own checkout, else the default from a hub checkout under `.hub/`, and a local composite action resolves at step time from the workspace, which is what makes the fallback expressible at all.
- **Settled** - The per-type lint steps are selected by tree detection (`hashFiles` against a `*.csproj` or a `pyproject.toml`), a third option needing neither a per-repo input nor a network read.
- **Settled** - The `validate` hook is one hook, not several. A domain compile, a Hugo build, a KiCad ERC, a codegen-drift check, and PowerShell tests are each a repo's own business behind the same hook, and a repo needing more than one check composes them inside its own composite action.
- **Settled** - `test-pull-request-task.yml` hosts nothing generic. The ruleset-bound aggregator stays in the caller stub by design (a called job's check context would read `<caller job> / <callee job>` and break the ruleset binding), so the only job left for a second hub task to wrap is one line calling `validate-task.yml`, which a caller stub already writes for itself.

- **Host the pure functions: `get-version-task.yml` and `publish-plan-task.yml`.** Neither has a repo-specific line, and the plan job is missing where D4.1 needs it.
- **Blocked by** - The gates, only for sequencing, since a repo adopts one stub per visit and the gates come first.
Expand Down
Loading