[Bug]: bundle build reports success but omits a symlinked bundle.yml/README.md, producing an artifact that cannot be installed #5088
Description
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 withspecify bundle install <artifact.zip>."src/specify_cli/bundles/packager.py:3: "specify bundle buildzips 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