Skip to content

Block a revise that would replace a different package's contents - #5245

Open
QuiltSimon wants to merge 2 commits into
masterfrom
fix/revise-gate-dst-mismatch
Open

Block a revise that would replace a different package's contents#5245
QuiltSimon wants to merge 2 commits into
masterfrom
fix/revise-gate-dst-mismatch

Conversation

@QuiltSimon

@QuiltSimon QuiltSimon commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Found reviewing the 26.8.0 candidate. Pre-existing, and adjacent to #5165's fix rather than caused by it.

useParams gates publishing with manifest._tag !== 'ready' && name.status._tag !== 'new'. But manifest is keyed on src, while params.name comes from dst. So: open the revise dialog on alice/p — its manifest loads ready — then edit the Name field to bob/q, a different existing package. name.status becomes exists, manifest is still ready for alice/p, and the gate passes.

Entries are sent as a complete replacement list, so this publishes alice/p's entry list over bob/q's latest revision. Same class of silent data loss #5165 closed, one condition short.

The fix

useParams now takes src (threaded from State.tsx, which already owns it) and blocks when a loaded manifest does not describe the destination:

if (src && name.status._tag === 'exists' && (dst.bucket !== src.bucket || dst.name !== src.name))
  return Invalid(new ERRORS.DestinationManifestMismatch())

Design notes:

  • Placed after the manifest gate. If the manifest never loaded there is nothing known about src to describe a mismatch against, and SourceManifestNotLoaded is the truthful report. A test pins the ordering.
  • Guarded on src && because plain package creation passes no src; gating on name.status === 'exists' alone would have broken creating into an existing name. A test pins this too.
  • Compared against src explicitly rather than treating exists as implying a mismatch. Both are true today, but the comparison states the invariant honestly instead of relying on that coincidence.
  • Bucket compared as well as name — a same-name/different-bucket destination is the same loss.

New DestinationManifestMismatch error beside SourceManifestNotLoaded, matching its shape. The message points at "load and revise it" — the exact link text Inputs/Name.tsx already renders for the exists state, which calls setSrc and is the real recovery path.

Tests

Six new cases in params.spec.ts. Two are the blocking cases; four guard the permissive side (src===dst revise, dst 'new', manifest-not-ready still reported as SourceManifestNotLoaded, no-src plain creation) so a later widening of the gate fails.

  • TZ=UTC npx vitest run app/containers/Bucket/PackageDialog app/containers/Bucket/errors.tsx — 125 passed (119 on master)
  • npx tsc --noEmit -p . — clean
  • npx oxfmt --check — clean

Regression-verified: restoring master's params.ts fails exactly the two new blocking cases and nothing else.

Known caveat

The new error's message is currently unreachable UI. Create.tsx:129 returns null when manifest._tag !== 'error', and the manifest is ready in this scenario — so the push button is correctly disabled and Inputs/Name.tsx still offers the recovery link, but this specific copy never renders. Wiring StatusMessage to surface it is a separate conceptual unit; flagging rather than folding it in.

The Copy dialog cannot reach this gate at all (disableRestore: true maps exists to new-revision), so Copy behavior is unchanged.

🤖 Generated with Claude Code

Greptile Summary

This PR threads the package source into publish-parameter validation and adds a destination-manifest mismatch error intended to prevent one package's loaded entries from replacing another package. It also adds regression tests and documents the fix.

  • Compares the revise source and destination when the destination is known to exist.
  • Adds permissive and blocking parameter-validation cases.
  • Adds the new bucket error and changelog entry.

Confidence Score: 4/5

This PR should not merge until publishing remains disabled during destination-existence and source-manifest transitions, which currently leave the destructive replacement path reachable.

The new guard blocks the settled mismatch case but skips loading and new-revision; retained ready manifest data can therefore still be submitted to a different existing destination.

Files Needing Attention: catalog/app/containers/Bucket/PackageDialog/State/params.ts, catalog/app/containers/Bucket/PackageDialog/State/params.spec.ts

Important Files Changed

Filename Overview
catalog/app/containers/Bucket/PackageDialog/State/params.ts Adds the central mismatch guard, but its exists-only condition leaves destructive asynchronous transition paths reachable.
catalog/app/containers/Bucket/PackageDialog/State/State.tsx Correctly threads src into parameter validation, although src and the asynchronously loaded manifest are not an atomic identity pair.
catalog/app/containers/Bucket/PackageDialog/State/params.spec.ts Covers settled mismatch states and permissive cases but omits pending existence and stale-manifest source transitions.
catalog/app/containers/Bucket/errors.tsx Adds a specific mismatch error with appropriate recovery guidance.
catalog/CHANGELOG.md Documents the intended prevention of cross-package content replacement.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Manifest for package A is ready] --> B[Destination changes to existing package B]
  B --> C{Asynchronous state}
  C -->|Existence query pending| D[name status: loading]
  C -->|Load and revise clicked| E[src becomes B; status: new-revision]
  D --> F[exists-only mismatch guard skipped]
  E --> G[Old A query data retained while B fetches]
  G --> F
  F --> H[useParams returns Ok]
  H --> I[A entries submitted to destination B]
Loading

Reviews (1): Last reviewed commit: "catalog: block a push whose destination ..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

…describe

useParams gated only on the manifest being ready, but the manifest is keyed on
`src` while the name comes from `dst`. Renaming the destination to a different
existing package left the gate satisfied, and entries ship as a complete
replacement list, so that package's latest revision was overwritten.

Thread `src` into useParams and compare it against `dst`. A genuinely new name
and the plain revise of the opened package are both still permitted.
@codecov

codecov Bot commented Aug 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 34.95%. Comparing base (fe9bda5) to head (878f9e1).

❗ There is a different number of reports uploaded between BASE (fe9bda5) and HEAD (878f9e1). Click for more details.

HEAD has 25 uploads less than BASE
Flag BASE (fe9bda5) HEAD (878f9e1)
lambda 14 0
py-shared 1 0
api-python 10 0
Additional details and impacted files
@@             Coverage Diff             @@
##           master    #5245       +/-   ##
===========================================
- Coverage   55.20%   34.95%   -20.26%     
===========================================
  Files         872      741      -131     
  Lines       36766    23811    -12955     
  Branches     6429     6433        +4     
===========================================
- Hits        20298     8322    -11976     
+ Misses      14724    13745      -979     
  Partials     1744     1744               
Flag Coverage Δ
api-python ?
catalog 34.95% <100.00%> (+0.02%) ⬆️
lambda ?
py-shared ?

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment on lines +83 to +88
if (
src &&
name.status._tag === 'exists' &&
(dst.bucket !== src.bucket || dst.name !== src.name)
) {
return Invalid(new ERRORS.DestinationManifestMismatch())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Async mismatch remains publishable

When a revise destination's existence query is loading, or “load and revise it” updates src while the prior manifest remains ready, this exists-only guard is skipped and useParams accepts the old source entries for the new destination, allowing that existing package's contents to be replaced.

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.

2 participants