Skip to content

ci: stop release re-runs from replacing published assets and NuGet versions - #56

Merged
codegiveness merged 5 commits into
mainfrom
ci/release-integrity
Oct 10, 2026
Merged

codegiveness merged 5 commits into
mainfrom
ci/release-integrity

Conversation

@codegiveness

@codegiveness codegiveness commented Oct 10, 2026 •

Copy link
Copy Markdown
Owner

Problem

Refs #40 (items 1, 4 and 5; see "Scope" below for items 2 and 3).

  • gh release upload --clobber let a release re-run silently replace already-published assets with differently built bytes.
  • dotnet nuget push --skip-duplicate treated any existing NuGet version as success, even if the registry held a different package.
  • Dependabot did not track the .NET SDK pin in global.json.
  • The release archives and npm tarball were not byte-reproducible: the win-x64 zip took on-disk modification times, and .NET's PAX tar writer embeds the process ID in extended-header names.

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

  • GitHub job: no --clobber. It reads the release's assets, hashes every built asset (including SHA256SUMS) and uploads only missing ones. An existing asset must be uploaded and have the same SHA-256 (GitHub's reported digest; if absent or not SHA-256, the asset is downloaded and hashed). Any mismatch or incomplete asset fails the job; nothing is replaced.
  • NuGet job: dotnet release-tool nuget --context ... replaces the inline shell step. It checks the nuget.org flat container for the version. Absent: dotnet nuget push without --skip-duplicate. Present: download the registry .nupkg and 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 the release-context artifact.
  • win-x64 zip is written by a small 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 in tools/PostgreSqlMcp.Build/Program.cs switch from PAX to GNU tar format (see Compatibility).
  • .github/dependabot.yml: new dotnet-sdk ecosystem for /, weekly.
  • New offline verifier command release-regressions (also run by integration).
  • CONTRIBUTING and CHANGELOG updated.

Compatibility and security impact

  • A re-run that would previously have replaced a GitHub asset or accepted a different NuGet package now fails; a maintainer must delete the specific GitHub asset deliberately, or release a new NuGet version. No tool names, parameters, env vars, CLI flags or targets formats of the server change; the Release tool gains a nuget command.
  • A NuGet re-run immediately after a first push but before nuget.org lists the version now fails with a 409 instead of succeeding silently; re-running after indexing compares and succeeds.
  • No new Actions; only an additional gh run download of an existing artifact in the nuget job (permissions unchanged). No credentials are logged: NUGET_API_KEY is passed to dotnet nuget push exactly as before.
  • Archive format change: the Linux/macOS tar.gz release archives and the npm tarball from tools/PostgreSqlMcp.Build are now GNU-format tar instead of PAX (same file names, modes, fixed owner/time metadata; standard tar and npm extract them; the packages smoke passed). Reason: .NET's PAX writer names extended headers PaxHeaders.<pid>, so identical content hashed differently per build and every re-dispatch would have failed the new asset comparison.
  • Still not reproducible (unchanged, documented in CHANGELOG/CONTRIBUTING): the .nupkg (dotnet pack stamps wall-clock times on zip entries; entry contents are identical, which is why the NuGet check compares entries), the CycloneDX SBOM (random serialNumber and timestamp) and so SHA256SUMS. 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, starter state, 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.p7s ignored, changed/missing/extra entry).
  • ... -- integration (serialized under the shared lock, rerun after the tar change): ALL MCP INTEGRATION SCENARIOS PASSED, including the new regressions.
  • Old behaviour reproduced: ZipFile.CreateFromDirectory over the same files with changed mtimes gave two different SHA-256 values.
  • Real Release archives run 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 four tar.gz differed (PaxHeaders.<pid> in the tar stream). After the change (GNU format): all five archives identical across both runs (linux-x64.tar.gz e191f4c2..., win-x64.zip f2c6fbed...; same values again in a second full session). tools/PostgreSqlMcp.Build -- package run twice with different TZ: the npm .tgz is byte-identical (53f1367d...); the .nupkg is 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.
  • Mutation check: switching CreateTarGz back to PAX makes release-regressions fail with "tar.gz archives must not contain process-ID-named PAX headers."
  • Read-only public data, v0.4.0: GitHub API returned 9 assets with sha256: digests; the parser plus planner treated the real nupkg, SHA256SUMS, bom.json as 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 .nupkg differs by bytes from the GitHub asset (2,089,614 vs 2,076,524 bytes) yet ComparePayload reports 0 differences; against registry 0.3.3 it reports 9.
  • Real CLI release-tool nuget --context with 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:

  • Docker PostgreSQL 17 fixture not run locally (emulated PG16 used); hosted CI not observed.
  • The GitHub upload path itself (gh release create/upload/download with real tokens) and the live dotnet nuget push path 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 reporting digest for freshly uploaded assets, the gh release download --pattern fallback, the new nuget job step with the OIDC API key and artifact download, and nuget.org's conflict behaviour.
  • actionlint was not available; release.yml and dependabot.yml were only YAML-parsed. Dependabot's dotnet-sdk ecosystem behaviour is unobserved until Dependabot runs.

Authorship

Written by an AI coding agent at the maintainer's request; the agent-authored label is applied (and ci).

Checklist

  • Title follows the commit format in docs/git-workflow.md
  • Affected docs, help and CHANGELOG.md updated
  • No credentials, real connection strings, customer SQL or database results anywhere in the change or this description
  • Workflow changes (if any) pin actions to full commit SHAs and keep least-privilege permissions

@codegiveness codegiveness added agent-authored Written by an AI agent; review accordingly ci Workflows and repository automation labels Oct 10, 2026
…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.
@codegiveness

Copy link
Copy Markdown
Owner Author

Rebased onto main 47416d4. #31 added commit-message: build blocks to .github/dependabot.yml, which conflicted. Resolution: keep #31's docker block and add the dotnet-sdk entry with the same build/scope commit-message settings. Everything else auto-merged.

After the rebase, all of these passed: build (0 warnings), Verify -- release-regressions, Verify -- sarif-regressions, and gitleaks on origin/main..HEAD (no leaks). The YAML parses with ecosystems github-actions, nuget, docker, dotnet-sdk. Integration was not rerun after the rebase because main gained only docs/config files.

@codegiveness

Copy link
Copy Markdown
Owner Author

Hosted CI observed on head fe659e2: all checks pass (only the push-only analyze is skipped). The verify job ran Verify -- integration against the Docker PostgreSQL fixture on the GitHub runner (ALL MCP INTEGRATION SCENARIOS PASSED) and Verify -- packages (Package verification passed): https://github.com/codegiveness/postgresql-sharp-mcp/actions/runs/38060607578/job/114237926270. This closes the "Docker PostgreSQL 17 fixture not run locally" gap noted above. Native installation passed on macOS and Windows.

# Conflicts:
#	CHANGELOG.md
#	tools/PostgreSqlMcp.Release/Program.cs
# Conflicts:
#	CHANGELOG.md
#	tools/PostgreSqlMcp.Release/Program.cs
#	tools/PostgreSqlMcp.Verify/PostgreSqlMcp.Verify.csproj
@codegiveness

Copy link
Copy Markdown
Owner Author

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.

@codegiveness
codegiveness marked this pull request as ready for review October 10, 2026 19:26
@codegiveness
codegiveness merged commit a3f8b71 into main Oct 10, 2026
21 checks passed
@codegiveness
codegiveness deleted the ci/release-integrity branch October 10, 2026 19:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent-authored Written by an AI agent; review accordingly ci Workflows and repository automation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant