{"record":{"id":"dd2da0d3fe71d7fe","repo":"jdx/mise","slug":"index-json-lists-no-manifests","errorCode":null,"errorMessage":"{}: index.json lists no manifests","messagePattern":"(.+?): index\\.json lists no manifests","errorType":"exception","errorClass":"eyre::Report","httpStatus":null,"severity":"error","filePath":"src/oci/registry.rs","lineNumber":931,"sourceCode":"    reference: &str,\n    update_index: bool,\n) -> Result<PushSummary> {\n    eyre::ensure!(\n        !crate::config::Settings::get().offline(),\n        \"offline mode is enabled\"\n    );\n    let r = Reference::parse(reference)?;\n    let layout = ImageLayout {\n        root: image_dir.to_path_buf(),\n    };\n\n    // Resolve the layout's single manifest. `mise oci build` always writes\n    // exactly one manifest into index.json.\n    let index_bytes = crate::file::read(image_dir.join(\"index.json\"))?;\n    let index: ImageIndex = serde_json::from_slice(&index_bytes).wrap_err(\"parsing index.json\")?;\n    let manifest_desc = match index.manifests.as_slice() {\n        [one] => one,\n        [] => bail!(\"{}: index.json lists no manifests\", image_dir.display()),\n        many => bail!(\n            \"{}: index.json lists {} manifests; multi-manifest layouts are not supported\",\n            image_dir.display(),\n            many.len()\n        ),\n    };\n    let manifest_bytes = layout.read_blob(&manifest_desc.digest)?;\n    let manifest: ImageManifest =\n        serde_json::from_slice(&manifest_bytes).wrap_err(\"parsing image manifest blob\")?;\n\n    // Cross-repo mount source: the base image's repository, when it lives on\n    // the destination registry (and isn't the destination repo itself).\n    let mount_from = manifest\n        .annotations\n        .get(ANNOTATION_BASE_NAME)\n        .and_then(|name| Reference::parse(name).ok())\n        .filter(|base| base.registry == r.registry && base.repository != r.repository)\n        .map(|base| base.repository);","sourceCodeStart":913,"sourceCodeEnd":949,"githubUrl":"https://github.com/jdx/mise/blob/9dcfcaa0dc8747a2577d3270b69bb9d8313b2807/src/oci/registry.rs#L913-L949","documentation":"`mise oci push` reads the image layout directory produced by `mise oci build` and expects index.json to reference exactly one manifest. An empty manifests array means the layout has no image content — the documented contract (\"build always writes exactly one manifest into index.json\") is broken, so push refuses rather than upload nothing.","triggerScenarios":"Pointing push at an image directory whose build failed or was interrupted before the manifest was written; a hand-emptied or incorrectly copied layout directory; passing the wrong --image-dir path that happens to contain an empty index.json.","commonSituations":"Build and push split across CI jobs where the build job failed but push ran anyway; artifacts copied incomidentally (index.json copied, blobs dir not); manual tampering with the layout directory.","solutions":["Re-run `mise oci build` to regenerate a complete layout, then push","Make the push step conditional on build success in CI (fail-fast between jobs)","Verify the layout before pushing: index.json's manifests array should list exactly one entry with a digest whose blob exists under blobs/"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Validate the layout before invoking `mise oci push`.\nfn layout_has_single_manifest(image_dir: &Path) -> Result<bool, String> {\n    let bytes = std::fs::read(image_dir.join(\"index.json\")).map_err(|e| e.to_string())?;\n    let index: serde_json::Value = serde_json::from_slice(&bytes).map_err(|e| e.to_string())?;\n    let n = index[\"manifests\"].as_array().map(|a| a.len()).unwrap_or(0);\n    Ok(n == 1)\n}\n\nif !layout_has_single_manifest(&image_dir)? {\n    anyhow::bail!(\"layout incomplete — re-run `mise oci build` before push\");\n}","typeGuard":"fn is_pushable_layout(image_dir: &Path) -> bool {\n    std::fs::read(image_dir.join(\"index.json\"))\n        .ok()\n        .and_then(|b| serde_json::from_slice::<serde_json::Value>(&b).ok())\n        .and_then(|v| v[\"manifests\"].as_array().map(|a| a.len() == 1))\n        .unwrap_or(false)\n}","tryCatchPattern":null,"preventionTips":["Gate the push CI step on build success (needs: build job / set -e in shared scripts)","Never hand-edit the image directory between build and push","Treat an empty manifests array as a corrupted artifact: regenerate, do not patch"],"tags":["mise","oci","push","image-layout","index-json","corrupt-artifact"],"backgroundTag":"oci-image-layout-invalid","analyzedSha":"9dcfcaa0dc8747a2577d3270b69bb9d8313b2807","analyzedAt":"2026-08-17T14:28:50.624Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}