Repository navigation
[Bug]: Bundle pins can become unresolvable after component catalog updates #4712
Description
Activity
github-actions commented
on Sep 23, 2026 on Sep 23, 2026 – with GitHub ActionsContributorMore actionsBug assessment — bundle-catalog-pin: Likely valid, needs reproduction · severity medium
Bug Assessment: Bundle pins can become unresolvable after component catalog updates
- Slug: bundle-catalog-pin
- Created: 2026-09-23T18:41:17Z
- Source: issue [Bug]: Bundle pins can become unresolvable after component catalog updates #4712
- Verdict: likely valid, needs reproduction
- Severity: medium
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
- Create or obtain a bundle whose extension or preset reference pins version
0.4.12. - Ensure an install-enabled catalog advertises that component at
0.4.12, with its release artifact available. - Change the catalog entry to advertise
0.5.1while leaving the0.4.12artifact at its former URL. - In a fresh initialized project with no affected component installed, configure that catalog.
- Run
specify bundle install <bundle-id>. - Observe whether resolution rejects the mismatch before attempting the pinned artifact.
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__.py- Catalog/reference models under
src/specify_cli/bundles/ tests/specify_cli/bundles/test_primitives.pyand related catalog/download tests
Tests to add or update:
- Install an extension and preset pinned to an older version when the current catalog advertises a newer version, asserting the pinned artifact is selected when its immutable source is available.
- Assert the downloaded archive’s manifest ID/version is checked against the component pin and that no newer artifact is substituted.
- Assert a missing historical artifact produces an actionable error.
- Preserve tests for current-catalog mismatches, discovery-only catalogs, offline mode, checksums, and HTTPS/redirect validation.
Risks & Considerations
- Changing the source schema or download APIs may require catalog/bundle contract and documentation updates.
- Historical URLs can disappear or become mutable; checksums and downloaded-manifest verification are necessary for reproducibility and supply-chain safety.
- Catalog install policy must remain enforced for the source actually used.
- Backward compatibility is needed for existing bundles that contain only component ID/version and no immutable artifact source; those should retain the current clear mismatch behavior unless a safe version-aware lookup is available.
Open Questions
- [NEEDS CLARIFICATION: Does
ComponentRef.sourceintend to identify a catalog, an artifact URL, or another resolver source?] - [NEEDS CLARIFICATION: Does the bundle manifest actually contain the original
0.4.12artifact URL or checksum?] - [NEEDS CLARIFICATION: Can the reported failure be reproduced with a fixture or request trace showing that the older artifact remains available?]
Generated by 🐛 Assess Bug from Labeled Issue for #4712 · copilot · gpt52codex · 2.83 AIC · ⌖ 6.11 AIC · ⊞ 22.4K · ◷
- addedtriage-nice-to-haveVerdict: evidence-backed fix or greenlit feature — land after reviewVerdict: evidence-backed fix or greenlit feature — land after review
on Sep 23, 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.Catalog entry Result Server GETs during install 0.4.12Exit 0, Installed 'repro-pin-bundle' (1 added, 0 already present)/catalog.json,/repro-pin-extension-0.4.12.zip0.5.1Exit 1, pinned/resolved version mismatch /catalog.jsononlyThe second run reported:
Error: Extension 'repro-pin-extension' 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.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.
- added a commit that references this issue
on Sep 24, 2026 - added a commit that references this issue
on Sep 30, 2026 - added a commit that references this issue
on Oct 6, 2026 - added a commit that references this issue
on Oct 6, 2026
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
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.