Skip to content

Catalog: Glacier rehydration UX in the preview - #4921

Merged
fiskus merged 88 commits into
masterfrom
max/glacier-rehydration-ux
Jun 10, 2026
Merged

Catalog: Glacier rehydration UX in the preview#4921
fiskus merged 88 commits into
masterfrom
max/glacier-rehydration-ux

Conversation

@fiskus

@fiskus fiskus commented May 27, 2026

Copy link
Copy Markdown
Member

Summary

Replaces the dead-end "Object Archived — Preview not available" message with a rehydration flow for Glacier (GLACIER) / Deep Archive (DEEP_ARCHIVE) objects.

  • Restore state is read from the x-amz-restore HEAD header. "Archived" now means effectively archived — a restored object previews normally and re-enables the toolbar.
  • RehydrateDialog: pick a tier (Standard / Bulk / Expedited) and duration (1–90 days, validated). Expedited is offered only when the storage class resolves to GLACIER (hidden for DEEP_ARCHIVE and for unknown / HEAD-less classes, which fall back to Standard / Bulk). Errors show inline.
  • ArchivedMessage: archived → restoring → restored. No background polling — while restoring it asks the user to refresh the page to check progress; the state is held optimistically after a 202.
  • Wired through all file views (standalone, packaged, embed).
  • A per-bucket ui.actions.restore BucketPreference (default on) controls whether the Rehydrate button is shown, mirroring the other ui.actions.* toggles. The button is also hidden from unauthenticated users — restore is billable and mutates objects, so it's authenticated-only, like download/zip. Hiding it is UI-only — it neither grants nor removes access; the user's IAM role does.
  • The x-amz-restore CORS expose-header (s3-proxy/nginx.conf) the browser HEAD needs to read restore state was already merged in s3-proxy: expose x-amz-restore header via CORS #4953 — s3-proxy is a separately built/deployed component, so this PR stays pure catalog scope.

Implementation notes

  • The restore runs through a new restoreObject GraphQL mutation under the user's role credentials, replacing the old direct s3.restoreObject SDK call. Per-user IAM is preserved. Typed errors map to UI affordances via interpretResult: RestoreAlreadyInProgress → in-progress flip, RestoreAccessDenied → inline "ask your admin to enable rehydration" IAM hint, InvalidObjectState → calm "not archived / already restored" message, GlacierExpeditedUnavailable → suggest Standard/Bulk, BucketNotFound / ObjectNotFound → calm "no longer exists" copy.
  • Restore state in listings, the version popover, and the gallery comes from S3's per-object RestoreStatus (requested via OptionalObjectAttributes), so a restored object reads as not archived there too — at no extra request cost, since the status rides along in the existing LIST call. The file-detail page additionally reads the live x-amz-restore HEAD header.

Test plan

  • npm run lint, tsc --noEmit, npm run test:only (specs: glacier, restoreObject, RehydrateDialog, ArchivedMessage, BucketPreferences)
  • Manual: archive a GLACIER / DEEP_ARCHIVE object, confirm Rehydrate → restoring → restored
  • Manual: button hidden when ui.actions.restore: false, shown by default
  • Manual: verify the 403 path with a role lacking s3:RestoreObject
1  list archived
2  file archived
3  rehydration dialog
4  restore in progress

Greptile Summary

This PR introduces a full Glacier/Deep Archive rehydration UX in the Quilt catalog preview — replacing the dead-end "Object Archived" message with an interactive restore flow backed by a new restoreObject GraphQL mutation, optimistic in-progress state, and per-bucket ui.actions.restore preference.

  • ArchivedMessage + RehydrateDialog: New components drive the archived → restoring → (reload to confirm) UX; rehydration is gated on both authentication and the per-bucket ui.actions.restore preference. The RestoreAlreadyInProgress case (previously flagged as a dead end) now correctly flips optimisticRestoring via { _tag: 'close', flip: true }.
  • glacier.ts: Centralised helper parses x-amz-restore headers and RestoreStatus LIST elements into a unified { restoring, archived } shape; all five object-access paths (listing, file detail, package tree, embed, search results) and the preview gate now use it.
  • Schema + preferences: restoreObject mutation added to the shared GraphQL schema; ui.actions.restore toggle wired through BucketPreferences, the config editor, and JSON schema.

Confidence Score: 5/5

Safe to merge — the rehydration flow is well-contained, all object-access paths are consistently updated, and the previously flagged RestoreAlreadyInProgress dead-end is now correctly handled.

All five object-access paths uniformly delegate to getArchiveState, which is thoroughly unit-tested. The RestoreAlreadyInProgress path now correctly flips optimisticRestoring via { _tag: 'close', flip: true }. Auth and preference gates are independently tested, including the new anon-hidden case. No regressions detected in the existing preview or listing logic.

No files require special attention. The CORS expose-header dependency (#4954) is acknowledged and ships separately; until then, x-amz-restore will be absent from HEAD responses but the feature degrades gracefully.

Important Files Changed

Filename Overview
catalog/app/utils/glacier.ts New utility centralising Glacier restore-state parsing from both HEAD x-amz-restore string and LIST RestoreStatus object; edge cases (malformed header, missing expiry, expired copy) handled correctly and well-covered by specs.
catalog/app/components/Preview/ArchivedMessage.tsx New component managing optimistic restore state with auth + pref gates; RestoreAlreadyInProgress path correctly flips optimisticRestoring via onSubmitted(); optimistic hold clears on object navigation via effect.
catalog/app/components/Preview/RehydrateDialog.tsx Dialog form for tier/days selection; interpretResult cleanly separates pure outcome mapping from imperative submit; Expedited gated on storageClass === 'GLACIER'; all OperationError variants mapped to friendly messages with correct flip/close semantics.
catalog/app/components/Preview/Display.jsx Replaced static 'Object Archived' message with ArchivedMessage render-prop pattern; Rehydrate action gated on !noDl (consistent with download/toolbar gating); passes archive info through correctly.
catalog/app/containers/Bucket/requests/requestsUntyped.js getObjectExistence now calls getArchiveState on the HEAD response; archived field carries `StorageClass
catalog/app/containers/Bucket/PackageTree/PackageTree.tsx ObjectAttrs interface updated to `archived: StorageClass
catalog/app/containers/Bucket/requests/bucketListing.ts Adds OptionalObjectAttributes: ['RestoreStatus'] to listObjectsV2 so restored copies surface as non-archived in listings; delegates archived calculation to getArchiveState.
shared/graphql/schema.graphql Adds GlacierRestoreTier enum, RestoreObjectSuccess type, RestoreObjectResult union, and restoreObject mutation with correct nullable version and required tier/days args.
catalog/app/utils/BucketPreferences/BucketPreferences.ts Adds restore to ActionPreferences type and defaults it to true, consistent with the other ui.actions.* toggles.
catalog/app/components/Preview/restoreObject.ts Thin hook wrapping the restoreObject GraphQL mutation; maps RetrievalTier to the schema enum; returns raw mutation union for the caller to branch on.

Sequence Diagram

sequenceDiagram
    participant User
    participant ArchivedMessage
    participant RehydrateDialog
    participant GraphQL as restoreObject mutation
    participant S3

    User->>ArchivedMessage: views archived object
    Note over ArchivedMessage: reads x-amz-restore header
    ArchivedMessage->>User: Object Archived + Rehydrate btn

    User->>ArchivedMessage: click Rehydrate
    ArchivedMessage->>RehydrateDialog: open(handle, storageClass)
    User->>RehydrateDialog: pick tier + days, submit

    RehydrateDialog->>GraphQL: restoreObject(bucket, key, version, tier, days)
    GraphQL->>S3: s3.restoreObject under user IAM role
    S3-->>GraphQL: 202 Accepted

    GraphQL-->>RehydrateDialog: RestoreObjectSuccess(alreadyRestored:false)
    RehydrateDialog->>ArchivedMessage: onSubmitted() flip:true
    RehydrateDialog->>RehydrateDialog: onClose()

    ArchivedMessage->>User: Restore in progress (optimistic)

    Note over User,S3: Hours later - user reloads page
    User->>ArchivedMessage: page reload
    ArchivedMessage->>S3: HEAD x-amz-restore
    S3-->>ArchivedMessage: object available
    ArchivedMessage->>User: Preview loads normally
Loading

Reviews (9): Last reviewed commit: "Revert "TEMP: deploy catalog to ECR from..." | Re-trigger Greptile

fiskus added 13 commits May 26, 2026 17:40
Replace the dead-end "Object Archived" message with a two-click rehydrate
flow: a dialog lets the user pick a retrieval tier (Standard/Bulk/Expedited)
and a 1-90 day duration, and the preview surface tracks restore state via
the S3 HEAD `x-amz-restore` header. No active polling; manual "Check status"
plus an optimistic in-progress hold after S3 accepts a 202.

The HEAD-derived `archived` flag now means "effectively archived" — a
restored-but-still-Glacier object loads its preview normally and re-enables
the file toolbar. LIST-derived sites (directory listings, version popover,
summary gallery) intentionally stay raw to avoid per-row HEAD storms;
NOTE comments document the gap.

The new `restoreObject` request currently goes via the AWS SDK with a
TODO marker to migrate to GraphQL once a server-side mutation lands.
- Replace the tier radio group with a native-backed select; lay the tier
  select and duration input side by side, with the per-tier timing/cost
  hint moved into the select's helper text.
- Shorten the duration label to "Duration (days)" and move the explanation
  into helper text.
- Surface restore errors inline as an Alert (not just a toast) so the
  AccessDenied/IAM hint is visible without leaving the dialog.
- Clarify the intro copy: rehydrating makes the archived object temporarily
  downloadable and never creates a new version or loses the object.
Thread an optional storageClass through the existing PreviewError.Archived
payload (alongside restore) so RehydrateDialog can drop the Expedited
retrieval option for DEEP_ARCHIVE objects, where it isn't supported. When
storageClass is absent (embed/search surfaces don't always plumb it) the
dialog falls back to showing all tiers — no regression.

Type notes:
- storageClass reuses the SDK's S3.StorageClass — we only consume and
  compare it, so the type's `| string` widening costs nothing and documents
  provenance.
- GlacierTier keeps its own literal union (we produce these values and want
  narrowing, which S3.Tier's `| string` member can't provide); add a comment
  explaining the deliberate divergence.
Tighten the comments added for the rehydration feature without losing
meaning, and drop a self-explanatory one on the error fallback.
- restore.ts: compose isEffectivelyArchived from isArchiveStorageClass +
  hasLiveRestoredCopy so the body reads for itself.
- requestsUntyped.js: reuse the shared isEffectivelyArchived instead of a
  duplicated inline archive/restore check and local helper.
- RehydrateDialog: extract clampDays(n) (returns the clamped string),
  dropping the inline clamp + comment.
parseRestoreHeader / isEffectivelyArchived / RestoreStatus are generic S3
HEAD helpers with no React or Preview dependency, but they lived in
components/Preview/loaders/, so requestsUntyped.js imported UP into a
component — an inverted dependency. Move restore.ts (+ spec) next to
object.ts in containers/Bucket/requests/; Preview now imports DOWN into
requests, matching every other module.
The embed view showed the Rehydrate button but had no onReload/resetKey, so
"Check status" never appeared and the view couldn't refresh after a submit.
Add resetKey state + handleReload (mirroring File.jsx): thread resetKey into
both getObjectExistence calls and previewOptions (useGate cache bust), and
pass handleReload as onReload. Embed now refreshes like the main file view.
The four HEAD-throwing surfaces (File, embed/File, SearchResults, PackageTree)
each built Archived({ handle, restore, storageClass }) by hand — duplication
that already let embed diverge. Add a typed Preview.archivedError(handle, src)
helper (typed ArchivedSource inputs) so every call site stays consistent and
can't forget a field.
useGate is the fifth HEAD-throwing surface; route its Archived construction
through Preview.archivedError like the other four, so no call site builds the
payload by hand. Imported via ../archivedError (not the Preview index) to
avoid the index -> load.jsx -> loaders cycle.
useGate's resetKey only ever busted gate's Data.use cache for a case that
can't occur in the rehydration flow: the parent's getObjectExistence archived
short-circuit always fires before Preview.load, so for an archived object the
gated loader never mounts; on reload the parent refetches and the loader
mounts fresh, re-running its HEAD. So resetKey threaded through useGate /
ECharts / Markdown / previewOptions was dead — remove it, restoring gate to a
pure function. The parent's getObjectExistence resetKey (which actually drives
reload) stays. Breadcrumbs added at the archived short-circuits noting the
loader-remount coupling reload depends on.
Replace the <Data> render-prop (and the awkward renderWithReload inner
function that captured `fetch` and needed a no-use-before-define disable)
with the useData hook, matching File.jsx and embed. handleReload is now the
same one-liner resetKey bump the siblings use. Net change is small
(+10/-20 ignoring the reindent from de-nesting AsyncResult.case).
Stop snapping the duration field on every keystroke (pasting 120 jumped to
90 mid-typing). handleDaysChange now stores the raw value; the existing
daysValid check still gates submission — disabled button + "Enter a value
between 1 and 90" helper text — so out-of-range input is caught before
submit without fighting the user. Drop the unused clampDays helper.

Tests reframed from clamp assertions to the validation matrix (empty,
below min, above max, non-integer, in-range), which also guards against
re-introducing clamp-on-change.
@codecov

codecov Bot commented May 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.58537% with 22 lines in your changes missing coverage. Please review.
✅ Project coverage is 46.70%. Comparing base (413c539) to head (0b42153).

Files with missing lines Patch % Lines
catalog/app/components/Preview/RehydrateDialog.tsx 91.30% 8 Missing ⚠️
catalog/app/components/Preview/restoreObject.ts 12.50% 7 Missing ⚠️
catalog/app/components/Preview/Display.jsx 0.00% 3 Missing ⚠️
catalog/app/containers/Bucket/File/File.jsx 0.00% 2 Missing ⚠️
.../app/containers/Bucket/PackageTree/PackageTree.tsx 0.00% 1 Missing ⚠️
catalog/app/embed/File.jsx 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #4921      +/-   ##
==========================================
+ Coverage   46.51%   46.70%   +0.19%     
==========================================
  Files         832      837       +5     
  Lines       34094    34247     +153     
  Branches     5833     5891      +58     
==========================================
+ Hits        15858    15995     +137     
- Misses      16237    16253      +16     
  Partials     1999     1999              
Flag Coverage Δ
api-python 93.14% <ø> (ø)
catalog 22.01% <86.58%> (+0.46%) ⬆️
lambda 96.63% <ø> (ø)
py-shared 98.02% <ø> (ø)

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.

@fiskus
fiskus marked this pull request as ready for review May 27, 2026 12:04
Comment thread catalog/app/containers/Bucket/PackageTree/PackageTree.tsx
A RestoreAlreadyInProgress (409) closed the dialog with a toast but never
reloaded, leaving ArchivedMessage idle ("Rehydrate" button) while a restore
was actually running — re-clicking just produced another 409. Treat it like
a fresh 202: call onSubmitted(false) so the message flips to "Restore in
progress" and refetches. (greptile PR review finding.)
@fiskus

fiskus commented May 27, 2026

Copy link
Copy Markdown
Member Author

@greptileai the 409 "already in progress" finding is addressed in ed36473: the RestoreAlreadyInProgress branch now calls onSubmitted(false), so ArchivedMessage flips to "Restore in progress" and refetches instead of staying idle (matching your suggested fix). Please re-review and update the confidence score.

@fiskus
fiskus marked this pull request as draft May 27, 2026 13:16
fiskus added 11 commits May 27, 2026 17:22
Map the new InvalidObjectState OperationError to ObjectNotArchivedError and
surface it without logging to Sentry, instead of falling through to the generic
'Failed to start restore' path.
Adds max/glacier-rehydration-ux to deploy-catalog.yaml on.push.branches so CI
builds a catalog image for the glacier.dev.quilttest.com test stack. Revert
before merge.
The mock fixtures were missing the top-level __typename: 'Mutation' (and
__typename: 'InputError'), which vitest ignores but the webpack/tsc build
rejects. Add them so the catalog build type-checks.
Match the codebase pattern (BaseError → BucketError → specific errors used
across containers/Bucket/errors.tsx): drop the manual setPrototypeOf and
this.name = '…' boilerplate from the four restore-related errors, since
BaseError already handles the prototype chain, captureStackTrace, and the
dynamic name getter. Class names and instanceof semantics are unchanged, so
RehydrateDialog catches and the GraphQL interpretRestoreResult dispatch
work without modification.
The catalog's archived/restore detection (parseRestoreHeader, isEffectivelyArchived)
reads the x-amz-restore HEAD response header to distinguish ongoing/expired
restores from cold-archived objects. The s3-proxy was hard-coding the
Access-Control-Expose-Headers list (and stripping S3's own) without including
x-amz-restore, so browsers received the header but JS couldn't read it —
rehydrated objects stayed showing 'Object Archived' instead of flipping to
'Restore in progress'. Add the header to the expose list.
fiskus added 3 commits June 9, 2026 13:35
getArchiveState returned the full parsed RestoreStatus (ongoing +
expiresAt), but the only thing any caller ever read was ongoing; expiresAt
was consumed solely inside hasLiveRestoredCopy to derive `archived`.
Return a `restoring` boolean instead and keep RestoreStatus (and expiresAt)
module-private. ArchiveInfo and the Exists payload now carry `restoring`
rather than a RestoreStatus object, so nothing unrendered is threaded to
the UI.

Addresses nl0's review comment on PR #4921.
Two more ObjectAttrs consumers in PackageTree's FileDisplay still
destructured/passed `restore`; rename them to `restoring`. Also fix the
PreviewError.Archived doc comment in types.js to match the ArchiveInfo
shape ({ storageClass, restoring }).
@nl0

nl0 commented Jun 9, 2026

Copy link
Copy Markdown
Member

Thanks @fiskus — verified the four review items, all resolved cleanly:

  • Q1 fail-safe — Expedited is now offered only when storageClass === 'GLACIER', so HEAD-less / unknown-class paths fall back to Standard/Bulk ✅
  • Q2 onSubmitted collapsed to no-arg ✅
  • Q3 RestoreResult named at its source ✅, and RestoreStatus/expiresAt confined to glacier.ts (good call keeping the load-bearing expiry private rather than surfacing it) ✅

A few follow-ups before this is good to go:

  1. Q4 — explicit disposition, please. The one item from my review still without a verdict: ensureObjectIsPresent (requestsUntyped.js) now passes the new field through its returned handle via { ...h }. Harmless at runtime — pure type-hygiene. Totally fine to wontfix; I just want it to be a deliberate call rather than slipping through unnoticed.

  2. Split the s3-proxy change into its own PR. s3-proxy/nginx.conf (the x-amz-restore CORS expose-header) is a separately built and deployed component from the catalog bundle. Pulling it into its own PR makes it independently reviewable, revertable, and deployable — and keeps this PR purely catalog scope.

  3. Title scope prefix. Consider renaming to Catalog: Glacier rehydration UX in the preview to match the component-scoped convention.

  4. Description ↔ changeset drift (minor):

    • The Summary still says "Expedited is hidden for DEEP_ARCHIVE" — post-fail-safe it's now "offered only when the class resolves to GLACIER" (so also hidden for unknown / HEAD-less class). Worth a one-line update.
    • The auto-generated Greptile section still references the now-removed onSubmitted(alreadyRestored) parameter and the onSubmitted(false) 202 flip in its sequence diagram — stale after the Q2 collapse; a re-trigger would refresh it.
    • The diff also carries lambdas/shared + lambdas/tabular_preview dependency bumps that aren't in master and look unrelated to Glacier — worth confirming they belong here vs landing on their own.

Otherwise this looks good. ✅

nl0 added a commit that referenced this pull request Jun 9, 2026
The catalog file page reads an object's Glacier / Deep Archive restore
state from the x-amz-restore HEAD response header; browsers only surface
that header cross-origin when it's listed in Access-Control-Expose-Headers.

Split out of #4921 (catalog Glacier rehydration UX) so this s3-proxy image
change is independently reviewable and deployable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@nl0

nl0 commented Jun 9, 2026

Copy link
Copy Markdown
Member

Re: the s3-proxy split (point 2 above) — done, I pulled it into #4954 (branched off master, single nginx.conf line). You can drop the s3-proxy/nginx.conf change from this branch and #4921 becomes pure catalog scope. Heads-up for the deploy PR: the s3-proxy image pin should then track #4954's merge rather than this branch.

@fiskus fiskus changed the title Glacier rehydration UX in the catalog preview Catalog: Glacier rehydration UX in the preview Jun 9, 2026
fiskus added 2 commits June 9, 2026 18:25
Splits the s3-proxy CORS change out of the catalog PR (#4921) so this
branch is pure catalog scope. The x-amz-restore expose-header now ships
in #4954, branched off master.
getObjectExistence now yields a `restoring` flag, but ensureObjectIsPresent
returns a plain S3 handle to summarize/README consumers that never read it.
Drop it in the destructure alongside deleted/archived so the handle keeps
its pre-existing shape.
@fiskus

fiskus commented Jun 9, 2026

Copy link
Copy Markdown
Member Author

Thanks — all four addressed:

  1. Q4 — ensureObjectIsPresent. Stripped rather than wontfixed (506fbbf95): destructuring restoring out too means we return the deliberate object handle summarize/README consume, not whatever falls out of the rest spread.

  2. s3-proxy split. Dropped nginx.conf (0448c00e2); x-amz-restore now lives solely in S3 Proxy: expose x-amz-restore response header via CORS #4954, so this PR is pure catalog scope.

  3. Title. Renamed to Catalog: Glacier rehydration UX in the preview.

  4. Description drift. Expedited reworded to "offered only when the class resolves to GLACIER"; nginx.conf bullet now points at S3 Proxy: expose x-amz-restore response header via CORS #4954; lambdas bumps already gone from the branch. Re-triggering Greptile to refresh the stale onSubmitted section.

@fiskus

fiskus commented Jun 9, 2026

Copy link
Copy Markdown
Member Author

@greptileai please re-review — the onSubmitted(alreadyRestored) param was collapsed to a no-arg onSubmitted and the s3-proxy nginx.conf change was split out to #4954, so the summary/sequence diagram are stale. Please refresh.

nl0
nl0 previously approved these changes Jun 9, 2026

@nl0 nl0 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approving — all review feedback is addressed and verified against 506fbbf9.

  • Q1 Expedited fail-safe (offered only when class resolves to GLACIER), Q2 onSubmitted collapsed to no-arg, Q3 RestoreResult named at source + RestoreStatus/expiresAt confined to glacier.ts — all confirmed.
  • Q4 ensureObjectIsPresent now destructures restoring out, so the returned handle is the deliberate object location (fixed, not wontfixed).
  • s3-proxy nginx.conf split out to the now-merged #4953 → this PR is pure catalog scope; retitled Catalog:; description refreshed.

The IAM-as-sole-authority model is clean, ui.actions.restore is pattern-conformant across every BucketPreferences layer, no per-row HEAD storm (archive state rides the existing LIST via OptionalObjectAttributes), and the mutation union is handled exhaustively via interpretResult. 🚢

fiskus added 2 commits June 10, 2026 13:16
Restore is billable and mutates objects, so hide the CTA from anonymous
users (mirrors the download/zip gate in FileView). Covered by two new
anon-gate tests.
Add the feature branch to deploy-catalog's push trigger so its catalog
image is built and pushed to ECR for pre-merge review. Revert before
merging to master.
@fiskus

fiskus commented Jun 10, 2026

Copy link
Copy Markdown
Member Author

@greptileai re-review please — added an authentication gate on the Rehydrate button (hidden from anonymous users, mirroring the download/zip gate in FileView), with tests for the anon-hidden and restore-in-progress cases.

@fiskus
fiskus requested a review from nl0 June 10, 2026 12:28

@nl0 nl0 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Re-approving after the stale-dismiss — the only PR-owned change since my last approval is ae639acb (gate Rehydrate button on authentication), which I reviewed: canRestore = authenticated && ui.actions.restore using the existing authenticated selector (mirrors the download/zip gate), with tests for the anon-hidden and anon-restoring-status cases. That cleanly resolves E1 catalog-side — anon never reaches the mutation. Everything else in the delta is the master merge (incl. the now-merged #4953). 🚢

@nl0

nl0 commented Jun 10, 2026

Copy link
Copy Markdown
Member

Tiny description nit (not code): the s3-proxy bullet in the PR body points at #4954, which is my closed duplicate — the x-amz-restore CORS change actually shipped in #4953, already merged to master. Could you repoint it to #4953 and reword "ships separately … fully live once #4954 is deployed" → "already merged in #4953"? (The auto-generated Greptile section echoes the same #4954, but that should correct itself on a re-trigger.)

@fiskus

fiskus commented Jun 10, 2026

Copy link
Copy Markdown
Member Author

Repointed to #4953 and reworded to "already merged" — thanks for catching the duplicate. Left the auto-generated Greptile section as-is; the re-trigger I just requested should refresh its #4954 reference.

@fiskus
fiskus added this pull request to the merge queue Jun 10, 2026
Merged via the queue into master with commit bb1b520 Jun 10, 2026
45 checks passed
@fiskus
fiskus deleted the max/glacier-rehydration-ux branch June 10, 2026 14:29
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.

3 participants