Make package publication retries safe#783
Merged
Merged
Conversation
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>
thomashoneyman
commented
Jul 22, 2026
| 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 |
Member
Author
There was a problem hiding this comment.
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.
thomashoneyman
commented
Jul 22, 2026
| pure $ reply $ Right unit | ||
| failWrite <- consumeFailure FailMetadataWrite env.failurePlan | ||
| if failWrite then do | ||
| pure $ reply $ Left "Injected metadata write failure." |
Member
Author
There was a problem hiding this comment.
The failure ref is just so we can inject errors like this into the evaluation and see how it propagates out.
f-f
approved these changes
Jul 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #645 by making package publication retries safe when publication is interrupted between writing:
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:
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:
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:
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: