diff --git a/.github/actions/detect-file-changes/action.yml b/.github/actions/detect-file-changes/action.yml index 6d3e19bbe..9c5cb25d4 100644 --- a/.github/actions/detect-file-changes/action.yml +++ b/.github/actions/detect-file-changes/action.yml @@ -13,5 +13,5 @@ runs: using: "node20" main: "detect-file-changes.js" outputs: - changes_detected: + changed: description: "Returns `true` if any changed file matches the specified patterns, `false` otherwise." diff --git a/.github/workflows/index-file-validation.yml b/.github/workflows/index-file-validation.yml new file mode 100644 index 000000000..cea5fe7bb --- /dev/null +++ b/.github/workflows/index-file-validation.yml @@ -0,0 +1,114 @@ +name: "Validate index.json files" + +on: + pull_request: + +jobs: + validate-json: + name: "πŸ“‹ Validate changed JSON files" + runs-on: ubuntu-22.04 + timeout-minutes: 10 + + steps: + - name: "☁️ Checkout repository" + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: "πŸ”Ž Detect relevant file changes" + id: filter + uses: ./.github/actions/detect-file-changes + with: + file-patterns: | + - 'metadata/index.json' + - 'metadata/*/*/index.json' + - 'tests/src/index.json' + - 'tests/src/*/*/*/index.json' + + - name: "πŸ”§ Setup Java" + if: steps.filter.outputs.changed == 'true' + uses: actions/setup-java@v4 + with: + distribution: 'graalvm' + java-version: '21' + + - name: "πŸ•ΈοΈ Populate matrix" + id: set-matrix + if: steps.filter.outputs.changed == 'true' + run: | + ./gradlew generateChangedIndexFileMatrix -PbaseCommit=${{ github.event.pull_request.base.sha }} -PnewCommit=${{ github.event.pull_request.head.sha }} + + echo "Matrix output:" + echo "${{ steps.set-matrix.outputs.matrix }}" + + - name: "Setup Python" + if: steps.filter.outputs.changed == 'true' + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: "Install validation tools" + if: steps.filter.outputs.changed == 'true' + run: | + pip install jq check-jsonschema + + - name: "Check that the changed index.json files conform to their schemas" + if: steps.filter.outputs.changed == 'true' + run: | + set -euo pipefail + + MATRIX='${{ steps.set-matrix.outputs.matrix }}' + + # Convert matrix JSON into an array of file paths + FILES=($(echo "$MATRIX" | jq -r '.index_files[]')) + + if [ ${#FILES[@]} -eq 0 ]; then + echo "No changed index.json files to validate." + exit 0 + fi + + # Track failures + FAILURES=() + + for FILE in "${FILES[@]}"; do + + # Determine schema for each file + if [[ "$FILE" == "metadata/index.json" ]]; then + SCHEMA="schemas/metadata-root-index-schema-v1.0.0.json" + elif [[ "$FILE" == metadata/*/*/index.json ]]; then + SCHEMA="schemas/metadata-library-index-schema-v1.0.0.json" + elif [[ "$FILE" == "tests/src/index.json" ]]; then + SCHEMA="schemas/tests-root-index-schema-v1.0.0.json" + elif [[ "$FILE" == tests/src/*/*/*/index.json ]]; then + SCHEMA="schemas/tests-project-index-schema-v1.0.0.json" + else + echo "ℹ️ Skipping: $FILE (no schema mapping)" + continue + fi + + echo "πŸ” Validating $FILE using $SCHEMA" + + # Step 1: Check JSON well-formedness + if ! jq -e . "$FILE" >/dev/null; then + echo "❌ $FILE is not valid JSON" + FAILURES+=("$FILE (invalid JSON)") + continue + fi + + # Step 2: Validate against schema + if ! check-jsonschema --schemafile "$SCHEMA" "$FILE"; then + FAILURES+=("$FILE (schema failure)") + continue + fi + + echo "βœ… $FILE validated successfully" + done + + # Fail at the end if any errors occurred + if [ ${#FAILURES[@]} -ne 0 ]; then + echo "::error ::Some files failed validation:" + for f in "${FAILURES[@]}"; do + echo "::error :: $f" + done + exit 1 + fi diff --git a/.github/workflows/library-and-framework-list-validation.yml b/.github/workflows/library-and-framework-list-validation.yml index 70343b417..703d5764e 100644 --- a/.github/workflows/library-and-framework-list-validation.yml +++ b/.github/workflows/library-and-framework-list-validation.yml @@ -17,7 +17,8 @@ jobs: uses: ./.github/actions/detect-file-changes with: file-patterns: | - - 'library-and-framework-list*.json' + - 'library-and-framework-list.json' + - 'schemas/library-and-framework-list-schema*.json' - uses: actions/setup-python@v4 with: @@ -43,4 +44,4 @@ jobs: if: steps.filter.outputs.changed == 'true' run: | pip install check-jsonschema - check-jsonschema --schemafile library-and-framework-list-schema.json library-and-framework-list.json + check-jsonschema --schemafile schemas/library-and-framework-list-schema-v1.0.0.json library-and-framework-list.json diff --git a/build.gradle b/build.gradle index abd10a873..c2fc5b6b8 100644 --- a/build.gradle +++ b/build.gradle @@ -99,10 +99,17 @@ tasks.register('package', Zip) { task -> task.destinationDirectory = layout.buildDirectory from(tck.metadataRoot) - // library-and-framework-list.json is used by Native Build Tools to provide additional - // information on library and framework support to the native image Build Report + from(project.rootDir) { + // library-and-framework-list.json is used by Native Build Tools to provide additional + // information on library and framework support to the native image Build Report include("library-and-framework-list.json") + + // Schemas are included in the ZIP root to allow Native Build Tools to verify + // structural compatibility with this release of GraalVM Reachability Metadata + include("schemas/metadata-root-index-schema-v1.0.0.json") + include("schemas/metadata-library-index-schema-v1.0.0.json") + include("schemas/library-and-framework-list-schema-v1.0.0.json") } } diff --git a/ci.json b/ci.json index 24aad6e87..8a7c2d905 100644 --- a/ci.json +++ b/ci.json @@ -15,5 +15,9 @@ "generateMatrixMatchingCoordinates": { "java": ["25","latest-ea"], "os": ["ubuntu-latest"] + }, + "generateChangedIndexFileMatrix": { + "java": ["latest-ea"], + "os": ["ubuntu-latest"] } } diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index f52b8d6b0..c803a4afe 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -344,4 +344,4 @@ Where: **Note:** To pass format and style checks, please run `sorted="$(jq -s '.[] | sort_by(.artifact)' library-and-framework-list.json)" && echo -E "${sorted}" > library-and-framework-list.json` before submitting a PR. -**Note:** The entries you add will be validated against [library-and-framework-list-schema.json](https://github.com/oracle/graalvm-reachability-metadata/blob/master/library-and-framework-list-schema.json) +**Note:** The entries you add will be validated against [library-and-framework-list-schema-v1.0.0.json](https://github.com/oracle/graalvm-reachability-metadata/blob/master/schemas/library-and-framework-list-schema-v1.0.0.json) diff --git a/library-and-framework-list-schema.json b/schemas/library-and-framework-list-schema-v1.0.0.json similarity index 100% rename from library-and-framework-list-schema.json rename to schemas/library-and-framework-list-schema-v1.0.0.json diff --git a/schemas/metadata-library-index-schema-v1.0.0.json b/schemas/metadata-library-index-schema-v1.0.0.json new file mode 100644 index 000000000..1e09a68f2 --- /dev/null +++ b/schemas/metadata-library-index-schema-v1.0.0.json @@ -0,0 +1,100 @@ +{ + "$id": "https://github.com/oracle/graalvm-reachability-metadata/schemas/metadata-library-index.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "Schema for metadata///index.json. Each entry describes a metadata bundle for a range of library versions.", + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "module", + "metadata-version", + "tested-versions" + ], + "properties": { + "module": { + "$ref": "#/$defs/moduleCoordinate", + "description": "Maven coordinates in the form ':'." + }, + "metadata-version": { + "type": "string", + "minLength": 1, + "description": "Subdirectory name where the metadata files for this entry reside, e.g. '7.1.0.Final'." + }, + "tested-versions": { + "type": "array", + "description": "Explicitly tested upstream library versions that this metadata is known to support.", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1 + } + }, + "latest": { + "type": "boolean", + "description": "Marks this entry as the latest/default metadata for currently supported versions." + }, + "default-for": { + "type": "string", + "description": "Java regular expression describing the version range for which this entry should be used by default (e.g. '7\\\\.1\\\\..*')." + }, + "override": { + "type": "boolean", + "description": "When true, expresses the intent to exclude outdated built-in metadata shipped with Native Image for the matched versions." + }, + "skipped-versions": { + "type": "array", + "description": "Versions explicitly excluded from support, each with a reason.", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ "version", "reason" ], + "properties": { + "version": { + "type": "string", + "minLength": 1 + }, + "reason": { + "type": "string", + "minLength": 1 + } + } + } + } + } + }, + "$defs": { + "moduleCoordinate": { + "type": "string", + "pattern": "^[^:]+:[^:]+$" + } + }, + "examples": [ + [ + { + "metadata-version": "0.0.1", + "module": "org.example:library", + "tested-versions": [ "0.0.1", "0.0.2" ], + "default-for": "0\\.0\\..*" + }, + { + "latest": true, + "metadata-version": "1.0.0", + "module": "org.example:library", + "tested-versions": [ "1.0.0", "1.1.0-M1", "1.1.0" ] + }, + { + "metadata-version": "1.19.0", + "module": "io.opentelemetry:opentelemetry-exporter-logging", + "tested-versions": [ "1.19.0" ], + "override": true, + "skipped-versions": [ + { "version": "1.0.5", "reason": "Known incompatible API change." } + ] + } + ] + ] +} diff --git a/schemas/metadata-root-index-schema-v1.0.0.json b/schemas/metadata-root-index-schema-v1.0.0.json new file mode 100644 index 000000000..0a5703ec3 --- /dev/null +++ b/schemas/metadata-root-index-schema-v1.0.0.json @@ -0,0 +1,67 @@ +{ + "$id": "https://github.com/oracle/graalvm-reachability-metadata/schemas/metadata-root-index.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "Schema for metadata/index.json. This file lists modules known to the repository and optionally the directory where their metadata is stored, the allowed package prefixes for metadata, and inter-module requirements. See docs/CONTRIBUTING.md (Metadata structure).", + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["module"], + "properties": { + "module": { + "$ref": "#/$defs/moduleCoordinate", + "description": "Maven coordinates for the module in the form ':'." + }, + "directory": { + "type": "string", + "minLength": 1, + "pattern": "^[^\\s].*$", + "description": "Repository-relative path under 'metadata/' containing this module's metadata (e.g. 'org.example/library'). If omitted, the entry may reference requirements only." + }, + "allowed-packages": { + "type": "array", + "description": "List of package (or fully-qualified name) prefixes considered valid sources of metadata entries for this module. Used to filter-in relevant JSON entries.", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1 + } + }, + "requires": { + "type": "array", + "description": "Optional list of module coordinates this module depends on. Each item is ':'.", + "minItems": 1, + "uniqueItems": true, + "items": { + "$ref": "#/$defs/moduleCoordinate" + } + } + } + }, + "$defs": { + "moduleCoordinate": { + "type": "string", + "pattern": "^[^:]+:[^:]+$" + } + }, + "examples": [ + [ + { + "directory": "org.example/library", + "module": "org.example:library" + }, + { + "allowed-packages": ["org.package.name"], + "module": "org.example:dependant-library", + "requires": ["org.example:library"] + }, + { + "allowed-packages" : [ "org.hibernate", "jakarta" ], + "directory" : "org.hibernate.orm/hibernate-envers", + "module" : "org.hibernate.orm:hibernate-envers", + "requires" : [ "org.hibernate.orm:hibernate-core" ] + } + ] + ] +} diff --git a/schemas/tests-project-index-schema-v1.0.0.json b/schemas/tests-project-index-schema-v1.0.0.json new file mode 100644 index 000000000..089ec5720 --- /dev/null +++ b/schemas/tests-project-index-schema-v1.0.0.json @@ -0,0 +1,30 @@ +{ + "$id": "https://github.com/oracle/graalvm-reachability-metadata/docs/schemas/tests-project-index-schema-v1.0.0.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Test Project Index", + "description": "Schema for tests/src////index.json. This file optionally customizes the test invocation via 'test-command'. If omitted or if the property is not present, a default command will be used (gradlew nativeTest with environment overrides) per TestInvocationTask.", + "type": "object", + "additionalProperties": false, + "properties": { + "test-command": { + "type": "array", + "description": "Command line to run for this test project. Supports template parameters: , , , .", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1, + "description": "Part of the command line. May include template parameters like , , , ." + } + } + }, + "examples": [ + { + "test-command": [ + "gradle", + "nativeTest", + "-Pmetadata.dir=", + "-Plibrary.version=" + ] + } + ] +} diff --git a/schemas/tests-root-index-schema-v1.0.0.json b/schemas/tests-root-index-schema-v1.0.0.json new file mode 100644 index 000000000..8e6050987 --- /dev/null +++ b/schemas/tests-root-index-schema-v1.0.0.json @@ -0,0 +1,74 @@ +{ + "$id": "https://github.com/oracle/graalvm-reachability-metadata/docs/schemas/tests-root-index-schema-v1.0.0.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Tests Root Index", + "description": "Schema for tests/src/index.json mapping test projects to libraries and versions (see docs/CONTRIBUTING.md).", + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ "test-project-path", "libraries" ], + "properties": { + "test-project-path": { + "type": "string", + "minLength": 1, + "pattern": "^[^\\s/]+/[^\\s/]+/[^\\s/]+$", + "description": "Path under tests/src: // (e.g. 'org.hibernate.orm/hibernate-core/7.1.0.Final')." + }, + "libraries": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ "name", "versions" ], + "properties": { + "name": { + "type": "string", + "pattern": "^[^:]+:[^:]+$", + "description": "Library coordinates in the form ':' (e.g. 'org.hibernate.orm:hibernate-core')." + }, + "versions": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1 + }, + "description": "List of library versions covered by this test project (strings as published upstream, e.g. '1.1.0', '7.1.0.Final')." + } + } + }, + "description": "Libraries covered by this test project path with the versions to run." + } + } + }, + "examples": [ + [ + { + "test-project-path": "org.example/library/0.0.1", + "libraries": [ + { + "name": "org.example:library", + "versions": [ + "0.0.1" + ] + } + ] + }, + { + "test-project-path" : "org.hibernate.orm/hibernate-core/7.1.0.Final", + "libraries" : [ + { + "name" : "org.hibernate.orm:hibernate-core", + "versions" : [ + "7.1.0.Final" + ] + } + ] + } + ] + ] +} diff --git a/tests/tck-build-logic/src/main/groovy/org.graalvm.internal.tck-harness.gradle b/tests/tck-build-logic/src/main/groovy/org.graalvm.internal.tck-harness.gradle index 05a9eaaf9..513d6144e 100644 --- a/tests/tck-build-logic/src/main/groovy/org.graalvm.internal.tck-harness.gradle +++ b/tests/tck-build-logic/src/main/groovy/org.graalvm.internal.tck-harness.gradle @@ -150,6 +150,13 @@ if (project.hasProperty("baseCommit")) { } } +List diffIndexFiles = new ArrayList<>() +if (project.hasProperty("baseCommit")) { + String baseCommit = project.findProperty("baseCommit") + String newCommit = Objects.requireNonNullElse(project.findProperty("newCommit"), "HEAD") + diffIndexFiles.addAll(tck.diffIndexFiles(baseCommit, newCommit)) +} + Map loadCi() { File f = project.file("ci.json") if (!f.exists()) { @@ -223,6 +230,32 @@ Provider generateChangedCoordinatesMatrix = tasks.register("generateChange } } +// gradle generateChangedIndexFileMatrix -PbaseCommit= -PnewCommit= +Provider generateChangedIndexFileMatrix = tasks.register("generateChangedIndexFileMatrix", DefaultTask) { task -> + task.setDescription("Returns matrix definition populated with changed index.json files") + task.setGroup(METADATA_GROUP) + + task.doFirst { + if (!project.hasProperty("baseCommit")) { + throw new GradleException("Missing 'baseCommit' property! Rerun Gradle with '-PbaseCommit='") + } + + boolean noneFound = diffIndexFiles.isEmpty() + + def matrix = [ + "index_files": noneFound ? [] : diffIndexFiles + ] + matrix.putAll(matrixDefaultsFor("generateChangedIndexFileMatrix")) + + if (noneFound) { + println "No changed index.json files were found!" + } + + writeGithubOutput("matrix", JsonOutput.toJson(matrix)) + writeGithubOutput("none-found", noneFound.toString()) + } +} + // gradle generateInfrastructureChangedCoordinatesMatrix -PbaseCommit= -PnewCommit= Provider generateInfrastructureChangedCoordinatesMatrix = tasks.register("generateInfrastructureChangedCoordinatesMatrix", DefaultTask) { task -> task.setDescription("Returns matrix definition populated with pre-selected coordinates when test infrastructure has changed") diff --git a/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/TckExtension.java b/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/TckExtension.java index c62423971..cb34d8c1e 100644 --- a/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/TckExtension.java +++ b/tests/tck-build-logic/src/main/groovy/org/graalvm/internal/tck/harness/TckExtension.java @@ -184,6 +184,28 @@ List diffCoordinates(String baseCommit, String newCommit) { return changedCoordinates; } + /** + * Returns a list of changed index.json files between baseCommit and newCommit. + * + * @return List of index.json files + */ + public List diffIndexFiles(String baseCommit, String newCommit) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + getExecOperations().exec(spec -> { + spec.setStandardOutput(baos); + spec.commandLine("git", "diff", "--name-only", "--diff-filter=ACMRT", + baseCommit, newCommit); + }); + + String output = baos.toString(StandardCharsets.UTF_8); + List diffFiles = Arrays.asList(output.split("\\r?\\n")); + + return diffFiles.stream() + .filter(f -> f.endsWith("index.json")) + .distinct() + .collect(Collectors.toList()); + } + private boolean metadataIndexContainsChangedEntries(Set changedCoordinates, List changedEntries) { boolean containsAll = true; for (var n : changedEntries) {