Repository navigation
ci: stop release re-runs from replacing published assets and NuGet versions - #56
Conversation
…rsions Upload only missing GitHub assets and fail when an existing asset's SHA-256 differs from the built file; compare the registry package payload (ignoring nuget.org's repository signature) before accepting an existing NuGet version; build the win-x64 zip with fixed timestamps; track the .NET SDK pin with Dependabot.
.NET's PAX tar writer names extended headers after the process ID, so identical content hashed differently per build. Write GNU format in both the release archives and the npm tarball, and cover the archive builders in release-regressions.
7bf1042 to
fe659e2
Compare
|
Rebased onto After the rebase, all of these passed: build (0 warnings), |
|
Hosted CI observed on head |
# Conflicts: # CHANGELOG.md # tools/PostgreSqlMcp.Release/Program.cs
# Conflicts: # CHANGELOG.md # tools/PostgreSqlMcp.Release/Program.cs # tools/PostgreSqlMcp.Verify/PostgreSqlMcp.Verify.csproj
|
Pre-merge review (agent-authored; merge authorized by the maintainer in this session). The independent reviewer confirmed the no-clobber upload plan, NuGet payload comparison and reproducible archives; its nits (keep the S_IFREG type bit in zip entry modes; correct the git-workflow re-upload line) are fixed in b4c8e88. Merged main twice: (1) #55's provenance bundle now passes through the same no-overwrite plan as one more asset, after ProvenanceBundleAsync validates its subjects against the built files; CONTRIBUTING and CHANGELOG say the bundle is signed per workflow run, so it only matches on a same-run re-run. (2) #58's shared runner: publication commands keep 'nuget', the root comes from RepositoryRoot.Find, and the verifier links both the release sources and the shared runner. Exercised the merged GitHub publish path with a throwaway fake gh and synthetic assets: fresh release uploads 10 assets including provenance.sigstore.json; an identical re-run uploads 0; an existing asset with different bytes is refused; a partial release uploads only the 2 missing assets; mismatched provenance subjects are refused before any gh call; an echoed synthetic token is printed as ***. release-regressions passes. Local integration on 377c664: ALL MCP INTEGRATION SCENARIOS PASSED. Hosted: all required checks pass (verify: integration plus 'Package verification passed'), and the Integration matrix passes on PostgreSQL 13-16 and 18. Not exercised: a real GitHub release upload and a real nuget.org push. |
Problem
Refs #40 (items 1, 4 and 5; see "Scope" below for items 2 and 3).
gh release upload --clobberlet a release re-run silently replace already-published assets with differently built bytes.dotnet nuget push --skip-duplicatetreated any existing NuGet version as success, even if the registry held a different package.global.json.Scope: item 2 (release tool prints raw child output while claiming "redacted runner diagnostics") is fixed inside the shared process runner's explicit output policy in the separate refactor PR for #43. Item 3 (fixture image/HypoPG pinning,
PostgresFixture.cs) is handled by the PostgreSQL-version PR. Neither is touched here.Behavior change
--clobber. It reads the release's assets, hashes every built asset (includingSHA256SUMS) and uploads only missing ones. An existing asset must beuploadedand have the same SHA-256 (GitHub's reporteddigest; if absent or not SHA-256, the asset is downloaded and hashed). Any mismatch or incomplete asset fails the job; nothing is replaced.dotnet release-tool nuget --context ...replaces the inline shell step. It checks the nuget.org flat container for the version. Absent:dotnet nuget pushwithout--skip-duplicate. Present: download the registry.nupkgand compare each entry's name and SHA-256 with the built package, ignoring only.signature.p7s(nuget.org's repository countersignature; whole-file hashes never match, see Evidence). Any other difference fails. The nuget job now also downloads therelease-contextartifact.ReleaseArchive.CreateZip: sorted entries, fixed 1980-01-01 timestamp (earliest ZIP time), same mode rule as the tar entries. The tar.gz archives (ReleaseArchive.CreateTarGz) and the npm tarball intools/PostgreSqlMcp.Build/Program.csswitch from PAX to GNU tar format (see Compatibility)..github/dependabot.yml: newdotnet-sdkecosystem for/, weekly.release-regressions(also run byintegration).Compatibility and security impact
nugetcommand.gh run downloadof an existing artifact in the nuget job (permissions unchanged). No credentials are logged:NUGET_API_KEYis passed todotnet nuget pushexactly as before.tar.gzrelease archives and the npm tarball fromtools/PostgreSqlMcp.Buildare now GNU-format tar instead of PAX (same file names, modes, fixed owner/time metadata; standardtarand npm extract them; thepackagessmoke passed). Reason: .NET's PAX writer names extended headersPaxHeaders.<pid>, so identical content hashed differently per build and every re-dispatch would have failed the new asset comparison..nupkg(dotnet packstamps wall-clock times on zip entries; entry contents are identical, which is why the NuGet check compares entries), the CycloneDX SBOM (randomserialNumberandtimestamp) and soSHA256SUMS. Therefore recover a partial publication by re-running the failed jobs of the same workflow run (same artifacts), not by dispatching a new run for the same tag, which fails closed on those assets.docs/git-workflow.md(Repository settings) says the release tool "re-uploads on a re-run"; that sentence is stale after this PR and is left for the maintainer since it was not part of the change.Evidence
Local, emulated PostgreSQL 16.15 fixture (no Docker):
dotnet build postgresql-sharp-mcp.slnx -c Release -p:RestoreLockedMode=true: 0 warnings, 0 errors.dotnet run --project tools/PostgreSqlMcp.Verify -c Release -- release-regressions: exit 0. Covers fresh/partial/complete existing assets, upper-case digest, different bytes, mismatch beside matching assets,starterstate, missing/non-SHA-256 digest with download fallback and tamper detection, JSON parsing, zip and tar.gz byte-identity across changed mtimes, entry order/fixed metadata/GNU format, changed payload, and NuGet payload comparison (reordered entries,.signature.p7signored, changed/missing/extra entry).... -- integration(serialized under the shared lock, rerun after the tar change):ALL MCP INTEGRATION SCENARIOS PASSED, including the new regressions.ZipFile.CreateFromDirectoryover the same files with changed mtimes gave two different SHA-256 values.Release archivesrun twice (TZ=Asia/Jakarta then TZ=America/New_York, fresh publish and new process each time), before the tar change: win-x64 zip identical, the fourtar.gzdiffered (PaxHeaders.<pid>in the tar stream). After the change (GNU format): all five archives identical across both runs (linux-x64.tar.gze191f4c2...,win-x64.zipf2c6fbed...; same values again in a second full session).tools/PostgreSqlMcp.Build -- packagerun twice with different TZ: the npm.tgzis byte-identical (53f1367d...); the.nupkgis not (see Compatibility). The extracted linux-x64 archive's server ran--version.... -- packages --artifacts <Build output>(serialized, emulated PG16) after the tar change:Package verification passed; offline local and global npm installs, fresh-cache npx tarball CLI/MCP without .NET, NuGet, native commands and shutdown verified.CreateTarGzback to PAX makesrelease-regressionsfail with "tar.gz archives must not contain process-ID-named PAX headers."sha256:digests; the parser plus planner treated the realnupkg,SHA256SUMS,bom.jsonas identical (0 missing), listed a new name as missing, and rejected a zeroed digest. nuget.org flat container: version listing found 0.4.0, not 9.9.9, not an unknown id. The registry.nupkgdiffers by bytes from the GitHub asset (2,089,614 vs 2,076,524 bytes) yetComparePayloadreports 0 differences; against registry 0.3.3 it reports 9.release-tool nuget --contextwith the public 0.4.0 asset as the "local" package and a dummy API key: "already exists with an identical payload ... not republished" (exit 0); with an extra entry added: "different payload ('extra.txt' is missing from the registry package)" (exit 1); without key: exit 1. No push occurred.Not verified:
gh release create/upload/downloadwith real tokens) and the livedotnet nuget pushpath were not run: no release was created, no asset uploaded or deleted, nothing pushed to nuget.org. Only a real release run can confirm: GitHub reportingdigestfor freshly uploaded assets, thegh release download --patternfallback, the new nuget job step with the OIDC API key and artifact download, and nuget.org's conflict behaviour.release.ymlanddependabot.ymlwere only YAML-parsed. Dependabot'sdotnet-sdkecosystem behaviour is unobserved until Dependabot runs.Authorship
Written by an AI coding agent at the maintainer's request; the
agent-authoredlabel is applied (andci).Checklist
CHANGELOG.mdupdated