Skip to content

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

Open
muhammadumer-waheed wants to merge 3 commits into
github:mainfrom
muhammadumer-waheed:fix/4712-fetch-pinned-release
Open

muhammadumer-waheed wants to merge 3 commits 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

Fixes #4712.

Summary

Bundle manifests pin component versions for reproducibility, but specify bundle install compared each pin against the version the catalog currently advertises and refused the install when they differed — even when the pinned release remained available at its original download URL. Catalog entries advertise a single release, so the pinned release was never even attempted.

This change makes the bundler retrieve the pinned release when a catalog entry has moved on:

  • Pin-aware retrieval. When the bundle pin differs from the advertised version (extensions and presets), the bundler derives the pinned release's URL from the catalog entry's own download_url by substituting the advertised version token in the URL path (bounded token matching: a v-prefixed tag, a versioned asset filename, or a version glued to an archive suffix; the optional v/V prefix is normalized on both the pin and the advertised version, so a v0.4.12 pin derives v0.4.12, never vv0.4.12, and a v0.5.1 advertisement also rewrites the bare token in asset-0.5.1.zip filenames) and retrieves that release through the catalog's existing download pipeline. When the pinned release cannot be identified from the catalog URL, or the retrieval fails, the error names the pinned and advertised versions instead of a bare pin mismatch or network error.
  • Explicit-URL downloads. The fetch stage of ExtensionCatalog.download_extension and PresetCatalog.download_pack is now download_extension_url / download_pack_url (HTTPS validation, size-limited fetch, optional SHA-256, archive-format detection, safe cache path), which the ID-based methods delegate to without behavior change.
  • Digest scope + content verification. The catalog's SHA-256 covers only the advertised release, so a retrieved pinned release is verified against no digest; the same host and the HTTPS rule still apply. Since a successful request to a derived URL does not prove the served artifact is the pinned release (a redirect, a fallback response, or a mislabeled historical asset can serve a different component or version), the bundler additionally verifies the retrieved archive's own manifest declares the pinned component (id and normalized version, using the installers' secure extraction and manifest lookup) before installing, and removes the unverified artifact when it does not.

Intentionally out of scope: workflows keep the hard pin check (their URL install path carries an interactive untrusted-source confirmation that a derived-URL fetch would bypass), bundled assets keep the hard check (no alternative release exists), and steps perform no pin check today.

Reproduction (the issue's scenario)

A local catalog server advertising specassay-check v0.5.1 while the 0.4.12 release artifact stays available at its original URL; the bundle pins 0.4.12:

  • Before (main): Error: 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.
  • After (this branch): ✓ Installed 'test-bundle' (1 added, 0 already present). — the registry records version 0.4.12 and the installed extension.yml declares version: 0.4.12.
  • Mislabeled artifact: the 0.4.12 URL serves an artifact whose manifest declares 0.5.1 (a redirect, fallback response, or renamed historical asset): Error: Extension 'specassay-check' is pinned to version 0.4.12 in the bundle manifest, but the catalog now advertises v0.5.1: the retrieved archive declares version '0.5.1' instead of the pinned version 0.4.12. ... — nothing is installed and the unverified artifact is removed.

Test plan

New regression tests fail on main (the pin mismatch raised before any retrieval attempt) and pass with this change; the review-round tests (archive verification, v-prefix derivation, static-path components) fail on their respective pre-fix commits (verified by stashing the src change); the full suite is otherwise unchanged — the 10 pre-existing *_python_parity "composed" failures reproduce identically on clean main:

$ .venv/bin/python -m pytest tests -q
10 failed, 8373 passed, 211 skipped    # with this change
10 failed, 8350 passed, 211 skipped    # clean main (same 10 failures; 8373 = 8350 + 23 new tests)
  • uvx ruff@0.15.0 check src tests → All checks passed.
  • markdownlint-cli2 docs/reference/bundles.md → 0 issues.

Manual test results

Agent: n/a (CLI-level change; verified by running specify bundle install directly) | OS/Shell: macOS/zsh

Command tested Notes
specify bundle install <bundle.yml> --integration copilot catalog advertises v0.5.1, bundle pins 0.4.12 → pinned release retrieved, manifest-verified, and installed, exit 0
specify bundle install <bundle.yml> (stale pin, non-derivable catalog URL) fails with a pin-aware error naming the pinned and advertised versions
specify bundle install <bundle.yml> (pinned URL serves a mislabeled artifact) fails with the pin-aware verification error; nothing installed, artifact removed

AI disclosure

Implemented with opencode (model: Qwen3.8-27B, running locally; autonomous within the session under user direction, no human line-by-line review before this PR was opened). Extent: issue triage, source analysis, code, tests, end-to-end reproduction, and docs were all generated by the agent.

…tises a newer version

Bundle manifests pin component versions for reproducibility, but
extension/preset bundle installation compared the pin against the version
the catalog currently advertises and refused the install when they
differed, even when the pinned release remained available at its original
download URL (github#4712). Catalog entries advertise a single release, so the
resolver never attempted to retrieve the pinned one.

When the pin differs from the advertised version, the bundler now derives
the pinned release's URL from the catalog entry's own download_url by
substituting the advertised version token in the URL path (bounded token
matching: a v-prefixed tag, a versioned asset filename, or a version glued
to an archive suffix) and retrieves that release through the catalog's
existing download pipeline (HTTPS validation, size limits, safe cache
path). The catalog's SHA-256 covers only the advertised release, so a
retrieved pinned release is verified against no digest; the same host and
the HTTPS rule still apply. When the pinned release cannot be identified
from the catalog URL, or the retrieval fails, the error names the pinned
and advertised versions instead of a bare pin mismatch or network error.

To expose explicit-URL retrieval without duplicating the download
pipeline, the fetch stage of ExtensionCatalog.download_extension and
PresetCatalog.download_pack is now download_extension_url /
download_pack_url, which the ID-based methods delegate to without behavior
change (a catalog entry with a null version now names the cached archive
"unknown" instead of erroring).

Workflows and bundled-asset installs keep their existing hard pin check:
a workflow's URL install path carries an interactive untrusted-source
confirmation that a derived-URL fetch would bypass, and a bundled asset
has no alternative release to retrieve.

New regression tests in tests/specify_cli/bundles/test_primitives.py fail
on main (the pin mismatch raised before any retrieval attempt) and pass
with this change, plus URL-derivation unit tests and explicit-URL download
coverage in tests/test_extensions.py and
tests/specify_cli/presets/test_catalog.py. Verified end-to-end with a
local catalog advertising 0.5.1 while a 0.4.12 release stays available:
`specify bundle install` now installs the pinned 0.4.12 release (on main
it fails with the reported error).

Fixes github#4712

Assisted-by: opencode (model: Qwen3.8-27B (local), autonomous)
@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

…versions in URL derivation

Address the Copilot review findings on this PR:

- A successful request to a derived pinned-release URL does not prove the
  served artifact is the pinned release (a redirect, a fallback response,
  or a mislabeled historical asset can serve a different component or
  version). The catalog digest covers only the advertised release and the
  primitive installers trust the archive manifest, so the bundler now
  verifies the retrieved archive's manifest declares the pinned component
  (id and normalized version) before installing, and removes the unverified
  artifact when it does not.
- URL derivation now normalizes the optional v/V prefix on both sides: a
  v-prefixed pin no longer produces "vv0.4.12" URLs, and a v-prefixed
  advertised version also rewrites the bare-form version token in asset
  filenames (e.g. v0.5.1 advertises -> v0.4.12/asset-0.4.12.zip).

New tests fail on the pre-review code (verified by stashing the src
change): 2 URL-derivation regressions and 3 negative archive-verification
cases (extension version/id mismatch, preset version mismatch). E2E: a
mislabeled 0.4.12 artifact (manifest declares 0.5.1) is refused with a
pin-aware error; the happy path still installs the pinned release.

Assisted-by: opencode (model: Qwen3.8-27B (local), autonomous)

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
@mnriem

mnriem commented Sep 28, 2026

Copy link
Copy Markdown
Collaborator

Please address Copilot feedback

…version positions

Address the second Copilot review round: a bounded version token inside a
static path component (a repository named "tool-0.5.1") used to be
rewritten as well, so deriving a pinned release from
https://github.com/acme/tool-0.5.1/releases/download/v0.5.1/tool.zip moved
the component's home repository to tool-0.4.12 and the valid pinned release
was never fetched.

Substitution is now restricted to recognized version positions: the asset
filename (the final path segment) and a segment that is exactly a version
token (a release tag or a versioned directory). Version-looking static
components in any other position are left untouched.

New coverage: a repository named tool-0.5.1 keeps its name in the derived
URL (release download and archive tag shapes, plus a non-GitHub host); a
segment that is exactly a version token is still treated as a version
position. The test fails on the pre-fix code (verified by stashing the src
change); full suite and ruff unchanged otherwise.

Assisted-by: opencode (model: Qwen3.8-27B (local), autonomous)

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.

This branch has not been deployed

No deployments
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

3 participants