Skip to content

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

Description

@KSchlobohm

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.

  1. Prepare a bundle that pins an extension or preset to version 0.4.12.

  2. Make that version available through an install-enabled component catalog.

  3. Update the catalog entry to advertise version 0.5.1, leaving the 0.4.12 release artifact available at its original URL.

  4. In a fresh initialized project with no affected components installed, configure that component catalog.

  5. Install the original bundle using specify bundle install <path-to-bundle.yml>.

  6. 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

  1. github-actions commented on Sep 23, 2026

    @github-actions
    Contributor

    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.12 while its install-enabled catalog later advertises 0.5.1. The older release artifact is reportedly still available at its original URL, but specify bundle install rejects 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

    1. Create or obtain a bundle whose extension or preset reference pins version 0.4.12.
    2. Ensure an install-enabled catalog advertises that component at 0.4.12, with its release artifact available.
    3. Change the catalog entry to advertise 0.5.1 while leaving the 0.4.12 artifact at its former URL.
    4. In a fresh initialized project with no affected component installed, configure that catalog.
    5. Run specify bundle install <bundle-id>.
    6. 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_version unconditionally 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_install obtains one current catalog record by component ID, validates its advertised version, and only then calls download_extension; the preset manager follows the same pattern earlier in the file.
    • src/specify_cli/extensions/__init__.py:4342-4389 — ExtensionCatalog.get_extension_info returns the merged/current entry and download_extension looks 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_info likewise returns the current merged entry by ID.
    • src/specify_cli/bundles/manifest.py:31-40,255-280 — bundle component references carry id, version, and an optional source, but the source value is only parsed into ComponentRef; 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_version fails before download, and the optional component source field 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 source identifies 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.py
    • src/specify_cli/bundles/primitives.py
    • src/specify_cli/extensions/__init__.py
    • src/specify_cli/presets/__init__.py
    • Catalog/reference models under src/specify_cli/bundles/
    • tests/specify_cli/bundles/test_primitives.py and 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.source intend to identify a catalog, an artifact URL, or another resolver source?]
    • [NEEDS CLARIFICATION: Does the bundle manifest actually contain the original 0.4.12 artifact 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 · ◷

  2. added
    triage-nice-to-haveVerdict: evidence-backed fix or greenlit feature — land after review
    on Sep 23, 2026
  3. Doribelove commented on Sep 24, 2026

    @Doribelove
    Contributor

    I reproduced this against main at 25d43a9482af998dc932142d0398794635a3a5e8 (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.12 and 0.5.1. A local bundle.yml pinned repro-pin-extension to 0.4.12. The install-enabled extension catalog initially advertised 0.4.12 with its ZIP URL and SHA-256; I then changed only the catalog's advertised version, URL, and digest to 0.5.1. Each install used a fresh .specify/ project and SPECKIT_CATALOG_URL pointing at that catalog.

    Catalog entry Result Server GETs during install
    0.4.12 Exit 0, Installed 'repro-pin-bundle' (1 added, 0 already present) /catalog.json, /repro-pin-extension-0.4.12.zip
    0.5.1 Exit 1, pinned/resolved version mismatch /catalog.json only

    The 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.12 ZIP returned 200 (SHA-256 5f70afb3ff7148066ee01e37283accc80301c025cc4736c696afd4ebd04e0212). 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 current main; 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.

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions