Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
*.swp
.venv
__pycache__/
*.py[cod]
22 changes: 22 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,19 @@ repos:
args: ["--fix", "--config=pyproject.toml"]
- id: ruff-format
args: ["--config=pyproject.toml"]
- repo: https://github.com/PyCQA/bandit
rev: 1.8.6
hooks:
- id: bandit
files: ^(release-candidate-context|release-candidate-dependencies|release-catalog)/.*\.py$
args: ["--quiet"]
- repo: https://github.com/PyCQA/flake8
rev: 7.3.0
hooks:
- id: flake8
files: ^(release-candidate-context|release-candidate-dependencies|release-catalog)/.*\.py$
additional_dependencies: [flake8-bugbear==25.10.21]
args: ["--select=B"]
- repo: https://github.com/rapidsai/pre-commit-hooks
rev: v1.7.0
hooks:
Expand Down Expand Up @@ -81,3 +94,12 @@ repos:
additional_dependencies: [jsonschema==4.25.1]
pass_filenames: false
files: ^(release-catalog/(config|entries)\.schema\.json|release-catalog/examples/cuvs-java/release-catalog-entries\.json|tests/release-catalog-config/.*\.json|ci/test_release_catalog_schemas\.py)$
- repo: local
hooks:
- id: candidate-context-tests
name: test candidate run bindings
entry: python -m pytest -q tests/test_candidate_context.py
language: python
additional_dependencies: [pytest]
pass_filenames: false
files: ^(release-candidate-context/|tests/test_candidate_context.py)
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
# Copyright (c) 2024-2026, NVIDIA CORPORATION.
# Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.

[tool.ruff]
line-length = 120
target-version = "py310"

[tool.ruff.lint.per-file-ignores]
"tests/test_candidate_context.py" = ["S101"]

[tool.ruff.lint]
select = [
# flake8-bugbear
Expand Down
42 changes: 42 additions & 0 deletions release-candidate-context/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Resolve release-candidate run context
description: Resolve an immutable controller binding without adding project workflow inputs.
inputs:
source-sha:
description: Full source SHA checked out for this build.
required: true
script:
description: Custom job script requiring a centrally reviewed policy; empty for standard builds.
default: ''
outputs:
train-sha256:
description: Exact release train bound to this workflow run.
value: ${{ steps.resolve.outputs.train-sha256 }}
custom-mode:
description: Build, test, or intermediate artifact policy.
value: ${{ steps.resolve.outputs.custom-mode }}
runs:
using: composite
steps:
- name: Locate context resolver in the job container
id: 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
test -f "${action_path}/resolver.py"
echo "path=${action_path}" >> "${GITHUB_OUTPUT}"
- name: Resolve exact run binding
id: resolve
shell: bash
env:
IMPLEMENTATION_PATH: ${{ steps.implementation.outputs.path }}
SOURCE_SHA: ${{ inputs.source-sha }}
CUSTOM_SCRIPT: ${{ inputs.script }}
run: python "${IMPLEMENTATION_PATH}/resolver.py" --source-sha "${SOURCE_SHA}" --script "${CUSTOM_SCRIPT}"
83 changes: 83 additions & 0 deletions release-candidate-context/resolver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
"""Resolve candidate metadata for unchanged caller workflows from an exact run binding."""

from __future__ import annotations

import argparse
import json
import os
import re
import subprocess # nosec B404 - invokes AWS CLI with an argument list, never a shell.
import time
from collections.abc import Mapping
from pathlib import Path


def validate_binding(binding: Mapping, environment: Mapping[str, str], source_sha: str, script: str) -> dict[str, str]:
"""Reject unrelated runs/sources and custom jobs not enrolled by the controller."""
expected = {
"schema_version": 1,
"repository": environment["GITHUB_REPOSITORY"],
"run_id": environment["GITHUB_RUN_ID"],
"source_sha": source_sha,
}
if any(binding.get(key) != value for key, value in expected.items()):
raise ValueError("candidate binding does not match this repository, run, or source SHA")
if not re.fullmatch(r"[0-9a-f]{40}|[0-9a-f]{64}", source_sha):
raise ValueError("candidate source SHA must be a full object ID")
if binding.get("workflow") != "build.yaml":
raise ValueError("candidate binding has an unsupported workflow")
workflow_ref = environment["GITHUB_WORKFLOW_REF"]
expected_prefix = f"{binding['repository']}/.github/workflows/build.yaml@refs/heads/"
if workflow_ref != expected_prefix + binding.get("workflow_ref", ""):
raise ValueError("candidate binding does not match the caller workflow ref")
if environment["GITHUB_EVENT_NAME"] != "workflow_dispatch":
raise ValueError("candidate binding requires a workflow dispatch")
digest = binding.get("train_sha256", "")
if not isinstance(digest, str) or not re.fullmatch(r"[0-9a-f]{64}", digest):
raise ValueError("candidate binding has no valid train digest")
mode = "build"
if script:
mode = binding.get("custom_jobs", {}).get(script)
if mode not in {"test", "artifact"}:
raise ValueError(f"custom candidate job has no reviewed policy: {script}")
return {"train-sha256": digest, "custom-mode": mode}


def read_binding(bucket: str, key: str, run=subprocess.run, sleep=time.sleep, attempts: int = 12) -> Mapping:
"""Wait at most 55 seconds for the controller's post-dispatch binding write."""
for attempt in range(attempts):
response = run(("aws", "s3", "cp", f"s3://{bucket}/{key}", "-"), text=True, capture_output=True, check=False)
if response.returncode == 0:
binding = json.loads(response.stdout)
if not isinstance(binding, dict):
raise ValueError("candidate binding must be a JSON object")
return binding
if "404" not in response.stderr and "NoSuchKey" not in response.stderr:
raise ValueError("unable to read candidate binding; check candidate-store access")
if attempt + 1 < attempts:
sleep(5)
raise ValueError("candidate run has no controller binding; refusing to guess a train")


def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--bucket", default="rapids-release-candidates")
parser.add_argument("--prefix", default="train-state")
parser.add_argument("--source-sha", required=True)
parser.add_argument("--script", default="")
args = parser.parse_args()
repository = os.environ["GITHUB_REPOSITORY"]
run_id = os.environ["GITHUB_RUN_ID"]
if not re.fullmatch(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+", repository) or not run_id.isdigit():
raise ValueError("invalid repository or run identity")
key = f"{args.prefix}/run-bindings/{repository}/{run_id}.json"
binding = read_binding(args.bucket, key)
outputs = validate_binding(binding, os.environ, args.source_sha, args.script)
with Path(os.environ["GITHUB_OUTPUT"]).open("a") as output:
for name, value in outputs.items():
output.write(f"{name}={value}\n")


if __name__ == "__main__":
main()
70 changes: 70 additions & 0 deletions release-candidate-dependencies/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: Prepare release-candidate dependencies
description: Materialize only this release train's private S3 outputs for a candidate build.

inputs:
candidate-train-sha256:
description: Canonical SHA-256 of the immutable release train.
required: true
candidate-bucket:
description: Private S3 bucket holding candidate build bundles.
required: false
default: rapids-release-candidates
candidate-train-prefix:
description: Root prefix for immutable candidate train membership records.
required: false
default: train-state
prepare-conda-channel:
description: Whether this job needs a local Conda channel for build dependencies.
required: false
default: "false"
artifact-family:
description: Release-unit family that this job is building.
required: true
arch:
description: Canonical RAPIDS architecture for this job matrix.
required: true
cuda-version:
description: CUDA version for this job matrix.
required: true
python-version:
description: Python version for this job matrix.
required: true

runs:
using: composite
steps:
- id: implementation
name: Resolve candidate dependency 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
if [[ ! -f "${action_path}/setup.sh" ]]; then
echo "release-candidate dependency implementation not found at ${action_path}" >&2
exit 1
fi
echo "path=${action_path}" >>"${GITHUB_OUTPUT}"
- name: Prepare private candidate dependency views
shell: bash
env:
RELEASE_CANDIDATE_BUCKET: ${{ inputs.candidate-bucket }}
RELEASE_CANDIDATE_TRAIN_PREFIX: ${{ inputs.candidate-train-prefix }}
RELEASE_CANDIDATE_TRAIN_SHA256: ${{ inputs.candidate-train-sha256 }}
RELEASE_CANDIDATE_PREPARE_CONDA_CHANNEL: ${{ inputs.prepare-conda-channel }}
RELEASE_CANDIDATE_ARTIFACT_FAMILY: ${{ inputs.artifact-family }}
RELEASE_CANDIDATE_ARCH: ${{ inputs.arch }}
RELEASE_CANDIDATE_CUDA_VERSION: ${{ inputs.cuda-version }}
RELEASE_CANDIDATE_PYTHON_VERSION: ${{ inputs.python-version }}
RELEASE_CANDIDATE_SHARED_ACTIONS_REPOSITORY: ${{ github.action_repository }}
RELEASE_CANDIDATE_SHARED_ACTIONS_REVISION: ${{ github.action_ref }}
RELEASE_CANDIDATE_SHARED_WORKFLOWS_REPOSITORY: ${{ job.workflow_repository }}
RELEASE_CANDIDATE_SHARED_WORKFLOWS_REVISION: ${{ job.workflow_sha }}
IMPLEMENTATION_PATH: ${{ steps.implementation.outputs.path }}
run: ${IMPLEMENTATION_PATH}/setup.sh
17 changes: 17 additions & 0 deletions release-candidate-dependencies/cmake.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/usr/bin/env bash
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.

set -euo pipefail

original_cmake="${RAPIDS_CANDIDATE_ORIGINAL_CMAKE:?candidate CMake path is required}"
rapids_cmake_sha="${RAPIDS_CANDIDATE_RAPIDS_CMAKE_SHA:?candidate RAPIDS CMake SHA is required}"

# CMake's build/install/script modes do not accept configure definitions. Only
# add the immutable RAPIDS CMake override when the invocation configures a tree.
case "${1:-}" in
--build|--install|--open|-E|-P|--help|--version)
exec "${original_cmake}" "$@"
;;
esac

exec "${original_cmake}" "-Drapids-cmake-sha=${rapids_cmake_sha}" "$@"
Loading
Loading