-
-
Notifications
You must be signed in to change notification settings - Fork 700
chore: add some skills and update agents doc #3696
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| --- | ||
| name: buildkite-get-results | ||
| description: Gets buildkite build results | ||
| --- | ||
|
|
||
| Pass the PR number to the `scripts/get_buildkite_results.py` script. | ||
|
|
||
| The `--jobs` flag can do glob-style filtering of jobs. | ||
|
|
||
| The `--download` flag will download job logs. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,203 @@ | ||
| #!/usr/bin/env python3 | ||
| import argparse | ||
| import json | ||
| import re | ||
| import subprocess | ||
| import sys | ||
| import urllib.request | ||
|
|
||
|
|
||
| def get_pr_checks(pr_number): | ||
| try: | ||
| # Check if gh is installed | ||
| subprocess.run( | ||
| ["gh", "--version"], | ||
| check=True, | ||
| stdout=subprocess.DEVNULL, | ||
| stderr=subprocess.DEVNULL, | ||
| ) | ||
| except FileNotFoundError: | ||
| print( | ||
| "Error: 'gh' (GitHub CLI) is not installed or not in PATH.", file=sys.stderr | ||
| ) | ||
| sys.exit(1) | ||
| except subprocess.CalledProcessError: | ||
| print("Error: 'gh' command failed. Is it installed?", file=sys.stderr) | ||
| sys.exit(1) | ||
|
|
||
| cmd = ["gh", "pr", "checks", str(pr_number), "--json", "bucket,name,link,state"] | ||
| try: | ||
| result = subprocess.run(cmd, capture_output=True, text=True, check=True) | ||
| return json.loads(result.stdout) | ||
| except subprocess.CalledProcessError as e: | ||
| print(f"Error fetching PR checks: {e.stderr}", file=sys.stderr) | ||
| sys.exit(1) | ||
|
|
||
|
|
||
| def get_buildkite_build_url(checks): | ||
| for check in checks: | ||
| # Looking for Buildkite check. The name usually contains "buildkite" | ||
| if "buildkite" in check.get("name", "").lower(): | ||
| return check.get("link") | ||
| return None | ||
|
|
||
|
|
||
| def fetch_buildkite_data(build_url): | ||
| # Convert https://buildkite.com/org/pipeline/builds/number | ||
| # to https://buildkite.com/org/pipeline/builds/number.json | ||
| if not build_url.endswith(".json"): | ||
| json_url = build_url + ".json" | ||
| else: | ||
| json_url = build_url | ||
|
|
||
| try: | ||
| with urllib.request.urlopen(json_url) as response: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| if response.status != 200: | ||
| print( | ||
| f"Error fetching data from {json_url}: Status {response.status}", | ||
| file=sys.stderr, | ||
| ) | ||
| return None | ||
| return json.loads(response.read().decode()) | ||
| except Exception as e: | ||
| print(f"Error fetching data from {json_url}: {e}", file=sys.stderr) | ||
| return None | ||
|
|
||
|
|
||
| def download_log(job_url, output_path): | ||
| # Construct raw log URL: job_url + "/raw" (Buildkite convention) | ||
| # job_url e.g. https://buildkite.com/org/pipeline/builds/14394#job-id | ||
| # Wait, the job['path'] gives /org/pipeline/builds/14394#job-id | ||
| # We want /org/pipeline/builds/14394/jobs/job-id/raw? No | ||
| # The clean URL for a job is https://buildkite.com/org/pipeline/builds/14394/jobs/job-id | ||
| # And raw log is https://buildkite.com/org/pipeline/builds/14394/jobs/job-id/raw | ||
|
|
||
| # We have full_url e.g. https://buildkite.com/bazel/rules-python-python/builds/14394#019c5cf9-e3cf-468f-a7b1-8f9f5ad4b08c | ||
| # We need to transform it. | ||
|
|
||
| if "#" in job_url: | ||
| base, job_id = job_url.split("#") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| # Ensure base doesn't end with / | ||
| if base.endswith("/"): | ||
| base = base[:-1] | ||
|
|
||
| # Build raw URL | ||
| raw_url = f"{base}/jobs/{job_id}/raw" | ||
| else: | ||
| print(f"Could not parse job URL for download: {job_url}", file=sys.stderr) | ||
| return False | ||
|
|
||
| try: | ||
| with urllib.request.urlopen(raw_url) as response: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| if response.status != 200: | ||
| print( | ||
| f"Error downloading log from {raw_url}: Status {response.status}", | ||
| file=sys.stderr, | ||
| ) | ||
| return False | ||
| with open(output_path, "wb") as f: | ||
| f.write(response.read()) | ||
| return True | ||
| except Exception as e: | ||
| print(f"Error downloading log from {raw_url}: {e}", file=sys.stderr) | ||
| return False | ||
|
|
||
|
|
||
| def main(): | ||
| parser = argparse.ArgumentParser(description="Get Buildkite CI results for a PR.") | ||
| parser.add_argument("pr_number", help="The PR number.") | ||
| parser.add_argument( | ||
| "--jobs", | ||
| action="append", | ||
| help="Filter by job name (regex match). Can be specified multiple times.", | ||
| ) | ||
| parser.add_argument( | ||
| "--download", | ||
| action="store_true", | ||
| help="If exactly one job is matched, download its log to a local file.", | ||
| ) | ||
|
|
||
| args = parser.parse_args() | ||
|
|
||
| print(f"Fetching checks for PR #{args.pr_number}...", file=sys.stderr) | ||
| checks = get_pr_checks(args.pr_number) | ||
|
|
||
| build_url = get_buildkite_build_url(checks) | ||
| if not build_url: | ||
| print("No Buildkite check found for this PR.", file=sys.stderr) | ||
| sys.exit(1) | ||
|
|
||
| print(f"Found Buildkite URL: {build_url}", file=sys.stderr) | ||
|
|
||
| data = fetch_buildkite_data(build_url) | ||
| if not data: | ||
| sys.exit(1) | ||
|
|
||
| print(f"Build State: {data.get('state')}") | ||
| print("-" * 40) | ||
|
|
||
| jobs = data.get("jobs", []) | ||
|
|
||
| filtered_jobs = [] | ||
| if args.jobs: | ||
| for job in jobs: | ||
| job_name = job.get("name") | ||
| if not job_name: | ||
| continue | ||
| for pattern in args.jobs: | ||
| if re.search(pattern, job_name, re.IGNORECASE): | ||
| filtered_jobs.append(job) | ||
| break | ||
| else: | ||
| filtered_jobs = jobs | ||
|
|
||
| for job in filtered_jobs: | ||
| name = job.get("name", "Unknown") | ||
| state = job.get("state", "Unknown") | ||
| path = job.get("path") | ||
| full_url = f"https://buildkite.com{path}" if path else "N/A" | ||
|
|
||
| passed = job.get("passed", False) | ||
| outcome = job.get("outcome") | ||
|
|
||
| if passed: | ||
| result_str = "PASSED" | ||
| elif outcome: | ||
| result_str = outcome.upper() | ||
| else: | ||
| result_str = state.upper() | ||
|
|
||
| print(f"Job: {name}") | ||
| print(f" Result: {result_str}") | ||
| print(f" URL: {full_url}") | ||
| print("") | ||
|
|
||
| if args.download: | ||
| if len(filtered_jobs) == 1: | ||
| job = filtered_jobs[0] | ||
| name = job.get("name", "unknown_job") | ||
| # Sanitize name for filename | ||
| safe_name = re.sub(r"[^a-zA-Z0-9_\-]", "_", name) | ||
| output_path = f"{safe_name}.log" | ||
|
|
||
| path = job.get("path") | ||
| if path: | ||
| full_url = f"https://buildkite.com{path}" | ||
| print(f"Downloading log for '{name}'...", file=sys.stderr) | ||
| if download_log(full_url, output_path): | ||
| print(f"Downloaded log to: {output_path}") | ||
| else: | ||
| print("Failed to download log.", file=sys.stderr) | ||
| else: | ||
| print("Job has no URL path, cannot download.", file=sys.stderr) | ||
| elif len(filtered_jobs) == 0: | ||
| print("No jobs matched to download.", file=sys.stderr) | ||
| else: | ||
| print( | ||
| f"Matched {len(filtered_jobs)} jobs. Please filter to exactly one job to download.", | ||
| file=sys.stderr, | ||
| ) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| --- | ||
| name: buildkite-retry-job | ||
| description: Retry a failed build kite job | ||
| --- | ||
|
|
||
| Use `scripts/retry_buildkite_jobs.py` to retry a job. This is best used | ||
| when there are network failures. | ||
|
|
||
| example: | ||
|
|
||
| ``` | ||
| retry_buildkite_jobs.py org pipeline build | ||
| ``` | ||
|
|
||
| The `--jobs` flag can be used to retry specific jobs. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| #!/usr/bin/env python3 | ||
| import argparse | ||
| import json | ||
| import os | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| import sys | ||
| import urllib.request | ||
| from urllib.error import HTTPError | ||
|
|
||
|
|
||
| def make_request(url, method="GET", data=None, token=None): | ||
| headers = { | ||
| "Authorization": f"Bearer {token}", | ||
| "Accept": "application/json", | ||
| } | ||
| if data: | ||
| data = json.dumps(data).encode("utf-8") | ||
| headers["Content-Type"] = "application/json" | ||
|
|
||
| req = urllib.request.Request(url, data=data, headers=headers, method=method) | ||
| try: | ||
| with urllib.request.urlopen(req) as response: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| return json.loads(response.read().decode()) | ||
| except HTTPError as e: | ||
| print(f"HTTP Error: {e.code} - {e.reason}", file=sys.stderr) | ||
| if e.fp: | ||
| print(e.fp.read().decode(), file=sys.stderr) | ||
| return None | ||
| except Exception as e: | ||
| print(f"Error: {e}", file=sys.stderr) | ||
| return None | ||
|
|
||
|
|
||
| def main(): | ||
| parser = argparse.ArgumentParser( | ||
| description="Retry failed jobs in a Buildkite build." | ||
| ) | ||
| parser.add_argument("org", help="Organization slug") | ||
| parser.add_argument("pipeline", help="Pipeline slug") | ||
| parser.add_argument("build", help="Build number") | ||
| parser.add_argument( | ||
| "--job-name", | ||
| help="Specific job name to retry (if failed). Regex/substring allowed.", | ||
| ) | ||
|
|
||
| args = parser.parse_args() | ||
| token = os.environ.get("BUILDKITE_API_TOKEN") | ||
|
|
||
| if not token: | ||
| print( | ||
| "Please set the BUILDKITE_API_TOKEN environment variable.", file=sys.stderr | ||
| ) | ||
| sys.exit(1) | ||
|
|
||
| url = f"https://api.buildkite.com/v2/organizations/{args.org}/pipelines/{args.pipeline}/builds/{args.build}" | ||
| print(f"Fetching build details from {url}...") | ||
| build_data = make_request(url, token=token) | ||
|
|
||
| if not build_data: | ||
| print("Failed to fetch build details.", file=sys.stderr) | ||
| sys.exit(1) | ||
|
|
||
| jobs = build_data.get("jobs", []) | ||
| failed_jobs = [j for j in jobs if j.get("state") == "failed"] | ||
|
|
||
| if not failed_jobs: | ||
| print("No failed jobs found in this build.") | ||
| sys.exit(0) | ||
|
|
||
| for job in failed_jobs: | ||
| job_id = job.get("id") | ||
| job_name = job.get("name", "Unknown") | ||
|
|
||
| if ( | ||
| args.job_name | ||
| and args.job_name.lower() not in job_name.lower() | ||
| and args.job_name.lower() not in job.get("step_key", "").lower() | ||
| ): | ||
| continue | ||
|
Comment on lines
+73
to
+78
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| print(f"Retrying job: {job_name} ({job_id})") | ||
| retry_url = f"https://api.buildkite.com/v2/organizations/{args.org}/pipelines/{args.pipeline}/builds/{args.build}/jobs/{job_id}/retry" | ||
|
|
||
| result = make_request(retry_url, method="PUT", token=token) | ||
| if result: | ||
| print(f" Successfully triggered retry for {job_name}") | ||
| else: | ||
| print(f" Failed to trigger retry for {job_name}") | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The URL construction for the JSON endpoint should handle potential trailing slashes in the build URL to ensure a valid path.