2021-01-07 03:47:00 -05:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
# Licensed to the Apache Software Foundation (ASF) under one
|
|
|
|
# or more contributor license agreements. See the NOTICE file
|
|
|
|
# distributed with this work for additional information
|
|
|
|
# regarding copyright ownership. The ASF licenses this file
|
|
|
|
# to you under the Apache License, Version 2.0 (the
|
|
|
|
# "License"); you may not use this file except in compliance
|
|
|
|
# with the License. You may obtain a copy of the License at
|
|
|
|
#
|
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
#
|
|
|
|
# Unless required by applicable law or agreed to in writing,
|
|
|
|
# software distributed under the License is distributed on an
|
|
|
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
|
|
# KIND, either express or implied. See the License for the
|
|
|
|
# specific language governing permissions and limitations
|
|
|
|
# under the License.
|
|
|
|
"""
|
|
|
|
Manually cancel previous GitHub Action workflow runs in queue.
|
|
|
|
|
|
|
|
Example:
|
|
|
|
# Set up
|
2021-02-05 15:22:34 -05:00
|
|
|
export GITHUB_TOKEN={{ your personal github access token }}
|
2021-01-11 09:09:10 -05:00
|
|
|
export GITHUB_REPOSITORY=apache/superset
|
2021-01-07 03:47:00 -05:00
|
|
|
|
2021-01-29 15:29:46 -05:00
|
|
|
# cancel previous jobs for a PR, will even cancel the running ones
|
2021-01-07 03:47:00 -05:00
|
|
|
./cancel_github_workflows.py 1042
|
|
|
|
|
|
|
|
# cancel previous jobs for a branch
|
|
|
|
./cancel_github_workflows.py my-branch
|
|
|
|
|
2021-01-29 15:29:46 -05:00
|
|
|
# cancel all jobs of a PR, including the latest runs
|
2021-01-07 03:47:00 -05:00
|
|
|
./cancel_github_workflows.py 1024 --include-last
|
|
|
|
"""
|
|
|
|
import os
|
2021-07-21 14:46:43 -04:00
|
|
|
from typing import Any, Dict, Iterable, Iterator, List, Optional, Union
|
2021-01-07 03:47:00 -05:00
|
|
|
|
|
|
|
import click
|
|
|
|
import requests
|
|
|
|
from click.exceptions import ClickException
|
|
|
|
from dateutil import parser
|
|
|
|
from typing_extensions import Literal
|
|
|
|
|
|
|
|
github_token = os.environ.get("GITHUB_TOKEN")
|
2021-01-11 09:09:10 -05:00
|
|
|
github_repo = os.environ.get("GITHUB_REPOSITORY", "apache/superset")
|
2021-01-07 03:47:00 -05:00
|
|
|
|
|
|
|
|
2021-07-21 14:46:43 -04:00
|
|
|
def request(
|
|
|
|
method: Literal["GET", "POST", "DELETE", "PUT"], endpoint: str, **kwargs: Any
|
|
|
|
) -> Dict[str, Any]:
|
2021-01-07 03:47:00 -05:00
|
|
|
resp = requests.request(
|
|
|
|
method,
|
|
|
|
f"https://api.github.com/{endpoint.lstrip('/')}",
|
|
|
|
headers={"Authorization": f"Bearer {github_token}"},
|
|
|
|
**kwargs,
|
|
|
|
).json()
|
|
|
|
if "message" in resp:
|
|
|
|
raise ClickException(f"{endpoint} >> {resp['message']} <<")
|
|
|
|
return resp
|
|
|
|
|
|
|
|
|
2021-07-21 14:46:43 -04:00
|
|
|
def list_runs(
|
2022-03-29 13:03:09 -04:00
|
|
|
repo: str,
|
|
|
|
params: Optional[Dict[str, str]] = None,
|
2021-07-21 14:46:43 -04:00
|
|
|
) -> Iterator[Dict[str, Any]]:
|
2021-01-29 15:29:46 -05:00
|
|
|
"""List all github workflow runs.
|
|
|
|
Returns:
|
|
|
|
An iterator that will iterate through all pages of matching runs."""
|
2021-07-21 14:46:43 -04:00
|
|
|
if params is None:
|
|
|
|
params = {}
|
2021-01-29 15:29:46 -05:00
|
|
|
page = 1
|
|
|
|
total_count = 10000
|
|
|
|
while page * 100 < total_count:
|
|
|
|
result = request(
|
|
|
|
"GET",
|
|
|
|
f"/repos/{repo}/actions/runs",
|
|
|
|
params={**params, "per_page": 100, "page": page},
|
|
|
|
)
|
|
|
|
total_count = result["total_count"]
|
|
|
|
for item in result["workflow_runs"]:
|
|
|
|
yield item
|
|
|
|
page += 1
|
2021-01-07 03:47:00 -05:00
|
|
|
|
|
|
|
|
2021-07-21 14:46:43 -04:00
|
|
|
def cancel_run(repo: str, run_id: Union[str, int]) -> Dict[str, Any]:
|
2021-01-07 03:47:00 -05:00
|
|
|
return request("POST", f"/repos/{repo}/actions/runs/{run_id}/cancel")
|
|
|
|
|
|
|
|
|
2021-07-21 14:46:43 -04:00
|
|
|
def get_pull_request(repo: str, pull_number: Union[str, int]) -> Dict[str, Any]:
|
2021-01-07 03:47:00 -05:00
|
|
|
return request("GET", f"/repos/{repo}/pulls/{pull_number}")
|
|
|
|
|
|
|
|
|
2021-01-29 15:29:46 -05:00
|
|
|
def get_runs(
|
2021-01-07 03:47:00 -05:00
|
|
|
repo: str,
|
2021-01-29 15:29:46 -05:00
|
|
|
branch: Optional[str] = None,
|
2021-01-07 03:47:00 -05:00
|
|
|
user: Optional[str] = None,
|
|
|
|
statuses: Iterable[str] = ("queued", "in_progress"),
|
|
|
|
events: Iterable[str] = ("pull_request", "push"),
|
2021-07-21 14:46:43 -04:00
|
|
|
) -> List[Dict[str, Any]]:
|
2021-01-07 03:47:00 -05:00
|
|
|
"""Get workflow runs associated with the given branch"""
|
|
|
|
return [
|
|
|
|
item
|
|
|
|
for event in events
|
|
|
|
for status in statuses
|
2021-01-29 15:29:46 -05:00
|
|
|
for item in list_runs(repo, {"event": event, "status": status})
|
|
|
|
if (branch is None or (branch == item["head_branch"]))
|
2021-01-07 03:47:00 -05:00
|
|
|
and (user is None or (user == item["head_repository"]["owner"]["login"]))
|
|
|
|
]
|
|
|
|
|
|
|
|
|
2021-07-21 14:46:43 -04:00
|
|
|
def print_commit(commit: Dict[str, Any], branch: str) -> None:
|
2021-01-07 03:47:00 -05:00
|
|
|
"""Print out commit message for verification"""
|
|
|
|
indented_message = " \n".join(commit["message"].split("\n"))
|
|
|
|
date_str = (
|
|
|
|
parser.parse(commit["timestamp"])
|
|
|
|
.astimezone(tz=None)
|
|
|
|
.strftime("%a, %d %b %Y %H:%M:%S")
|
|
|
|
)
|
|
|
|
print(
|
2021-01-29 15:29:46 -05:00
|
|
|
f"""HEAD {commit["id"]} ({branch})
|
2021-01-07 03:47:00 -05:00
|
|
|
Author: {commit["author"]["name"]} <{commit["author"]["email"]}>
|
|
|
|
Date: {date_str}
|
|
|
|
|
|
|
|
{indented_message}
|
|
|
|
"""
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@click.command()
|
|
|
|
@click.option(
|
|
|
|
"--repo",
|
|
|
|
default=github_repo,
|
2021-01-11 09:09:10 -05:00
|
|
|
help="The github repository name. For example, apache/superset.",
|
2021-01-07 03:47:00 -05:00
|
|
|
)
|
|
|
|
@click.option(
|
|
|
|
"--event",
|
|
|
|
type=click.Choice(["pull_request", "push", "issue"]),
|
|
|
|
default=["pull_request", "push"],
|
|
|
|
show_default=True,
|
|
|
|
multiple=True,
|
|
|
|
)
|
|
|
|
@click.option(
|
|
|
|
"--include-last/--skip-last",
|
|
|
|
default=False,
|
|
|
|
show_default=True,
|
|
|
|
help="Whether to also cancel the lastest run.",
|
|
|
|
)
|
|
|
|
@click.option(
|
|
|
|
"--include-running/--skip-running",
|
|
|
|
default=True,
|
|
|
|
show_default=True,
|
|
|
|
help="Whether to also cancel running workflows.",
|
|
|
|
)
|
2021-01-29 15:29:46 -05:00
|
|
|
@click.argument("branch_or_pull", required=False)
|
2021-01-07 03:47:00 -05:00
|
|
|
def cancel_github_workflows(
|
2021-01-29 15:29:46 -05:00
|
|
|
branch_or_pull: Optional[str],
|
|
|
|
repo: str,
|
2021-01-07 03:47:00 -05:00
|
|
|
event: List[str],
|
|
|
|
include_last: bool,
|
|
|
|
include_running: bool,
|
2021-07-21 14:46:43 -04:00
|
|
|
) -> None:
|
2021-01-07 03:47:00 -05:00
|
|
|
"""Cancel running or queued GitHub workflows by branch or pull request ID"""
|
|
|
|
if not github_token:
|
|
|
|
raise ClickException("Please provide GITHUB_TOKEN as an env variable")
|
|
|
|
|
|
|
|
statuses = ("queued", "in_progress") if include_running else ("queued",)
|
2021-01-29 15:29:46 -05:00
|
|
|
events = event
|
2021-01-07 03:47:00 -05:00
|
|
|
pr = None
|
|
|
|
|
2021-01-29 15:29:46 -05:00
|
|
|
if branch_or_pull is None:
|
|
|
|
title = "all jobs" if include_last else "all duplicate jobs"
|
|
|
|
elif branch_or_pull.isdigit():
|
2021-01-07 03:47:00 -05:00
|
|
|
pr = get_pull_request(repo, pull_number=branch_or_pull)
|
2021-01-29 15:29:46 -05:00
|
|
|
title = f"pull request #{pr['number']} - {pr['title']}"
|
2021-01-07 03:47:00 -05:00
|
|
|
else:
|
2021-01-29 15:29:46 -05:00
|
|
|
title = f"branch [{branch_or_pull}]"
|
2021-01-07 03:47:00 -05:00
|
|
|
|
|
|
|
print(
|
|
|
|
f"\nCancel {'active' if include_running else 'previous'} "
|
2021-01-29 15:29:46 -05:00
|
|
|
f"workflow runs for {title}\n"
|
2021-01-07 03:47:00 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
if pr:
|
2021-01-29 15:29:46 -05:00
|
|
|
runs = get_runs(
|
2021-01-07 03:47:00 -05:00
|
|
|
repo,
|
|
|
|
statuses=statuses,
|
|
|
|
events=event,
|
|
|
|
branch=pr["head"]["ref"],
|
|
|
|
user=pr["user"]["login"],
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
user = None
|
|
|
|
branch = branch_or_pull
|
2021-01-29 15:29:46 -05:00
|
|
|
if branch and ":" in branch:
|
2021-01-07 03:47:00 -05:00
|
|
|
[user, branch] = branch.split(":", 2)
|
2021-01-29 15:29:46 -05:00
|
|
|
runs = get_runs(
|
2022-03-29 13:03:09 -04:00
|
|
|
repo,
|
|
|
|
branch=branch,
|
|
|
|
user=user,
|
|
|
|
statuses=statuses,
|
|
|
|
events=events,
|
2021-01-07 03:47:00 -05:00
|
|
|
)
|
|
|
|
|
2021-01-29 15:29:46 -05:00
|
|
|
# sort old jobs to the front, so to cancel older jobs first
|
2021-01-07 03:47:00 -05:00
|
|
|
runs = sorted(runs, key=lambda x: x["created_at"])
|
2021-01-29 15:29:46 -05:00
|
|
|
if runs:
|
|
|
|
print(
|
|
|
|
f"Found {len(runs)} potential runs of\n"
|
|
|
|
f" status: {statuses}\n event: {events}\n"
|
|
|
|
)
|
|
|
|
else:
|
2021-01-07 03:47:00 -05:00
|
|
|
print(f"No {' or '.join(statuses)} workflow runs found.\n")
|
|
|
|
return
|
|
|
|
|
|
|
|
if not include_last:
|
2021-01-29 15:29:46 -05:00
|
|
|
# Keep the latest run for each workflow and cancel all others
|
2021-01-07 03:47:00 -05:00
|
|
|
seen = set()
|
|
|
|
dups = []
|
|
|
|
for item in reversed(runs):
|
2021-01-29 15:29:46 -05:00
|
|
|
key = f'{item["event"]}_{item["head_branch"]}_{item["workflow_id"]}'
|
|
|
|
if key in seen:
|
2021-01-07 03:47:00 -05:00
|
|
|
dups.append(item)
|
|
|
|
else:
|
2021-01-29 15:29:46 -05:00
|
|
|
seen.add(key)
|
2021-01-07 03:47:00 -05:00
|
|
|
if not dups:
|
|
|
|
print(
|
|
|
|
"Only the latest runs are in queue. "
|
|
|
|
"Use --include-last to force cancelling them.\n"
|
|
|
|
)
|
|
|
|
return
|
|
|
|
runs = dups[::-1]
|
|
|
|
|
|
|
|
last_sha = None
|
|
|
|
|
|
|
|
print(f"\nCancelling {len(runs)} jobs...\n")
|
|
|
|
for entry in runs:
|
|
|
|
head_commit = entry["head_commit"]
|
|
|
|
if head_commit["id"] != last_sha:
|
|
|
|
last_sha = head_commit["id"]
|
2021-01-29 15:29:46 -05:00
|
|
|
print("")
|
|
|
|
print_commit(head_commit, entry["head_branch"])
|
2021-01-07 03:47:00 -05:00
|
|
|
try:
|
|
|
|
print(f"[{entry['status']}] {entry['name']}", end="\r")
|
|
|
|
cancel_run(repo, entry["id"])
|
2021-07-21 14:46:43 -04:00
|
|
|
print(f"[Canceled] {entry['name']} ")
|
2021-01-07 03:47:00 -05:00
|
|
|
except ClickException as error:
|
|
|
|
print(f"[Error: {error.message}] {entry['name']} ")
|
|
|
|
print("")
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
# pylint: disable=no-value-for-parameter
|
|
|
|
cancel_github_workflows()
|