{"record":{"id":"650c01d0fa878f71","repo":"github/spec-kit","slug":"no-extension-yml-found-in-archive","errorCode":null,"errorMessage":"No extension.yml found in archive","messagePattern":"No extension\\.yml found in archive","errorType":"validation","errorClass":"ValidationError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/extensions/__init__.py","lineNumber":2723,"sourceCode":"                archive_file=archive_file,\n                source_name=source_name,\n                content_type=content_type,\n                error_type=ValidationError,\n            )\n\n            # Find extension directory (may be nested)\n            extension_dir = temp_path\n            manifest_path = extension_dir / \"extension.yml\"\n\n            # Check if manifest is in a subdirectory\n            if not manifest_path.exists():\n                subdirs = [d for d in temp_path.iterdir() if d.is_dir()]\n                if len(subdirs) == 1:\n                    extension_dir = subdirs[0]\n                    manifest_path = extension_dir / \"extension.yml\"\n\n            if not manifest_path.exists():\n                raise ValidationError(\"No extension.yml found in archive\")\n\n            # Install from extracted directory\n            return self.install_from_directory(\n                extension_dir, speckit_version, priority=priority, force=force\n            )\n\n    def _config_root_is_contained(self, specify_dir: Path) -> bool:\n        \"\"\"Report whether `.specify` is a real directory inside the project.\n\n        Checked component by component so a symlink anywhere on the path is\n        rejected before it becomes the containment root. A missing `.specify`\n        is fine: scaffolding creates it under the project root.\n        \"\"\"\n        try:\n            root = self.project_root.resolve()\n        except OSError:\n            return False\n        current = self.project_root","sourceCodeStart":2705,"sourceCodeEnd":2741,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/extensions/__init__.py#L2705-L2741","documentation":"Thrown by ExtensionManager.install_from_zip after extracting an archive to a temp directory: neither the extraction root nor the single top-level subdirectory contains an extension.yml manifest. The installer only looks one level deep, so a manifest nested further down (or absent entirely) is treated as an invalid extension package.","triggerScenarios":"Calling install_from_zip (directly or via `specify extension install <path-or-url>` on a .zip/.tar.gz) where the archive has no extension.yml at its root and either has multiple top-level directories or a single subdir that also lacks extension.yml.","commonSituations":"Zipping the parent workspace folder instead of the extension package; an archive with multiple nested wrappers (dist/my-ext-v1.0/my-ext/extension.yml); downloading a source tarball from GitHub whose root is a repo folder plus .github, tests, etc.; forgetting to include the manifest at all.","solutions":["Rebuild the archive so extension.yml sits at the root or inside exactly one top-level directory: `cd my-ext && zip -r ../my-ext.zip .`","Inspect the archive layout first: `unzip -l my-ext.zip` or `tar -tzf my-ext.tar.gz` and confirm the manifest's depth","If nesting is unavoidable, extract locally and install from the directory instead (install_from_directory), pointing at the folder that holds extension.yml","If packaging via GitHub 'Source code' archives, add a release artifact or CI step that repacks with the manifest at the root"],"exampleFix":"# before: zip created from parent directory\nzip -r my-ext.zip my-ext/          # root = my-ext/, extension.yml inside -> OK only if single dir\n\n# problem: multiple dirs at root\nzip -r bundle.zip ext/ docs/ tests/ # root has 3 dirs, manifest not found\n\n# after: single top-level dir containing the manifest, or manifest at root\ncd my-ext && zip -r ../my-ext.zip .","handlingStrategy":"validation","validationCode":"import zipfile, tarfile\nfrom pathlib import Path\n\ndef archive_has_root_manifest(path: Path) -> bool:\n    \"\"\"True if extension.yml is at archive root or its single top-level dir.\"\"\"\n    names = []\n    if zipfile.is_zipfile(path):\n        with zipfile.ZipFile(path) as z:\n            names = z.namelist()\n    elif tarfile.is_tarfile(path):\n        with tarfile.open(path) as t:\n            names = t.getnames()\n    else:\n        return False\n    tops = {n.split('/')[0] for n in names if n.strip()}\n    return 'extension.yml' in names or (\n        len(tops) == 1 and f'{next(iter(tops))}/extension.yml' in names\n    )\n\nassert archive_has_root_manifest(Path('my-ext.zip'))","typeGuard":null,"tryCatchPattern":"from specify_cli.extensions import ExtensionError\n\ntry:\n    manager.install_from_zip(archive, speckit_version)\nexcept ExtensionError as e:\n    if 'No extension.yml found' in str(e):\n        # repackage with manifest at root, or install from directory\n        ...","preventionTips":["Standardize extension packaging: manifest at archive root or one top-level dir","Add a CI packaging step that asserts archive_has_root_manifest before publishing"],"tags":["extensions","packaging","zip","manifest"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}