diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 4634c155..7b3ebce7 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -20,3 +20,34 @@ jobs: fetch-depth: 1 persist-credentials: false - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 + - name: Test release build-output 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 + 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 + 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 + source-sha: ${{ github.event.pull_request.head.sha }} + - name: Verify release build-output 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 diff --git a/README.md b/README.md index b14bfb81..4ea1b0ca 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,41 @@ 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-build-output-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. + +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. + +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 +the wrapper, which allows a feature-branch wrapper to dispatch to its matching +implementation during canary testing. + There can be more complicated arrangements of more actions, but the idea is to have the local clone of the shared-actions repository be the first step of an action. diff --git a/release-build-output-dispatch/action.yml b/release-build-output-dispatch/action.yml new file mode 100644 index 00000000..154ba3d7 --- /dev/null +++ b/release-build-output-dispatch/action.yml @@ -0,0 +1,72 @@ +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 new file mode 100644 index 00000000..7c6965e3 --- /dev/null +++ b/release-build-output/action.yml @@ -0,0 +1,90 @@ +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-build-output/describe-conda.sh b/release-build-output/describe-conda.sh new file mode 100755 index 00000000..6449addd --- /dev/null +++ b/release-build-output/describe-conda.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + +set -euo pipefail + +if [[ "$#" -ne 1 || ! -d "$1" ]]; then + echo "usage: $0 CONDA_OUTPUT_DIRECTORY" >&2 + exit 1 +fi + +output_directory="$(realpath "$1")" +descriptors='[]' +package_count=0 + +while IFS= read -r package_path; do + package_path="$(realpath "${package_path}")" + if [[ "${package_path}" != "${output_directory}"/* ]]; then + echo "Conda package must resolve inside output directory: ${package_path}" >&2 + exit 1 + fi + relative_path="${package_path#"${output_directory}/"}" + + case "${package_path}" in + *.conda) + info_members=() + while IFS= read -r info_member; do + info_members+=("${info_member}") + done < <(unzip -Z1 "${package_path}" | awk '/^info-.*\.tar\.zst$/') + if [[ "${#info_members[@]}" -ne 1 ]]; then + echo ".conda package must contain exactly one info-*.tar.zst member: ${relative_path}" >&2 + exit 1 + fi + index_json="$(unzip -p "${package_path}" "${info_members[0]}" | zstd -dc | tar -xOf - info/index.json)" + ;; + *.tar.bz2) + index_json="$(tar -xOjf "${package_path}" info/index.json)" + ;; + *) + echo "unsupported Conda package extension: ${relative_path}" >&2 + exit 1 + ;; + esac + + if ! jq -e ' + type == "object" + and (.name | type == "string" and length > 0) + and (.version | type == "string" and length > 0) + and (.build | type == "string" and length > 0) + and (.subdir | type == "string" and length > 0) + ' <<<"${index_json}" >/dev/null; then + echo "Conda info/index.json must contain exact name, version, build, and subdir fields: ${relative_path}" >&2 + exit 1 + fi + + package="$(jq -c '{ecosystem: "conda", name, version, build, platform: .subdir}' <<<"${index_json}")" + descriptors="$(jq -cn \ + --arg path "${relative_path}" \ + --argjson package "${package}" \ + --argjson current "${descriptors}" \ + '$current + [{path: $path, package: $package}]')" + package_count=$((package_count + 1)) +done < <(find "${output_directory}" -type f \( -name '*.conda' -o -name '*.tar.bz2' \) -print | sort) + +if [[ "${package_count}" -eq 0 ]]; then + echo "Conda output directory contains no .conda or .tar.bz2 packages: ${output_directory}" >&2 + exit 1 +fi + +printf '%s\n' "${descriptors}" diff --git a/release-build-output/describe-wheels.sh b/release-build-output/describe-wheels.sh new file mode 100755 index 00000000..bc6b925e --- /dev/null +++ b/release-build-output/describe-wheels.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + +set -euo pipefail + +if [[ "$#" -ne 1 || ! -d "$1" ]]; then + echo "usage: $0 WHEEL_OUTPUT_DIRECTORY" >&2 + exit 1 +fi + +output_directory="$(realpath "$1")" +descriptors='[]' +wheel_count=0 + +while IFS= read -r wheel_path; do + wheel_path="$(realpath "${wheel_path}")" + if [[ "${wheel_path}" != "${output_directory}"/* ]]; then + echo "wheel must resolve inside output directory: ${wheel_path}" >&2 + exit 1 + fi + relative_path="${wheel_path#"${output_directory}/"}" + + metadata_members=() + while IFS= read -r metadata_member; do + metadata_members+=("${metadata_member}") + done < <(unzip -Z1 "${wheel_path}" | awk '/\.dist-info\/METADATA$/') + if [[ "${#metadata_members[@]}" -ne 1 ]]; then + echo "wheel must contain exactly one .dist-info/METADATA file: ${relative_path}" >&2 + exit 1 + fi + + metadata="$(unzip -p "${wheel_path}" "${metadata_members[0]}")" + package_name="$(awk 'tolower($0) ~ /^name:[[:space:]]*/ {sub(/^[^:]*:[[:space:]]*/, ""); sub(/\r$/, ""); print; exit}' <<<"${metadata}")" + package_version="$(awk 'tolower($0) ~ /^version:[[:space:]]*/ {sub(/^[^:]*:[[:space:]]*/, ""); sub(/\r$/, ""); print; exit}' <<<"${metadata}")" + if [[ -z "${package_name}" || -z "${package_version}" ]]; then + echo "wheel metadata must contain non-empty Name and Version fields: ${relative_path}" >&2 + exit 1 + fi + + descriptors="$(jq -cn \ + --arg path "${relative_path}" \ + --arg name "${package_name}" \ + --arg version "${package_version}" \ + --argjson current "${descriptors}" \ + '$current + [{path: $path, package: {ecosystem: "wheel", name: $name, version: $version}}]')" + wheel_count=$((wheel_count + 1)) +done < <(find "${output_directory}" -type f -name '*.whl' -print | sort) + +if [[ "${wheel_count}" -eq 0 ]]; then + echo "wheel output directory contains no .whl files: ${output_directory}" >&2 + exit 1 +fi + +printf '%s\n' "${descriptors}" diff --git a/release-build-output/materialize.sh b/release-build-output/materialize.sh new file mode 100755 index 00000000..673dd6ba --- /dev/null +++ b/release-build-output/materialize.sh @@ -0,0 +1,367 @@ +#!/usr/bin/env bash +# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + +set -euo pipefail + +require_nonempty() { + local name="$1" + local value="$2" + if [[ -z "${value}" ]]; then + echo "${name} must be a non-empty string" >&2 + exit 1 + fi +} + +require_nonempty "RELEASE_UNIT" "${RELEASE_UNIT:-}" +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_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}" + +require_plain_filename() { + local label="$1" + local filename="$2" + if [[ "${filename}" == */* || "${filename}" == .* || "${filename}" == *".."* ]]; then + echo "${label} must be a plain filename" >&2 + exit 1 + 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 + +if [[ ! -d "${RELEASE_OUTPUT_DIRECTORY}" ]]; then + echo "output-directory does not exist or is not a directory: ${RELEASE_OUTPUT_DIRECTORY}" >&2 + exit 1 +fi + +ensure_relative_pattern() { + local field="$1" + local pattern="$2" + if [[ "${pattern}" == /* || "${pattern}" == */../* || "${pattern}" == ../* || "${pattern}" == *"/.." ]]; then + echo "${field} must be a relative path inside output-directory: ${pattern}" >&2 + exit 1 + fi +} + +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 + exit 1 + fi + RELEASE_PACKAGE="$(jq -c . "${package_file_path}")" +fi + +if ! 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 + exit 1 +fi + +if ! jq -e 'type == "array" and length > 0' <<<"${RELEASE_ARTIFACTS}" >/dev/null; then + echo "release-artifacts must be a non-empty JSON array" >&2 + 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")" +trap 'rm -f "${temporary_manifest}"' EXIT + +printf '%s\n' '{"schema_version":1,"producer":"release-platform","artifacts":[]}' >"${temporary_manifest}" + +resolve_one_file() { + local field="$1" + local pattern="$2" + local -a matches=() + + ensure_relative_pattern "${field}" "${pattern}" + while IFS= read -r match; do + matches+=("${match}") + done < <(compgen -G "${output_directory}/${pattern}" || true) + if [[ "${#matches[@]}" -ne 1 || ! -f "${matches[0]:-}" ]]; then + echo "${field} pattern must resolve to exactly one file: ${pattern}" >&2 + exit 1 + fi + + local resolved + resolved="$(realpath "${matches[0]}")" + if [[ "${resolved}" != "${output_directory}"/* ]]; then + echo "${field} must resolve inside output-directory: ${pattern}" >&2 + exit 1 + fi + 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" + local artifact_digest + artifact_digest="$(sha256sum "${output_directory}/${primary_path}" | awk '{print $1}')" + local artifact_label="${primary_path//\//_}" + printf 'release-evidence/%s.%s.%s.json\n' "${artifact_label}" "${artifact_digest}" "${kind}" +} + +copy_supplied_evidence() { + local primary_path="$1" + local supplied_path="$2" + local kind="$3" + local artifact_digest + artifact_digest="$(sha256sum "${output_directory}/${primary_path}" | awk '{print $1}')" + local artifact_label="${primary_path//\//_}" + local destination + destination="release-evidence/${artifact_label}.${artifact_digest}/${kind}-$(basename "${supplied_path}")" + mkdir -p "$(dirname "${output_directory}/${destination}")" + cp "${output_directory}/${supplied_path}" "${output_directory}/${destination}" + printf '%s\n' "${destination}" +} + +write_generated_sbom() { + local primary_path="$1" + local package="$2" + local destination="$3" + local artifact_digest + artifact_digest="$(sha256sum "${output_directory}/${primary_path}" | awk '{print $1}')" + mkdir -p "$(dirname "${output_directory}/${destination}")" + jq -n -S \ + --arg artifact_digest "${artifact_digest}" \ + --arg artifact_path "${primary_path}" \ + --argjson package "${package}" \ + '{ + spdxVersion: "SPDX-2.3", + dataLicense: "CC0-1.0", + SPDXID: "SPDXRef-DOCUMENT", + name: ("RAPIDS release artifact " + $artifact_path), + documentNamespace: ("https://rapids.ai/release-platform/spdx/" + $artifact_digest), + creationInfo: { + creators: ["Tool: rapidsai/shared-workflows release-build-output"], + created: (now | strftime("%Y-%m-%dT%H:%M:%SZ")) + }, + documentDescribes: ["SPDXRef-Artifact"], + packages: [{ + SPDXID: "SPDXRef-Artifact", + name: $package.name, + versionInfo: $package.version, + downloadLocation: "NOASSERTION", + filesAnalyzed: false, + checksums: [{algorithm: "SHA256", checksumValue: $artifact_digest}] + }], + relationships: [{ + spdxElementId: "SPDXRef-DOCUMENT", + relationshipType: "DESCRIBES", + relatedSpdxElement: "SPDXRef-Artifact" + }], + comment: "Artifact-identity SBOM envelope. A producer-supplied dependency SBOM may replace this record." + }' >"${output_directory}/${destination}" +} + +write_generated_provenance() { + local primary_path="$1" + local package="$2" + local destination="$3" + local artifact_digest + artifact_digest="$(sha256sum "${output_directory}/${primary_path}" | awk '{print $1}')" + mkdir -p "$(dirname "${output_directory}/${destination}")" + jq -n -S \ + --arg artifact_digest "${artifact_digest}" \ + --arg artifact_path "${primary_path}" \ + --arg repository "${GITHUB_REPOSITORY:-}" \ + --arg run_attempt "${GITHUB_RUN_ATTEMPT:-}" \ + --arg run_id "${GITHUB_RUN_ID:-}" \ + --arg source_sha "${source_sha}" \ + --arg workflow_ref "${GITHUB_WORKFLOW_REF:-}" \ + --argjson package "${package}" \ + '{ + _type: "https://in-toto.io/Statement/v1", + subject: [{name: $artifact_path, digest: {sha256: $artifact_digest}}], + 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}, + resolvedDependencies: [{ + uri: ("git+https://github.com/" + $repository + "@" + $source_sha), + digest: {gitCommit: $source_sha} + }] + }, + runDetails: { + builder: {id: ("https://github.com/" + $workflow_ref)}, + metadata: {invocationId: ("https://github.com/" + $repository + "/actions/runs/" + $run_id + "/attempts/" + $run_attempt)} + } + } + }' >"${output_directory}/${destination}" +} + +shopt -s globstar nullglob +artifact_metadata='[]' +while IFS= read -r descriptor; do + if ! jq -e ' + type == "object" + and (keys - ["path", "sbom", "provenance", "signature", "package"] | length == 0) + 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)) + ' <<<"${descriptor}" >/dev/null; then + echo "every release-artifacts entry must contain path and optional SBOM/provenance/signature/package overrides" >&2 + exit 1 + fi + + primary_path="$(resolve_one_file path "$(jq -r '.path' <<<"${descriptor}")")" + 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}")" + fi + + if [[ -n "${sbom_pattern}" ]]; then + supplied_sbom_path="$(resolve_one_file sbom "${sbom_pattern}")" + sbom_path="$(copy_supplied_evidence "${primary_path}" "${supplied_sbom_path}" "sbom")" + sbom_kind="producer-dependency" + else + sbom_path="$(generated_evidence_path "${primary_path}" "spdx")" + write_generated_sbom "${primary_path}" "${package}" "${sbom_path}" + sbom_kind="generated-identity" + fi + if [[ -n "${provenance_pattern}" ]]; then + supplied_provenance_path="$(resolve_one_file provenance "${provenance_pattern}")" + provenance_path="$(copy_supplied_evidence "${primary_path}" "${supplied_provenance_path}" "provenance")" + else + 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}" \ + --arg path "${primary_path}" \ + --arg sbom "${sbom_path}" \ + --arg provenance "${provenance_path}" \ + --argjson package "${package}" \ + '{unit_id: $unit_id, path: $path, sbom: $sbom, 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}")" + 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" + 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 + 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}" \ + '{ + schema_version: 1, + producer: "shared-workflows", + release_unit: $unit_id, + source_artifact: $artifact_name, + build_output_manifest: $manifest_name, + build_environment: { + 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}" diff --git a/release-build-output/prepare.sh b/release-build-output/prepare.sh new file mode 100755 index 00000000..f3aa97f4 --- /dev/null +++ b/release-build-output/prepare.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + +set -euo pipefail + +require_nonempty() { + local name="$1" + local value="$2" + if [[ -z "${value}" ]]; then + echo "${name} must be a non-empty string" >&2 + exit 1 + fi +} + +require_nonempty "RELEASE_ARTIFACT_TYPE" "${RELEASE_ARTIFACT_TYPE:-}" +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}" + ;; + *) + echo "artifact-type must be one of: conda, custom, wheel" >&2 + exit 1 + ;; +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/tests/release_build_output_descriptors_test.sh b/tests/release_build_output_descriptors_test.sh new file mode 100755 index 00000000..c6b6c46c --- /dev/null +++ b/tests/release_build_output_descriptors_test.sh @@ -0,0 +1,83 @@ +#!/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 + +wheel_output_directory="${temporary_directory}/wheels" +wheel_staging_directory="${temporary_directory}/wheel-staging" +mkdir -p "${wheel_output_directory}" "${wheel_staging_directory}/libkvikio_cu12-26.8.0.dist-info" +printf '%s\n' \ + 'Metadata-Version: 2.1' \ + 'Name: libkvikio-cu12' \ + 'Version: 26.8.0a32' \ + >"${wheel_staging_directory}/libkvikio_cu12-26.8.0.dist-info/METADATA" +( + cd "${wheel_staging_directory}" + 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}")" +jq -e ' + . == [{ + path: "libkvikio_cu12-26.8.0a32-py3-none-manylinux_2_28_x86_64.whl", + package: {ecosystem: "wheel", name: "libkvikio-cu12", version: "26.8.0a32"} + }] +' <<<"${wheel_descriptors}" >/dev/null + +conda_output_directory="${temporary_directory}/conda" +tar_bz2_staging_directory="${temporary_directory}/tar-bz2-staging" +conda_staging_directory="${temporary_directory}/conda-staging" +mkdir -p \ + "${conda_output_directory}/linux-64" \ + "${conda_output_directory}/noarch" \ + "${tar_bz2_staging_directory}/info" \ + "${conda_staging_directory}/info" + +jq -n \ + '{name: "rapids-dask-dependency", version: "26.08.0", build: "py_0", subdir: "noarch"}' \ + >"${tar_bz2_staging_directory}/info/index.json" +tar -cjf \ + "${conda_output_directory}/noarch/rapids-dask-dependency-26.08.0-py_0.tar.bz2" \ + -C "${tar_bz2_staging_directory}" \ + info/index.json + +jq -n \ + '{name: "librmm", version: "26.08.00a32", build: "cuda12_260714_2f567060", subdir: "linux-64"}' \ + >"${conda_staging_directory}/info/index.json" +tar -cf "${temporary_directory}/info-librmm.tar" -C "${conda_staging_directory}" info/index.json +zstd -q -f "${temporary_directory}/info-librmm.tar" -o "${temporary_directory}/info-librmm.tar.zst" +( + cd "${temporary_directory}" + zip -q \ + "${conda_output_directory}/linux-64/librmm-26.08.00a32-cuda12_260714_2f567060.conda" \ + info-librmm.tar.zst +) + +conda_descriptors="$("${repository_root}/release-build-output/describe-conda.sh" "${conda_output_directory}")" +jq -e ' + length == 2 + and any(.[]; + .path == "linux-64/librmm-26.08.00a32-cuda12_260714_2f567060.conda" + and .package == { + ecosystem: "conda", + name: "librmm", + version: "26.08.00a32", + build: "cuda12_260714_2f567060", + platform: "linux-64" + } + ) + and any(.[]; + .path == "noarch/rapids-dask-dependency-26.08.0-py_0.tar.bz2" + and .package == { + ecosystem: "conda", + name: "rapids-dask-dependency", + version: "26.08.0", + build: "py_0", + platform: "noarch" + } + ) +' <<<"${conda_descriptors}" >/dev/null diff --git a/tests/release_build_output_prepare_test.sh b/tests/release_build_output_prepare_test.sh new file mode 100755 index 00000000..3598e1e5 --- /dev/null +++ b/tests/release_build_output_prepare_test.sh @@ -0,0 +1,59 @@ +#!/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 + +mkdir -p "${temporary_directory}/wheel/libkvikio_cu12-26.8.0.dist-info" +printf '%s\n' \ + 'Metadata-Version: 2.1' \ + 'Name: libkvikio-cu12' \ + 'Version: 26.8.0' \ + >"${temporary_directory}/wheel/libkvikio_cu12-26.8.0.dist-info/METADATA" +( + cd "${temporary_directory}/wheel" + zip -qr "${temporary_directory}/libkvikio_cu12-26.8.0-py3-none-manylinux_2_28_x86_64.whl" . +) + +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 + +"${repository_root}/release-build-output/prepare.sh" + +grep -Fx 'package={"ecosystem":"wheel","name":"bundle"}' "${GITHUB_OUTPUT}" +artifacts="$(sed -n 's/^artifacts=//p' "${GITHUB_OUTPUT}")" +jq -e ' + . == [{ + path: "libkvikio_cu12-26.8.0-py3-none-manylinux_2_28_x86_64.whl", + package: {ecosystem: "wheel", name: "libkvikio-cu12", version: "26.8.0"} + }] +' <<<"${artifacts}" >/dev/null + +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 + +"${repository_root}/release-build-output/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 + echo "prepare.sh unexpectedly accepted an invalid artifact type" >&2 + exit 1 +fi +grep -Fx 'artifact-type must be one of: conda, custom, wheel' "${temporary_directory}/invalid-error" diff --git a/tests/release_build_output_test.sh b/tests/release_build_output_test.sh new file mode 100755 index 00000000..2a5dc6e1 --- /dev/null +++ b/tests/release_build_output_test.sh @@ -0,0 +1,165 @@ +#!/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"