Skip to content

Check Composite Action Metadata Against the GitHub Action Schema - #1686

Merged
ptr727 merged 3 commits into
developfrom
feature/1155-lint-composite-actions
Sep 18, 2026
Merged

ptr727 merged 3 commits into
developfrom
feature/1155-lint-composite-actions

Conversation

@ptr727

@ptr727 ptr727 commented Sep 18, 2026

Copy link
Copy Markdown
Owner

Check Composite Action Metadata Against the GitHub Action Schema

Implements item 4 of the answers recorded on
#1155. Items 1
to 3 are prose and land separately.

The gap

workflow-ci-contract "After Any Workflow Edit" sent a reader to
actionlint. actionlint's own file list is .github/workflows/*.yml and
*.yaml, and it opens a composite action's action.yml only through a
workflow's uses: ./.github/actions/<name>, and only to judge that
caller's with: block against the action's declared inputs. Nothing
read the action file as action metadata. So a reader edited an
action.yml, ran the prescribed check, and got a clean result that had
judged only the caller's contract with it.

Why actionlint cannot be the tool, measured rather than inferred

docker run --rm -v "$PWD":/repo -w /repo rhysd/actionlint:latest .github/actions/validate/action.yml exits 1 with six syntax-check
errors, "jobs" section is missing in workflow, "on" section is missing in workflow, and unexpected key for each of description,
inputs, outputs and runs. It parses action metadata as a workflow,
so widening its globs is not available.

Measured on a scratch repository, so the corrected skill text claims no
more than what actionlint does: a composite action carrying a malformed
if: expression and an unterminated shell if in a run: block, plus
a second action referenced by no workflow and missing both its top-level
description and an input's, passes actionlint at exit 0. The same two
files put to check-jsonschema report the two missing descriptions.

What runs instead

check-jsonschema's vendor.github-actions builtin, which is GitHub's
own action-metadata schema. It runs via uvx, beside the ruff and mypy
invocations already in the same job, because check-jsonschema
publishes no container image: upstream python-jsonschema carries no
Dockerfile and Docker Hub holds only unaffiliated rebuilds. That is why
scripts/docker_lint.py, which the recorded answer named, is unchanged:
it runs each linter in a container with --network none, so neither a
pulled image nor an in-container install is available to it.

  • validate-task.yml gains a Lint composite actions step, gated on
    the metadata existing, taking its file list from git and skipping
    rather than invoking on an empty argument list, matching the shell and
    PowerShell steps above it. It sits after the Python steps rather than
    beside actionlint because it needs the uv they set up.
  • Setup uv step now also fires for a repo that carries composite
    actions and no pyproject.toml, which would otherwise reach the new
    step with no uvx on the runner.
  • Both the guard and the file list use **, the guard through
    hashFiles and the list through a :(glob) pathspec, so the two
    describe one set. hashFiles's * does not cross a / and git's
    default pathspec * does, so the earlier * form would have skipped
    a repo whose actions nest one level deeper while still checking a
    nested file whenever a flat one existed.
  • OPERATIONS.md "Local Verification" runs the same command, with the
    same empty guard. Without it, a checkout carrying no composite action
    reaches check-jsonschema with no instance files, which exits 2 and
    aborts the rest of the block under its own set -Eeuo pipefail.
  • GOVERNANCE.md "Running the Linters Locally" names the new check,
    since that block states it is the whole of validate-task.yml, and
    both host READMEs name it in their own enumeration of the lint tools
    that run through a container or uvx rather than natively. That
    section's own lead names it too, since it tells an agent to trust that
    enumeration before reporting a check unavailable.

What the corrected skill text says

"After Any Workflow Edit" now says which part of a composite action each
check reaches, and what neither reads: a composite action's run:
bodies and its if: expressions. It also says that where no workflow in
the repository names the action, actionlint reaches it not at all, which
is the ordinary shape downstream: a repo's hooks are invoked from a hub
reusable workflow, and no workflow in that repo carries a
uses: ./.github/actions/... line, so its own actionlint run never
opens them. docs/reusable-workflows.md stated the reason wrongly in a
snippet comment and is corrected to that one.

The schema check's scope is stated as .github/actions/, matching the
step's own guard, since an action file outside that tree is read by
neither check.

The producer-failure shape this repository already ruled against

The step's file list is written to a file rather than into a process
substitution. .github/workflows/canonical-review-sweep.yml states the
reason in this tree already: mapfile reports success whatever the
producer did. Measured both forms against a failing git ls-files: the
redirect aborts the step at 128, and mapfile < <(...) continues with
an empty array and exits 0, which would have reported "no composite
action metadata is tracked" and gone green with the gate never run.

Dropping mapfile also keeps the OPERATIONS.md block runnable on
macOS, whose /bin/bash is 3.2 and has no such builtin. That block
opens with set -Eeuo pipefail, so the failure would not have skipped
one check, it would have truncated every gate below it.

The list is git ls-files -z into xargs -0, since a bare xargs
splits on whitespace and honours quotes. Measured: a tracked
.github/actions/my hook/action.yml reaches the checker as two
arguments and the run exits 123, where -0 passes it whole and exits 0.
-0 is the form both GNU and BSD carry, where -d is GNU-only and
would break the macOS copy. The pathspecs are :(top,glob) so a run
from a subdirectory reads the same nine files rather than reporting a
silent clean over none.

The eight findings the new gate raised on its first run

A gate that fails on the corpus it guards blocks every commit from the
moment it lands, per GOVERNANCE.md, so the tree is brought to green in
the same change. Eight inputs carried no description, which GitHub's
schema requires: semver2 in docker-prepare-default and
nuget-build-default, and assembly-version,
assembly-file-version and assembly-informational-version in both
dotnet-publish-default and nuget-build-default. Each now describes
the NBGV output it threads and the MSBuild property it sets.
dotnet-publish-default passes its assembly-version as both
AssemblyVersion and Version, and its semver2 reaches
PackageVersion alone, so those two say so rather than reading like
their nuget-build-default counterparts, where semver2 is Version.

Verification

The declared local gate set in OPERATIONS.md "Local Verification"
passes whole, and docker_lint.py is clean for actionlint,
markdownlint and editorconfig-checker. The new command was run at the
exact form the workflow and the local block use, over all nine tracked
action files, and its empty-list path was run in a scratch repository
carrying none. workflow-ci-contract regenerates into all three
distributions.

Local review

Three adversarial passes, 22 findings, every one dispositioned. The first pass disproved this change's own central claim: the draft said actionlint reads no action file at all, and it does, opening a local action through a workflow's uses: ./<path> to judge that caller's with: block. Correcting the premise did not invalidate the gate, since nothing before this reached the action file's own metadata, but the justification was rewritten around what actionlint actually does.

Each later pass found a defect in the previous pass's fix. Round 2 replaced mapfile to stop a producer failure reading as an empty list, and introduced an xargs quoting regression in doing so. Round 3 settled both with git ls-files -z into xargs -0.

Fleet blast radius

Every action.yml the fleet carries outside this repository was fetched and checked against the same builtin: LanguageTags, aiopurpleair, Financial-Modeling, HomeAutomation-Config, ESPHome-Config and Blog all pass, so no repository reds on its next pin bump. Blog and HomeAutomation-Config carry composite actions and no pyproject.toml, so both newly run setup-uv and one uvx resolve in their lint job once they bump. That cost is intended rather than incidental.

Known stale, deliberately untouched

validate-task.yml's file header and docs/reusable-workflows.md both enumerate the lint job's checks and neither names this one. Both are comment lines, which prose_lint.py's comment-added rule refuses on a change that edits them unless the pull request says comments are wanted. Left for that decision rather than edited here.

🤖 Generated with Claude Code

Implements item 4 of the answers recorded on
[#1155](#1155). Items 1
to 3 are prose and land separately.

## The gap

`workflow-ci-contract` "After Any Workflow Edit" sent a reader to
actionlint. actionlint's own file list is `.github/workflows/*.yml` and
`*.yaml`, and it opens a composite action's `action.yml` only through a
workflow's `uses: ./.github/actions/<name>`, and only to judge that
caller's `with:` block against the action's declared inputs. Nothing
read the action file as action metadata. So a reader edited an
`action.yml`, ran the prescribed check, and got a clean result that had
judged only the caller's contract with it.

## Why actionlint cannot be the tool, measured rather than inferred

`docker run --rm -v "$PWD":/repo -w /repo rhysd/actionlint:latest
.github/actions/validate/action.yml` exits 1 with six syntax-check
errors, `"jobs" section is missing in workflow`, `"on" section is
missing in workflow`, and `unexpected key` for each of `description`,
`inputs`, `outputs` and `runs`. It parses action metadata as a workflow,
so widening its globs is not available.

Measured on a scratch repository, so the corrected skill text claims no
more than what actionlint does: a composite action carrying a malformed
`if:` expression and an unterminated shell `if` in a `run:` block, plus
a second action referenced by no workflow and missing both its top-level
`description` and an input's, passes actionlint at exit 0. The same two
files put to `check-jsonschema` report the two missing descriptions.

## What runs instead

`check-jsonschema`'s `vendor.github-actions` builtin, which is GitHub's
own action-metadata schema. It runs via `uvx`, beside the ruff and mypy
invocations already in the same job, because `check-jsonschema`
publishes no container image: upstream `python-jsonschema` carries no
Dockerfile and Docker Hub holds only unaffiliated rebuilds. That is why
`scripts/docker_lint.py`, which the recorded answer named, is unchanged:
it runs each linter in a container with `--network none`, so neither a
pulled image nor an in-container install is available to it.

- `validate-task.yml` gains a `Lint composite actions step`, gated on
  the metadata existing, taking its file list from git and skipping
  rather than invoking on an empty argument list, matching the shell and
  PowerShell steps above it. It sits after the Python steps rather than
  beside actionlint because it needs the uv they set up.
- `Setup uv step` now also fires for a repo that carries composite
  actions and no `pyproject.toml`, which would otherwise reach the new
  step with no `uvx` on the runner.
- Both the guard and the file list use `**`, the guard through
  `hashFiles` and the list through a `:(glob)` pathspec, so the two
  describe one set. `hashFiles`'s `*` does not cross a `/` and git's
  default pathspec `*` does, so the earlier `*` form would have skipped
  a repo whose actions nest one level deeper while still checking a
  nested file whenever a flat one existed.
- `OPERATIONS.md` "Local Verification" runs the same command, with the
  same empty guard. Without it, a checkout carrying no composite action
  reaches `check-jsonschema` with no instance files, which exits 2 and
  aborts the rest of the block under its own `set -Eeuo pipefail`.
- `GOVERNANCE.md` "Running the Linters Locally" names the new check,
  since that block states it is the whole of `validate-task.yml`, and
  both host READMEs name it in their own enumeration of the lint tools
  that run through a container or `uvx` rather than natively. That
  section's own lead names it too, since it tells an agent to trust that
  enumeration before reporting a check unavailable.

## What the corrected skill text says

"After Any Workflow Edit" now says which part of a composite action each
check reaches, and what neither reads: a composite action's `run:`
bodies and its `if:` expressions. It also says that where no workflow in
the repository names the action, actionlint reaches it not at all, which
is the ordinary shape downstream: a repo's hooks are invoked from a hub
reusable workflow, and no workflow in that repo carries a
`uses: ./.github/actions/...` line, so its own actionlint run never
opens them. `docs/reusable-workflows.md` stated the reason wrongly in a
snippet comment and is corrected to that one.

The schema check's scope is stated as `.github/actions/`, matching the
step's own guard, since an action file outside that tree is read by
neither check.

## The producer-failure shape this repository already ruled against

The step's file list is written to a file rather than into a process
substitution. `.github/workflows/canonical-review-sweep.yml` states the
reason in this tree already: `mapfile` reports success whatever the
producer did. Measured both forms against a failing `git ls-files`: the
redirect aborts the step at 128, and `mapfile < <(...)` continues with
an empty array and exits 0, which would have reported "no composite
action metadata is tracked" and gone green with the gate never run.

Dropping `mapfile` also keeps the `OPERATIONS.md` block runnable on
macOS, whose `/bin/bash` is 3.2 and has no such builtin. That block
opens with `set -Eeuo pipefail`, so the failure would not have skipped
one check, it would have truncated every gate below it.

The list is `git ls-files -z` into `xargs -0`, since a bare `xargs`
splits on whitespace and honours quotes. Measured: a tracked
`.github/actions/my hook/action.yml` reaches the checker as two
arguments and the run exits 123, where `-0` passes it whole and exits 0.
`-0` is the form both GNU and BSD carry, where `-d` is GNU-only and
would break the macOS copy. The pathspecs are `:(top,glob)` so a run
from a subdirectory reads the same nine files rather than reporting a
silent clean over none.

## The eight findings the new gate raised on its first run

A gate that fails on the corpus it guards blocks every commit from the
moment it lands, per `GOVERNANCE.md`, so the tree is brought to green in
the same change. Eight inputs carried no `description`, which GitHub's
schema requires: `semver2` in `docker-prepare-default` and
`nuget-build-default`, and `assembly-version`,
`assembly-file-version` and `assembly-informational-version` in both
`dotnet-publish-default` and `nuget-build-default`. Each now describes
the NBGV output it threads and the MSBuild property it sets.
`dotnet-publish-default` passes its `assembly-version` as both
`AssemblyVersion` and `Version`, and its `semver2` reaches
`PackageVersion` alone, so those two say so rather than reading like
their `nuget-build-default` counterparts, where `semver2` is `Version`.

## Verification

The declared local gate set in `OPERATIONS.md` "Local Verification"
passes whole, and `docker_lint.py` is clean for actionlint,
markdownlint and editorconfig-checker. The new command was run at the
exact form the workflow and the local block use, over all nine tracked
action files, and its empty-list path was run in a scratch repository
carrying none. `workflow-ci-contract` regenerates into all three
distributions.
Copilot AI lite review requested due to automatic review settings September 18, 2026 18:49
@coderabbitai

coderabbitai Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 59 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository: ptr727/ProjectTemplate/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 950f428d-c8db-410a-8457-d32249b4fb85

📥 Commits

Reviewing files that changed from the base of the PR and between 5de7af3 and fa40979.

⛔ Files ignored due to path filters (3)
  • .claude-plugin/fleet-skills/.source-digests/workflow-ci-contract is excluded by !.claude-plugin/fleet-skills/**
  • .claude-plugin/fleet-skills/skills/workflow-ci-contract/SKILL.md is excluded by !.claude-plugin/fleet-skills/**
  • .github/skills/workflow-ci-contract/SKILL.md is excluded by !.github/skills/**
📒 Files selected for processing (10)
  • .agents/skills/workflow-ci-contract/SKILL.md
  • .github/actions/docker-prepare-default/action.yml
  • .github/actions/dotnet-publish-default/action.yml
  • .github/actions/nuget-build-default/action.yml
  • .github/workflows/validate-task.yml
  • GOVERNANCE.md
  • OPERATIONS.md
  • docs/reusable-workflows.md
  • host-setup/linux/README.md
  • host-setup/windows/README.md

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The updated workflow-ci-contract skill text contains an incorrect actionlint file-glob description that should be corrected to avoid misleading local verification guidance.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

This PR closes the CI gap where composite action metadata (.github/actions/**/action.{yml,yaml}) was not being validated as action metadata by adding a check-jsonschema schema check (via uvx) and updating docs/skill text to reflect the new verification surface.

Changes:

  • Add a uvx check-jsonschema --builtin-schema vendor.github-actions gate for tracked composite action metadata in validate-task.yml, plus a matching local invocation in OPERATIONS.md.
  • Expand setup-uv gating so repos without pyproject.toml still get uvx when they carry composite actions.
  • Fix existing composite action metadata failures by adding missing inputs.*.description fields, and update documentation/skill distributions accordingly.
File summaries
File Description
OPERATIONS.md Adds a local check-jsonschema invocation guarded on a non-empty tracked action list.
host-setup/windows/README.md Updates host-tooling docs to include check-jsonschema in the “not installed natively” list.
host-setup/linux/README.md Updates host-tooling docs to include check-jsonschema in the “not installed natively” list.
GOVERNANCE.md Documents check-jsonschema as part of the authoritative lint set for repos carrying .github/actions/.
docs/reusable-workflows.md Updates a comment describing when .github/actions/** should be included in target path filters.
.github/workflows/validate-task.yml Adds a composite action metadata schema-check step; ensures setup-uv runs when composite actions exist.
.github/skills/workflow-ci-contract/SKILL.md Regenerated distributed skill copy reflecting the updated workflow guidance.
.github/actions/nuget-build-default/action.yml Adds required input descriptions to satisfy GitHub’s action metadata schema.
.github/actions/dotnet-publish-default/action.yml Adds/clarifies required input descriptions to satisfy GitHub’s action metadata schema.
.github/actions/docker-prepare-default/action.yml Adds required input description to satisfy GitHub’s action metadata schema.
.claude-plugin/fleet-skills/skills/workflow-ci-contract/SKILL.md Regenerated distributed skill copy reflecting the updated workflow guidance.
.claude-plugin/fleet-skills/.source-digests/workflow-ci-contract Updates the distribution digest for the regenerated workflow-ci-contract skill.
.agents/skills/workflow-ci-contract/SKILL.md Updates the source skill text describing what actionlint does vs schema validation.
Review details
  • Files reviewed: 13/13 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Answers the review round on
[#1686](#1686). Every
claim below was measured against `rhysd/actionlint:latest` in a scratch
repository rather than reasoned about, because this paragraph has now
been wrong four rounds running.

## Three claims were false, one of them as the reviewer asked for it

**Discovery is recursive.** The reviewer asked for
`.github/workflows/*.yml` and `.github/workflows/*.yaml`, and `*` does
not cross a `/`. A file at `.github/workflows/sub/nested.yml` is linted,
so writing the reviewer's form would have put a fresh false statement
into carried content. The text now says actionlint discovers
`.github/workflows/` recursively, both extensions.

**It does validate a referenced action's metadata.** Measured against a
local action reached through `uses: ./.github/actions/hook`:
`description is required in metadata of "H" action`. It checks `name`,
the top-level `description`, and `runs.using`, reporting each against
the calling workflow. "It never validates the action file as action
metadata" was false.

**It reads more of the caller than `with:`.** `${{
steps.h.outputs.notdeclared }}` draws `property "notdeclared" is not
defined in object type {realout: string}`, so it types the caller's
`steps.<id>.outputs.<name>` against the action's declared outputs too.

What actionlint does not reach that way is a missing per-input
`description` and an unexpected top-level key, which is what leaves the
schema check something to do. The text says that rather than implying a
clean split.

## The schema is not GitHub's

`vendor.github-actions` is SchemaStore's vendored schema rather than a
GitHub-published one, and the two diverge. `runs.using: node16` passes
the schema and actionlint rejects it: `invalid runner name "node16" ...
valid runners are "composite", "docker", "node20", and "node24"`. So the
text no longer calls it GitHub's schema, since a green run there is not
a statement about what GitHub accepts.

## Two `GOVERNANCE.md` corrections

The CI bullet said "for a repo that carries a **composite** action". The
guard is `hashFiles('.github/actions/**/action.yml', ...)` and the file
list is the same two patterns, so any action metadata under that tree
fires it whatever `runs.using` declares.

`check-jsonschema` comes back out of the "not installed natively"
sentence. That sentence sends a reader to this section for the working
invocation and the section states none for it, so naming it there
promised an answer that is not present. The CI bullet still names it and
gives the `uvx` form.

## Why this is a second commit

The previous commit is already pushed, and `GOVERNANCE.md` "Git and
Commit Rules" says never force push, so refining it by amend is not
available. The branch is append-only from its first push.
Copilot AI review requested due to automatic review settings September 18, 2026 20:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The updated canonical skill text contains a repository-inaccurate claim about actionlint workflow discovery and an unnecessary/contradictory provenance assertion about the schema source that should be corrected before merge.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (1)

.agents/skills/workflow-ci-contract/SKILL.md:33

  • The skill claims actionlint discovers .github/workflows/ recursively, but this repository's actionlint invocation targets only .github/workflows/*.yml and .github/workflows/*.yaml (non-recursive), so the guidance here is misleading for nested workflow paths.
**actionlint discovers `.github/workflows/` recursively, both extensions, and opens no action file on its own.** It reaches a local action's `action.yml` only through a workflow's `uses: ./<path>`, wherever in the tree that path leads, and reports everything it finds there against the calling workflow: the caller's `with:` block against the action's declared inputs, the caller's `steps.<id>.outputs.<name>` against its declared outputs, and the action's own `name`, top-level `description` and `runs.using`. A missing per-input `description` and an unexpected top-level key are the metadata it does not reach that way. Pointing it at an action file directly makes it parse the file as a workflow and report several syntax-check errors, so widening its file list is not available. **Where no workflow in the repository names the action, actionlint reaches it not at all**, which is the ordinary shape for a repository whose hooks are invoked from a hub reusable workflow rather than from a workflow of its own.
  • Files reviewed: 13/13 changed files
  • Comments generated: 1
  • Review effort level: Lite

@ptr727

ptr727 commented Sep 18, 2026

Copy link
Copy Markdown
Owner Author

Answering the suppressed comment on .agents/skills/workflow-ci-contract/SKILL.md:33, which opens no thread, so it goes here:

The skill claims actionlint discovers .github/workflows/ recursively, but this repository's actionlint invocation targets only .github/workflows/*.yml and .github/workflows/*.yaml (non-recursive), so the guidance here is misleading for nested workflow paths.

Declining, with a measurement rather than an argument. Both halves are testable and both come out the other way.

1. actionlint's own discovery is recursive. With no file arguments, in a scratch git repository holding .github/workflows/sub/nested.yml:

$ docker run --rm -v "$PWD":/repo -w /repo rhysd/actionlint:latest
.github/workflows/sub/nested.yml:1:1: "jobs" section is missing in workflow [syntax-check]

The nested workflow is linted. That is also the form CI runs, since the raven-actions/actionlint step in validate-task.yml passes no file list and so uses this default.

2. This repository's own invocation is recursive too. scripts/docker_lint.py's actionlint entry does declare those two patterns, but it passes them to git ls-files -- as a pathspec, not to a shell. Git's default pathspec magic is wildmatch without WM_PATHNAME, so * matches /. Measured on a scratch repository holding one flat and one nested workflow:

$ git ls-files -- '.github/workflows/*.yml' '.github/workflows/*.yaml'
.github/workflows/flat.yml
.github/workflows/sub/nested.yml

So the declared patterns do select nested workflows, and the local runner agrees with CI rather than diverging from it.

The general point worth keeping, because it has now come up twice in this review: a git pathspec is not a shell glob and not minimatch. It is load-bearing in this pull request's own new step, which is why that file list is spelled :(top,glob).github/actions/**/action.yml. The :(glob) prefix opts into WM_PATHNAME, where * then stops crossing / and ** becomes what spans directories, the opposite behaviour to the bare pathspec above from the same tool. The two are spelled differently on purpose.

No change made for this one. If a nested workflow can be produced that either invocation misses, I will take the finding.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The CI gating change is properly guarded, consistent with the documented local invocation, and the updated action metadata fixes bring the tree back to green under the new schema check.

Review details
  • Files reviewed: 13/13 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Answers Copilot's second round on
[#1686](#1686).

The sentence asserted where the builtin schema comes from, "SchemaStore's
vendored schema rather than a GitHub-published one". The origin is not
what a reader of this skill needs, and it is a claim that has to be kept
true as upstream moves.

What a reader needs is the behaviour, which is measured rather than
asserted: the schema accepts `runs.using: node16` and actionlint rejects
it with `invalid runner name "node16" ... valid runners are "composite",
"docker", "node20", and "node24"`, so a green schema run is not a
statement about what GitHub currently accepts. That is what the sentence
now says, and nothing about provenance.

The round's other finding, that this repository's actionlint invocation
is non-recursive, is declined on the pull request rather than here,
since it opened no thread. `scripts/docker_lint.py` passes its patterns
to `git ls-files` as a pathspec rather than to a shell, and git's default
pathspec magic is wildmatch without `WM_PATHNAME`, so `*` matches `/` and
`git ls-files -- '.github/workflows/*.yml'` returns a nested workflow.
Copilot AI review requested due to automatic review settings September 18, 2026 21:35
@ptr727

ptr727 commented Sep 18, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 18, 2026 •

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The added CI and local gates correctly validate composite action metadata with appropriate guards, and the PR fixes the existing schema violations it would newly enforce.

Review details
  • Files reviewed: 13/13 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@ptr727

ptr727 commented Sep 18, 2026

Copy link
Copy Markdown
Owner Author

Recording an execution boundary on this pull request's review coverage, rather than leaving it to be inferred from a green check.

CodeRabbit did not review this pull request. Its check reports pass, and that status is not a review: its own reply to the explicit @coderabbitai review trigger reads

⚠️ Action not completed. Review rate limited.

and it has posted zero review comments here. This repository also receives no automatic CodeRabbit review at all, having fewer than ten stars, so the explicit trigger was the only route and it was refused on quota.

What has read this change, so the coverage claim is exact rather than implied:

  • GitHub Copilot, three rounds, the last on fa40979 at 13 of 13 changed files, every thread replied and resolved, and the one suppressed finding answered in this conversation with a re-runnable measurement.
  • Four local adversarial passes, one per head, each recorded through scripts/local_review.py against the content digest it actually read.
  • The declared local gate set in OPERATIONS.md "Local Verification", passing whole on every head, plus docker_lint.py for actionlint, markdownlint and editorconfig-checker, and actionlint over the modified workflow.
  • CI, 8 of 8 checks.

The maintainer's call was to proceed on that coverage rather than hold the change for a quota reset. Stated here so a later reader does not mistake CodeRabbit's pass for a second reviewer having read this diff.

@coderabbitai

coderabbitai Bot commented Sep 18, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@ptr727, thanks for documenting the boundary.

The pass check does not indicate that CodeRabbit completed a review on this pull request. The explicit review request was rate limited. The stated local reviews, GitHub Copilot reviews, local verification gates, and CI checks are separate coverage sources.


✏️ Learnings added
Learnt from: ptr727
URL: https://github.com/ptr727/ProjectTemplate/pull/1686

Timestamp: 2026-09-18T21:54:24.853Z
Learning: For GitHub pull request `#1686` in ptr727/ProjectTemplate, a passing CodeRabbit check does not mean CodeRabbit reviewed the pull request. The explicit `coderabbitai review` request was rate limited and produced no CodeRabbit review comments. The maintainer documented separate coverage from GitHub Copilot, local adversarial reviews, local verification gates, and CI.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

@ptr727
ptr727 merged commit d0e722f into develop Sep 18, 2026
9 checks passed
@ptr727
ptr727 deleted the feature/1155-lint-composite-actions branch September 18, 2026 21:55
ptr727 added a commit that referenced this pull request Sep 18, 2026
…Check (#1687)

Promotes the two changes that implement
[#1155](#1155
recorded answers, items 1 to 3 as prose and item 4 as tooling.

## The delta

Two squash commits, 21 files, read from `git diff origin/main
origin/develop` rather than from a local fast-forward's file list:

- `e18730f`
[#1685](#1685), the
carried-text defects.
- `d0e722f`
[#1686](#1686), the
composite-action schema check.

## What changes for a carrier

**`repo-worktree`** no longer forbids in one unit the command it
requires in another. "The Base Branch" names the base-clone return as a
second exception beside the cleanup step, with the ban rescoped to
commands that mutate the working tree, index or HEAD, so `worktree
remove` and `branch -d` sit outside it rather than as uncounted
exceptions. "Creating a Worktree" splits the continuation by which
checkout holds the branch: the base clone is returned to `develop` when
its tree is clean and stopped on when it is not, while a previous
session's worktree is retired rather than switched, since it normally
occupies the very path `worktree add` wants.

**`resync-a-repo`** carries both audit invocations itself rather than
pointing at `RESYNC.md` section 2 for them, so the skill works read in
isolation. `python3 spec/audit.py <RepoName>` measures the ground-truth
branch the registry names, and `--branch <ref>` previews in-flight work.
The invocation is spelled with `python3` at all four sites, since a
shebang does not execute on Windows and this fleet declares that
platform first-class.

**`GOVERNANCE.md` "Repository Layout"** gains rows for
`STANDUP.md`/`RESYNC.md`, `.agents/`, `host-setup/`, `.github/skills/`
and `.claude-plugin/fleet-skills/`, and no longer describes this
repository's CI as lint-only or its `.github/workflows/` as holding only
its own workflows.

**A new CI gate** validates every tracked `action.yml` under
`.github/actions/` against the action-metadata schema, which actionlint
cannot do because it parses an action file as a workflow. It found eight
real defects on its first run, all fixed in the same change.
`workflow-ci-contract` now states what each check reads and what neither
reads, all of it measured rather than asserted.

## Review coverage

Both pull requests reached every Merge Gate item before merging: review
on head, full coverage, no refusal, zero unresolved threads and zero
suppressed findings on head, checks 8 of 8.

One boundary, already recorded on
[#1686](#1686) and
repeated here so it is not inferred from a green check: **CodeRabbit did
not review that pull request.** Its check reports `pass` while its own
reply to the explicit trigger reads "Review rate limited", and it posted
zero comments there. It did review
[#1685](#1685). Proceeding
on Copilot plus the local passes there was the maintainer's call rather
than an inference from the check.

## No closing references

None of the issues this work touches is finished by it, so this pull
request closes nothing.

- **#1155** keeps items 5 to 10. Item 5 has a recorded answer and no
implementation, and items 6 to 10 have no answer.
- **[#1684](#1684
keeps `README.md`'s copy of the lint-only claim and the question of
whether the prose term or the registry classification is the defect.
- **[#1682](#1682 is
unimplemented, though this round's own cleanup ran its recommended fix
by hand and it worked: the remote branch delete ran from each task's
worktree before that worktree was removed, which the guard permits where
the base clone would have denied it.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants