Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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 .agents/skills/dotnet-codestyle/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ The .NET mechanics, narrowest first:

xUnit v3 (`xunit.v3`, not the legacy `xunit`) + AwesomeAssertions (`.Should()` API, never native
asserts). Arrange-Act-Assert pattern, descriptive underscore names, `[Theory]`/`[InlineData]` for
parameterized tests. See `references/testing.md` for the framework setup template.
parameterized tests. A test project on `xunit.v3` 4.0.0 or later is MTP-based, and also carries a `global.json` runner declaration, a `Microsoft.Testing.Extensions.CodeCoverage` floor, and no `xunit.runner.visualstudio`. See `references/testing.md` for the framework setup template and that configuration.

## Project configuration

Expand Down
16 changes: 16 additions & 0 deletions .agents/skills/dotnet-codestyle/references/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,19 @@
2. **Organization**: Arrange-Act-Assert pattern.
3. **Naming**: descriptive names with underscores.
4. **Theory tests**: use `[Theory]` with `[InlineData]`.

## Microsoft.Testing.Platform and coverage

A test project on `xunit.v3` 4.0.0 or later is MTP-based, and the .NET 10 SDK and later refuse to run one through the VSTest target, so such a project also carries:

- a root **`global.json`** declaring `{"test": {"runner": "Microsoft.Testing.Platform"}}`, which is what selects the driver `dotnet test` runs the project through,
- **`Microsoft.Testing.Extensions.CodeCoverage`** at **18.9.0 or later**, in place of `coverlet.collector`, whose VSTest data collector MTP ignores without failing,
- no **`xunit.runner.visualstudio`**, the VSTest adapter MTP replaces.

A project not yet MTP-based keeps the VSTest collector, and that lagging state is a migration owed rather than drift, until its own `xunit.v3` bump forces the move.

**The version floor is load-bearing rather than cautionary.** Below 18.1.0 the extension is built against Microsoft.Testing.Platform 1.x, and an 18.0.x resolution, which is what a `>= 18.0.0` range picks, throws a `TypeLoadException` against the 2.x platform `xunit.v3` 4.0.0 carries, runs zero tests, and **still writes a well-formed Cobertura file reporting full coverage**, so only the non-zero exit says the run reported nothing. 18.9.0 is the first release on Microsoft.Testing.Platform 2.3.x, where every test project writes into the one shared `--results-directory` the invocation names rather than resolving that relative path per project.

The CI invocation `WORKFLOW.md` D1.6 requires is `dotnet test --coverage --coverage-output-format cobertura --results-directory ./coverage`. Two further details of it are equally load-bearing, and neither failure reds the job on its own. `--coverage-output` stays unset, because pinning one filename gives every test project in the solution the same path and a solution with more than one then keeps only whichever ran last. Leaving it unset produces the default name `<guid>.cobertura.xml`, which `codecov-cli`'s own file finder does not match, so the report is renamed before the upload reads the directory, per `WORKFLOW.md` D1.6.

**Diagnosing a local run.** `dotnet test` under the CI configuration reports zero tests on some machines where CI reports the full suite on the same SDK, which reads as a broken repository and is a broken driver. The target string the run prints separates the two: `net10.0` with no architecture means the driver resolved none, and `net10.0|<arch>` with no tests means the tests did not register, which is the case that points back at the three requirements above.
9 changes: 7 additions & 2 deletions .agents/skills/operational-vs-release-workflow/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,15 @@ rather than guessing from the repo's contents.
`HISTORY.md`, and release notes name the version as `Version 1.0` (the floor), never the concrete
build height, which is both wrong (the real height differs) and a maintenance trap.
"Correcting" `1.0` to `1.0.0` is a defect.
- **A no-op publish (unchanged NBGV `SemVer2`) re-pushes nothing to any target keyed on the
version string, except Docker, which always re-pushes** to pick up upstream base-image
- **A no-op publish on a schedule or push trigger (unchanged NBGV `SemVer2`) re-pushes nothing to
any target keyed on the version string, except Docker, which always re-pushes** (a dispatch
refreshes the release instead of skipping) to pick up upstream base-image
refreshes. Full guarantee and the `version.json` `pathFilters` boundary:
`references/release-publish-mechanics.md`.
- **A package push can fail after the release is already cut**, since it runs after the release
task and no gate covers it. A full re-run is always available inside its bounded
window and is the only route once the branch tip has moved:
`references/release-publish-mechanics.md`.
- **Adding, dropping, or wiring a release target** (which leaf task, which artifact-naming
contract, which seam a given output belongs to: a GitHub Release asset, a package-registry push,
an image-registry push, a filesystem deploy, or a source-only repo with no build layer at all),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

Full detail for the "Publishing" rules in `SKILL.md`. Load this when adding or removing a release
target, wiring a new leaf build task, deciding where a build output belongs (a GitHub Release
asset, a package-registry push, an image push, a deploy), or setting up a wrapper repo that tracks
an upstream release, not for reading the release model's shape (the SKILL.md summary covers that).
asset, a package-registry push, an image push, a deploy), recovering a package push that failed
after the release was already cut, or setting up a wrapper repo that tracks an upstream release,
not for reading the release model's shape (the SKILL.md summary covers that).

## Reusable-task parameter contract

Expand Down Expand Up @@ -128,6 +129,26 @@ NBGV git height and therefore `SemVer2`, and the next publish *does* create a fr
even when the shipped binary is byte-identical. This is accepted NBGV behavior, and `pathFilters`
are intentionally not added.

## Recovering a failed registry push

A package publish job is gated like everything else, `needs:` the release-task call, so a failed build skips it. The **push inside it** is what no gate can reach, because it runs after the whole release task and therefore after `github-release`. `WORKFLOW.md` D4.5 names the two recovery routes and leaves their mechanics here. A rejected token exchange, a registry outage, or a trusted-publishing policy naming the wrong workflow file leaves a published release and tag for a version that never reached the registry. The recovery is a re-dispatch or a full re-run rather than a cleanup. **A full re-run is always available inside its window, and a re-dispatch only while the branch tip has not moved**, so the tip decides whether there is a choice at all rather than which route to take. What re-dispatch buys, where it is available, is that it outlives the re-run window.

**Re-dispatch, available only while the tip has not moved.** A `workflow_dispatch` takes a ref rather than a commit, and D2.3 admits only `main` or `develop`, so what it builds is that branch's tip at dispatch time. While the tip is still the commit whose push failed, a re-dispatch rebuilds the same version and runs its push again, refreshing the release the way any dispatch does.

This is a time-of-check-to-time-of-use race rather than a guarded operation: nothing compares the tip against the failed run, so a push landing between the two mints a new version instead of erroring, and the operator sees a green publish that left the failed version unpublished. Confirm the failed run's own head commit still equals the branch tip immediately before dispatching, reading it as `gh run view <id> --json headSha` against `gh api repos/{owner}/{repo}/branches/<branch>` for the branch that run built rather than whichever branch is to hand. Where the two differ, or where the check is not worth making, prefer the re-run route, which is bound to that commit by construction, and fall back to re-dispatch only once the re-run window below has closed.

**Re-run all jobs, available inside the window whatever the tip has done.** `gh run rerun <id>` replays the run under the original event's `GITHUB_SHA` and `GITHUB_REF` and re-executes every job rather than only the failed ones. The publisher pins the release task to that commit with `ref: ${{ github.sha }}`, so `get-version` recomputes the same version from the same commit and history, each build leaf checks out the `GitCommitId` that job emits, the package artifact D5.2 deleted is rebuilt and re-uploaded rather than missing when `publish-<target>` downloads it, and that job retries the push it failed. The release itself needs nothing from the re-run, the failed run having already cut it, though on a dispatch-triggered run the re-run re-enters `github-release`, which refreshes the release per D4.4's dispatch leg and runs the `release-asset-*` delete with it per D5.2. A re-dispatch here would build the new tip instead, and NBGV derives the version from git height, so that is a further version and the one whose push failed never reaches the registry.

Three qualifications come with the re-run route.

- D4.4 and `WORKFLOW.md` 5B's S9 describe a re-run whose predecessor push **succeeded**, where the registry dedupes the second one. This is the case they do not cover, and its retried push is the first the registry ever receives for that version.
- GitHub offers a re-run only within **30 days** of the initial run, and a repository's own **log** retention setting can be shorter, so the usable window is the shorter of the two. This is the run's own retention and is unrelated to D5.4's `retention-days: 1`, which bounds an uploaded artifact rather than the run.
- **Re-run failed jobs** (`--failed`) does not serve here. D5.2's delete runs on the path that reaches this case, its gate being `!cancelled()` and the download having succeeded, so it has already removed the package artifact a `--failed` re-run would download, and only the full re-run rebuilds it.

Past the window, a moved tip leaves that version with no route to the registry. The release and tag already name it, and removing them is not the answer: leave them, and let the next publish carry a later version, recording the gap in `HISTORY.md`, since the release body is regenerated on any later dispatch refresh and cannot hold the record.

What no route settles in advance is whether the registry accepts the retried push.

## Wrapper repos that track an upstream release

A repo wrapping an upstream release uses the hub-hosted `check-upstream-version-task.yml`: a
Expand Down
2 changes: 1 addition & 1 deletion .agents/skills/python-codestyle/references/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ are in `references/profiles.md`.
Use `pytest` with configuration in `[tool.pytest.ini_options]`. Default invocation:
`uv run pytest`.

**Coverage.** Before creating or modifying `pyproject.toml`, read `WORKFLOW.md` D1.6 for the coverage obligations a build-profile repo with tests owes.
**Coverage.** A build-profile repository with tests declares **`pytest-cov`** among its test dependencies, a dev dependency group where the repository is a uv project and a `requirements*.txt` entry where it is on pip, and selects the coverage source in its own `pyproject.toml`, an `addopts` entry of `--cov=<package>` in practice. CI adds `--cov-report=xml` to the invocation, so the repository owes the dependency and the selector rather than that flag. Both halves are load-bearing and they fail differently: without the dependency the CI run exits non-zero on an unrecognized argument, and with the dependency but no selector it measures nothing, writes no file, and exits zero. Leave the report at the repository root as `coverage.xml`, the one path CI names. `WORKFLOW.md` D1.6 owns the pipeline half, the upload and the check that fails when no report was written.

- One test file per module under test, named `test_<module>.py`.
- Test functions named `test_<scenario>_<expected_behavior>`, descriptive and not numbered.
Expand Down
2 changes: 1 addition & 1 deletion .agents/skills/workflow-ci-contract/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ description: >-

## The Contract Text

`references/architecture.md`, `references/d-guarantees.md`, and `references/test-methodology.md` carry `WORKFLOW.md` sections 3, 4, and 5 whole, each as a generated include, so the pipeline's architecture, a guarantee's exact wording, and the audit-trace-probe procedure are each one read away rather than restated in full here. A defect in an include region is fixed in `WORKFLOW.md` and regenerated, never edited in this skill, per the `skill-lifecycle` Skill. `WORKFLOW.md` keeps sections 1, 2, and 6 itself, the applicability rule, the style-rule pointer, and the per-project-type walkthroughs that say which items go N/A per type, so read those there.
`references/architecture.md`, `references/d-guarantees.md`, and `references/test-methodology.md` carry `WORKFLOW.md` sections 3, 4, and 5 whole, each as a generated include, so the pipeline's architecture, a guarantee's exact wording, and the audit-trace-probe procedure are each one read away rather than restated in full here. A defect in an include region is fixed in `WORKFLOW.md` and regenerated, never edited in this skill, per the `skill-lifecycle` Skill. `WORKFLOW.md` keeps sections 1, 2, and 6 itself, the applicability rule, the style-rule pointer, and the per-project-type walkthroughs, which say which constructs each type adds, map each construct to the scenarios it reaches, and carry three rules for reading a row, one of which is about a repository declaring more than one type, so read those there.

## After Any Workflow Edit

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Their CI is lint/validation only (editorconfig/EOL plus domain linters such as H

- **Orchestration** is generic and forms the standardization baseline **at the job level**: the single-branch publisher, the `get-version`, `validate-release`, and `github-release` jobs, and the `changes -> smoke-build -> aggregator` shape of the PR workflow. These job *bodies* should not need per-repo edits.
- **Build** is repo-owned in shape: the `build-<target>` leaf tasks, whether this repo hosts them itself or reaches hub-hosted ones by pin.
- **What the repo curates** (by design, not a leak): the *list* of targets. This is **not** a byte-for-byte file carry. Adding or dropping a target edits the orchestrator's surface: the `enable_<target>` inputs and the `build-<target>` job + its `github-release` **and** `build-docker` `needs:` entries in the release task, **and** the `changes` paths-filter entry + output + the `smoke-build` enable-forward in the PR workflow, plus the separate `publish-<target>` job for a package target. "Verbatim" applies to the `github-release` job and the version/publish-plan logic, not to the release task's job list or the paths-filter. Subsetting is symmetric: the same surface you trim to drop a target you extend to add a new one (e.g. a `release-asset-<branch>-library` producer needs a new `enable_library` input, a `build-library` job, its two `needs:` entries, and a `library` paths-filter entry, output, and `smoke-build` enable-forward).
- **What the repo curates** (by design, not a leak): the *list* of targets. This is **not** a byte-for-byte file carry. Adding or dropping a target edits the orchestrator's surface: the `enable_<target>` inputs and the `build-<target>` job + its `github-release` **and** `build-docker` `needs:` entries in the release task, **and** the `changes` paths-filter entry + output + the `smoke-build` enable-forward in the PR workflow, plus the separate `publish-<target>` job for a package target. "Verbatim" applies to the `github-release` job and the version/publish-plan logic, except that job's own `needs:` list, and never to the release task's job list or the paths-filter. Subsetting is symmetric: the same surface you trim to drop a target you extend to add a new one (e.g. a `release-asset-<branch>-library` producer needs a new `enable_library` input, a `build-library` job, its two `needs:` entries, and a `library` paths-filter entry, output, and `smoke-build` enable-forward).

### The Seam Contract

Expand Down
Loading