Skip to content

[Bug]: bundle build reports success but omits a symlinked bundle.yml/README.md, producing an artifact that cannot be installed #5088

Open

Description

@hnipps

Bug Description

build_bundle checks that bundle.yml and README.md exist and validates the manifest by following symlinks (src/specify_cli/bundles/packager.py:40-51). _collect_files then skips every symlinked file (packager.py:143-145), including one whose target is inside the bundle directory. When either file is a symlink, the build succeeds but leaves it out of the zip. specify bundle install <artifact.zip> then rejects the artifact.

Steps to Reproduce

From the root of a fresh clone after uv sync, save the script below as repro.py and run uv run python -I repro.py.

import subprocess, tempfile, zipfile
from pathlib import Path

work = Path(tempfile.mkdtemp())
bundle = work / "mybundle"
(bundle / "shared").mkdir(parents=True)
(bundle / "shared" / "bundle.yml").write_text(
    'schema_version: "1.0"\n'
    'bundle: {id: demo, name: D, version: "1.0.0", role: dev, description: d, author: a, license: MIT}\n'
    'requires: {speckit_version: ">=0.1.0"}\n'
)
(bundle / "shared" / "README.md").write_text("# demo\n")
(bundle / "bundle.yml").symlink_to("shared/bundle.yml")  # symlinks inside the bundle
(bundle / "README.md").symlink_to("shared/README.md")

def specify(*args, cwd):
    run = subprocess.run(["specify", *args], cwd=cwd, capture_output=True, text=True,
                         env={"PATH": __import__("os").environ["PATH"], "COLUMNS": "200"})
    print(f"$ specify {' '.join(args)}  (exit {run.returncode})")
    print((run.stdout + run.stderr).strip())

specify("bundle", "build", "--path", str(bundle), "--output", str(work / "dist"), cwd=work)
artifact = work / "dist" / "demo-1.0.0.zip"
print("zip members:", zipfile.ZipFile(artifact).namelist())
project = work / "project"; project.mkdir()
specify("bundle", "install", str(artifact), "--offline", cwd=project)

Expected Behavior

Either the artifact contains bundle.yml and README.md at its root, or the build fails with a clear error:

  • docs/reference/bundles.md:220: "The artifact embeds the manifest and can be installed directly with specify bundle install <artifact.zip>."
  • src/specify_cli/bundles/packager.py:3: "specify bundle build zips the manifest, README, and any local assets".

Actual Behavior

$ specify bundle build --path /tmp/tmpeh5mps96/mybundle --output /tmp/tmpeh5mps96/dist  (exit 0)
✓ Built demo-1.0.0.zip (2 files) → /tmp/tmpeh5mps96/dist/demo-1.0.0.zip
zip members: ['shared/README.md', 'shared/bundle.yml']
$ specify bundle install /tmp/tmpeh5mps96/dist/demo-1.0.0.zip --offline  (exit 1)
Error: Artifact '/tmp/tmpeh5mps96/dist/demo-1.0.0.zip' does not contain a bundle.yml.

Error Logs

Error: Artifact '/tmp/tmpeh5mps96/dist/demo-1.0.0.zip' does not contain a bundle.yml.

Specify CLI Version

1.2.4.dev0 (main at 27d14e2)

AI Agent

Not applicable

Operating System

Linux 6.18 (Ubuntu container)

Python Version

Python 3.12.3

Additional Context

Skipping symlinks protects against files that point outside the bundle. The problem is only that the checks before it and the packaging step disagree about which files count.

Also reproduced on macOS (Darwin 25.6, arm64) with Python 3.14.4 and uv run python -I repro.py against 27d14e2; the output matches the above.

AI Disclosure

Found and reproduced with Claude Code (model: Claude Opus 4.8), run autonomously under human supervision. The human reporter re-ran every reproduction above before filing.

Activity

  1. added
    triage-can-waitVerdict: valid and in-scope but deprioritized; held behind the evidence gate
    on Oct 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    triage-can-waitVerdict: valid and in-scope but deprioritized; held behind the evidence gate

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions