diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 7b3ebce7..2a578473 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -20,34 +20,38 @@ jobs: fetch-depth: 1 persist-credentials: false - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 - - name: Test release build-output materialization + - name: Test release catalog materialization run: | - ./tests/release_build_output_descriptors_test.sh - ./tests/release_build_output_prepare_test.sh - ./tests/release_build_output_test.sh - - name: Prepare release build-output dispatch smoke test + ./tests/release_catalog_config_test.sh + ./tests/release_catalog_descriptors_test.sh + ./tests/release_catalog_prepare_test.sh + ./tests/release_catalog_test.sh + - name: Prepare release catalog dispatch smoke test run: | - mkdir -p release-build-output-smoke - printf '%s\n' smoke >release-build-output-smoke/package.tar.gz - - name: Run release build-output dispatch smoke test - uses: ./release-build-output-dispatch + mkdir -p release-catalog-smoke + printf '%s\n' smoke >release-catalog-smoke/package.tar.gz + - name: Run release catalog dispatch smoke test + uses: ./release-catalog-dispatch env: SHARED_ACTIONS_REPO: ${{ github.event.pull_request.head.repo.full_name }} SHARED_ACTIONS_REF: ${{ github.event.pull_request.head.sha }} with: - artifact-type: custom - output-directory: release-build-output-smoke - release-artifacts: '[{"path":"package.tar.gz"}]' - release-package: '{"ecosystem":"archive","name":"smoke","version":"1.0"}' - release-unit: archive:smoke - source-artifact-name: release-build-output-dispatch-smoke + config: >- + { + "artifact_type": "custom", + "release_catalog_key": "archive:smoke", + "output_directory": "release-catalog-smoke", + "package": {"ecosystem": "archive", "name": "smoke", "version": "1.0"}, + "artifacts": [{"path": "package.tar.gz"}] + } + source-artifact-name: release-catalog-dispatch-smoke source-sha: ${{ github.event.pull_request.head.sha }} - - name: Verify release build-output dispatch smoke test + - name: Verify release catalog dispatch smoke test run: | jq -e ' - .artifacts[0].unit_id == "archive:smoke" - and .artifacts[0].path == "package.tar.gz" - ' release-build-output-smoke/release-build-output.json >/dev/null - jq -e ' - .metadata.artifacts == [{path: "package.tar.gz", sbom_kind: "generated-identity"}] - ' release-build-output-smoke/release-build-metadata.json >/dev/null + .producer == "shared-workflows" + and .source.artifact == "release-catalog-dispatch-smoke" + and .entries[0].release_catalog_key == "archive:smoke" + and .entries[0].path == "package.tar.gz" + and .entries[0].sbom_kind == "generated-identity" + ' release-catalog-smoke/release-catalog-entries.json >/dev/null diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 799d7320..6cf849f3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,13 @@ ci: autoupdate_schedule: quarterly - skip: [actionlint-docker] + # These hooks require tools unavailable in pre-commit.ci: + # + # * actionlint-docker requires Docker + # * release-catalog-config requires jq + # + # Both run in other CI jobs with controlled runtime dependencies. + skip: [actionlint-docker, release-catalog-config] repos: - repo: https://github.com/pre-commit/pre-commit-hooks @@ -11,6 +17,7 @@ repos: hooks: - id: trailing-whitespace - id: check-added-large-files + - id: check-json - id: check-yaml - id: end-of-file-fixer - repo: https://github.com/zizmorcore/zizmor-pre-commit @@ -52,3 +59,18 @@ repos: hooks: - id: yamllint additional_dependencies: [pyyaml] + - repo: https://github.com/python-jsonschema/check-jsonschema + rev: 0.37.2 + hooks: + - id: check-jsonschema + name: validate release catalog config schema fixtures + args: [--schemafile, release-catalog/config.schema.json] + files: ^tests/release-catalog-config/.*\.json$ + - repo: local + hooks: + - id: release-catalog-config + name: validate release catalog configuration + entry: ./tests/release_catalog_config_test.sh + language: system + pass_filenames: false + files: ^(release-catalog/(config\.schema\.json|validate-config\.sh)|tests/release_catalog_config_test\.sh)$ diff --git a/README.md b/README.md index 4ea1b0ca..88248366 100644 --- a/README.md +++ b/README.md @@ -9,35 +9,15 @@ A dispatch action is one that: * clones the shared-actions repository (repo/ref changeable using env vars) * runs (dispatches to) another action within the clone, using a relative path -## Release build-output companions +## Release catalog companions -`release-build-output-dispatch` validates a producer's local build artifact +`release-catalog-dispatch` validates a producer's local build artifact directory and uploads a companion artifact named -`release-build-output-`. The companion contains -`release-build-output.json`, `release-build-metadata.json`, provenance, and an -SBOM record for every primary artifact. +`release-catalog-`. -Conda and wheel jobs can set `artifact-type` to `conda` or `wheel` and omit -`release-artifacts`; the implementation reads package metadata from the built -files. Custom bundles provide explicit artifact descriptors and either inline -package identity or a producer-created package JSON file. - -```yaml -- name: Create release build-output companion - uses: rapidsai/shared-actions/release-build-output-dispatch@main - with: - artifact-type: wheel - output-directory: ${{ steps.package-name.outputs.WHEEL_OUTPUT_DIR }} - release-unit: wheel:example - source-artifact-name: ${{ steps.package-name.outputs.RAPIDS_PACKAGE_NAME }} - source-sha: ${{ github.sha }} -``` - -A descriptor-selected producer SBOM is classified as `producer-dependency`. -When no SBOM is supplied, the action generates an SPDX artifact-identity -envelope and classifies it as `generated-identity`. The generated envelope -contains the primary artifact's identity and SHA-256 but no dependency -inventory; it must not be treated as dependency coverage. +See the [release catalog documentation](release-catalog/README.md) +for configuration, examples, source-revision handling, companion contents, and +evidence semantics. The dispatch wrapper honors `SHARED_ACTIONS_REPO` and `SHARED_ACTIONS_REF`. When neither is set, it checks out the same repository and ref used to invoke diff --git a/release-build-output-dispatch/action.yml b/release-build-output-dispatch/action.yml deleted file mode 100644 index 154ba3d7..00000000 --- a/release-build-output-dispatch/action.yml +++ /dev/null @@ -1,72 +0,0 @@ -name: Dispatch release build output -description: Check out the selected shared-actions revision and create a release build-output companion. - -inputs: - artifact-type: - description: One of conda, wheel, or custom. - required: true - release-unit: - description: Release-platform unit ID for every primary artifact in this bundle. - required: true - release-package: - description: JSON package fields shared by the bundle. - required: false - release-package-file: - description: Relative path to producer-created package JSON inside output-directory. - required: false - release-artifacts: - description: JSON artifact and evidence descriptors relative to output-directory. - required: false - output-directory: - description: Directory containing the primary files and any producer-supplied evidence. - required: true - manifest-name: - description: Filename to write inside output-directory. - required: false - default: release-build-output.json - metadata-name: - description: Filename for the build metadata envelope. - required: false - default: release-build-metadata.json - source-artifact-name: - description: Name of the GitHub Actions artifact bundle containing this output. - required: true - source-sha: - description: Source revision built by the producing job. - required: false - -outputs: - manifest-path: - description: Absolute path to the generated manifest. - value: ${{ steps.release-build-output.outputs.manifest-path }} - metadata-path: - description: Absolute path to the build metadata envelope. - value: ${{ steps.release-build-output.outputs.metadata-path }} - manifest-artifact-name: - description: Name of the uploaded GitHub Actions companion artifact. - value: ${{ steps.release-build-output.outputs.manifest-artifact-name }} - -runs: - using: composite - steps: - - name: Check out shared-actions implementation - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - repository: ${{ env.SHARED_ACTIONS_REPO || github.action_repository || 'rapidsai/shared-actions' }} - ref: ${{ env.SHARED_ACTIONS_REF || github.action_ref || 'main' }} - path: ./shared-actions - persist-credentials: false - - id: release-build-output - name: Create release build-output companion - uses: ./shared-actions/release-build-output - with: - artifact-type: ${{ inputs.artifact-type }} - release-unit: ${{ inputs.release-unit }} - release-package: ${{ inputs.release-package }} - release-package-file: ${{ inputs.release-package-file }} - release-artifacts: ${{ inputs.release-artifacts }} - output-directory: ${{ inputs.output-directory }} - manifest-name: ${{ inputs.manifest-name }} - metadata-name: ${{ inputs.metadata-name }} - source-artifact-name: ${{ inputs.source-artifact-name }} - source-sha: ${{ inputs.source-sha }} diff --git a/release-build-output/action.yml b/release-build-output/action.yml deleted file mode 100644 index 7c6965e3..00000000 --- a/release-build-output/action.yml +++ /dev/null @@ -1,90 +0,0 @@ -name: Create release build output companion -description: Validate a build artifact bundle and upload its release-platform manifest and evidence companion. - -inputs: - artifact-type: - description: One of conda, wheel, or custom. Conda and wheel descriptors are derived when release-artifacts is omitted. - required: true - release-unit: - description: Release-platform unit ID for every primary artifact in this bundle. - required: true - release-package: - description: JSON package fields shared by the bundle. Provide this or release-package-file for custom artifacts. - required: false - release-package-file: - description: Relative path to producer-created package JSON inside output-directory. - required: false - release-artifacts: - description: JSON artifact and evidence descriptors relative to output-directory. Required for custom artifacts. - required: false - output-directory: - description: Directory containing the primary files and any producer-supplied evidence. - required: true - manifest-name: - description: Filename to write inside output-directory. - required: false - default: release-build-output.json - metadata-name: - description: Filename for the build-environment and SBOM-classification envelope. - required: false - default: release-build-metadata.json - source-artifact-name: - description: Name of the GitHub Actions artifact bundle containing this output. - required: true - source-sha: - description: Source revision built by the producing job. Defaults to the current workflow SHA. - required: false - -outputs: - manifest-path: - description: Absolute path to the generated manifest. - value: ${{ steps.materialize.outputs.manifest-path }} - metadata-path: - description: Absolute path to the build metadata envelope. - value: ${{ steps.materialize.outputs.metadata-path }} - manifest-artifact-name: - description: Name of the uploaded GitHub Actions companion artifact. - value: ${{ steps.companion-name.outputs.name }} - -runs: - using: composite - steps: - - id: prepare - name: Describe release artifacts - shell: bash - env: - RELEASE_ARTIFACTS: ${{ inputs.release-artifacts }} - RELEASE_ARTIFACT_TYPE: ${{ inputs.artifact-type }} - RELEASE_OUTPUT_DIRECTORY: ${{ inputs.output-directory }} - RELEASE_PACKAGE: ${{ inputs.release-package }} - RELEASE_PACKAGE_FILE: ${{ inputs.release-package-file }} - run: ./shared-actions/release-build-output/prepare.sh - - id: materialize - name: Materialize release build-output records - shell: bash - env: - RELEASE_ARTIFACTS: ${{ steps.prepare.outputs.artifacts }} - RELEASE_MANIFEST_NAME: ${{ inputs.manifest-name }} - RELEASE_METADATA_NAME: ${{ inputs.metadata-name }} - RELEASE_OUTPUT_DIRECTORY: ${{ inputs.output-directory }} - RELEASE_PACKAGE: ${{ steps.prepare.outputs.package }} - RELEASE_PACKAGE_FILE: ${{ inputs.release-package-file }} - RELEASE_SOURCE_ARTIFACT_NAME: ${{ inputs.source-artifact-name }} - RELEASE_SOURCE_SHA: ${{ inputs.source-sha || github.sha }} - RELEASE_UNIT: ${{ inputs.release-unit }} - run: ./shared-actions/release-build-output/materialize.sh - - id: companion-name - name: Set companion artifact name - shell: bash - env: - SOURCE_ARTIFACT_NAME: ${{ inputs.source-artifact-name }} - run: echo "name=release-build-output-${SOURCE_ARTIFACT_NAME}" >>"${GITHUB_OUTPUT}" - - name: Upload release build-output companion - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - if-no-files-found: error - name: ${{ steps.companion-name.outputs.name }} - path: | - ${{ inputs.output-directory }}/${{ inputs.manifest-name }} - ${{ inputs.output-directory }}/${{ inputs.metadata-name }} - ${{ inputs.output-directory }}/release-evidence/** diff --git a/release-catalog-dispatch/action.yml b/release-catalog-dispatch/action.yml new file mode 100644 index 00000000..d53309ca --- /dev/null +++ b/release-catalog-dispatch/action.yml @@ -0,0 +1,91 @@ +name: Dispatch release catalog +description: Create a release catalog companion from the selected shared-actions revision. + +inputs: + config: + description: >- + JSON string with configuration for this action. Schema and field documentation: + https://github.com/rapidsai/shared-actions/blob/main/release-catalog/config.schema.json + required: true + source-artifact-name: + description: Name of the GitHub Actions artifact bundle containing this output. + required: true + source-sha: + description: Source revision built by the producing job. + required: false + +outputs: + entries-path: + description: Absolute path to the generated release catalog entries. + value: ${{ steps.materialize.outputs.entries-path }} + companion-artifact-name: + description: Name of the uploaded GitHub Actions companion artifact. + value: ${{ steps.companion-name.outputs.name }} + +runs: + using: composite + steps: + - id: implementation + name: Resolve release catalog implementation + shell: bash + env: + ACTION_PATH: ${{ github.action_path }} + HOST_WORKSPACE: ${{ github.workspace }} + run: | + host_root="$(dirname "$(dirname "${HOST_WORKSPACE}")")" + container_root="$(dirname "$(dirname "${PWD}")")" + action_path="${ACTION_PATH}" + if [[ ! -d "${action_path}" && "${action_path}" == "${host_root}/"* ]]; then + action_path="${container_root}/${action_path#"${host_root}/"}" + fi + implementation_path="$(dirname "${action_path}")/release-catalog" + if [[ ! -d "${implementation_path}" ]]; then + echo "release-catalog implementation not found at ${implementation_path}" >&2 + exit 1 + fi + echo "path=${implementation_path}" >>"${GITHUB_OUTPUT}" + - id: configuration + name: Validate release catalog configuration + shell: bash + env: + IMPLEMENTATION_PATH: ${{ steps.implementation.outputs.path }} + RELEASE_CATALOG_CONFIG: ${{ inputs.config }} + run: "${IMPLEMENTATION_PATH}/validate-config.sh" + - id: prepare + name: Describe release artifacts + shell: bash + env: + IMPLEMENTATION_PATH: ${{ steps.implementation.outputs.path }} + RELEASE_ARTIFACTS: ${{ steps.configuration.outputs.artifacts }} + RELEASE_ARTIFACT_TYPE: ${{ steps.configuration.outputs.artifact_type }} + RELEASE_OUTPUT_DIRECTORY: ${{ steps.configuration.outputs.output_directory }} + RELEASE_PACKAGE_IDENTITY_FILE: ${{ steps.configuration.outputs.package_identity_file }} + run: "${IMPLEMENTATION_PATH}/prepare.sh" + - id: materialize + name: Materialize release catalog entries + shell: bash + env: + IMPLEMENTATION_PATH: ${{ steps.implementation.outputs.path }} + RELEASE_ARTIFACTS: ${{ steps.prepare.outputs.artifacts }} + RELEASE_ARTIFACT_TYPE: ${{ steps.configuration.outputs.artifact_type }} + RELEASE_ENTRIES_NAME: release-catalog-entries.json + RELEASE_OUTPUT_DIRECTORY: ${{ steps.configuration.outputs.output_directory }} + RELEASE_PACKAGE_IDENTITY_FILE: ${{ steps.configuration.outputs.package_identity_file }} + RELEASE_SOURCE_ARTIFACT_NAME: ${{ inputs.source-artifact-name }} + RELEASE_SOURCE_SHA: ${{ inputs.source-sha || github.sha }} + RELEASE_CATALOG_KEY: ${{ steps.configuration.outputs.release_catalog_key }} + run: "${IMPLEMENTATION_PATH}/materialize.sh" + - id: companion-name + name: Set companion artifact name + shell: bash + env: + SOURCE_ARTIFACT_NAME: ${{ inputs.source-artifact-name }} + run: echo "name=release-catalog-${SOURCE_ARTIFACT_NAME}" >>"${GITHUB_OUTPUT}" + - name: Upload release catalog companion + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + if-no-files-found: error + name: ${{ steps.companion-name.outputs.name }} + path: | + ${{ steps.configuration.outputs.output_directory }}/release-catalog-entries.json + ${{ steps.configuration.outputs.output_directory }}/release-evidence/** diff --git a/release-catalog/README.md b/release-catalog/README.md new file mode 100644 index 00000000..b1a7785c --- /dev/null +++ b/release-catalog/README.md @@ -0,0 +1,145 @@ +# Release catalog companions + +`release-catalog-dispatch` records the exact files produced by a package +build and uploads a companion GitHub Actions artifact named +`release-catalog-`. Release tooling uses the +companion to associate primary artifacts with their build identity and +available evidence without reconstructing the build later. + +The companion contains: + +```text +. +├── release-catalog-entries.json +└── release-evidence + ├── .provenance.json + └── .spdx.json +``` + +`release-catalog-entries.json` is one atomic job-level envelope. Its `source` +object records the source artifact and build context, while its `entries` array +contains the release catalog entries produced by that job. The release platform +validates and aggregates entry arrays from selected builds into the release +catalog; there is no separate metadata document to keep synchronized. + +## Configuration + +Every producer passes one `config` JSON object. Its canonical schema and field +documentation are in [`config.schema.json`](config.schema.json). + +The `artifact_type` field determines how the action discovers primary files: + +- `conda` and `wheel` derive artifact descriptors and package identity from the + built packages; +- `custom` requires explicit artifact descriptors and a + `package_identity_file` created by the producer during the build. + +`release_catalog_key` identifies the release catalog entry that owns these +artifacts. Every file from every matrix variant in the same publishable artifact +set uses the same key, and multiple files are aggregated. Standard RAPIDS Conda +and wheel workflows construct it as `:`, such as +`conda:cudf`; custom producers select an existing catalog key, such as +`maven:cuvs-java`. Do not generate a UUID or a per-build value. + +Use a distinct `release_catalog_key` only when the release catalog intentionally gives the +outputs different release policy. Examples include: + +* different versioning +* validation requirements +* dependency ordering +* publication destinations +* promotion strategy. + +Multiple package names from one repository do not by themselves justify separate +keys: for example, `cudf` and `dask-cudf` Conda packages remain part of +`conda:cudf` when they share one release policy. + +The action validates the configuration before inspecting build outputs. It then +verifies properties that depend on produced files, including the package +identity contents and whether primary-artifact and evidence paths resolve +unambiguously. + +## Source revision + +`source-sha` must identify the repository revision actually checked out and +built. RAPIDS shared workflows pass `${{ env.RAPIDS_SHA }}`: + +- Conda build workflows set `RAPIDS_SHA` to `git rev-parse HEAD` immediately + after checkout. +- Wheel and custom workflows use `rapids-github-info`; it uses `inputs.sha` + when supplied and otherwise sets `RAPIDS_SHA` to `git rev-parse HEAD`. + +This distinction matters when a reusable workflow checks out a repository or +revision different from the workflow event. Direct callers should likewise +resolve the checked-out commit instead of assuming `${{ github.sha }}` names +the built source. + +## Standard package example + +```yaml +- name: Create wheel release catalog companion + uses: rapidsai/shared-actions/release-catalog-dispatch@main + with: + config: >- + { + "artifact_type": "wheel", + "release_catalog_key": "wheel:example", + "output_directory": ${{ toJSON(steps.package-name.outputs.WHEEL_OUTPUT_DIR) }} + } + source-artifact-name: ${{ steps.package-name.outputs.RAPIDS_PACKAGE_NAME }} + source-sha: ${{ env.RAPIDS_SHA }} +``` + +## Custom package example + +```yaml +- name: Create custom release catalog companion + uses: rapidsai/shared-actions/release-catalog-dispatch@main + with: + config: >- + { + "artifact_type": "custom", + "release_catalog_key": "maven:cuvs-java", + "output_directory": "java/cuvs-java/target", + "package_identity_file": "cuvs-java.release-package-identity.json", + "artifacts": [{"path": "cuvs-java-*-x86_64-cuda*.jar"}] + } + source-artifact-name: cuvs-java + source-sha: ${{ env.RAPIDS_SHA }} +``` + +Before the action runs, the producer creates +`java/cuvs-java/target/cuvs-java.release-package-identity.json`. The +`package_identity_file` value is the path to that file relative to +`output_directory`. For example: + +```json +{ + "ecosystem": "maven", + "name": "ai.rapids:cuvs-java", + "version": "26.08.0" +} +``` + +## Evidence semantics + +A descriptor-selected producer SBOM is classified as `producer-dependency`. +It is evidence supplied by the producer and may contain a dependency inventory. + +When no SBOM is selected, the action generates an SPDX artifact-identity +envelope classified as `generated-identity`. It records package identity and +the primary artifact SHA-256, but contains no dependency or source-license +inventory. It must not be reported as producer-supplied dependency coverage. + +Producer-supplied SBOM, provenance, and signature sidecars are copied under +`release-evidence/` so the companion remains independently consumable. + +Concrete producer-supplied evidence examples include: + +- an official [SPDX 2.3 dependency SBOM](https://github.com/spdx/spdx-examples/blob/2181917ef6ff74de89252ee785583c27a38d6199/presentations/OSS-NA-2023/SPDXVersion2.3/03-SBOMwDependency.json); +- an official [SLSA provenance v1 statement](https://github.com/slsa-framework/github-actions-buildtypes/blob/5f855ef0106dad3ee0e0f1046dc31b3b65152956/workflow/v1/example.json); +- a Maven Central [detached ASCII-armored signature](https://repo1.maven.org/maven2/org/apache/commons/commons-lang3/3.17.0/commons-lang3-3.17.0.jar.asc). + +These examples illustrate the expected purpose of the files, not required +serialization formats. The action copies producer-supplied evidence as opaque +sidecars and does not validate their contents or require these formats. diff --git a/release-catalog/config.schema.json b/release-catalog/config.schema.json new file mode 100644 index 00000000..5610a1d3 --- /dev/null +++ b/release-catalog/config.schema.json @@ -0,0 +1,123 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://rapids.ai/schemas/release-catalog-config-v1.json", + "title": "Release catalog configuration", + "description": "Producer configuration for one release catalog companion.", + "type": "object", + "additionalProperties": false, + "required": [ + "artifact_type", + "release_catalog_key" + ], + "properties": { + "artifact_type": { + "description": "Metadata can be extracted from the artifact for conda/wheel types. More info needed for custom type.", + "type": "string", + "enum": [ + "conda", + "custom", + "wheel" + ] + }, + "release_catalog_key": { + "description": "Release-catalog entry name that owns these artifacts. Default for conda & wheels is :, such as conda:cudf. Custom jobs specify their own, such as maven:cuvs-java.", + "type": "string", + "minLength": 1, + "pattern": "^[^\\r\\n]+$" + }, + "output_directory": { + "description": "Base directory for artifact and evidence paths. Defaults to the job working directory.", + "type": "string", + "minLength": 1, + "pattern": "^[^\\r\\n]+$", + "default": "." + }, + "package_identity_file": { + "description": "Path, relative to output_directory, to producer-created package identity JSON. Required for custom artifacts; Conda and wheel identities are extracted from the artifacts. The file must contain ecosystem, name, and version and may contain build and platform.", + "type": "string", + "minLength": 1, + "pattern": "^[^\\r\\n]+$" + }, + "artifacts": { + "description": "Primary release files and optional SBoM evidence sidecar files", + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/artifact" + } + } + }, + "allOf": [ + { + "if": { + "properties": { + "artifact_type": { + "const": "custom" + } + }, + "required": [ + "artifact_type" + ] + }, + "then": { + "required": [ + "artifacts", + "package_identity_file" + ] + }, + "else": { + "required": [ + "output_directory" + ], + "not": { + "anyOf": [ + { + "required": [ + "artifacts" + ] + }, + { + "required": [ + "package_identity_file" + ] + } + ] + } + } + } + ], + "$defs": { + "artifact": { + "type": "object", + "additionalProperties": false, + "required": [ + "path" + ], + "properties": { + "path": { + "type": "string", + "minLength": 1, + "pattern": "^[^\\r\\n]+$" + }, + "sbom": { + "description": "Dependency SBOM path or glob. Example file: https://github.com/spdx/spdx-examples/blob/2181917ef6ff74de89252ee785583c27a38d6199/presentations/OSS-NA-2023/SPDXVersion2.3/03-SBOMwDependency.json", + "type": "string", + "minLength": 1, + "pattern": "^[^\\r\\n]+$" + }, + "provenance": { + "description": "Provenance path or glob. Example file: https://github.com/slsa-framework/github-actions-buildtypes/blob/5f855ef0106dad3ee0e0f1046dc31b3b65152956/workflow/v1/example.json", + "type": "string", + "minLength": 1, + "pattern": "^[^\\r\\n]+$" + }, + "signature": { + "description": "Signature path or glob. Example file: https://repo1.maven.org/maven2/org/apache/commons/commons-lang3/3.17.0/commons-lang3-3.17.0.jar.asc", + "type": "string", + "minLength": 1, + "pattern": "^[^\\r\\n]+$" + } + } + } + } +} diff --git a/release-build-output/describe-conda.sh b/release-catalog/describe-conda.sh similarity index 100% rename from release-build-output/describe-conda.sh rename to release-catalog/describe-conda.sh diff --git a/release-build-output/describe-wheels.sh b/release-catalog/describe-wheels.sh similarity index 100% rename from release-build-output/describe-wheels.sh rename to release-catalog/describe-wheels.sh diff --git a/release-build-output/materialize.sh b/release-catalog/materialize.sh similarity index 59% rename from release-build-output/materialize.sh rename to release-catalog/materialize.sh index 673dd6ba..24c45fcf 100755 --- a/release-build-output/materialize.sh +++ b/release-catalog/materialize.sh @@ -12,22 +12,13 @@ require_nonempty() { fi } -require_nonempty "RELEASE_UNIT" "${RELEASE_UNIT:-}" +require_nonempty "RELEASE_CATALOG_KEY" "${RELEASE_CATALOG_KEY:-}" +require_nonempty "RELEASE_ARTIFACT_TYPE" "${RELEASE_ARTIFACT_TYPE:-}" require_nonempty "RELEASE_OUTPUT_DIRECTORY" "${RELEASE_OUTPUT_DIRECTORY:-}" -require_nonempty "RELEASE_MANIFEST_NAME" "${RELEASE_MANIFEST_NAME:-}" -require_nonempty "RELEASE_METADATA_NAME" "${RELEASE_METADATA_NAME:-}" +require_nonempty "RELEASE_ENTRIES_NAME" "${RELEASE_ENTRIES_NAME:-}" require_nonempty "RELEASE_ARTIFACTS" "${RELEASE_ARTIFACTS:-}" require_nonempty "RELEASE_SOURCE_ARTIFACT_NAME" "${RELEASE_SOURCE_ARTIFACT_NAME:-}" -if [[ -n "${RELEASE_PACKAGE:-}" && -n "${RELEASE_PACKAGE_FILE:-}" ]]; then - echo "release-package and release-package-file are mutually exclusive" >&2 - exit 1 -fi -if [[ -z "${RELEASE_PACKAGE:-}" && -z "${RELEASE_PACKAGE_FILE:-}" ]]; then - echo "one of release-package or release-package-file is required" >&2 - exit 1 -fi - source_sha="${RELEASE_SOURCE_SHA:-${GITHUB_SHA:-}}" require_nonempty "RELEASE_SOURCE_SHA or GITHUB_SHA" "${source_sha}" @@ -40,12 +31,7 @@ require_plain_filename() { fi } -require_plain_filename "manifest-name" "${RELEASE_MANIFEST_NAME}" -require_plain_filename "metadata-name" "${RELEASE_METADATA_NAME}" -if [[ "${RELEASE_MANIFEST_NAME}" == "${RELEASE_METADATA_NAME}" ]]; then - echo "manifest-name and metadata-name must differ" >&2 - exit 1 -fi +require_plain_filename "entries-name" "${RELEASE_ENTRIES_NAME}" if [[ ! -d "${RELEASE_OUTPUT_DIRECTORY}" ]]; then echo "output-directory does not exist or is not a directory: ${RELEASE_OUTPUT_DIRECTORY}" >&2 @@ -62,26 +48,40 @@ ensure_relative_pattern() { } output_directory="$(realpath "${RELEASE_OUTPUT_DIRECTORY}")" -if [[ -n "${RELEASE_PACKAGE_FILE:-}" ]]; then - ensure_relative_pattern "release-package-file" "${RELEASE_PACKAGE_FILE}" - package_file_path="$(realpath "${output_directory}/${RELEASE_PACKAGE_FILE}")" - if [[ "${package_file_path}" != "${output_directory}"/* || ! -f "${package_file_path}" ]]; then - echo "release-package-file must resolve to one file inside output-directory: ${RELEASE_PACKAGE_FILE}" >&2 +package_identity='' +case "${RELEASE_ARTIFACT_TYPE}" in + custom) + require_nonempty "RELEASE_PACKAGE_IDENTITY_FILE" "${RELEASE_PACKAGE_IDENTITY_FILE:-}" + ensure_relative_pattern "package-identity-file" "${RELEASE_PACKAGE_IDENTITY_FILE}" + package_identity_path="$(realpath "${output_directory}/${RELEASE_PACKAGE_IDENTITY_FILE}")" + if [[ "${package_identity_path}" != "${output_directory}"/* || ! -f "${package_identity_path}" ]]; then + echo "package-identity-file must resolve to one file inside output-directory: ${RELEASE_PACKAGE_IDENTITY_FILE}" >&2 + exit 1 + fi + package_identity="$(jq -c . "${package_identity_path}")" + ;; + conda | wheel) + if [[ -n "${RELEASE_PACKAGE_IDENTITY_FILE:-}" ]]; then + echo "package-identity-file is only valid for custom artifacts" >&2 + exit 1 + fi + ;; + *) + echo "artifact-type must be one of: conda, custom, wheel" >&2 exit 1 - fi - RELEASE_PACKAGE="$(jq -c . "${package_file_path}")" -fi + ;; +esac -if ! jq -e ' +if [[ "${RELEASE_ARTIFACT_TYPE}" == "custom" ]] && ! jq -e ' type == "object" and (keys - ["ecosystem", "name", "version", "build", "platform"] | length == 0) and (.ecosystem | type == "string" and length > 0) and (.name | type == "string" and length > 0) - and ((.version // "") | type == "string") - and ((.build // "") | type == "string") - and ((.platform // "") | type == "string") -' <<<"${RELEASE_PACKAGE}" >/dev/null; then - echo "release-package must be a package object with ecosystem and name; version may be supplied or derived per artifact" >&2 + and (.version | type == "string" and length > 0) + and ((has("build") | not) or (.build | type == "string" and length > 0)) + and ((has("platform") | not) or (.platform | type == "string" and length > 0)) +' <<<"${package_identity}" >/dev/null; then + echo "package identity file must contain non-empty ecosystem, name, and version strings and only optional build or platform strings" >&2 exit 1 fi @@ -90,12 +90,11 @@ if ! jq -e 'type == "array" and length > 0' <<<"${RELEASE_ARTIFACTS}" >/dev/null exit 1 fi -manifest_path="${output_directory}/${RELEASE_MANIFEST_NAME}" -metadata_path="${output_directory}/${RELEASE_METADATA_NAME}" -temporary_manifest="$(mktemp "${output_directory}/.release-build-output.XXXXXX")" +entries_path="${output_directory}/${RELEASE_ENTRIES_NAME}" +temporary_manifest="$(mktemp "${output_directory}/.release-catalog.XXXXXX")" trap 'rm -f "${temporary_manifest}"' EXIT -printf '%s\n' '{"schema_version":1,"producer":"release-platform","artifacts":[]}' >"${temporary_manifest}" +printf '%s\n' '{"entries":[]}' >"${temporary_manifest}" resolve_one_file() { local field="$1" @@ -120,49 +119,6 @@ resolve_one_file() { printf '%s\n' "${resolved#"${output_directory}/"}" } -derive_package_version() { - local ecosystem="$1" - local package_name="$2" - local artifact_path="$3" - local filename - filename="$(basename "${artifact_path}")" - - case "${ecosystem}" in - conda) - local conda_prefix="${package_name}-" - if [[ "${filename}" != "${conda_prefix}"* ]]; then - echo "Conda artifact filename does not start with package name '${package_name}': ${filename}" >&2 - exit 1 - fi - local conda_remainder="${filename#"${conda_prefix}"}" - local conda_version="${conda_remainder%%-*}" - if [[ -z "${conda_version}" || "${conda_version}" == "${conda_remainder}" ]]; then - echo "cannot derive Conda package version from artifact filename: ${filename}" >&2 - exit 1 - fi - printf '%s\n' "${conda_version}" - ;; - wheel) - local wheel_prefix="${package_name//-/_}-" - if [[ "${filename}" != "${wheel_prefix}"* ]]; then - echo "wheel artifact filename does not start with normalized package name '${package_name}': ${filename}" >&2 - exit 1 - fi - local wheel_remainder="${filename#"${wheel_prefix}"}" - local wheel_version="${wheel_remainder%%-*}" - if [[ -z "${wheel_version}" || "${wheel_version}" == "${wheel_remainder}" ]]; then - echo "cannot derive wheel package version from artifact filename: ${filename}" >&2 - exit 1 - fi - printf '%s\n' "${wheel_version}" - ;; - *) - echo "release-package version is required for ${ecosystem} artifacts" >&2 - exit 1 - ;; - esac -} - generated_evidence_path() { local primary_path="$1" local kind="$2" @@ -204,7 +160,7 @@ write_generated_sbom() { name: ("RAPIDS release artifact " + $artifact_path), documentNamespace: ("https://rapids.ai/release-platform/spdx/" + $artifact_digest), creationInfo: { - creators: ["Tool: rapidsai/shared-workflows release-build-output"], + creators: ["Tool: rapidsai/shared-workflows release catalog"], created: (now | strftime("%Y-%m-%dT%H:%M:%SZ")) }, documentDescribes: ["SPDXRef-Artifact"], @@ -247,8 +203,8 @@ write_generated_provenance() { predicateType: "https://slsa.dev/provenance/v1", predicate: { buildDefinition: { - buildType: "https://rapids.ai/release-platform/build-output/v1", - externalParameters: {release_unit: env.RELEASE_UNIT, package: $package}, + buildType: "https://rapids.ai/release-platform/catalog-record/v1", + externalParameters: {release_catalog_key: env.RELEASE_CATALOG_KEY, package: $package}, resolvedDependencies: [{ uri: ("git+https://github.com/" + $repository + "@" + $source_sha), digest: {gitCommit: $source_sha} @@ -263,20 +219,35 @@ write_generated_provenance() { } shopt -s globstar nullglob -artifact_metadata='[]' while IFS= read -r descriptor; do - if ! jq -e ' + if ! jq -e --arg artifact_type "${RELEASE_ARTIFACT_TYPE}" ' type == "object" - and (keys - ["path", "sbom", "provenance", "signature", "package"] | length == 0) + and ( + if $artifact_type == "custom" then + (keys - ["path", "sbom", "provenance", "signature"] | length == 0) + else + (keys - ["path", "sbom", "provenance", "signature", "package"] | length == 0) + end + ) and (.path | type == "string" and length > 0) and ((.sbom // "") | type == "string") and ((.provenance // "") | type == "string") and ((.signature // "") | type == "string") - and ((.package // {}) | type == "object") - and ((.package // {} | keys - ["ecosystem", "name", "version", "build", "platform"]) | length == 0) - and ((.package // {} | to_entries | map(.value | type == "string" and length > 0) | all)) + and ( + if $artifact_type == "custom" then + has("package") | not + else + (.package | type == "object") + and (.package | keys - ["ecosystem", "name", "version", "build", "platform"] | length == 0) + and (.package.ecosystem | type == "string" and length > 0) + and (.package.name | type == "string" and length > 0) + and (.package.version | type == "string" and length > 0) + and ((.package | has("build") | not) or (.package.build | type == "string" and length > 0)) + and ((.package | has("platform") | not) or (.package.platform | type == "string" and length > 0)) + end + ) ' <<<"${descriptor}" >/dev/null; then - echo "every release-artifacts entry must contain path and optional SBOM/provenance/signature/package overrides" >&2 + echo "release artifact descriptor is invalid for ${RELEASE_ARTIFACT_TYPE}: ${descriptor}" >&2 exit 1 fi @@ -284,13 +255,10 @@ while IFS= read -r descriptor; do sbom_pattern="$(jq -r '.sbom // empty' <<<"${descriptor}")" provenance_pattern="$(jq -r '.provenance // empty' <<<"${descriptor}")" signature_pattern="$(jq -r '.signature // empty' <<<"${descriptor}")" - package_override="$(jq -c '.package // {}' <<<"${descriptor}")" - - package="$(jq -cn --argjson base "${RELEASE_PACKAGE}" --argjson override "${package_override}" '$base + $override')" - package_version="$(jq -r '.version // empty' <<<"${package}")" - if [[ -z "${package_version}" ]]; then - package_version="$(derive_package_version "$(jq -r '.ecosystem' <<<"${package}")" "$(jq -r '.name' <<<"${package}")" "${primary_path}")" - package="$(jq -c --arg version "${package_version}" '. + {version: $version}' <<<"${package}")" + if [[ "${RELEASE_ARTIFACT_TYPE}" == "custom" ]]; then + package="${package_identity}" + else + package="$(jq -c '.package' <<<"${descriptor}")" fi if [[ -n "${sbom_pattern}" ]]; then @@ -309,59 +277,48 @@ while IFS= read -r descriptor; do provenance_path="$(generated_evidence_path "${primary_path}" "provenance")" write_generated_provenance "${primary_path}" "${package}" "${provenance_path}" fi - artifact="$(jq -cn \ - --arg unit_id "${RELEASE_UNIT}" \ + entry="$(jq -cn \ + --arg release_catalog_key "${RELEASE_CATALOG_KEY}" \ --arg path "${primary_path}" \ --arg sbom "${sbom_path}" \ + --arg sbom_kind "${sbom_kind}" \ --arg provenance "${provenance_path}" \ --argjson package "${package}" \ - '{unit_id: $unit_id, path: $path, sbom: $sbom, provenance: $provenance, package: $package}')" + '{release_catalog_key: $release_catalog_key, path: $path, sbom: $sbom, sbom_kind: $sbom_kind, provenance: $provenance, package: $package}')" if [[ -n "${signature_pattern}" ]]; then supplied_signature_path="$(resolve_one_file signature "${signature_pattern}")" signature_path="$(copy_supplied_evidence "${primary_path}" "${supplied_signature_path}" "signature")" - artifact="$(jq -c --arg signature "${signature_path}" '. + {signature: $signature}' <<<"${artifact}")" + entry="$(jq -c --arg signature "${signature_path}" '. + {signature: $signature}' <<<"${entry}")" fi - artifact_metadata="$(jq -cn \ - --arg path "${primary_path}" \ - --arg sbom_kind "${sbom_kind}" \ - --argjson artifacts "${artifact_metadata}" \ - '$artifacts + [{path: $path, sbom_kind: $sbom_kind}]')" - - jq --argjson artifact "${artifact}" '.artifacts += [$artifact]' "${temporary_manifest}" >"${temporary_manifest}.next" + jq --argjson entry "${entry}" '.entries += [$entry]' "${temporary_manifest}" >"${temporary_manifest}.next" mv "${temporary_manifest}.next" "${temporary_manifest}" done < <(jq -c '.[]' <<<"${RELEASE_ARTIFACTS}") -if ! jq -e '.artifacts as $items | ($items | map([.unit_id, .path] | join("\u0000")) | unique | length) == ($items | length)' "${temporary_manifest}" >/dev/null; then - echo "release-artifacts contains duplicate unit/path entries" >&2 +if ! jq -e '.entries as $items | ($items | map([.release_catalog_key, .path] | join("\u0000")) | unique | length) == ($items | length)' "${temporary_manifest}" >/dev/null; then + echo "release-artifacts contains duplicate release_catalog_key/path entries" >&2 exit 1 fi -jq -S . "${temporary_manifest}" >"${manifest_path}" jq -n -S \ --arg artifact_name "${RELEASE_SOURCE_ARTIFACT_NAME}" \ - --arg manifest_name "${RELEASE_MANIFEST_NAME}" \ --arg repository "${GITHUB_REPOSITORY:-}" \ --arg run_attempt "${GITHUB_RUN_ATTEMPT:-}" \ --arg run_id "${GITHUB_RUN_ID:-}" \ --arg sha "${source_sha}" \ - --arg unit_id "${RELEASE_UNIT}" \ --arg workflow_ref "${GITHUB_WORKFLOW_REF:-}" \ - --argjson artifact_metadata "${artifact_metadata}" \ + --argjson entries "$(jq -c '.entries' "${temporary_manifest}")" \ '{ schema_version: 1, producer: "shared-workflows", - release_unit: $unit_id, - source_artifact: $artifact_name, - build_output_manifest: $manifest_name, - build_environment: { + source: { + artifact: $artifact_name, repository: $repository, sha: $sha, workflow_ref: $workflow_ref, run_id: $run_id, run_attempt: $run_attempt }, - metadata: {artifacts: $artifact_metadata} - }' >"${metadata_path}" -echo "manifest-path=${manifest_path}" >>"${GITHUB_OUTPUT}" -echo "metadata-path=${metadata_path}" >>"${GITHUB_OUTPUT}" + entries: $entries + }' >"${entries_path}" +echo "entries-path=${entries_path}" >>"${GITHUB_OUTPUT}" diff --git a/release-build-output/prepare.sh b/release-catalog/prepare.sh similarity index 67% rename from release-build-output/prepare.sh rename to release-catalog/prepare.sh index f3aa97f4..4a803d28 100755 --- a/release-build-output/prepare.sh +++ b/release-catalog/prepare.sh @@ -17,27 +17,21 @@ require_nonempty "RELEASE_OUTPUT_DIRECTORY" "${RELEASE_OUTPUT_DIRECTORY:-}" script_directory="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" artifacts="${RELEASE_ARTIFACTS:-}" -package="${RELEASE_PACKAGE:-}" case "${RELEASE_ARTIFACT_TYPE}" in conda) if [[ -z "${artifacts}" ]]; then artifacts="$("${script_directory}/describe-conda.sh" "${RELEASE_OUTPUT_DIRECTORY}")" fi - if [[ -z "${package}" && -z "${RELEASE_PACKAGE_FILE:-}" ]]; then - package='{"ecosystem":"conda","name":"bundle"}' - fi ;; wheel) if [[ -z "${artifacts}" ]]; then artifacts="$("${script_directory}/describe-wheels.sh" "${RELEASE_OUTPUT_DIRECTORY}")" fi - if [[ -z "${package}" && -z "${RELEASE_PACKAGE_FILE:-}" ]]; then - package='{"ecosystem":"wheel","name":"bundle"}' - fi ;; custom) require_nonempty "RELEASE_ARTIFACTS" "${artifacts}" + require_nonempty "RELEASE_PACKAGE_IDENTITY_FILE" "${RELEASE_PACKAGE_IDENTITY_FILE:-}" ;; *) echo "artifact-type must be one of: conda, custom, wheel" >&2 @@ -45,10 +39,4 @@ case "${RELEASE_ARTIFACT_TYPE}" in ;; esac -if [[ -z "${package}" && -z "${RELEASE_PACKAGE_FILE:-}" ]]; then - echo "one of release-package or release-package-file is required for custom artifacts" >&2 - exit 1 -fi - printf 'artifacts=%s\n' "${artifacts}" >>"${GITHUB_OUTPUT}" -printf 'package=%s\n' "${package}" >>"${GITHUB_OUTPUT}" diff --git a/release-catalog/validate-config.sh b/release-catalog/validate-config.sh new file mode 100755 index 00000000..9cbfafd1 --- /dev/null +++ b/release-catalog/validate-config.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + +set -euo pipefail + +emit_error() { + local message="$1" + printf '::error title=Invalid release catalog configuration::%s\n' "${message}" >&2 +} + +config="${RELEASE_CATALOG_CONFIG:-}" +if [[ -z "${config}" ]]; then + emit_error "release catalog configuration must be a non-empty JSON object" + exit 1 +fi + +if ! compact_config="$(jq -ce . <<<"${config}" 2>/dev/null)"; then + parse_error="$(jq -ce . <<<"${config}" 2>&1 || true)" + emit_error "release catalog configuration must be valid JSON: ${parse_error}" + exit 1 +fi + +validation_errors="$(jq -r ' + def single_line_string: + type == "string" and length > 0 and (test("[\\r\\n]") | not); + if type != "object" then + ["release catalog configuration must be a JSON object"] + else + (keys - ["artifact_type", "release_catalog_key", "output_directory", "package_identity_file", "artifacts"]) as $unknown + | [ + if ($unknown | length) > 0 then + "unknown field(s): " + ($unknown | join(", ")) + else empty end, + if (.artifact_type == "conda" or .artifact_type == "custom" or .artifact_type == "wheel") then empty + else "artifact_type must be one of: conda, custom, wheel" end, + if (.release_catalog_key | single_line_string) then empty + else "release_catalog_key must be a non-empty, single-line string" end, + if (has("output_directory") | not) or (.output_directory | single_line_string) then empty + else "output_directory must be a non-empty, single-line string when supplied" end + ] + + if .artifact_type == "custom" then + [ + if (.package_identity_file | single_line_string) then empty + else "package_identity_file is required for custom artifacts and must be a non-empty, single-line path relative to output_directory" end, + if (.artifacts | type == "array" and length > 0) then empty + else "artifacts must be a non-empty array for custom artifacts" end + ] + elif .artifact_type == "conda" or .artifact_type == "wheel" then + [ + if (.output_directory | single_line_string) then empty + else "output_directory is required for conda and wheel artifacts" end, + if has("artifacts") or has("package_identity_file") then + "artifacts and package_identity_file are only valid when artifact_type is custom" + else empty end + ] + else [] end + + if .artifact_type == "custom" and (.artifacts | type) == "array" then + [ + .artifacts | to_entries[] + | .key as $index + | .value as $artifact + | if ($artifact | type) != "object" then + "artifacts[\($index)] must be an object" + else + ($artifact | keys - ["path", "sbom", "provenance", "signature"]) as $artifact_unknown + | if ($artifact_unknown | length) > 0 then + "artifacts[\($index)] has unknown field(s): " + ($artifact_unknown | join(", ")) + else empty end, + if ($artifact.path | single_line_string) then empty + else "artifacts[\($index)].path must be a non-empty, single-line string" end, + ($artifact | to_entries[] + | select(.key == "sbom" or .key == "provenance" or .key == "signature") + | select((.value | single_line_string) | not) + | "artifacts[\($index)].\(.key) must be a non-empty, single-line string") + end + ] + else [] end + end + | .[] +' <<<"${compact_config}")" + +if [[ -n "${validation_errors}" ]]; then + while IFS= read -r validation_error; do + emit_error "${validation_error}" + done <<<"${validation_errors}" + exit 1 +fi + +if [[ -z "${GITHUB_OUTPUT:-}" ]]; then + emit_error "GITHUB_OUTPUT is required" + exit 1 +fi + +{ + printf 'artifact_type=%s\n' "$(jq -r '.artifact_type' <<<"${compact_config}")" + printf 'release_catalog_key=%s\n' "$(jq -r '.release_catalog_key' <<<"${compact_config}")" + printf 'output_directory=%s\n' "$(jq -r '.output_directory // "."' <<<"${compact_config}")" + printf 'package_identity_file=%s\n' "$(jq -r '.package_identity_file // empty' <<<"${compact_config}")" + printf 'artifacts=%s\n' "$(jq -c '.artifacts // empty' <<<"${compact_config}")" +} >>"${GITHUB_OUTPUT}" diff --git a/tests/release-catalog-config/package-identity-file.json b/tests/release-catalog-config/package-identity-file.json new file mode 100644 index 00000000..0c6bacd8 --- /dev/null +++ b/tests/release-catalog-config/package-identity-file.json @@ -0,0 +1,12 @@ +{ + "artifact_type": "custom", + "release_catalog_key": "maven:cuvs-java", + "output_directory": "java/cuvs-java/target", + "package_identity_file": "cuvs-java.release-package-identity.json", + "artifacts": [ + { + "path": "cuvs-java-*-x86_64-cuda*.jar", + "sbom": "cuvs-java.spdx.json" + } + ] +} diff --git a/tests/release_build_output_test.sh b/tests/release_build_output_test.sh deleted file mode 100755 index 2a5dc6e1..00000000 --- a/tests/release_build_output_test.sh +++ /dev/null @@ -1,165 +0,0 @@ -#!/usr/bin/env bash -# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. - -set -euo pipefail - -repository_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -temporary_directory="$(mktemp -d)" -trap 'rm -rf "${temporary_directory}"' EXIT - -bundle_directory="${temporary_directory}/bundle" -mkdir -p "${bundle_directory}" -printf '%s\n' jar >"${bundle_directory}/cuvs-java-26.08.0.jar" -printf '%s\n' sbom >"${bundle_directory}/cuvs-java-26.08.0.spdx.json" -printf '%s\n' provenance >"${bundle_directory}/cuvs-java-26.08.0.provenance.jsonl" -printf '%s\n' signature >"${bundle_directory}/cuvs-java-26.08.0.jar.asc" -jq -n \ - '{ecosystem: "maven", name: "ai.rapids:cuvs-java", version: "26.08.0"}' \ - >"${bundle_directory}/cuvs-java-package.json" - -GITHUB_OUTPUT="${temporary_directory}/github-output" -GITHUB_REPOSITORY="rapidsai/cuvs" -GITHUB_RUN_ATTEMPT="1" -GITHUB_RUN_ID="1234" -GITHUB_SHA="0123456789012345678901234567890123456789" -GITHUB_WORKFLOW_REF="rapidsai/cuvs/.github/workflows/build.yaml@refs/heads/release/26.08" -export GITHUB_OUTPUT -RELEASE_ARTIFACTS="$(jq -cn '[{path: "cuvs-java-*.jar", sbom: "cuvs-java-*.spdx.json", provenance: "cuvs-java-*.provenance.jsonl", signature: "cuvs-java-*.jar.asc"}]')" -RELEASE_MANIFEST_NAME="release-build-output.json" -RELEASE_METADATA_NAME="release-build-metadata.json" -RELEASE_OUTPUT_DIRECTORY="${bundle_directory}" -RELEASE_PACKAGE='' -RELEASE_PACKAGE_FILE="cuvs-java-package.json" -RELEASE_SOURCE_ARTIFACT_NAME="cuvs-java-cuda12.9.1" -RELEASE_SOURCE_SHA="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" -RELEASE_UNIT="maven:cuvs-java" -export GITHUB_REPOSITORY GITHUB_RUN_ATTEMPT GITHUB_RUN_ID GITHUB_SHA GITHUB_WORKFLOW_REF -export RELEASE_ARTIFACTS RELEASE_MANIFEST_NAME RELEASE_METADATA_NAME RELEASE_OUTPUT_DIRECTORY RELEASE_PACKAGE RELEASE_PACKAGE_FILE -export RELEASE_SOURCE_ARTIFACT_NAME RELEASE_SOURCE_SHA RELEASE_UNIT - -"${repository_root}/release-build-output/materialize.sh" - -canonical_bundle_directory="$(realpath "${bundle_directory}")" -manifest_path="${canonical_bundle_directory}/release-build-output.json" -metadata_path="${canonical_bundle_directory}/release-build-metadata.json" -jq -e ' - .schema_version == 1 - and .producer == "release-platform" - and (.artifacts | length == 1) - and .artifacts[0].unit_id == "maven:cuvs-java" - and .artifacts[0].path == "cuvs-java-26.08.0.jar" - and .artifacts[0].package.name == "ai.rapids:cuvs-java" -' "${manifest_path}" >/dev/null -jq -e ' - .schema_version == 1 - and .producer == "shared-workflows" - and .release_unit == "maven:cuvs-java" - and .source_artifact == "cuvs-java-cuda12.9.1" - and .build_output_manifest == "release-build-output.json" - and .build_environment.repository == "rapidsai/cuvs" - and .build_environment.sha == "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - and .metadata.artifacts == [{path: "cuvs-java-26.08.0.jar", sbom_kind: "producer-dependency"}] -' "${metadata_path}" >/dev/null -grep -Fx "manifest-path=${manifest_path}" "${GITHUB_OUTPUT}" -grep -Fx "metadata-path=${metadata_path}" "${GITHUB_OUTPUT}" - -supplied_sbom_path="$(jq -r '.artifacts[0].sbom' "${manifest_path}")" -supplied_provenance_path="$(jq -r '.artifacts[0].provenance' "${manifest_path}")" -supplied_signature_path="$(jq -r '.artifacts[0].signature' "${manifest_path}")" -[[ "${supplied_sbom_path}" == release-evidence/*/sbom-* ]] -[[ "${supplied_provenance_path}" == release-evidence/*/provenance-* ]] -[[ "${supplied_signature_path}" == release-evidence/*/signature-* ]] -grep -Fx sbom "${canonical_bundle_directory}/${supplied_sbom_path}" -grep -Fx provenance "${canonical_bundle_directory}/${supplied_provenance_path}" -grep -Fx signature "${canonical_bundle_directory}/${supplied_signature_path}" - -isolated_companion_directory="${temporary_directory}/isolated-companion" -mkdir -p "${isolated_companion_directory}" -cp "${manifest_path}" "${metadata_path}" "${isolated_companion_directory}/" -cp -R "${canonical_bundle_directory}/release-evidence" "${isolated_companion_directory}/" -while IFS= read -r evidence_path; do - test -f "${isolated_companion_directory}/${evidence_path}" -done < <(jq -r '.artifacts[] | .sbom, .provenance, (.signature // empty)' "${isolated_companion_directory}/release-build-output.json") - -generated_directory="${temporary_directory}/generated-bundle" -mkdir -p "${generated_directory}/linux-64" -printf '%s\n' conda >"${generated_directory}/linux-64/kvikio-26.08.00a32-cuda12_260714_2f567060.conda" - -RELEASE_ARTIFACTS="$(jq -cn '[{path: "linux-64/kvikio-*.conda"}]')" -RELEASE_OUTPUT_DIRECTORY="${generated_directory}" -RELEASE_PACKAGE="$(jq -cn '{ecosystem: "conda", name: "kvikio"}')" -RELEASE_PACKAGE_FILE='' -RELEASE_SOURCE_ARTIFACT_NAME="kvikio_conda_python_kvikio_x86_64_abi3_cu12" -RELEASE_SOURCE_SHA="bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" -RELEASE_UNIT="conda:kvikio" -export RELEASE_ARTIFACTS RELEASE_OUTPUT_DIRECTORY RELEASE_PACKAGE RELEASE_PACKAGE_FILE RELEASE_SOURCE_ARTIFACT_NAME RELEASE_SOURCE_SHA RELEASE_UNIT - -"${repository_root}/release-build-output/materialize.sh" - -generated_manifest_path="${generated_directory}/release-build-output.json" -generated_metadata_path="${generated_directory}/release-build-metadata.json" -generated_sbom_path="$(jq -r '.artifacts[0].sbom' "${generated_manifest_path}")" -generated_provenance_path="$(jq -r '.artifacts[0].provenance' "${generated_manifest_path}")" -jq -e ' - .artifacts[0].unit_id == "conda:kvikio" - and .artifacts[0].path == "linux-64/kvikio-26.08.00a32-cuda12_260714_2f567060.conda" - and .artifacts[0].package == {ecosystem: "conda", name: "kvikio", version: "26.08.00a32"} -' "${generated_manifest_path}" >/dev/null -jq -e ' - .spdxVersion == "SPDX-2.3" - and .packages[0].name == "kvikio" - and .packages[0].versionInfo == "26.08.00a32" -' "${generated_directory}/${generated_sbom_path}" >/dev/null -jq -e ' - .predicateType == "https://slsa.dev/provenance/v1" - and .predicate.buildDefinition.externalParameters.release_unit == "conda:kvikio" - and .predicate.buildDefinition.resolvedDependencies[0].digest.gitCommit == "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" -' "${generated_directory}/${generated_provenance_path}" >/dev/null -jq -e ' - .release_unit == "conda:kvikio" - and .source_artifact == "kvikio_conda_python_kvikio_x86_64_abi3_cu12" - and .build_environment.sha == "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" - and .metadata.artifacts == [{path: "linux-64/kvikio-26.08.00a32-cuda12_260714_2f567060.conda", sbom_kind: "generated-identity"}] -' "${generated_metadata_path}" >/dev/null - -wheel_directory="${temporary_directory}/wheel-bundle" -mkdir -p "${wheel_directory}" -printf '%s\n' wheel >"${wheel_directory}/libkvikio_cu12-26.8.0a32-py3-none-manylinux_2_28_x86_64.whl" - -RELEASE_ARTIFACTS="$(jq -cn '[{path: "libkvikio_cu12-*.whl"}]')" -RELEASE_OUTPUT_DIRECTORY="${wheel_directory}" -RELEASE_PACKAGE="$(jq -cn '{ecosystem: "wheel", name: "libkvikio-cu12"}')" -RELEASE_SOURCE_ARTIFACT_NAME="kvikio_wheel_cpp_libkvikio_x86_64_cu12" -RELEASE_SOURCE_SHA="cccccccccccccccccccccccccccccccccccccccc" -RELEASE_UNIT="wheel:kvikio" -export RELEASE_ARTIFACTS RELEASE_OUTPUT_DIRECTORY RELEASE_PACKAGE RELEASE_SOURCE_ARTIFACT_NAME RELEASE_SOURCE_SHA RELEASE_UNIT - -"${repository_root}/release-build-output/materialize.sh" - -jq -e ' - .artifacts[0].unit_id == "wheel:kvikio" - and .artifacts[0].path == "libkvikio_cu12-26.8.0a32-py3-none-manylinux_2_28_x86_64.whl" - and .artifacts[0].package == {ecosystem: "wheel", name: "libkvikio-cu12", version: "26.8.0a32"} -' "${wheel_directory}/release-build-output.json" >/dev/null -jq -e ' - .metadata.artifacts == [{path: "libkvikio_cu12-26.8.0a32-py3-none-manylinux_2_28_x86_64.whl", sbom_kind: "generated-identity"}] -' "${wheel_directory}/release-build-metadata.json" >/dev/null - -missing_version_directory="${temporary_directory}/missing-version-bundle" -mkdir -p "${missing_version_directory}" -printf '%s\n' archive >"${missing_version_directory}/bundle.tar.gz" - -RELEASE_ARTIFACTS="$(jq -cn '[{path: "bundle.tar.gz"}]')" -RELEASE_OUTPUT_DIRECTORY="${missing_version_directory}" -RELEASE_PACKAGE="$(jq -cn '{ecosystem: "archive", name: "bundle"}')" -RELEASE_PACKAGE_FILE='' -RELEASE_SOURCE_ARTIFACT_NAME="bundle-archive" -RELEASE_SOURCE_SHA="dddddddddddddddddddddddddddddddddddddddd" -RELEASE_UNIT="archive:bundle" -export RELEASE_ARTIFACTS RELEASE_OUTPUT_DIRECTORY RELEASE_PACKAGE RELEASE_PACKAGE_FILE RELEASE_SOURCE_ARTIFACT_NAME RELEASE_SOURCE_SHA RELEASE_UNIT - -if "${repository_root}/release-build-output/materialize.sh" 2>"${temporary_directory}/missing-version-error"; then - echo "materialize.sh unexpectedly accepted a custom artifact without a package version" >&2 - exit 1 -fi -grep -Fx 'release-package version is required for archive artifacts' "${temporary_directory}/missing-version-error" diff --git a/tests/release_catalog_config_test.sh b/tests/release_catalog_config_test.sh new file mode 100755 index 00000000..ccfe3959 --- /dev/null +++ b/tests/release_catalog_config_test.sh @@ -0,0 +1,103 @@ +#!/usr/bin/env bash +# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + +set -euo pipefail + +repository_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +validator="${repository_root}/release-catalog/validate-config.sh" +temporary_directory="$(mktemp -d)" +trap 'rm -rf "${temporary_directory}"' EXIT + +assert_invalid() { + local name="$1" + local config="$2" + local expected="$3" + local error_file="${temporary_directory}/${name}.error" + + if RELEASE_CATALOG_CONFIG="${config}" GITHUB_OUTPUT="${temporary_directory}/${name}.output" \ + "${validator}" 2>"${error_file}"; then + echo "validate-config.sh unexpectedly accepted ${name}" >&2 + exit 1 + fi + if ! grep -F -- "${expected}" "${error_file}" >/dev/null; then + echo "validate-config.sh did not explain ${name}; output was:" >&2 + sed 's/^/ /' "${error_file}" >&2 + exit 1 + fi +} + +valid_output="${temporary_directory}/valid.output" +RELEASE_CATALOG_CONFIG="$(<"${repository_root}/tests/release-catalog-config/package-identity-file.json")" \ + GITHUB_OUTPUT="${valid_output}" "${validator}" + +grep -Fx 'release_catalog_key=maven:cuvs-java' "${valid_output}" +grep -Fx 'artifact_type=custom' "${valid_output}" +grep -Fx 'output_directory=java/cuvs-java/target' "${valid_output}" +grep -Fx 'package_identity_file=cuvs-java.release-package-identity.json' "${valid_output}" +grep -Fx 'artifacts=[{"path":"cuvs-java-*-x86_64-cuda*.jar","sbom":"cuvs-java.spdx.json"}]' "${valid_output}" + +assert_invalid \ + malformed-json \ + '{"release_catalog_key":' \ + 'release catalog configuration must be valid JSON' + +assert_invalid \ + not-an-object \ + '[]' \ + 'release catalog configuration must be a JSON object' + +assert_invalid \ + missing-fields \ + '{}' \ + 'release_catalog_key must be a non-empty, single-line string' +grep -F 'artifact_type must be one of: conda, custom, wheel' "${temporary_directory}/missing-fields.error" >/dev/null + +assert_invalid \ + custom-missing-fields \ + '{"artifact_type":"custom","release_catalog_key":"archive:smoke"}' \ + 'package_identity_file is required for custom artifacts' +grep -F 'artifacts must be a non-empty array for custom artifacts' "${temporary_directory}/custom-missing-fields.error" >/dev/null + +assert_invalid \ + unknown-field \ + '{"artifact_type":"custom","release_catalog_key":"archive:smoke","package_identity_file":"package.json","artifacts":[{"path":"smoke.tar.gz"}],"component_id":"archive:smoke"}' \ + 'unknown field(s): component_id' + +assert_invalid \ + inline-package \ + '{"artifact_type":"custom","release_catalog_key":"archive:smoke","package_identity_file":"package.json","package":{"ecosystem":"archive","name":"smoke","version":"1.0"},"artifacts":[{"path":"smoke.tar.gz"}]}' \ + 'unknown field(s): package' + +assert_invalid \ + per-artifact-package \ + '{"artifact_type":"custom","release_catalog_key":"archive:smoke","package_identity_file":"package.json","artifacts":[{"path":"smoke.tar.gz","package":{"ecosystem":"archive","name":"smoke","version":"1.0"}}]}' \ + 'artifacts[0] has unknown field(s): package' + +assert_invalid \ + malformed-artifact \ + '{"artifact_type":"custom","release_catalog_key":"archive:smoke","package_identity_file":"package.json","artifacts":[{"file":"smoke.tar.gz","sbom":false}]}' \ + 'artifacts[0] has unknown field(s): file' +grep -F 'artifacts[0].path must be a non-empty, single-line string' "${temporary_directory}/malformed-artifact.error" >/dev/null +grep -F 'artifacts[0].sbom must be a non-empty, single-line string' "${temporary_directory}/malformed-artifact.error" >/dev/null + +assert_invalid \ + standard-missing-output-directory \ + '{"artifact_type":"conda","release_catalog_key":"conda:smoke"}' \ + 'output_directory is required for conda and wheel artifacts' + +assert_invalid \ + standard-custom-fields \ + '{"artifact_type":"wheel","release_catalog_key":"wheel:smoke","output_directory":"dist","artifacts":[{"path":"smoke.whl"}]}' \ + 'artifacts and package_identity_file are only valid when artifact_type is custom' + +standard_output="${temporary_directory}/standard.output" +RELEASE_CATALOG_CONFIG='{ + "artifact_type": "wheel", + "release_catalog_key": "wheel:kvikio", + "output_directory": "dist" +}' GITHUB_OUTPUT="${standard_output}" "${validator}" + +grep -Fx 'artifact_type=wheel' "${standard_output}" +grep -Fx 'release_catalog_key=wheel:kvikio' "${standard_output}" +grep -Fx 'output_directory=dist' "${standard_output}" +grep -Fx 'artifacts=' "${standard_output}" diff --git a/tests/release_build_output_descriptors_test.sh b/tests/release_catalog_descriptors_test.sh similarity index 92% rename from tests/release_build_output_descriptors_test.sh rename to tests/release_catalog_descriptors_test.sh index c6b6c46c..1c608e51 100755 --- a/tests/release_build_output_descriptors_test.sh +++ b/tests/release_catalog_descriptors_test.sh @@ -20,7 +20,7 @@ printf '%s\n' \ zip -qr "${wheel_output_directory}/libkvikio_cu12-26.8.0a32-py3-none-manylinux_2_28_x86_64.whl" . ) -wheel_descriptors="$("${repository_root}/release-build-output/describe-wheels.sh" "${wheel_output_directory}")" +wheel_descriptors="$("${repository_root}/release-catalog/describe-wheels.sh" "${wheel_output_directory}")" jq -e ' . == [{ path: "libkvikio_cu12-26.8.0a32-py3-none-manylinux_2_28_x86_64.whl", @@ -57,7 +57,7 @@ zstd -q -f "${temporary_directory}/info-librmm.tar" -o "${temporary_directory}/i info-librmm.tar.zst ) -conda_descriptors="$("${repository_root}/release-build-output/describe-conda.sh" "${conda_output_directory}")" +conda_descriptors="$("${repository_root}/release-catalog/describe-conda.sh" "${conda_output_directory}")" jq -e ' length == 2 and any(.[]; diff --git a/tests/release_build_output_prepare_test.sh b/tests/release_catalog_prepare_test.sh similarity index 77% rename from tests/release_build_output_prepare_test.sh rename to tests/release_catalog_prepare_test.sh index 3598e1e5..399df071 100755 --- a/tests/release_build_output_prepare_test.sh +++ b/tests/release_catalog_prepare_test.sh @@ -22,13 +22,11 @@ GITHUB_OUTPUT="${temporary_directory}/wheel-output" RELEASE_ARTIFACTS='' RELEASE_ARTIFACT_TYPE=wheel RELEASE_OUTPUT_DIRECTORY="${temporary_directory}" -RELEASE_PACKAGE='' -RELEASE_PACKAGE_FILE='' -export GITHUB_OUTPUT RELEASE_ARTIFACTS RELEASE_ARTIFACT_TYPE RELEASE_OUTPUT_DIRECTORY RELEASE_PACKAGE RELEASE_PACKAGE_FILE +RELEASE_PACKAGE_IDENTITY_FILE='' +export GITHUB_OUTPUT RELEASE_ARTIFACTS RELEASE_ARTIFACT_TYPE RELEASE_OUTPUT_DIRECTORY RELEASE_PACKAGE_IDENTITY_FILE -"${repository_root}/release-build-output/prepare.sh" +"${repository_root}/release-catalog/prepare.sh" -grep -Fx 'package={"ecosystem":"wheel","name":"bundle"}' "${GITHUB_OUTPUT}" artifacts="$(sed -n 's/^artifacts=//p' "${GITHUB_OUTPUT}")" jq -e ' . == [{ @@ -40,19 +38,17 @@ jq -e ' GITHUB_OUTPUT="${temporary_directory}/custom-output" RELEASE_ARTIFACTS="$(jq -cn '[{path: "bundle.tar.gz", sbom: "bundle.spdx.json"}]')" RELEASE_ARTIFACT_TYPE=custom -RELEASE_PACKAGE='' -RELEASE_PACKAGE_FILE=release-package.json -export GITHUB_OUTPUT RELEASE_ARTIFACTS RELEASE_ARTIFACT_TYPE RELEASE_PACKAGE RELEASE_PACKAGE_FILE +RELEASE_PACKAGE_IDENTITY_FILE=release-package-identity.json +export GITHUB_OUTPUT RELEASE_ARTIFACTS RELEASE_ARTIFACT_TYPE RELEASE_PACKAGE_IDENTITY_FILE -"${repository_root}/release-build-output/prepare.sh" +"${repository_root}/release-catalog/prepare.sh" grep -Fx 'artifacts=[{"path":"bundle.tar.gz","sbom":"bundle.spdx.json"}]' "${GITHUB_OUTPUT}" -grep -Fx 'package=' "${GITHUB_OUTPUT}" GITHUB_OUTPUT="${temporary_directory}/invalid-output" RELEASE_ARTIFACT_TYPE=archive export GITHUB_OUTPUT RELEASE_ARTIFACT_TYPE -if "${repository_root}/release-build-output/prepare.sh" 2>"${temporary_directory}/invalid-error"; then +if "${repository_root}/release-catalog/prepare.sh" 2>"${temporary_directory}/invalid-error"; then echo "prepare.sh unexpectedly accepted an invalid artifact type" >&2 exit 1 fi diff --git a/tests/release_catalog_test.sh b/tests/release_catalog_test.sh new file mode 100755 index 00000000..db347cf3 --- /dev/null +++ b/tests/release_catalog_test.sh @@ -0,0 +1,153 @@ +#!/usr/bin/env bash +# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + +set -euo pipefail + +repository_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +temporary_directory="$(mktemp -d)" +trap 'rm -rf "${temporary_directory}"' EXIT + +bundle_directory="${temporary_directory}/bundle" +mkdir -p "${bundle_directory}" +printf '%s\n' jar >"${bundle_directory}/cuvs-java-26.08.0.jar" +printf '%s\n' sbom >"${bundle_directory}/cuvs-java-26.08.0.spdx.json" +printf '%s\n' provenance >"${bundle_directory}/cuvs-java-26.08.0.provenance.jsonl" +printf '%s\n' signature >"${bundle_directory}/cuvs-java-26.08.0.jar.asc" +jq -n \ + '{ecosystem: "maven", name: "ai.rapids:cuvs-java", version: "26.08.0"}' \ + >"${bundle_directory}/cuvs-java-package-identity.json" + +GITHUB_OUTPUT="${temporary_directory}/github-output" +GITHUB_REPOSITORY="rapidsai/cuvs" +GITHUB_RUN_ATTEMPT="1" +GITHUB_RUN_ID="1234" +GITHUB_SHA="0123456789012345678901234567890123456789" +GITHUB_WORKFLOW_REF="rapidsai/cuvs/.github/workflows/build.yaml@refs/heads/release/26.08" +export GITHUB_OUTPUT +RELEASE_ARTIFACTS="$(jq -cn '[{path: "cuvs-java-*.jar", sbom: "cuvs-java-*.spdx.json", provenance: "cuvs-java-*.provenance.jsonl", signature: "cuvs-java-*.jar.asc"}]')" +RELEASE_ARTIFACT_TYPE=custom +RELEASE_ENTRIES_NAME="release-catalog-entries.json" +RELEASE_OUTPUT_DIRECTORY="${bundle_directory}" +RELEASE_PACKAGE_IDENTITY_FILE="cuvs-java-package-identity.json" +RELEASE_SOURCE_ARTIFACT_NAME="cuvs-java-cuda12.9.1" +RELEASE_SOURCE_SHA="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +RELEASE_CATALOG_KEY="maven:cuvs-java" +export GITHUB_REPOSITORY GITHUB_RUN_ATTEMPT GITHUB_RUN_ID GITHUB_SHA GITHUB_WORKFLOW_REF +export RELEASE_ARTIFACTS RELEASE_ARTIFACT_TYPE RELEASE_ENTRIES_NAME RELEASE_OUTPUT_DIRECTORY RELEASE_PACKAGE_IDENTITY_FILE +export RELEASE_SOURCE_ARTIFACT_NAME RELEASE_SOURCE_SHA RELEASE_CATALOG_KEY + +"${repository_root}/release-catalog/materialize.sh" + +canonical_bundle_directory="$(realpath "${bundle_directory}")" +entries_path="${canonical_bundle_directory}/release-catalog-entries.json" +test ! -e "${canonical_bundle_directory}/release-catalog-metadata.json" +jq -e ' + .schema_version == 1 + and .producer == "shared-workflows" + and .source.artifact == "cuvs-java-cuda12.9.1" + and .source.repository == "rapidsai/cuvs" + and .source.sha == "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + and (.entries | length == 1) + and .entries[0].release_catalog_key == "maven:cuvs-java" + and .entries[0].path == "cuvs-java-26.08.0.jar" + and .entries[0].package.name == "ai.rapids:cuvs-java" + and .entries[0].sbom_kind == "producer-dependency" +' "${entries_path}" >/dev/null +grep -Fx "entries-path=${entries_path}" "${GITHUB_OUTPUT}" + +supplied_sbom_path="$(jq -r '.entries[0].sbom' "${entries_path}")" +supplied_provenance_path="$(jq -r '.entries[0].provenance' "${entries_path}")" +supplied_signature_path="$(jq -r '.entries[0].signature' "${entries_path}")" +[[ "${supplied_sbom_path}" == release-evidence/*/sbom-* ]] +[[ "${supplied_provenance_path}" == release-evidence/*/provenance-* ]] +[[ "${supplied_signature_path}" == release-evidence/*/signature-* ]] +grep -Fx sbom "${canonical_bundle_directory}/${supplied_sbom_path}" +grep -Fx provenance "${canonical_bundle_directory}/${supplied_provenance_path}" +grep -Fx signature "${canonical_bundle_directory}/${supplied_signature_path}" + +isolated_companion_directory="${temporary_directory}/isolated-companion" +mkdir -p "${isolated_companion_directory}" +cp "${entries_path}" "${isolated_companion_directory}/" +cp -R "${canonical_bundle_directory}/release-evidence" "${isolated_companion_directory}/" +while IFS= read -r evidence_path; do + test -f "${isolated_companion_directory}/${evidence_path}" +done < <(jq -r '.entries[] | .sbom, .provenance, (.signature // empty)' "${isolated_companion_directory}/release-catalog-entries.json") + +generated_directory="${temporary_directory}/generated-bundle" +mkdir -p "${generated_directory}/linux-64" +printf '%s\n' conda >"${generated_directory}/linux-64/kvikio-26.08.00a32-cuda12_260714_2f567060.conda" + +RELEASE_ARTIFACTS="$(jq -cn '[{path: "linux-64/kvikio-*.conda", package: {ecosystem: "conda", name: "kvikio", version: "26.08.00a32"}}]')" +RELEASE_ARTIFACT_TYPE=conda +RELEASE_OUTPUT_DIRECTORY="${generated_directory}" +RELEASE_PACKAGE_IDENTITY_FILE='' +RELEASE_SOURCE_ARTIFACT_NAME="kvikio_conda_python_kvikio_x86_64_abi3_cu12" +RELEASE_SOURCE_SHA="bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" +RELEASE_CATALOG_KEY="conda:kvikio" +export RELEASE_ARTIFACTS RELEASE_ARTIFACT_TYPE RELEASE_OUTPUT_DIRECTORY RELEASE_PACKAGE_IDENTITY_FILE RELEASE_SOURCE_ARTIFACT_NAME RELEASE_SOURCE_SHA RELEASE_CATALOG_KEY + +"${repository_root}/release-catalog/materialize.sh" + +generated_entries_path="${generated_directory}/release-catalog-entries.json" +generated_sbom_path="$(jq -r '.entries[0].sbom' "${generated_entries_path}")" +generated_provenance_path="$(jq -r '.entries[0].provenance' "${generated_entries_path}")" +jq -e ' + .source.artifact == "kvikio_conda_python_kvikio_x86_64_abi3_cu12" + and .source.sha == "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + and .entries[0].release_catalog_key == "conda:kvikio" + and .entries[0].path == "linux-64/kvikio-26.08.00a32-cuda12_260714_2f567060.conda" + and .entries[0].package == {ecosystem: "conda", name: "kvikio", version: "26.08.00a32"} + and .entries[0].sbom_kind == "generated-identity" +' "${generated_entries_path}" >/dev/null +jq -e ' + .spdxVersion == "SPDX-2.3" + and .packages[0].name == "kvikio" + and .packages[0].versionInfo == "26.08.00a32" +' "${generated_directory}/${generated_sbom_path}" >/dev/null +jq -e ' + .predicateType == "https://slsa.dev/provenance/v1" + and .predicate.buildDefinition.externalParameters.release_catalog_key == "conda:kvikio" + and .predicate.buildDefinition.resolvedDependencies[0].digest.gitCommit == "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" +' "${generated_directory}/${generated_provenance_path}" >/dev/null +wheel_directory="${temporary_directory}/wheel-bundle" +mkdir -p "${wheel_directory}" +printf '%s\n' wheel >"${wheel_directory}/libkvikio_cu12-26.8.0a32-py3-none-manylinux_2_28_x86_64.whl" + +RELEASE_ARTIFACTS="$(jq -cn '[{path: "libkvikio_cu12-*.whl", package: {ecosystem: "wheel", name: "libkvikio-cu12", version: "26.8.0a32"}}]')" +RELEASE_ARTIFACT_TYPE=wheel +RELEASE_OUTPUT_DIRECTORY="${wheel_directory}" +RELEASE_SOURCE_ARTIFACT_NAME="kvikio_wheel_cpp_libkvikio_x86_64_cu12" +RELEASE_SOURCE_SHA="cccccccccccccccccccccccccccccccccccccccc" +RELEASE_CATALOG_KEY="wheel:kvikio" +export RELEASE_ARTIFACTS RELEASE_ARTIFACT_TYPE RELEASE_OUTPUT_DIRECTORY RELEASE_SOURCE_ARTIFACT_NAME RELEASE_SOURCE_SHA RELEASE_CATALOG_KEY + +"${repository_root}/release-catalog/materialize.sh" + +jq -e ' + .entries[0].release_catalog_key == "wheel:kvikio" + and .entries[0].path == "libkvikio_cu12-26.8.0a32-py3-none-manylinux_2_28_x86_64.whl" + and .entries[0].package == {ecosystem: "wheel", name: "libkvikio-cu12", version: "26.8.0a32"} + and .entries[0].sbom_kind == "generated-identity" +' "${wheel_directory}/release-catalog-entries.json" >/dev/null + +missing_version_directory="${temporary_directory}/missing-version-bundle" +mkdir -p "${missing_version_directory}" +printf '%s\n' archive >"${missing_version_directory}/bundle.tar.gz" +jq -n \ + '{ecosystem: "archive", name: "bundle"}' \ + >"${missing_version_directory}/bundle-package-identity.json" + +RELEASE_ARTIFACTS="$(jq -cn '[{path: "bundle.tar.gz"}]')" +RELEASE_ARTIFACT_TYPE=custom +RELEASE_OUTPUT_DIRECTORY="${missing_version_directory}" +RELEASE_PACKAGE_IDENTITY_FILE='bundle-package-identity.json' +RELEASE_SOURCE_ARTIFACT_NAME="bundle-archive" +RELEASE_SOURCE_SHA="dddddddddddddddddddddddddddddddddddddddd" +RELEASE_CATALOG_KEY="archive:bundle" +export RELEASE_ARTIFACTS RELEASE_ARTIFACT_TYPE RELEASE_OUTPUT_DIRECTORY RELEASE_PACKAGE_IDENTITY_FILE RELEASE_SOURCE_ARTIFACT_NAME RELEASE_SOURCE_SHA RELEASE_CATALOG_KEY + +if "${repository_root}/release-catalog/materialize.sh" 2>"${temporary_directory}/missing-version-error"; then + echo "materialize.sh unexpectedly accepted a custom artifact without a package version" >&2 + exit 1 +fi +grep -Fx 'package identity file must contain non-empty ecosystem, name, and version strings and only optional build or platform strings' "${temporary_directory}/missing-version-error"