diff --git a/.github/workflows/cancel_duplicates.yml b/.github/workflows/cancel_duplicates.yml new file mode 100644 index 000000000..787d5f3a4 --- /dev/null +++ b/.github/workflows/cancel_duplicates.yml @@ -0,0 +1,20 @@ +name: Cancel Duplicates +on: + workflow_run: + workflows: ["CI"] + types: ["requested"] + +jobs: + cancel-duplicate-workflow-runs: + name: "Cancel duplicate workflow runs" + runs-on: ubuntu-latest + steps: + - uses: apache-superset/cancel-workflow-runs@953e057 + name: "Cancel duplicate workflow runs" + with: + cancelMode: duplicates + cancelFutureDuplicates: true + token: ${{ secrets.GITHUB_TOKEN }} + sourceRunId: ${{ github.event.workflow_run.id }} + notifyPRCancel: true + skipEventTypes: '["push", "pull_request", "pull_request_target"]' diff --git a/.github/workflows/license-check.yml b/.github/workflows/license-check.yml deleted file mode 100644 index 6105dd4c9..000000000 --- a/.github/workflows/license-check.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: License - -on: - push: - branches: [ master ] - pull_request: - -jobs: - check: - runs-on: ubuntu-18.04 - steps: - - uses: actions/checkout@v2 - - name: Setup Java - uses: actions/setup-java@v1 - with: - java-version: 8 - - name: Generate fossa report - env: - FOSSA_API_KEY: ${{ secrets.FOSSA_API_KEY }} - run: | - set -eo pipefail - if [[ "${{github.event_name}}" != "pull_request" ]]; then - ./scripts/fossa.sh - exit 0 - fi - - URL="https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files" - FILES=$(curl -s -X GET -G $URL | jq -r '.[] | .filename') - - cat<> {resp['message']} <<") + return resp + + +def list_runs(repo: str, params=None): + return request("GET", f"/repos/{repo}/actions/runs", params=params) + + +def cancel_run(repo: str, run_id: Union[str, int]): + return request("POST", f"/repos/{repo}/actions/runs/{run_id}/cancel") + + +def get_pull_request(repo: str, pull_number: Union[str, int]): + return request("GET", f"/repos/{repo}/pulls/{pull_number}") + + +def get_runs_by_branch( + repo: str, + branch: str, + user: Optional[str] = None, + statuses: Iterable[str] = ("queued", "in_progress"), + events: Iterable[str] = ("pull_request", "push"), +): + """Get workflow runs associated with the given branch""" + return [ + item + for event in events + for status in statuses + for item in list_runs( + repo, {"event": event, "status": status, "per_page": 100} + )["workflow_runs"] + if item["head_branch"] == branch + and (user is None or (user == item["head_repository"]["owner"]["login"])) + ] + + +def print_commit(commit): + """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( + f"""HEAD {commit["id"]} +Author: {commit["author"]["name"]} <{commit["author"]["email"]}> +Date: {date_str} + + {indented_message} +""" + ) + + +@click.command() +@click.option( + "--repo", + default=github_repo, + help="The github repository name. For example, apache/incubator-superset.", +) +@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.", +) +@click.argument("branch_or_pull") +def cancel_github_workflows( + branch_or_pull: str, + repo, + event: List[str], + include_last: bool, + include_running: bool, +): + """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",) + pr = None + + if branch_or_pull.isdigit(): + pr = get_pull_request(repo, pull_number=branch_or_pull) + target_type = "pull request" + title = f"#{pr['number']} - {pr['title']}" + else: + target_type = "branch" + title = branch_or_pull + + print( + f"\nCancel {'active' if include_running else 'previous'} " + f"workflow runs for {target_type}\n\n {title}\n" + ) + + if pr: + # full branch name + runs = get_runs_by_branch( + repo, + statuses=statuses, + events=event, + branch=pr["head"]["ref"], + user=pr["user"]["login"], + ) + else: + user = None + branch = branch_or_pull + if ":" in branch: + [user, branch] = branch.split(":", 2) + runs = get_runs_by_branch( + repo, statuses=statuses, events=event, branch=branch_or_pull, user=user + ) + + runs = sorted(runs, key=lambda x: x["created_at"]) + if not runs: + print(f"No {' or '.join(statuses)} workflow runs found.\n") + return + + if not include_last: + # Only keep one item for each workflow + seen = set() + dups = [] + for item in reversed(runs): + if item["workflow_id"] in seen: + dups.append(item) + else: + seen.add(item["workflow_id"]) + 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"] + print_commit(head_commit) + try: + print(f"[{entry['status']}] {entry['name']}", end="\r") + cancel_run(repo, entry["id"]) + print(f"[Cancled] {entry['name']} ") + except ClickException as error: + print(f"[Error: {error.message}] {entry['name']} ") + print("") + + +if __name__ == "__main__": + # pylint: disable=no-value-for-parameter + cancel_github_workflows() diff --git a/setup.cfg b/setup.cfg index 80e52a361..c4d4140cd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,7 +30,7 @@ combine_as_imports = true include_trailing_comma = true line_length = 88 known_first_party = superset -known_third_party =alembic,apispec,backoff,bleach,cachelib,celery,click,colorama,contextlib2,cron_descriptor,croniter,cryptography,dateutil,flask,flask_appbuilder,flask_babel,flask_caching,flask_compress,flask_login,flask_migrate,flask_sqlalchemy,flask_talisman,flask_testing,flask_wtf,freezegun,geohash,geopy,holidays,humanize,isodate,jinja2,jwt,markdown,markupsafe,marshmallow,msgpack,numpy,pandas,parameterized,parsedatetime,pathlib2,pgsanity,pkg_resources,polyline,prison,pyarrow,pyhive,pyparsing,pytest,pytz,redis,retry,selenium,setuptools,simplejson,slack,sqlalchemy,sqlalchemy_utils,sqlparse,typing_extensions,werkzeug,wtforms,wtforms_json,yaml +known_third_party =alembic,apispec,backoff,bleach,cachelib,celery,click,colorama,contextlib2,cron_descriptor,croniter,cryptography,dateutil,flask,flask_appbuilder,flask_babel,flask_caching,flask_compress,flask_login,flask_migrate,flask_sqlalchemy,flask_talisman,flask_testing,flask_wtf,freezegun,geohash,geopy,holidays,humanize,isodate,jinja2,jwt,markdown,markupsafe,marshmallow,msgpack,numpy,pandas,parameterized,parsedatetime,pathlib2,pgsanity,pkg_resources,polyline,prison,pyarrow,pyhive,pyparsing,pytest,pytz,redis,requests,retry,selenium,setuptools,simplejson,slack,sqlalchemy,sqlalchemy_utils,sqlparse,typing_extensions,werkzeug,wtforms,wtforms_json,yaml multi_line_output = 3 order_by_type = false