Skip to content

Catalog version history only accrues by observation — a fresh Forge cannot reach older ctl versions #103

Description

@jgruberf5

Problem

D-033 version rows are created only by observing a version bump during a sync. A content repo publishes one version at HEAD, so the catalog's version list is a function of how long that Forge has been watching the repo — not of what versions actually exist.

Consequence: a freshly installed bnk-forge syncs bnkctl-index and sees exactly one version. There is no path to deploy a known-good older ctl release. Older versions exist only in the content repo's git history, and nothing reconstructs them.

This is a production problem, not a theoretical one. ctl binaries gain features continuously; an operator hitting a regression wants to redeploy a version that worked. On an established Forge that's possible only by luck of having been synced at the right moment. On a new install it's impossible.

Evidence

mwiget/ocibnkctl has 14 releases. mwiget/bnkctl-index publishes a single pack directory (tools/ocibnkctl/) with one module.version and one pinned digest — every prior version is a git-history commit, not a catalog row.

Observed directly on a live instance. Before sync:

 id |   name    |      path       | version  | is_latest
 64 | ocibnkctl | tools/ocibnkctl | 2.3.1.18 | t

After bumping the index to 2.3.1.19 and re-syncing (modules_created: 1):

 id |   name    |      path       | version  | is_latest
 64 | ocibnkctl | tools/ocibnkctl | 2.3.1.18 | f
 66 | ocibnkctl | tools/ocibnkctl | 2.3.1.19 | t

Two rows — but only because this Forge happened to be running for both. A Forge installed an hour later gets row 66 alone, with 2.3.1.18 unreachable.

Why the current code produces this

The mechanism is working as designed; the design just can't serve the use case.

  • backend/services/module_sync_service.py _upsert_pack_module (~1040-1200) — identity is (module_source_id, source_path, version); a bump falls through the existing-row lookups and creates a NEW immutable row. Accumulation is correct but strictly incremental.
  • _inactivate_stale_manifest_modules (~1243) prunes by path, not version, so accrued rows survive later syncs. Good — the rows persist once they exist. The gap is purely that they are never created retroactively.
  • backend/services/module_version_query.py available_module_versions / recompute_is_latest — the version axis and re-pin action work correctly on whatever rows exist.

See docs/adr/D-033-multi-version-module-catalog.md.

Directions

Not a recommendation yet — the tradeoff is a product call.

A. Index publishes every version. One pack directory per released ctl version, so any fresh sync yields the full set immediately. Works with the sync exactly as written today — zero backend change. Costs repo bloat and publishing discipline in the content repo. Note module.path must equal the pack's directory path, so per-version directories would be distinct source_paths and land as separate modules, not versions of one — this needs design work to avoid fragmenting the version axis.

B. A source type that enumerates upstream releases. The sync reads GitHub releases for the wrapped tool and synthesizes version rows from one pack template. Less duplication and it tracks reality automatically, but it needs a resolvable digest per release and a way to express "these releases map to this pack".

C. Backfill on demand. An action that walks the content repo's git history for a path and materializes the version rows it finds. Recovers history for existing repos without changing the publishing model. Older rows would pin digests carrying known bugs — acceptable, since that is the point, but it should be visible in the UI.

A stopgap available today: re-point a source's git_ref at successive older commits and sync each. Each deposits that commit's version row and they persist. Workable but manual and easy to get wrong.

Open questions

  • Should an older version row be selectable for a new deployment, or only for re-pinning an existing module that already ran it?
  • How should versions whose runner images have been deleted from the registry be represented — hidden, or shown as unavailable?
  • Does option A's per-version-directory problem have a clean answer, or does it force B/C?

Raised by @mwiget after a live deploy: "a user might want to redeploy a known working version on a new bnk-forge that doesn't have the version history built up over time."


Migrated from sp-prod-field/bnk-forge #482 (opened 2026-07-20; original labels: enhancement). That repository is archived and read-only.
Bare #NNN references in the text above refer to issues and PRs in the original repository, not to numbering here.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    backendFastAPI backend: routes, services, taskscatalogModule/blueprint catalog, sources, sync, versionscontainer-runnerContainer artifact engine, runner, and supply chainenhancementNew capability or improvement to existing behaviourwontfixThis will not be worked on

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions