1#!/usr/bin/env python3 2 3from __future__ import annotations 4 5import argparse 6import fnmatch 7import subprocess 8import textwrap 9from pathlib import Path 10from typing import Any 11 12import yaml 13 14 15REPO_ROOT = Path(__file__).parent.parent.parent 16CONFIG_YML = REPO_ROOT / ".circleci" / "config.yml" 17WORKFLOWS_DIR = REPO_ROOT / ".github" / "workflows" 18 19 20WORKFLOWS_TO_CHECK = [ 21 "binary_builds", 22 "build", 23 "master_build", 24 # These are formatted slightly differently, skip them 25 # "scheduled-ci", 26 # "debuggable-scheduled-ci", 27 # "slow-gradcheck-scheduled-ci", 28 # "promote", 29] 30 31 32def add_job( 33 workflows: dict[str, Any], 34 workflow_name: str, 35 type: str, 36 job: dict[str, Any], 37 past_jobs: dict[str, Any], 38) -> None: 39 """ 40 Add job 'job' under 'type' and 'workflow_name' to 'workflow' in place. Also 41 add any dependencies (they must already be in 'past_jobs') 42 """ 43 if workflow_name not in workflows: 44 workflows[workflow_name] = {"when": "always", "jobs": []} 45 46 requires = job.get("requires", None) 47 if requires is not None: 48 for requirement in requires: 49 dependency = past_jobs[requirement] 50 add_job( 51 workflows, 52 dependency["workflow_name"], 53 dependency["type"], 54 dependency["job"], 55 past_jobs, 56 ) 57 58 workflows[workflow_name]["jobs"].append({type: job}) 59 60 61def get_filtered_circleci_config( 62 workflows: dict[str, Any], relevant_jobs: list[str] 63) -> dict[str, Any]: 64 """ 65 Given an existing CircleCI config, remove every job that's not listed in 66 'relevant_jobs' 67 """ 68 new_workflows: dict[str, Any] = {} 69 past_jobs: dict[str, Any] = {} 70 for workflow_name, workflow in workflows.items(): 71 if workflow_name not in WORKFLOWS_TO_CHECK: 72 # Don't care about this workflow, skip it entirely 73 continue 74 75 for job_dict in workflow["jobs"]: 76 for type, job in job_dict.items(): 77 if "name" not in job: 78 # Job doesn't have a name so it can't be handled 79 print("Skipping", type) 80 else: 81 if job["name"] in relevant_jobs: 82 # Found a job that was specified at the CLI, add it to 83 # the new result 84 add_job(new_workflows, workflow_name, type, job, past_jobs) 85 86 # Record the job in case it's needed as a dependency later 87 past_jobs[job["name"]] = { 88 "workflow_name": workflow_name, 89 "type": type, 90 "job": job, 91 } 92 93 return new_workflows 94 95 96def commit_ci(files: list[str], message: str) -> None: 97 # Check that there are no other modified files than the ones edited by this 98 # tool 99 stdout = subprocess.run( 100 ["git", "status", "--porcelain"], stdout=subprocess.PIPE 101 ).stdout.decode() 102 for line in stdout.split("\n"): 103 if line == "": 104 continue 105 if line[0] != " ": 106 raise RuntimeError( 107 f"Refusing to commit while other changes are already staged: {line}" 108 ) 109 110 # Make the commit 111 subprocess.run(["git", "add"] + files) 112 subprocess.run(["git", "commit", "-m", message]) 113 114 115if __name__ == "__main__": 116 parser = argparse.ArgumentParser( 117 description="make .circleci/config.yml only have a specific set of jobs and delete GitHub actions" 118 ) 119 parser.add_argument("--job", action="append", help="job name", default=[]) 120 parser.add_argument( 121 "--filter-gha", help="keep only these github actions (glob match)", default="" 122 ) 123 parser.add_argument( 124 "--make-commit", 125 action="store_true", 126 help="add change to git with to a do-not-merge commit", 127 ) 128 args = parser.parse_args() 129 130 touched_files = [CONFIG_YML] 131 with open(CONFIG_YML) as f: 132 config_yml = yaml.safe_load(f.read()) 133 134 config_yml["workflows"] = get_filtered_circleci_config( 135 config_yml["workflows"], args.job 136 ) 137 138 with open(CONFIG_YML, "w") as f: 139 yaml.dump(config_yml, f) 140 141 if args.filter_gha: 142 for relative_file in WORKFLOWS_DIR.iterdir(): 143 path = REPO_ROOT.joinpath(relative_file) 144 if not fnmatch.fnmatch(path.name, args.filter_gha): 145 touched_files.append(path) 146 path.resolve().unlink() 147 148 if args.make_commit: 149 jobs_str = "\n".join([f" * {job}" for job in args.job]) 150 message = textwrap.dedent( 151 f""" 152 [skip ci][do not merge] Edit config.yml to filter specific jobs 153 154 Filter CircleCI to only run: 155 {jobs_str} 156 157 See [Run Specific CI Jobs](https://github.com/pytorch/pytorch/blob/master/CONTRIBUTING.md#run-specific-ci-jobs) for details. 158 """ 159 ).strip() 160 commit_ci([str(f.relative_to(REPO_ROOT)) for f in touched_files], message) 161