From 893dd75fbde33689a78b83eef7a46d34f21364d2 Mon Sep 17 00:00:00 2001 From: Allison Piper Date: Tue, 23 Jul 2024 17:11:02 +0000 Subject: [PATCH] Use shared GHA infra from external repo. --- .github/actions/workflow-build/action.yml | 139 -- .../actions/workflow-build/build-workflow.py | 1139 ----------------- .../prepare-workflow-dispatch.py | 95 -- .github/actions/workflow-results/action.yml | 212 --- .../actions/workflow-results/final-summary.py | 67 - .../workflow-results/parse-job-times.py | 129 -- .../prepare-execution-summary.py | 365 ------ .../workflow-results/verify-job-success.py | 30 - .../actions/workflow-run-job-linux/action.yml | 197 --- .../workflow-run-job-windows/action.yml | 98 -- .github/workflows/ci-workflow-nightly.yml | 12 +- .../workflows/ci-workflow-pull-request.yml | 14 +- .github/workflows/verify-devcontainers.yml | 141 -- ...rkflow-dispatch-standalone-group-linux.yml | 43 - ...flow-dispatch-standalone-group-windows.yml | 36 - ...orkflow-dispatch-two-stage-group-linux.yml | 28 - ...kflow-dispatch-two-stage-group-windows.yml | 28 - .../workflow-dispatch-two-stage-linux.yml | 75 -- .../workflow-dispatch-two-stage-windows.yml | 61 - 19 files changed, 13 insertions(+), 2896 deletions(-) delete mode 100644 .github/actions/workflow-build/action.yml delete mode 100755 .github/actions/workflow-build/build-workflow.py delete mode 100755 .github/actions/workflow-build/prepare-workflow-dispatch.py delete mode 100644 .github/actions/workflow-results/action.yml delete mode 100755 .github/actions/workflow-results/final-summary.py delete mode 100755 .github/actions/workflow-results/parse-job-times.py delete mode 100755 .github/actions/workflow-results/prepare-execution-summary.py delete mode 100755 .github/actions/workflow-results/verify-job-success.py delete mode 100644 .github/actions/workflow-run-job-linux/action.yml delete mode 100644 .github/actions/workflow-run-job-windows/action.yml delete mode 100644 .github/workflows/verify-devcontainers.yml delete mode 100644 .github/workflows/workflow-dispatch-standalone-group-linux.yml delete mode 100644 .github/workflows/workflow-dispatch-standalone-group-windows.yml delete mode 100644 .github/workflows/workflow-dispatch-two-stage-group-linux.yml delete mode 100644 .github/workflows/workflow-dispatch-two-stage-group-windows.yml delete mode 100644 .github/workflows/workflow-dispatch-two-stage-linux.yml delete mode 100644 .github/workflows/workflow-dispatch-two-stage-windows.yml diff --git a/.github/actions/workflow-build/action.yml b/.github/actions/workflow-build/action.yml deleted file mode 100644 index 3842886589a..00000000000 --- a/.github/actions/workflow-build/action.yml +++ /dev/null @@ -1,139 +0,0 @@ -name: "CCCL Build Workflow" -description: "Parses a matrix definition and exports a set of dispatchable build/test/etc jobs." - -inputs: - workflows: - description: "Space separated list of workflows in matrix file to run" - required: true - allow_override: - description: "If true, the requested `workflows` will be ignored when a non-empty 'override' workflow exists in the matrix file." - default: "false" - required: false - inspect_changes_script: - description: "If defined, run this script to determine which projects/deps need to be tested." - default: "" - required: false - inspect_changes_base_sha: - description: "If defined, use this base ref for inspect-changes script." - default: "" - required: false - matrix_file: - description: "Path to the matrix file in the consumer repository." - default: "ci/matrix.yaml" - required: false - matrix_parser: - description: "Path to the matrix parser script (default if blank: build-workflow.py from action dir)" - default: "" - required: false - slack_token: - description: "The Slack token to use for notifications. No notifications will be sent if not provided." - required: false - slack_log: - description: "Slack channel ID for verbose notifications." - required: false - slack_alert: - description: "Slack channel ID for alert notifications." - required: false - -outputs: - workflow: - description: "The dispatchable workflows" - value: ${{ steps.outputs.outputs.WORKFLOW }} - -runs: - using: "composite" - steps: - - - name: Send Slack log notification - if: ${{inputs.slack_token != '' && inputs.slack_log != '' }} - uses: slackapi/slack-github-action@v1.26.0 - env: - SLACK_BOT_TOKEN: ${{ inputs.slack_token }} - WORKFLOW_TYPE: ${{ github.workflow }} # nightly, weekly, pr, etc. - SUMMARY_URL: https://github.com/${{github.repository}}/actions/runs/${{github.run_id}} - with: - channel-id: ${{ inputs.slack_log }} - slack-message: | - Workflow '${{ env.WORKFLOW_TYPE }}' starting... - - Details: ${{ env.SUMMARY_URL }} - - - name: Inspect changes - if: ${{ inputs.inspect_changes_script != '' && inputs.inspect_changes_base_sha != '' }} - id: inspect-changes - shell: bash --noprofile --norc -euo pipefail {0} - env: - base_ref: ${{ inputs.inspect_changes_base_sha }} - run: | - echo "Running inspect-changes script..." - ${{ inputs.inspect_changes_script }} ${base_ref} ${GITHUB_SHA} - echo "Exporting summary..." - mkdir workflow - cp ${GITHUB_STEP_SUMMARY} workflow/changes.md - - - name: Parse matrix file into a workflow - id: build-workflow - shell: bash --noprofile --norc -euo pipefail {0} - env: - allow_override: ${{ inputs.allow_override == 'true' && '--allow-override' || ''}} - dirty_projects_flag: ${{ inputs.inspect_changes_script != '' && '--dirty-projects' || ''}} - dirty_projects: ${{ inputs.inspect_changes_script != '' && steps.inspect-changes.outputs.dirty_projects || ''}} - matrix_parser: ${{ inputs.matrix_parser && inputs.matrix_parser || '${GITHUB_ACTION_PATH}/build-workflow.py' }} - run: | - echo "Parsing matrix file into a workflow..." - - ${{ env.matrix_parser }} ${{ inputs.matrix_file }} \ - --workflows ${{ inputs.workflows }} \ - ${{ env.allow_override }} \ - ${{ env.dirty_projects_flag }} ${{ env.dirty_projects }} - - echo "::group::Workflow" - cat workflow/workflow.json - echo "::endgroup::" - - echo "::group::Runners" - cat workflow/runner_summary.json | jq -r '"# \(.heading)\n\n\(.body)"' | tee -a "${GITHUB_STEP_SUMMARY}" - echo "::endgroup::" - - echo "::group::Job List" - cat workflow/job_list.txt - echo "::endgroup::" - - - name: Create dispatch workflows - shell: bash --noprofile --norc -euo pipefail {0} - run: | - "${GITHUB_ACTION_PATH}/prepare-workflow-dispatch.py" workflow/workflow.json - - echo "::group::Dispatch Workflows" - cat workflow/dispatch.json - echo "::endgroup::" - - - name: Set outputs - id: outputs - shell: bash --noprofile --norc -euo pipefail {0} - run: | - echo "::group::GHA Output: WORKFLOW" - printf "WORKFLOW=%s\n" "$(cat workflow/dispatch.json | jq -c '.')" | tee -a "${GITHUB_OUTPUT}" - echo "::endgroup::" - - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: workflow - path: workflow/ - compression-level: 0 - - - name: Send Slack error notification - if: ${{ failure() && inputs.slack_token != '' && (inputs.slack_alert != '' || inputs.slack_log != '') }} - uses: slackapi/slack-github-action@v1.26.0 - env: - SLACK_BOT_TOKEN: ${{ inputs.slack_token }} - WORKFLOW_TYPE: ${{ github.workflow }} # nightly, weekly, pr, etc. - SUMMARY_URL: https://github.com/${{github.repository}}/actions/runs/${{github.run_id}} - CHANNEL_SEP: ${{ (inputs.slack_log != '' && inputs.slack_alert != '') && ',' || ''}} - with: - channel-id: '${{ inputs.slack_log }}${{env.CHANNEL_SEP}}${{ inputs.slack_alert }}' - slack-message: | - Workflow '${{ env.WORKFLOW_TYPE }}' encountered an error while preparing to run. - - Details: ${{ env.SUMMARY_URL }} diff --git a/.github/actions/workflow-build/build-workflow.py b/.github/actions/workflow-build/build-workflow.py deleted file mode 100755 index a3b216e3fd8..00000000000 --- a/.github/actions/workflow-build/build-workflow.py +++ /dev/null @@ -1,1139 +0,0 @@ -#!/usr/bin/env python3 - -""" -Concepts: -- matrix_job: an entry of a workflow matrix, converted from matrix.yaml["workflow"][id] into a JSON object. - Example: - { - "jobs": [ - "test" - ], - "project": [ - "libcudacxx", - "cub", - "thrust" - ], - "ctk": "11.1", - "cudacxx": 'nvcc', - "cxx": 'gcc10', - "sm": "75-real", - "std": 17 - "cpu": "amd64", - "gpu": "t4", - } - -Matrix jobs are read from the matrix.yaml file and converted into a JSON object and passed to matrix_job_to_dispatch_group, where -the matrix job is turned into one or more dispatch groups consisting of potentially many jobs. - -- dispatch_group_json: A json object used in conjunction with the ci-dispatch-groups.yml GHA workflow. - Example: - { - "": { - "standalone": [ {}, ... ] - "two_stage": [ {}, ] - } - } - -- two_stage_json: A json object that represents bulk-synchronous producer/consumer jobs, used with ci-dispatch-two-stage.yml. - Example: - { - "id": "", # Used as a compact unique name for the GHA dispatch workflows. - "producers": [ {}, ... ], - "consumers": [ {}, ... ] - } - -- job_json: A json object that represents a single job in a workflow. Used with ci-dispatch-job.yml. - Example: - { - "id": "", # Used as a compact unique name for the GHA dispatch workflows. - "name": "...", - "runner": "...", - "image": "...", - "command": "..." }, - } -""" - -import argparse -import base64 -import copy -import functools -import json -import os -import re -import struct -import sys -import yaml - - -matrix_yaml = None - - -# Decorators to cache static results of functions: -# static_result: function has no args, same result each invocation. -# memoize_result: result depends on args. -def static_result(func): return functools.lru_cache(maxsize=1)(func) -def memoize_result(func): return functools.lru_cache(maxsize=None)(func) - - -def generate_guids(): - """ - Simple compact global unique ID generator. - Produces up to 65535 unique IDs between 1-3 characters in length. - Throws an exception once exhausted. - """ - i = 0 - while True: - # Generates a base64 hash of an incrementing 16-bit integer: - hash = base64.b64encode(struct.pack(">H", i)).decode('ascii') - # Strips off up-to 2 leading 'A' characters and a single trailing '=' characters, if they exist: - guid = re.sub(r'^A{0,2}', '', hash).removesuffix("=") - yield guid - i += 1 - if i >= 65535: - raise Exception("GUID generator exhausted.") - - -guid_generator = generate_guids() - - -def write_json_file(filename, json_object): - with open(filename, 'w') as f: - json.dump(json_object, f, indent=2) - - -def write_text_file(filename, text): - with open(filename, 'w') as f: - print(text, file=f) - - -def error_message_with_matrix_job(matrix_job, message): - return f"{matrix_job['origin']['workflow_location']}: {message}\n Input: {matrix_job['origin']['original_matrix_job']}" - - -@memoize_result -def canonicalize_ctk_version(ctk_string): - if ctk_string in matrix_yaml['ctk_versions']: - return ctk_string - - # Check for aka's: - for ctk_key, ctk_value in matrix_yaml['ctk_versions'].items(): - if 'aka' in ctk_value and ctk_string == ctk_value['aka']: - return ctk_key - - raise Exception(f"Unknown CTK version '{ctk_string}'") - - -def get_ctk(ctk_string): - result = matrix_yaml['ctk_versions'][ctk_string] - result["version"] = ctk_string - return result - - -@memoize_result -def parse_cxx_string(cxx_string): - "Returns (id, version) tuple. Version may be None if not present." - return re.match(r'^([a-z]+)-?([\d\.]+)?$', cxx_string).groups() - - -@memoize_result -def canonicalize_host_compiler_name(cxx_string): - """ - Canonicalize the host compiler cxx_string. - - Valid input formats: 'gcc', 'gcc10', or 'gcc-12'. - Output format: 'gcc12'. - - If no version is specified, the latest version is used. - """ - id, version = parse_cxx_string(cxx_string) - - if not id in matrix_yaml['host_compilers']: - raise Exception( - f"Unknown host compiler '{id}'. Valid options are: {', '.join(matrix_yaml['host_compilers'].keys())}") - - hc_def = matrix_yaml['host_compilers'][id] - hc_versions = hc_def['versions'] - - if not version: - version = max(hc_def['versions'].keys(), key=lambda x: tuple(map(int, x.split('.')))) - - # Check for aka's: - if not version in hc_def['versions']: - for version_key, version_data in hc_def['versions'].items(): - if 'aka' in version_data and version == version_data['aka']: - version = version_key - - if not version in hc_def['versions']: - raise Exception( - f"Unknown version '{version}' for host compiler '{id}'.") - - cxx_string = f"{id}{version}" - - return cxx_string - - -@memoize_result -def get_host_compiler(cxx_string): - "Expects a canonicalized cxx_string." - id, version = parse_cxx_string(cxx_string) - - if not id in matrix_yaml['host_compilers']: - raise Exception( - f"Unknown host compiler '{id}'. Valid options are: {', '.join(matrix_yaml['host_compilers'].keys())}") - - hc_def = matrix_yaml['host_compilers'][id] - - if not version in hc_def['versions']: - raise Exception( - f"Unknown version '{version}' for host compiler '{id}'. Valid options are: {', '.join(hc_def['versions'].keys())}") - - version_def = hc_def['versions'][version] - - result = {'id': id, - 'name': hc_def['name'], - 'version': version, - 'container_tag': hc_def['container_tag'], - 'exe': hc_def['exe']} - - for key, value in version_def.items(): - result[key] = value - - return result - - -def get_device_compiler(matrix_job): - id = matrix_job['cudacxx'] - if not id in matrix_yaml['device_compilers'].keys(): - raise Exception( - f"Unknown device compiler '{id}'. Valid options are: {', '.join(matrix_yaml['device_compilers'].keys())}") - result = matrix_yaml['device_compilers'][id] - result['id'] = id - - if id == 'nvcc': - ctk = get_ctk(matrix_job['ctk']) - result['version'] = ctk['version'] - result['stds'] = ctk['stds'] - elif id == 'clang': - host_compiler = get_host_compiler(matrix_job['cxx']) - result['version'] = host_compiler['version'] - result['stds'] = host_compiler['stds'] - else: - raise Exception(f"Cannot determine version/std info for device compiler '{id}'") - - return result - - -@memoize_result -def get_gpu(gpu_string): - if not gpu_string in matrix_yaml['gpus']: - raise Exception( - f"Unknown gpu '{gpu_string}'. Valid options are: {', '.join(matrix_yaml['gpus'].keys())}") - - result = matrix_yaml['gpus'][gpu_string] - result['id'] = gpu_string - - if not 'testing' in result: - result['testing'] = False - - return result - - -@memoize_result -def get_project(project): - if not project in matrix_yaml['projects'].keys(): - raise Exception( - f"Unknown project '{project}'. Valid options are: {', '.join(matrix_yaml['projects'].keys())}") - - result = matrix_yaml['projects'][project] - result['id'] = project - - if not 'name' in result: - result['name'] = project - - if not 'job_map' in result: - result['job_map'] = {} - - return result - - -@memoize_result -def get_job_type_info(job): - if not job in matrix_yaml['jobs'].keys(): - raise Exception( - f"Unknown job '{job}'. Valid options are: {', '.join(matrix_yaml['jobs'].keys())}") - - result = matrix_yaml['jobs'][job] - result['id'] = job - - if not 'name' in result: - result['name'] = job.capitalize() - if not 'gpu' in result: - result['gpu'] = False - if not 'needs' in result: - result['needs'] = None - if not 'invoke' in result: - result['invoke'] = {} - if not 'prefix' in result['invoke']: - result['invoke']['prefix'] = job - if not 'args' in result['invoke']: - result['invoke']['args'] = "" - - return result - - -@memoize_result -def get_tag_info(tag): - if not tag in matrix_yaml['tags'].keys(): - raise Exception( - f"Unknown tag '{tag}'. Valid options are: {', '.join(matrix_yaml['tags'].keys())}") - - result = matrix_yaml['tags'][tag] - result['id'] = tag - - if 'required' not in result: - result['required'] = False - - if 'default' in result: - result['required'] = False - else: - result['default'] = None - - - return result - - -@static_result -def get_all_matrix_job_tags_sorted(): - all_tags = set(matrix_yaml['tags'].keys()) - - # Sorted using a highly subjective opinion on importance: - # Always first, information dense: - sorted_important_tags = ['project', 'jobs', 'cudacxx', 'cxx', 'ctk', 'gpu', 'std', 'sm', 'cpu'] - - # Always last, derived: - sorted_noise_tags = ['origin'] - - # In between? - sorted_tags = set(sorted_important_tags + sorted_noise_tags) - sorted_meh_tags = sorted(list(all_tags - sorted_tags)) - - return sorted_important_tags + sorted_meh_tags + sorted_noise_tags - - -def lookup_supported_stds(matrix_job): - stds = set(matrix_yaml['all_stds']) - if 'ctk' in matrix_job: - ctk = get_ctk(matrix_job['ctk']) - stds = stds & set(ctk['stds']) - if 'cxx' in matrix_job: - host_compiler = get_host_compiler(matrix_job['cxx']) - stds = stds & set(host_compiler['stds']) - if 'cudacxx' in matrix_job: - device_compiler = get_device_compiler(matrix_job) - stds = stds & set(device_compiler['stds']) - if 'project' in matrix_job: - project = get_project(matrix_job['project']) - stds = stds & set(project['stds']) - return sorted(list(stds)) - - -def is_windows(matrix_job): - host_compiler = get_host_compiler(matrix_job['cxx']) - return host_compiler['container_tag'] == 'cl' - - -def generate_dispatch_group_name(matrix_job): - project = get_project(matrix_job['project']) - ctk = matrix_job['ctk'] - device_compiler = get_device_compiler(matrix_job) - host_compiler = get_host_compiler(matrix_job['cxx']) - - compiler_info = "" - if device_compiler['id'] == 'nvcc': - compiler_info = f"{device_compiler['name']} {host_compiler['name']}" - elif device_compiler['id'] == 'clang': - compiler_info = f"{device_compiler['name']}" - else: - compiler_info = f"{device_compiler['name']}-{device_compiler['version']} {host_compiler['name']}" - - return f"{project['name']} CTK{ctk} {compiler_info}" - - -def generate_dispatch_job_name(matrix_job, job_type): - job_info = get_job_type_info(job_type) - std_str = ("C++" + str(matrix_job['std']) + " ") if 'std' in matrix_job else '' - cpu_str = matrix_job['cpu'] - gpu_str = (', ' + matrix_job['gpu'].upper()) if job_info['gpu'] else "" - cuda_compile_arch = (" sm{" + str(matrix_job['sm']) + "}") if 'sm' in matrix_job else "" - cmake_options = (' ' + matrix_job['cmake_options']) if 'cmake_options' in matrix_job else "" - - host_compiler = get_host_compiler(matrix_job['cxx']) - - config_tag = f"{std_str}{host_compiler['name']}{host_compiler['version']}" - - extra_info = f":{cuda_compile_arch}{cmake_options}" if cuda_compile_arch or cmake_options else "" - - return f"[{config_tag}] {job_info['name']}({cpu_str}{gpu_str}){extra_info}" - - -def generate_dispatch_job_runner(matrix_job, job_type): - runner_os = "windows" if is_windows(matrix_job) else "linux" - cpu = matrix_job['cpu'] - - job_info = get_job_type_info(job_type) - if not job_info['gpu']: - return f"{runner_os}-{cpu}-cpu16" - - gpu = get_gpu(matrix_job['gpu']) - suffix = "-testing" if gpu['testing'] else "" - - return f"{runner_os}-{cpu}-gpu-{gpu['id']}-latest-1{suffix}" - - -def generate_dispatch_job_ctk_version(matrix_job, job_type): - ".devcontainers/launch.sh --cuda option:" - return matrix_job['ctk'] - - -def generate_dispatch_job_host_compiler(matrix_job, job_type): - ".devcontainers/launch.sh --host option:" - host_compiler = get_host_compiler(matrix_job['cxx']) - return host_compiler['container_tag'] + host_compiler['version'] - - -def generate_dispatch_job_image(matrix_job, job_type): - devcontainer_version = matrix_yaml['devcontainer_version'] - ctk = matrix_job['ctk'] - host_compiler = generate_dispatch_job_host_compiler(matrix_job, job_type) - - if is_windows(matrix_job): - return f"rapidsai/devcontainers:{devcontainer_version}-cuda{ctk}-{host_compiler}" - - return f"rapidsai/devcontainers:{devcontainer_version}-cpp-{host_compiler}-cuda{ctk}" - - -def generate_dispatch_job_command(matrix_job, job_type): - script_path = "./ci/windows" if is_windows(matrix_job) else "./ci" - script_ext = ".ps1" if is_windows(matrix_job) else ".sh" - - job_info = get_job_type_info(job_type) - job_prefix = job_info['invoke']['prefix'] - job_args = job_info['invoke']['args'] - - project = get_project(matrix_job['project']) - script_name = f"{script_path}/{job_prefix}_{project['id']}{script_ext}" - - std_str = str(matrix_job['std']) if 'std' in matrix_job else '' - - device_compiler = get_device_compiler(matrix_job) - - cuda_compile_arch = matrix_job['sm'] if 'sm' in matrix_job else '' - cmake_options = matrix_job['cmake_options'] if 'cmake_options' in matrix_job else '' - - command = f"\"{script_name}\"" - if job_args: - command += f" {job_args}" - if std_str: - command += f" -std \"{std_str}\"" - if cuda_compile_arch: - command += f" -arch \"{cuda_compile_arch}\"" - if device_compiler['id'] != 'nvcc': - command += f" -cuda \"{device_compiler['exe']}\"" - if cmake_options: - command += f" -cmake-options \"{cmake_options}\"" - - return command - - -def generate_dispatch_job_origin(matrix_job, job_type): - # Already has silename, line number, etc: - origin = matrix_job['origin'].copy() - - origin_job = matrix_job.copy() - del origin_job['origin'] - - job_info = get_job_type_info(job_type) - - # The origin tags are used to build the execution summary for the CI PR comment. - # Use the human readable job label for the execution summary: - origin_job['jobs'] = job_info['name'] - - # Replace some of the clunkier tags with a summary-friendly version: - if 'cxx' in origin_job: - host_compiler = get_host_compiler(matrix_job['cxx']) - del origin_job['cxx'] - - origin_job['cxx'] = host_compiler['name'] + host_compiler['version'] - origin_job['cxx_family'] = host_compiler['name'] - - if 'cudacxx' in origin_job: - device_compiler = get_device_compiler(matrix_job) - del origin_job['cudacxx'] - - origin_job['cudacxx'] = device_compiler['name'] + device_compiler['version'] - origin_job['cudacxx_family'] = device_compiler['name'] - - origin['matrix_job'] = origin_job - - return origin - - -def generate_dispatch_job_json(matrix_job, job_type): - return { - 'cuda': generate_dispatch_job_ctk_version(matrix_job, job_type), - 'host': generate_dispatch_job_host_compiler(matrix_job, job_type), - 'name': generate_dispatch_job_name(matrix_job, job_type), - 'runner': generate_dispatch_job_runner(matrix_job, job_type), - 'image': generate_dispatch_job_image(matrix_job, job_type), - 'command': generate_dispatch_job_command(matrix_job, job_type), - 'origin': generate_dispatch_job_origin(matrix_job, job_type) - } - - -# Create a single build producer, and a separate consumer for each test_job_type: -def generate_dispatch_two_stage_json(matrix_job, producer_job_type, consumer_job_types): - producer_json = generate_dispatch_job_json(matrix_job, producer_job_type) - - consumers_json = [] - for consumer_job_type in consumer_job_types: - consumers_json.append(generate_dispatch_job_json(matrix_job, consumer_job_type)) - - return { - "producers": [producer_json], - "consumers": consumers_json - } - - -def generate_dispatch_group_jobs(matrix_job): - dispatch_group_jobs = { - "standalone": [], - "two_stage": [] - } - - # The jobs tag is left unexploded to optimize scheduling here. - job_types = set(matrix_job['jobs']) - - # Add all dpendencies to the job_types set: - standalone = set([]) - two_stage = {} # {producer: set([consumer, ...])} - for job_type in job_types: - job_info = get_job_type_info(job_type) - dep = job_info['needs'] - if dep: - if dep in two_stage: - two_stage[dep].add(job_type) - else: - two_stage[dep] = set([job_type]) - else: - standalone.add(job_type) - - standalone.difference_update(two_stage.keys()) - - for producer, consumers in two_stage.items(): - dispatch_group_jobs['two_stage'].append( - generate_dispatch_two_stage_json(matrix_job, producer, list(consumers))) - - for job_type in standalone: - dispatch_group_jobs['standalone'].append(generate_dispatch_job_json(matrix_job, job_type)) - - return dispatch_group_jobs - - -def matrix_job_to_dispatch_group(matrix_job, group_prefix=""): - return {group_prefix + generate_dispatch_group_name(matrix_job): - generate_dispatch_group_jobs(matrix_job)} - - -def merge_dispatch_groups(accum_dispatch_groups, new_dispatch_groups): - for group_name, group_json in new_dispatch_groups.items(): - if group_name not in accum_dispatch_groups: - accum_dispatch_groups[group_name] = group_json - else: - # iterate standalone and two_stage: - for key, value in group_json.items(): - accum_dispatch_groups[group_name][key] += value - - -def compare_dispatch_jobs(job1, job2): - "Compare two dispatch job specs for equality. Considers only name/runner/image/command." - # Ignores the 'origin' key, which may vary between identical job specifications. - return (job1['name'] == job2['name'] and - job1['runner'] == job2['runner'] and - job1['image'] == job2['image'] and - job1['command'] == job2['command']) - - -def dispatch_job_in_container(job, container): - "Check if a dispatch job is in a container, using compare_dispatch_jobs." - for job2 in container: - if compare_dispatch_jobs(job, job2): - return True - return False - - -def remove_dispatch_job_from_container(job, container): - "Remove a dispatch job from a container, using compare_dispatch_jobs." - for i, job2 in enumerate(container): - if compare_dispatch_jobs(job, job2): - del container[i] - return True - return False - - -def finalize_workflow_dispatch_groups(workflow_dispatch_groups_orig): - workflow_dispatch_groups = copy.deepcopy(workflow_dispatch_groups_orig) - - # Check to see if any .two_stage.producers arrays have more than 1 job, which is not supported. - # See ci-dispatch-two-stage.yml for details. - for group_name, group_json in workflow_dispatch_groups.items(): - if 'two_stage' in group_json: - for two_stage_json in group_json['two_stage']: - num_producers = len(two_stage_json['producers']) - if num_producers > 1: - producer_names = "" - for job in two_stage_json['producers']: - producer_names += f" - {job['name']}\n" - error_message = f"ci-dispatch-two-stage.yml currently only supports a single producer. " - error_message += f"Found {num_producers} producers in '{group_name}':\n{producer_names}" - print(f"::error file=ci/matrix.yaml::{error_message}", file=sys.stderr) - raise Exception(error_message) - - # Merge consumers for any two_stage arrays that have the same producer(s). Print a warning. - for group_name, group_json in workflow_dispatch_groups.items(): - if not 'two_stage' in group_json: - continue - two_stage_json = group_json['two_stage'] - merged_producers = [] - merged_consumers = [] - for two_stage in two_stage_json: - producers = two_stage['producers'] - consumers = two_stage['consumers'] - - # Make sure this gets updated if we add support for multiple producers: - assert (len(producers) == 1) - producer = producers[0] - - if dispatch_job_in_container(producer, merged_producers): - producer_index = merged_producers.index(producers) - matching_consumers = merged_consumers[producer_index] - - producer_name = producer['name'] - print(f"::notice file=ci/matrix.yaml::Merging consumers for duplicate producer '{producer_name}' in '{group_name}'", - file=sys.stderr) - consumer_names = ", ".join([consumer['name'] for consumer in matching_consumers]) - print(f"::notice file=ci/matrix.yaml::Original consumers: {consumer_names}", file=sys.stderr) - consumer_names = ", ".join([consumer['name'] for consumer in consumers]) - print(f"::notice file=ci/matrix.yaml::Duplicate consumers: {consumer_names}", file=sys.stderr) - # Merge if unique: - for consumer in consumers: - if not dispatch_job_in_container(consumer, matching_consumers): - matching_consumers.append(consumer) - consumer_names = ", ".join([consumer['name'] for consumer in matching_consumers]) - print(f"::notice file=ci/matrix.yaml::Merged consumers: {consumer_names}", file=sys.stderr) - else: - merged_producers.append(producer) - merged_consumers.append(consumers) - # Update with the merged lists: - two_stage_json = [] - for producer, consumers in zip(merged_producers, merged_consumers): - two_stage_json.append({'producers': [producer], 'consumers': consumers}) - group_json['two_stage'] = two_stage_json - - # Check for any duplicate jobs in standalone arrays. Warn and remove duplicates. - for group_name, group_json in workflow_dispatch_groups.items(): - standalone_jobs = group_json['standalone'] if 'standalone' in group_json else [] - unique_standalone_jobs = [] - for job_json in standalone_jobs: - if dispatch_job_in_container(job_json, unique_standalone_jobs): - print(f"::notice file=ci/matrix.yaml::Removing duplicate standalone job '{job_json['name']}' in '{group_name}'", - file=sys.stderr) - else: - unique_standalone_jobs.append(job_json) - - # If any producer/consumer jobs exist in standalone arrays, warn and remove the standalones. - two_stage_jobs = group_json['two_stage'] if 'two_stage' in group_json else [] - for two_stage_job in two_stage_jobs: - for producer in two_stage_job['producers']: - if remove_dispatch_job_from_container(producer, unique_standalone_jobs): - print(f"::notice file=ci/matrix.yaml::Removing standalone job '{producer['name']}' " + - f"as it appears as a producer in '{group_name}'", - file=sys.stderr) - for consumer in two_stage_job['consumers']: - if remove_dispatch_job_from_container(producer, unique_standalone_jobs): - print(f"::notice file=ci/matrix.yaml::Removing standalone job '{consumer['name']}' " + - f"as it appears as a consumer in '{group_name}'", - file=sys.stderr) - standalone_jobs = list(unique_standalone_jobs) - group_json['standalone'] = standalone_jobs - - # If any producer or consumer job appears more than once, warn and leave as-is. - all_two_stage_jobs = [] - duplicate_jobs = {} - for two_stage_job in two_stage_jobs: - for job in two_stage_job['producers'] + two_stage_job['consumers']: - if dispatch_job_in_container(job, all_two_stage_jobs): - duplicate_jobs[job['name']] = duplicate_jobs.get(job['name'], 1) + 1 - else: - all_two_stage_jobs.append(job) - for job_name, count in duplicate_jobs.items(): - print(f"::warning file=ci/matrix.yaml::" + - f"Job '{job_name}' appears {count} times in '{group_name}'.", - f"Cannot remove duplicate while resolving dependencies. This job WILL execute {count} times.", - file=sys.stderr) - - # Remove all named values that contain an empty list of jobs: - for group_name, group_json in workflow_dispatch_groups.items(): - if not group_json['standalone'] and not group_json['two_stage']: - del workflow_dispatch_groups[group_name] - elif not group_json['standalone']: - del group_json['standalone'] - elif not group_json['two_stage']: - del group_json['two_stage'] - - # Natural sort impl (handles embedded numbers in strings, case insensitive) - def natural_sort_key(key): - return [(int(text) if text.isdigit() else text.lower()) for text in re.split('(\d+)', key)] - - # Sort the dispatch groups by name: - workflow_dispatch_groups = dict(sorted(workflow_dispatch_groups.items(), key=lambda x: natural_sort_key(x[0]))) - - # Sort the jobs within each dispatch group: - for group_name, group_json in workflow_dispatch_groups.items(): - if 'standalone' in group_json: - group_json['standalone'] = sorted(group_json['standalone'], key=lambda x: natural_sort_key(x['name'])) - if 'two_stage' in group_json: - group_json['two_stage'] = sorted( - group_json['two_stage'], key=lambda x: natural_sort_key(x['producers'][0]['name'])) - - # Assign unique IDs in appropriate locations. - # These are used to give "hidden" dispatch jobs a short, unique name, - # otherwise GHA generates a long, cluttered name. - for group_name, group_json in workflow_dispatch_groups.items(): - if 'standalone' in group_json: - for job_json in group_json['standalone']: - job_json['id'] = next(guid_generator) - if 'two_stage' in group_json: - for two_stage_json in group_json['two_stage']: - two_stage_json['id'] = next(guid_generator) - for job_json in two_stage_json['producers'] + two_stage_json['consumers']: - job_json['id'] = next(guid_generator) - - return workflow_dispatch_groups - - -def find_workflow_line_number(workflow_name): - regex = re.compile(f"^( )*{workflow_name}:", re.IGNORECASE) - line_number = 0 - with open(matrix_yaml['filename'], 'r') as f: - for line in f: - line_number += 1 - if regex.match(line): - return line_number - raise Exception( - f"Workflow '{workflow_name}' not found in {matrix_yaml['filename]']} (could not match regex: {regex})") - - -def get_matrix_job_origin(matrix_job, workflow_name, workflow_location): - filename = matrix_yaml['filename'] - original_matrix_job = json.dumps(matrix_job, indent=None, separators=(', ', ': ')) - original_matrix_job = original_matrix_job.replace('"', '') - return { - 'filename': filename, - 'workflow_name': workflow_name, - 'workflow_location': workflow_location, - 'original_matrix_job': original_matrix_job - } - - -@static_result -def get_excluded_matrix_jobs(): - return parse_workflow_matrix_jobs(None, 'exclude') - - -def apply_matrix_job_exclusion(matrix_job, exclusion): - # Excluded tags to remove from unexploded tag categories: { tag: [exluded_value1, excluded_value2] } - update_dict = {} - - for tag, excluded_values in exclusion.items(): - # Not excluded if a specified tag isn't even present: - if not tag in matrix_job: - return matrix_job - - # Some tags are left unexploded (e.g. 'jobs') to optimize scheduling, - # so the values can be either a list or a single value. - # Standardize to a list for comparison: - if type(excluded_values) != list: - excluded_values = [excluded_values] - matrix_values = matrix_job[tag] - if type(matrix_values) != list: - matrix_values = [matrix_values] - - # Identify excluded values that are present in the matrix job for this tag: - matched_tag_values = [value for value in matrix_values if value in excluded_values] - # Not excluded if no values match for a tag: - if not matched_tag_values: - return matrix_job - - # If there is only a partial match to the matrix values, record the matches in the update_dict. - # If the match is complete, do nothing. - if len(matched_tag_values) < len(matrix_values): - update_dict[tag] = matched_tag_values - - # If we get here, the matrix job matches and should be updated or excluded entirely. - # If all tag matches are complete, then update_dict will be empty and the job should be excluded entirely - if not update_dict: - return None - - # If update_dict is populated, remove the matched values from the matrix job and return it. - new_matrix_job = copy.deepcopy(matrix_job) - for tag, values in update_dict.items(): - for value in values: - new_matrix_job[tag].remove(value) - - return new_matrix_job - - -def remove_excluded_jobs(matrix_jobs): - '''Remove jobs that match all tags in any of the exclusion matrix jobs.''' - excluded = get_excluded_matrix_jobs() - filtered_matrix_jobs = [] - for matrix_job_orig in matrix_jobs: - matrix_job = copy.deepcopy(matrix_job_orig) - for exclusion in excluded: - matrix_job = apply_matrix_job_exclusion(matrix_job, exclusion) - if not matrix_job: - break - if matrix_job: - filtered_matrix_jobs.append(matrix_job) - return filtered_matrix_jobs - - -def validate_tags(matrix_job, ignore_required=False): - all_tags = matrix_yaml['tags'].keys() - - if not ignore_required: - for tag in all_tags: - tag_info = get_tag_info(tag) - if tag not in matrix_job: - if tag_info['required']: - raise Exception(error_message_with_matrix_job(matrix_job, f"Missing required tag '{tag}'")) - if 'cudacxx' in matrix_job: - if matrix_job['cudacxx'] == 'clang' and ('cxx' not in matrix_job or 'clang' not in matrix_job['cxx']): - raise Exception(error_message_with_matrix_job(matrix_job, f"cudacxx=clang requires cxx=clang.")) - - for tag in matrix_job: - if tag == 'origin': - continue - if tag not in all_tags: - raise Exception(error_message_with_matrix_job(matrix_job, f"Unknown tag '{tag}'")) - - if 'gpu' in matrix_job and matrix_job['gpu'] not in matrix_yaml['gpus'].keys(): - raise Exception(error_message_with_matrix_job(matrix_job, f"Unknown gpu '{matrix_job['gpu']}'")) - - -def set_default_tags(matrix_job): - all_tags = matrix_yaml['tags'].keys() - for tag in all_tags: - if tag in matrix_job: - continue - - tag_info = get_tag_info(tag) - if tag_info['default']: - matrix_job[tag] = tag_info['default'] - - -def canonicalize_tags(matrix_job): - if 'ctk' in matrix_job: - matrix_job['ctk'] = canonicalize_ctk_version(matrix_job['ctk']) - if 'cxx' in matrix_job: - matrix_job['cxx'] = canonicalize_host_compiler_name(matrix_job['cxx']) - - -def set_derived_tags(matrix_job): - if 'sm' in matrix_job and matrix_job['sm'] == 'gpu': - if not 'gpu' in matrix_job: - raise Exception(error_message_with_matrix_job(matrix_job, f"\"sm: 'gpu'\" requires tag 'gpu'.")) - gpu = get_gpu(matrix_job['gpu']) - matrix_job['sm'] = gpu['sm'] - - if 'std' in matrix_job and matrix_job['std'] == 'all': - matrix_job['std'] = lookup_supported_stds(matrix_job) - - # Add all deps before applying project job maps: - for job in matrix_job['jobs']: - job_info = get_job_type_info(job) - dep = job_info['needs'] - if dep and dep not in matrix_job['jobs']: - matrix_job['jobs'].append(dep) - - # Apply project job map: - project = get_project(matrix_job['project']) - for original_job, expanded_jobs in project['job_map'].items(): - if original_job in matrix_job['jobs']: - matrix_job['jobs'].remove(original_job) - matrix_job['jobs'] += expanded_jobs - - -def next_explode_tag(matrix_job): - non_exploded_tags = ['jobs'] - - for tag in matrix_job: - if not tag in non_exploded_tags and isinstance(matrix_job[tag], list): - return tag - return None - - -def explode_tags(matrix_job, explode_tag=None): - if not explode_tag: - explode_tag = next_explode_tag(matrix_job) - - if not explode_tag: - return [matrix_job] - - result = [] - for value in matrix_job[explode_tag]: - new_job = copy.deepcopy(matrix_job) - new_job[explode_tag] = value - result.extend(explode_tags(new_job)) - - return result - - -def preprocess_matrix_jobs(matrix_jobs, is_exclusion_matrix=False): - result = [] - if is_exclusion_matrix: - for matrix_job in matrix_jobs: - validate_tags(matrix_job, ignore_required=True) - for job in explode_tags(matrix_job): - canonicalize_tags(job) - result.append(job) - else: - for matrix_job in matrix_jobs: - validate_tags(matrix_job) - set_default_tags(matrix_job) - for job in explode_tags(matrix_job): - canonicalize_tags(job) - set_derived_tags(job) - # The derived tags may need to be exploded again: - result.extend(explode_tags(job)) - return result - - -def parse_workflow_matrix_jobs(args, workflow_name): - # Special handling for exclusion matrix: don't validate, add default, etc. Only explode. - is_exclusion_matrix = (workflow_name == 'exclude') - - if not workflow_name in matrix_yaml['workflows']: - if (is_exclusion_matrix): # Valid, no exclusions if not defined - return [] - raise Exception(f"Workflow '{workflow_name}' not found in matrix file '{matrix_yaml['filename']}'") - - matrix_jobs = matrix_yaml['workflows'][workflow_name] - if not matrix_jobs or len(matrix_jobs) == 0: - return [] - - workflow_line_number = find_workflow_line_number(workflow_name) - - # Tag with the original matrix info, location, etc. for error messages and post-processing. - # Do this first so the original tags / order /idx match the inpt object exactly. - if not is_exclusion_matrix: - for idx, matrix_job in enumerate(matrix_jobs): - workflow_location = f"{matrix_yaml['filename']}:{workflow_line_number} (job {idx + 1})" - matrix_job['origin'] = get_matrix_job_origin(matrix_job, workflow_name, workflow_location) - - # Fill in default values, explode lists. - matrix_jobs = preprocess_matrix_jobs(matrix_jobs, is_exclusion_matrix) - - if args: - if args.dirty_projects != None: # Explicitly check for None, as an empty list is valid: - matrix_jobs = [job for job in matrix_jobs if job['project'] in args.dirty_projects] - - # Don't remove excluded jobs if we're currently parsing them: - if not is_exclusion_matrix: - matrix_jobs = remove_excluded_jobs(matrix_jobs) - - # Sort the tags by, *ahem*, "importance": - sorted_tags = get_all_matrix_job_tags_sorted() - matrix_jobs = [{tag: matrix_job[tag] for tag in sorted_tags if tag in matrix_job} for matrix_job in matrix_jobs] - - return matrix_jobs - - -def parse_workflow_dispatch_groups(args, workflow_name): - # Add origin information to each matrix job, explode, filter, add defaults, etc. - # The resulting matrix_jobs list is a complete and standardized list of jobs for the dispatch_group builder. - matrix_jobs = parse_workflow_matrix_jobs(args, workflow_name) - - # If we're printing multiple workflows, add a prefix to the group name to differentiate them. - group_prefix = f"[{workflow_name}] " if len(args.workflows) > 1 else "" - - # Convert the matrix jobs into a dispatch group object: - workflow_dispatch_groups = {} - for matrix_job in matrix_jobs: - matrix_job_dispatch_group = matrix_job_to_dispatch_group(matrix_job, group_prefix) - merge_dispatch_groups(workflow_dispatch_groups, matrix_job_dispatch_group) - - return workflow_dispatch_groups - - -def write_outputs(final_workflow): - job_list = [] - runner_counts = {} - id_to_full_job_name = {} - - total_jobs = 0 - - def process_job_array(group_name, array_name, parent_json): - nonlocal job_list - nonlocal runner_counts - nonlocal total_jobs - - job_array = parent_json[array_name] if array_name in parent_json else [] - for job_json in job_array: - total_jobs += 1 - job_list.append(f"{total_jobs:4} id: {job_json['id']:<4} {array_name:13} {job_json['name']}") - id_to_full_job_name[job_json['id']] = f"{group_name} {job_json['name']}" - runner = job_json['runner'] - runner_counts[runner] = runner_counts.get(runner, 0) + 1 - - for group_name, group_json in final_workflow.items(): - job_list.append(f"{'':4} {group_name}:") - process_job_array(group_name, 'standalone', group_json) - if 'two_stage' in group_json: - for two_stage_json in group_json['two_stage']: - process_job_array(group_name, 'producers', two_stage_json) - process_job_array(group_name, 'consumers', two_stage_json) - - # Sort by descending counts: - runner_counts = {k: v for k, v in sorted(runner_counts.items(), key=lambda item: item[1], reverse=True)} - - runner_heading = f"🏃‍ Runner counts (total jobs: {total_jobs})" - - runner_counts_table = f"| {'#':^4} | Runner\n" - runner_counts_table += "|------|------\n" - for runner, count in runner_counts.items(): - runner_counts_table += f"| {count:4} | `{runner}`\n" - - runner_json = {"heading": runner_heading, "body": runner_counts_table} - - os.makedirs("workflow", exist_ok=True) - write_json_file("workflow/workflow.json", final_workflow) - write_json_file("workflow/job_ids.json", id_to_full_job_name) - write_text_file("workflow/job_list.txt", "\n".join(job_list)) - write_json_file("workflow/runner_summary.json", runner_json) - - -def write_override_matrix(override_matrix): - os.makedirs("workflow", exist_ok=True) - write_json_file("workflow/override.json", override_matrix) - - -def print_gha_workflow(args): - workflow_names = args.workflows - if args.allow_override and 'override' in matrix_yaml['workflows']: - override_matrix = matrix_yaml['workflows']['override'] - if override_matrix and len(override_matrix) > 0: - print(f"::notice::Using 'override' workflow instead of '{workflow_names}'") - workflow_names = ['override'] - write_override_matrix(override_matrix) - - final_workflow = {} - for workflow_name in workflow_names: - workflow_dispatch_groups = parse_workflow_dispatch_groups(args, workflow_name) - merge_dispatch_groups(final_workflow, workflow_dispatch_groups) - - final_workflow = finalize_workflow_dispatch_groups(final_workflow) - - write_outputs(final_workflow) - - -def print_devcontainer_info(args): - devcontainer_version = matrix_yaml['devcontainer_version'] - - matrix_jobs = [] - - # Remove the `exclude` and `override` entries: - ignored_matrix_keys = ['exclude', 'override'] - workflow_names = [key for key in matrix_yaml['workflows'].keys() if key not in ignored_matrix_keys] - for workflow_name in workflow_names: - matrix_jobs.extend(parse_workflow_matrix_jobs(args, workflow_name)) - - # Remove all but the following keys from the matrix jobs: - keep_keys = ['ctk', 'cxx'] - combinations = [{key: job[key] for key in keep_keys} for job in matrix_jobs] - - # Remove duplicates and filter out windows jobs: - unique_combinations = [] - for combo in combinations: - if not is_windows(combo) and combo not in unique_combinations: - unique_combinations.append(combo) - - for combo in unique_combinations: - host_compiler = get_host_compiler(combo['cxx']) - del combo['cxx'] - combo['compiler_name'] = host_compiler['container_tag'] - combo['compiler_version'] = host_compiler['version'] - combo['compiler_exe'] = host_compiler['exe'] - - combo['cuda'] = combo['ctk'] - del combo['ctk'] - - devcontainer_json = {'devcontainer_version': devcontainer_version, 'combinations': unique_combinations} - - # Pretty print the devcontainer json to stdout: - print(json.dumps(devcontainer_json, indent=2)) - - -def preprocess_matrix_yaml(matrix): - # Make all CTK version keys into strings: - new_ctk = {} - for version, attrs in matrix['ctk_versions'].items(): - new_ctk[str(version)] = attrs - matrix['ctk_versions'] = new_ctk - - # Make all compiler version keys into strings: - for id, hc_def in matrix['host_compilers'].items(): - new_versions = {} - for version, attrs in hc_def['versions'].items(): - new_versions[str(version)] = attrs - hc_def['versions'] = new_versions - - return matrix - - -def main(): - parser = argparse.ArgumentParser(description='Compute matrix for workflow') - parser.add_argument('matrix_file', help='Path to the matrix YAML file') - parser_mode_group = parser.add_argument_group('Output Mode', "Must specify one of these options.") - parser_mode = parser_mode_group.add_mutually_exclusive_group(required=True) - parser_mode.add_argument('--workflows', nargs='+', - help='Print GHA workflow with jobs from [pull_request, nightly, weekly, etc]') - parser_mode.add_argument('--devcontainer-info', action='store_true', - help='Print devcontainer info instead of GHA workflows.') - parser.add_argument('--dirty-projects', nargs='*', help='Filter jobs to only these projects') - parser.add_argument('--allow-override', action='store_true', - help='If a non-empty "override" workflow exists, it will be used instead of those in --workflows.') - args = parser.parse_args() - - # Check if the matrix file exists - if not os.path.isfile(args.matrix_file): - print(f"Error: Matrix file '{args.matrix_file}' does not exist.") - sys.exit(1) - - with open(args.matrix_file, 'r') as f: - global matrix_yaml - matrix_yaml = yaml.safe_load(f) - matrix_yaml = preprocess_matrix_yaml(matrix_yaml) - matrix_yaml['filename'] = args.matrix_file - - if args.workflows: - print_gha_workflow(args) - elif args.devcontainer_info: - print_devcontainer_info(args) - else: - parser.print_usage() - sys.exit(1) - - -if __name__ == '__main__': - main() diff --git a/.github/actions/workflow-build/prepare-workflow-dispatch.py b/.github/actions/workflow-build/prepare-workflow-dispatch.py deleted file mode 100755 index fbdde691019..00000000000 --- a/.github/actions/workflow-build/prepare-workflow-dispatch.py +++ /dev/null @@ -1,95 +0,0 @@ -#!/usr/bin/env python3 - -""" -This script prepares a full workflow for GHA dispatch. - -To avoid skipped jobs from cluttering the GHA UI, this script splits the full workflow.json into multiple workflows -that don't require large numbers of skipped jobs in the workflow implementation. -""" - -import argparse -import json -import os -import sys - - -def write_json_file(filename, json_object): - with open(filename, 'w') as f: - json.dump(json_object, f, indent=2) - - -def is_windows(job): - return job['runner'].startswith('windows') - - -def split_workflow(workflow): - linux_standalone = {} - linux_two_stage = {} - windows_standalone = {} - windows_two_stage = {} - - def strip_extra_info(job): - del job['origin'] - - for group_name, group_json in workflow.items(): - standalone = group_json['standalone'] if 'standalone' in group_json else [] - two_stage = group_json['two_stage'] if 'two_stage' in group_json else [] - - if len(standalone) > 0: - for job in standalone: - strip_extra_info(job) - - if is_windows(standalone[0]): - windows_standalone[group_name] = standalone - else: - linux_standalone[group_name] = standalone - - if len(two_stage) > 0: - for ts in two_stage: - for job in ts['producers']: - strip_extra_info(job) - for job in ts['consumers']: - strip_extra_info(job) - - if is_windows(two_stage[0]['producers'][0]): - windows_two_stage[group_name] = two_stage - else: - linux_two_stage[group_name] = two_stage - - dispatch = { - 'linux_standalone': { - 'keys': list(linux_standalone.keys()), - 'jobs': linux_standalone}, - 'linux_two_stage': { - 'keys': list(linux_two_stage.keys()), - 'jobs': linux_two_stage}, - 'windows_standalone': { - 'keys': list(windows_standalone.keys()), - 'jobs': windows_standalone}, - 'windows_two_stage': { - 'keys': list(windows_two_stage.keys()), - 'jobs': windows_two_stage} - } - - os.makedirs('workflow', exist_ok=True) - write_json_file('workflow/dispatch.json', dispatch) - - -def main(): - parser = argparse.ArgumentParser(description='Prepare a full workflow for GHA dispatch.') - parser.add_argument('workflow_json', help='Path to the full workflow.json file') - args = parser.parse_args() - - # Check if the workflow file exists - if not os.path.isfile(args.workflow_json): - print(f"Error: Matrix file '{args.workflow_json}' not found.") - sys.exit(1) - - with open(args.workflow_json) as f: - workflow = json.load(f) - - split_workflow(workflow) - - -if __name__ == '__main__': - main() diff --git a/.github/actions/workflow-results/action.yml b/.github/actions/workflow-results/action.yml deleted file mode 100644 index f14d6d496e8..00000000000 --- a/.github/actions/workflow-results/action.yml +++ /dev/null @@ -1,212 +0,0 @@ -name: "CCCL Workflow Sentinal" -description: "Check the results of the dispatched jobs and comment on the PR." - -inputs: - github_token: - description: "The GitHub token to use for commenting on the PR. No comment will be made if not provided." - required: false - pr_number: - description: "The PR number to comment on, if applicable. No comment will be made if not provided." - required: false - slack_token: - description: "The Slack token to use for notifications. No notifications will be sent if not provided." - required: false - slack_log: - description: "Slack channel ID for verbose notifications." - required: false - slack_alert: - description: "Slack channel ID for alert notifications." - required: false - -outputs: - success: - description: "Whether any jobs failed." - value: ${{ steps.check-success.outputs.success }} - -runs: - using: "composite" - steps: - - - name: Download workflow artifacts - uses: actions/download-artifact@v4 - with: - name: workflow - path: workflow/ - - - name: Download job artifacts - continue-on-error: true # This may fail if no jobs succeed. The checks below will catch this. - uses: actions/download-artifact@v4 - with: - path: jobs - pattern: jobs-* - merge-multiple: true - - - name: Clean up job artifacts - continue-on-error: true - shell: bash --noprofile --norc -euo pipefail {0} - run: | - # Fix artifacts written on windows: - echo "::group::Fixing line endings in job artifacts" - sudo apt-get update - sudo apt-get install -y dos2unix - find jobs -type f -exec dos2unix -v {} \; - echo "::endgroup::" - - echo "::group::Job artifacts" - tree jobs - echo "::endgroup::" - - - name: Fetch workflow job info - if: ${{ inputs.github_token != ''}} - continue-on-error: true - uses: actions/github-script@v7 - with: - github-token: ${{ inputs.github_token }} - script: | - const fs = require('fs'); - - const owner = context.repo.owner; - const repo = context.repo.repo; - const runId = context.runId; - - github.paginate( - 'GET /repos/{owner}/{repo}/actions/runs/{run_id}/jobs?filter=all', - { - owner: owner, - repo: repo, - run_id: runId - } - ) - .then(jobs => { - console.log('::group::Jobs JSON'); - console.log(JSON.stringify(jobs, null, 2)); - console.log('::endgroup::'); - fs.mkdirSync("results", { recursive: true }); - fs.writeFileSync('results/jobs.json', JSON.stringify(jobs, null, 2)); - console.log(`Fetched ${jobs.length} jobs and saved to results/jobs.json`); - }) - .catch(error => { - console.error(error); - }); - - - name: Parse job times - continue-on-error: true - shell: bash --noprofile --norc -euo pipefail {0} - run: | - echo "Parsing job times..." - python3 "${GITHUB_ACTION_PATH}/parse-job-times.py" workflow/workflow.json results/jobs.json - - - name: Prepare execution summary - continue-on-error: true - shell: bash --noprofile --norc -euo pipefail {0} - run: | - echo "Generating execution summary..." - python3 "${GITHUB_ACTION_PATH}/prepare-execution-summary.py" workflow/workflow.json results/job_times.json - - - name: Prepare final summary - id: final-summary - continue-on-error: true - shell: bash --noprofile --norc -euo pipefail {0} - run: | - echo "::group::Final Summary" - python3 "${GITHUB_ACTION_PATH}/final-summary.py" | tee final_summary.md - echo "::endgroup::" - - # This allows multiline strings and special characters to be passed through the GHA outputs: - url_encode_string() { - python3 -c "import sys; from urllib.parse import quote; print(quote(sys.stdin.read()))" - } - - echo "::group::GHA Output: SUMMARY" - printf "SUMMARY=%s\n" "$(cat final_summary.md | url_encode_string)" | tee -a "${GITHUB_OUTPUT}" - echo "::endgroup::" - - echo "::group::GHA Output: EXEC_SUMMARY" - printf "EXEC_SUMMARY=%s\n" "$(cat execution/heading.txt)" | tee -a "${GITHUB_OUTPUT}" - echo "::endgroup::" - - cp final_summary.md ${GITHUB_STEP_SUMMARY} - - - name: Comment on PR - if: ${{ !cancelled() && inputs.pr_number != '' && inputs.github_token != ''}} - continue-on-error: true - env: - PR_NUMBER: ${{ fromJSON(inputs.pr_number) }} - COMMENT_BODY: ${{ steps.final-summary.outputs.SUMMARY }} - uses: actions/github-script@v7 - with: - github-token: ${{ inputs.github_token }} - script: | - const pr_number = process.env.PR_NUMBER; - const owner = context.repo.owner; - const repo = context.repo.repo; - // Decode URL-encoded string for proper display in comments - const commentBody = decodeURIComponent(process.env.COMMENT_BODY); - console.log('::group::Commenting on PR #' + pr_number + ' with the following message:') - console.log(commentBody); - console.log('::endgroup::'); - github.rest.issues.createComment({ - owner: owner, - repo: repo, - issue_number: pr_number, - body: commentBody - }); - - - name: Check for job success - id: check-success - shell: bash --noprofile --norc -euo pipefail {0} - run: | - echo "::group::Checking for success artifacts" - "${GITHUB_ACTION_PATH}/verify-job-success.py" workflow/job_ids.json - result=$? - echo "::endgroup::" - - if [[ $result -ne 0 ]]; then - echo "success=false" >> "${GITHUB_OUTPUT}" - exit 1 - fi - - if [ -f workflow/override.json ]; then - echo "::notice::Workflow matrix was overridden. Failing jobs." - echo "Override matrix:" - cat workflow/override.json | jq -c '.' - echo "success=false" >> "${GITHUB_OUTPUT}" - exit 1 - fi - - echo "success=true" >> "${GITHUB_OUTPUT}" - - - name: Send Slack log notification - if: ${{ always() && inputs.slack_token != '' && inputs.slack_log != '' }} - uses: slackapi/slack-github-action@v1.26.0 - env: - SLACK_BOT_TOKEN: ${{ inputs.slack_token }} - WORKFLOW_TYPE: ${{ github.workflow }} # nightly, weekly, pr, etc. - STATUS: ${{ steps.check-success.outcome }} - EXEC_SUMMARY: ${{ steps.final-summary.outputs.EXEC_SUMMARY }} - SUMMARY_URL: https://github.com/${{github.repository}}/actions/runs/${{github.run_id}} - with: - channel-id: ${{ inputs.slack_log }} - slack-message: | - Workflow '${{ env.WORKFLOW_TYPE }}' has finished with status `${{ env.STATUS }}`: - - ${{ env.EXEC_SUMMARY }} - - Details: ${{ env.SUMMARY_URL }} - - - name: Send Slack alert notification - if: ${{ failure() && inputs.slack_token != '' && inputs.slack_alert != '' }} - uses: slackapi/slack-github-action@v1.26.0 - env: - SLACK_BOT_TOKEN: ${{ inputs.slack_token }} - WORKFLOW_TYPE: ${{ github.workflow }} # nightly, weekly, pr, etc. - EXEC_SUMMARY: ${{ steps.final-summary.outputs.EXEC_SUMMARY }} - SUMMARY_URL: https://github.com/${{github.repository}}/actions/runs/${{github.run_id}} - with: - channel-id: ${{ inputs.slack_alert }} - slack-message: | - Workflow '${{ env.WORKFLOW_TYPE }}' has failed: - - ${{ env.EXEC_SUMMARY }} - - Details: ${{ env.SUMMARY_URL }} diff --git a/.github/actions/workflow-results/final-summary.py b/.github/actions/workflow-results/final-summary.py deleted file mode 100755 index 62cfa63bf75..00000000000 --- a/.github/actions/workflow-results/final-summary.py +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env python3 - -import json -import os -import re -import sys - - -def read_file(filepath): - with open(filepath, 'r') as f: - return f.read().rstrip("\n ") - - -def print_text_file(filepath): - if os.path.exists(filepath): - print(read_file(filepath) + "\n\n") - - -def print_json_summary(summary, heading_level): - print(f"
{summary['heading']}\n") - print(summary["body"] + "\n") - print("
\n") - - -def print_summary_file(filepath, heading_level): - if os.path.exists(filepath): - with open(filepath, 'r') as f: - print_json_summary(json.load(f), heading_level) - - -def print_json_file(filepath, heading): - if os.path.exists(filepath): - json_data = json.load(open(filepath)) - print(f"

{heading}

\n") - print('```json') - print(json.dumps(json_data, indent=2)) - print('```') - print("
\n") - - -def main(): - # Parse project summaries and sort them by the number of failed jobs: - projects = [] - project_file_regex = "[0-9]+_.+_summary.json" - for filename in sorted(os.listdir("execution/projects")): - match = re.match(project_file_regex, filename) - if match: - with open(f"execution/projects/{filename}", 'r') as f: - projects.append(json.load(f)) - - print(f"
{read_file('execution/heading.txt')}\n") - - print("
    ") - for project in projects: - print("
  • ") - print_json_summary(project, 3) - print("
\n") - - print_json_file('workflow/override.json', '🛠️ Override Matrix') - print_text_file('workflow/changes.md') - print_summary_file("workflow/runner_summary.json", 2) - - print("
") - - -if __name__ == '__main__': - main() diff --git a/.github/actions/workflow-results/parse-job-times.py b/.github/actions/workflow-results/parse-job-times.py deleted file mode 100755 index b30d585a0a6..00000000000 --- a/.github/actions/workflow-results/parse-job-times.py +++ /dev/null @@ -1,129 +0,0 @@ -#!/usr/bin/env python3 - -import argparse -import datetime -import json -import os -import sys - - -def get_jobs_json(jobs_file): - # Return the contents of jobs.json - with open(jobs_file) as f: - result = json.load(f) - - return result - - -def get_workflow_json(workflow_file): - # Return the contents of ~/cccl/.local/tmp/workflow.json - with open(workflow_file) as f: - return json.load(f) - - -def write_json(filepath, json_object): - with open(filepath, 'w') as f: - json.dump(json_object, f, indent=4) - - -def generate_job_id_map(workflow): - '''Map full job name to job id''' - job_id_map = {} - for group_name, group_json in workflow.items(): - standalone = group_json['standalone'] if 'standalone' in group_json else [] - for job in standalone: - name = f"{group_name} / {job['name']}" - job_id_map[name] = job['id'] - two_stage = group_json['two_stage'] if 'two_stage' in group_json else [] - for pc in two_stage: - producers = pc['producers'] - consumers = pc['consumers'] - for job in producers + consumers: - name = f"{group_name} / {pc['id']} / {job['name']}" - job_id_map[name] = job['id'] - - return job_id_map - - -def main(): - # Accept two command line arguments: - parser = argparse.ArgumentParser(description='Parse job times') - parser.add_argument('workflow', type=str, help='Path to workflow.json') - parser.add_argument('jobs', type=str, help='Path to jobs.json') - args = parser.parse_args() - - jobs = get_jobs_json(args.jobs) - workflow = get_workflow_json(args.workflow) - - # Converts full github job names into job ids: - job_id_map = generate_job_id_map(workflow) - - # Map of id -> { } - result = {} - - unknown_jobs = [job for job in jobs if job['name'] not in job_id_map] - jobs = [job for job in jobs if job['name'] in job_id_map] - - # Process jobs: - for job in jobs: - name = job['name'] - - id = job_id_map[name] - - # Job times are 2024-05-09T06:52:20Z - started_at = job['started_at'] - started_time = datetime.datetime.strptime(started_at, "%Y-%m-%dT%H:%M:%SZ") - started_time_epoch_secs = started_time.timestamp() - - completed_at = job['completed_at'] - completed_time = datetime.datetime.strptime(completed_at, "%Y-%m-%dT%H:%M:%SZ") - completed_time_epoch_secs = completed_time.timestamp() - - job_seconds = (completed_time - started_time).total_seconds() - job_duration = str(datetime.timedelta(seconds=job_seconds)) - - result[id] = {} - result[id]['name'] = name - result[id]['started_at'] = started_at - result[id]['completed_at'] = completed_at - result[id]['started_epoch_secs'] = started_time_epoch_secs - result[id]['completed_epoch_secs'] = completed_time_epoch_secs - result[id]['job_duration'] = job_duration - result[id]['job_seconds'] = job_seconds - - # Find the "Run command" step and record its duration: - command_seconds = 0 - for step in job['steps']: - if step['name'].lower() == "run command": - step_started_at = step['started_at'] - step_started_time = datetime.datetime.strptime(step_started_at, "%Y-%m-%dT%H:%M:%SZ") - step_completed_at = step['completed_at'] - step_completed_time = datetime.datetime.strptime(step_completed_at, "%Y-%m-%dT%H:%M:%SZ") - command_seconds = (step_completed_time - step_started_time).total_seconds() - break - - command_duration = str(datetime.timedelta(seconds=command_seconds)) - - result[id]['command_seconds'] = command_seconds - result[id]['command_duration'] = command_duration - - os.makedirs("results", exist_ok=True) - write_json("results/job_times.json", result) - - print("::group::Unmapped jobs") - print("\n".join([job['name'] for job in unknown_jobs])) - print("::endgroup::") - - print("::group::Job times") - print(f"{'Job':^10} {'Command':^10} {'Overhead':^10} Name") - print(f"{'-'*10} {'-'*10} {'-'*10} {'-'*10}") - for id, stats in result.items(): - job_seconds = stats['job_seconds'] - command_seconds = stats['command_seconds'] - overhead = (job_seconds - command_seconds) * 100 / command_seconds if command_seconds > 0 else 100 - print(f"{stats['job_duration']:10} {stats['command_duration']:10} {overhead:10.0f} {stats['name']}") - print("::endgroup::") - - -if __name__ == "__main__": - main() diff --git a/.github/actions/workflow-results/prepare-execution-summary.py b/.github/actions/workflow-results/prepare-execution-summary.py deleted file mode 100755 index 07dbdde8df3..00000000000 --- a/.github/actions/workflow-results/prepare-execution-summary.py +++ /dev/null @@ -1,365 +0,0 @@ -#!/usr/bin/env python3 - - -import argparse -import functools -import json -import os -import re -import sys - - -def job_succeeded(job): - # The job was successful if the success file exists: - return os.path.exists(f'jobs/{job["id"]}/success') - - -def natural_sort_key(key): - # Natural sort impl (handles embedded numbers in strings, case insensitive) - return [(int(text) if text.isdigit() else text.lower()) for text in re.split('(\d+)', key)] - - -# Print the prepared text summary to the file at the given path -def write_text(filepath, summary): - with open(filepath, 'w') as f: - print(summary, file=f) - - -# Print the prepared JSON object to the file at the given path -def write_json(filepath, json_object): - with open(filepath, 'w') as f: - json.dump(json_object, f, indent=4) - - -def extract_jobs(workflow): - jobs = [] - for group_name, group in workflow.items(): - if "standalone" in group: - jobs += group["standalone"] - if "two_stage" in group: - for two_stage in group["two_stage"]: - jobs += two_stage["producers"] - jobs += two_stage["consumers"] - return jobs - - -@functools.lru_cache(maxsize=None) -def get_sccache_stats(job_id): - sccache_file = f'jobs/{job_id}/sccache_stats.json' - if os.path.exists(sccache_file): - with open(sccache_file) as f: - return json.load(f) - return None - - -def update_summary_entry(entry, job, job_times=None): - if 'passed' not in entry: - entry['passed'] = 0 - if 'failed' not in entry: - entry['failed'] = 0 - - if job_succeeded(job): - entry['passed'] += 1 - else: - entry['failed'] += 1 - - if job_times: - time_info = job_times[job["id"]] - job_time = time_info["job_seconds"] - command_time = time_info["command_seconds"] - - if not 'job_time' in entry: - entry['job_time'] = 0 - if not 'command_time' in entry: - entry['command_time'] = 0 - if not 'max_job_time' in entry: - entry['max_job_time'] = 0 - - entry['job_time'] += job_time - entry['command_time'] += command_time - entry['max_job_time'] = max(entry['max_job_time'], job_time) - - sccache_stats = get_sccache_stats(job["id"]) - if sccache_stats: - sccache_stats = sccache_stats['stats'] - requests = sccache_stats.get('compile_requests', 0) - hits = 0 - if 'cache_hits' in sccache_stats: - cache_hits = sccache_stats['cache_hits'] - if 'counts' in cache_hits: - counts = cache_hits['counts'] - for lang, lang_hits in counts.items(): - hits += lang_hits - if 'sccache' not in entry: - entry['sccache'] = {'requests': requests, 'hits': hits} - else: - entry['sccache']['requests'] += requests - entry['sccache']['hits'] += hits - - return entry - - -def build_summary(jobs, job_times=None): - summary = {'projects': {}} - projects = summary['projects'] - - for job in jobs: - update_summary_entry(summary, job, job_times) - - matrix_job = job["origin"]["matrix_job"] - - project = matrix_job["project"] - if not project in projects: - projects[project] = {'tags': {}} - tags = projects[project]['tags'] - - update_summary_entry(projects[project], job, job_times) - - for tag in matrix_job.keys(): - if tag == 'project': - continue - - if not tag in tags: - tags[tag] = {'values': {}} - values = tags[tag]['values'] - - update_summary_entry(tags[tag], job, job_times) - - value = str(matrix_job[tag]) - - if not value in values: - values[value] = {} - update_summary_entry(values[value], job, job_times) - - # Natural sort the value strings within each tag: - for project, project_summary in projects.items(): - for tag, tag_summary in project_summary['tags'].items(): - tag_summary['values'] = dict(sorted(tag_summary['values'].items(), - key=lambda item: natural_sort_key(item[0]))) - - # Sort the tags within each project so that: - # - "Likely culprits" come first. These are tags that have multiple values, but only one has failures. - # - Tags with multiple values and mixed pass/fail results come next. - # - Tags with all failing values come next. - # - Tags with no failures are last. - def rank_tag(tag_summary): - tag_failures = tag_summary['failed'] - num_values = len(tag_summary['values']) - num_failing_values = sum(1 for value_summary in tag_summary['values'].values() if value_summary['failed'] > 0) - - if num_values > 1: - if num_failing_values == 1: - return 0 - elif num_failing_values > 0 and num_failing_values < num_values: - return 1 - elif tag_failures > 0: - return 2 - return 3 - for project, project_summary in projects.items(): - project_summary['tags'] = dict(sorted(project_summary['tags'].items(), - key=lambda item: (rank_tag(item[1]), item[0]))) - - return summary - - -def get_walltime(job_times): - "Return the walltime for all jobs in seconds." - start = None - end = None - for job_id, job_time in job_times.items(): - job_start_timestamp = job_time['started_epoch_secs'] - job_end_timestamp = job_time['completed_epoch_secs'] - if not start or job_start_timestamp < start: - start = job_start_timestamp - if not end or job_end_timestamp > end: - end = job_end_timestamp - return end - start - - -def format_seconds(seconds): - days, remainder = divmod(seconds, 86400) - hours, remainder = divmod(remainder, 3600) - minutes, seconds = divmod(remainder, 60) - days = int(days) - hours = int(hours) - minutes = int(minutes) - seconds = int(seconds) - if (days > 0): - return f'{days}d {hours:02}h' - elif (hours > 0): - return f'{hours}h {minutes:02}m' - else: - return f'{minutes}m {seconds:02}s' - - -def get_summary_stats(summary): - passed = summary['passed'] - failed = summary['failed'] - total = passed + failed - - percent = int(100 * passed / total) if total > 0 else 0 - pass_string = f'Pass: {percent:>3}%/{total}' - - stats = f'{pass_string:<14}' - - if 'job_time' in summary and total > 0 and summary['job_time'] > 0: - job_time = summary['job_time'] - max_job_time = summary['max_job_time'] - total_job_duration = format_seconds(job_time) - avg_job_duration = format_seconds(job_time / total) - max_job_duration = format_seconds(max_job_time) - stats += f' | Total: {total_job_duration:>7} | Avg: {avg_job_duration:>7} | Max: {max_job_duration:>7}' - - if 'sccache' in summary: - sccache = summary['sccache'] - requests = sccache["requests"] - hits = sccache["hits"] - hit_percent = int(100 * hits / requests) if requests > 0 else 0 - hit_string = f'Hits: {hit_percent:>3}%/{requests}' - stats += f' | {hit_string:<17}' - - return stats - - -def get_summary_heading(summary, walltime): - passed = summary['passed'] - failed = summary['failed'] - - if summary['passed'] == 0: - flag = '🟥' - elif summary['failed'] > 0: - flag = '🟨' - else: - flag = '🟩' - - return f'{flag} CI finished in {walltime}: {get_summary_stats(summary)}' - - -def get_project_heading(project, project_summary): - if project_summary['passed'] == 0: - flag = '🟥' - elif project_summary['failed'] > 0: - flag = '🟨' - else: - flag = '🟩' - - return f'{flag} {project}: {get_summary_stats(project_summary)}' - - -def get_tag_line(tag, tag_summary): - passed = tag_summary['passed'] - failed = tag_summary['failed'] - values = tag_summary['values'] - - # Find the value with an failure rate that matches the tag's failure rate: - suspicious = None - if len(values) > 1 and failed > 0: - for value, value_summary in values.items(): - if value_summary['failed'] == failed: - suspicious = value_summary - suspicious['name'] = value - break - - # Did any jobs with this value pass? - likely_culprit = suspicious if suspicious and suspicious['passed'] == 0 else None - - note = '' - if likely_culprit: - flag = '🚨' - note = f': {likely_culprit["name"]} {flag}' - elif suspicious: - flag = '🔍' - note = f': {suspicious["name"]} {flag}' - elif passed == 0: - flag = '🟥' - elif failed > 0: - flag = '🟨' - else: - flag = '🟩' - - return f'{flag} {tag}{note}' - - -def get_value_line(value, value_summary, tag_summary): - passed = value_summary['passed'] - failed = value_summary['failed'] - total = passed + failed - - parent_size = len(tag_summary['values']) - parent_failed = tag_summary['failed'] - - is_suspicious = failed > 0 and failed == parent_failed and parent_size > 1 - is_likely_culprit = is_suspicious and passed == 0 - - if is_likely_culprit: - flag = '🔥' - elif is_suspicious: - flag = '🔍' - elif passed == 0: - flag = '🟥' - elif failed > 0: - flag = '🟨' - else: - flag = '🟩' - - left_aligned = f"{flag} {value}" - return f' {left_aligned:<20} {get_summary_stats(value_summary)}' - - -def get_project_summary_body(project, project_summary): - body = ['```'] - for tag, tag_summary in project_summary['tags'].items(): - body.append(get_tag_line(tag, tag_summary)) - for value, value_summary in tag_summary['values'].items(): - body.append(get_value_line(value, value_summary, tag_summary)) - body.append('```') - return "\n".join(body) - - -def write_project_summary(idx, project, project_summary): - heading = get_project_heading(project, project_summary) - body = get_project_summary_body(project, project_summary) - - summary = {'heading': heading, 'body': body} - - write_json(f'execution/projects/{idx:03}_{project}_summary.json', summary) - - -def write_workflow_summary(workflow, job_times=None): - summary = build_summary(extract_jobs(workflow), job_times) - walltime = format_seconds(get_walltime(job_times)) if job_times else '[unknown]' - - os.makedirs('execution/projects', exist_ok=True) - - write_text('execution/heading.txt', get_summary_heading(summary, walltime)) - - # Sort summary projects so that projects with failures come first, and ties - # are broken by the total number of jobs: - def sort_project_key(project_summary): - failed = project_summary[1]['failed'] - total = project_summary[1]['passed'] + failed - return (-failed, -total) - - for i, (project, project_summary) in enumerate(sorted(summary['projects'].items(), key=sort_project_key)): - write_project_summary(i, project, project_summary) - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument('workflow', type=argparse.FileType('r')) - parser.add_argument('job_times', type=argparse.FileType('r')) - args = parser.parse_args() - - workflow = json.load(args.workflow) - - # The timing file is not required. - try: - job_times = json.load(args.job_times) - except: - job_times = None - - write_workflow_summary(workflow, job_times) - - -if __name__ == '__main__': - main() diff --git a/.github/actions/workflow-results/verify-job-success.py b/.github/actions/workflow-results/verify-job-success.py deleted file mode 100755 index d3b9b3c4099..00000000000 --- a/.github/actions/workflow-results/verify-job-success.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env python3 - -import argparse -import json -import os -import sys - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument("job_id_map", type=argparse.FileType('r')) - args = parser.parse_args() - - job_id_map = json.load(args.job_id_map) - - # For each job id, verify that the success artifact exists - success = True - for job_id, job_name in job_id_map.items(): - success_file = f'jobs/{job_id}/success' - print(f'Verifying job with id "{job_id}": "{job_name}"') - if not os.path.exists(success_file): - print(f'Failed: Artifact "{success_file}" not found') - success = False - - if not success: - sys.exit(1) - - -if __name__ == '__main__': - main() diff --git a/.github/actions/workflow-run-job-linux/action.yml b/.github/actions/workflow-run-job-linux/action.yml deleted file mode 100644 index d10d1dca2ef..00000000000 --- a/.github/actions/workflow-run-job-linux/action.yml +++ /dev/null @@ -1,197 +0,0 @@ -name: "Run Linux Job" -description: "Run a job on a Linux runner." - -# This job now uses a docker-outside-of-docker (DOOD) strategy. -# -# The GitHub Actions runner application mounts the host's docker socket `/var/run/docker.sock` into the -# container. By using a container with the `docker` CLI, this container can launch docker containers -# using the host's docker daemon. -# -# This allows us to run actions that require node v20 in the `cruizba/ubuntu-dind:jammy-26.1.3` container, and -# then launch our Ubuntu18.04-based GCC 6/7 containers to build and test CCCL. -# -# The main inconvenience to this approach is that any container mounts have to match the paths of the runner host, -# not the paths as seen in the intermediate (`cruizba/ubuntu-dind`) container. -# -# Note: I am using `cruizba/ubuntu-dind:jammy-26.1.3` instead of `docker:latest`, because GitHub doesn't support -# JS actions in alpine aarch64 containers, instead failing actions with this error: -# ``` -# Error: JavaScript Actions in Alpine containers are only supported on x64 Linux runners. Detected Linux Arm64 -# ``` - -inputs: - id: - description: "A unique identifier." - required: true - command: - description: "The command to run." - required: true - image: - description: "The Docker image to use." - required: true - runner: - description: "The GHA runs-on value." - required: true - cuda: - description: "The CUDA version to use when selecting a devcontainer." - required: true - host: - description: "The host compiler to use when selecting a devcontainer." - required: true - -runs: - using: "composite" - steps: - - name: Install dependencies - shell: sh - run: | - # Install script dependencies - apt update - apt install -y --no-install-recommends tree git - - name: Checkout repo - uses: actions/checkout@v4 - with: - path: ${{github.event.repository.name}} - persist-credentials: false - - name: Add NVCC problem matcher - shell: bash --noprofile --norc -euo pipefail {0} - run: | - echo "::add-matcher::${{github.event.repository.name}}/.github/problem-matchers/problem-matcher.json" - - name: Get AWS credentials for sccache bucket - uses: aws-actions/configure-aws-credentials@v4 - with: - role-to-assume: arn:aws:iam::279114543810:role/gha-oidc-NVIDIA - aws-region: us-east-2 - role-duration-seconds: 43200 # 12 hours - - name: Run command # Do not change this step's name, it is checked in parse-job-times.py - shell: bash --noprofile --norc -euo pipefail {0} - env: - CI: true - RUNNER: "${{inputs.runner}}" - # Dereferencing the command from an env var instead of a GHA input avoids issues with escaping - # semicolons and other special characters (e.g. `-arch "60;70;80"`). - COMMAND: "${{inputs.command}}" - AWS_ACCESS_KEY_ID: "${{env.AWS_ACCESS_KEY_ID}}" - AWS_SESSION_TOKEN: "${{env.AWS_SESSION_TOKEN}}" - AWS_SECRET_ACCESS_KEY: "${{env.AWS_SECRET_ACCESS_KEY}}" - run: | - echo "[host] github.workspace: ${{github.workspace}}" - echo "[container] GITHUB_WORKSPACE: ${GITHUB_WORKSPACE:-}" - echo "[container] PWD: $(pwd)" - - # Necessary because we're doing docker-outside-of-docker: - # Make a symlink in the container that matches the host's ${{github.workspace}}, so that way `$(pwd)` - # in `.devcontainer/launch.sh` constructs volume paths relative to the hosts's ${{github.workspace}}. - mkdir -p "$(dirname "${{github.workspace}}")" - ln -s "$(pwd)" "${{github.workspace}}" - cd "${{github.workspace}}" - - cat <<"EOF" > ci.sh - #! /usr/bin/env bash - set -eo pipefail - echo -e "\e[1;34mRunning as '$(whoami)' user in $(pwd):\e[0m" - echo -e "\e[1;34m${COMMAND}\e[0m" - eval "${COMMAND}" || exit_code=$? - if [ ! -z "$exit_code" ]; then - echo -e "::group::️❗ \e[1;31mInstructions to Reproduce CI Failure Locally\e[0m" - echo "::error:: To replicate this failure locally, follow the steps below:" - echo "1. Clone the repository, and navigate to the correct branch and commit:" - echo " git clone --branch $GITHUB_REF_NAME --single-branch https://github.com/$GITHUB_REPOSITORY.git && cd $(echo $GITHUB_REPOSITORY | cut -d'/' -f2) && git checkout $GITHUB_SHA" - echo "" - echo "2. Run the failed command inside the same Docker container used by this CI job:" - echo " .devcontainer/launch.sh -d -c ${{inputs.cuda}} -H ${{inputs.host}} -- ${COMMAND}" - echo "" - echo "For additional information, see:" - echo " - DevContainer Documentation: https://github.com/NVIDIA/cccl/blob/main/.devcontainer/README.md" - echo " - Continuous Integration (CI) Overview: https://github.com/NVIDIA/cccl/blob/main/ci-overview.md" - exit $exit_code - fi - EOF - - chmod +x ci.sh - - mkdir "$RUNNER_TEMP/.aws"; - - cat < "$RUNNER_TEMP/.aws/config" - [default] - bucket=rapids-sccache-devs - region=us-east-2 - EOF - - cat < "$RUNNER_TEMP/.aws/credentials" - [default] - aws_access_key_id=$AWS_ACCESS_KEY_ID - aws_session_token=$AWS_SESSION_TOKEN - aws_secret_access_key=$AWS_SECRET_ACCESS_KEY - EOF - - chmod 0600 "$RUNNER_TEMP/.aws/credentials" - chmod 0664 "$RUNNER_TEMP/.aws/config" - - declare -a gpu_request=() - - # Explicitly pass which GPU to use if on a GPU runner - if [[ "${RUNNER}" = *"-gpu-"* ]]; then - gpu_request+=(--gpus "device=${NVIDIA_VISIBLE_DEVICES}") - fi - - host_path() { - sed "s@/__w@$(dirname "$(dirname "${{github.workspace}}")")@" <<< "$1" - } - - # Launch this container using the host's docker daemon - set -x - ${{github.event.repository.name}}/.devcontainer/launch.sh \ - --docker \ - --cuda ${{inputs.cuda}} \ - --host ${{inputs.host}} \ - "${gpu_request[@]}" \ - --env "CI=$CI" \ - --env "VAULT_HOST=" \ - --env "COMMAND=$COMMAND" \ - --env "GITHUB_ENV=$GITHUB_ENV" \ - --env "GITHUB_SHA=$GITHUB_SHA" \ - --env "GITHUB_PATH=$GITHUB_PATH" \ - --env "GITHUB_OUTPUT=$GITHUB_OUTPUT" \ - --env "GITHUB_ACTIONS=$GITHUB_ACTIONS" \ - --env "GITHUB_REF_NAME=$GITHUB_REF_NAME" \ - --env "GITHUB_WORKSPACE=$GITHUB_WORKSPACE" \ - --env "GITHUB_REPOSITORY=$GITHUB_REPOSITORY" \ - --env "GITHUB_STEP_SUMMARY=$GITHUB_STEP_SUMMARY" \ - --volume "${{github.workspace}}/ci.sh:/ci.sh" \ - --volume "$(host_path "$RUNNER_TEMP")/.aws:/root/.aws" \ - --volume "$(dirname "$(dirname "${{github.workspace}}")"):/__w" \ - -- /ci.sh - - - name: Prepare job artifacts - shell: bash --noprofile --norc -euo pipefail {0} - run: | - echo "Prepare job artifacts" - result_dir="jobs/${{inputs.id}}" - mkdir -p "$result_dir" - - touch "$result_dir/success" - - # Finds a matching file in the repo directory and copies it to the results directory. - find_and_copy() { - filename="$1" - filepath="$(find ${{github.event.repository.name}} -name "${filename}" -print -quit)" - if [[ -z "$filepath" ]]; then - echo "${filename} does not exist in repo directory." - return 1 - fi - cp -v "$filepath" "$result_dir" - } - - find_and_copy "sccache_stats.json" || true # Ignore failures - - echo "::group::Job artifacts" - tree "$result_dir" - echo "::endgroup::" - - - name: Upload job artifacts - uses: actions/upload-artifact@v4 - with: - name: jobs-${{inputs.id}} - path: jobs - compression-level: 0 diff --git a/.github/actions/workflow-run-job-windows/action.yml b/.github/actions/workflow-run-job-windows/action.yml deleted file mode 100644 index 805beff3446..00000000000 --- a/.github/actions/workflow-run-job-windows/action.yml +++ /dev/null @@ -1,98 +0,0 @@ -name: "Run Windows Job" -description: "Run a job on a Windows runner." - -inputs: - id: - description: "A unique identifier." - required: true - command: - description: "The command to run." - required: true - image: - description: "The Docker image to use." - required: true - -runs: - using: "composite" - steps: - - name: Configure env - shell: bash --noprofile --norc -euo pipefail {0} - run: | - echo "SCCACHE_BUCKET=rapids-sccache-devs" | tee -a "${GITHUB_ENV}" - echo "SCCACHE_REGION=us-east-2" | tee -a "${GITHUB_ENV}" - echo "SCCACHE_IDLE_TIMEOUT=0" | tee -a "${GITHUB_ENV}" - echo "SCCACHE_S3_USE_SSL=true" | tee -a "${GITHUB_ENV}" - echo "SCCACHE_S3_NO_CREDENTIALS=false" | tee -a "${GITHUB_ENV}" - - name: Get AWS credentials for sccache bucket - uses: aws-actions/configure-aws-credentials@v4 - with: - role-to-assume: arn:aws:iam::279114543810:role/gha-oidc-NVIDIA - aws-region: us-east-2 - role-duration-seconds: 43200 # 12 hours - - name: Checkout repo - uses: actions/checkout@v4 - with: - path: ${{github.event.repository.name}} - persist-credentials: false - - name: Fetch ${{ inputs.image }} - shell: bash --noprofile --norc -euo pipefail {0} - run: docker pull ${{ inputs.image }} - - name: Prepare paths for docker - shell: powershell - id: paths - run: | - echo "HOST_REPO=${{ github.workspace }}\${{ github.event.repository.name }}".Replace('\', '/') | Out-File -FilePath $env:GITHUB_OUTPUT -Append - echo "MOUNT_REPO=C:/${{ github.event.repository.name }}" | Out-File -FilePath $env:GITHUB_OUTPUT -Append - cat $env:GITHUB_OUTPUT - - name: Run command # Do not change this step's name, it is checked in parse-job-times.py - shell: bash --noprofile --norc -euo pipefail {0} - run: | - docker run \ - --mount type=bind,source="${{steps.paths.outputs.HOST_REPO}}",target="${{steps.paths.outputs.MOUNT_REPO}}" \ - --workdir "${{steps.paths.outputs.MOUNT_REPO}}" \ - ${{ inputs.image }} \ - powershell -c " - [System.Environment]::SetEnvironmentVariable('AWS_ACCESS_KEY_ID','${{env.AWS_ACCESS_KEY_ID}}'); - [System.Environment]::SetEnvironmentVariable('AWS_SECRET_ACCESS_KEY','${{env.AWS_SECRET_ACCESS_KEY}}'); - [System.Environment]::SetEnvironmentVariable('AWS_SESSION_TOKEN','${{env.AWS_SESSION_TOKEN }}'); - [System.Environment]::SetEnvironmentVariable('SCCACHE_BUCKET','${{env.SCCACHE_BUCKET}}'); - [System.Environment]::SetEnvironmentVariable('SCCACHE_REGION','${{env.SCCACHE_REGION}}'); - [System.Environment]::SetEnvironmentVariable('SCCACHE_IDLE_TIMEOUT','${{env.SCCACHE_IDLE_TIMEOUT}}'); - [System.Environment]::SetEnvironmentVariable('SCCACHE_S3_USE_SSL','${{env.SCCACHE_S3_USE_SSL}}'); - [System.Environment]::SetEnvironmentVariable('SCCACHE_S3_NO_CREDENTIALS','${{env.SCCACHE_S3_NO_CREDENTIALS}}'); - git config --global --add safe.directory '${{steps.paths.outputs.MOUNT_REPO}}'; - ${{inputs.command}}" - - name: Prepare job artifacts - shell: bash --noprofile --norc -euo pipefail {0} - id: done - run: | - echo "SUCCESS=true" | tee -a "${GITHUB_OUTPUT}" - - result_dir="jobs/${{inputs.id}}" - mkdir -p "$result_dir" - - touch "$result_dir/success" - - # Finds a matching file in the repo directory and copies it to the results directory. - find_and_copy() { - filename="$1" - filepath="$(find ${{github.event.repository.name}} -name "${filename}" -print -quit)" - if [[ -z "$filepath" ]]; then - echo "${filename} does not exist in repo directory." - return 1 - fi - cp -v "$filepath" "$result_dir" - } - - find_and_copy "sccache_stats.json" || true # Ignore failures - - echo "::group::Job artifacts" - find "$result_dir" # Tree not available in this image. - echo "::endgroup::" - - - name: Upload job artifacts - uses: actions/upload-artifact@v4 - with: - name: jobs-${{inputs.id}} - path: jobs - compression-level: 0 diff --git a/.github/workflows/ci-workflow-nightly.yml b/.github/workflows/ci-workflow-nightly.yml index fdf281b8063..5ec8955eb24 100644 --- a/.github/workflows/ci-workflow-nightly.yml +++ b/.github/workflows/ci-workflow-nightly.yml @@ -44,7 +44,7 @@ jobs: persist-credentials: false - name: Build workflow id: build-workflow - uses: ./.github/actions/workflow-build + uses: NVIDIA/cccl-gha/actions/workflow-build@v1 with: workflows: nightly slack_token: ${{ secrets.SLACK_NOTIFIER_BOT_TOKEN }} @@ -62,7 +62,7 @@ jobs: fail-fast: false matrix: name: ${{ fromJSON(needs.build-workflow.outputs.workflow)['linux_two_stage']['keys'] }} - uses: ./.github/workflows/workflow-dispatch-two-stage-group-linux.yml + uses: NVIDIA/cccl-gha/workflows/workflow-dispatch-two-stage-group-linux.yml@v1 with: pc-array: ${{ toJSON(fromJSON(needs.build-workflow.outputs.workflow)['linux_two_stage']['jobs'][matrix.name]) }} @@ -77,7 +77,7 @@ jobs: fail-fast: false matrix: name: ${{ fromJSON(needs.build-workflow.outputs.workflow)['windows_two_stage']['keys'] }} - uses: ./.github/workflows/workflow-dispatch-two-stage-group-windows.yml + uses: NVIDIA/cccl-gha/workflows/workflow-dispatch-two-stage-group-windows.yml@v1 with: pc-array: ${{ toJSON(fromJSON(needs.build-workflow.outputs.workflow)['windows_two_stage']['jobs'][matrix.name]) }} @@ -92,7 +92,7 @@ jobs: fail-fast: false matrix: name: ${{ fromJSON(needs.build-workflow.outputs.workflow)['linux_standalone']['keys'] }} - uses: ./.github/workflows/workflow-dispatch-standalone-group-linux.yml + uses: NVIDIA/cccl-gha/workflows/workflow-dispatch-standalone-group-linux.yml@v1 with: job-array: ${{ toJSON(fromJSON(needs.build-workflow.outputs.workflow)['linux_standalone']['jobs'][matrix.name]) }} @@ -107,7 +107,7 @@ jobs: fail-fast: false matrix: name: ${{ fromJSON(needs.build-workflow.outputs.workflow)['windows_standalone']['keys'] }} - uses: ./.github/workflows/workflow-dispatch-standalone-group-windows.yml + uses: NVIDIA/cccl-gha/workflows/workflow-dispatch-standalone-group-windows.yml@v1 with: job-array: ${{ toJSON(fromJSON(needs.build-workflow.outputs.workflow)['windows_standalone']['jobs'][matrix.name]) }} @@ -131,7 +131,7 @@ jobs: - name: Check workflow success id: check-workflow - uses: ./.github/actions/workflow-results + uses: NVIDIA/cccl-gha/actions/workflow-results@v1 with: github_token: ${{ secrets.GITHUB_TOKEN }} slack_token: ${{ secrets.SLACK_NOTIFIER_BOT_TOKEN }} diff --git a/.github/workflows/ci-workflow-pull-request.yml b/.github/workflows/ci-workflow-pull-request.yml index 06cd639164b..580b3a104aa 100644 --- a/.github/workflows/ci-workflow-pull-request.yml +++ b/.github/workflows/ci-workflow-pull-request.yml @@ -56,7 +56,7 @@ jobs: - name: Build workflow if: ${{ !contains(github.event.head_commit.message, '[skip-matrix]') }} id: build-workflow - uses: ./.github/actions/workflow-build + uses: NVIDIA/cccl-gha/actions/workflow-build@v1 env: pr_worflow: ${{ !contains(github.event.head_commit.message, '[workflow:!pull_request]') && 'pull_request' || '' }} nightly_workflow: ${{ contains(github.event.head_commit.message, '[workflow:nightly]') && 'nightly' || '' }} @@ -81,7 +81,7 @@ jobs: fail-fast: false matrix: name: ${{ fromJSON(needs.build-workflow.outputs.workflow)['linux_two_stage']['keys'] }} - uses: ./.github/workflows/workflow-dispatch-two-stage-group-linux.yml + uses: NVIDIA/cccl-gha/workflows/workflow-dispatch-two-stage-group-linux.yml@v1 with: pc-array: ${{ toJSON(fromJSON(needs.build-workflow.outputs.workflow)['linux_two_stage']['jobs'][matrix.name]) }} @@ -98,7 +98,7 @@ jobs: fail-fast: false matrix: name: ${{ fromJSON(needs.build-workflow.outputs.workflow)['windows_two_stage']['keys'] }} - uses: ./.github/workflows/workflow-dispatch-two-stage-group-windows.yml + uses: NVIDIA/cccl-gha/workflows/workflow-dispatch-two-stage-group-windows.yml@v1 with: pc-array: ${{ toJSON(fromJSON(needs.build-workflow.outputs.workflow)['windows_two_stage']['jobs'][matrix.name]) }} @@ -115,7 +115,7 @@ jobs: fail-fast: false matrix: name: ${{ fromJSON(needs.build-workflow.outputs.workflow)['linux_standalone']['keys'] }} - uses: ./.github/workflows/workflow-dispatch-standalone-group-linux.yml + uses: NVIDIA/cccl-gha/workflows/workflow-dispatch-standalone-group-linux.yml@v1 with: job-array: ${{ toJSON(fromJSON(needs.build-workflow.outputs.workflow)['linux_standalone']['jobs'][matrix.name]) }} @@ -132,7 +132,7 @@ jobs: fail-fast: false matrix: name: ${{ fromJSON(needs.build-workflow.outputs.workflow)['windows_standalone']['keys'] }} - uses: ./.github/workflows/workflow-dispatch-standalone-group-windows.yml + uses: NVIDIA/cccl-gha/workflows/workflow-dispatch-standalone-group-windows.yml@v1 with: job-array: ${{ toJSON(fromJSON(needs.build-workflow.outputs.workflow)['windows_standalone']['jobs'][matrix.name]) }} @@ -157,7 +157,7 @@ jobs: - name: Check workflow success id: check-workflow - uses: ./.github/actions/workflow-results + uses: NVIDIA/cccl-gha/actions/workflow-results@v1 with: github_token: ${{ secrets.GITHUB_TOKEN }} pr_number: ${{ needs.build-workflow.outputs.pr_number }} @@ -169,7 +169,7 @@ jobs: permissions: id-token: write contents: read - uses: ./.github/workflows/verify-devcontainers.yml + uses: NVIDIA/cccl-gha/workflows/verify-devcontainers.yml@v1 with: base_sha: ${{ needs.build-workflow.outputs.base_sha }} diff --git a/.github/workflows/verify-devcontainers.yml b/.github/workflows/verify-devcontainers.yml deleted file mode 100644 index 1e8733de801..00000000000 --- a/.github/workflows/verify-devcontainers.yml +++ /dev/null @@ -1,141 +0,0 @@ -name: Verify devcontainers - -on: - workflow_call: - inputs: - base_sha: - type: string - description: 'For PRs, set the base SHA to conditionally run this workflow only when relevant files are modified.' - required: false - - -defaults: - run: - shell: bash -euo pipefail {0} - -permissions: - contents: read - -jobs: - get-devcontainer-list: - name: Verify devcontainer files are up-to-date - outputs: - skip: ${{ steps.inspect-changes.outputs.skip }} - devcontainers: ${{ steps.get-list.outputs.devcontainers }} - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - persist-credentials: false - - name: Setup jq and yq - run: | - sudo apt-get update - sudo apt-get install jq -y - sudo wget -O /usr/local/bin/yq https://github.com/mikefarah/yq/releases/download/v4.34.2/yq_linux_amd64 - sudo chmod +x /usr/local/bin/yq - - name: Run the script to generate devcontainer files - run: | - ./.devcontainer/make_devcontainers.sh --verbose --clean - - name: Check for changes - run: | - if [[ $(git diff --stat) != '' || $(git status --porcelain | grep '^??') != '' ]]; then - git diff --minimal - git status --porcelain - echo "::error:: Dev Container files are out of date or there are untracked files. Run the .devcontainer/make_devcontainers.sh script and commit the changes." - exit 1 - else - echo "::note::Dev Container files are up-to-date." - fi - - name: Inspect changes - if: ${{ inputs.base_sha != '' }} - id: inspect-changes - env: - BASE_SHA: ${{ inputs.base_sha }} - run: | - echo "Fetch history and determine merge base..." - git fetch origin --unshallow -q - git fetch origin $BASE_SHA -q - merge_base_sha=$(git merge-base $GITHUB_SHA $BASE_SHA) - - echo "Head SHA: $GITHUB_SHA" - echo "PR Base SHA: $BASE_SHA" - echo "Merge Base SHA: $merge_base_sha" - - echo "Checking for changes to devcontainer/matrix files..." - - all_dirty_files=$(git diff --name-only "${merge_base_sha}" "${GITHUB_SHA}") - echo "::group::All dirty files" - echo "${all_dirty_files}" - echo "::endgroup::" - - file_regex="^(.devcontainer|ci/matrix.yaml|.github/actions/workflow-build/build-workflow.py)" - echo "Regex: ${file_regex}" - - relevant_dirty_files=$(echo "${all_dirty_files}" | grep -E "${file_regex}" || true) - echo "::group::Relevant dirty files" - echo "${relevant_dirty_files}" - echo "::endgroup::" - - if [[ -z "${relevant_dirty_files}" ]]; then - echo "No relevant changes detected. Skipping devcontainer testing." - echo "skip=true" >> $GITHUB_OUTPUT - else - echo "Detected relevant changes. Continuing." - echo "skip=false" >> $GITHUB_OUTPUT - fi - - name: Get list of devcontainer.json paths and names - if: ${{ steps.inspect-changes.outputs.skip != 'true' }} - id: get-list - run: | - devcontainers=$(find .devcontainer/ -name 'devcontainer.json' | while read -r devcontainer; do - jq --arg path "$devcontainer" '{path: $path, name: .name}' "$devcontainer" - done | jq -s -c .) - echo "devcontainers=${devcontainers}" | tee --append "${GITHUB_OUTPUT}" - - verify-devcontainers: - name: ${{matrix.devcontainer.name}} - needs: get-devcontainer-list - if: ${{ needs.get-devcontainer-list.outputs.skip != 'true' }} - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - devcontainer: ${{fromJson(needs.get-devcontainer-list.outputs.devcontainers)}} - permissions: - id-token: write - contents: read - steps: - - name: Check out the code - uses: actions/checkout@v4 - with: - persist-credentials: false - - # We don't really need sccache configured, but we need the AWS credentials envvars to be set - # in order to avoid the devcontainer hanging waiting for GitHub authentication - - name: Get AWS credentials for sccache bucket - uses: aws-actions/configure-aws-credentials@v4 - with: - role-to-assume: arn:aws:iam::279114543810:role/gha-oidc-NVIDIA - aws-region: us-east-2 - role-duration-seconds: 43200 # 12 hours - - name: Set environment variables - run: | - echo "SCCACHE_BUCKET=rapids-sccache-devs" >> $GITHUB_ENV - echo "SCCACHE_REGION=us-east-2" >> $GITHUB_ENV - echo "SCCACHE_IDLE_TIMEOUT=32768" >> $GITHUB_ENV - echo "SCCACHE_S3_USE_SSL=true" >> $GITHUB_ENV - echo "SCCACHE_S3_NO_CREDENTIALS=false" >> $GITHUB_ENV - - - name: Run in devcontainer - uses: devcontainers/ci@v0.3 - with: - push: never - configFile: ${{ matrix.devcontainer.path }} - env: | - SCCACHE_REGION=${{ env.SCCACHE_REGION }} - AWS_ACCESS_KEY_ID=${{ env.AWS_ACCESS_KEY_ID }} - AWS_SESSION_TOKEN=${{ env.AWS_SESSION_TOKEN }} - AWS_SECRET_ACCESS_KEY=${{ env.AWS_SECRET_ACCESS_KEY }} - runCmd: | - .devcontainer/verify_devcontainer.sh diff --git a/.github/workflows/workflow-dispatch-standalone-group-linux.yml b/.github/workflows/workflow-dispatch-standalone-group-linux.yml deleted file mode 100644 index e0216b7c6d9..00000000000 --- a/.github/workflows/workflow-dispatch-standalone-group-linux.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: "Workflow/Dispatch/StandaloneGroup/Linux" - -defaults: - run: - shell: bash --noprofile --norc -euo pipefail {0} - -on: - workflow_call: - inputs: - job-array: - description: "The dispatch.json's linux_standalone.jobs. array of dispatch jobs." - type: string - required: true - -jobs: - run-jobs: - name: "${{ matrix.name }}" - strategy: - fail-fast: false - matrix: - include: ${{ fromJSON(inputs.job-array) }} - permissions: - id-token: write - contents: read - runs-on: ${{ matrix.runner }} - container: - image: cruizba/ubuntu-dind:jammy-26.1.3 # See workflow-run-job-linux for details - env: - NVIDIA_VISIBLE_DEVICES: ${{env.NVIDIA_VISIBLE_DEVICES}} - steps: - - name: Checkout repo - uses: actions/checkout@v4 - with: - persist-credentials: false - - name: Run job - uses: ./.github/actions/workflow-run-job-linux - with: - id: ${{ matrix.id }} - command: ${{ matrix.command }} - image: ${{ matrix.image }} - runner: ${{ matrix.runner }} - cuda: ${{ matrix.cuda }} - host: ${{ matrix.host }} diff --git a/.github/workflows/workflow-dispatch-standalone-group-windows.yml b/.github/workflows/workflow-dispatch-standalone-group-windows.yml deleted file mode 100644 index 454ad6345d3..00000000000 --- a/.github/workflows/workflow-dispatch-standalone-group-windows.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: "Workflow/Dispatch/StandaloneGroup/Windows" - -defaults: - run: - shell: bash --noprofile --norc -euo pipefail {0} - -on: - workflow_call: - inputs: - job-array: - description: "The dispatch.json's windows_standalone.jobs. array of dispatch jobs." - type: string - required: true - -jobs: - run-jobs: - name: ${{ matrix.name }} - runs-on: ${{ matrix.runner }} - permissions: - id-token: write - contents: read - strategy: - fail-fast: false - matrix: - include: ${{ fromJSON(inputs.job-array) }} - steps: - - name: Checkout repo - uses: actions/checkout@v4 - with: - persist-credentials: false - - name: Run job - uses: ./.github/actions/workflow-run-job-windows - with: - id: ${{ matrix.id }} - command: ${{ matrix.command }} - image: ${{ matrix.image }} diff --git a/.github/workflows/workflow-dispatch-two-stage-group-linux.yml b/.github/workflows/workflow-dispatch-two-stage-group-linux.yml deleted file mode 100644 index c84f5c4af6f..00000000000 --- a/.github/workflows/workflow-dispatch-two-stage-group-linux.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: "Workflow/Dispatch/TwoStageGroup/Linux" - -defaults: - run: - shell: bash --noprofile --norc -euo pipefail {0} - -on: - workflow_call: - inputs: - pc-array: - description: "The dispatch.json's linux_two_stage.jobs. array of producer/consumer chains." - type: string - required: true - -jobs: - dispatch-pcs: - name: ${{ matrix.id }} - permissions: - id-token: write - contents: read - strategy: - fail-fast: false - matrix: - include: ${{ fromJSON(inputs.pc-array) }} - uses: ./.github/workflows/workflow-dispatch-two-stage-linux.yml - with: - producers: ${{ toJSON(matrix.producers) }} - consumers: ${{ toJSON(matrix.consumers) }} diff --git a/.github/workflows/workflow-dispatch-two-stage-group-windows.yml b/.github/workflows/workflow-dispatch-two-stage-group-windows.yml deleted file mode 100644 index b6c6f1c25af..00000000000 --- a/.github/workflows/workflow-dispatch-two-stage-group-windows.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: "Workflow/Dispatch/TwoStageGroup/Windows" - -defaults: - run: - shell: bash --noprofile --norc -euo pipefail {0} - -on: - workflow_call: - inputs: - pc-array: - description: "The dispatch.json's windows_two_stage.jobs. array of producer/consumer chains." - type: string - required: true - -jobs: - dispatch-pcs: - name: ${{ matrix.id }} - permissions: - id-token: write - contents: read - strategy: - fail-fast: false - matrix: - include: ${{ fromJSON(inputs.pc-array) }} - uses: ./.github/workflows/workflow-dispatch-two-stage-windows.yml - with: - producers: ${{ toJSON(matrix.producers) }} - consumers: ${{ toJSON(matrix.consumers) }} diff --git a/.github/workflows/workflow-dispatch-two-stage-linux.yml b/.github/workflows/workflow-dispatch-two-stage-linux.yml deleted file mode 100644 index f1d9d518d3e..00000000000 --- a/.github/workflows/workflow-dispatch-two-stage-linux.yml +++ /dev/null @@ -1,75 +0,0 @@ -name: "Workflow/Dispatch/TwoStage/Linux" - -defaults: - run: - shell: bash --noprofile --norc -euo pipefail {0} - -on: - workflow_call: - inputs: - producers: - description: "The dispatch.json's linux_two_stage.jobs.[*].producers array." - type: string - required: true - consumers: - description: "The dispatch.json's linux_two_stage.jobs.[*].consumers array." - type: string - required: true - -jobs: - # Accumulating results from multiple producers is not easily implemented. For now, only a single producer is supported. - # The build-workflow.py script will emit an error if more than one producer is specified. - producer: - name: ${{ fromJSON(inputs.producers)[0].name }} - runs-on: ${{ fromJSON(inputs.producers)[0].runner }} - permissions: - id-token: write - contents: read - container: - image: cruizba/ubuntu-dind:jammy-26.1.3 # See workflow-run-job-linux for details - env: - NVIDIA_VISIBLE_DEVICES: ${{env.NVIDIA_VISIBLE_DEVICES}} - steps: - - name: Checkout repo - uses: actions/checkout@v4 - with: - persist-credentials: false - - name: Run job - uses: ./.github/actions/workflow-run-job-linux - with: - id: ${{ fromJSON(inputs.producers)[0].id }} - command: ${{ fromJSON(inputs.producers)[0].command }} - image: ${{ fromJSON(inputs.producers)[0].image }} - runner: ${{ fromJSON(inputs.producers)[0].runner }} - cuda: ${{ fromJSON(inputs.producers)[0].cuda }} - host: ${{ fromJSON(inputs.producers)[0].host }} - - consumers: - name: "${{ matrix.name }}" - needs: producer - runs-on: ${{ matrix.runner }} - permissions: - id-token: write - contents: read - container: - image: cruizba/ubuntu-dind:jammy-26.1.3 # See workflow-run-job-linux for details - env: - NVIDIA_VISIBLE_DEVICES: ${{env.NVIDIA_VISIBLE_DEVICES}} - strategy: - fail-fast: false - matrix: - include: ${{ fromJSON(inputs.consumers) }} - steps: - - name: Checkout repo - uses: actions/checkout@v4 - with: - persist-credentials: false - - name: Run job - uses: ./.github/actions/workflow-run-job-linux - with: - id: ${{ matrix.id }} - command: ${{ matrix.command }} - image: ${{ matrix.image }} - runner: ${{ matrix.runner }} - cuda: ${{ matrix.cuda }} - host: ${{ matrix.host }} diff --git a/.github/workflows/workflow-dispatch-two-stage-windows.yml b/.github/workflows/workflow-dispatch-two-stage-windows.yml deleted file mode 100644 index 22a5bb4ed22..00000000000 --- a/.github/workflows/workflow-dispatch-two-stage-windows.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: "Workflow/Dispatch/TwoStage/Windows" - -defaults: - run: - shell: bash --noprofile --norc -euo pipefail {0} - -on: - workflow_call: - inputs: - producers: - description: "The dispatch.json's windows_two_stage.jobs.[*].producers array." - type: string - required: true - consumers: - description: "The dispatch.json's windows_two_stage.jobs.[*].consumers array." - type: string - required: true - -jobs: - # Accumulating results from multiple producers is not easily implemented. For now, only a single producer is supported. - # The build-workflow.py script will emit an error if more than one producer is specified. - producer: - name: ${{ fromJSON(inputs.producers)[0].name }} - runs-on: ${{ fromJSON(inputs.producers)[0].runner }} - permissions: - id-token: write - contents: read - steps: - - name: Checkout repo - uses: actions/checkout@v4 - with: - persist-credentials: false - - name: Run job - uses: ./.github/actions/workflow-run-job-windows - with: - id: ${{ fromJSON(inputs.producers)[0].id }} - command: ${{ fromJSON(inputs.producers)[0].command }} - image: ${{ fromJSON(inputs.producers)[0].image }} - - consumers: - name: ${{ matrix.name }} - needs: producer - runs-on: ${{ matrix.runner }} - permissions: - id-token: write - contents: read - strategy: - fail-fast: false - matrix: - include: ${{ fromJSON(inputs.consumers) }} - steps: - - name: Checkout repo - uses: actions/checkout@v4 - with: - persist-credentials: false - - name: Run job - uses: ./.github/actions/workflow-run-job-windows - with: - id: ${{ matrix.id }} - command: ${{ matrix.command }} - image: ${{ matrix.image }}