Skip to content

Publish the retained candidate on failed-jobs publication retries - #62

Merged
deku2026 merged 1 commit into
mainfrom
fix/publication-retry-identity
Sep 25, 2026
Merged

deku2026 merged 1 commit into
mainfrom
fix/publication-retry-identity

Conversation

@deku2026

Copy link
Copy Markdown
Contributor

Fixes the publication failure of run 36116514219 (fd2eb8e). A failed-jobs-only retry of Publish NuGet was rejected with Candidate version does not match publisher identity.

Root cause (verified)

  • Allocation. preflight allocates the main candidate version once, before build, as 1.0.0-ci.<run number>.<attempt>. The packaging README documents that "retrying only failed downstream jobs retains the already allocated version and producer artifact".
  • The re-derivation. Since 6b1a6d5 (2026-09-21), the publish step ran release_channels.py --expected-version, which re-derived the version from the publish job's own GITHUB_RUN_ATTEMPT and required equality.
    • In run 36116514219, attempt 1 failed on a transient 403 during native legal-source staging. The failed-jobs retry (attempt 2) kept preflight's 1.0.0-ci.24.1, the publisher computed 1.0.0-ci.24.2, and it refused the retained candidate.
    • The job logs confirm PACKAGE_VERSION: 1.0.0-ci.24.1 in attempt 2.
  • Regression, not new behavior. Before 6b1a6d5 the same retry path worked. Run 35554989640 (6cb0899) had a failed-jobs-only attempt 2: preflight was carried over from attempt 1, and it published the retained 1.0.0-ci.17.1.
  • Why it stayed hidden. Every publication after 6b1a6d5 succeeded in attempt 1, and the selection test covered attempt 1 only.
  • Configuration. None was involved: the nuget environment admitted the job, and the failure is the tool's own check.

Fix

  • release_channels.py: allocation is unchanged. A full re-run still allocates …​.<new attempt> and builds a new candidate.
  • Publication authorization no longer re-derives a version.
    • A main candidate must carry this run number and an allocation attempt ≤ the current attempt. Stable tags keep exact equality.
    • The downloaded manifest.json must name this commit and version, and a producing attempt of this run between the allocation and the current attempt.
    • The artifact name must be that producer's nuget-candidate-<run-id>-<attempt>.
    • Artifacts from another run, attempt or candidate, and unbound or "latest" names, are rejected. packages.py verify still rechecks every package and hash.
  • Unchanged. There is still no --skip-duplicate: an existing version fails, and partial uploads follow the documented full re-run.
  • publish-nuget.yml passes the manifest and artifact name into the authorization.
  • Docs. The packaging README and docs/dependency-policy.md now document the retry contract. They also note that a re-run executes the original commit's workflow, so this fix applies to runs of later commits.

Validation

  • python -m unittest discover -s eng -p test_dependency_policy.py: 14 tests pass. The two new tests cover:
    • initial execution;
    • a failed-jobs-only retry with a retained or repacked artifact;
    • a full re-run with a new allocation;
    • stable tags across attempts;
    • rejection of 10 malformed or foreign versions, a wrong tag version, and 9 manifest or artifact mismatches.
  • The real command line was exercised offline with a synthetic push event:
    • main's module rejects the attempt-2 retry of 1.0.0-ci.24.1 exactly as in production.
    • The fixed module authorizes it, runs the dependency audit and emits version=1.0.0-ci.24.1.
    • A missing manifest and a wrong artifact are rejected.
  • eng/packaging tests need a packed candidate and run in CI's pack job. They error identically on unmodified main without artifacts/packages.
  • Not exercised: a live failed-jobs retry of a publication. No extra publication is triggered solely for verification. This merge's own publish run exercises the normal attempt-1 path.

🤖 Generated with Claude Code

Root cause: since 6b1a6d5 the publish job re-derived the version from its
own GITHUB_RUN_ATTEMPT (release_channels.py --expected-version) and
required it to equal the candidate version. A failed-jobs-only re-run
keeps the preflight allocation and producer artifact from the earlier
attempt (the documented retry contract), so the publisher computed
1.0.0-ci.24.2 for the retained 1.0.0-ci.24.1 candidate and refused it
(run 36116514219, fd2eb8e). Before 6b1a6d5 the same retry path published
the retained 1.0.0-ci.17.1 (run 35554989640); every publication since
6b1a6d5 succeeded in attempt 1, so the path went unexercised and the
selection test covered attempt 1 only. No repository or environment
configuration was involved.

- release_channels.py keeps allocation unchanged and adds publication
  authorization: a main candidate must carry this run number and an
  allocation attempt no later than the current attempt; stable tags keep
  exact equality. The downloaded manifest must name this commit and
  version and a producing attempt of this run between the allocation and
  the current attempt, and the artifact name must be that producer's
  nuget-candidate-<run-id>-<attempt>. Other runs, attempts, candidates
  and unbound artifact names are rejected; packages.py verify still
  rechecks every package.
- publish-nuget.yml passes the manifest and artifact name.
- Offline tests cover initial execution, failed-jobs-only retry
  (retained or repacked artifact), full re-run (new allocation), stable
  tags across attempts and rejection of mismatched identities.
- Packaging README and dependency policy document the retry contract.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@deku2026
deku2026 merged commit 48298b4 into main Sep 25, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant