diff --git a/task/github-add-comment/0.8/README.md b/task/github-add-comment/0.8/README.md new file mode 100644 index 0000000000..94c183f361 --- /dev/null +++ b/task/github-add-comment/0.8/README.md @@ -0,0 +1,149 @@ +# Add a comment to an issue or a pull request + +The `github-add-comment` task let you add a comment to a pull request or an +issue. + +## Install the Task + +``` +kubectl apply -f https://api.hub.tekton.dev/v1/resource/tekton/task/github-add-comment/0.7/raw +``` + +## Secrets + +This Task requires access to a GitHub token set via a Kubernetes Secret. By default, the name of this Secret should be `github` and the secret key should be `token`, but you can configure this via the `GITHUB_TOKEN_SECRET_NAME` and `GITHUB_TOKEN_SECRET_KEY` [parameters](#parameters) described below. + +To create such a Secret via `kubectl`: + +``` +kubectl create secret generic github --from-literal token="MY_TOKEN" +``` + +Check [this](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line) to get personal access token for `Github`. + +See GitHub's documentation on [Understanding scopes for OAuth Apps](https://developer.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps/) to figure out what scopes you need to give to this token to add comment to an issue or a pull request. + +## Parameters + +- **GITHUB_HOST_URL:**: The GitHub host domain (_default:_ `api.github.com`) +- **API_PATH_PREFIX:**: The GitHub Enterprise has a prefix for the API path. _e.g:_ `/api/v3` +- **REQUEST_URL:**: The GitHub pull request or issue url, _e.g:_ + `https://github.com/tektoncd/catalog/issues/46` +- **COMMENT_OR_FILE:**: The actual comment to add or the filename inside the + optional workspace `comment-file` containing comment to post. _e.g:_ `don't forget to eat your vegetables before commiting.` _or_ `input.txt` +- **COMMENT_TAG:**: An invisible tag to be added into the comment. The tag is + made invisible by embedding in an an HTML comment. The tag allows + for later retrieval of the comment, and it allows replacing an existing comment. _e.g._ `myservice.[commit-sha]`. (_default:_ `""`). +- **REPLACE:**: When a tag is specified, and `REPLACE` is `true`, look for a + comment with a matching tag and replace it with the new comment. (_default:_ `false`). +- **DELETE:**: When a tag is specified and `DELETE` is true, look for a comment + with a matching tag and delete it, instead of adding a new comment. (_default:_ `false`). +- **GITHUB_TOKEN_SECRET_NAME**: The name of the Kubernetes Secret that + contains the GitHub token. (_default:_ `github`). +- **GITHUB_TOKEN_SECRET_KEY**: The key within the Kubernetes Secret that contains the GitHub token. (_default:_ `token`). + +## Results + +- **OLD_COMMENT:**: The old text of the comment, if any. +- **NEW_COMMENT:**: The new text of the comment, if any. + +## Workspaces + +- **comment-file**: The optional workspace containing comment file to be posted. + +## Platforms + +The Task can be run on `linux/amd64`, `linux/s390x` and `linux/ppc64le` platforms. + +## Usage + +This TaskRun add a comment to an issue. + +```yaml +--- +apiVersion: tekton.dev/v1beta1 +kind: TaskRun +metadata: + labels: + tekton.dev/task: github-add-comment + name: github-add-comment-to-pr-22 +spec: + taskRef: + kind: Task + name: github-add-comment + params: + - name: REQUEST_URL + value: https://github.com/chmouel/scratchpad/pull/46 + - name: COMMENT_OR_FILE + value: | + The cat went here and there + And the moon spun round like a top, + And the nearest kin of the moon, + The creeping cat, looked up. + Black Minnaloushe stared at the moon, + For, wander and wail as he would, + The pure cold light in the sky + Troubled his animal blood. +``` + +### When passing a comment via file + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: comment-cm +data: + input.txt: | + This is the sample input comment via file. +--- +apiVersion: tekton.dev/v1beta1 +kind: TaskRun +metadata: + labels: + tekton.dev/task: github-add-comment + name: github-add-comment-to-pr-22 +spec: + taskRef: + kind: Task + name: github-add-comment + workspace: + - name: comment-file + configMap: + name: comment-cm + params: + - name: REQUEST_URL + value: https://github.com/chmouel/scratchpad/pull/46 + - name: COMMENT_OR_FILE + value: "input.txt" +``` + +### This TaskRun replaces a comment in an issue + +```yaml +--- +apiVersion: tekton.dev/v1beta1 +kind: TaskRun +metadata: + name: github-add-comment-to-pr-22 +spec: + taskRef: + kind: Task + name: github-add-comment + params: + - name: REQUEST_URL + value: https://github.com/chmouel/scratchpad/pull/46 + - name: COMMENT_TAG + value: catalog-sha123abc + - name: REPLACE + value: "true" + - name: COMMENT_OR_FILE + value: | + The cat went here and there + And the moon spun round like a top, + And the nearest kin of the moon, + The creeping cat, looked up. + Black Minnaloushe stared at the moon, + For, wander and wail as he would, + The pure cold light in the sky + Troubled his animal blood. diff --git a/task/github-add-comment/0.8/github-add-comment.yaml b/task/github-add-comment/0.8/github-add-comment.yaml new file mode 100644 index 0000000000..b80e6fa6d4 --- /dev/null +++ b/task/github-add-comment/0.8/github-add-comment.yaml @@ -0,0 +1,216 @@ +--- +apiVersion: tekton.dev/v1beta1 +kind: Task +metadata: + name: github-add-comment + labels: + app.kubernetes.io/version: "0.8" + annotations: + tekton.dev/categories: Git + tekton.dev/pipelines.minVersion: "0.17.0" + tekton.dev/tags: github + tekton.dev/displayName: "add github comment" + tekton.dev/platforms: "linux/amd64,linux/s390x,linux/ppc64le" +spec: + description: >- + This Task will add a comment to a pull request or an issue. + + It can take either a filename or a comment as input and can + post the comment back to GitHub accordingly. + + workspaces: + - name: comment-file + optional: true + description: The optional workspace containing comment file to be posted. + + results: + - name: OLD_COMMENT + description: The old text of the comment, if any. + + - name: NEW_COMMENT + description: The new text of the comment, if any. + + params: + - name: GITHUB_HOST_URL + description: | + The GitHub host, adjust this if you run a GitHub enteprise. + default: "api.github.com" + type: string + + - name: API_PATH_PREFIX + description: | + The API path prefix, GitHub Enterprise has a prefix e.g. /api/v3 + default: "" + type: string + + - name: REQUEST_URL + description: | + The GitHub issue or pull request URL where we want to add a new + comment. + type: string + + - name: COMMENT_OR_FILE + description: | + The actual comment to add or the filename containing comment to post. + type: string + + - name: GITHUB_TOKEN_SECRET_NAME + description: | + The name of the Kubernetes Secret that contains the GitHub token. + type: string + default: github + + - name: GITHUB_TOKEN_SECRET_KEY + description: | + The key within the Kubernetes Secret that contains the GitHub token. + type: string + default: token + + - name: AUTH_TYPE + description: | + The type of authentication to use. You could use the less secure "Basic" for example + type: string + default: Bearer + + - name: COMMENT_TAG + description: | + An invisible tag to be added into the comment. The tag is made + invisible by embedding in an an HTML comment. The tag allows for later + retrieval of the comment, and it allows replacing an existing comment. + type: string + default: "" + + - name: REPLACE + description: | + When a tag is specified, and `REPLACE` is `true`, look for a comment + with a matching tag and replace it with the new comment. + type: string + default: "false" # Alternative value: "true" + + - name: DELETE + description: | + When a tag is specified, and `DELETE` is `true`, look for a comment + with a matching tag and delete it instead of adding a new comment. + type: string + default: "false" # Alternative value: "true" + + steps: + - name: post-comment + workingDir: $(workspaces.comment-file.path) + env: + - name: GITHUBTOKEN + valueFrom: + secretKeyRef: + name: $(params.GITHUB_TOKEN_SECRET_NAME) + key: $(params.GITHUB_TOKEN_SECRET_KEY) + + image: registry.access.redhat.com/ubi8/ubi-minimal:8.2 + script: | + #!/usr/libexec/platform-python + import json + import os + import http.client + import sys + import urllib.parse + + authHeader = "$(params.AUTH_TYPE) " + os.environ["GITHUBTOKEN"] + + split_url = urllib.parse.urlparse( + "$(params.REQUEST_URL)").path.split("/") + + # This will convert https://github.com/foo/bar/pull/202 to + # api url path /repos/foo/issues/ + api_url = "{base}/repos/{package}/issues/{id}".format( + base="$(params.API_PATH_PREFIX)", package="/".join(split_url[1:3]), id=split_url[-1]) + + commentParamValue = """$(params.COMMENT_OR_FILE)""" + + # check if workspace is bound and parameter passed is a filename or not + if "$(workspaces.comment-file.bound)" == "true" and os.path.exists(commentParamValue): + commentParamValue = open(commentParamValue, "r").read() + + # If a tag was specified, append it to the comment + if "$(params.COMMENT_TAG)": + commentParamValue += "".format(tag="$(params.COMMENT_TAG)") + + data = { + "body": commentParamValue, + } + + # This is for our fake github server + if "$(params.GITHUB_HOST_URL)".startswith("http://"): + conn = http.client.HTTPConnection("$(params.GITHUB_HOST_URL)".replace("http://", "")) + else: + conn = http.client.HTTPSConnection("$(params.GITHUB_HOST_URL)") + + # If REPLACE is true, we need to search for comments first + matching_comment = "" + if "$(params.REPLACE)" == "true" or "$(params.DELETE)" == "true": + if not "$(params.COMMENT_TAG)": + print("REPLACE or DELETE requested but no COMMENT_TAG specified") + sys.exit(1) + r = conn.request( + "GET", + api_url + "/comments", + headers={ + "User-Agent": "TektonCD, the peaceful cat", + "Authorization": authHeader, + }) + + resp = conn.getresponse() + if not str(resp.status).startswith("2"): + print("Error: %d" % (resp.status)) + print(resp.read()) + sys.exit(1) + print(resp.status) + + comments = json.loads(resp.read()) + print(comments) + # If more than one comment is found take the last one + matching_comment = [x for x in comments if '$(params.COMMENT_TAG)' in x['body']][-1:] + if matching_comment: + with open("$(results.OLD_COMMENT.path)", "w") as result_old: + result_old.write(str(matching_comment[0])) + matching_comment = matching_comment[0]['url'] + + if matching_comment: + if "$(params.DELETE)" == "true": + method = "DELETE" + else: + method = "PATCH" + target_url = urllib.parse.urlparse(matching_comment).path + else: + method = "POST" + target_url = api_url + "/comments" + + # if DELETE is true, don't send any data. + if method == "DELETE": + print("Deleting comment on GitHub at {}".format(target_url)) + r = conn.request( + method, + target_url, + headers={ + "User-Agent": "TektonCD, the peaceful cat", + "Authorization": authHeader, + }) + else: + print("Sending this data to GitHub with {}: ".format(method)) + print(data) + r = conn.request( + method, + target_url, + body=json.dumps(data), + headers={ + "User-Agent": "TektonCD, the peaceful cat", + "Authorization": authHeader, + }) + resp = conn.getresponse() + if not str(resp.status).startswith("2"): + print("Error: %d" % (resp.status)) + print(resp.read()) + sys.exit(1) + elif method != "DELETE: + with open("$(results.NEW_COMMENT.path)", "wb") as result_new: + result_new.write(resp.read()) + print("a GitHub comment has been {} to $(params.REQUEST_URL)".format( + "updated" if matching_comment else "added")) diff --git a/task/github-add-comment/0.8/tests/fixtures/github-post-comment.yaml b/task/github-add-comment/0.8/tests/fixtures/github-post-comment.yaml new file mode 100644 index 0000000000..25d827ac95 --- /dev/null +++ b/task/github-add-comment/0.8/tests/fixtures/github-post-comment.yaml @@ -0,0 +1,47 @@ +# Default GitHub +--- +headers: + method: PATCH + path: /repos/{repo:[^/]+/[^/]+}/issues/comments/{comment:[0-9]+} +response: + status: 200 + output: '{"status": 200}' + content-type: text/json +--- +headers: + method: GET + path: /repos/{repo:[^/]+/[^/]+}/issues/{issue:[0-9]+}/comments +response: + status: 200 + content-type: text/json + file: /fixtures/list-comment-response.json +--- +headers: + method: DELETE + path: /repos/{repo:[^/]+/[^/]+}/issues/comments/{comment:[0-9]+} +response: + status: 204 + +# GitHub Enterprise Server +--- +headers: + method: PATCH + path: /api/v3/repos/{repo:[^/]+/[^/]+}/issues/comments/{comment:[0-9]+} +response: + status: 200 + output: '{"status": 200}' + content-type: text/json +--- +headers: + method: GET + path: /api/v3/repos/{repo:[^/]+/[^/]+}/issues/{issue:[0-9]+}/comments +response: + status: 200 + content-type: text/json + file: /fixtures/list-comment-response-ghe.json +--- +headers: + method: DELETE + path: /api/v3/repos/{repo:[^/]+/[^/]+}/issues/comments/{comment:[0-9]+} +response: + status: 204 diff --git a/task/github-add-comment/0.8/tests/fixtures/list-comment-response-ghe.json b/task/github-add-comment/0.8/tests/fixtures/list-comment-response-ghe.json new file mode 100644 index 0000000000..ca98f81961 --- /dev/null +++ b/task/github-add-comment/0.8/tests/fixtures/list-comment-response-ghe.json @@ -0,0 +1,66 @@ +[ + { + "url": "https://github.example.com/api/v3/repos/tektoncd/plumbing/issues/comments/111111111111", + "html_url": "https://github.example.com/tektoncd/plumbing/pull/111#issuecomment-111111111111", + "issue_url": "https://github.example.com/api/v3/repos/tektoncd/plumbing/issues/111", + "id": 111111111111, + "node_id": "abcd==", + "user": { + "login": "johndoe", + "id": 12345, + "node_id": "MDQ6VXNlcjk4OTgw", + "avatar_url": "https://github.example.com/avatars/u/12345?v=4", + "gravatar_id": "", + "url": "https://github.example.com/api/v3/users/johndoe", + "html_url": "https://github.example.com/johndoe", + "followers_url": "https://github.example.com/api/v3/users/johndoe/followers", + "following_url": "https://github.example.com/api/v3/users/johndoe/following{/other_user}", + "gists_url": "https://github.example.com/api/v3/users/johndoe/gists{/gist_id}", + "starred_url": "https://github.example.com/api/v3/users/johndoe/starred{/owner}{/repo}", + "subscriptions_url": "https://github.example.com/api/v3/users/johndoe/subscriptions", + "organizations_url": "https://github.example.com/api/v3/users/johndoe/orgs", + "repos_url": "https://github.example.com/api/v3/users/johndoe/repos", + "events_url": "https://github.example.com/api/v3/users/johndoe/events{/privacy}", + "received_events_url": "https://github.example.com/api/v3/users/johndoe/received_events", + "type": "User", + "site_admin": false + }, + "created_at": "2021-01-18T12:56:24Z", + "updated_at": "2021-01-18T12:56:24Z", + "author_association": "MEMBER", + "body": "/kind feature", + "performed_via_github_app": null + }, + { + "url": "https://github.example.com/api/v3/repos/tektoncd/plumbing/issues/comments/2222222222", + "html_url": "https://github.example.com/tektoncd/plumbing/pull/111#issuecomment-2222222222", + "issue_url": "https://github.example.com/api/v3/repos/tektoncd/plumbing/issues/111", + "id": 2222222222, + "node_id": "MDEyOklzc3VlQ29tbWVudDc2MjIzNDExNg==", + "user": { + "login": "johndoe", + "id": 12345, + "node_id": "123abc123", + "avatar_url": "https://github.example.com/avatars/u/12345?v=4", + "gravatar_id": "", + "url": "https://github.example.com/api/v3/users/johndoe", + "html_url": "https://github.example.com/johndoe", + "followers_url": "https://github.example.com/api/v3/users/johndoe/followers", + "following_url": "https://github.example.com/api/v3/users/johndoe/following{/other_user}", + "gists_url": "https://github.example.com/api/v3/users/johndoe/gists{/gist_id}", + "starred_url": "https://github.example.com/api/v3/users/johndoe/starred{/owner}{/repo}", + "subscriptions_url": "https://github.example.com/api/v3/users/johndoe/subscriptions", + "organizations_url": "https://github.example.com/api/v3/users/johndoe/orgs", + "repos_url": "https://github.example.com/api/v3/users/johndoe/repos", + "events_url": "https://github.example.com/api/v3/users/johndoe/events{/privacy}", + "received_events_url": "https://github.example.com/api/v3/users/johndoe/received_events", + "type": "User", + "site_admin": false + }, + "created_at": "2021-01-18T12:57:02Z", + "updated_at": "2021-01-18T12:57:02Z", + "author_association": "MEMBER", + "body": "Comment on GHE", + "performed_via_github_app": null + } +] \ No newline at end of file diff --git a/task/github-add-comment/0.8/tests/fixtures/list-comment-response.json b/task/github-add-comment/0.8/tests/fixtures/list-comment-response.json new file mode 100644 index 0000000000..e9221d620e --- /dev/null +++ b/task/github-add-comment/0.8/tests/fixtures/list-comment-response.json @@ -0,0 +1,66 @@ +[ + { + "url": "https://api.github.com/repos/tektoncd/plumbing/issues/comments/111111111111", + "html_url": "https://github.com/tektoncd/plumbing/pull/111#issuecomment-111111111111", + "issue_url": "https://api.github.com/repos/tektoncd/plumbing/issues/111", + "id": 111111111111, + "node_id": "abcd==", + "user": { + "login": "johndoe", + "id": 12345, + "node_id": "MDQ6VXNlcjk4OTgw", + "avatar_url": "https://avatars.githubusercontent.com/u/12345?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/johndoe", + "html_url": "https://github.com/johndoe", + "followers_url": "https://api.github.com/users/johndoe/followers", + "following_url": "https://api.github.com/users/johndoe/following{/other_user}", + "gists_url": "https://api.github.com/users/johndoe/gists{/gist_id}", + "starred_url": "https://api.github.com/users/johndoe/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/johndoe/subscriptions", + "organizations_url": "https://api.github.com/users/johndoe/orgs", + "repos_url": "https://api.github.com/users/johndoe/repos", + "events_url": "https://api.github.com/users/johndoe/events{/privacy}", + "received_events_url": "https://api.github.com/users/johndoe/received_events", + "type": "User", + "site_admin": false + }, + "created_at": "2021-01-18T12:56:24Z", + "updated_at": "2021-01-18T12:56:24Z", + "author_association": "MEMBER", + "body": "/kind feature", + "performed_via_github_app": null + }, + { + "url": "https://api.github.com/repos/tektoncd/plumbing/issues/comments/2222222222", + "html_url": "https://github.com/tektoncd/plumbing/pull/111#issuecomment-2222222222", + "issue_url": "https://api.github.com/repos/tektoncd/plumbing/issues/111", + "id": 2222222222, + "node_id": "MDEyOklzc3VlQ29tbWVudDc2MjIzNDExNg==", + "user": { + "login": "johndoe", + "id": 12345, + "node_id": "123abc123", + "avatar_url": "https://avatars.githubusercontent.com/u/12345?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/johndoe", + "html_url": "https://github.com/johndoe", + "followers_url": "https://api.github.com/users/johndoe/followers", + "following_url": "https://api.github.com/users/johndoe/following{/other_user}", + "gists_url": "https://api.github.com/users/johndoe/gists{/gist_id}", + "starred_url": "https://api.github.com/users/johndoe/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/johndoe/subscriptions", + "organizations_url": "https://api.github.com/users/johndoe/orgs", + "repos_url": "https://api.github.com/users/johndoe/repos", + "events_url": "https://api.github.com/users/johndoe/events{/privacy}", + "received_events_url": "https://api.github.com/users/johndoe/received_events", + "type": "User", + "site_admin": false + }, + "created_at": "2021-01-18T12:57:02Z", + "updated_at": "2021-01-18T12:57:02Z", + "author_association": "MEMBER", + "body": "Comment on GH public", + "performed_via_github_app": null + } +] \ No newline at end of file diff --git a/task/github-add-comment/0.8/tests/pre-apply-task-hook.sh b/task/github-add-comment/0.8/tests/pre-apply-task-hook.sh new file mode 100644 index 0000000000..c2aa6f551e --- /dev/null +++ b/task/github-add-comment/0.8/tests/pre-apply-task-hook.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +kubectl -n ${tns} create secret generic github --from-literal token="secret" diff --git a/task/github-add-comment/0.8/tests/run.yaml b/task/github-add-comment/0.8/tests/run.yaml new file mode 100644 index 0000000000..2b42c453e8 --- /dev/null +++ b/task/github-add-comment/0.8/tests/run.yaml @@ -0,0 +1,95 @@ +--- +apiVersion: tekton.dev/v1beta1 +kind: Pipeline +metadata: + name: github-add-comment-test +spec: + params: + - name: API_PATH_PREFIX + type: string + default: "" + - name: EXPECTED_OLD_COMMENT + type: string + default: "Comment on GH public" + workspaces: + - name: comment-file + optional: true + tasks: + - name: add-comment + taskRef: + name: github-add-comment + workspaces: + - name: comment-file + workspace: comment-file + params: + - name: GITHUB_HOST_URL + value: http://127.0.0.1:8080 + - name: API_PATH_PREFIX + value: $(params.API_PATH_PREFIX) + - name: COMMENT_OR_FILE + value: "Hello from TektonCD test" + - name: REQUEST_URL + value: https://github.com/tektoncd/catalog/issues/1 + - name: REPLACE + value: "true" + - name: COMMENT_TAG + value: TEST_TAG123 + - name: verify-result + runAfter: + - add-comment + params: + - name: EXPECTED_OLD_COMMENT + value: $(params.EXPECTED_OLD_COMMENT) + - name: ACTUAL_OLD_COMMENT + value: $(tasks.add-comment.results.OLD_COMMENT) + taskSpec: + params: + - name: EXPECTED_OLD_COMMENT + type: string + - name: ACTUAL_OLD_COMMENT + type: string + steps: + - image: registry.access.redhat.com/ubi8/ubi-minimal:8.2 + script: | + #!/usr/libexec/platform-python + + assert "$(params.EXPECTED_OLD_COMMENT)" in "$(params.ACTUAL_OLD_COMMENT)" + - name: delete-comment + runAfter: + - verify-result + taskRef: + name: github-add-comment + params: + - name: GITHUB_HOST_URL + value: http://127.0.0.1:8080 + - name: API_PATH_PREFIX + value: $(params.API_PATH_PREFIX) + - name: COMMENT_OR_FILE + value: "" + - name: REQUEST_URL + value: https://github.com/tektoncd/catalog/issues/1 + - name: DELETE + value: "true" + - name: COMMENT_TAG + value: TEST_TAG123 +--- +apiVersion: tekton.dev/v1beta1 +kind: PipelineRun +metadata: + name: github-add-comment-test-run +spec: + pipelineRef: + name: github-add-comment-test +--- +apiVersion: tekton.dev/v1beta1 +kind: PipelineRun +metadata: + name: github-add-comment-test-run-ghe +spec: + pipelineRef: + name: github-add-comment-test + params: + - name: API_PATH_PREFIX + value: "/api/v3" + - name: EXPECTED_OLD_COMMENT + value: "Comment on GHE"