Skip to content

feat(ops): prune superseded dataset releases through a manual workflow - #265

Merged
amanthanvi merged 3 commits into
mainfrom
t3code/prune-superseded-releases
Sep 14, 2026
Merged

amanthanvi merged 3 commits into
mainfrom
t3code/prune-superseded-releases

Conversation

@amanthanvi

@amanthanvi amanthanvi commented Sep 14, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Add a manual prune-releases workflow (dry run by default, production environment, shares the update-dataset concurrency group) that removes superseded inactive dataset releases in bounded, retried Convex batches and then sweeps orphaned content blobs and their stored files.
  • Selection is defensive on top of deleteInactiveReleaseBatch (which already refuses active pointers and marks pruning before its first delete): always finish releases already marked pruning; never touch unsealed releases whose declared upload is complete (still activatable); prune incomplete unsealed drafts only behind an opt-in unsealed_min_age_hours; prune sealed legacy or failed-manifest releases regardless of age (activation rejects them); apply min_age_hours (default 24, min 1) to sealed declared releases; retain the newest keep_per_distro (default 1, min 1) verified releases per distro as rollback targets.
  • maintenance:previewInactiveReleases now paginates with a cursor and reports createdAt (server time), sealed, pruning, manifestBasis, manifestVerified, manifestError, and uploadComplete.
  • deleteInactiveReleaseBatch and cleanupOrphanContentBlobsBatch now delete the _storage files behind pruned page contents and orphaned blobs (only when no other blob shares the file).
  • Driver hardening: a failure stops new deletions and fails the run; Convex stderr is surfaced with deploy keys redacted; each batch retries with backoff and batch sizes halve on failure and stay reduced; the whole-table orphan sweep is opt-in for previews.
  • Runbooks, architecture doc, and changelog updated; cutover rollback guidance now points at the workflow for post-incident cleanup and notes the manual /ingest/* paths are not covered by the concurrency group.

Motivation

Every ingest creates a new release while activation and promotion only move pointers, so superseded releases accumulate. After the September 14 re-ingest that unblocked deploys, the legacy September 1 and 3 releases for all seven distros are still stored and can never be re-activated (LEGACY_ALIAS_EXPECTATION_UNKNOWN).

Validation

  • pnpm ops:test: 37 passed (17 for the prune script, including a workflow-wiring contract that checks provenance precedes deploy-key exposure).
  • pnpm convex:test: 121 passed (paginated preview, storage-file deletion for page contents and shared/sole blob files); pnpm convex:typecheck clean.
  • scripts/check-convex-public-api.mjs and scripts/check-osv-exceptions.mjs pass; workflow YAML parses.
  • Two adversarial review passes (independent agents); every confirmed finding from both is addressed in the follow-up commits.

Rollout

Merging deploys the updated internal functions only. Pruning runs only on manual dispatch: first with defaults to preview, then -f apply=true.

Every ingest creates a new release while activation and promotion only move
pointers, so superseded releases accumulate. Add a manual prune-releases
workflow (dry run by default, production environment, shares the
update-dataset concurrency group) driving a Node script that:

- pages through maintenance:previewInactiveReleases,
- never touches unsealed or recent releases,
- always finishes releases already marked pruning,
- retains the newest verified declared release per distro as a rollback
  target and prunes legacy or failed-manifest releases that can never be
  activated again,
- deletes in bounded confirmed batches via deleteInactiveReleaseBatch,
- then sweeps orphaned content blobs.

previewInactiveReleases now paginates with a cursor and reports sealed,
pruning, manifestBasis, manifestVerified, and manifestError so callers can
judge rollback eligibility. Covered by ops tests, a Convex integration test,
and runbook/architecture/changelog updates.
@vercel

vercel Bot commented Sep 14, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
betterman Ready Ready Preview Sep 14, 2026 11:32pm UTC

Request Review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Sorry @amanthanvi, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 1 hour and 13 minutes by commenting @sourcery-ai review. Upgrade to get a review now.

@coderabbitai

coderabbitai Bot commented Sep 14, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 95b218c2-3e4a-4932-903a-014455af33f2


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai

sourcery-ai Bot commented Sep 14, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR adds a manual, production-gated, dry-run-by-default release-pruning workflow. It paginates inactive-release discovery, applies conservative age/sealing/manifest-based rollback retention, deletes selected releases in bounded Convex batches, and performs orphan-blob cleanup while coordinating with dataset updates; associated tests and operational documentation cover the policy and workflow wiring.

Sequence diagram for the manual release pruning workflow

sequenceDiagram
    participant Operator
    participant GitHubActions
    participant PruneScript
    participant Convex

    Operator->>GitHubActions: workflow_dispatch
    GitHubActions->>PruneScript: pnpm convex:prune-releases
    PruneScript->>Convex: maintenance:previewInactiveReleases(cursor, limit)
    Convex-->>PruneScript: inactive releases, continueCursor
    loop Until isDone
        PruneScript->>Convex: maintenance:previewInactiveReleases(cursor, limit)
        Convex-->>PruneScript: next inactive page
    end
    PruneScript->>PruneScript: selectPrunable
    alt apply is false
        PruneScript->>Convex: cleanupOrphanContentBlobsBatch(dryRun=true)
    else apply is true
        loop Selected releases
            PruneScript->>Convex: maintenance:deleteInactiveReleaseBatch(maxDocs=200)
            Convex-->>PruneScript: deletion progress
        end
        PruneScript->>Convex: cleanupOrphanContentBlobsBatch(dryRun=false)
    end
Loading

Flow diagram for inactive release selection policy

flowchart TD
    A[Inactive release preview] --> B{Already pruning?}
    B -- Yes --> C[Prune and finish]
    B -- No --> D{Sealed?}
    D -- No --> E[Skip unsealed upload]
    D -- Yes --> F{Minimum age reached?}
    F -- No --> G[Skip recent release]
    F -- Yes --> H{Declared and manifest verified?}
    H -- No --> C
    H -- Yes --> I{Within keep_per_distro rollback retention?}
    I -- Yes --> J[Retain rollback candidate]
    I -- No --> C
Loading

File-Level Changes

Change Details Files
Add a guarded manual workflow and CLI orchestration for previewing and pruning inactive releases.
  • Expose manual inputs for apply mode, retention count, and minimum age.
  • Use production environment and shared ingestion/promotion concurrency.
  • Default to dry run, validate options, paginate release discovery, and emit summaries.
  • Delete releases through bounded confirmed batches with bounded parallelism, then sweep orphan blobs.
.github/workflows/prune-releases.yml
scripts/prune-inactive-releases.mjs
scripts/prune-inactive-releases.test.mjs
package.json
Make inactive-release inspection complete and expose the metadata needed to evaluate rollback eligibility.
  • Add cursor-based pagination to the internal preview query.
  • Report sealing, pruning state, manifest basis, verification, and errors.
  • Add coverage for multi-page results, active filtering, and eligibility metadata.
convex/maintenance.ts
convex/releasePreview.test.ts
Implement defensive retention and deletion policy for superseded releases.
  • Never select unsealed or recently ingested releases.
  • Always finish releases already marked pruning.
  • Retain the newest verified, declared releases per locale/distro and prune legacy or failed-manifest releases.
  • Rely on the deletion mutation's active-pointer guard and fail on malformed or stalled responses.
scripts/prune-inactive-releases.mjs
scripts/prune-inactive-releases.test.mjs
Document the operational rollout, rollback implications, and maintenance behavior.
  • Add preview/apply commands and retention guidance to the multi-distro runbook.
  • Clarify rollback cleanup sequencing and retained rollback candidates.
  • Document pruning and orphan-blob cleanup in the architecture and changelog.
docs/runbooks/multi-distro-ops.md
docs/runbooks/bad-ingestion-release.md
docs/runbooks/convex-production-cutover.md
docs/ARCHITECTURE.md
CHANGELOG.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@greptile-apps

greptile-apps Bot commented Sep 14, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

The PR should not merge until release selection consistently implements the documented rule for recent sealed releases that have permanently failed manifest verification.

Fix All in CodexFindings

  1. P1 Age Gate Retains Invalid Releases ▶
Fix with agent prompt
### Issue 1
scripts/prune-inactive-releases.mjs:64-69
Legacy verification can seal a release before later recording that its manifest failed. The minimum-age check runs before manifest eligibility is evaluated, so a recent release in this permanently non-activatable state is skipped instead of pruned, contrary to the documented policy that failed or legacy manifests are removed regardless of age.

```suggestion
    const activatable = release.manifestBasis === 'declared' && release.manifestVerified === true
    if (activatable) {
      const age = now - releaseTime(release)
      if (!Number.isFinite(age) || age < minAgeMs) {
        skipped.push({ ...release, reason: 'younger than the minimum age' })
        continue
      }
    }
```

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

This PR adds a manually dispatched, dry-run-by-default production workflow for pruning superseded dataset releases. It extends the internal maintenance preview with pagination and rollback-eligibility metadata, adds bounded release deletion and orphan-blob sweep orchestration, and documents the operational process.

  • Shares workflow concurrency with dataset ingestion and promotion.
  • Retains configurable verified rollback candidates by locale and distro.
  • Processes release deletion and orphan cleanup in bounded batches.
  • Adds Convex and Node tests covering preview pagination, policy selection, batching, and workflow wiring.
  • The selection order currently delays pruning recent sealed releases that have already become permanently ineligible for activation.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Manual workflow dispatch] --> B[Paginate inactive releases]
  B --> C[Classify releases]
  C -->|Retain| D[Verified rollback candidates]
  C -->|Skip| E[Unsealed or recent releases]
  C -->|Prune| F[Bounded parallel release deletion]
  F --> G[Repeat until each release row is removed]
  D --> H[Orphan content-blob sweep]
  E --> H
  G --> H
  H --> I[Write job summary]
Loading

Reviews (1) · Last reviewed commit: "feat(ops): prune superseded dataset rele..."

Comment thread scripts/prune-inactive-releases.mjs Outdated
- Stop handing out new deletions after the first failure and surface the
  Convex error text (redacting deploy keys) instead of a bare exit code.
- Retry each batch with backoff; batch limits start below the Convex maxima
  and halve on failure so oversized transactions degrade instead of stalling.
- Apply the age floor only to sealed declared releases, which are the only
  ones activation could still accept; legacy and failed manifests are pruned
  regardless of age. Prefer the server-assigned creation time over the
  client-supplied ingestedAt.
- Require keep_per_distro >= 1 and min_age_hours >= 1; make the whole-table
  orphan sweep opt-in for previews; add an opt-in age floor for abandoned
  unsealed drafts (sample or interrupted ingests).
- Delete stored payload files with pruned page contents and with orphaned
  blobs when no other blob shares the file, so _storage no longer grows
  monotonically.
- Document the manual /ingest/activate rollback interlock gap.
- Treat an empty BETTERMAN_PRUNE_SWEEP_ORPHANS as unset so the workflow's
  'auto' default actually sweeps orphans on apply; reject other values.
- Keep the reduced batch size after a failed attempt instead of
  re-escalating on every batch.
- Report uploadComplete from the preview and never prune an unsealed
  release whose declared upload is complete; it is still activatable.
- Reject unparseable release times instead of sorting NaN.
- Redact project-scoped deploy keys and arbitrary token characters.
- Log createdAt in the plan and each release as it finishes.
- Propagate a thrown null from the concurrency helper.
@amanthanvi
amanthanvi merged commit 663b224 into main Sep 14, 2026
15 checks passed
@amanthanvi
amanthanvi deleted the t3code/prune-superseded-releases branch September 14, 2026 23:36

This branch was successfully deployed

1 active deployment
Preview — 65310500 Deployed Sep 14, 2026 by vercel[bot]
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