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
4 changes: 2 additions & 2 deletions .agents/skills/skill-lifecycle/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ A skill surfaces at a trigger moment. A rule that binds every action all the tim
Packaging keeps one topic in one authoritative place while the skill makes it surface automatically. It has three shapes, and each pairing states which it uses:

- **Moved content.** The law-doc section keeps a summary and the skill holds the full rules (`git-commit-conventions`, `comment-and-doc-style`, `pr-review-conduct`). The section ends with the standard pointer sentence: packaged as the named skill at `.agents/skills/<name>/SKILL.md` in the hub, not a repo-relative link since that path is hub-local and not carried into every fleet repo, read the skill for the full rules.
- **Kept authority.** The source doc keeps the full rules and the skill is the summary that routes to them (`audit-a-repo` over `AUDIT.md`, `workflow-ci-contract` over `WORKFLOW.md`). The skill states per topic which doc section owns it.
- **Included content.** The doc keeps the full rules and the skill needs them whole to work in isolation, so it carries the section as a generated include rather than as a summary or a copy, declared with the region markers "The Pipeline" above describes and keyed on the doc's section (`agent-conduct` over the three `GOVERNANCE.md` sections it surfaces). The doc side states the shape with one sentence naming the skill that includes the section, and the skill side is the region itself. A section carried this way is read outside its own document, so it names a sibling section by document and heading rather than as above or below, and it links to no file by a relative path, since the path would resolve against the skill's directory rather than the doc's. The doc wins by construction, since `scripts/build_dist.py` writes the region from it and its `--check` reports a region that differs from it as stale.
- **Kept authority.** The source doc keeps the full rules and the skill is the summary that routes to them (`audit-a-repo` over `AUDIT.md`, `workflow-ci-contract` over `WORKFLOW.md` outside section 4). The skill states per topic which doc section owns it.
- **Included content.** The doc keeps the full rules and the skill needs them whole to work in isolation, so it carries the section as a generated include rather than as a summary or a copy, declared with the region markers "The Pipeline" above describes and keyed on the doc's section (`agent-conduct` over the three `GOVERNANCE.md` sections it surfaces, `workflow-ci-contract` over `WORKFLOW.md` section 4). The doc side states the shape with one sentence naming the skill that includes the section, and the skill side is the region itself. A section carried this way is read outside its own document, so it names a sibling section by document and heading rather than as above or below, and it links to no file by a relative path, since the path would resolve against the skill's directory rather than the doc's. The doc wins by construction, since `scripts/build_dist.py` writes the region from it and its `--check` reports a region that differs from it as stale.

In every shape the doc is the authority when the two are found to disagree, the moved-content shape included: the doc's summary says what the rule is, and the skill's full text is what gets corrected. A deliberate change to a packaged rule is not such a disagreement. It lands where the full text lives, and in the same change the author either edits the other side's summary to match, since a summary has no mechanical check, or regenerates the include, which has one. A rule stated fully in both places by hand is the drift this pattern exists to prevent, and an include is the one full second statement that cannot drift undetected.
28 changes: 10 additions & 18 deletions .agents/skills/workflow-ci-contract/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,40 +8,32 @@ description: >-

## Why This Exists

`WORKFLOW.md` in the hub is a behavioral contract stating required outcomes rather than a required implementation. This skill is that contract's surface, so an agent editing workflow YAML has the contract in view. It carries the summary plus the binding rules, with the guarantee catalog and the test methodology split into `references/`. `WORKFLOW.md` keeps authority for the contract and methodology, and `GOVERNANCE.md` ("Workflow YAML Conventions", "Release Model") wins where those two overlap, which `WORKFLOW.md`'s own canonical-scope note states.
`WORKFLOW.md` is the fleet's CI/CD behavioral contract. This skill is that contract's surface, so an agent editing workflow YAML has the contract in view. It carries the summary, with the contract's section 4 carried whole as a generated include and the test methodology indexed in `references/`. `WORKFLOW.md`'s own canonical-scope note says which of it and `GOVERNANCE.md` is authoritative where the two overlap.

## How the Contract Is Read

- **Outcomes, not bytes.** A workflow is correct when it satisfies the section 4 contract against the expected inputs and outputs, not when it matches a catalog snippet byte for byte. Two repos may implement one guarantee with different YAML.
- **Outcomes, not bytes.** A workflow is judged against `WORKFLOW.md` section 4's expected inputs and outputs, never against a snippet byte for byte, per `GOVERNANCE.md` "Foundational Principles".
- **Applicability.** A guarantee governing a construct the repo does not contain is N/A: recorded, excluded from the verdict, never a defect. A source-only pipeline is mostly N/A and that is fine.
- **Operational is binary.** Every applicable guarantee holds, or the workflow is not operational. A single applicable input-output mismatch is a defect regardless of how clean the YAML looks.
- **Reached, not carried.** A standard workflow whose job graph is identical across repos of a type is a `workflow_call` task the hub hosts once, and a repo carries only a caller stub pinned to a hub release commit plus a composite-action hook at `.github/actions/<hook>` for what is its own. A hub task reaches its own actions and sibling tasks through `$/`, which resolves at that pinned commit. The merge-bot is the first, and `docs/reusable-workflows.md` in the hub carries the model, the hook contract, and the phase each workflow migrates in. Until a workflow's phase ships, its copy is graded as below.
- **Reached, not carried.** A standard workflow whose job graph is identical across repos of a type is reached as a hub-hosted `workflow_call` task, per `GOVERNANCE.md` "Hub-Hosted Tooling". The repo's own surface is the caller stub, pinned to a hub release commit, and a composite-action hook at `.github/actions/<hook>` for what is its own. A hub task reaches its own actions and sibling tasks through `$/`, which resolves at that pinned commit. The merge-bot is the first, and `docs/reusable-workflows.md` in the hub carries the model, the hook contract, and the phase each workflow migrates in. Until a workflow's phase ships, its copy is graded as below.
- **Two layers.** Orchestration (the PR entry workflow, publisher, version and release jobs) is generic and standard at the job level. Build leaves (the `build-<target>` tasks) are repo-owned. Inputs like `github`/`dockerhub`/`expect_release_assets` live on the orchestrator, a leaf receives `ref`/`branch`/`smoke` and whatever else its target needs, a derived `push` among them where that leaf pushes, so assert each input in the layer that declares it. A package target declares no push input on either layer, its push living in a separate `publish-<target>` job in the repo's own publisher. What a repo curates is the list of targets, and adding or dropping one edits the whole surface together: the `enable_<target>` input, the `build-<target>` job and its `github-release` and `build-docker` `needs:` entries, the `changes` paths-filter entry and output, the `smoke-build` enable-forward, and a package target's `publish-<target>` job (D6.4).

## Style Rules That Break in One-Line Diffs
## Style Rules

- **Pin every action to a commit SHA** with a trailing `# vX.Y.Z` comment, first-party included. The one documented no-pin exception is `dotnet/nbgv@master`. Invent no others.
- **Names carry meaning**: `-task.yml` files and "task" names are reusable (`on: workflow_call`), entry points end in what they do and their names end in "action", every job `name:` ends in "job" and every step in "step". A ruleset-bound required check's job `name:` and the ruleset `context:` are one string renamed together, in the live ruleset and the hub's `repo-config/` payloads in lockstep, or required-check enforcement silently breaks.
- **Concurrency**: top-level workflows use `group: '${{ github.workflow }}-${{ github.ref }}'` with `cancel-in-progress: true`. Two are documented exceptions. The publisher takes a global ref-independent group with `cancel-in-progress: false`, so publishes serialize and never cancel mid-push. The merge-bot takes `cancel-in-progress: false` and keys on the PR number rather than `github.ref`, per D8.1, so each PR queues independently and every event runs to completion.
- **Shells**: every multi-line bash `run:` starts `set -Eeuo pipefail`. Multi-line `if:` uses `>-`, never `|`.
- **Boolean inputs** are declared in both trigger blocks and compared against both forms, `${{ inputs.foo == true || inputs.foo == 'true' }}`, since `workflow_dispatch` delivers strings.
- **Permissions validate before `if:`**, so a callee declares `permissions:` only where every caller grants that scope at startup and otherwise declares none, running under the calling job's grant. A callee's extra scope (`actions: write` for cleanup) is granted by the caller at the one entry point that needs it.
- **Chaining across optional jobs** allowlists `success`/`skipped` explicitly, because `!= 'failure'` lets `cancelled` through.
- **Docker layer cache** targets a registry tag (`buildcache-<branch>`), never `type=gha`.
- **Workflow YAML is LF.** Preserve endings on every edit.
`GOVERNANCE.md` "Workflow YAML Conventions" and `WORKFLOW.md` section 2 keep the style rules. Read both before editing a workflow or a composite action.

## The Core Behavioral Spine

- **PRs validate fast and never publish**: a paths-filter smoke-builds only changed targets, the caller's own job reaching the reusable validator, or the replacement it points its aggregator at, always runs, and one required aggregator gates the merge, running under `if: always()` so a failed or skipped dependency cannot skip the gate itself, treating skipped smoke as pass and blocking on failure or cancelled. Smoke does a full compile/lint/test but pushes nothing and uploads nothing, every `upload-artifact` gated on smoke being false, which is `!inputs.smoke` at the workflow layer and `inputs.smoke != 'true'` in a composite action, whose inputs are strings.
- **A human merge never auto-publishes**: a `plan` job decides once and every job gates on it. Publishes come from a code-affecting bot push to `main`, a manual dispatch of `main` or `develop`, or the main-only weekly Docker schedule, while a publisher whose only trigger is `workflow_dispatch` (`releaseTrigger: dispatch-only`) reaches the dispatch alone, its bot-push and schedule paths never firing, which covers a source-only repo and an operational repo alike. Each run builds the one trigger branch, the default branch a clean `X.Y.Z`, anything else a prerelease `X.Y.Z-g<sha>`, with NBGV owning the patch from git height. The gate's branch literal, the `prerelease` expression, and `version.json`'s `publicReleaseRefSpec` each name the repo's actual default branch, and a divergence among the three is a defect. The release tags the built commit's SHA (`GitCommitId`), never a branch name.
- **Validate at entry**: cross-input and input-versus-derived-state invariants are asserted once at entry, in a dedicated job or in a step of an entry job, and the downstream jobs `needs:` that job, failing fast with `::error::` before expensive work. The release gate checks branch-versus-prerelease in both directions, strips `+buildmetadata`, and on smoke skips the check while the job still succeeds.
- **A human merge never auto-publishes**, per `GOVERNANCE.md` "Release Model": a `plan` job decides once and every job gates on it. Publishes come from a code-affecting bot push to `main`, a manual dispatch of `main` or `develop`, or the main-only weekly Docker schedule, while a publisher whose only trigger is `workflow_dispatch` (`releaseTrigger: dispatch-only`) reaches the dispatch alone, its bot-push and schedule paths never firing, which covers a source-only repo and an operational repo alike. Each run builds the one trigger branch, the default branch a clean `X.Y.Z`, anything else a prerelease `X.Y.Z-g<sha>`, with NBGV owning the patch from git height. The gate's branch literal, the `prerelease` expression, and `version.json`'s `publicReleaseRefSpec` each name the repo's actual default branch, and a divergence among the three is a defect. The release tags the built commit's SHA (`GitCommitId`), never a branch name.
- **Validate at entry**: `GOVERNANCE.md` "Workflow YAML Conventions" keeps the rule. The release gate that applies it checks branch-versus-prerelease in both directions, strips `+buildmetadata`, and on smoke skips the check while the job still succeeds.
- **The seam contract**: a target contributes a release file by uploading `release-asset-<branch>-<target>`, and the release job collects by `pattern:` plus `merge-multiple:`, never `artifact-ids:`, canonical even for a single target. A caller with no file target passes `expect_release_assets: false`, which covers a Docker-only, a PyPI-only, and a source-only repo, while a NuGet-only caller keeps the default `true`, its leaf uploading a `release-asset-*` that carries the package.
- **Artifacts are an intra-run handoff**: a cross-job transfer artifact is deleted at the job that consumes it, while an intermediate consumed only within the same run may instead rely on the `retention-days: 1` every upload sets. That delete is gated to the condition that made the artifact redundant, which is the release-create step's own condition where that step is the consumer, and `if: ${{ !cancelled() && steps.<download-step-id>.outcome == 'success' }}` where a package publish job's push is, since an `if:` carrying no status-check function inherits `success()` and skips on exactly the failed push that leaves the artifact already downloaded. Cleanup is best-effort, and never a blanket delete of the run's artifact set, which destroys the diagnostics you need when the run fails.
- **Artifacts are an intra-run handoff**: `GOVERNANCE.md` "Workflow YAML Conventions" keeps the consume-then-delete rule, the condition that gates the delete, and the blanket-delete ban. D5 in `references/d-guarantees.md` states the outputs a pipeline shows for them.
- **No-op republish**: an unchanged version re-pushes nothing, the release-create step skips when the tag exists and is refreshed only on a dispatch, registries dedupe server-side (`--skip-duplicate`, `skip-existing: true`), and Docker alone always re-pushes by design.
- **A build failure blocks every publish target**: `github-release` needs every build and guards with `!failure() && !cancelled()` as the terminal registry pusher (Docker) does, and a package target's separate `publish-<target>` job `needs:` the release-task call, so no build failure ships anything partial. A failed package push is outside that, since it runs after the release is cut.

A condensed catalog of `WORKFLOW.md` section 4 is in `references/d-guarantees.md`. `references/test-methodology.md` indexes `WORKFLOW.md` section 5's audit, trace, and probe procedure, and the sweep itself is run from section 5, which carries the whole core list, the per-type addenda, and the scenario table.
`references/d-guarantees.md` carries `WORKFLOW.md` section 4 whole as a generated include, so a guarantee's exact wording is one read away. `references/test-methodology.md` indexes `WORKFLOW.md` section 5's audit, trace, and probe procedure, and the sweep itself is run from section 5, which carries the whole core list, the per-type addenda, and the scenario table.

## After Any Workflow Edit

Workflow-only changes are not smoke-built, so run actionlint locally before pushing. Run it from the repository being checked, as `python3 /path/to/ProjectTemplate/scripts/docker_lint.py --root "$PWD" --linter actionlint`, using the hub-hosted wrapper documented in `GOVERNANCE.md`'s hub-only "Running the Linters Locally (Known-Working Invocations)" section. actionlint includes `shellcheck` for `run:` blocks, so `--linter actionlint` already covers them. A workflow change is still only fully exercised by CI, since `secrets: inherit`, `permissions:`, and `needs:` wiring resolve only in a real run.
A workflow-only change is not smoke-built, and actionlint still runs on it in CI. `GOVERNANCE.md` "Verification Discipline" requires the repository's whole lint gate before every push, rather than actionlint alone. A workflow change is still only fully exercised by CI, per the same "Verification Discipline" section.
Loading