{"record":{"id":"e0446962df1524b2","repo":"elastic/elasticsearch","slug":"expected-a-single-original-jar-but-found-oldja","errorCode":null,"errorMessage":"Expected a single original jar, but found: ${oldJarNames}","messagePattern":"Expected a single original jar, but found: (.+?)","errorType":"validation","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/JarApiComparisonTask.java","lineNumber":72,"sourceCode":" *     a non-exported package considered part of the stable api?</li>\n *     <li>Changing method types to their superclass or return types to an implementation\n *     class will be considered a change by this approach, even though that doesn't break\n *     an API.</li>\n *     <li>Finally, moving a method up the class hierarchy is not really a breaking change,\n *     but it will trip this test.</li>\n * </ol>\n */\n@CacheableTask\npublic abstract class JarApiComparisonTask extends PrecommitTask {\n\n    @TaskAction\n    public void compare() {\n        FileCollection fileCollection = getOldJar().get();\n        File newJarFile = getNewJar().get().getSingleFile();\n\n        Set<String> oldJarNames = fileCollection.getFiles().stream().map(File::getName).collect(Collectors.toSet());\n        if (oldJarNames.size() > 1) {\n            throw new IllegalStateException(\"Expected a single original jar, but found: \" + oldJarNames);\n        }\n        if (oldJarNames.contains(newJarFile.getName())) {\n            throw new IllegalStateException(\n                \"We should be comparing different jars, but original and new jars were both: \" + newJarFile.getAbsolutePath()\n            );\n        }\n\n        JarScanner oldJS = new JarScanner(getOldJar().get().getSingleFile().getPath());\n        JarScanner newJS = new JarScanner(newJarFile.getPath());\n        try {\n            JarScanner.compareSignatures(oldJS.jarSignature(), newJS.jarSignature());\n        } catch (IOException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    @CompileClasspath\n    public abstract Property<FileCollection> getOldJar();","sourceCodeStart":54,"sourceCodeEnd":90,"githubUrl":"https://github.com/elastic/elasticsearch/blob/db6a809a667c081ca1dc7500389d26975573215f/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/JarApiComparisonTask.java#L54-L90","documentation":"Thrown as IllegalStateException at the start of JarApiComparisonTask.compare() when the oldJar FileCollection resolves to more than one file. The API comparison logic compares exactly one old (reference) jar against one new jar; multiple old jars make the comparison ambiguous and indicate a configuration error in how the old jar dependency was declared.","triggerScenarios":"At line 70, fileCollection.getFiles() is mapped to a Set<String> of filenames. If size > 1 (line 71), the exception fires. This happens when getOldJar() resolves a configuration that contains multiple jars — e.g., a compile classpath instead of a single artifact, or a configuration that pulls transitive jars.","commonSituations":"The oldJar property was wired to a resolvable configuration that includes transitive dependencies; the DRA/BWC dependency resolution returned both the jar and a parent directory artifact; a Copy task's output directory leaked into the file collection (noted in the code comment at line 611-613); misconfiguration where oldJar is a classpath rather than a single artifact.","solutions":["Ensure getOldJar() resolves to a configuration containing exactly one jar — use an artifact-specific configuration, not a transitive classpath.","If using a DRA download, ensure the task type's getOutputs() contains only the jar (the code comment explains DownloadMavenJarTask was created for this reason — line 616).","Inspect the set in the error message to identify the extra files; trace which dependency/configuration added them.","Use builtBy and artifact type attributes to narrow the configuration to a single file."],"exampleFix":"// before: oldJar resolves a transitive configuration\ngetOldJar().from(configurations.named(\"bwcOldApi\"))  // pulls transitives → multiple files\n\n// after: create a single-artifact configuration\nconfigurations.register(\"bwcOldApiJar\") {\n  it.canBeConsumed = false\n  it.canBeResolved = true\n  it.dependencies.add(project.dependencies.create(\"org.elasticsearch:elasticsearch-server:${oldVersion}@jar\"))\n}\ngetOldJar().from(configurations.named(\"bwcOldApiJar\"))","handlingStrategy":"validation","validationCode":"Set<File> files = getOldJar().get().getFiles();\nif (files.size() != 1) {\n    throw new IllegalStateException(\"oldJar must resolve to exactly one file, found \" + files.size() + \": \" + files);\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Wire getOldJar() to a single-artifact configuration, not a transitive classpath.","Use artifact type attributes (@jar) to ensure a single file resolves.","Use a dedicated task type (like DownloadMavenJarTask) whose outputs contain only the jar.","Test the configuration resolves to one file before wiring it to the comparison task."],"tags":["api-comparison","bwc","configuration","validation","gradle"],"backgroundTag":null,"analyzedSha":"db6a809a667c081ca1dc7500389d26975573215f","analyzedAt":"2026-08-12T01:39:14.192Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}