feat(oci): retain app version history so bumps do not lose old versions (BUNYIP-386) #380

Merged
nrupard merged 3 commits from feat/BUNYIP-386-oci-version-history into main 2026-07-20 22:30:08 +02:00
Owner

Summary

Fixes BUNYIP-386: Bunyip's OCI distribution served only the single applications.pinned_image_tag, so an admin bump (v0.7 -> v0.8) overwrote the pin and made v0.7 unpullable through Bunyip, with no warning or grace. This adds an append-only version history so a bump ADDS a version and historical tags stay pullable.

Changes

  • Migration 20260720000010_create_application_versions.sql: application_versions (application_id FK, image_tag, image_digest, published_at, release_notes_url, yanked; UNIQUE(application_id, image_tag)), backfilled from each app's current pinned_image_tag.
  • ApplicationRepository (bunyip-domain, shared): record_version (idempotent INSERT ... ON CONFLICT DO NOTHING) and is_pullable_version (non-yanked EXISTS). Runtime sqlx, so the offline .sqlx cache needs no regeneration.
  • Bump path (admin.rs): records the current pinned tag on every distribution update, best-effort (a history-write error never fails the update).
  • Serve (oci_registry::get_manifest): now allows the pinned tag, any sha256 digest, OR any recorded non-yanked version. yanked pulls a bad version out of circulation without deleting history.

Scope note

pinned_image_tag stays the current/default pointer. Versions bumped BEFORE this lands are already lost from Bunyip and are not recovered (a one-time upstream dev.a8n.run tag scan could reintroduce them later). An admin "yank" endpoint / UI is a follow-up; this adds the column and honours it in serving.

Verification

cargo build --workspace + clippy --workspace --all-targets -- -D warnings + fmt --all --check all green (SQLX_OFFLINE, pinned rust-builder). A DB-backed version_history_roundtrip_and_yank test is added (record -> pullable, unrecorded -> not, yank -> not, idempotent count). It skips when DATABASE_URL is unset and was not run against a live postgres here - please run just pre-commit (or the dev-postgres test path) before merge to execute it.

#BUNYIP-386

## Summary Fixes BUNYIP-386: Bunyip's OCI distribution served only the single `applications.pinned_image_tag`, so an admin bump (v0.7 -> v0.8) overwrote the pin and made v0.7 unpullable through Bunyip, with no warning or grace. This adds an append-only version history so a bump ADDS a version and historical tags stay pullable. ## Changes - **Migration `20260720000010_create_application_versions.sql`**: `application_versions (application_id FK, image_tag, image_digest, published_at, release_notes_url, yanked; UNIQUE(application_id, image_tag))`, backfilled from each app's current `pinned_image_tag`. - **`ApplicationRepository` (bunyip-domain, shared)**: `record_version` (idempotent `INSERT ... ON CONFLICT DO NOTHING`) and `is_pullable_version` (non-yanked `EXISTS`). Runtime sqlx, so the offline `.sqlx` cache needs no regeneration. - **Bump path (`admin.rs`)**: records the current pinned tag on every distribution update, best-effort (a history-write error never fails the update). - **Serve (`oci_registry::get_manifest`)**: now allows the pinned tag, any sha256 digest, OR any recorded non-yanked version. `yanked` pulls a bad version out of circulation without deleting history. ## Scope note `pinned_image_tag` stays the current/default pointer. Versions bumped BEFORE this lands are already lost from Bunyip and are not recovered (a one-time upstream `dev.a8n.run` tag scan could reintroduce them later). An admin "yank" endpoint / UI is a follow-up; this adds the column and honours it in serving. ## Verification `cargo build --workspace` + `clippy --workspace --all-targets -- -D warnings` + `fmt --all --check` all green (SQLX_OFFLINE, pinned rust-builder). A DB-backed `version_history_roundtrip_and_yank` test is added (record -> pullable, unrecorded -> not, yank -> not, idempotent count). It skips when `DATABASE_URL` is unset and was **not** run against a live postgres here - please run `just pre-commit` (or the dev-postgres test path) before merge to execute it. #BUNYIP-386
feat(oci): retain app version history so bumps do not lose old versions (BUNYIP-386)
All checks were successful
E2E / Playwright against deployment (pull_request) Successful in 31s
Check / fmt + clippy + build + tests (pull_request) Successful in 20m1s
31b483ced2
Bunyip's OCI proxy served only the single `applications.pinned_image_tag`, so an admin bump (v0.7 -> v0.8) overwrote the pin and made v0.7 unpullable through Bunyip, with no warning and no grace. This adds an append-only version history so a bump ADDS a version instead of replacing it, and historical tags stay pullable.

New `application_versions` table (application_id, image_tag, image_digest, published_at, release_notes_url, yanked; UNIQUE(application_id, image_tag)), backfilled from each app's current pinned tag. `ApplicationRepository::record_version` (idempotent INSERT ... ON CONFLICT DO NOTHING) and `is_pullable_version` (non-yanked EXISTS check) are added as runtime sqlx queries so the offline .sqlx cache needs no regeneration. The admin distribution-update handler records the current pinned tag on every save (best-effort; a history-write error never fails the update). `oci_registry::get_manifest` now serves the pinned tag, any sha256 digest, OR any recorded non-yanked version, so a pin bump no longer makes older tags unpullable; the `yanked` flag pulls a bad/insecure version out of circulation without deleting the append-only history.

Verified in the pinned rust-builder (SQLX_OFFLINE): `cargo build --workspace`, `clippy --workspace --all-targets -- -D warnings`, and `fmt --all --check` all green. A DB-backed `version_history_roundtrip_and_yank` test is added (record -> pullable, unrecorded -> not, yank -> not, idempotent count); it compiles and skips when DATABASE_URL is unset and was NOT executed against a live postgres here, so it needs a run with the dev postgres before merge.

#BUNYIP-386

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat(oci): self-heal version history + fill digest on the serve path (BUNYIP-386)
Some checks failed
E2E / Playwright against deployment (pull_request) Successful in 58s
Check / fmt + clippy + build + tests (pull_request) Has been cancelled
d1bdbf0ae5
Review follow-up (option b). The admin-bump append of the pinned tag was best-effort and not transactional with the pin update, so a transient failure could leave a pinned tag unrecorded and then lose it on the next bump. get_manifest now records the served tag into application_versions after the manifest resolves (tag references only, best-effort so a history write never fails a pull), so the invariant "the pinned tag is always in the history" self-heals on first pull. It also passes the resolved digest.

record_version now upserts instead of ON CONFLICT DO NOTHING: `DO UPDATE SET image_digest = COALESCE(application_versions.image_digest, EXCLUDED.image_digest)`. This fills image_digest the first time a tag's manifest is resolved (closing the "resolved lazily" gap) while keeping an already-set digest and staying idempotent; the bump path (digest = None) still just ensures the row exists.

Validated the upsert on a throwaway postgres: digest fills to the first resolved value, is kept on subsequent serves and on a null-digest bump, and the row count stays 1 (no duplicates). Green on build + clippy -D warnings + fmt (SQLX_OFFLINE) via the pinned rust-builder.

#BUNYIP-386

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat(download): retain + serve historical binary releases (BUNYIP-386)
All checks were successful
E2E / Playwright against deployment (pull_request) Successful in 28s
Check / fmt + clippy + build + tests (pull_request) Successful in 9m29s
Create release / Create release from merged PR (pull_request) Has been skipped
e62f10848d
BUNYIP-386 initially covered only the OCI proxy (pinned_image_tag), but the Forgejo release/package download path had the identical single-version oversight: bumping pinned_release_tag overwrote the pin and made the old binary unreachable through Bunyip, since the download route only ever served the current pin. This generalizes the version history to both distribution paths so binaries are retained and reachable the same way images are.

Model: application_versions is now artifact-agnostic. image_tag/image_digest are renamed to version_tag/artifact_digest (the digest is OCI-only and stays NULL for binary versions, which have no single digest), and the backfill records both pin columns via UNION. record_version/is_pullable_version operate on version_tag; download_source gains download_source_for(version) to resolve an arbitrary tag rather than only the pin. The migration is edited in place rather than added as a rename step because the branch is unmerged and has not been applied to any real database.

Serve: a new versioned route GET /applications/{slug}/downloads/{version}/{asset_name} resolves a specific recorded release, gated by the same serve-relax allow-list the OCI proxy uses (the current pin is always servable; any other tag must be a recorded, non-yanked version; a miss returns 404, not 403, so a restricted product's tag set does not leak by status code). The default route self-heals the pinned tag into history on serve, and the admin bump append now records both the image and release pins.

Verified on a throwaway postgres against the real migration file: the UNION backfill covers binary pins, record_version is idempotent and fills the digest once (COALESCE keeps it), the allow-list yanks per-version, and rows cascade-delete with the app. Green on build + clippy + fmt (SQLX_OFFLINE) via the pinned rust-builder, and all 277 bunyip-domain unit tests pass including a new download_source_for assertion.

#BUNYIP-386

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
nrupard deleted branch feat/BUNYIP-386-oci-version-history 2026-07-20 22:30:08 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/bunyip!380
No description provided.