diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..1377554e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.swp diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 84e1fcab..339ee8ff 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,3 +13,9 @@ repos: rev: v1.7.4 hooks: - id: actionlint-docker + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.8.3 + hooks: + - id: ruff + args: ["--fix"] + - id: ruff-format diff --git a/README.md b/README.md index aecbad81..35e2e523 100644 --- a/README.md +++ b/README.md @@ -81,4 +81,4 @@ jobs: id: telemetry-setup # DO NOT change this in PRs uses: rapidsai/shared-actions/dispatch-script@main -``` \ No newline at end of file +``` diff --git a/check_nightly_success/check-nightly-success/action.yaml b/check_nightly_success/check-nightly-success/action.yaml new file mode 100644 index 00000000..2ad52971 --- /dev/null +++ b/check_nightly_success/check-nightly-success/action.yaml @@ -0,0 +1,36 @@ +name: check-nightly-success +description: Check if the nightlies have succeeded recently. +inputs: + repo: + description: "The repository to check" + required: true + type: string + repo_owner: + description: "The org that owns the repo (default: rapidsai)" + required: false + default: "rapidsai" + type: string + workflow_id: + description: "The workflow whose runs to check" + required: false + default: "test.yaml" + type: string + max_days_without_success: + description: "The number of consecutive days that may go by without a successful CI run" + required: false + default: 7 + type: integer + +runs: + using: composite + steps: + - name: Run the Python script + shell: bash + env: + REPO: ${{ inputs.repo }} + REPO_OWNER: ${{ inputs.repo_owner }} + WORKFLOW_ID: ${{ inputs.workflow_id }} + MAX_DAYS_WITHOUT_SUCCESS: ${{ inputs.max_days_without_success }} + run: | + python -m pip install requests + python shared-actions/check_nightly_success/check-nightly-success/check.py ${REPO} --repo-owner ${REPO_OWNER} --workflow-id ${WORKFLOW_ID} --max-days-without-success ${MAX_DAYS_WITHOUT_SUCCESS} diff --git a/check_nightly_success/check-nightly-success/check.py b/check_nightly_success/check-nightly-success/check.py new file mode 100644 index 00000000..5536706b --- /dev/null +++ b/check_nightly_success/check-nightly-success/check.py @@ -0,0 +1,97 @@ +"""Check whether a GHA workflow has run successfully in the last N days.""" +# ruff: noqa: INP001 + +import argparse +import itertools +import os +import re +import sys +from datetime import datetime + +import requests + +# Constants +GITHUB_TOKEN = os.environ["RAPIDS_GH_TOKEN"] +GOOD_STATUSES = {"success"} + + +def main( + repo: str, + repo_owner: str, + workflow_id: str, + max_days_without_success: int, + num_attempts: int = 5, +) -> bool: + """Check whether a GHA workflow has run successfully in the last N days.""" + headers = {"Authorization": f"token {GITHUB_TOKEN}"} + url = f"https://api.github.com/repos/{repo_owner}/{repo}/actions/workflows/{workflow_id}/runs" + exceptions = [] + for _ in range(num_attempts): + try: + response = requests.get(url, headers=headers, timeout=10) + response.raise_for_status() + break + except requests.RequestException as e: + exceptions.append(e) + else: + sep = "\n\t" + msg = ( + f"Failed to fetch {url} after {num_attempts} attempts with the following " + f"errors: {sep}{'{sep}'.join(exceptions)}" + ) + raise RuntimeError(msg) + + runs = response.json()["workflow_runs"] + tz = datetime.fromisoformat(runs[0]["run_started_at"]).tzinfo + now = datetime.now(tz=tz) + + branch_ok = {} + for branch, branch_runs in itertools.groupby(runs, key=lambda r: r["head_branch"]): + if not re.match("branch-[0-9]{2}.[0-9]{2}", branch): + continue + + branch_ok[branch] = False + for run in sorted(branch_runs, key=lambda r: r["run_started_at"], reverse=True): + if ( + now - datetime.fromisoformat(run["run_started_at"]) + ).days > max_days_without_success: + break + if run["conclusion"] in GOOD_STATUSES: + branch_ok[branch] = True + break + + failed_branches = [k for k, v in branch_ok.items() if not v] + if failed_branches: + print( # noqa: T201 + f"Branches with no successful runs of {workflow_id} in the last " + f"{max_days_without_success} days: " + f"{', '.join(failed_branches)}", + ) + return len(failed_branches) > 0 + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("repo", type=str, help="Repository name") + parser.add_argument( + "--repo-owner", + default="rapidsai", + help="Repository organization/owner", + ) + parser.add_argument("--workflow-id", default="test.yaml", help="Workflow ID") + parser.add_argument( + "--max-days-without-success", + type=int, + default=7, + help="Maximum number of days without a successful run", + ) + args = parser.parse_args() + + sys.exit( + main( + args.repo, + args.repo_owner, + args.workflow_id, + args.max_days_without_success, + ), + ) diff --git a/check_nightly_success/dispatch/action.yml b/check_nightly_success/dispatch/action.yml new file mode 100644 index 00000000..e1faa40b --- /dev/null +++ b/check_nightly_success/dispatch/action.yml @@ -0,0 +1,39 @@ +name: dispatch-check-nightly-success +description: Clone shared-actions and dispatch to the check-nightly-success action. +inputs: + repo: + description: "The repository to check" + required: true + type: string + repo_owner: + description: "The org that owns the repo (default: rapidsai)" + required: false + default: "rapidsai" + type: string + workflow_id: + description: "The workflow whose runs to check" + required: false + default: "test.yaml" + type: string + max_days_without_success: + description: "The number of consecutive days that may go by without a successful CI run" + required: false + default: 7 + type: integer + +runs: + using: 'composite' + steps: + - name: Clone shared-actions repo + uses: actions/checkout@v4 + with: + repository: ${{ env.SHARED_ACTIONS_REPO || 'rapidsai/shared-actions' }} + ref: ${{ env.SHARED_ACTIONS_REF || 'main' }} + path: ./shared-actions + - name: Run check-nightly-success + uses: ./shared-actions/check_nightly_success/check-nightly-success + with: + repo: ${{ inputs.repo }} + repo_owner: ${{ inputs.repo_owner }} + workflow_id: ${{ inputs.workflow_id }} + max_days_without_success: ${{ inputs.max_days_without_success }} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..08d54a55 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,17 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. + +[tool.ruff] +target-version = "py310" + +[tool.ruff.lint] +select = ["ALL"] +ignore = [ + # Incompatible with D211 + "D203", + # Incompatible with D213 + "D213", + # Incompatible with ruff-format + "COM812", + "ISC001", +] +fixable = ["ALL"]