From 35285041bc0b1d4c12e6ccecf7802012766109e1 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff Date: Thu, 10 Dec 2020 00:18:28 +0100 Subject: [PATCH 1/6] Escape inner escape sequences properly in Python Unfortunately, as the arguments travel through the CLI interface, there are some escape sequences that become mangled. By doubling the literal backslashes in the JSON data, we effectively escape where we should escape again. --- github_status_embed/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/github_status_embed/types.py b/github_status_embed/types.py index 0531996..900ec74 100644 --- a/github_status_embed/types.py +++ b/github_status_embed/types.py @@ -206,7 +206,7 @@ class PullRequest(TypedDataclass, optional=True): def from_payload(cls, arguments: typing.Dict[str, str]) -> typing.Optional[PullRequest]: """Create a Pull Request instance from Pull Request Payload JSON.""" # Safe load the JSON Payload provided as a command line argument. - raw_payload = arguments.pop('pull_request_payload') + raw_payload = arguments.pop('pull_request_payload').replace("\\", "\\\\") log.debug(f"Attempting to parse PR Payload JSON: {raw_payload!r}.") try: payload = json.loads(raw_payload) From 226ab2d387a0573b04219b3b53ac8726d3daf7b6 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff Date: Wed, 16 Dec 2020 11:24:11 +0100 Subject: [PATCH 2/6] Use defaults for args available in environment A number of arguments that the action needs are available using the `github` context provided in a workflow. While I still want to make keep the embed fully configurable, I've now set default values that make sense in most situations. One caveat is that if the embed is sent from a `workflow_run` event (e.g., to allow for accessing secrets), the `github` context will be that of the `workflow_run` trigger; not the workflow that triggered the workflow_run-triggered workflow. --- action.yaml | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/action.yaml b/action.yaml index f1afb00..b8d5b32 100644 --- a/action.yaml +++ b/action.yaml @@ -5,15 +5,18 @@ description: Send an enhanced GitHub Actions status embed to a Discord webhook inputs: workflow_name: description: 'name of the workflow' - required: true + required: false + default: ${{ github.workflow }} run_id: description: 'run id of this workflow run' - required: true + required: false + default: ${{ github.run_id }} run_number: description: 'number of this workflow run' - required: true + required: false + default: ${{ github.run_number }} status: description: 'results status communicated with this embed' @@ -21,19 +24,23 @@ inputs: repository: description: 'GitHub repository name, including owner' - required: true + required: false + default: ${{ github.repository }} actor: description: 'actor that initiated the workflow run' - required: true + required: false + default: ${{ github.actor }} ref: description: 'The branch or tag that triggered the workflow' - required: true + required: false + default: ${{ github.ref }} sha: description: 'sha of the commit that triggered the workflow' - required: true + required: false + default: ${{ github.sha }} webhook_id: description: 'ID of the Discord webhook that is targeted' @@ -64,12 +71,12 @@ inputs: required: false debug: - description: 'Pull Request in jSON payload form' + description: 'Output debug logging as annotations' required: false default: 'false' dry_run: - description: 'Pull Request in jSON payload form' + description: 'Do not send a request to the webhook endpoint' required: false default: 'false' From e35260c80233e9af0edb895f163e3389d62381f7 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff Date: Wed, 16 Dec 2020 11:40:43 +0100 Subject: [PATCH 3/6] Add deprecation warning for `pull_request_payload` Unfortunately, passing JSON serialized strings to the Python package as command line arguments parsed by argparse led to some issues with escape sequences getting mangled. While I've introduced a fix for the issue, it's quite fragile and I'm not confident enough in the fix that I want to continue supporting it in the first major release. That's why I've added a deprecation warning to communicate that it will be removed in version 1.0.0. --- github_status_embed/__main__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/github_status_embed/__main__.py b/github_status_embed/__main__.py index a22868e..bd3d5bb 100644 --- a/github_status_embed/__main__.py +++ b/github_status_embed/__main__.py @@ -64,6 +64,7 @@ workflow = Workflow.from_arguments(arguments) webhook = Webhook.from_arguments(arguments) if arguments.get("pull_request_payload", False): + log.warning("The use of `pull_request_payload` is deprecated and will be removed in v1.0.0") pull_request = PullRequest.from_payload(arguments) else: pull_request = PullRequest.from_arguments(arguments) From 8d045493ec8c674cc33b66e95d3e038160c2f00b Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff Date: Wed, 16 Dec 2020 12:07:53 +0100 Subject: [PATCH 4/6] Update README to reflect making arguments optional --- README.md | 48 ++++++++++++++++++------------------------------ 1 file changed, 18 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 65e6b0c..df72034 100644 --- a/README.md +++ b/README.md @@ -56,16 +56,6 @@ jobs: webhook_id: '1234567890' # Has to be provided as a string webhook_token: ${{ secrets.webhook_token }} - # Information about the current workflow - workflow_name: ${{ github.workflow }} - run_id: ${{ github.run_id }} - run_number: ${{ github.run_number }} - status: ${{ job.status }} - actor: ${{ github.actor }} - repository: ${{ github.repository }} - ref: ${{ github.ref }} - sha: ${{ github.sha }} - # Optional arguments for PR-related events # Note: There's no harm in including these lines for non-PR # events, as non-existing paths in objects will evaluate to @@ -75,33 +65,31 @@ jobs: pr_number: ${{ github.event.pull_request.number }} pr_title: ${{ github.event.pull_request.title }} pr_source: ${{ github.event.pull_request.head.label }} - # Or simply provide the raw Pull Request payload in JSON format - pull_request_payload: ${{ toJson(github.event.pull_request) }} ``` ### Command specification **Note:** The example step above contains the typical way of providing the arguments. -| Argument | Description | Required | +| Argument | Description | Default | | --- | --- | :---: | -| webhook_id | ID of the Discord webhook (use a string) | yes | -| webhook_token | Token of the Discord webhook | yes | -| workflow_name | Name of the workflow | yes | -| run_id | Run ID of the workflow | yes | -| run_number | Run number of the workflow | yes | -| status | Status for the embed; one of ["succes", "failure", "cancelled"] | yes | -| actor | Actor who requested the workflow | yes | -| repository | Repository; has to be in form `owner/repo` | yes | -| ref | Branch or tag ref that triggered the workflow run | yes | -| sha | Full commit SHA that triggered the workflow run. | yes | -| pr_author_login | **Login** of the Pull Request author | no¹ | -| pr_number | Pull Request number | no¹ | -| pr_title | Title of the Pull Request | no¹ | -| pr_source | Source branch for the Pull Request | no¹ | -| pull_request_payload | PR payload in JSON format² | no³ | -| debug | set to "true" to turn on debug logging | no | -| dry_run | set to "true" to not send the webhook request | no | +| status | Status for the embed; one of ["succes", "failure", "cancelled"] | (required) | +| webhook_id | ID of the Discord webhook (use a string) | (required) | +| webhook_token | Token of the Discord webhook | (required) | +| workflow_name | Name of the workflow | github.workflow | +| run_id | Run ID of the workflow | github.run_id | +| run_number | Run number of the workflow | github.run_number | +| actor | Actor who requested the workflow | github.actor | +| repository | Repository; has to be in form `owner/repo` | github.repository | +| ref | Branch or tag ref that triggered the workflow run | github.ref | +| sha | Full commit SHA that triggered the workflow run. | github.sha | +| pr_author_login | **Login** of the Pull Request author | (optional)¹ | +| pr_number | Pull Request number | (optional)¹ | +| pr_title | Title of the Pull Request | (optional)¹ | +| pr_source | Source branch for the Pull Request | (optional)¹ | +| debug | set to "true" to turn on debug logging | false | +| dry_run | set to "true" to not send the webhook request | false | +| pull_request_payload | PR payload in JSON format² **(deprecated)** | (deprecated)³ | 1) The Action will determine whether to send an embed tailored towards a Pull Request Check Run or towards a general workflow run based on the presence of non-null values for the four pull request arguments. This means that you either have to provide **all** of them or **none** of them. From 870e5979d9a73d41278deb62a779521256c9456c Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff Date: Wed, 16 Dec 2020 12:08:45 +0100 Subject: [PATCH 5/6] Add recipe for using workflow_run for PR embeds --- README.md | 111 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 109 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index df72034..ae8a0c8 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ jobs: ### Command specification -**Note:** The example step above contains the typical way of providing the arguments. +**Note:** The default values assume that the workflow you want to report the status of is also the workflow that is running this action. If this is not possible (e.g., because you don't have access to secrets in a `pull_request`-triggered workflow), you could use a `workflow_run` triggered workflow that reports the status of the workflow that triggered it. See the recipes section below for an example. | Argument | Description | Default | | --- | --- | :---: | @@ -101,4 +101,111 @@ jobs: ## Recipes -To be added. +### Reporting the status of a `pull_request`-triggered workflow + +One complication with `pull_request`-triggered workflows is that your secrets won't be available if the workflow is triggered for a pull request made from a fork. As you'd typically provide the webhook token as a secret, this makes using this action in such a workflow slightly more complicated. + +However, GitHub has provided an additional workflow trigger specifically for this situation: [`workflow_run`](https://docs.github.com/en/free-pro-team@latest/actions/reference/events-that-trigger-workflows#workflow_run). You can use this event to start a workflow whenever another workflow is being run or has just finished. As workflows triggered by `workflow_run` always run in the base repository, it has full access to your secrets. + +To give your `workflow_run`-triggered workflow access to all the information we need to build a Pull Request status embed, you'll need to share some details from the original workflow in some way. One way to do that is by uploading an artifact. To do that, add these two steps to the end of your `pull_request`-triggered workflow: + +```yaml +name: Lint & Test + +on: + pull_request: + + +jobs: + lint-test: + runs-on: ubuntu-latest + + steps: + # Your regular steps here + + # ------------------------------------------------------------------------------- + + # Prepare the Pull Request Payload artifact. If this fails, we + # we fail silently using the `continue-on-error` option. It's + # nice if this succeeds, but if it fails for any reason, it + # does not mean that our lint-test checks failed. + - name: Prepare Pull Request Payload artifact + id: prepare-artifact + if: always() && github.event_name == 'pull_request' + continue-on-error: true + run: cat $GITHUB_EVENT_PATH | jq '.pull_request' > pull_request_payload.json + + # This only makes sense if the previous step succeeded. To + # get the original outcome of the previous step before the + # `continue-on-error` conclusion is applied, we use the + # `.outcome` value. This step also fails silently. + - name: Upload a Build Artifact + if: always() && steps.prepare-artifact.outcome == 'success' + continue-on-error: true + uses: actions/upload-artifact@v2 + with: + name: pull-request-payload + path: pull_request_payload.json +``` + +Then, add a new workflow that is triggered whenever the workflow above is run: + +```yaml +name: Status Embed + +on: + workflow_run: + workflows: + - Lint & Test + types: + - completed + +jobs: + status_embed: + name: Send Status Embed to Discord + runs-on: ubuntu-latest + + steps: + # Process the artifact uploaded in the `pull_request`-triggered workflow: + - name: Get Pull Request Information + id: pr_info + if: github.event.workflow_run.event == 'pull_request' + run: | + curl -s -H "Authorization: token $GITHUB_TOKEN" ${{ github.event.workflow_run.artifacts_url }} > artifacts.json + DOWNLOAD_URL=$(cat artifacts.json | jq -r '.artifacts[] | select(.name == "pull-request-payload") | .archive_download_url') + [ -z "$DOWNLOAD_URL" ] && exit 1 + wget --quiet --header="Authorization: token $GITHUB_TOKEN" -O pull_request_payload.zip $DOWNLOAD_URL || exit 2 + unzip -p pull_request_payload.zip > pull_request_payload.json + [ -s pull_request_payload.json ] || exit 3 + echo "::set-output name=pr_author_login::$(jq -r '.user.login // empty' pull_request_payload.json)" + echo "::set-output name=pr_number::$(jq -r '.number // empty' pull_request_payload.json)" + echo "::set-output name=pr_title::$(jq -r '.title // empty' pull_request_payload.json)" + echo "::set-output name=pr_source::$(jq -r '.head.label // empty' pull_request_payload.json)" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # Send an informational status embed to Discord instead of the + # standard embeds that Discord sends. This embed will contain + # more information and we can fine tune when we actually want + # to send an embed. + - name: GitHub Actions Status Embed for Discord + uses: SebastiaanZ/github-status-embed-for-discord@v0.2.1 + with: + # Webhook token + webhook_id: '1234567' + webhook_token: ${{ secrets.webhook_token }} + + # We need to provide the information of the workflow that + # triggered this workflow instead of this workflow. + workflow_name: ${{ github.event.workflow_run.name }} + run_id: ${{ github.event.workflow_run.id }} + run_number: ${{ github.event.workflow_run.run_number }} + status: ${{ github.event.workflow_run.conclusion }} + sha: ${{ github.event.workflow_run.head_sha }} + + # Now we can use the information extracted in the previous step: + pr_author_login: ${{ steps.pr_info.outputs.pr_author_login }} + pr_number: ${{ steps.pr_info.outputs.pr_number }} + pr_title: ${{ steps.pr_info.outputs.pr_title }} + pr_source: ${{ steps.pr_info.outputs.pr_source }} +``` From 6dc926caa6bfcc9656960fd367ba1d226dc2848b Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff Date: Wed, 16 Dec 2020 12:50:06 +0100 Subject: [PATCH 6/6] Add validation rule for empty sized containers As it seems, GitHub passes unset values as empty strings, meaning that they pass validation for the string type. To prevent empty strings from making it past the verification feature, I've now added size validation for whenever a value is a subclass of `collections.abc.Sized`. --- github_status_embed/types.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/github_status_embed/types.py b/github_status_embed/types.py index 900ec74..634e441 100644 --- a/github_status_embed/types.py +++ b/github_status_embed/types.py @@ -1,5 +1,6 @@ from __future__ import annotations +import collections import dataclasses import enum import json @@ -71,6 +72,8 @@ def from_arguments(cls, arguments: typing.Dict[str, str]) -> typing.Optional[Typ value = _type[value.upper()] else: value = _type(value) + if isinstance(value, collections.Sized) and len(value) == 0: + raise ValueError except (ValueError, KeyError): raise InvalidArgument(f"invalid value for `{attribute}`: {value}") from None else: