{"record":{"id":"6a8b4b48e2734e79","repo":"github/spec-kit","slug":"kind-component-id-is-pinned-to-version-pinn","errorCode":null,"errorMessage":"{kind} '{component_id}' is pinned to version {pinned} in the bundle manifest, but the resolved version is {actual}. Update the bundle's pinned version or the source before installing.","messagePattern":"(.+?) '(.+?)' is pinned to version (.+?) in the bundle manifest, but the resolved version is (.+?)\\. Update the bundle's pinned version or the source before installing\\.","errorType":"exception","errorClass":"BundlerError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/bundler/services/primitives.py","lineNumber":56,"sourceCode":"    Bundle manifests pin component versions for reproducibility; installing\n    whatever the resolved source (catalog *or* bundled asset) provides would\n    silently violate the pin. When the source advertises no version we cannot\n    enforce the pin, so installation proceeds (the source, not the bundler,\n    owns that gap).\n    \"\"\"\n    if not pinned or advertised is None:\n        return\n    actual = str(advertised).strip()\n    if not actual:\n        return\n    from ..lib.versioning import parse_version\n\n    try:\n        matches = parse_version(actual) == parse_version(pinned)\n    except BundlerError:\n        matches = actual == str(pinned).strip()\n    if not matches:\n        raise BundlerError(\n            f\"{kind} '{component_id}' is pinned to version {pinned} in the bundle \"\n            f\"manifest, but the resolved version is {actual}. Update the bundle's \"\n            \"pinned version or the source before installing.\"\n        )\n\n\ndef _bundled_manifest_version(manifest_path: Path, root_key: str) -> str | None:\n    \"\"\"Best-effort read of a bundled asset's declared version from its manifest.\n\n    Returns ``None`` when the manifest is missing/unreadable/invalid, which\n    ``_assert_pinned_version`` treats as \"cannot enforce\" (proceed) — matching\n    the catalog \"advertises no version\" escape hatch.\n    \"\"\"\n    try:\n        import yaml\n\n        data = yaml.safe_load(manifest_path.read_text(encoding=\"utf-8\"))\n        if isinstance(data, dict):","sourceCodeStart":38,"sourceCodeEnd":74,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/bundler/services/primitives.py#L38-L74","documentation":"The bundle manifest pins a component to a specific version, but the version actually resolved at install time (from the bundled asset's own manifest or the catalog entry) differs. _assert_pinned_version compares via parse_version when both parse cleanly, falling back to exact string comparison, and raises to prevent silently installing something other than what the bundle was tested against.","triggerScenarios":"Installing a bundle where component.version in bundle.yml says e.g. 1.2.0 but the catalog has 1.3.0, or the bundled preset.yml/extension.yml inside the artifact declares a different version. Also fires when the pinned string is not parseable and does not string-match the advertised value.","commonSituations":"A component published a new version after the bundle was built; the bundle was built by copying an older component into the artifact while pinning the newer number; hand-edited bundle.yml with a stale version; '1.0' vs '1.0.0' style mismatches when one side fails version parsing.","solutions":["Decide which version is correct: if you want the resolved one, update the component's pinned version in bundle.yml and rebuild the bundle.","If the pin is correct, refresh the bundled asset source so the artifact contains that exact version, then rebuild.","For '1.0' vs '1.0.0' mismatches, normalize the pinned string in bundle.yml to the same format the catalog/asset advertises.","Re-run `specify bundle validate` and `specify bundle build` after editing, then reinstall."],"exampleFix":"# before (bundle.yml)\ncomponents:\n  - kind: presets\n    id: my-preset\n    version: 1.2.0   # catalog now serves 1.3.0\n\n# after (accept the new version)\ncomponents:\n  - kind: presets\n    id: my-preset\n    version: 1.3.0","handlingStrategy":"validation","validationCode":"# Before installing, confirm the pin still matches what the catalog serves\nfrom specify_cli.presets import PresetCatalog\n\ncatalog = PresetCatalog(project_root)\ninfo = catalog.get_pack_info(component_id)\nif info and component_version and str(info.get(\"version\", \"\")).strip() != component_version:\n    print(f\"pin {component_version} != catalog {info.get('version')}; update bundle.yml\")","typeGuard":null,"tryCatchPattern":"from specify_cli.bundler.core import BundlerError\n\ntry:\n    install_bundle(project_root, plan, installer)\nexcept BundlerError as exc:\n    if \"pinned to version\" in str(exc):\n        # message names component, pinned, and actual; decide which to keep\n        reconcile_pin_and_rebuild(plan)","preventionTips":["Pin components to exact versions and rebuild bundles promptly after any component release you adopt.","Run a periodic drift check comparing bundle.yml pins against catalog versions.","Use the same version format (e.g. semver X.Y.Z) on both sides to avoid parse-fallback string mismatches."],"tags":["bundler","versioning","pinned-version","drift"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}