Repository navigation
[Bug]: Bundle pins can become unresolvable after component catalog updates #4712
Description
Bug Description
Bundle 0.4.12 pins an extension to 0.4.12. Because component catalogs contain only the currently advertised release, updating the extension’s catalog entry to 0.5.1 replaces the version and download URL used by bundle resolution. The resolver looks up the extension by ID—not by ID and pinned version—so the unchanged bundle now resolves to the 0.5.1 entry. The subsequent version check rejects that selection. It never attempts to retrieve 0.4.12, even when that release remains available.
Steps to Reproduce
This scenario is derived from source inspection; runtime reproduction is still needed.
-
Prepare a bundle that pins an extension or preset to version
0.4.12. -
Make that version available through an install-enabled component catalog.
-
Update the catalog entry to advertise version
0.5.1, leaving the0.4.12release artifact available at its original URL. -
In a fresh initialized project with no affected components installed, configure that component catalog.
-
Install the original bundle using
specify bundle install <path-to-bundle.yml>. -
Observe that component resolution rejects the version mismatch before attempting to retrieve the pinned release.
Expected Behavior
The bundle retrieves and installs its pinned component version when that release remains available, regardless of whether a newer version is advertised.
If the pinned version cannot be retrieved, installation reports that clearly without silently substituting another version.
Actual Behavior
The inspected resolver compares the bundle’s pin against the selected catalog entry’s version and raises an error when they differ. It does not attempt to locate the older pinned release.
Specify CLI Version
1.0.10
AI Agent
GitHub Copilot
Operating System
To be recorded during runtime reproduction.
Python Version
To be recorded during runtime reproduction.
Error Logs
No runtime logs captured. The inspected extension resolver produces the following error for the example above:
Extension 'specassay-check' is pinned to version 0.4.12 in the bundle manifest, but the resolved version is 0.5.1. Update the bundle's pinned version or the source before installing.Additional Context
Observed during #4703
AI Disclosure
GitHub Copilot using GPT-6 Astra (gpt-6-astra), human-supervised, assisted with source inspection, related-issue research, and drafting this report.
Activity
github-actions commentedon Sep 23, 2026
Bug assessment — bundle-catalog-pin: Likely valid, needs reproduction · severity medium
Bug Assessment: Bundle pins can become unresolvable after component catalog updates
Report (summarized)
The report describes a bundle that pins an extension or preset at
0.4.12while its install-enabled catalog later advertises0.5.1. The older release artifact is reportedly still available at its original URL, butspecify bundle installrejects the component before downloading it with a pinned/resolved-version mismatch. The report is based on source inspection; no runtime logs or environment details were provided. It references #4703 for additional context.Symptom
An existing bundle cannot be installed after the catalog entry for one of its pinned components advances, even when the pinned artifact remains available. Expected behavior is to retrieve the exact pinned version, or report a clear retrieval failure; actual behavior is to compare the bundle pin with the catalog’s current advertised version and stop first.
Reproduction
0.4.12.0.4.12, with its release artifact available.0.5.1while leaving the0.4.12artifact at its former URL.specify bundle install <bundle-id>.The report does not identify the catalog payload, exact component ID beyond the example
specassay-check, or whether the original URL is represented anywhere in the bundle manifest. [NEEDS CLARIFICATION: confirm the catalog format and provide a runtime reproduction with network/request evidence.]Suspected Code Paths
src/specify_cli/bundles/primitives.py:33-60—_assert_pinned_versionunconditionally raises when a component’s manifest pin differs from the version advertised by the selected catalog metadata.src/specify_cli/bundles/primitives.py:245-300—_ExtensionKindManager._do_installobtains one current catalog record by component ID, validates its advertised version, and only then callsdownload_extension; the preset manager follows the same pattern earlier in the file.src/specify_cli/extensions/__init__.py:4342-4389—ExtensionCatalog.get_extension_inforeturns the merged/current entry anddownload_extensionlooks up that entry again by ID, so the installer has no API for downloading a historical URL independently of the current metadata.src/specify_cli/presets/__init__.py:5139-5159—PresetCatalog.get_pack_infolikewise returns the current merged entry by ID.src/specify_cli/bundles/manifest.py:31-40,255-280— bundle component references carryid,version, and an optionalsource, but the source value is only parsed intoComponentRef; the inspected primitive adapters do not use it to select a historical catalog entry or artifact URL.Root Cause Hypothesis
The component installer treats the current catalog record selected by ID as both the source of truth for artifact selection and the authority for reproducibility. That makes a bundle’s immutable version pin incompatible with mutable catalog metadata: after an entry is updated,
_assert_pinned_versionfails before download, and the optional componentsourcefield does not provide a usable historical lookup path. Confidence: high for the reported rejection path; medium for the intended historical-download contract because no runtime reproduction or catalog fixture was supplied.Proposed Remediation
Preferred: Make bundle component references resolve an immutable artifact source when one is recorded, rather than resolving only the latest catalog metadata by ID. Define and validate the source contract (for example, an artifact URL plus checksum, or a catalog entry/version address), pass that source through the extension and preset download APIs, and verify the downloaded manifest’s ID and exact version before installation. Keep the version mismatch guard for mutable/current catalog resolution and fail clearly when a pinned artifact cannot be found; never silently substitute the newer version.
If the intended contract is that
sourceidentifies a catalog rather than an artifact, add version-aware catalog lookup and preserve the matching historical entry/artifact URL in the catalog or bundle metadata. The implementation must avoid making an arbitrary “latest” lookup and should retain existing install-policy and URL/security validation.Files likely to change:
src/specify_cli/bundles/manifest.pysrc/specify_cli/bundles/primitives.pysrc/specify_cli/extensions/__init__.pysrc/specify_cli/presets/__init__.pysrc/specify_cli/bundles/tests/specify_cli/bundles/test_primitives.pyand related catalog/download testsTests to add or update:
Risks & Considerations
Open Questions
ComponentRef.sourceintend to identify a catalog, an artifact URL, or another resolver source?]0.4.12artifact URL or checksum?]Doribelove commentedon Sep 24, 2026
I reproduced this against
mainat25d43a9482af998dc932142d0398794635a3a5e8(specify-cli 1.0.12.dev0) on Ubuntu Linux x86_64 with Python 3.12.14.I used a localhost-only HTTP server with two synthetic extension ZIPs, versions
0.4.12and0.5.1. A localbundle.ymlpinnedrepro-pin-extensionto0.4.12. The install-enabled extension catalog initially advertised0.4.12with its ZIP URL and SHA-256; I then changed only the catalog's advertised version, URL, and digest to0.5.1. Each install used a fresh.specify/project andSPECKIT_CATALOG_URLpointing at that catalog.0.4.12Installed 'repro-pin-bundle' (1 added, 0 already present)/catalog.json,/repro-pin-extension-0.4.12.zip0.5.1/catalog.jsononlyThe second run reported:
After that failure, a direct HTTP GET for the old
0.4.12ZIP returned 200 (SHA-2565f70afb3ff7148066ee01e37283accc80301c025cc4736c696afd4ebd04e0212). Thus the failure occurs before either ZIP is requested, rather than because the pinned artifact is unavailable. This confirms the reported behavior for an extension on currentmain; I did not test presets or remote hosting.I also saw #4719's maintainer-authored plan to add exact-version catalog lookup in separate, reviewable slices. The extension-catalog slice looks like the appropriate first code change; a later bundler slice can use that lookup without weakening pin enforcement. Please flag if someone is already working on the extension slice.
AI assistance disclosure: Posted on behalf of @Doribelove by OpenAI Codex (model: GPT-6, autonomous mode with default task settings). The agent inspected the source, generated and ran the localhost reproduction, and fully drafted this comment. The contributor has not independently run this reproduction.
feat(extensions): select exact catalog releases
feat(extensions): select exact catalog releases (#4726)
fix(bundles): install pinned catalog releases via exact-release selec…
fix(bundles): install pinned catalog releases via exact-release selec…