Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Each guarantee is a MUST from `WORKFLOW.md` section 4, stated as the output a co
- **D4.2** `target_commitish` is the built commit's SHA (NBGV `GitCommitId`), never a branch name and never a separately re-resolved ref.
- **D4.3** Every release is a tag plus source zip, README, and LICENSE, `prerelease` equals `branch != default`, file targets attach `release-asset-*`, and a no-file-target caller (Docker-only, PyPI-only, source-only) passes `expect_release_assets: false` or the release-create step fails on unmatched files, a source-only one setting every `enable_*` input false with it. A NuGet caller is not one of those, since its leaf uploads a `release-asset-*` carrying the package.
- **D4.4** No-op republish on a schedule or push trigger: an unchanged version re-pushes nothing and the release-create skips when the tag exists, while a dispatch re-run refreshes it and runs the paired asset delete with it, registries dedupe server-side under `dotnet nuget push --skip-duplicate` and PyPI's `skip-existing: true`, and Docker always re-pushes by design.
- **D4.5** A failed build blocks every publish target: `github-release` needs every build and the terminal registry pusher (Docker) needs every other build, both guarding `!failure() && !cancelled()` so a disabled or unchanged target, skipped rather than failed, still lets the release be cut and the image pushed, 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: the `publish-<target>` job runs after the whole release task and so after `github-release`, and can leave a release and tag for a version the registry never received, recovered by re-dispatching the same commit rather than by cleanup.
- **D4.5** A failed build blocks every publish target: `github-release` needs every build and the terminal registry pusher (Docker) needs every other build, both guarding `!failure() && !cancelled()` so a disabled or unchanged target, skipped rather than failed, still lets the release be cut and the image pushed, 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: the `publish-<target>` job runs after the whole release task and so after `github-release`, and can leave a release and tag for a version the registry never received. The recovery is a re-dispatch rather than a cleanup, and it repairs that version only while the branch tip has not moved, since NBGV derives the version from git height, so a re-dispatch after the tip has moved publishes a further version and leaves the first release without its package.
- **D4.6** A deploy check asserts which release and which environment answer, waiting for convergence to a bounded timeout, with an unreachable host reported distinctly from an HTTP status.

## D5: Resource Cleanup
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/fleet-skills/.source-digest
Original file line number Diff line number Diff line change
@@ -1 +1 @@
52bb883ade7b516e
9cf4ad34f6781963
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Each guarantee is a MUST from `WORKFLOW.md` section 4, stated as the output a co
- **D4.2** `target_commitish` is the built commit's SHA (NBGV `GitCommitId`), never a branch name and never a separately re-resolved ref.
- **D4.3** Every release is a tag plus source zip, README, and LICENSE, `prerelease` equals `branch != default`, file targets attach `release-asset-*`, and a no-file-target caller (Docker-only, PyPI-only, source-only) passes `expect_release_assets: false` or the release-create step fails on unmatched files, a source-only one setting every `enable_*` input false with it. A NuGet caller is not one of those, since its leaf uploads a `release-asset-*` carrying the package.
- **D4.4** No-op republish on a schedule or push trigger: an unchanged version re-pushes nothing and the release-create skips when the tag exists, while a dispatch re-run refreshes it and runs the paired asset delete with it, registries dedupe server-side under `dotnet nuget push --skip-duplicate` and PyPI's `skip-existing: true`, and Docker always re-pushes by design.
- **D4.5** A failed build blocks every publish target: `github-release` needs every build and the terminal registry pusher (Docker) needs every other build, both guarding `!failure() && !cancelled()` so a disabled or unchanged target, skipped rather than failed, still lets the release be cut and the image pushed, 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: the `publish-<target>` job runs after the whole release task and so after `github-release`, and can leave a release and tag for a version the registry never received, recovered by re-dispatching the same commit rather than by cleanup.
- **D4.5** A failed build blocks every publish target: `github-release` needs every build and the terminal registry pusher (Docker) needs every other build, both guarding `!failure() && !cancelled()` so a disabled or unchanged target, skipped rather than failed, still lets the release be cut and the image pushed, 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: the `publish-<target>` job runs after the whole release task and so after `github-release`, and can leave a release and tag for a version the registry never received. The recovery is a re-dispatch rather than a cleanup, and it repairs that version only while the branch tip has not moved, since NBGV derives the version from git height, so a re-dispatch after the tip has moved publishes a further version and leaves the first release without its package.
- **D4.6** A deploy check asserts which release and which environment answer, waiting for convergence to a bounded timeout, with an unreachable host reported distinctly from an HTTP status.

## D5: Resource Cleanup
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Each guarantee is a MUST from `WORKFLOW.md` section 4, stated as the output a co
- **D4.2** `target_commitish` is the built commit's SHA (NBGV `GitCommitId`), never a branch name and never a separately re-resolved ref.
- **D4.3** Every release is a tag plus source zip, README, and LICENSE, `prerelease` equals `branch != default`, file targets attach `release-asset-*`, and a no-file-target caller (Docker-only, PyPI-only, source-only) passes `expect_release_assets: false` or the release-create step fails on unmatched files, a source-only one setting every `enable_*` input false with it. A NuGet caller is not one of those, since its leaf uploads a `release-asset-*` carrying the package.
- **D4.4** No-op republish on a schedule or push trigger: an unchanged version re-pushes nothing and the release-create skips when the tag exists, while a dispatch re-run refreshes it and runs the paired asset delete with it, registries dedupe server-side under `dotnet nuget push --skip-duplicate` and PyPI's `skip-existing: true`, and Docker always re-pushes by design.
- **D4.5** A failed build blocks every publish target: `github-release` needs every build and the terminal registry pusher (Docker) needs every other build, both guarding `!failure() && !cancelled()` so a disabled or unchanged target, skipped rather than failed, still lets the release be cut and the image pushed, 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: the `publish-<target>` job runs after the whole release task and so after `github-release`, and can leave a release and tag for a version the registry never received, recovered by re-dispatching the same commit rather than by cleanup.
- **D4.5** A failed build blocks every publish target: `github-release` needs every build and the terminal registry pusher (Docker) needs every other build, both guarding `!failure() && !cancelled()` so a disabled or unchanged target, skipped rather than failed, still lets the release be cut and the image pushed, 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: the `publish-<target>` job runs after the whole release task and so after `github-release`, and can leave a release and tag for a version the registry never received. The recovery is a re-dispatch rather than a cleanup, and it repairs that version only while the branch tip has not moved, since NBGV derives the version from git height, so a re-dispatch after the tip has moved publishes a further version and leaves the first release without its package.
- **D4.6** A deploy check asserts which release and which environment answer, waiting for convergence to a bounded timeout, with an unreachable host reported distinctly from an HTTP status.

## D5: Resource Cleanup
Expand Down
2 changes: 1 addition & 1 deletion WORKFLOW.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ The required behaviors, organized by domain. Each is a **MUST**, stated as the o
- **D4.2 Tag the built commit.** Output: the release `target_commitish` is the built commit's SHA (NBGV's `GitCommitId`), never a branch name or a separately re-resolved ref. *Prevents: the tag landing on the default branch instead of the built tree.*
- **D4.3 Release contents.** Output: every release contains a tag on the built commit plus the auto source zip, README, and LICENSE. File targets attach `release-asset-*`. The `prerelease` value equals `branch != default`. A no-file-target caller sets `expect_release_assets: false` to reach the no-asset shape. This applies to Docker-only, PyPI-only, and source-only repos. A NuGet target is not among them, since its leaf uploads a `release-asset-*` carrying the package, so a NuGet-only caller keeps the default `true`. The setting relaxes `fail_on_unmatched_files` and skips the asset download. The release-create step fails when no assets exist and the setting retains its default `true`. A source-only caller also sets every `enable_*` input false.
- **D4.4 No-op republish.** Input: a re-run whose version is unchanged, on a schedule or push trigger. Output: nothing is re-pushed, because the release-create step is skipped when the tag exists, and the paired asset-delete is skipped with it. A **dispatch** re-run refreshes the release instead and runs that delete with it, which is why a dispatch-only publisher records this item's skip leg as unreachable rather than failed. Registry pushes are no-ops. The NuGet/PyPI publish steps are **not** statically gated on existence. They run and the **server** dedupes (`dotnet nuget push --skip-duplicate` turns a 409 into success, and PyPI does the same under `skip-existing: true`). **Docker always re-pushes** the image (base-image refresh), independently of the release-create skip, within the same run. *Prevents: duplicate releases and wasted pushes.*
- **D4.5 A build failure blocks every publish target.** Input: a real publish where one enabled build fails. Output: nothing publishes. `github-release` needs every build and carries the same `!failure() && !cancelled()` guard the terminal registry pusher (Docker) does, since the implicit `success()` would otherwise skip both on every run that disables a target rather than only on a failed one. A failed build therefore skips the release (no tag, no release), and Docker, which needs every other build, skips with it (no image push) while a disabled or unchanged target (skipped, not failed) still lets docker push. *Prevents: a partial publish, e.g. a Docker image pushed while .NET publish failed and no release was cut.* A package target's separate publish job needs its own gate for the same reason, since it sits outside the `github-release` and Docker `needs:` chains: it `needs:` the release-task call, so a failed build skips it with the rest. What no gate covers is a failed **push**, because that job runs after the whole release task and therefore after `github-release`, for the trusted-publishing reason section 3's package-registry bullet gives. A rejected token exchange, a registry outage, or a trusted-publishing policy naming the wrong workflow file therefore leaves a published release and tag for a version that never reached the registry. The recovery is a re-dispatch rather than a cleanup. A re-dispatch of the commit that failed refreshes that release rather than cutting a second one (D4.4) and runs its push again. A re-dispatch after the branch tip has moved builds the new tip instead, since NBGV derives the version from git height, so it publishes a further version and leaves the first one's release without its package.
- **D4.5 A build failure blocks every publish target.** Input: a real publish where one enabled build fails. Output: nothing publishes. `github-release` needs every build and carries the same `!failure() && !cancelled()` guard the terminal registry pusher (Docker) does, since the implicit `success()` would otherwise skip both on every run that disables a target rather than only on a failed one. A failed build therefore skips the release (no tag, no release), and Docker, which needs every other build, skips with it (no image push) while a disabled or unchanged target (skipped, not failed) still lets docker push. *Prevents: a partial publish, e.g. a Docker image pushed while .NET publish failed and no release was cut.* A package target's separate publish job needs its own gate for the same reason, since it sits outside the `github-release` and Docker `needs:` chains: it `needs:` the release-task call, so a failed build skips it with the rest. What no gate covers is a failed **push**, because that job runs after the whole release task and therefore after `github-release`, for the trusted-publishing reason section 3's package-registry bullet gives. A rejected token exchange, a registry outage, or a trusted-publishing policy naming the wrong workflow file therefore leaves a published release and tag for a version that never reached the registry. The recovery is a re-dispatch rather than a cleanup. A re-dispatch while the branch tip is still the commit whose push failed refreshes that release rather than cutting a second one (D4.4) and runs its push again, the dispatch naming the branch rather than the commit. A re-dispatch after the branch tip has moved builds the new tip instead, since NBGV derives the version from git height, so it publishes a further version and leaves the first one's release without its package.
Comment thread
ptr727 marked this conversation as resolved.
Outdated
- **D4.6 Deploy verification names the release.** Input: a deploy to a filesystem on a host the project owns that completes without error. Output: a check against the running host asserts **which release is answering**, not merely that it answers. The artifact stamps its own version into the configuration it ships, and the check compares that against the version just installed, **waiting for convergence to a bounded timeout** rather than sampling once, because content goes live the instant a pointer moves while server rules wait on an asynchronous reload. The same check asserts **which environment** answered, since several environments serve a byte-identical artifact and a proxy rule aimed at the wrong one answers healthily under the right hostname. An unreachable host is reported distinctly from an HTTP status. *Prevents: a green deploy over a host still serving the previous release's configuration, a URL contract checked against the wrong environment, and a dead config watcher read as a routing fault.*

### D5 - Resource Cleanup
Expand Down
16 changes: 8 additions & 8 deletions reports/canonical-review.json
Original file line number Diff line number Diff line change
Expand Up @@ -291,11 +291,11 @@
},
{
"unit": ".agents/skills/workflow-ci-contract/references/d-guarantees.md > D4: Release and Publish",
"digest": "sha256:903841ce4df88f8c74cb2f1d3bbb3fc5f8a813093e819f2726b1a4dbfbba444e",
"digest": "sha256:e3fbb2b2de51a2679272e1ff1637681886241da62113ea9020550a3823ef9b43",
"reviewer": "agent-skill",
"findings": 9,
"hubCommit": "445bd440ca7df6fc32a28dd34be9469d2c603f18",
"stamp": "2026-09-02T16:49:49Z"
"findings": 2,
"hubCommit": "c6e7526cfade19f28f0ab09c4ab283ed00295d7e",
"stamp": "2026-09-02T20:49:13Z"
},
{
"unit": ".agents/skills/workflow-ci-contract/references/d-guarantees.md > D5: Resource Cleanup",
Expand Down Expand Up @@ -451,11 +451,11 @@
},
{
"unit": "WORKFLOW.md > 4. Behavioral Contract: Expected Outcomes",
"digest": "sha256:f2d26a329e8bacd1580643c7d164119f7f8df6e09abb34544edc4debdb95e704",
"digest": "sha256:f6048347c0d4492067504afde742eeb23c2e741cd0c928641734ffac0ff71f0f",
"reviewer": "agent-skill",
"findings": 9,
"hubCommit": "398abeedb76d01d87437993255c49ade98d72c99",
"stamp": "2026-09-02T16:58:05Z"
"findings": 4,
"hubCommit": "c6e7526cfade19f28f0ab09c4ab283ed00295d7e",
"stamp": "2026-09-02T20:49:14Z"
},
{
"unit": "WORKFLOW.md > 5. Test Methodology",
Expand Down