{"record":{"id":"53c5f03920472a1a","repo":"github/spec-kit","slug":"no-readme-md-found-in-bundle-dir-every-bundle","errorCode":null,"errorMessage":"No README.md found in '{bundle_dir}'. Every bundle must ship a README.md describing it.","messagePattern":"No README\\.md found in '(.+?)'\\. Every bundle must ship a README\\.md describing it\\.","errorType":"exception","errorClass":"BundlerError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/bundler/services/packager.py","lineNumber":46,"sourceCode":"@dataclass\nclass BuildResult:\n    artifact_path: Path\n    file_count: int\n\n\ndef build_bundle(\n    bundle_dir: Path,\n    output_dir: Path | None = None,\n) -> BuildResult:\n    bundle_dir = Path(bundle_dir).resolve()\n    manifest_path = bundle_dir / \"bundle.yml\"\n    if not manifest_path.exists():\n        raise BundlerError(f\"No bundle.yml found in '{bundle_dir}'.\")\n\n    # The artifact contract requires a human-facing README.md alongside the\n    # manifest; refuse early rather than publish a bundle with no description.\n    if not (bundle_dir / \"README.md\").exists():\n        raise BundlerError(\n            f\"No README.md found in '{bundle_dir}'. Every bundle must ship a \"\n            \"README.md describing it.\"\n        )\n\n    manifest = BundleManifest.from_file(manifest_path)\n    report = validate_manifest(manifest)\n    if not report.ok:\n        raise BundlerError(\n            \"Refusing to build an invalid manifest. Run 'specify bundle validate' \"\n            \"and fix:\\n  - \" + \"\\n  - \".join(report.errors)\n        )\n\n    out_dir = Path(output_dir).resolve() if output_dir else bundle_dir\n    out_dir.mkdir(parents=True, exist_ok=True)\n    artifact_name = f\"{manifest.bundle.id}-{manifest.bundle.version}.zip\"\n    artifact_path = out_dir / artifact_name\n    # Defense in depth: even though validate_manifest() rejects unsafe ids, make\n    # sure a crafted id cannot push the artifact outside the output directory.","sourceCodeStart":28,"sourceCodeEnd":64,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/bundler/services/packager.py#L28-L64","documentation":"The bundle artifact contract requires a human-facing README.md next to bundle.yml; build_bundle() refuses to publish a bundle with no description. This is a deliberate quality gate, not an incidental file check — it fires even when bundle.yml itself is valid.","triggerScenarios":"Calling build_bundle() on a directory that has bundle.yml but no README.md — typically an author who wrote the manifest and components but never documented the bundle.","commonSituations":"New bundle authors; README saved as readme.md or README.rst (case/extension mismatch); README placed in a subdirectory instead of the bundle root.","solutions":["Create <bundle_dir>/README.md describing what the bundle installs and why (exact name, top level of the bundle dir).","Check for case or extension mismatches: readme.md/README.rst will not satisfy the check.","Re-run `specify bundle build` after adding the file."],"exampleFix":"echo '# My Bundle\\nInstalls the X preset and Y extension.' > <bundle_dir>/README.md","handlingStrategy":"validation","validationCode":"from pathlib import Path\n\nassert (bundle_dir / \"README.md\").is_file(), \"bundle needs a top-level README.md\"\nbuild_bundle(bundle_dir)","typeGuard":null,"tryCatchPattern":"from specify_cli.bundler.core import BundlerError\n\ntry:\n    build_bundle(bundle_dir)\nexcept BundlerError as exc:\n    if \"README.md\" in str(exc):\n        (bundle_dir / \"README.md\").write_text(\"# Bundle\\nTODO: describe.\")\n    raise","preventionTips":["Treat README.md as part of the bundle manifest contract — create it the moment you create bundle.yml.","Use the exact name README.md at the bundle root; case and extension matter.","Add a repo lint/check that every bundle directory ships both files."],"tags":["bundler","build","readme","contract"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}