From f997a0692ca0f26740d2bdef2695c3e881d4e918 Mon Sep 17 00:00:00 2001 From: Nikita Titov Date: Wed, 13 Jan 2021 21:53:19 +0300 Subject: [PATCH] [ci] improve experience with optional GitHub workflows (#3740) * improve experience with optional GitHub workflows * Update README.md * Update r_artifacts.yml * Update optional_checks.yml * continue * Update triggering_comments.yml * Update README.md * Update r_artifacts.yml * Update r_artifacts.yml * Update r_artifacts.yml * Update r_valgrind.yml * Update r_artifacts.yml * Update r_valgrind.yml * Update r_valgrind.yml * Update r_valgrind.yml * add docstrings to fix lint * better formatting for multi-line commands --- .ci/append_comment.sh | 50 ++++++++++++++ .ci/get_workflow_status.py | 82 +++++++++++++++++++++++ .ci/rerun_workflow.sh | 47 +++++++++++++ .ci/set_commit_status.sh | 53 +++++++++++++++ .ci/trigger_dispatch_run.sh | 51 ++++++++++++++ .github/workflows/optional_checks.yml | 25 +++++++ .github/workflows/r_artifacts.yml | 28 ++++++-- .github/workflows/r_valgrind.yml | 39 ++++++++++- .github/workflows/triggering_comments.yml | 34 ++++++++++ R-package/README.md | 6 +- 10 files changed, 404 insertions(+), 11 deletions(-) create mode 100755 .ci/append_comment.sh create mode 100644 .ci/get_workflow_status.py create mode 100755 .ci/rerun_workflow.sh create mode 100755 .ci/set_commit_status.sh create mode 100755 .ci/trigger_dispatch_run.sh create mode 100644 .github/workflows/optional_checks.yml create mode 100644 .github/workflows/triggering_comments.yml diff --git a/.ci/append_comment.sh b/.ci/append_comment.sh new file mode 100755 index 000000000000..14e0111249cf --- /dev/null +++ b/.ci/append_comment.sh @@ -0,0 +1,50 @@ +#!/bin/bash +# +# [description] +# Update comment appending a given body to the specified original comment. +# +# [usage] +# append_comment.sh +# +# COMMENT_ID: ID of comment that should be modified. +# +# BODY: Text that will be appended to the original comment body. + +set -e + +if [ -z "$GITHUB_ACTIONS" ]; then + echo "Must be run inside GitHub Actions CI" + exit -1 +fi + +if [ $# -ne 2 ]; then + echo "Usage: $0 " + exit -1 +fi + +comment_id=$1 +body=$2 + +old_comment_body=$( + curl -sL \ + -H "Accept: application/vnd.github.v3+json" \ + -H "Authorization: token $SECRETS_WORKFLOW" \ + "${GITHUB_API_URL}/repos/microsoft/LightGBM/issues/comments/$comment_id" | \ + jq '.body' +) +body=${body/failure/failure ❌} +body=${body/error/failure ❌} +body=${body/cancelled/failure ❌} +body=${body/timed_out/failure ❌} +body=${body/success/success ✔️} +data=$( + jq -n \ + --argjson body "${old_comment_body%?}\r\n\r\n$body\"" \ + '{"body":$body}' +) +curl -sL \ + -X PATCH \ + -H "Accept: application/vnd.github.v3+json" \ + -H "Authorization: token $SECRETS_WORKFLOW" \ + -d "$data" \ + "${GITHUB_API_URL}/repos/microsoft/LightGBM/issues/comments/$comment_id" diff --git a/.ci/get_workflow_status.py b/.ci/get_workflow_status.py new file mode 100644 index 000000000000..82a57a9537b9 --- /dev/null +++ b/.ci/get_workflow_status.py @@ -0,0 +1,82 @@ +# coding: utf-8 +"""Get the most recent status of workflow for the current PR.""" +import json +from os import environ +from sys import argv, exit +from time import sleep +try: + from urllib import request +except ImportError: + import urllib2 as request + + +def get_runs(workflow_name): + """Get all triggering workflow comments in the current PR. + + Parameters + ---------- + workflow_name : string + Name of the workflow. + + Returns + ------- + pr_runs : list + List of comment objects sorted by the time of creation in decreasing order. + """ + pr_runs = [] + if environ.get("GITHUB_EVENT_NAME", "") == "pull_request": + pr_number = int(environ.get("GITHUB_REF").split('/')[-2]) + req = request.Request(url="{}/repos/microsoft/LightGBM/issues/{}/comments".format(environ.get("GITHUB_API_URL"), + pr_number), + headers={"Accept": "application/vnd.github.v3+json"}) + url = request.urlopen(req) + data = json.loads(url.read().decode('utf-8')) + url.close() + pr_runs = [i for i in data + if i['author_association'].lower() in {'owner', 'member', 'collaborator'} + and i['body'].startswith('/gha run') + and 'Workflow **{}** has been triggered!'.format(workflow_name) in i['body']] + return pr_runs[::-1] + + +def get_status(runs): + """Get the most recent status of workflow for the current PR. + + Parameters + ---------- + runs : list + List of comment objects sorted by the time of creation in decreasing order. + + Returns + ------- + status : string + The most recent status of workflow. + Can be 'success', 'failure' or 'in-progress'. + """ + status = 'success' + for run in runs: + body = run['body'] + if "Status: " in body: + if "Status: skipped" in body: + continue + if "Status: failure" in body: + status = 'failure' + break + if "Status: success" in body: + status = 'success' + break + else: + status = 'in-progress' + break + return status + + +if __name__ == "__main__": + workflow_name = argv[1] + while True: + status = get_status(get_runs(workflow_name)) + if status != 'in-progress': + break + sleep(60) + if status == 'failure': + exit(1) diff --git a/.ci/rerun_workflow.sh b/.ci/rerun_workflow.sh new file mode 100755 index 000000000000..eb098adeeaeb --- /dev/null +++ b/.ci/rerun_workflow.sh @@ -0,0 +1,47 @@ +#!/bin/bash +# +# [description] +# Rerun specified workflow for given pull request. +# +# [usage] +# rerun_workflow.sh +# +# WORKFLOW_ID: Identifier (config name of ID) of a workflow to be rerun. +# +# PR_NUMBER: Number of pull request for which workflow should be rerun. +# +# PR_BRANCH: Name of pull request's branch. + +set -e + +if [ -z "$GITHUB_ACTIONS" ]; then + echo "Must be run inside GitHub Actions CI" + exit -1 +fi + +if [ $# -ne 3 ]; then + echo "Usage: $0 " + exit -1 +fi + +workflow_id=$1 +pr_number=$2 +pr_branch=$3 + +runs=$( + curl -sL \ + -H "Accept: application/vnd.github.v3+json" \ + -H "Authorization: token $SECRETS_WORKFLOW" \ + "${GITHUB_API_URL}/repos/microsoft/LightGBM/actions/workflows/${workflow_id}/runs?event=pull_request&branch=${pr_branch}" | \ + jq '.workflow_runs' +) +runs=$(echo $runs | jq --arg pr_number "$pr_number" --arg pr_branch "$pr_branch" 'map(select(.event == "pull_request" and ((.pull_requests | length) != 0 and (.pull_requests[0].number | tostring) == $pr_number or .head_branch == $pr_branch)))') +runs=$(echo $runs | jq 'sort_by(.run_number) | reverse') + +if [[ $(echo $runs | jq 'length') -gt 0 ]]; then + curl -sL \ + -X POST \ + -H "Accept: application/vnd.github.v3+json" \ + -H "Authorization: token $SECRETS_WORKFLOW" \ + "${GITHUB_API_URL}/repos/microsoft/LightGBM/actions/runs/$(echo $runs | jq '.[0].id')/rerun" +fi diff --git a/.ci/set_commit_status.sh b/.ci/set_commit_status.sh new file mode 100755 index 000000000000..b10dd3ec27c3 --- /dev/null +++ b/.ci/set_commit_status.sh @@ -0,0 +1,53 @@ +#!/bin/bash +# +# [description] +# Set a status with a given name to the specified commit. +# +# [usage] +# set_commit_status.sh +# +# NAME: Name of status. +# Status with existing name overwrites a previous one. +# +# STATUS: Status to be set. +# Can be "error", "failure", "pending" or "success". +# +# SHA: SHA of a commit to set a status on. + +set -e + +if [ -z "$GITHUB_ACTIONS" ]; then + echo "Must be run inside GitHub Actions CI" + exit -1 +fi + +if [ $# -ne 3 ]; then + echo "Usage: $0 " + exit -1 +fi + +name=$1 + +status=$2 +status=${status/error/failure} +status=${status/cancelled/failure} +status=${status/timed_out/failure} +status=${status/in_progress/pending} +status=${status/queued/pending} + +sha=$3 + +data=$( + jq -n \ + --arg state $status \ + --arg url "${GITHUB_SERVER_URL}/microsoft/LightGBM/actions/runs/${GITHUB_RUN_ID}" \ + --arg name "$name" \ + '{"state":$state,"target_url":$url,"context":$name}' +) + +curl -sL \ + -X POST \ + -H "Accept: application/vnd.github.v3+json" \ + -H "Authorization: token $SECRETS_WORKFLOW" \ + -d "$data" \ + "${GITHUB_API_URL}/repos/microsoft/LightGBM/statuses/$sha" diff --git a/.ci/trigger_dispatch_run.sh b/.ci/trigger_dispatch_run.sh new file mode 100755 index 000000000000..040d6e9a581d --- /dev/null +++ b/.ci/trigger_dispatch_run.sh @@ -0,0 +1,51 @@ +#!/bin/bash +# +# [description] +# Trigger manual workflow run by a dispatch event. +# +# [usage] +# trigger_dispatch_run.sh +# +# PR_URL: URL of pull request from which dispatch is triggering. +# +# COMMENT_ID: ID of comment that is triggering a dispatch. +# +# DISPATCH_NAME: Name of a dispatch to be triggered. + +set -e + +if [ -z "$GITHUB_ACTIONS" ]; then + echo "Must be run inside GitHub Actions CI" + exit -1 +fi + +if [ $# -ne 3 ]; then + echo "Usage: $0 " + exit -1 +fi + +pr_url=$1 +comment_id=$2 +dispatch_name=$3 + +pr=$( + curl -sL \ + -H "Accept: application/vnd.github.v3+json" \ + -H "Authorization: token $SECRETS_WORKFLOW" \ + "$pr_url" +) +data=$( + jq -n \ + --arg event_type "$dispatch_name" \ + --arg pr_number "$(echo $pr | jq '.number')" \ + --arg pr_sha "$(echo $pr | jq '.head.sha')" \ + --arg pr_branch "$(echo $pr | jq '.head.ref')" \ + --arg comment_number "$comment_id" \ + '{"event_type":$event_type,"client_payload":{"pr_number":$pr_number,"pr_sha":$pr_sha,"pr_branch":$pr_branch,"comment_number":$comment_number}}' +) +curl -sL \ + -X POST \ + -H "Accept: application/vnd.github.v3+json" \ + -H "Authorization: token $SECRETS_WORKFLOW" \ + -d "$data" \ + "${GITHUB_API_URL}/repos/microsoft/LightGBM/dispatches" diff --git a/.github/workflows/optional_checks.yml b/.github/workflows/optional_checks.yml new file mode 100644 index 000000000000..7cca8ec56524 --- /dev/null +++ b/.github/workflows/optional_checks.yml @@ -0,0 +1,25 @@ +name: Optional checks + +on: + pull_request: + branches: + - master + +jobs: + all-successful: + timeout-minutes: 120 + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v2.3.4 + with: + fetch-depth: 5 + submodules: false + - name: Check that all tests succeeded + run: | + workflows=("R valgrind tests") + for i in "${workflows[@]}"; do + python "$GITHUB_WORKSPACE/.ci/get_workflow_status.py" "$i" \ + || { echo "The last reported status from workflow \"$i\" is failure. Commit fixes and rerun the workflow."; \ + exit -1; } + done diff --git a/.github/workflows/r_artifacts.yml b/.github/workflows/r_artifacts.yml index 62f111833298..d69a1f4d2656 100644 --- a/.github/workflows/r_artifacts.yml +++ b/.github/workflows/r_artifacts.yml @@ -1,27 +1,39 @@ name: R artifact builds on: - pull_request_review_comment: - types: [created] + repository_dispatch: + types: [gha_run_build_r_artifacts] jobs: cran-package: name: cran-package - if: github.event.comment.body == '/gha build r-artifacts' && contains('OWNER,MEMBER,COLLABORATOR', github.event.comment.author_association) timeout-minutes: 60 runs-on: ubuntu-latest container: rocker/r-base + env: + SECRETS_WORKFLOW: ${{ secrets.WORKFLOW }} steps: - - name: Install Git before checkout + - name: Install essential software before checkout shell: bash run: | apt-get update - apt-get install --no-install-recommends -y git + apt-get install --no-install-recommends -y \ + curl \ + git \ + jq - name: Checkout repository uses: actions/checkout@v2.3.4 with: fetch-depth: 5 submodules: true + repository: microsoft/LightGBM + ref: "refs/pull/${{ github.event.client_payload.pr_number }}/merge" + - name: Send init status + if: ${{ always() }} + run: | + $GITHUB_WORKSPACE/.ci/append_comment.sh \ + "${{ github.event.client_payload.comment_number }}" \ + "Workflow **${{ github.workflow }}** has been triggered! 🚀\r\n${GITHUB_SERVER_URL}/microsoft/LightGBM/actions/runs/${GITHUB_RUN_ID}" - name: Build package shell: bash id: build_package @@ -37,3 +49,9 @@ jobs: with: name: ${{ steps.build_package.outputs.artifact_name }} path: ${{ steps.build_package.outputs.artifact_path }} + - name: Send final status + if: ${{ always() }} + run: | + $GITHUB_WORKSPACE/.ci/append_comment.sh \ + "${{ github.event.client_payload.comment_number }}" \ + "Status: ${{ job.status }}." diff --git a/.github/workflows/r_valgrind.yml b/.github/workflows/r_valgrind.yml index 1ddc1a7afc1e..e9af3ae6fdc5 100644 --- a/.github/workflows/r_valgrind.yml +++ b/.github/workflows/r_valgrind.yml @@ -1,22 +1,40 @@ name: R valgrind tests on: - pull_request_review_comment: - types: [created] + repository_dispatch: + types: [gha_run_r_valgrind] jobs: test-r-valgrind: name: r-package (ubuntu-latest, R-devel, valgrind) - if: github.event.comment.body == '/gha run r-valgrind' && contains('OWNER,MEMBER,COLLABORATOR', github.event.comment.author_association) timeout-minutes: 120 runs-on: ubuntu-latest container: wch1/r-debug + env: + SECRETS_WORKFLOW: ${{ secrets.WORKFLOW }} steps: + - name: Install essential software before checkout + shell: bash + run: | + apt-get update + apt-get install --no-install-recommends -y \ + curl \ + git \ + jq - name: Checkout repository uses: actions/checkout@v2.3.4 with: fetch-depth: 5 submodules: true + repository: microsoft/LightGBM + ref: "refs/pull/${{ github.event.client_payload.pr_number }}/merge" + - name: Send init status + if: ${{ always() }} + run: | + $GITHUB_WORKSPACE/.ci/set_commit_status.sh "${{ github.workflow }}" "pending" "${{ github.event.client_payload.pr_sha }}" + $GITHUB_WORKSPACE/.ci/append_comment.sh \ + "${{ github.event.client_payload.comment_number }}" \ + "Workflow **${{ github.workflow }}** has been triggered! 🚀\r\n${GITHUB_SERVER_URL}/microsoft/LightGBM/actions/runs/${GITHUB_RUN_ID}" - name: Install packages shell: bash run: | @@ -26,3 +44,18 @@ jobs: - name: Run tests with valgrind shell: bash run: ./.ci/test_r_package_valgrind.sh + - name: Send final status + if: ${{ always() }} + run: | + $GITHUB_WORKSPACE/.ci/set_commit_status.sh "${{ github.workflow }}" "${{ job.status }}" "${{ github.event.client_payload.pr_sha }}" + $GITHUB_WORKSPACE/.ci/append_comment.sh \ + "${{ github.event.client_payload.comment_number }}" \ + "Status: ${{ job.status }}." + - name: Rerun workflow-indicator + if: ${{ always() }} + run: | + bash $GITHUB_WORKSPACE/.ci/rerun_workflow.sh \ + "optional_checks.yml" \ + "${{ github.event.client_payload.pr_number }}" \ + "${{ github.event.client_payload.pr_branch }}" \ + || true diff --git a/.github/workflows/triggering_comments.yml b/.github/workflows/triggering_comments.yml new file mode 100644 index 000000000000..f20b397449e8 --- /dev/null +++ b/.github/workflows/triggering_comments.yml @@ -0,0 +1,34 @@ +name: Triggering comments + +on: + issue_comment: + types: [created] + +jobs: + triggering-tests: + if: github.event.issue.pull_request && contains('OWNER,MEMBER,COLLABORATOR', github.event.comment.author_association) + runs-on: ubuntu-latest + env: + SECRETS_WORKFLOW: ${{ secrets.WORKFLOW }} + steps: + - name: Checkout repository + uses: actions/checkout@v2.3.4 + with: + fetch-depth: 5 + submodules: false + + - name: Trigger R valgrind tests + if: github.event.comment.body == '/gha run r-valgrind' + run: | + $GITHUB_WORKSPACE/.ci/trigger_dispatch_run.sh \ + "${{ github.event.issue.pull_request.url }}" \ + "${{ github.event.comment.id }}" \ + "gha_run_r_valgrind" + + - name: Trigger R artifact builds + if: github.event.comment.body == '/gha run build-r-artifacts' + run: | + $GITHUB_WORKSPACE/.ci/trigger_dispatch_run.sh \ + "${{ github.event.issue.pull_request.url }}" \ + "${{ github.event.comment.id }}" \ + "gha_run_build_r_artifacts" diff --git a/R-package/README.md b/R-package/README.md index 7edd14717955..a1664ec1c951 100644 --- a/R-package/README.md +++ b/R-package/README.md @@ -245,9 +245,9 @@ sh build-cran-package.sh This will create a file `lightgbm_${VERSION}.tar.gz`, where `VERSION` is the version of `LightGBM`. -Alternatively, GitHub Actions can generate this file for you. On a pull request, go to the "Files changed" tab and create a comment with this phrase: +Alternatively, GitHub Actions can generate this file for you. On a pull request, create a comment with this phrase: -> /gha build r-artifacts +> /gha run build-r-artifacts Go to https://github.com/microsoft/LightGBM/actions, and find the most recent run of the "R artifact builds" workflow. If it ran successfully, you'll find a download link for the package (in `.zip` format) in that run's "Artifacts" section. @@ -389,7 +389,7 @@ RDvalgrind \ | cat ``` -These tests can also be triggered on any pull request by leaving a review on the "Files changed" tab in a pull request: +These tests can also be triggered on any pull request by leaving a comment in a pull request: > /gha run r-valgrind