Skip to content

fix(bundles): retrieve pinned component releases when a catalog advertises a newer version - #4753

Merged
mnriem merged 1 commit into
github:mainfrom
muhammadumer-waheed:fix/4712-fetch-pinned-release
Oct 6, 2026
Merged

mnriem merged 1 commit into
github:mainfrom
muhammadumer-waheed:fix/4712-fetch-pinned-release

Conversation

@muhammadumer-waheed

@muhammadumer-waheed muhammadumer-waheed commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #4712.

Description

Bundle manifests pin component versions for reproducibility, but specify bundle install compared each extension/preset pin against the version the catalog currently advertises and refused the install when they differed — even when the catalog still listed the pinned release.

This PR builds the fix on the exact-release catalog support from #4726 (extensions) and #4823 (presets), as suggested in review, instead of deriving historical URLs:

  • Exact-release selection. For a pinned extension or preset, the bundler selects the pinned release from the winning catalog entry (select_release, the same path specify extension add --version / specify preset add --version use) and downloads that record with download_extension_info / download_pack_info, so the release's own download_url and sha256 are used.
  • Archive verification. The pin is passed to install_from_zip as expected_id / expected_version, so the archive must declare the pinned component and version.
  • Missing pin. When the winning catalog entry has no release for the pinned version, the install fails before any download with an error naming the pinned and advertised versions. It never substitutes the advertised release or falls through to a lower-priority catalog.
  • Unchanged. Unpinned components install the advertised release as before. Entries advertising no version still cannot enforce a pin. Workflows and bundled assets keep the hard pin check (separate [Feature]: Support multiple versions per ID across catalog types #4719 slices).

docs/reference/bundles.md documents the behavior.

Testing

  • Ran existing tests with uv sync && uv run pytest — 9450 passed, 264 skipped
  • uvx ruff@0.15.0 check src tests — all checks passed
  • markdownlint-cli2 docs/reference/bundles.md — 0 issues

New tests in tests/specify_cli/bundles/test_primitives.py (fail without the primitives.py change):

  • pinned historical release selected, with its own URL and digest, and verified against the pin (extensions, presets)
  • pin missing from the catalog entry refuses before download (extensions, presets)
  • unpinned component still installs the advertised release without pin verification (extensions, presets)
  • archive declaring a different version is refused, nothing is installed, and the downloaded archive is removed

Existing bundle tests that mocked download_extension / download_pack now mock the *_info variants.

AI Disclosure

  • I did not use AI assistance for this contribution
  • I did use AI assistance (fill in the disclosure below)

AI disclosure: Reworked with Claude Code (model: Claude Opus 5.5), human-directed: I chose the approach and reviewed the result before pushing. Extent: code, tests, and docs were generated by the agent. The original revision of this PR was implemented with opencode (model: Qwen3.8-27B, local).

@mnriem mnriem added the triage-can-wait Verdict: valid and in-scope but deprioritized; held behind the evidence gate label Sep 26, 2026
@mnriem
mnriem requested a balanced review from Copilot September 28, 2026 12:39

Copilot AI left a comment

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.

Copilot review overview

🟡 Changes recommended

Derived downloads can install the wrong manifest version, and valid v-prefixed versions produce incorrect URLs.

Review effort: Balanced
Findings: 1 High severity · 1 Medium severity

Open (2)
What changed in this PR

Fixes stale bundle pins by deriving and downloading historical extension/preset release URLs.

Changes:

  • Adds explicit-URL catalog download APIs.
  • Adds pin-aware URL derivation and error handling.
  • Documents and tests pinned-release retrieval.
File Description
src/​specify_cli/​bundles/​primitives.py Implements pinned-release resolution.
src/​specify_cli/​extensions/​__init__.py Adds explicit extension URL downloads.
src/​specify_cli/​presets/​_catalog.py Adds explicit preset URL downloads.
tests/​specify_cli/​bundles/​test_primitives.py Tests pin resolution and failures.
tests/​test_extensions.py Tests extension URL downloads.
tests/​specify_cli/​presets/​test_catalog.py Tests preset URL downloads.
docs/​reference/​bundles.md Documents historical-release behavior.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/specify_cli/bundles/primitives.py Outdated
Comment thread src/specify_cli/bundles/primitives.py Outdated
@mnriem

mnriem commented Sep 28, 2026

Copy link
Copy Markdown
Collaborator

Please address Copilot feedback and fix test & lint errors

Copilot AI left a comment

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.

Copilot review overview

🟡 Changes recommended

URL derivation can incorrectly rewrite static repository path segments containing the advertised version.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
Resolved since last review (2)

Comment thread src/specify_cli/bundles/primitives.py Outdated
@mnriem

mnriem commented Sep 28, 2026

Copy link
Copy Markdown
Collaborator

Please address Copilot feedback

Copilot AI left a comment

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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copilot AI left a comment

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.

Copilot review overview

🔵 Needs a closer look

URL derivation fails when equivalent v and V prefixes differ between the catalog version and URL.

Review effort: Balanced
Findings: None

@KSchlobohm

KSchlobohm commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

@muhammadumer-waheed Thanks for the work and regression tests here. I think we should pause this PR until the extension and preset version-history work under #4719 lands, then use its exact release URLs and checksums for the bundle fix rather than deriving URLs.

@markuswondrak

Copy link
Copy Markdown
Contributor

Sorry for interrupting here — we stumbled across this while digging into #4712, and wanted to check before doing anything.

Context

Bundle manifests pin each component to an exact version for reproducibility. Catalog entries advertise a current release and optionally keep older ones under a releases map, each with its own download_url and sha256. The problem (#4712, with a confirmed local reproduction): a bundle pinned to an older release fails to install — the bundler resolves the component by ID, receives the current release, compares it against the pin, and errors out before downloading anything, even though the pinned release is still available.

Gap: the catalog can resolve the exact release, but the bundler doesn't ask for it

The failure is in _ExtensionKindManager._do_install (src/specify_cli/bundles/primitives.py):

  • :305 — catalog.get_extension_info(component.id) resolves by ID only, so it returns the current release
  • :315 — _assert_pinned_version(...) then compares the pin against that current version and raises before any download
  • :318 — catalog.download_extension(component.id) would re-resolve the current release anyway

Since #4726 landed, the catalog can already select and download an exact historical release:

info = catalog.get_extension_info(id, version="0.4.12")  # exact record, or None if absent
path = catalog.download_extension_info(info)              # downloads + verifies that record's own url/sha256

That is the same shared API the CLI's extension add --version already uses (command_add.py:178,269), so the bundler does not need to derive the pinned URL from the current one — it just needs to pass component.version through.

Question (@KSchlobohm @mnriem): now that #4726 provides exact-release selection on main, how would you prefer this land — folded into this PR, or as a separate focused PR (e.g. fix/4712-..., referencing #4719's bundler slice)? I

f this PR's URL-derivation approach is still the intended one, we're happy to work within it instead; just want to avoid duplicating effort. Scope would stay extensions-only (presets #4823 and workflows stay separate slices).

@digimangos

Copy link
Copy Markdown
Contributor

This PR addresses retrieving pinned component releases, but it also highlights a few related versioning gaps:

  • Preset catalogs currently advertise one release per preset, with no historical-release selection. How should a bundle resolve a pinned preset version once its catalog entry has advanced?
  • Presets can declare extension version constraints, but unmet constraints are advisory and do not prevent installation. A bundle can independently pin that extension to a different version. Which version should take precedence, and how should an incompatible combination be handled?
  • Bundle manifests have their own version, but bundle catalogs currently advertise only one release per bundle ID. Should catalogs support selecting historical bundle releases too?

These may be outside this PR’s scope, but it would be useful to track them alongside the broader catalog-versioning work.

@mnriem

mnriem commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator

Thanks—yes, these belong with the broader catalog-versioning work tracked in github/spec-kit#4719, which is intentionally being delivered as separate catalog-family and bundler slices rather than expanding this PR:

  • Historical preset-release selection is already proposed in github/spec-kit#4823.
  • Historical bundle-release selection is covered by the outstanding bundle-catalog slice.
  • Exact component-pin resolution and incompatible installed-version handling are covered by the outstanding bundler slice.

The interaction between a preset’s requires.extensions constraint and an exact bundle pin is a useful acceptance case to make explicit there. Conceptually, the bundle pin selects the exact version, while the preset declaration remains a compatibility constraint; an incompatible combination should fail clearly rather than silently choosing one version or treating the conflict as advisory.

So I’d keep github/spec-kit#4753 focused on github/spec-kit#4712 and track these broader semantics and remaining delivery slices under github/spec-kit#4719.

@muhammadumer-waheed

muhammadumer-waheed commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor Author

@markuswondrak thanks for digging into this, and good catch. Now that #4726 (extensions) and #4823 (presets) are on main, URL derivation isn't needed. I'll fold the fix into this PR rather than open a separate one, so we don't duplicate effort.

Plan for #4753:

  • Reset the branch to upstream/main.
  • In the bundler, resolve each component with its pinned version (get_extension_info(id, version=component.version) → download_extension_info(info), and the equivalent preset API from feat(presets): select exact catalog releases #4823), so the exact release record's own URL and SHA-256 are used.
  • Fail with a clear error when the pinned release isn't in the catalog.
  • Drop the URL-derivation logic, the explicit-URL download split, and the extra archive-manifest check, since the catalog download path now covers those checks.
  • Keep workflows and bundled assets out of scope, per the [Feature]: Support multiple versions per ID across catalog types #4719 slicing.

@KSchlobohm @mnriem: this follows the direction you suggested earlier.

…tion

Bundle manifests pin component versions, but extension and preset
installs compared the pin against the version the catalog currently
advertises and refused the install when they differed, even when the
catalog still listed the pinned release (github#4712).

The bundler now selects the pinned release from the winning catalog
entry with the exact-release support added in github#4726 (extensions) and
github#4823 (presets), downloads that record with its own URL and SHA-256
via download_extension_info / download_pack_info, and passes the pin
as expected_id / expected_version so the archive must declare the
pinned component. A pin the winning entry does not list fails with an
error naming the pinned and advertised versions, without substituting
the advertised release or falling through to another catalog.

Unpinned components, entries advertising no version, workflows and
bundled assets keep their existing behavior.

Fixes github#4712
@muhammadumer-waheed
muhammadumer-waheed force-pushed the fix/4712-fetch-pinned-release branch from 23d5ccb to 4be71da Compare October 6, 2026 09:56
@muhammadumer-waheed

Copy link
Copy Markdown
Contributor Author

Pushed the rework described above (4be71da). The branch is now a single commit on top of main.

One deviation from that plan: I kept archive verification, but through the catalog's own path rather than the bespoke check. The pin is passed to install_from_zip as expected_id / expected_version, the same way extension add --version / preset add --version do. That way, a mislabeled historical asset is still refused.

Full suite passes locally and the PR description is updated.

@KSchlobohm @mnriem: please take a look when you get the chance.

Copilot AI left a comment

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.

Copilot review overview

🟢 Approval recommended

The implementation matches the stated resolution contract and includes focused positive and negative regression coverage, though tests were not independently executed during review.

Review effort: Balanced
Findings: None

@mnriem
mnriem merged commit 7cac170 into github:main Oct 6, 2026
15 checks passed
@muhammadumer-waheed

Copy link
Copy Markdown
Contributor Author

@mnriem I'll take the remaining Bundler work for workflow and step component pins. Bundle installs will select the pinned release through the exact-release support from #4788/#4840, rather than comparing against the advertised version. Steps will enforce their pin, which they currently don't. Online bundle validate will check the exact pinned version, not just the ID. This doesn't touch the Bundle catalog slice (@markuswondrak).

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

Labels

triage-can-wait Verdict: valid and in-scope but deprioritized; held behind the evidence gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Bundle pins can become unresolvable after component catalog updates

6 participants