Skip to content

Make package publication retries safe#783

Merged
thomashoneyman merged 3 commits into
masterfrom
fix/645-safe-publication-retries
Jul 22, 2026
Merged

Make package publication retries safe#783
thomashoneyman merged 3 commits into
masterfrom
fix/645-safe-publication-retries

Conversation

@thomashoneyman

@thomashoneyman thomashoneyman commented Jul 22, 2026

Copy link
Copy Markdown
Member

Closes #645 by making package publication retries safe when publication is interrupted between writing:

  1. the package tarball to storage,
  2. package metadata to the registry repository, and
  3. the manifest to the registry index.

We don't delete any artifacts after a downstream failure (since we've historically treated them as immutable). Instead, we do forward reconciliation: verify and reuse the existing tarball, repair missing metadata or manifest state, and reject any conflicting artifact or changed source.

Problem

Publication spans several independent systems and we can't do atomic commits. Previously, if the tarball upload succeeded but writing metadata or the registry-index manifest failed, retrying publication encountered an existing storage object and stopped. This left the package in a permanently incomplete state that required manual repair.

Approach

Reconcile existing storage

When an upload fails, we first check whether the requested package version actually made it into storage or not. If it does, the stored tarball is downloaded and verified against the hash and size of the newly produced tarball:

  • If they match, the artifact is reused and publication continues.
  • A missing, corrupt, or mismatched artifact is rejected and we error out (so we can still end up needing manual repair)
  • Existing storage never gets overwritten.

This also handles uploads that succeeded (the package was written) but returned an error to the caller.

Repair incomplete downstream state

When metadata already contains the package version, then on publish we verify the stored tarball using the immutable hash and size recorded in metadata.

If the registry-index manifest is missing, the manifest embedded in the stored tarball must match the manifest produced from the current source. This prevents a moved or modified Git ref from changing an already-uploaded release.

Once verified, publication idempotently reissues both Git writes. Reissuing both writes is intentional. A failed push can leave a local checkout with a commit that has not reached its remote; an idempotent write gives the repository effect another opportunity to push it.

Preserve immutability

Retries fail instead of reconciling in these cases:

  • storage contains a tarball with different bytes or hash,
  • the current source manifest differs from the manifest in the stored tarball, or
  • the registry index already contains a different manifest for the package version.

A package version is only treated as fully published once both metadata and its registry-index manifest are present.

Why forward reconciliation instead of rollback?

Basically, we treat the tarball as an immutable publication artifact; once it has been verified, completing the missing downstream writes is safer and more reliable than deleting and recreating it.

This makes retries convergent:

no durable state
  -> storage
  -> storage + metadata
  -> storage + metadata + manifest

Each retry either advances toward the complete state or rejects a conflicting immutable artifact, in which case something has gone horribly wrong and we have to go fix it.

Tests

I updated our Run-based testing to go through each of these conditions:

  • successful publication,
  • an upload that completes but reports failure,
  • metadata failure leaving storage-only state,
  • manifest failure leaving storage and metadata state,
  • successful retry through each incomplete state,
  • corrupt stored tarball rejection,
  • changed source manifest rejection,
  • conflicting pre-existing tarball rejection, and
  • final convergence to complete storage, metadata, and manifest state.

thomashoneyman and others added 3 commits July 22, 2026 15:01
Amp-Thread-ID: https://ampcode.com/threads/T-019f8a43-a415-7373-9d19-72aed17834d0

Co-authored-by: Thomas Honeyman <admin@thomashoneyman.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019f8a43-a415-7373-9d19-72aed17834d0

Co-authored-by: Thomas Honeyman <admin@thomashoneyman.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019f8a43-a415-7373-9d19-72aed17834d0

Co-authored-by: Thomas Honeyman <admin@thomashoneyman.com>
runGitHubCacheMemory :: forall r a. CacheRef -> Run (GITHUB_CACHE + LOG + EFFECT + r) a -> Run (LOG + EFFECT + r) a
runGitHubCacheMemory = Cache.interpret GitHub._githubCache <<< Cache.handleMemory

runCompilerCacheMock :: forall r a. Run (COMPILER_CACHE + LOG + r) a -> Run (LOG + r) a

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ran some timings and this was really slow, especially in the retry scenarios I added. Instead, we can just use a tmpdir and run this the same as production.

pure $ reply $ Right unit
failWrite <- consumeFailure FailMetadataWrite env.failurePlan
if failWrite then do
pure $ reply $ Left "Injected metadata write failure."

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The failure ref is just so we can inject errors like this into the evaluation and see how it propagates out.

@thomashoneyman
thomashoneyman merged commit 1f66591 into master Jul 22, 2026
7 checks passed
@thomashoneyman
thomashoneyman deleted the fix/645-safe-publication-retries branch July 22, 2026 17:08
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.

Roll back package uploads on metadata failure

2 participants