From 505cffe75d4980e8e8f3f1cfac28e0147482e048 Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Sat, 30 Oct 2021 14:23:43 +0100 Subject: [PATCH] Add emojis to status updates --- .github/workflows/test-react.yaml | 15 ++++ CHANGELOG.md | 6 ++ image/Dockerfile | 1 + image/actions.sh | 13 +++ image/entrypoints/apply.sh | 10 +-- image/entrypoints/destroy-workspace.sh | 1 + image/entrypoints/plan.sh | 4 +- image/tools/convert_output.py | 7 +- image/tools/github_comment_react.py | 109 +++++++++++++++++++++++++ tests/target/main.tf | 6 ++ 10 files changed, 163 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/test-react.yaml create mode 100755 image/tools/github_comment_react.py diff --git a/.github/workflows/test-react.yaml b/.github/workflows/test-react.yaml new file mode 100644 index 00000000..00fb9300 --- /dev/null +++ b/.github/workflows/test-react.yaml @@ -0,0 +1,15 @@ +name: Test Reaction + +on: [issue_comment] + +jobs: + issue_comment: + if: ${{ github.event.issue.pull_request && contains(github.event.comment.user.login, 'dflook') }} + runs-on: ubuntu-latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - name: Plan + uses: ./terraform-version + with: + path: tests/plan/no_changes diff --git a/CHANGELOG.md b/CHANGELOG.md index 99873f8b..aed4de38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,12 @@ When using an action you can specify the version as: - `@v1.18` to use the latest patch release for the specific minor version - `@v1` to use the latest patch release for the specific major version +## Unreleased + +### Changed +- When triggered by `issue_comment` or `pull_request_review_comment` events, the action will first add a :+1: reaction to the comment +- PR comment status messages lead with a single emoji that gives a progress update at a glance + ## [1.18.0] - 2021-10-30 ### Added diff --git a/image/Dockerfile b/image/Dockerfile index 1fc04d69..ddd9a4c3 100644 --- a/image/Dockerfile +++ b/image/Dockerfile @@ -13,6 +13,7 @@ COPY tools/convert_version.py /usr/local/bin/convert_version COPY tools/workspace_exists.py /usr/local/bin/workspace_exists COPY tools/compact_plan.py /usr/local/bin/compact_plan COPY tools/format_tf_credentials.py /usr/local/bin/format_tf_credentials +COPY tools/github_comment_react.py /usr/local/bin/github_comment_react RUN echo "StrictHostKeyChecking no" >> /etc/ssh/ssh_config \ && echo "IdentityFile /.ssh/id_rsa" >> /etc/ssh/ssh_config \ diff --git a/image/actions.sh b/image/actions.sh index e68c31e4..53e9ab0a 100644 --- a/image/actions.sh +++ b/image/actions.sh @@ -85,6 +85,10 @@ function setup() { exit 1 fi + if ! github_comment_react +1 2>"$STEP_TMP_DIR/github_comment_react.stderr"; then + debug_file "$STEP_TMP_DIR/github_comment_react.stderr" + fi + local TERRAFORM_BIN_DIR TERRAFORM_BIN_DIR="$JOB_TMP_DIR/terraform-bin-dir" # tfswitch guesses the wrong home directory... @@ -185,13 +189,22 @@ function init-backend() { } function select-workspace() { + local WORKSPACE_EXIT + + set +e (cd "$INPUT_PATH" && terraform workspace select "$INPUT_WORKSPACE") >"$STEP_TMP_DIR/workspace_select" 2>&1 + WORKSPACE_EXIT=$? + set -e if [[ -s "$STEP_TMP_DIR/workspace_select" ]]; then start_group "Selecting workspace" cat "$STEP_TMP_DIR/workspace_select" end_group fi + + if [[ $WORKSPACE_EXIT -ne 0 ]]; then + exit $WORKSPACE_EXIT + fi } function set-common-plan-args() { diff --git a/image/entrypoints/apply.sh b/image/entrypoints/apply.sh index 86ca32a1..b1385973 100755 --- a/image/entrypoints/apply.sh +++ b/image/entrypoints/apply.sh @@ -12,7 +12,7 @@ set-plan-args PLAN_OUT="$STEP_TMP_DIR/plan.out" if [[ -v GITHUB_TOKEN ]]; then - update_status "Applying plan in $(job_markdown_ref)" + update_status ":orange_circle: Applying plan in $(job_markdown_ref)" fi exec 3>&1 @@ -40,10 +40,10 @@ function apply() { set -e if [[ $APPLY_EXIT -eq 0 ]]; then - update_status "Plan applied in $(job_markdown_ref)" + update_status ":white_check_mark: Plan applied in $(job_markdown_ref)" else set_output failure-reason apply-failed - update_status "Error applying plan in $(job_markdown_ref)" + update_status ":x: Error applying plan in $(job_markdown_ref)" exit 1 fi } @@ -69,7 +69,7 @@ fi if [[ $PLAN_EXIT -eq 1 ]]; then cat >&2 "$STEP_TMP_DIR/terraform_plan.stderr" - update_status "Error applying plan in $(job_markdown_ref)" + update_status ":x: Error applying plan in $(job_markdown_ref)" exit 1 fi @@ -110,7 +110,7 @@ else else echo "Not applying the plan - it has changed from the plan on the PR" echo "The plan on the PR must be up to date. Alternatively, set the auto_approve input to 'true' to apply outdated plans" - update_status "Plan not applied in $(job_markdown_ref) (Plan has changed)" + update_status ":x: Plan not applied in $(job_markdown_ref) (Plan has changed)" echo "Plan changes:" debug_log diff "$STEP_TMP_DIR/plan.txt" "$STEP_TMP_DIR/approved-plan.txt" diff --git a/image/entrypoints/destroy-workspace.sh b/image/entrypoints/destroy-workspace.sh index c417073b..84f8559e 100755 --- a/image/entrypoints/destroy-workspace.sh +++ b/image/entrypoints/destroy-workspace.sh @@ -31,4 +31,5 @@ workspace=$INPUT_WORKSPACE INPUT_WORKSPACE=default init-backend +debug_log terraform workspace delete -no-color -lock-timeout=300s "$workspace" (cd "$INPUT_PATH" && terraform workspace delete -no-color -lock-timeout=300s "$workspace") diff --git a/image/entrypoints/plan.sh b/image/entrypoints/plan.sh index 17554735..fac425bf 100755 --- a/image/entrypoints/plan.sh +++ b/image/entrypoints/plan.sh @@ -39,7 +39,7 @@ if [[ "$GITHUB_EVENT_NAME" == "pull_request" || "$GITHUB_EVENT_NAME" == "issue_c fi if [[ $PLAN_EXIT -eq 1 ]]; then - if ! STATUS="Failed to generate plan in $(job_markdown_ref)" github_pr_comment plan <"$STEP_TMP_DIR/terraform_plan.stderr" 2>"$STEP_TMP_DIR/github_pr_comment.stderr"; then + if ! STATUS=":x: Failed to generate plan in $(job_markdown_ref)" github_pr_comment plan <"$STEP_TMP_DIR/terraform_plan.stderr" 2>"$STEP_TMP_DIR/github_pr_comment.stderr"; then debug_file "$STEP_TMP_DIR/github_pr_comment.stderr" exit 1 else @@ -54,7 +54,7 @@ if [[ "$GITHUB_EVENT_NAME" == "pull_request" || "$GITHUB_EVENT_NAME" == "issue_c TF_CHANGES=true fi - if ! TF_CHANGES=$TF_CHANGES STATUS="Plan generated in $(job_markdown_ref)" github_pr_comment plan <"$STEP_TMP_DIR/plan.txt" 2>"$STEP_TMP_DIR/github_pr_comment.stderr"; then + if ! TF_CHANGES=$TF_CHANGES STATUS=":memo: Plan generated in $(job_markdown_ref)" github_pr_comment plan <"$STEP_TMP_DIR/plan.txt" 2>"$STEP_TMP_DIR/github_pr_comment.stderr"; then debug_file "$STEP_TMP_DIR/github_pr_comment.stderr" exit 1 else diff --git a/image/tools/convert_output.py b/image/tools/convert_output.py index 862c5c2a..d258c6d5 100755 --- a/image/tools/convert_output.py +++ b/image/tools/convert_output.py @@ -30,12 +30,15 @@ def convert_to_github(outputs: Dict) -> Iterable[str]: yield f'::set-output name={name}::{value}' if __name__ == '__main__': + + input_string = sys.stdin.read() try: - outputs = json.load(sys.stdin) + outputs = json.loads(input_string) if not isinstance(outputs, dict): raise Exception('Unable to parse outputs') except: - exit(1) + sys.stderr.write(input_string) + raise for line in convert_to_github(outputs): print(line) diff --git a/image/tools/github_comment_react.py b/image/tools/github_comment_react.py new file mode 100755 index 00000000..9c5e89b3 --- /dev/null +++ b/image/tools/github_comment_react.py @@ -0,0 +1,109 @@ +#!/usr/bin/python3 + +import datetime +import json +import os +import sys +from typing import Optional, cast, NewType, TypedDict + +import requests + +GitHubUrl = NewType('GitHubUrl', str) +CommentReactionUrl = NewType('CommentReactionUrl', GitHubUrl) + + +class GitHubActionsEnv(TypedDict): + """ + Environment variables that are set by the actions runner + """ + GITHUB_API_URL: str + GITHUB_TOKEN: str + GITHUB_EVENT_PATH: str + GITHUB_EVENT_NAME: str + GITHUB_REPOSITORY: str + GITHUB_SHA: str + + +job_tmp_dir = os.environ.get('JOB_TMP_DIR', '.') +step_tmp_dir = os.environ.get('STEP_TMP_DIR', '.') + +env = cast(GitHubActionsEnv, os.environ) + + +def github_session(github_env: GitHubActionsEnv) -> requests.Session: + """ + A request session that is configured for the github API + """ + session = requests.Session() + session.headers['authorization'] = f'token {github_env["GITHUB_TOKEN"]}' + session.headers['user-agent'] = 'terraform-github-actions' + session.headers['accept'] = 'application/vnd.github.v3+json' + return session + + +def github_api_request(method: str, *args, **kwargs) -> requests.Response: + response = github.request(method, *args, **kwargs) + + if 400 <= response.status_code < 500: + debug(str(response.headers)) + + try: + message = response.json()['message'] + + if response.headers['X-RateLimit-Remaining'] == '0': + limit_reset = datetime.datetime.fromtimestamp(int(response.headers['X-RateLimit-Reset'])) + sys.stdout.write(message) + sys.stdout.write(f' Try again when the rate limit resets at {limit_reset} UTC.\n') + exit(1) + + if message != 'Resource not accessible by integration': + sys.stdout.write(message) + sys.stdout.write('\n') + debug(response.content.decode()) + + except Exception: + sys.stdout.write(response.content.decode()) + sys.stdout.write('\n') + raise + + return response + + +def debug(msg: str) -> None: + sys.stderr.write(msg) + sys.stderr.write('\n') + + +def find_reaction_url(actions_env: GitHubActionsEnv) -> Optional[CommentReactionUrl]: + event_type = actions_env['GITHUB_EVENT_NAME'] + + if event_type not in ['issue_comment', 'pull_request_review_comment']: + return None + + with open(actions_env['GITHUB_EVENT_PATH']) as f: + event = json.load(f) + + return event['comment']['reactions']['url'] + + +def react(comment_reaction_url: CommentReactionUrl, reaction_type: str) -> None: + github_api_request('post', comment_reaction_url, json={'content': reaction_type}) + + +def main() -> None: + if len(sys.argv) < 2: + print(f'''Usage: + {sys.argv[0]} ''') + + debug(repr(sys.argv)) + + reaction_url = find_reaction_url(env) + if reaction_url is not None: + react(reaction_url, sys.argv[1]) + + +if __name__ == '__main__': + if 'GITHUB_TOKEN' not in env: + exit(0) + github = github_session(env) + main() diff --git a/tests/target/main.tf b/tests/target/main.tf index 98b2d21f..16fd8bd3 100644 --- a/tests/target/main.tf +++ b/tests/target/main.tf @@ -2,12 +2,18 @@ resource "random_string" "count" { count = 1 length = var.length + + special = false + min_special = 0 } resource "random_string" "foreach" { for_each = toset(["hello"]) length = var.length + + special = false + min_special = 0 } variable "length" {