From 001db0b87850adf187a4cd089a461bd0975ea98d Mon Sep 17 00:00:00 2001 From: Thomas Stringer Date: Thu, 14 Jul 2022 22:28:23 -0400 Subject: [PATCH 01/60] Troubleshoot and log failed group expansion Currently group expansion is failing. I suspect this is due to workflow permissions, as this succeeds locally. This additional logging should show unexpected errors that aren't just a non-group expansion. --- approvers.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/approvers.go b/approvers.go index 22bfc32..922948a 100644 --- a/approvers.go +++ b/approvers.go @@ -45,8 +45,10 @@ func retrieveApprovers(client *github.Client, repoOwner string) ([]string, error } func expandGroupFromUser(client *github.Client, org, userOrTeam string) []string { + fmt.Printf("Attempting to expand user %s/%s as a group (may not succeed)\n", org, userOrTeam) users, _, err := client.Teams.ListTeamMembersBySlug(context.Background(), org, userOrTeam, &github.TeamListTeamMembersOptions{}) - if err != nil || len(users) == 0 { + if err != nil { + fmt.Printf("%v\n", err) return nil } From 8cbc88a78cc3f0ca28bf0ebbad9c3e1f32dab20b Mon Sep 17 00:00:00 2001 From: Thomas Stringer Date: Thu, 14 Jul 2022 22:29:36 -0400 Subject: [PATCH 02/60] Troubleshoot and log failed group expansion (#34) Currently group expansion is failing. I suspect this is due to workflow permissions, as this succeeds locally. This additional logging should show unexpected errors that aren't just a non-group expansion. --- approvers.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/approvers.go b/approvers.go index 22bfc32..922948a 100644 --- a/approvers.go +++ b/approvers.go @@ -45,8 +45,10 @@ func retrieveApprovers(client *github.Client, repoOwner string) ([]string, error } func expandGroupFromUser(client *github.Client, org, userOrTeam string) []string { + fmt.Printf("Attempting to expand user %s/%s as a group (may not succeed)\n", org, userOrTeam) users, _, err := client.Teams.ListTeamMembersBySlug(context.Background(), org, userOrTeam, &github.TeamListTeamMembersOptions{}) - if err != nil || len(users) == 0 { + if err != nil { + fmt.Printf("%v\n", err) return nil } From 46dbdde6568c295c1f026f02d2108f8b24b5d45f Mon Sep 17 00:00:00 2001 From: Thomas Stringer Date: Thu, 14 Jul 2022 22:31:30 -0400 Subject: [PATCH 03/60] Release v1.6.0-rc.2 (#35) --- action.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/action.yaml b/action.yaml index fdf1af3..d2146b6 100644 --- a/action.yaml +++ b/action.yaml @@ -18,4 +18,4 @@ inputs: required: false runs: using: docker - image: docker://ghcr.io/trstringer/manual-approval:1.6.0-rc.1 + image: docker://ghcr.io/trstringer/manual-approval:1.6.0-rc.2 From 1e890961bdd553d244490fa0e9001f32d2b85572 Mon Sep 17 00:00:00 2001 From: Thomas Stringer Date: Sun, 14 Aug 2022 18:52:30 -0400 Subject: [PATCH 04/60] Add documentation for team approver (#39) * Add documentation for team approver This feature is now tested and this PR adds the necessary documentation to show how to implement org team expansion for approvers. Signed-off-by: Thomas Stringer * fix action version Signed-off-by: Thomas Stringer --- README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/README.md b/README.md index 1c116b6..e64c259 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,31 @@ steps: - `minimum-approvals` is an integer that sets the minimum number of approvals required to progress the workflow. Defaults to ALL approvers. - `issue-title` is a string that will be appened to the title of the issue. +## Org team approver + +If you want to have `approvers` set to an org team, then you need to take a different approach. The default [GitHub Actions automatic token](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token) does not have the necessary permissions to list out team members. If you would like to use this then you need to generate a token from a GitHub App with the correct set of permissions. + +Create a GitHub App with **read-only access to organization members**. Once the app is created, add a repo secret with the app ID. In the GitHub App settings, generate a private key and add that as a secret in the repo as well. You can get the app token by using the [`tibdex/github-app-token`](https://github.com/tibdex/github-app-token) GitHub Action: + +```yaml +jobs: + myjob: + runs-on: ubuntu-latest + steps: + - name: Generate token + id: generate_token + uses: tibdex/github-app-token@v1 + with: + app_id: ${{ secrets.APP_ID }} + private_key: ${{ secrets.APP_PRIVATE_KEY }} + - name: Wait for approval + uses: trstringer/manual-approval@v1 + with: + secret: ${{ steps.generate_token.outputs.token }} + approvers: myteam + minimum-approvals: 1 +``` + ## Timeout If you'd like to force a timeout of your workflow pause, you can specify `timeout-minutes` at either the [step](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepstimeout-minutes) level or the [job](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idtimeout-minutes) level. From 367124c80bc5676cbea1b7bbbae6730c50ec66bb Mon Sep 17 00:00:00 2001 From: Thomas Stringer Date: Sun, 14 Aug 2022 19:00:18 -0400 Subject: [PATCH 05/60] Release v1.6.0 (#40) Signed-off-by: Thomas Stringer --- action.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/action.yaml b/action.yaml index d2146b6..d8c79ee 100644 --- a/action.yaml +++ b/action.yaml @@ -18,4 +18,4 @@ inputs: required: false runs: using: docker - image: docker://ghcr.io/trstringer/manual-approval:1.6.0-rc.2 + image: docker://ghcr.io/trstringer/manual-approval:1.6.0 From 2e9a86d9a2233b343eedd5117097e45fe9c3d8ba Mon Sep 17 00:00:00 2001 From: Troy Witthoeft Date: Sun, 21 Aug 2022 13:10:25 -0400 Subject: [PATCH 06/60] Update approvers.go (#44) Handling usernames with whitespace issue = https://github.com/trstringer/manual-approval/issues/43 --- approvers.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/approvers.go b/approvers.go index 922948a..19a8f01 100644 --- a/approvers.go +++ b/approvers.go @@ -16,6 +16,10 @@ func retrieveApprovers(client *github.Client, repoOwner string) ([]string, error requiredApproversRaw := os.Getenv(envVarApprovers) requiredApprovers := strings.Split(requiredApproversRaw, ",") + for i := range requiredApprovers { + requiredApprovers[i] = strings.TrimSpace(requiredApprovers[i]) + } + for _, approverUser := range requiredApprovers { expandedUsers := expandGroupFromUser(client, repoOwner, approverUser) if expandedUsers != nil { From 984d11212e0091c9b087f5c296e62f40be6e8363 Mon Sep 17 00:00:00 2001 From: Wojciech Sielski Date: Thu, 27 Oct 2022 21:06:13 +0200 Subject: [PATCH 07/60] Adapt for Enterprise version - respect GITHUB ENV variables (#52) Co-authored-by: wojciech.sielski --- main.go | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/main.go b/main.go index bb65edc..dfc69fe 100644 --- a/main.go +++ b/main.go @@ -96,13 +96,23 @@ func newCommentLoopChannel(ctx context.Context, apprv *approvalEnvironment, clie return channel } -func newGithubClient(ctx context.Context) *github.Client { +func newGithubClient(ctx context.Context) (*github.Client, error) { token := os.Getenv(envVarToken) ts := oauth2.StaticTokenSource( &oauth2.Token{AccessToken: token}, ) tc := oauth2.NewClient(ctx, ts) - return github.NewClient(tc) + + serverUrl, serverUrlPresent := os.LookupEnv("GITHUB_SERVER_URL") + apiUrl, apiUrlPresent := os.LookupEnv("GITHUB_API_URL") + + if serverUrlPresent { + if ! apiUrlPresent { + apiUrl = serverUrl + } + return github.NewEnterpriseClient(apiUrl, serverUrl, tc) + } + return github.NewClient(tc), nil } func validateInput() error { @@ -148,7 +158,11 @@ func main() { repoOwner := os.Getenv(envVarRepoOwner) ctx := context.Background() - client := newGithubClient(ctx) + client, err := newGithubClient(ctx) + if err != nil { + fmt.Printf("error connecting to server: %v\n", err) + os.Exit(1) + } approvers, err := retrieveApprovers(client, repoOwner) if err != nil { From 955f242e7071d229505dd25bbc089bf5144b31f9 Mon Sep 17 00:00:00 2001 From: Thomas Stringer Date: Sun, 6 Nov 2022 08:12:10 -0500 Subject: [PATCH 08/60] Add documentation for required permissions (#57) Signed-off-by: Thomas Stringer --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index e64c259..96674cf 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,17 @@ steps: ... ``` +## Permissions + +For the action to create a new issue in your project, please ensure that the action has write permissions on issues. You may have to add the following to your workflow: + +```yaml +permissions: + issues: write +``` + +For more information on permissions, please look at the [GitHub documentation](https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs). + ## Limitations * While the workflow is paused, it will still continue to consume a concurrent job allocation out of the [max concurrent jobs](https://docs.github.com/en/actions/learn-github-actions/usage-limits-billing-and-administration#usage-limits). From 0871d5bc6bf3d00a7dba6eaeaad87808294aa50c Mon Sep 17 00:00:00 2001 From: Thomas Stringer Date: Sun, 6 Nov 2022 08:21:37 -0500 Subject: [PATCH 09/60] Create release v1.7.0 (#58) Signed-off-by: Thomas Stringer --- action.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/action.yaml b/action.yaml index d8c79ee..0572f18 100644 --- a/action.yaml +++ b/action.yaml @@ -18,4 +18,4 @@ inputs: required: false runs: using: docker - image: docker://ghcr.io/trstringer/manual-approval:1.6.0 + image: docker://ghcr.io/trstringer/manual-approval:1.7.0 From a7a4994e002ba4d6312e3289a17ba285b2164d3b Mon Sep 17 00:00:00 2001 From: Jens H Date: Wed, 9 Nov 2022 23:51:36 +0100 Subject: [PATCH 10/60] Update README with app token timeout limits (#42) --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 96674cf..085854b 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,8 @@ If you want to have `approvers` set to an org team, then you need to take a diff Create a GitHub App with **read-only access to organization members**. Once the app is created, add a repo secret with the app ID. In the GitHub App settings, generate a private key and add that as a secret in the repo as well. You can get the app token by using the [`tibdex/github-app-token`](https://github.com/tibdex/github-app-token) GitHub Action: +*Note: The GitHub App tokens expire after 1 hour which implies duration for the approval cannot exceed 60 minutes or the job will fail due to bad credentials. See [docs](https://docs.github.com/en/rest/apps/apps#create-an-installation-access-token-for-an-app).* + ```yaml jobs: myjob: From 753e13e9357f41a29eba2d9f2f076ca0bf3c1b2f Mon Sep 17 00:00:00 2001 From: Timothy Ng <5664347+timorthi@users.noreply.github.com> Date: Thu, 10 Nov 2022 09:25:31 -0800 Subject: [PATCH 11/60] Option to exclude workflow initiator (`GITHUB_ACTOR`) as an approver (#59) * Add constant for GITHUB_ACTOR env var * autoformat * Ignore workflow initiator * Fix incorrect if-else * refactor: camelcase userName * fix typo * Add allow-workflow-initiator-as-approver input * Add shouldIncludeWorkflowInitiator * Add usage & description for allow-workflow-initiator-as-approver * Clearer input description * refactor: rename inputs/vars to 'exclude' * update error msg with correct input name * docs: move note on optional/default to description --- README.md | 4 +++- action.yaml | 3 +++ approvers.go | 25 +++++++++++++++++++------ constants.go | 16 +++++++++------- main.go | 2 +- 5 files changed, 35 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 085854b..18a78c2 100644 --- a/README.md +++ b/README.md @@ -32,11 +32,13 @@ steps: approvers: user1,user2,org-team1 minimum-approvals: 1 issue-title: "Deploying v1.3.5 to prod from staging" + exclude-workflow-initiator-as-approver: false ``` - `approvers` is a comma-delimited list of all required approvers. An approver can either be a user or an org team. (*Note: Required approvers must have the ability to be set as approvers in the repository. If you add an approver that doesn't have this permission then you would receive an HTTP/402 Validation Failed error when running this action*) - `minimum-approvals` is an integer that sets the minimum number of approvals required to progress the workflow. Defaults to ALL approvers. -- `issue-title` is a string that will be appened to the title of the issue. +- `issue-title` is a string that will be appended to the title of the issue. +- `exclude-workflow-initiator-as-approver` is a boolean that indicates if the workflow initiator (determined by the `GITHUB_ACTOR` environment variable) should be filtered from the final list of approvers. This is optional and defaults to `false`. Set this to `true` to prevent users in the `approvers` list from being able to self-approve workflows. ## Org team approver diff --git a/action.yaml b/action.yaml index 0572f18..9fd7fdb 100644 --- a/action.yaml +++ b/action.yaml @@ -16,6 +16,9 @@ inputs: issue-title: description: The custom subtitle for the issue required: false + exclude-workflow-initiator-as-approver: + description: Whether or not to filter out the user who initiated the workflow as an approver if they are in the approvers list + default: false runs: using: docker image: docker://ghcr.io/trstringer/manual-approval:1.7.0 diff --git a/approvers.go b/approvers.go index 19a8f01..d7aae15 100644 --- a/approvers.go +++ b/approvers.go @@ -11,19 +11,27 @@ import ( ) func retrieveApprovers(client *github.Client, repoOwner string) ([]string, error) { - approvers := []string{} + workflowInitiator := os.Getenv(envVarWorkflowInitiator) + shouldExcludeWorkflowInitiatorRaw := os.Getenv(envVarExcludeWorkflowInitiatorAsApprover) + shouldExcludeWorkflowInitiator, parseBoolErr := strconv.ParseBool(shouldExcludeWorkflowInitiatorRaw) + if parseBoolErr != nil { + return nil, fmt.Errorf("error parsing exclude-workflow-initiator-as-approver flag: %w", parseBoolErr) + } + approvers := []string{} requiredApproversRaw := os.Getenv(envVarApprovers) requiredApprovers := strings.Split(requiredApproversRaw, ",") for i := range requiredApprovers { - requiredApprovers[i] = strings.TrimSpace(requiredApprovers[i]) + requiredApprovers[i] = strings.TrimSpace(requiredApprovers[i]) } - + for _, approverUser := range requiredApprovers { - expandedUsers := expandGroupFromUser(client, repoOwner, approverUser) + expandedUsers := expandGroupFromUser(client, repoOwner, approverUser, workflowInitiator, shouldExcludeWorkflowInitiator) if expandedUsers != nil { approvers = append(approvers, expandedUsers...) + } else if strings.EqualFold(workflowInitiator, approverUser) && shouldExcludeWorkflowInitiator { + fmt.Printf("Not adding user '%s' as an approver as they are the workflow initiator\n", approverUser) } else { approvers = append(approvers, approverUser) } @@ -48,7 +56,7 @@ func retrieveApprovers(client *github.Client, repoOwner string) ([]string, error return approvers, nil } -func expandGroupFromUser(client *github.Client, org, userOrTeam string) []string { +func expandGroupFromUser(client *github.Client, org, userOrTeam string, workflowInitiator string, shouldExcludeWorkflowInitiator bool) []string { fmt.Printf("Attempting to expand user %s/%s as a group (may not succeed)\n", org, userOrTeam) users, _, err := client.Teams.ListTeamMembersBySlug(context.Background(), org, userOrTeam, &github.TeamListTeamMembersOptions{}) if err != nil { @@ -58,7 +66,12 @@ func expandGroupFromUser(client *github.Client, org, userOrTeam string) []string userNames := make([]string, 0, len(users)) for _, user := range users { - userNames = append(userNames, user.GetLogin()) + userName := user.GetLogin() + if strings.EqualFold(userName, workflowInitiator) && shouldExcludeWorkflowInitiator { + fmt.Printf("Not adding user '%s' from group '%s' as an approver as they are the workflow initiator\n", userName, userOrTeam) + } else { + userNames = append(userNames, userName) + } } return userNames diff --git a/constants.go b/constants.go index 2d1b532..3124ec2 100644 --- a/constants.go +++ b/constants.go @@ -5,13 +5,15 @@ import "time" const ( pollingInterval time.Duration = 10 * time.Second - envVarRepoFullName string = "GITHUB_REPOSITORY" - envVarRunID string = "GITHUB_RUN_ID" - envVarRepoOwner string = "GITHUB_REPOSITORY_OWNER" - envVarToken string = "INPUT_SECRET" - envVarApprovers string = "INPUT_APPROVERS" - envVarMinimumApprovals string = "INPUT_MINIMUM-APPROVALS" - envVarIssueTitle string = "INPUT_ISSUE-TITLE" + envVarRepoFullName string = "GITHUB_REPOSITORY" + envVarRunID string = "GITHUB_RUN_ID" + envVarRepoOwner string = "GITHUB_REPOSITORY_OWNER" + envVarWorkflowInitiator string = "GITHUB_ACTOR" + envVarToken string = "INPUT_SECRET" + envVarApprovers string = "INPUT_APPROVERS" + envVarMinimumApprovals string = "INPUT_MINIMUM-APPROVALS" + envVarIssueTitle string = "INPUT_ISSUE-TITLE" + envVarExcludeWorkflowInitiatorAsApprover string = "INPUT_EXCLUDE-WORKFLOW-INITIATOR-AS-APPROVER" ) var ( diff --git a/main.go b/main.go index dfc69fe..efa4ba9 100644 --- a/main.go +++ b/main.go @@ -107,7 +107,7 @@ func newGithubClient(ctx context.Context) (*github.Client, error) { apiUrl, apiUrlPresent := os.LookupEnv("GITHUB_API_URL") if serverUrlPresent { - if ! apiUrlPresent { + if !apiUrlPresent { apiUrl = serverUrl } return github.NewEnterpriseClient(apiUrl, serverUrl, tc) From 05a49ff052f6ccde485197c71f00688a09f09017 Mon Sep 17 00:00:00 2001 From: Thomas Stringer Date: Fri, 11 Nov 2022 17:59:51 -0500 Subject: [PATCH 12/60] Fix bug with period in org team name (#60) Currently if you have an org team name with a period in it, the team will not be found with a 404 response. This is because the GitHub API requires that the periods are replaced with hyphens. This PR fixes this behavior and closes #55. Signed-off-by: Thomas Stringer --- approvers.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/approvers.go b/approvers.go index d7aae15..c3f129f 100644 --- a/approvers.go +++ b/approvers.go @@ -58,7 +58,13 @@ func retrieveApprovers(client *github.Client, repoOwner string) ([]string, error func expandGroupFromUser(client *github.Client, org, userOrTeam string, workflowInitiator string, shouldExcludeWorkflowInitiator bool) []string { fmt.Printf("Attempting to expand user %s/%s as a group (may not succeed)\n", org, userOrTeam) - users, _, err := client.Teams.ListTeamMembersBySlug(context.Background(), org, userOrTeam, &github.TeamListTeamMembersOptions{}) + + // GitHub replaces periods in the team name with hyphens. If a period is + // passed to the request it would result in a 404. So we need to replace + // and occurrences with a hyphen. + formattedUserOrTeam := strings.ReplaceAll(userOrTeam, ".", "-") + + users, _, err := client.Teams.ListTeamMembersBySlug(context.Background(), org, formattedUserOrTeam, &github.TeamListTeamMembersOptions{}) if err != nil { fmt.Printf("%v\n", err) return nil From f204a352710533e7d6ee2c4cfcdd88bca6b4d49e Mon Sep 17 00:00:00 2001 From: Thomas Stringer Date: Sun, 13 Nov 2022 12:03:48 -0500 Subject: [PATCH 13/60] Add dev guide to the documentation (#61) Signed-off-by: Thomas Stringer --- README.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/README.md b/README.md index 18a78c2..e130d1c 100644 --- a/README.md +++ b/README.md @@ -96,3 +96,57 @@ For more information on permissions, please look at the [GitHub documentation](h * While the workflow is paused, it will still continue to consume a concurrent job allocation out of the [max concurrent jobs](https://docs.github.com/en/actions/learn-github-actions/usage-limits-billing-and-administration#usage-limits). * A job (including a paused job) will be failed [after 6 hours](https://docs.github.com/en/actions/learn-github-actions/usage-limits-billing-and-administration#usage-limits). * A paused job is still running compute/instance/virtual machine and will continue to incur costs. + +## Development + +### Running test code + +To test out your code in an action, you need to build the image and push it to a different container registry repository. For instance, if I want to test some code I won't build the image with the main image repository. Prior to this, comment out the label binding the image to a repo: + +```dockerfile +# LABEL org.opencontainers.image.source https://github.com/trstringer/manual-approval +``` + +Build the image: + +``` +$ VERSION=1.7.1-rc.1 make IMAGE_REPO=ghcr.io/trstringer/manual-approval-test build +``` + +*Note: The image version can be whatever you want, as this image wouldn't be pushed to production. It is only for testing.* + +Push the image to your container registry: + +``` +$ VERSION=1.7.1-rc.1 make IMAGE_REPO=ghcr.io/trstringer/manual-approval-test push +``` + +To test out the image you will need to modify `action.yaml` so that it points to your new image that you're testing: + +```yaml + image: docker://ghcr.io/trstringer/manual-approval-test:1.7.0-rc.1 +``` + +Then to test out the image, run a workflow specifying your dev branch: + +```yaml +- name: Wait for approval + uses: your-github-user/manual-approval@your-dev-branch + with: + secret: ${{ secrets.GITHUB_TOKEN }} + approvers: trstringer +``` + +For `uses`, this should point to your repo and dev branch. + +*Note: To test out the action that uses an approver that is an org team, refer to the [org team approver](#org-team-approver) section for instructions.* + +### Create a release + +1. Build the new version's image: `$ VERSION=1.7.0 make build` +1. Push the new image: `$ VERSION=1.7.0 make push` +1. Create a release branch and modify `action.yaml` to point to the new image +1. Open and merge a PR to add these changes to the default branch +1. Make sure to fetch the new changes into your local repo: `$ git checkout main && git fetch origin && git merge origin main` +1. Delete the `v1` tag locally and remotely: `$ git tag -d v1 && git push --delete origin v1` +1. Create and push new tags: `$ git tag v1.7.0 && git tag v1 && git push origin --tags` From 3276970bc102aff5919cf5382656a2613a80451b Mon Sep 17 00:00:00 2001 From: Thomas Stringer Date: Sun, 13 Nov 2022 16:17:08 -0500 Subject: [PATCH 14/60] Release v1.8.0 (#62) Signed-off-by: Thomas Stringer --- action.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/action.yaml b/action.yaml index 9fd7fdb..e82ad18 100644 --- a/action.yaml +++ b/action.yaml @@ -21,4 +21,4 @@ inputs: default: false runs: using: docker - image: docker://ghcr.io/trstringer/manual-approval:1.7.0 + image: docker://ghcr.io/trstringer/manual-approval:1.8.0 From 36a6b4b6cb56a9e6fd64fd2eaf0b434ce2dec00e Mon Sep 17 00:00:00 2001 From: Thomas Stringer Date: Sun, 13 Nov 2022 16:21:01 -0500 Subject: [PATCH 15/60] Add GitHub release creation to dev docs (#63) Signed-off-by: Thomas Stringer --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e130d1c..8133a74 100644 --- a/README.md +++ b/README.md @@ -150,3 +150,4 @@ For `uses`, this should point to your repo and dev branch. 1. Make sure to fetch the new changes into your local repo: `$ git checkout main && git fetch origin && git merge origin main` 1. Delete the `v1` tag locally and remotely: `$ git tag -d v1 && git push --delete origin v1` 1. Create and push new tags: `$ git tag v1.7.0 && git tag v1 && git push origin --tags` +1. Create the GitHub project release From a28646d11347536e3bb3cf6d302cd745692a297e Mon Sep 17 00:00:00 2001 From: Thomas Stringer Date: Mon, 14 Nov 2022 19:27:43 -0500 Subject: [PATCH 16/60] Add the issue link to the action output (#64) Signed-off-by: Thomas Stringer --- approval.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/approval.go b/approval.go index 7f24547..cb40534 100644 --- a/approval.go +++ b/approval.go @@ -77,8 +77,13 @@ Respond %s to continue workflow or %s to cancel.`, Body: &issueBody, Assignees: &a.issueApprovers, }) + if err != nil { + return err + } a.approvalIssueNumber = a.approvalIssue.GetNumber() - return err + + fmt.Printf("Issue created: %s\n", a.approvalIssue.GetURL()) + return nil } func approvalFromComments(comments []*github.IssueComment, approvers []string, minimumApprovals int) (approvalStatus, error) { From 948dfe9537d89917bd12e8a6868a28f83400fb32 Mon Sep 17 00:00:00 2001 From: Daniel Gonzalez <3019505+gonzolino@users.noreply.github.com> Date: Fri, 20 Jan 2023 13:13:43 +0100 Subject: [PATCH 17/60] Add new input 'issue-body' (#74) * Add new input 'issue-body' Similar to the 'issue-title' input, 'issue-body' allows to put custom text to the body of the approval issue. * Update readme for 'issue-body' input --- README.md | 2 ++ action.yaml | 3 +++ approval.go | 9 ++++++++- constants.go | 1 + main.go | 3 ++- 5 files changed, 16 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8133a74..704993f 100644 --- a/README.md +++ b/README.md @@ -32,12 +32,14 @@ steps: approvers: user1,user2,org-team1 minimum-approvals: 1 issue-title: "Deploying v1.3.5 to prod from staging" + issue-body: "Please approve or deny the deployment of version v1.3.5." exclude-workflow-initiator-as-approver: false ``` - `approvers` is a comma-delimited list of all required approvers. An approver can either be a user or an org team. (*Note: Required approvers must have the ability to be set as approvers in the repository. If you add an approver that doesn't have this permission then you would receive an HTTP/402 Validation Failed error when running this action*) - `minimum-approvals` is an integer that sets the minimum number of approvals required to progress the workflow. Defaults to ALL approvers. - `issue-title` is a string that will be appended to the title of the issue. +- `issue-body` is a string that will be prepended to the body of the issue. - `exclude-workflow-initiator-as-approver` is a boolean that indicates if the workflow initiator (determined by the `GITHUB_ACTOR` environment variable) should be filtered from the final list of approvers. This is optional and defaults to `false`. Set this to `true` to prevent users in the `approvers` list from being able to self-approve workflows. ## Org team approver diff --git a/action.yaml b/action.yaml index e82ad18..e4666fd 100644 --- a/action.yaml +++ b/action.yaml @@ -16,6 +16,9 @@ inputs: issue-title: description: The custom subtitle for the issue required: false + issue-body: + description: The custom body for the issue + required: false exclude-workflow-initiator-as-approver: description: Whether or not to filter out the user who initiated the workflow as an approver if they are in the approvers list default: false diff --git a/approval.go b/approval.go index cb40534..101445d 100644 --- a/approval.go +++ b/approval.go @@ -18,11 +18,12 @@ type approvalEnvironment struct { approvalIssue *github.Issue approvalIssueNumber int issueTitle string + issueBody string issueApprovers []string minimumApprovals int } -func newApprovalEnvironment(client *github.Client, repoFullName, repoOwner string, runID int, approvers []string, minimumApprovals int, issueTitle string) (*approvalEnvironment, error) { +func newApprovalEnvironment(client *github.Client, repoFullName, repoOwner string, runID int, approvers []string, minimumApprovals int, issueTitle, issueBody string) (*approvalEnvironment, error) { repoOwnerAndName := strings.Split(repoFullName, "/") if len(repoOwnerAndName) != 2 { return nil, fmt.Errorf("repo owner and name in unexpected format: %s", repoFullName) @@ -38,6 +39,7 @@ func newApprovalEnvironment(client *github.Client, repoFullName, repoOwner strin issueApprovers: approvers, minimumApprovals: minimumApprovals, issueTitle: issueTitle, + issueBody: issueBody, }, nil } @@ -63,6 +65,11 @@ Respond %s to continue workflow or %s to cancel.`, formatAcceptedWords(approvedWords), formatAcceptedWords(deniedWords), ) + + if a.issueBody != "" { + issueBody = fmt.Sprintf("%s\n\n%s", a.issueBody, issueBody) + } + var err error fmt.Printf( "Creating issue in repo %s/%s with the following content:\nTitle: %s\nApprovers: %s\nBody:\n%s\n", diff --git a/constants.go b/constants.go index 3124ec2..a57d5ad 100644 --- a/constants.go +++ b/constants.go @@ -13,6 +13,7 @@ const ( envVarApprovers string = "INPUT_APPROVERS" envVarMinimumApprovals string = "INPUT_MINIMUM-APPROVALS" envVarIssueTitle string = "INPUT_ISSUE-TITLE" + envVarIssueBody string = "INPUT_ISSUE-BODY" envVarExcludeWorkflowInitiatorAsApprover string = "INPUT_EXCLUDE-WORKFLOW-INITIATOR-AS-APPROVER" ) diff --git a/main.go b/main.go index efa4ba9..5aad15e 100644 --- a/main.go +++ b/main.go @@ -171,6 +171,7 @@ func main() { } issueTitle := os.Getenv(envVarIssueTitle) + issueBody := os.Getenv(envVarIssueBody) minimumApprovalsRaw := os.Getenv(envVarMinimumApprovals) minimumApprovals := 0 if minimumApprovalsRaw != "" { @@ -180,7 +181,7 @@ func main() { os.Exit(1) } } - apprv, err := newApprovalEnvironment(client, repoFullName, repoOwner, runID, approvers, minimumApprovals, issueTitle) + apprv, err := newApprovalEnvironment(client, repoFullName, repoOwner, runID, approvers, minimumApprovals, issueTitle, issueBody) if err != nil { fmt.Printf("error creating approval environment: %v\n", err) os.Exit(1) From 639442a9fabc52e9a1e457bd0774dab3ca00d04a Mon Sep 17 00:00:00 2001 From: Brandon Ward Date: Thu, 16 Feb 2023 18:20:43 -0700 Subject: [PATCH 18/60] Feature: Allow Custom Approval or Denial Words (#66) * Add support for custom approval and denial words * Work out some regex nuances with GitHub --- .gitignore | 1 + README.md | 9 ++ action.yaml | 6 ++ approval.go | 10 +- approval_test.go | 248 ++++++++++++++++++++++++++++++++--------------- constants.go | 29 +++++- 6 files changed, 220 insertions(+), 83 deletions(-) diff --git a/.gitignore b/.gitignore index 4c49bd7..a9ad188 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .env +.idea/ diff --git a/README.md b/README.md index 704993f..25218e9 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,8 @@ steps: issue-title: "Deploying v1.3.5 to prod from staging" issue-body: "Please approve or deny the deployment of version v1.3.5." exclude-workflow-initiator-as-approver: false + additional-approved-words: '' + additional-denied-words: '' ``` - `approvers` is a comma-delimited list of all required approvers. An approver can either be a user or an org team. (*Note: Required approvers must have the ability to be set as approvers in the repository. If you add an approver that doesn't have this permission then you would receive an HTTP/402 Validation Failed error when running this action*) @@ -41,6 +43,13 @@ steps: - `issue-title` is a string that will be appended to the title of the issue. - `issue-body` is a string that will be prepended to the body of the issue. - `exclude-workflow-initiator-as-approver` is a boolean that indicates if the workflow initiator (determined by the `GITHUB_ACTOR` environment variable) should be filtered from the final list of approvers. This is optional and defaults to `false`. Set this to `true` to prevent users in the `approvers` list from being able to self-approve workflows. +- `additional-approved-words` is a comma separated list of strings to expand the dictionary of words that indicate approval. This is optional and defaults to an empty string. +- `additional-denied-words` is a comma separated list of strings to expand the dictionary of words that indicate denial. This is optional and defaults to an empty string. + +### Using Custom Words + +GitHub has a rich library of emojis, and these all work in additional approved words or denied words. Some values GitHub will store in their text version - i.e. `:shipit:`. Other emojis, GitHub will store in their unicode emoji form, like ✅. +For a seamless experience, it is recommended that you add the custom words to a GitHub comment, and then copy it back out of the comment into your actions configuration yaml. ## Org team approver diff --git a/action.yaml b/action.yaml index e4666fd..48f13cf 100644 --- a/action.yaml +++ b/action.yaml @@ -22,6 +22,12 @@ inputs: exclude-workflow-initiator-as-approver: description: Whether or not to filter out the user who initiated the workflow as an approver if they are in the approvers list default: false + additional-approved-words: + description: Comma separated list of words that can be used to approve beyond the defaults. + default: '' + additional-denied-words: + description: Comma separated list of words that can be used to deny beyond the defaults. + default: '' runs: using: docker image: docker://ghcr.io/trstringer/manual-approval:1.8.0 diff --git a/approval.go b/approval.go index 101445d..a720aef 100644 --- a/approval.go +++ b/approval.go @@ -145,10 +145,14 @@ func approversIndex(approvers []string, name string) int { func isApproved(commentBody string) (bool, error) { for _, approvedWord := range approvedWords { - matched, err := regexp.MatchString(fmt.Sprintf("(?i)^%s[.!]*\n*$", approvedWord), commentBody) + re, err := regexp.Compile(fmt.Sprintf("(?i)^%s[.!]*\n*\\s*$", approvedWord)) if err != nil { + fmt.Printf("Error parsing. %v", err) return false, err } + + matched := re.MatchString(commentBody) + if matched { return true, nil } @@ -159,10 +163,12 @@ func isApproved(commentBody string) (bool, error) { func isDenied(commentBody string) (bool, error) { for _, deniedWord := range deniedWords { - matched, err := regexp.MatchString(fmt.Sprintf("(?i)^%s[.!]*\n*$", deniedWord), commentBody) + re, err := regexp.Compile(fmt.Sprintf("(?i)^%s[.!]*\n*\\s*$", deniedWord)) if err != nil { + fmt.Printf("Error parsing. %v", err) return false, err } + matched := re.MatchString(commentBody) if matched { return true, nil } diff --git a/approval_test.go b/approval_test.go index 4ff0c83..afc215f 100644 --- a/approval_test.go +++ b/approval_test.go @@ -176,84 +176,130 @@ func TestApprovalFromComments(t *testing.T) { func TestApprovedCommentBody(t *testing.T) { testCases := []struct { - name string - commentBody string - isSuccess bool + name string + commentBody string + isSuccess bool + customApprovalWord string }{ { - name: "approved_lowercase_no_punctuation", - commentBody: "approved", - isSuccess: true, + name: "approved_lowercase_no_punctuation", + commentBody: "approved", + isSuccess: true, + customApprovalWord: "", }, { - name: "approve_lowercase_no_punctuation", - commentBody: "approve", - isSuccess: true, + name: "approve_lowercase_no_punctuation", + commentBody: "approve", + isSuccess: true, + customApprovalWord: "", }, { - name: "lgtm_lowercase_no_punctuation", - commentBody: "lgtm", - isSuccess: true, + name: "lgtm_lowercase_no_punctuation", + commentBody: "lgtm", + isSuccess: true, + customApprovalWord: "", }, { - name: "yes_lowercase_no_punctuation", - commentBody: "yes", - isSuccess: true, + name: "yes_lowercase_no_punctuation", + commentBody: "yes", + isSuccess: true, + customApprovalWord: "", }, { - name: "approve_uppercase_no_punctuation", - commentBody: "APPROVE", - isSuccess: true, + name: "approve_uppercase_no_punctuation", + commentBody: "APPROVE", + isSuccess: true, + customApprovalWord: "", }, { - name: "approved_titlecase_period", - commentBody: "Approved.", - isSuccess: true, + name: "approved_titlecase_period", + commentBody: "Approved.", + isSuccess: true, + customApprovalWord: "", }, { - name: "approved_titlecase_exclamation", - commentBody: "Approved!", - isSuccess: true, + name: "approved_titlecase_exclamation", + commentBody: "Approved!", + isSuccess: true, + customApprovalWord: "", }, { - name: "approved_titlecase_multi_exclamation", - commentBody: "Approved!!", - isSuccess: true, + name: "approved_titlecase_multi_exclamation", + commentBody: "Approved!!", + isSuccess: true, + customApprovalWord: "", }, { - name: "approved_titlecase_question", - commentBody: "Approved?", - isSuccess: false, + name: "approved_titlecase_question", + commentBody: "Approved?", + isSuccess: false, + customApprovalWord: "", }, { - name: "sentence_with_keyword", - commentBody: "should i approve this", - isSuccess: false, + name: "sentence_with_keyword", + commentBody: "should i approve this", + isSuccess: false, + customApprovalWord: "", }, { - name: "sentence_without_keyword", - commentBody: "this is just some random comment", - isSuccess: false, + name: "sentence_without_keyword", + commentBody: "this is just some random comment", + isSuccess: false, + customApprovalWord: "", }, { - name: "approved_with_newline", - commentBody: "approved\n", - isSuccess: true, + name: "approved_with_newline", + commentBody: "approved\n", + isSuccess: true, + customApprovalWord: "", }, { - name: "approved_with_exclamation_newline", - commentBody: "approved!\n", - isSuccess: true, + name: "approved_with_exclamation_newline", + commentBody: "approved!\n", + isSuccess: true, + customApprovalWord: "", }, { - name: "approved_with_multi_exclamation_multi_newline", - commentBody: "approved!!!\n\n\n", - isSuccess: true, + name: "approved_with_multi_exclamation_multi_newline", + commentBody: "approved!!!\n\n\n", + isSuccess: true, + customApprovalWord: "", + }, + { + name: "approved_with_custom_approval_word", + commentBody: "shipit", + isSuccess: true, + customApprovalWord: "shipit", + }, + { + name: "approved_with_github_emoji_syntax", + commentBody: ":shipit:", + isSuccess: true, + customApprovalWord: ":shipit:", + }, + { + name: "approved_with_custom_hashtag", + commentBody: "#shipit", + isSuccess: true, + customApprovalWord: "#shipit", + }, + { + name: "approved_with_actual_emoji_✅", + commentBody: "✅ ", + isSuccess: true, + customApprovalWord: "✅", }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { + // before each + word := testCase.customApprovalWord + if len(word) > 0 { + approvedWords = append(approvedWords, word) + } + + // test actual, err := isApproved(testCase.commentBody) if err != nil { t.Fatalf("error getting approval: %v", err) @@ -261,70 +307,111 @@ func TestApprovedCommentBody(t *testing.T) { if actual != testCase.isSuccess { t.Fatalf("expected %v but got %v", testCase.isSuccess, actual) } + + // after each + if len(word) > 0 { + approvedWords = approvedWords[:len(approvedWords)-1] + } }) } } func TestDeniedCommentBody(t *testing.T) { testCases := []struct { - name string - commentBody string - isSuccess bool + name string + commentBody string + isSuccess bool + customDenialWord string }{ { - name: "denied_lowercase_no_punctuation", - commentBody: "denied", - isSuccess: true, + name: "denied_lowercase_no_punctuation", + commentBody: "denied", + isSuccess: true, + customDenialWord: "", }, { - name: "deny_lowercase_no_punctuation", - commentBody: "deny", - isSuccess: true, + name: "deny_lowercase_no_punctuation", + commentBody: "deny", + isSuccess: true, + customDenialWord: "", }, { - name: "no_lowercase_no_punctuation", - commentBody: "no", - isSuccess: true, + name: "no_lowercase_no_punctuation", + commentBody: "no", + isSuccess: true, + customDenialWord: "", }, { - name: "deny_uppercase_no_punctuation", - commentBody: "DENY", - isSuccess: true, + name: "deny_uppercase_no_punctuation", + commentBody: "DENY", + isSuccess: true, + customDenialWord: "", }, { - name: "denied_titlecase_period", - commentBody: "Denied.", - isSuccess: true, + name: "denied_titlecase_period", + commentBody: "Denied.", + isSuccess: true, + customDenialWord: "", }, { - name: "denied_titlecase_exclamation", - commentBody: "Denied!", - isSuccess: true, + name: "denied_titlecase_exclamation", + commentBody: "Denied!", + isSuccess: true, + customDenialWord: "", }, { - name: "deny_titlecase_question", - commentBody: "Deny?", - isSuccess: false, + name: "deny_titlecase_question", + commentBody: "Deny?", + isSuccess: false, + customDenialWord: "", }, { - name: "sentence_with_keyword", - commentBody: "should i deny this", - isSuccess: false, + name: "sentence_with_keyword", + commentBody: "should i deny this", + isSuccess: false, + customDenialWord: "", }, { - name: "sentence_without_keyword", - commentBody: "this is just some random comment", - isSuccess: false, + name: "sentence_without_keyword", + commentBody: "this is just some random comment", + isSuccess: false, + customDenialWord: "", }, { - name: "denied_with_newline", - commentBody: "denied\n", - isSuccess: true, + name: "denied_with_newline", + commentBody: "denied\n", + isSuccess: true, + customDenialWord: "", + }, + { + name: "denied_with_custom_word", + commentBody: "naw", + isSuccess: true, + customDenialWord: "naw", + }, + { + name: "denied_with_github_emoji", + commentBody: ":no_entry_sign: ", + isSuccess: true, + customDenialWord: ":no_entry_sign:", + }, + { + name: "denied_with_hashtag", + commentBody: "#noway", + isSuccess: true, + customDenialWord: "#noway", }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { + // before each + word := testCase.customDenialWord + if len(word) > 0 { + deniedWords = append(deniedWords, word) + } + + // test actual, err := isDenied(testCase.commentBody) if err != nil { t.Fatalf("error getting approval: %v", err) @@ -332,6 +419,11 @@ func TestDeniedCommentBody(t *testing.T) { if actual != testCase.isSuccess { t.Fatalf("expected %v but got %v", testCase.isSuccess, actual) } + + // after each + if len(word) > 0 { + deniedWords = deniedWords[:len(deniedWords)-1] + } }) } } diff --git a/constants.go b/constants.go index a57d5ad..03e4b4e 100644 --- a/constants.go +++ b/constants.go @@ -1,6 +1,10 @@ package main -import "time" +import ( + "os" + "strings" + "time" +) const ( pollingInterval time.Duration = 10 * time.Second @@ -15,9 +19,28 @@ const ( envVarIssueTitle string = "INPUT_ISSUE-TITLE" envVarIssueBody string = "INPUT_ISSUE-BODY" envVarExcludeWorkflowInitiatorAsApprover string = "INPUT_EXCLUDE-WORKFLOW-INITIATOR-AS-APPROVER" + envVarAdditionalApprovedWords string = "INPUT_ADDITIONAL-APPROVED-WORDS" + envVarAdditionalDeniedWords string = "INPUT_ADDITIONAL-DENIED-WORDS" ) var ( - approvedWords = []string{"approved", "approve", "lgtm", "yes"} - deniedWords = []string{"denied", "deny", "no"} + additionalApprovedWords = readAdditionalWords(envVarAdditionalApprovedWords) + additionalDeniedWords = readAdditionalWords(envVarAdditionalDeniedWords) + + approvedWords = append([]string{"approved", "approve", "lgtm", "yes"}, additionalApprovedWords...) + deniedWords = append([]string{"denied", "deny", "no"}, additionalDeniedWords...) ) + +func readAdditionalWords(envVar string) []string { + rawValue := strings.TrimSpace(os.Getenv(envVar)) + if len(rawValue) == 0 { + // Nothing else to do here. + return []string{} + } + slicedWords := strings.Split(rawValue, ",") + for i := range slicedWords { + // no leading or trailing spaces in user provided words. + slicedWords[i] = strings.TrimSpace(slicedWords[i]) + } + return slicedWords +} From a824dad59ac64ac825ab8725599e483e24aa2815 Mon Sep 17 00:00:00 2001 From: Thomas Stringer Date: Sat, 18 Feb 2023 09:43:31 -0500 Subject: [PATCH 19/60] Release v1.9.0 (#84) Signed-off-by: Thomas Stringer --- action.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/action.yaml b/action.yaml index 48f13cf..cbd28ce 100644 --- a/action.yaml +++ b/action.yaml @@ -30,4 +30,4 @@ inputs: default: '' runs: using: docker - image: docker://ghcr.io/trstringer/manual-approval:1.8.0 + image: docker://ghcr.io/trstringer/manual-approval:1.9.0 From dd1555b1d17966cb58500676d79a181c53bb6c12 Mon Sep 17 00:00:00 2001 From: Augusto Melo <4723788+augustomelo@users.noreply.github.com> Date: Sun, 23 Apr 2023 16:02:50 +0100 Subject: [PATCH 20/60] fix(#75): display corect base URL if using GHE (#90) It was displaying the GH URL even though the GHE was configured, using the client.baseURL will correctly display a correct value. https://pkg.go.dev/github.com/google/go-github/v50/github#Client --- approval.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/approval.go b/approval.go index a720aef..a34c647 100644 --- a/approval.go +++ b/approval.go @@ -44,7 +44,7 @@ func newApprovalEnvironment(client *github.Client, repoFullName, repoOwner strin } func (a approvalEnvironment) runURL() string { - return fmt.Sprintf("https://github.com/%s/actions/runs/%d", a.repoFullName, a.runID) + return fmt.Sprintf("%s%s/actions/runs/%d", a.client.BaseURL.String(), a.repoFullName, a.runID) } func (a *approvalEnvironment) createApprovalIssue(ctx context.Context) error { From 315f51d8882cd7d255d4252fb40f94fd36a37ae2 Mon Sep 17 00:00:00 2001 From: Vanley Date: Thu, 13 Jun 2024 01:21:14 +0100 Subject: [PATCH 21/60] add timeout-minutes to valid imputs (#121) --- action.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/action.yaml b/action.yaml index cbd28ce..b0c705c 100644 --- a/action.yaml +++ b/action.yaml @@ -13,6 +13,9 @@ inputs: minimum-approvals: description: Minimum number of approvals to progress workflow required: false + timeout-minutes: + description: Force timeout of your workflow pause + required: false issue-title: description: The custom subtitle for the issue required: false From 662b3ddbc7685f897992051e87e1b4b58c07dc03 Mon Sep 17 00:00:00 2001 From: Thomas Stringer Date: Wed, 12 Jun 2024 20:42:28 -0400 Subject: [PATCH 22/60] Create new release (#123) --- Session.vim | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ action.yaml | 2 +- 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 Session.vim diff --git a/Session.vim b/Session.vim new file mode 100644 index 0000000..0cdd50a --- /dev/null +++ b/Session.vim @@ -0,0 +1,50 @@ +let SessionLoad = 1 +let s:so_save = &g:so | let s:siso_save = &g:siso | setg so=0 siso=0 | setl so=-1 siso=-1 +let v:this_session=expand(":p") +silent only +silent tabonly +cd ~/dev/manual-approval +if expand('%') == '' && !&modified && line('$') <= 1 && getline(1) == '' + let s:wipebuf = bufnr('%') +endif +let s:shortmess_save = &shortmess +if &shortmess =~ 'A' + set shortmess=aoOA +else + set shortmess=aoO +endif +badd +0 action.yaml +argglobal +%argdel +edit action.yaml +argglobal +balt action.yaml +setlocal fdm=expr +setlocal fde=nvim_treesitter#foldexpr() +setlocal fmr={{{,}}} +setlocal fdi=# +setlocal fdl=99 +setlocal fml=1 +setlocal fdn=20 +setlocal fen +let s:l = 36 - ((35 * winheight(0) + 20) / 41) +if s:l < 1 | let s:l = 1 | endif +keepjumps exe s:l +normal! zt +keepjumps 36 +normal! 058| +tabnext 1 +if exists('s:wipebuf') && len(win_findbuf(s:wipebuf)) == 0 && getbufvar(s:wipebuf, '&buftype') isnot# 'terminal' + silent exe 'bwipe ' . s:wipebuf +endif +unlet! s:wipebuf +set winheight=1 winwidth=20 +let &shortmess = s:shortmess_save +let s:sx = expand(":p:r")."x.vim" +if filereadable(s:sx) + exe "source " . fnameescape(s:sx) +endif +let &g:so = s:so_save | let &g:siso = s:siso_save +doautoall SessionLoadPost +unlet SessionLoad +" vim: set ft=vim : diff --git a/action.yaml b/action.yaml index b0c705c..7866c0b 100644 --- a/action.yaml +++ b/action.yaml @@ -33,4 +33,4 @@ inputs: default: '' runs: using: docker - image: docker://ghcr.io/trstringer/manual-approval:1.9.0 + image: docker://ghcr.io/trstringer/manual-approval:1.9.1 From 287b77b4700056dbcf79cec3b2383612ac7e112a Mon Sep 17 00:00:00 2001 From: Yucel Okcu Date: Thu, 20 Jun 2024 00:20:51 +0300 Subject: [PATCH 23/60] fix: use correct base URL if not enterprise (#125) --- approval.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/approval.go b/approval.go index a34c647..4005004 100644 --- a/approval.go +++ b/approval.go @@ -44,7 +44,11 @@ func newApprovalEnvironment(client *github.Client, repoFullName, repoOwner strin } func (a approvalEnvironment) runURL() string { - return fmt.Sprintf("%s%s/actions/runs/%d", a.client.BaseURL.String(), a.repoFullName, a.runID) + baseUrl := a.client.BaseURL.String() + if strings.Contains(baseUrl, "github.com") { + baseUrl = "https://github.com/" + } + return fmt.Sprintf("%s%s/actions/runs/%d", baseUrl, a.repoFullName, a.runID) } func (a *approvalEnvironment) createApprovalIssue(ctx context.Context) error { From d2e412d84000579996fd00d7834a817f92af19d3 Mon Sep 17 00:00:00 2001 From: Florian Kaiser Date: Wed, 19 Jun 2024 23:22:23 +0200 Subject: [PATCH 24/60] Fix issue URL in job log (#112) --- approval.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/approval.go b/approval.go index 4005004..e2bb13f 100644 --- a/approval.go +++ b/approval.go @@ -93,7 +93,7 @@ Respond %s to continue workflow or %s to cancel.`, } a.approvalIssueNumber = a.approvalIssue.GetNumber() - fmt.Printf("Issue created: %s\n", a.approvalIssue.GetURL()) + fmt.Printf("Issue created: %s\n", a.approvalIssue.GetHTMLURL()) return nil } From aba06e32f19b0890e99cfd2a9af7868a671cba16 Mon Sep 17 00:00:00 2001 From: Liz MacLean <18120837+lizziemac@users.noreply.github.com> Date: Fri, 14 Feb 2025 20:23:48 -0500 Subject: [PATCH 25/60] Introduce a 'fail-on-denial' boolean (#147) * Introduce a 'fail-on-approval' boolean * Use test docker image * oops * change default and fix logic * Update action.yaml * Fix linting error --- Makefile | 3 ++- action.yaml | 9 ++++++++- approval.go | 4 +++- constants.go | 1 + main.go | 48 ++++++++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 60 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 3ec553a..8c3fc9b 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,5 @@ IMAGE_REPO=ghcr.io/trstringer/manual-approval +TARGET_PLATFORM=linux/amd64 .PHONY: build build: @@ -6,7 +7,7 @@ build: echo "VERSION is required"; \ exit 1; \ fi - docker build -t $(IMAGE_REPO):$$VERSION . + docker build --platform $(TARGET_PLATFORM) -t $(IMAGE_REPO):$$VERSION . .PHONY: push push: diff --git a/action.yaml b/action.yaml index 7866c0b..bef33d7 100644 --- a/action.yaml +++ b/action.yaml @@ -24,13 +24,20 @@ inputs: required: false exclude-workflow-initiator-as-approver: description: Whether or not to filter out the user who initiated the workflow as an approver if they are in the approvers list - default: false + required: false + default: 'false' additional-approved-words: description: Comma separated list of words that can be used to approve beyond the defaults. + required: false default: '' additional-denied-words: description: Comma separated list of words that can be used to deny beyond the defaults. + required: false default: '' + fail-on-denial: + description: Whether or not to fail the workflow if the approval is denied + required: false + default: 'true' runs: using: docker image: docker://ghcr.io/trstringer/manual-approval:1.9.1 diff --git a/approval.go b/approval.go index e2bb13f..d69df54 100644 --- a/approval.go +++ b/approval.go @@ -21,9 +21,10 @@ type approvalEnvironment struct { issueBody string issueApprovers []string minimumApprovals int + failOnDenial bool } -func newApprovalEnvironment(client *github.Client, repoFullName, repoOwner string, runID int, approvers []string, minimumApprovals int, issueTitle, issueBody string) (*approvalEnvironment, error) { +func newApprovalEnvironment(client *github.Client, repoFullName, repoOwner string, runID int, approvers []string, minimumApprovals int, issueTitle, issueBody string, failOnDenial bool) (*approvalEnvironment, error) { repoOwnerAndName := strings.Split(repoFullName, "/") if len(repoOwnerAndName) != 2 { return nil, fmt.Errorf("repo owner and name in unexpected format: %s", repoFullName) @@ -40,6 +41,7 @@ func newApprovalEnvironment(client *github.Client, repoFullName, repoOwner strin minimumApprovals: minimumApprovals, issueTitle: issueTitle, issueBody: issueBody, + failOnDenial: failOnDenial, }, nil } diff --git a/constants.go b/constants.go index 03e4b4e..0e08102 100644 --- a/constants.go +++ b/constants.go @@ -21,6 +21,7 @@ const ( envVarExcludeWorkflowInitiatorAsApprover string = "INPUT_EXCLUDE-WORKFLOW-INITIATOR-AS-APPROVER" envVarAdditionalApprovedWords string = "INPUT_ADDITIONAL-APPROVED-WORDS" envVarAdditionalDeniedWords string = "INPUT_ADDITIONAL-DENIED-WORDS" + envVarFailOnDenial string = "INPUT_FAIL-ON-DENIAL" ) var ( diff --git a/main.go b/main.go index 5aad15e..5349a87 100644 --- a/main.go +++ b/main.go @@ -12,6 +12,19 @@ import ( "golang.org/x/oauth2" ) +func setActionOutput(name, value string) error { + f, err := os.OpenFile(os.Getenv("GITHUB_OUTPUT"), os.O_APPEND|os.O_WRONLY, 0600) + if err != nil { + return err + } + defer f.Close() + + if _, err = f.WriteString(fmt.Sprintf("%s=%s\n", name, value)); err != nil { + return err + } + return nil +} + func handleInterrupt(ctx context.Context, client *github.Client, apprv *approvalEnvironment) { newState := "closed" closeComment := "Workflow cancelled, closing issue." @@ -71,7 +84,14 @@ func newCommentLoopChannel(ctx context.Context, apprv *approvalEnvironment, clie close(channel) case approvalStatusDenied: newState := "closed" - closeComment := "Request denied. Closing issue and failing workflow." + closeComment := "Request denied. Closing issue " + if !apprv.failOnDenial { + closeComment += "but continuing" + } else { + closeComment += "and failing" + } + closeComment += " workflow." + _, _, err := client.Issues.CreateComment(ctx, apprv.repoOwner, apprv.repo, apprv.approvalIssueNumber, &github.IssueComment{ Body: &closeComment, }) @@ -170,6 +190,16 @@ func main() { os.Exit(1) } + failOnDenial := true + failOnDenialRaw := os.Getenv(envVarFailOnDenial) + if failOnDenialRaw != "" { + failOnDenial, err = strconv.ParseBool(failOnDenialRaw) + if err != nil { + fmt.Printf("error parsing fail on denial: %v\n", err) + os.Exit(1) + } + } + issueTitle := os.Getenv(envVarIssueTitle) issueBody := os.Getenv(envVarIssueBody) minimumApprovalsRaw := os.Getenv(envVarMinimumApprovals) @@ -181,7 +211,7 @@ func main() { os.Exit(1) } } - apprv, err := newApprovalEnvironment(client, repoFullName, repoOwner, runID, approvers, minimumApprovals, issueTitle, issueBody) + apprv, err := newApprovalEnvironment(client, repoFullName, repoOwner, runID, approvers, minimumApprovals, issueTitle, issueBody, failOnDenial) if err != nil { fmt.Printf("error creating approval environment: %v\n", err) os.Exit(1) @@ -200,6 +230,20 @@ func main() { select { case exitCode := <-commentLoopChannel: + approvalStatus := "" + + if (!failOnDenial && exitCode == 1) { + approvalStatus = "denied" + exitCode = 0 + } else if (exitCode == 1) { + approvalStatus = "denied" + } else { + approvalStatus = "approved" + } + if err := setActionOutput("approval_status", approvalStatus); err != nil { + fmt.Printf("error setting action output: %v\n", err) + exitCode = 1 + } os.Exit(exitCode) case <-killSignalChannel: handleInterrupt(ctx, client, apprv) From 17c84dd68836c83380433bf434b73e094ddbfc04 Mon Sep 17 00:00:00 2001 From: Liz MacLean <18120837+lizziemac@users.noreply.github.com> Date: Mon, 17 Feb 2025 15:12:35 -0500 Subject: [PATCH 26/60] Remove default title if custom title is provided --- approval.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/approval.go b/approval.go index d69df54..c6e454e 100644 --- a/approval.go +++ b/approval.go @@ -57,7 +57,7 @@ func (a *approvalEnvironment) createApprovalIssue(ctx context.Context) error { issueTitle := fmt.Sprintf("Manual approval required for workflow run %d", a.runID) if a.issueTitle != "" { - issueTitle = fmt.Sprintf("%s: %s", issueTitle, a.issueTitle) + issueTitle = a.issueTitle } issueBody := fmt.Sprintf(`Workflow is pending manual review. From c17f1c63ff8c81f8b35f98be67488f5b2e3dd241 Mon Sep 17 00:00:00 2001 From: Sanskar Arora <55059942+sunny-1651@users.noreply.github.com> Date: Sun, 23 Feb 2025 00:20:00 +0530 Subject: [PATCH 27/60] Adding the capability to create the approval issue in the another repository (#142) * Adding the capability to create the approval issue in another repository * reverting testing changes * reverting test changes contd. * Updating readme and removing a debug line * Introduce a 'fail-on-denial' boolean (#147) * Introduce a 'fail-on-approval' boolean * Use test docker image * oops * change default and fix logic * Update action.yaml * Fix linting error * Adding the capability to create the approval issue in another repository * reverting testing changes * reverting test changes contd. * moving targetRepo inputs from env vars to action-args * Update constants.go * Update constants.go to resolve nuances created by inconsistent tab length settings --------- Co-authored-by: Liz MacLean <18120837+lizziemac@users.noreply.github.com> --- .github/workflows/ci.yaml | 1 + README.md | 19 +++++++++++++++++++ action.yaml | 6 ++++++ approval.go | 12 ++++++++---- constants.go | 2 ++ main.go | 27 +++++++++++++++++++-------- 6 files changed, 55 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a6ab720..5fa65b1 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -20,6 +20,7 @@ jobs: steps: - name: Checkout uses: actions/checkout@v2 + - name: Build run: make build env: diff --git a/README.md b/README.md index 25218e9..68336f9 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,25 @@ steps: - `additional-approved-words` is a comma separated list of strings to expand the dictionary of words that indicate approval. This is optional and defaults to an empty string. - `additional-denied-words` is a comma separated list of strings to expand the dictionary of words that indicate denial. This is optional and defaults to an empty string. +### Creating Issues in a different repository + +```yaml +steps: + - uses: trstringer/manual-approval@v1 + with: + secret: ${{ github.TOKEN }} + approvers: user1,user2,org-team1 + minimum-approvals: 1 + issue-title: "Deploying v1.3.5 to prod from staging" + issue-body: "Please approve or deny the deployment of version v1.3.5." + exclude-workflow-initiator-as-approver: false + additional-approved-words: '' + additional-denied-words: '' + target-repository: repository-name + target-repository-owner: owner-id +``` +- if either of `target-repository` or `target-repository-owner` is missing or is an empty string then the issue will be created in the same repository where this step is used. + ### Using Custom Words GitHub has a rich library of emojis, and these all work in additional approved words or denied words. Some values GitHub will store in their text version - i.e. `:shipit:`. Other emojis, GitHub will store in their unicode emoji form, like ✅. diff --git a/action.yaml b/action.yaml index bef33d7..c515b96 100644 --- a/action.yaml +++ b/action.yaml @@ -34,6 +34,12 @@ inputs: description: Comma separated list of words that can be used to deny beyond the defaults. required: false default: '' + target-repository-owner: + description: Owner of the repository in which the issue will be created. + default: '' + target-repository: + description: Name of the repository in which the issue will be created. + default: '' fail-on-denial: description: Whether or not to fail the workflow if the approval is denied required: false diff --git a/approval.go b/approval.go index d69df54..540cd71 100644 --- a/approval.go +++ b/approval.go @@ -21,10 +21,12 @@ type approvalEnvironment struct { issueBody string issueApprovers []string minimumApprovals int + targetRepoOwner string + targetRepoName string failOnDenial bool } -func newApprovalEnvironment(client *github.Client, repoFullName, repoOwner string, runID int, approvers []string, minimumApprovals int, issueTitle, issueBody string, failOnDenial bool) (*approvalEnvironment, error) { +func newApprovalEnvironment(client *github.Client, repoFullName, repoOwner string, runID int, approvers []string, minimumApprovals int, issueTitle, issueBody string, targetRepoOwner string, targetRepoName string, failOnDenial bool) (*approvalEnvironment, error) { repoOwnerAndName := strings.Split(repoFullName, "/") if len(repoOwnerAndName) != 2 { return nil, fmt.Errorf("repo owner and name in unexpected format: %s", repoFullName) @@ -41,6 +43,8 @@ func newApprovalEnvironment(client *github.Client, repoFullName, repoOwner strin minimumApprovals: minimumApprovals, issueTitle: issueTitle, issueBody: issueBody, + targetRepoOwner: targetRepoOwner, + targetRepoName: targetRepoName, failOnDenial: failOnDenial, }, nil } @@ -79,13 +83,13 @@ Respond %s to continue workflow or %s to cancel.`, var err error fmt.Printf( "Creating issue in repo %s/%s with the following content:\nTitle: %s\nApprovers: %s\nBody:\n%s\n", - a.repoOwner, - a.repo, + a.targetRepoOwner, + a.targetRepoName, issueTitle, a.issueApprovers, issueBody, ) - a.approvalIssue, _, err = a.client.Issues.Create(ctx, a.repoOwner, a.repo, &github.IssueRequest{ + a.approvalIssue, _, err = a.client.Issues.Create(ctx, a.targetRepoOwner, a.targetRepoName, &github.IssueRequest{ Title: &issueTitle, Body: &issueBody, Assignees: &a.issueApprovers, diff --git a/constants.go b/constants.go index 0e08102..027aafa 100644 --- a/constants.go +++ b/constants.go @@ -22,6 +22,8 @@ const ( envVarAdditionalApprovedWords string = "INPUT_ADDITIONAL-APPROVED-WORDS" envVarAdditionalDeniedWords string = "INPUT_ADDITIONAL-DENIED-WORDS" envVarFailOnDenial string = "INPUT_FAIL-ON-DENIAL" + envVarTargetRepoOwner string = "INPUT_TARGET-REPOSITORY-OWNER" + envVarTargetRepo string = "INPUT_TARGET-REPOSITORY" ) var ( diff --git a/main.go b/main.go index 5349a87..e9944bb 100644 --- a/main.go +++ b/main.go @@ -6,6 +6,7 @@ import ( "os" "os/signal" "strconv" + "strings" "time" "github.com/google/go-github/v43/github" @@ -29,14 +30,14 @@ func handleInterrupt(ctx context.Context, client *github.Client, apprv *approval newState := "closed" closeComment := "Workflow cancelled, closing issue." fmt.Println(closeComment) - _, _, err := client.Issues.CreateComment(ctx, apprv.repoOwner, apprv.repo, apprv.approvalIssueNumber, &github.IssueComment{ + _, _, err := client.Issues.CreateComment(ctx, apprv.targetRepoOwner, apprv.targetRepoName, apprv.approvalIssueNumber, &github.IssueComment{ Body: &closeComment, }) if err != nil { fmt.Printf("error commenting on issue: %v\n", err) return } - _, _, err = client.Issues.Edit(ctx, apprv.repoOwner, apprv.repo, apprv.approvalIssueNumber, &github.IssueRequest{State: &newState}) + _, _, err = client.Issues.Edit(ctx, apprv.targetRepoOwner, apprv.targetRepoName, apprv.approvalIssueNumber, &github.IssueRequest{State: &newState}) if err != nil { fmt.Printf("error closing issue: %v\n", err) return @@ -47,7 +48,7 @@ func newCommentLoopChannel(ctx context.Context, apprv *approvalEnvironment, clie channel := make(chan int) go func() { for { - comments, _, err := client.Issues.ListComments(ctx, apprv.repoOwner, apprv.repo, apprv.approvalIssueNumber, &github.IssueListCommentsOptions{}) + comments, _, err := client.Issues.ListComments(ctx, apprv.targetRepoOwner, apprv.targetRepoName, apprv.approvalIssueNumber, &github.IssueListCommentsOptions{}) if err != nil { fmt.Printf("error getting comments: %v\n", err) channel <- 1 @@ -65,7 +66,7 @@ func newCommentLoopChannel(ctx context.Context, apprv *approvalEnvironment, clie case approvalStatusApproved: newState := "closed" closeComment := "All approvers have approved, continuing workflow and closing this issue." - _, _, err := client.Issues.CreateComment(ctx, apprv.repoOwner, apprv.repo, apprv.approvalIssueNumber, &github.IssueComment{ + _, _, err := client.Issues.CreateComment(ctx, apprv.targetRepoOwner, apprv.targetRepoName, apprv.approvalIssueNumber, &github.IssueComment{ Body: &closeComment, }) if err != nil { @@ -73,7 +74,7 @@ func newCommentLoopChannel(ctx context.Context, apprv *approvalEnvironment, clie channel <- 1 close(channel) } - _, _, err = client.Issues.Edit(ctx, apprv.repoOwner, apprv.repo, apprv.approvalIssueNumber, &github.IssueRequest{State: &newState}) + _, _, err = client.Issues.Edit(ctx, apprv.targetRepoOwner, apprv.targetRepoName, apprv.approvalIssueNumber, &github.IssueRequest{State: &newState}) if err != nil { fmt.Printf("error closing issue: %v\n", err) channel <- 1 @@ -92,7 +93,7 @@ func newCommentLoopChannel(ctx context.Context, apprv *approvalEnvironment, clie } closeComment += " workflow." - _, _, err := client.Issues.CreateComment(ctx, apprv.repoOwner, apprv.repo, apprv.approvalIssueNumber, &github.IssueComment{ + _, _, err := client.Issues.CreateComment(ctx, apprv.targetRepoOwner, apprv.targetRepoName, apprv.approvalIssueNumber, &github.IssueComment{ Body: &closeComment, }) if err != nil { @@ -100,7 +101,7 @@ func newCommentLoopChannel(ctx context.Context, apprv *approvalEnvironment, clie channel <- 1 close(channel) } - _, _, err = client.Issues.Edit(ctx, apprv.repoOwner, apprv.repo, apprv.approvalIssueNumber, &github.IssueRequest{State: &newState}) + _, _, err = client.Issues.Edit(ctx, apprv.targetRepoOwner, apprv.targetRepoName, apprv.approvalIssueNumber, &github.IssueRequest{State: &newState}) if err != nil { fmt.Printf("error closing issue: %v\n", err) channel <- 1 @@ -169,6 +170,9 @@ func main() { os.Exit(1) } + targetRepoName := os.Getenv(envVarTargetRepo) + targetRepoOwner := os.Getenv(envVarTargetRepoOwner) + repoFullName := os.Getenv(envVarRepoFullName) runID, err := strconv.Atoi(os.Getenv(envVarRunID)) if err != nil { @@ -177,6 +181,12 @@ func main() { } repoOwner := os.Getenv(envVarRepoOwner) + if targetRepoName == "" || targetRepoOwner == "" { + parts := strings.SplitN(repoFullName, "/", 2) + targetRepoOwner = parts[0] + targetRepoName = parts[1] + } + ctx := context.Background() client, err := newGithubClient(ctx) if err != nil { @@ -211,7 +221,8 @@ func main() { os.Exit(1) } } - apprv, err := newApprovalEnvironment(client, repoFullName, repoOwner, runID, approvers, minimumApprovals, issueTitle, issueBody, failOnDenial) + + apprv, err := newApprovalEnvironment(client, repoFullName, repoOwner, runID, approvers, minimumApprovals, issueTitle, issueBody, targetRepoOwner, targetRepoName, failOnDenial) if err != nil { fmt.Printf("error creating approval environment: %v\n", err) os.Exit(1) From 846a0ce970c14b5c4e23061e50e841d622a82913 Mon Sep 17 00:00:00 2001 From: Bailey Everts Date: Sat, 22 Feb 2025 11:53:00 -0700 Subject: [PATCH 28/60] feat: use blocks for issue-body (#137) --- approval.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/approval.go b/approval.go index 540cd71..c7b673d 100644 --- a/approval.go +++ b/approval.go @@ -64,12 +64,14 @@ func (a *approvalEnvironment) createApprovalIssue(ctx context.Context) error { issueTitle = fmt.Sprintf("%s: %s", issueTitle, a.issueTitle) } - issueBody := fmt.Sprintf(`Workflow is pending manual review. -URL: %s + issueBody := fmt.Sprintf(`> Workflow is pending manual review. +> URL: %s -Required approvers: %s +> [!IMPORTANT] +> Required approvers: %s -Respond %s to continue workflow or %s to cancel.`, +> [!TIP] +> Respond %s to continue workflow or %s to cancel.`, a.runURL(), a.issueApprovers, formatAcceptedWords(approvedWords), @@ -77,8 +79,9 @@ Respond %s to continue workflow or %s to cancel.`, ) if a.issueBody != "" { - issueBody = fmt.Sprintf("%s\n\n%s", a.issueBody, issueBody) + issueBody = fmt.Sprintf(">%s\n>\n%s", a.issueBody, issueBody) } + issueBody = fmt.Sprintf(">[!NOTE]\n%s", issueBody) var err error fmt.Printf( From 77d7e0184d4cb81961299ec47ce783cb96bde67d Mon Sep 17 00:00:00 2001 From: Bailey Everts Date: Sun, 23 Feb 2025 09:36:20 -0700 Subject: [PATCH 29/60] feat: dispaly users as a list (#138) --- approval.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/approval.go b/approval.go index c7b673d..f168ab6 100644 --- a/approval.go +++ b/approval.go @@ -64,16 +64,22 @@ func (a *approvalEnvironment) createApprovalIssue(ctx context.Context) error { issueTitle = fmt.Sprintf("%s: %s", issueTitle, a.issueTitle) } + approversBody := "" + for _, approver := range a.issueApprovers { + approversBody = fmt.Sprintf("%s> * @%s\n", approversBody, approver) + } + issueBody := fmt.Sprintf(`> Workflow is pending manual review. > URL: %s > [!IMPORTANT] -> Required approvers: %s +> Required approvers: +%s > [!TIP] > Respond %s to continue workflow or %s to cancel.`, a.runURL(), - a.issueApprovers, + approversBody, formatAcceptedWords(approvedWords), formatAcceptedWords(deniedWords), ) From 8b4df3f1bc12bd87945015fe497dce8f59c576b7 Mon Sep 17 00:00:00 2001 From: Sanskar Arora <55059942+sunny-1651@users.noreply.github.com> Date: Tue, 4 Mar 2025 05:56:53 +0530 Subject: [PATCH 30/60] Dependabot codeql patch (#152) * addding codecheck yamls * updating versions * code ql to run only on main repo not forks --- .github/dependabot.yml | 7 +++++++ .github/workflows/codeql.yml | 37 ++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/codeql.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..21c0165 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 # default: 5 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..6d68125 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,37 @@ +name: "Code Analysis" + +on: + push: + pull_request: + +permissions: + contents: read + +jobs: + CodeQL-Build: + if: github.repository == 'trstringer/manual-approval' + permissions: + actions: read + contents: read + security-events: write + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Golang + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: go + + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 From 2177c2e2376645b3bad2dabd24d77b3af170f11f Mon Sep 17 00:00:00 2001 From: Sanskar Arora <55059942+sunny-1651@users.noreply.github.com> Date: Tue, 4 Mar 2025 21:54:44 +0530 Subject: [PATCH 31/60] Update codeql.yml --- .github/workflows/codeql.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 6d68125..f5442a1 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -2,6 +2,8 @@ name: "Code Analysis" on: push: + branches: + - "main" pull_request: permissions: From 824801926666d7718ae522c838704d64611ab80a Mon Sep 17 00:00:00 2001 From: Liz MacLean <18120837+lizziemac@users.noreply.github.com> Date: Thu, 6 Mar 2025 15:50:38 -0500 Subject: [PATCH 32/60] Cleanup README and add some more informative messaging (#149) --- README.md | 51 ++++++++++++++++++++++++++++++--------------------- action.yaml | 3 --- main.go | 3 ++- 3 files changed, 32 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 68336f9..37317e0 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,13 @@ + # Manual Workflow Approval [![ci](https://github.com/trstringer/manual-approval/actions/workflows/ci.yaml/badge.svg)](https://github.com/trstringer/manual-approval/actions/workflows/ci.yaml) Pause a GitHub Actions workflow and require manual approval from one or more approvers before continuing. -This is a very common feature for a deployment or release pipeline, and while [this functionality is available from GitHub](https://docs.github.com/en/actions/managing-workflow-runs/reviewing-deployments), it requires the use of environments and if you want to use this for private repositories then you need GitHub Enterprise. This action provides manual approval without the use of environments, and is freely available to use on private repositories. +This is a very common feature for a deployment or release pipeline, and while [this functionality is available from GitHub](https://docs.github.com/en/actions/managing-workflow-runs/reviewing-deployments), it requires the use of environments and if you want to use this for private repositories, then you need GitHub Enterprise. This action provides manual approval without the use of environments, and is freely available to use on private repositories. -*Note: This approval duration is subject to the broader 72 hours timeout for a workflow. So keep that in mind when figuring out how quickly an approver must respond.* +*Note: This approval duration is subject to the broader 35 day timeout for a workflow, as well as usage costs. So keep that in mind when figuring out how quickly an approver must respond. See [limitations](#limitations) for more information.* The way this action works is the following: @@ -34,17 +35,23 @@ steps: issue-title: "Deploying v1.3.5 to prod from staging" issue-body: "Please approve or deny the deployment of version v1.3.5." exclude-workflow-initiator-as-approver: false + fail-on-denial: true additional-approved-words: '' additional-denied-words: '' ``` -- `approvers` is a comma-delimited list of all required approvers. An approver can either be a user or an org team. (*Note: Required approvers must have the ability to be set as approvers in the repository. If you add an approver that doesn't have this permission then you would receive an HTTP/402 Validation Failed error when running this action*) -- `minimum-approvals` is an integer that sets the minimum number of approvals required to progress the workflow. Defaults to ALL approvers. -- `issue-title` is a string that will be appended to the title of the issue. -- `issue-body` is a string that will be prepended to the body of the issue. -- `exclude-workflow-initiator-as-approver` is a boolean that indicates if the workflow initiator (determined by the `GITHUB_ACTOR` environment variable) should be filtered from the final list of approvers. This is optional and defaults to `false`. Set this to `true` to prevent users in the `approvers` list from being able to self-approve workflows. -- `additional-approved-words` is a comma separated list of strings to expand the dictionary of words that indicate approval. This is optional and defaults to an empty string. -- `additional-denied-words` is a comma separated list of strings to expand the dictionary of words that indicate denial. This is optional and defaults to an empty string. +* `approvers` is a comma-delimited list of all required approvers. An approver can either be a user or an org team. (*Note: Required approvers must have the ability to be set as approvers in the repository. If you add an approver that doesn't have this permission then you would receive an HTTP/402 Validation Failed error when running this action*) +* `minimum-approvals` is an integer that sets the minimum number of approvals required to progress the workflow. Defaults to ALL approvers. +* `issue-title` is a string that will be appended to the title of the issue. +* `issue-body` is a string that will be prepended to the body of the issue. +* `exclude-workflow-initiator-as-approver` is a boolean that indicates if the workflow initiator (determined by the `GITHUB_ACTOR` environment variable) should be filtered from the final list of approvers. This is optional and defaults to `false`. Set this to `true` to prevent users in the `approvers` list from being able to self-approve workflows. +* `fail-on-denial` is a boolean that indicates if the workflow should fail if any approver denies the approval. This is optional and defaults to `true`. Set this to `false` to allow the workflow to continue if any approver denies the approval. +* `additional-approved-words` is a comma separated list of strings to expand the dictionary of words that indicate approval. This is optional and defaults to an empty string. +* `additional-denied-words` is a comma separated list of strings to expand the dictionary of words that indicate denial. This is optional and defaults to an empty string. + +### Outputs + +* `approval_status` is a string that indicates the final status of the approval. This will be either `approved` or `denied`. ### Creating Issues in a different repository @@ -124,8 +131,10 @@ For more information on permissions, please look at the [GitHub documentation](h ## Limitations * While the workflow is paused, it will still continue to consume a concurrent job allocation out of the [max concurrent jobs](https://docs.github.com/en/actions/learn-github-actions/usage-limits-billing-and-administration#usage-limits). -* A job (including a paused job) will be failed [after 6 hours](https://docs.github.com/en/actions/learn-github-actions/usage-limits-billing-and-administration#usage-limits). * A paused job is still running compute/instance/virtual machine and will continue to incur costs. +* Expirations (also mentioned elsewhere in this document): + * A job (including a paused job) will be failed [after 6 hours, and a workflow will be failed after 35 days](https://docs.github.com/en/actions/learn-github-actions/usage-limits-billing-and-administration#usage-limits). + * GitHub App tokens expire after 1 hour which implies duration for the approval cannot exceed 60 minutes or the job will fail due to bad credentials. See [docs](https://docs.github.com/en/rest/apps/apps#create-an-installation-access-token-for-an-app) ## Development @@ -139,16 +148,16 @@ To test out your code in an action, you need to build the image and push it to a Build the image: -``` -$ VERSION=1.7.1-rc.1 make IMAGE_REPO=ghcr.io/trstringer/manual-approval-test build +```shell +VERSION=1.7.1-rc.1 make IMAGE_REPO=ghcr.io/trstringer/manual-approval-test build ``` *Note: The image version can be whatever you want, as this image wouldn't be pushed to production. It is only for testing.* Push the image to your container registry: -``` -$ VERSION=1.7.1-rc.1 make IMAGE_REPO=ghcr.io/trstringer/manual-approval-test push +```shell +VERSION=1.7.1-rc.1 make IMAGE_REPO=ghcr.io/trstringer/manual-approval-test push ``` To test out the image you will need to modify `action.yaml` so that it points to your new image that you're testing: @@ -174,10 +183,10 @@ For `uses`, this should point to your repo and dev branch. ### Create a release 1. Build the new version's image: `$ VERSION=1.7.0 make build` -1. Push the new image: `$ VERSION=1.7.0 make push` -1. Create a release branch and modify `action.yaml` to point to the new image -1. Open and merge a PR to add these changes to the default branch -1. Make sure to fetch the new changes into your local repo: `$ git checkout main && git fetch origin && git merge origin main` -1. Delete the `v1` tag locally and remotely: `$ git tag -d v1 && git push --delete origin v1` -1. Create and push new tags: `$ git tag v1.7.0 && git tag v1 && git push origin --tags` -1. Create the GitHub project release +2. Push the new image: `$ VERSION=1.7.0 make push` +3. Create a release branch and modify `action.yaml` to point to the new image +4. Open and merge a PR to add these changes to the default branch +5. Make sure to fetch the new changes into your local repo: `$ git checkout main && git fetch origin && git merge origin main` +6. Delete the `v1` tag locally and remotely: `$ git tag -d v1 && git push --delete origin v1` +7. Create and push new tags: `$ git tag v1.7.0 && git tag v1 && git push origin --tags` +8. Create the GitHub project release diff --git a/action.yaml b/action.yaml index c515b96..9998363 100644 --- a/action.yaml +++ b/action.yaml @@ -13,9 +13,6 @@ inputs: minimum-approvals: description: Minimum number of approvals to progress workflow required: false - timeout-minutes: - description: Force timeout of your workflow pause - required: false issue-title: description: The custom subtitle for the issue required: false diff --git a/main.go b/main.go index e9944bb..b771f92 100644 --- a/main.go +++ b/main.go @@ -29,6 +29,7 @@ func setActionOutput(name, value string) error { func handleInterrupt(ctx context.Context, client *github.Client, apprv *approvalEnvironment) { newState := "closed" closeComment := "Workflow cancelled, closing issue." + fmt.Println(closeComment) _, _, err := client.Issues.CreateComment(ctx, apprv.targetRepoOwner, apprv.targetRepoName, apprv.approvalIssueNumber, &github.IssueComment{ Body: &closeComment, @@ -65,7 +66,7 @@ func newCommentLoopChannel(ctx context.Context, apprv *approvalEnvironment, clie switch approved { case approvalStatusApproved: newState := "closed" - closeComment := "All approvers have approved, continuing workflow and closing this issue." + closeComment := fmt.Sprintf("The required number of approvals (%d) has been met; continuing workflow and closing this issue.", apprv.minimumApprovals) _, _, err := client.Issues.CreateComment(ctx, apprv.targetRepoOwner, apprv.targetRepoName, apprv.approvalIssueNumber, &github.IssueComment{ Body: &closeComment, }) From c10ec77612398b015cd669cfc6f931fbde744c8a Mon Sep 17 00:00:00 2001 From: Mateus Caruccio Date: Fri, 7 Mar 2025 06:46:54 +0000 Subject: [PATCH 33/60] Feature: Save output (#132) --- action.yaml | 7 ++++++ approval.go | 39 ++++++++++++++++++++++++++++++++ approval_test.go | 59 ++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 28 +++++++++++------------ 4 files changed, 119 insertions(+), 14 deletions(-) diff --git a/action.yaml b/action.yaml index 9998363..a61c3c9 100644 --- a/action.yaml +++ b/action.yaml @@ -41,6 +41,13 @@ inputs: description: Whether or not to fail the workflow if the approval is denied required: false default: 'true' +outputs: + issue-number: + description: The number of the issue created + issue-url: + description: The URL of the issue created + approval-status: + description: The status of the approval ("approved" or "denied") runs: using: docker image: docker://ghcr.io/trstringer/manual-approval:1.9.1 diff --git a/approval.go b/approval.go index 1ed12d9..9516b64 100644 --- a/approval.go +++ b/approval.go @@ -3,6 +3,7 @@ package main import ( "context" "fmt" + "os" "regexp" "strings" @@ -112,6 +113,44 @@ func (a *approvalEnvironment) createApprovalIssue(ctx context.Context) error { return nil } +func (a *approvalEnvironment) SetActionOutputs(outputs map[string]string) (bool, error) { + outputFile := os.Getenv("GITHUB_OUTPUT") + if outputFile == "" { + return false, nil + } + + f, err := os.OpenFile(outputFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + + if err != nil { + return false, err + } + defer f.Close() + + var pairs []string + + for key, value := range outputs { + pairs = append(pairs, fmt.Sprintf("%s=%s", key, value)) + } + + // Add a newline before writing the new outputs if the file is not empty. This prevents + // two outputs from being written on the same line. + fileInfo, err := f.Stat() + if err != nil { + return false, err + } + if fileInfo.Size() > 0 { + if _, err := f.WriteString("\n"); err != nil { + return false, err + } + } + + if _, err := f.WriteString(strings.Join(pairs, "\n")); err != nil { + return false, err + } + + return true, nil +} + func approvalFromComments(comments []*github.IssueComment, approvers []string, minimumApprovals int) (approvalStatus, error) { remainingApprovers := make([]string, len(approvers)) copy(remainingApprovers, approvers) diff --git a/approval_test.go b/approval_test.go index afc215f..cefe1b0 100644 --- a/approval_test.go +++ b/approval_test.go @@ -1,6 +1,8 @@ package main import ( + "errors" + "os" "testing" "github.com/google/go-github/v43/github" @@ -427,3 +429,60 @@ func TestDeniedCommentBody(t *testing.T) { }) } } + +func TestSaveOutput(t *testing.T) { + testCases := []struct { + name string + approvalIssueNumber int + env_github_output string + isSuccess bool + }{ + { + name: "save_output_with_env", + approvalIssueNumber: 123, + env_github_output: "./output.txt", + isSuccess: true, + }, + { + name: "fail_save_output_without_env", + approvalIssueNumber: 123, + env_github_output: "", + isSuccess: false, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + os.Setenv("GITHUB_OUTPUT", testCase.env_github_output) + a := approvalEnvironment{ + client: nil, + repoFullName: "", + repo: "", + repoOwner: "", + runID: -1, + approvalIssueNumber: testCase.approvalIssueNumber, + issueTitle: "", + issueBody: "", + issueApprovers: nil, + minimumApprovals: 0, + } + + os.Remove(testCase.env_github_output) + actual, err := a.SetActionOutputs(nil) + + if err != nil { + t.Fatalf("error creating output file: %v: %v", testCase.env_github_output, err) + } + + if actual != testCase.isSuccess { + t.Fatalf("expected %v but got %v", testCase.isSuccess, actual) + } + + if actual == true { + if _, err := os.Stat(testCase.env_github_output); errors.Is(err, os.ErrNotExist) { + t.Fatalf("expected create output file %v but it was not", testCase.env_github_output) + } + } + }) + } +} diff --git a/main.go b/main.go index b771f92..e439961 100644 --- a/main.go +++ b/main.go @@ -13,19 +13,6 @@ import ( "golang.org/x/oauth2" ) -func setActionOutput(name, value string) error { - f, err := os.OpenFile(os.Getenv("GITHUB_OUTPUT"), os.O_APPEND|os.O_WRONLY, 0600) - if err != nil { - return err - } - defer f.Close() - - if _, err = f.WriteString(fmt.Sprintf("%s=%s\n", name, value)); err != nil { - return err - } - return nil -} - func handleInterrupt(ctx context.Context, client *github.Client, apprv *approvalEnvironment) { newState := "closed" closeComment := "Workflow cancelled, closing issue." @@ -235,6 +222,16 @@ func main() { os.Exit(1) } + outputs := map[string]string { + "issue-number": fmt.Sprintf("%d", apprv.approvalIssueNumber), + "issue-url": apprv.approvalIssue.GetHTMLURL(), + } + _, err = apprv.SetActionOutputs(outputs) + if err != nil { + fmt.Printf("error saving output: %v", err) + os.Exit(1) + } + killSignalChannel := make(chan os.Signal, 1) signal.Notify(killSignalChannel, os.Interrupt) @@ -252,7 +249,10 @@ func main() { } else { approvalStatus = "approved" } - if err := setActionOutput("approval_status", approvalStatus); err != nil { + outputs := map[string]string { + "approval-status": approvalStatus, + } + if _, err := apprv.SetActionOutputs(outputs); err != nil { fmt.Printf("error setting action output: %v\n", err) exitCode = 1 } From e6592a6b3284d0b354d3fa087600e76af66bb238 Mon Sep 17 00:00:00 2001 From: sunny-1651 Date: Fri, 7 Mar 2025 04:38:42 +0530 Subject: [PATCH 34/60] create go sum at build-time with fresh hashes --- .github/workflows/ci.yaml | 2 + .gitignore | 1 + Dockerfile | 1 + Makefile | 4 + go.sum | 383 -------------------------------------- 5 files changed, 8 insertions(+), 383 deletions(-) delete mode 100644 go.sum diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5fa65b1..dbcfa1a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -21,6 +21,8 @@ jobs: - name: Checkout uses: actions/checkout@v2 + - name: Refresh module hashsums + run: make tidy - name: Build run: make build env: diff --git a/.gitignore b/.gitignore index a9ad188..03ad3d9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .env .idea/ +go.sum diff --git a/Dockerfile b/Dockerfile index 6ec73aa..da0f22e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,7 @@ FROM golang:1.17 AS builder COPY . /var/app WORKDIR /var/app +RUN go mod tidy RUN CGO_ENABLED=0 go build -o app . FROM alpine:3.14 diff --git a/Makefile b/Makefile index 8c3fc9b..56ba8bc 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,10 @@ IMAGE_REPO=ghcr.io/trstringer/manual-approval TARGET_PLATFORM=linux/amd64 +.PHONY: tidy +tidy: + go mod tidy + .PHONY: build build: @if [ -z "$$VERSION" ]; then \ diff --git a/go.sum b/go.sum deleted file mode 100644 index fdb1ce0..0000000 --- a/go.sum +++ /dev/null @@ -1,383 +0,0 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/bradleyfalzon/ghinstallation/v2 v2.0.4/go.mod h1:B40qPqJxWE0jDZgOR1JmaMy+4AY1eBP+IByOvqyAKp0= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= -github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/google/go-github/v41 v41.0.0/go.mod h1:XgmCA5H323A9rtgExdTcnDkcqp6S30AVACCBDOonIxg= -github.com/google/go-github/v43 v43.0.0 h1:y+GL7LIsAIF2NZlJ46ZoC/D1W1ivZasT0lnWHMYPZ+U= -github.com/google/go-github/v43 v43.0.0/go.mod h1:ZkTvvmCXBvsfPpTHXnH/d2hP9Y0cTbvN9kr5xqyXOIc= -github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= -github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a h1:qfl7ob3DIEs3Ml9oLuPwY2N04gymzAW04WsUQHIClgM= -golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= From 330ce9b42bd3ee087d843f64b239f2b8e6ffaaff Mon Sep 17 00:00:00 2001 From: Liz MacLean <18120837+lizziemac@users.noreply.github.com> Date: Mon, 17 Mar 2025 10:58:39 -0400 Subject: [PATCH 35/60] Create CONTRIBUTING.md (#160) --- CONTRIBUTING.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..bf270b3 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,44 @@ +# Contribution Guide + +Thank you for your interest in contributing to this project! We appreciate your efforts to make this project better. Please follow the guidelines below to ensure a smooth contribution process. + +## Guidelines + +### Code Contributions +- Follow the existing code style and conventions. +- Write meaningful PR titles. +- Ensure your changes do not break existing functionality. +- Run tests before submitting a pull request (PR). +- Build and run your changes in your own environment to ensure they work as expected. +- Add or update documentation if necessary. + +### Submitting a Pull Request +1. Ensure your branch is up to date with the main branch: + + ```sh + git fetch upstream + git checkout main + git merge upstream/main + ``` + +2. Push your changes to your fork: + + ```sh + git push origin feature-branch-name + ``` + +3. Open a PR on GitHub, describing your changes clearly. +4. If your PR is related to an issue, use the [GitHub key words](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests#linking-a-pull-request-to-an-issue). +This will ensure that the related issue will automatically close when your change is merged. +5. Address any feedback and update your PR as necessary. + +### Issue Reporting +- **Check for existing issues before opening a new one.** +- Provide clear, concise descriptions with steps to reproduce. +- Include logs, screenshots, or example code if relevant. +- Suggest possible solutions if applicable. + +## License +By contributing, you agree that your contributions will be licensed under the project's [LICENSE](LICENSE). + +Thank you for contributing! From ef4dddd64bf9eca035ffe51d23f12d20e3a83613 Mon Sep 17 00:00:00 2001 From: Liz MacLean <18120837+lizziemac@users.noreply.github.com> Date: Mon, 17 Mar 2025 10:58:51 -0400 Subject: [PATCH 36/60] Issue templates (#161) --- .github/ISSUE_TEMPLATE/bug_report.md | 29 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 22 +++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..d0c25c5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +- [ ] I have searched through the current issues and did not find any that were related. + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..6089845 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,22 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +- [ ] I have searched through the current feature requests and did not find any that were related. + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. From c1ebf589e26f42869b0ee9f5fde3cf15e6fef997 Mon Sep 17 00:00:00 2001 From: Simeon Widdis Date: Sun, 20 Apr 2025 08:36:57 -0700 Subject: [PATCH 37/60] Make name comparison case-insensitive (#172) Signed-off-by: Simeon Widdis --- approval.go | 10 +++++----- approval_test.go | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/approval.go b/approval.go index 9516b64..8487e26 100644 --- a/approval.go +++ b/approval.go @@ -136,12 +136,12 @@ func (a *approvalEnvironment) SetActionOutputs(outputs map[string]string) (bool, // two outputs from being written on the same line. fileInfo, err := f.Stat() if err != nil { - return false, err + return false, err } if fileInfo.Size() > 0 { - if _, err := f.WriteString("\n"); err != nil { - return false, err - } + if _, err := f.WriteString("\n"); err != nil { + return false, err + } } if _, err := f.WriteString(strings.Join(pairs, "\n")); err != nil { @@ -194,7 +194,7 @@ func approvalFromComments(comments []*github.IssueComment, approvers []string, m func approversIndex(approvers []string, name string) int { for idx, approver := range approvers { - if approver == name { + if strings.EqualFold(approver, name) { return idx } } diff --git a/approval_test.go b/approval_test.go index cefe1b0..efddf7f 100644 --- a/approval_test.go +++ b/approval_test.go @@ -3,6 +3,7 @@ package main import ( "errors" "os" + "strings" "testing" "github.com/google/go-github/v43/github" @@ -16,6 +17,8 @@ func TestApprovalFromComments(t *testing.T) { bodyDenied := "Denied" bodyPending := "not approval or denial" + login1u := strings.ToUpper(login1) + testCases := []struct { name string comments []*github.IssueComment @@ -160,6 +163,17 @@ func TestApprovalFromComments(t *testing.T) { expectedStatus: approvalStatusPending, minimumApprovals: 2, }, + { + name: "single_approver_single_comment_approved_case_insensitive", + comments: []*github.IssueComment{ + { + User: &github.User{Login: &login1u}, + Body: &bodyApproved, + }, + }, + approvers: []string{login1}, + expectedStatus: approvalStatusApproved, + }, } for _, testCase := range testCases { From bfb0ba38d02a528a2c101381809dc6c3062732c1 Mon Sep 17 00:00:00 2001 From: Sunny <55059942+snskArora@users.noreply.github.com> Date: Sat, 26 Apr 2025 12:02:02 +0530 Subject: [PATCH 38/60] Adding support for issue-body with more than 65536 words, issue body to be created as an issue comment (#167) * push a built package to ghcr * adding err catch * auth check * auth update * update uname * update uname * testing with splitted issues * minor fix * revert testing changes * Update approval.go --- .github/workflows/ci.yaml | 1 + Makefile | 6 +-- approval.go | 86 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 87 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index dbcfa1a..c0d4e03 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -27,6 +27,7 @@ jobs: run: make build env: VERSION: latest + - name: Test run: make test - name: Lint diff --git a/Makefile b/Makefile index 56ba8bc..54f20b9 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ tidy: .PHONY: build build: - @if [ -z "$$VERSION" ]; then \ + @if [ -z "$(VERSION)" ]; then \ echo "VERSION is required"; \ exit 1; \ fi @@ -15,11 +15,11 @@ build: .PHONY: push push: - @if [ -z "$$VERSION" ]; then \ + @if [ -z "$(VERSION)" ]; then \ echo "VERSION is required"; \ exit 1; \ fi - docker push $(IMAGE_REPO):$$VERSION + docker push $(IMAGE_REPO):$(VERSION) .PHONY: test test: diff --git a/approval.go b/approval.go index 8487e26..2b7baa1 100644 --- a/approval.go +++ b/approval.go @@ -85,9 +85,6 @@ func (a *approvalEnvironment) createApprovalIssue(ctx context.Context) error { formatAcceptedWords(deniedWords), ) - if a.issueBody != "" { - issueBody = fmt.Sprintf(">%s\n>\n%s", a.issueBody, issueBody) - } issueBody = fmt.Sprintf(">[!NOTE]\n%s", issueBody) var err error @@ -109,6 +106,16 @@ func (a *approvalEnvironment) createApprovalIssue(ctx context.Context) error { } a.approvalIssueNumber = a.approvalIssue.GetNumber() + bodyChunks := splitLongString(a.issueBody) + for _, chunk := range bodyChunks { + _, _, err = a.client.Issues.CreateComment(ctx, a.targetRepoOwner, a.targetRepoName, *a.approvalIssue.Number, &github.IssueComment{ + Body: &chunk, + }) + if err != nil { + return fmt.Errorf("failed to add comment chunk to issue: %w", err) + } + } + fmt.Printf("Issue created: %s\n", a.approvalIssue.GetHTMLURL()) return nil } @@ -244,3 +251,76 @@ func formatAcceptedWords(words []string) string { return strings.Join(quotedWords, ", ") } + +func splitLongLine(line string, maxL int) ([]string, bool) { + if len(line) <= maxL { + return []string{line}, false + } + + words := strings.Fields(line) + var result []string + var currentLine string + + for _, word := range words { + if len(currentLine)+len(word)+1 > maxL { + result = append(result, currentLine) + currentLine = word + } else { + if currentLine != "" { + currentLine += " " + } + currentLine += word + } + } + if currentLine != "" { + result = append(result, currentLine) + } + return result, true +} + +func splitLongString(input string) []string { + maxLength := 65536 + var result []string + + lines := strings.Split(input, "\n") + currentChunk := strings.Builder{} + currentLength := 0 + + for i, line := range lines { + lineLength := len(line) + if i < len(lines)-1 { + lineLength++ + } + + if currentLength+lineLength > maxLength { + if currentChunk.Len() > 0 { + result = append(result, currentChunk.String()) + currentChunk.Reset() + currentLength = 0 + } + } + + lineSplit, isLongLine := splitLongLine(line, maxLength) + if isLongLine { + if currentChunk.Len() > 0 { + result = append(result, currentChunk.String()) + currentChunk.Reset() + } + result = append(result, lineSplit[:len(lineSplit)-1]...) + currentChunk.WriteString(lineSplit[len(lineSplit)-1]) + currentLength = len(lineSplit[len(lineSplit)-1]) + } else { + currentChunk.WriteString(line) + currentLength += lineLength + } + + if i < len(lines)-1 { + currentChunk.WriteString("\n") + } + } + if currentChunk.Len() > 0 { + result = append(result, currentChunk.String()) + } + return result +} + From 9ef3307c2579bc7ba1c3f292c7942b9ce57a4186 Mon Sep 17 00:00:00 2001 From: Liz MacLean <18120837+lizziemac@users.noreply.github.com> Date: Sat, 10 May 2025 11:10:29 -0400 Subject: [PATCH 39/60] Update README.md to have a more explicit callout about timeout-minutes (#180) --- README.md | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 37317e0..4c87544 100644 --- a/README.md +++ b/README.md @@ -108,14 +108,30 @@ jobs: If you'd like to force a timeout of your workflow pause, you can specify `timeout-minutes` at either the [step](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepstimeout-minutes) level or the [job](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idtimeout-minutes) level. +> **Note:** The `timeout-minutes` option has been removed from the `manual-approval` inputs, as it did nothing and incorrectly assured users that they were in fact +> getting timeout behavior. Please use one of the below two approaches instead. +> +> If you are currently using `timeout-minutes` as a `manual-approval` input, you may see a warning, but this will not break your action. + For instance, if you want your manual approval step to timeout after an hour you could do the following: ```yaml -steps: - - uses: trstringer/manual-approval@v1 - timeout-minutes: 60 +jobs: + approval: + steps: + - uses: trstringer/manual-approval@v1 + timeout-minutes: 60 ... ``` +or +```yaml +jobs: + approval: + timeout-minutes: 10 + steps: + - uses: trstringer/manual-approval@v1 + +``` ## Permissions From fb0bfeda5ff2d043dcad82c10be2db6e46086662 Mon Sep 17 00:00:00 2001 From: Liz MacLean <18120837+lizziemac@users.noreply.github.com> Date: Tue, 20 May 2025 18:17:01 -0400 Subject: [PATCH 40/60] Fix typo in README related to the action output of approval status (#181) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4c87544..86a2034 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ steps: ### Outputs -* `approval_status` is a string that indicates the final status of the approval. This will be either `approved` or `denied`. +* `approval-status` is a string that indicates the final status of the approval. This will be either `approved` or `denied`. ### Creating Issues in a different repository From a1f96b91e22565d2730d88ae055c8dc333e471c1 Mon Sep 17 00:00:00 2001 From: Sunny <55059942+snskArora@users.noreply.github.com> Date: Tue, 27 May 2025 01:44:38 +0530 Subject: [PATCH 41/60] Updates for release 1.10.0 (#183) --- Dockerfile | 2 +- action.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index da0f22e..d757e2e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ RUN go mod tidy RUN CGO_ENABLED=0 go build -o app . FROM alpine:3.14 -LABEL org.opencontainers.image.source https://github.com/trstringer/manual-approval +LABEL org.opencontainers.image.source=https://github.com/trstringer/manual-approval RUN apk update && apk add ca-certificates COPY --from=builder /var/app/app /var/app/app CMD ["/var/app/app"] diff --git a/action.yaml b/action.yaml index a61c3c9..0126c0a 100644 --- a/action.yaml +++ b/action.yaml @@ -50,4 +50,4 @@ outputs: description: The status of the approval ("approved" or "denied") runs: using: docker - image: docker://ghcr.io/trstringer/manual-approval:1.9.1 + image: docker://ghcr.io/trstringer/manual-approval:1.10.0 From 3441ed44b6da65234eadb26bd056be8c1996e8b3 Mon Sep 17 00:00:00 2001 From: Sunny <55059942+snskArora@users.noreply.github.com> Date: Fri, 13 Jun 2025 19:16:46 +0530 Subject: [PATCH 42/60] Multi arch build & Version bump (#185) * Upgrading versions * mod tidy * LInting * Linting * Update action.yaml * [skip ci] Reverting test changes * msc chnages * Update Makefile * Update README.md * Update README.md * Update README.md * Update Makefile * [skip ci] Update README.md * cosmetic changes --- Dockerfile | 2 +- Makefile | 13 ++++++++----- README.md | 28 +++++++++++++++++++--------- approval.go | 6 ++++-- approval_test.go | 7 +++++-- go.mod | 10 +++------- 6 files changed, 40 insertions(+), 26 deletions(-) diff --git a/Dockerfile b/Dockerfile index d757e2e..4fa74dd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.17 AS builder +FROM golang:1.24 AS builder COPY . /var/app WORKDIR /var/app RUN go mod tidy diff --git a/Makefile b/Makefile index 54f20b9..4a5a791 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ IMAGE_REPO=ghcr.io/trstringer/manual-approval -TARGET_PLATFORM=linux/amd64 +TARGET_PLATFORM=linux/amd64,linux/arm64,linux/arm/v8 .PHONY: tidy tidy: @@ -11,15 +11,18 @@ build: echo "VERSION is required"; \ exit 1; \ fi - docker build --platform $(TARGET_PLATFORM) -t $(IMAGE_REPO):$$VERSION . + docker build -t $(IMAGE_REPO):$(VERSION) . -.PHONY: push +.PHONY: build_push push: @if [ -z "$(VERSION)" ]; then \ echo "VERSION is required"; \ exit 1; \ fi - docker push $(IMAGE_REPO):$(VERSION) + docker buildx create --use --name mybuilder + docker buildx build --push --platform $(TARGET_PLATFORM) -t $(IMAGE_REPO):$(VERSION) . + docker buildx rm mybuilder + .PHONY: test test: @@ -27,4 +30,4 @@ test: .PHONY: lint lint: - docker run --rm -v $$(pwd):/app -w /app golangci/golangci-lint:v1.46.2 golangci-lint run -v + docker run --rm -v $$(pwd):/app -w /app golangci/golangci-lint:v2.1.6 golangci-lint run -v diff --git a/README.md b/README.md index 86a2034..b0ad57a 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,15 @@ These are case insensitive with optional punctuation either a period or an excla In all cases, `manual-approval` will close the initial GitHub issue. +🖥️ Supported Runners, The action is compatible with the following runner types: +- Linux/amd64 — 64-bit Intel/AMD (x86_64) +- Linux/arm64 — 64-bit ARM (Apple M1) +- Linux/arm/v8 — 64-bit ARM + +🚫 Unsupported +- Windows/amd64 — 64-bit Windows systems are currently not supported. +- Non-Linux runners of any architecture. + ## Usage ```yaml @@ -108,7 +117,9 @@ jobs: If you'd like to force a timeout of your workflow pause, you can specify `timeout-minutes` at either the [step](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepstimeout-minutes) level or the [job](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idtimeout-minutes) level. -> **Note:** The `timeout-minutes` option has been removed from the `manual-approval` inputs, as it did nothing and incorrectly assured users that they were in fact +> [!Note] +> +> The `timeout-minutes` option has been removed from the `manual-approval` inputs, as it did nothing and incorrectly assured users that they were in fact > getting timeout behavior. Please use one of the below two approaches instead. > > If you are currently using `timeout-minutes` as a `manual-approval` input, you may see a warning, but this will not break your action. @@ -198,11 +209,10 @@ For `uses`, this should point to your repo and dev branch. ### Create a release -1. Build the new version's image: `$ VERSION=1.7.0 make build` -2. Push the new image: `$ VERSION=1.7.0 make push` -3. Create a release branch and modify `action.yaml` to point to the new image -4. Open and merge a PR to add these changes to the default branch -5. Make sure to fetch the new changes into your local repo: `$ git checkout main && git fetch origin && git merge origin main` -6. Delete the `v1` tag locally and remotely: `$ git tag -d v1 && git push --delete origin v1` -7. Create and push new tags: `$ git tag v1.7.0 && git tag v1 && git push origin --tags` -8. Create the GitHub project release +1. Build and push the new image: `$ VERSION=1.7.0 make build_push` +2. Create a release branch and modify `action.yaml` to point to the new image +3. Open and merge a PR to add these changes to the default branch +4. Make sure to fetch the new changes into your local repo: `$ git checkout main && git fetch origin && git merge origin main` +5. Delete the `v1` tag locally and remotely: `$ git tag -d v1 && git push --delete origin v1` +6. Create and push new tags: `$ git tag v1.7.0 && git tag v1 && git push origin --tags` +7. Create the GitHub project release diff --git a/approval.go b/approval.go index 2b7baa1..6391ad2 100644 --- a/approval.go +++ b/approval.go @@ -127,11 +127,13 @@ func (a *approvalEnvironment) SetActionOutputs(outputs map[string]string) (bool, } f, err := os.OpenFile(outputFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - if err != nil { return false, err } - defer f.Close() + + defer func() { + _ = f.Close() // Error explicitly ignored as there is nothing to handle if file close fails. + }() var pairs []string diff --git a/approval_test.go b/approval_test.go index efddf7f..086a6fa 100644 --- a/approval_test.go +++ b/approval_test.go @@ -467,7 +467,7 @@ func TestSaveOutput(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - os.Setenv("GITHUB_OUTPUT", testCase.env_github_output) + t.Setenv("GITHUB_OUTPUT", testCase.env_github_output) a := approvalEnvironment{ client: nil, repoFullName: "", @@ -481,7 +481,10 @@ func TestSaveOutput(t *testing.T) { minimumApprovals: 0, } - os.Remove(testCase.env_github_output) + if err := os.Remove(testCase.env_github_output); err != nil && !os.IsNotExist(err) { + t.Fatalf("failed to remove file: %v", err) + } + actual, err := a.SetActionOutputs(nil) if err != nil { diff --git a/go.mod b/go.mod index 413e6d2..9c0d645 100644 --- a/go.mod +++ b/go.mod @@ -1,17 +1,13 @@ module github.com/trstringer/manual-approval -go 1.17 +go 1.24 require ( github.com/google/go-github/v43 v43.0.0 - golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a + golang.org/x/oauth2 v0.30.0 ) require ( - github.com/golang/protobuf v1.4.2 // indirect github.com/google/go-querystring v1.1.0 // indirect - golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect - golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/protobuf v1.25.0 // indirect + golang.org/x/crypto v0.35.0 // indirect ) From dd3e4e00d2123a2587991c60f29d3edec3c2fc5c Mon Sep 17 00:00:00 2001 From: Marco Stuurman Date: Fri, 13 Jun 2025 21:08:17 +0200 Subject: [PATCH 43/60] Add option for using a file as input for the issue body (#140) * Add option for using a file as input for the issue body * Revert irrelevant version bumps --- action.yaml | 3 +++ constants.go | 1 + main.go | 12 +++++++++++- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/action.yaml b/action.yaml index 0126c0a..7918c1b 100644 --- a/action.yaml +++ b/action.yaml @@ -19,6 +19,9 @@ inputs: issue-body: description: The custom body for the issue required: false + issue-body-file-path: + description: The file path to a custom body for the issue + required: false exclude-workflow-initiator-as-approver: description: Whether or not to filter out the user who initiated the workflow as an approver if they are in the approvers list required: false diff --git a/constants.go b/constants.go index 027aafa..88867e3 100644 --- a/constants.go +++ b/constants.go @@ -18,6 +18,7 @@ const ( envVarMinimumApprovals string = "INPUT_MINIMUM-APPROVALS" envVarIssueTitle string = "INPUT_ISSUE-TITLE" envVarIssueBody string = "INPUT_ISSUE-BODY" + envVarIssueBodyFilePath string = "INPUT_ISSUE-BODY-FILE-PATH" envVarExcludeWorkflowInitiatorAsApprover string = "INPUT_EXCLUDE-WORKFLOW-INITIATOR-AS-APPROVER" envVarAdditionalApprovedWords string = "INPUT_ADDITIONAL-APPROVED-WORDS" envVarAdditionalDeniedWords string = "INPUT_ADDITIONAL-DENIED-WORDS" diff --git a/main.go b/main.go index e439961..e18a395 100644 --- a/main.go +++ b/main.go @@ -199,7 +199,17 @@ func main() { } issueTitle := os.Getenv(envVarIssueTitle) - issueBody := os.Getenv(envVarIssueBody) + var issueBody string + if os.Getenv(envVarIssueBodyFilePath) != "" { + fileContents, err := os.ReadFile(os.Getenv(envVarIssueBodyFilePath)) + if err != nil { + fmt.Printf("error reading issue body file: %v\n", err) + os.Exit(1) + } + issueBody = string(fileContents) + } else { + issueBody = os.Getenv(envVarIssueBody) + } minimumApprovalsRaw := os.Getenv(envVarMinimumApprovals) minimumApprovals := 0 if minimumApprovalsRaw != "" { From e2f9040b95adbec8aa77ea7ba7f583ef29e9a77d Mon Sep 17 00:00:00 2001 From: Sunny <55059942+snskArora@users.noreply.github.com> Date: Sat, 14 Jun 2025 02:06:37 +0530 Subject: [PATCH 44/60] Updates for release 1.11.0 (#187) * Updates for release 1.11.0 * [skip ci] Update README.md * [skip ci] Update README.md --- Makefile | 2 +- README.md | 17 +++++++++++++++-- action.yaml | 2 +- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 4a5a791..420c847 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ build: docker build -t $(IMAGE_REPO):$(VERSION) . .PHONY: build_push -push: +build_push: @if [ -z "$(VERSION)" ]; then \ echo "VERSION is required"; \ exit 1; \ diff --git a/README.md b/README.md index b0ad57a..3aa07ac 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ steps: minimum-approvals: 1 issue-title: "Deploying v1.3.5 to prod from staging" issue-body: "Please approve or deny the deployment of version v1.3.5." + issue-body-file-path: relative/file_path/wrt/repo/root exclude-workflow-initiator-as-approver: false fail-on-denial: true additional-approved-words: '' @@ -51,13 +52,25 @@ steps: * `approvers` is a comma-delimited list of all required approvers. An approver can either be a user or an org team. (*Note: Required approvers must have the ability to be set as approvers in the repository. If you add an approver that doesn't have this permission then you would receive an HTTP/402 Validation Failed error when running this action*) * `minimum-approvals` is an integer that sets the minimum number of approvals required to progress the workflow. Defaults to ALL approvers. -* `issue-title` is a string that will be appended to the title of the issue. -* `issue-body` is a string that will be prepended to the body of the issue. +* `issue-title` is a string that will be used as the title of the approval-issue. +* `issue-body` is a string that will be added as comments on the approval-issue. +* `issue-body-file-path` is a string which is the file path, this file's content will be added as comments on the approval-issue. If both issue-body and issue-body-file-path are given then the file contents are considered for issue comments. * `exclude-workflow-initiator-as-approver` is a boolean that indicates if the workflow initiator (determined by the `GITHUB_ACTOR` environment variable) should be filtered from the final list of approvers. This is optional and defaults to `false`. Set this to `true` to prevent users in the `approvers` list from being able to self-approve workflows. * `fail-on-denial` is a boolean that indicates if the workflow should fail if any approver denies the approval. This is optional and defaults to `true`. Set this to `false` to allow the workflow to continue if any approver denies the approval. * `additional-approved-words` is a comma separated list of strings to expand the dictionary of words that indicate approval. This is optional and defaults to an empty string. * `additional-denied-words` is a comma separated list of strings to expand the dictionary of words that indicate denial. This is optional and defaults to an empty string. +> [!Note] +> 1. If You are using issue-body-file-path then please make sure the file is reachable, for example, idf the file is in your repo then please checkout to your repo in the same job as the approval issue. +> 2. When using issue-body, the content string is passed as an arguent which is limited by github at 10kb. For content >= 10kb, use files for passing the issue body. + +> [!CAUTION] +> When using file please make sure that the file size remains under 125 KB (A safe limit, to stay under the threshold), If the file size is huge then the file content will be broken into a lot chunks representing an issue comment each, With theese many api requests the API rate limit is exceeded and the actions will be temporarily blocked resulting in an error message like: `403 You have exceeded a secondary rate limit and have been temporarily blocked from content creation. Please retry your request again later.` +> 5 MB is a crude estimate as secondary rate limits apply to a user so your user (usually the bot using app token for authentication) will not be able to do anything for some time. Primary limit might still reset quickly but secondary limits will need some cool-off time. + + +The file method works unless the file itself is very big that after breaking it into chunks of 65k characters, it exceeds the API limit + ### Outputs * `approval-status` is a string that indicates the final status of the approval. This will be either `approved` or `denied`. diff --git a/action.yaml b/action.yaml index 7918c1b..a8b661b 100644 --- a/action.yaml +++ b/action.yaml @@ -53,4 +53,4 @@ outputs: description: The status of the approval ("approved" or "denied") runs: using: docker - image: docker://ghcr.io/trstringer/manual-approval:1.10.0 + image: docker://ghcr.io/trstringer/manual-approval:1.11.0 From 65197b14118ec3026025207d608c6f9fc112948b Mon Sep 17 00:00:00 2001 From: Sunny <55059942+snskArora@users.noreply.github.com> Date: Sat, 14 Jun 2025 02:17:44 +0530 Subject: [PATCH 45/60] Issue Content handling -> README update (#188) * Updates for release 1.11.0 * [skip ci] Update README.md * [skip ci] Update README.md * Update README.md --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 3aa07ac..86508e1 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,12 @@ steps: GitHub has a rich library of emojis, and these all work in additional approved words or denied words. Some values GitHub will store in their text version - i.e. `:shipit:`. Other emojis, GitHub will store in their unicode emoji form, like ✅. For a seamless experience, it is recommended that you add the custom words to a GitHub comment, and then copy it back out of the comment into your actions configuration yaml. +# v1.9.x → v1.10.0 +🚨 Update: Approval Issue Content Handling 🚨 +Starting from v1.10.0, the behaviour for issue contents has changed: +- The issue-body and issue-body-file-path are now added as comments on the issue instead of being set as the issue’s main description/body. +- The issue title is now exactly what is provided as input, instead of being appended to or wrapped in a predefined string. + ## Org team approver If you want to have `approvers` set to an org team, then you need to take a different approach. The default [GitHub Actions automatic token](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token) does not have the necessary permissions to list out team members. If you would like to use this then you need to generate a token from a GitHub App with the correct set of permissions. From 809247d487a2518d55f6e931da23d98bd51d28ee Mon Sep 17 00:00:00 2001 From: Sunny <55059942+snskArora@users.noreply.github.com> Date: Sat, 14 Jun 2025 02:23:59 +0530 Subject: [PATCH 46/60] Update README.md (#189) --- README.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 86508e1..d1303d8 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,12 @@ In all cases, `manual-approval` will close the initial GitHub issue. - Windows/amd64 — 64-bit Windows systems are currently not supported. - Non-Linux runners of any architecture. +# v1.9.x → v1.10.0 +🚨 Update: Approval Issue Content Handling 🚨 +Starting from v1.10.0, the behaviour for issue contents has changed: +- The issue-body and issue-body-file-path are now added as comments on the issue instead of being set as the issue’s main description/body. +- The issue title is now exactly what is provided as input, instead of being appended to or wrapped in a predefined string. + ## Usage ```yaml @@ -65,7 +71,8 @@ steps: > 2. When using issue-body, the content string is passed as an arguent which is limited by github at 10kb. For content >= 10kb, use files for passing the issue body. > [!CAUTION] -> When using file please make sure that the file size remains under 125 KB (A safe limit, to stay under the threshold), If the file size is huge then the file content will be broken into a lot chunks representing an issue comment each, With theese many api requests the API rate limit is exceeded and the actions will be temporarily blocked resulting in an error message like: `403 You have exceeded a secondary rate limit and have been temporarily blocked from content creation. Please retry your request again later.` +> When using file please make sure that the file size remains under 125 KB (A safe limit, to stay under the threshold), If the file size is huge then the file content will be broken into a lot chunks representing an issue comment each, With theese many api requests the API rate limit is exceeded and the actions will be temporarily blocked resulting in an error message like: `403 You have exceeded a secondary rate limit and have been temporarily blocked from content creation. Please retry your request again later.` +> > 5 MB is a crude estimate as secondary rate limits apply to a user so your user (usually the bot using app token for authentication) will not be able to do anything for some time. Primary limit might still reset quickly but secondary limits will need some cool-off time. @@ -99,12 +106,6 @@ steps: GitHub has a rich library of emojis, and these all work in additional approved words or denied words. Some values GitHub will store in their text version - i.e. `:shipit:`. Other emojis, GitHub will store in their unicode emoji form, like ✅. For a seamless experience, it is recommended that you add the custom words to a GitHub comment, and then copy it back out of the comment into your actions configuration yaml. -# v1.9.x → v1.10.0 -🚨 Update: Approval Issue Content Handling 🚨 -Starting from v1.10.0, the behaviour for issue contents has changed: -- The issue-body and issue-body-file-path are now added as comments on the issue instead of being set as the issue’s main description/body. -- The issue title is now exactly what is provided as input, instead of being appended to or wrapped in a predefined string. - ## Org team approver If you want to have `approvers` set to an org team, then you need to take a different approach. The default [GitHub Actions automatic token](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token) does not have the necessary permissions to list out team members. If you would like to use this then you need to generate a token from a GitHub App with the correct set of permissions. From 7d6b9585eac7e14c6f2d2b24669044a65ca1c719 Mon Sep 17 00:00:00 2001 From: Sunny <55059942+snskArora@users.noreply.github.com> Date: Sat, 14 Jun 2025 04:40:46 +0530 Subject: [PATCH 47/60] Patch/readme (#191) * Update README.md * Update README.md * Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d1303d8..9c86785 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ For a seamless experience, it is recommended that you add the custom words to a ## Org team approver -If you want to have `approvers` set to an org team, then you need to take a different approach. The default [GitHub Actions automatic token](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token) does not have the necessary permissions to list out team members. If you would like to use this then you need to generate a token from a GitHub App with the correct set of permissions. +If you want to have `approvers` set to an org team, then you need to take a different approach. The default [GitHub Actions automatic token](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token) does not have the necessary permissions to list out team members. If you would like to use this then you need to generate a token from a GitHub App with the correct set of permissions. Apart from this the GH app will also need the Issue: Read & Write role. Create a GitHub App with **read-only access to organization members**. Once the app is created, add a repo secret with the app ID. In the GitHub App settings, generate a private key and add that as a secret in the repo as well. You can get the app token by using the [`tibdex/github-app-token`](https://github.com/tibdex/github-app-token) GitHub Action: @@ -182,6 +182,7 @@ For more information on permissions, please look at the [GitHub documentation](h * Expirations (also mentioned elsewhere in this document): * A job (including a paused job) will be failed [after 6 hours, and a workflow will be failed after 35 days](https://docs.github.com/en/actions/learn-github-actions/usage-limits-billing-and-administration#usage-limits). * GitHub App tokens expire after 1 hour which implies duration for the approval cannot exceed 60 minutes or the job will fail due to bad credentials. See [docs](https://docs.github.com/en/rest/apps/apps#create-an-installation-access-token-for-an-app) +* Adding a GH team instead of a user won't circumvent the issue of having a maximum of 10 assignees, github doesn't allow assigning an issue to a team but only to a user. ## Development From 157e108e95778215585a0f6fb3f51c01a2ad5e3d Mon Sep 17 00:00:00 2001 From: Ben Friebe Date: Sat, 2 Aug 2025 02:28:40 +1000 Subject: [PATCH 48/60] Fix grammar, spelling, and punctuation in the README (#193) --- README.md | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 9c86785..93162e4 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Pause a GitHub Actions workflow and require manual approval from one or more approvers before continuing. -This is a very common feature for a deployment or release pipeline, and while [this functionality is available from GitHub](https://docs.github.com/en/actions/managing-workflow-runs/reviewing-deployments), it requires the use of environments and if you want to use this for private repositories, then you need GitHub Enterprise. This action provides manual approval without the use of environments, and is freely available to use on private repositories. +This is a very common feature for a deployment or release pipeline, and while [this functionality is available from GitHub](https://docs.github.com/en/actions/managing-workflow-runs/reviewing-deployments), it requires the use of environments, and if you want to use this for private repositories, then you need GitHub Enterprise. This action provides manual approval without the use of environments and is freely available to use on private repositories. *Note: This approval duration is subject to the broader 35 day timeout for a workflow, as well as usage costs. So keep that in mind when figuring out how quickly an approver must respond. See [limitations](#limitations) for more information.* @@ -60,23 +60,23 @@ steps: * `minimum-approvals` is an integer that sets the minimum number of approvals required to progress the workflow. Defaults to ALL approvers. * `issue-title` is a string that will be used as the title of the approval-issue. * `issue-body` is a string that will be added as comments on the approval-issue. -* `issue-body-file-path` is a string which is the file path, this file's content will be added as comments on the approval-issue. If both issue-body and issue-body-file-path are given then the file contents are considered for issue comments. +* `issue-body-file-path` is a string that is the file path, this file's content will be added as comments on the approval-issue. If both issue-body and issue-body-file-path are given, then the file contents are considered for issue comments. * `exclude-workflow-initiator-as-approver` is a boolean that indicates if the workflow initiator (determined by the `GITHUB_ACTOR` environment variable) should be filtered from the final list of approvers. This is optional and defaults to `false`. Set this to `true` to prevent users in the `approvers` list from being able to self-approve workflows. * `fail-on-denial` is a boolean that indicates if the workflow should fail if any approver denies the approval. This is optional and defaults to `true`. Set this to `false` to allow the workflow to continue if any approver denies the approval. * `additional-approved-words` is a comma separated list of strings to expand the dictionary of words that indicate approval. This is optional and defaults to an empty string. * `additional-denied-words` is a comma separated list of strings to expand the dictionary of words that indicate denial. This is optional and defaults to an empty string. > [!Note] -> 1. If You are using issue-body-file-path then please make sure the file is reachable, for example, idf the file is in your repo then please checkout to your repo in the same job as the approval issue. +> 1. If You are using issue-body-file-path then please make sure the file is reachable; for example, if the file is in your repo, then please checkout to your repo in the same job as the approval issue. > 2. When using issue-body, the content string is passed as an arguent which is limited by github at 10kb. For content >= 10kb, use files for passing the issue body. > [!CAUTION] -> When using file please make sure that the file size remains under 125 KB (A safe limit, to stay under the threshold), If the file size is huge then the file content will be broken into a lot chunks representing an issue comment each, With theese many api requests the API rate limit is exceeded and the actions will be temporarily blocked resulting in an error message like: `403 You have exceeded a secondary rate limit and have been temporarily blocked from content creation. Please retry your request again later.` +> When using a file please make sure that the file size remains under 125 KB (a safe limit, to stay under the threshold). If the file size is huge, then the file content will be broken into multiple chunks, each representing an issue comment. With this many API requests the API rate limit is exceeded, and the actions will be temporarily blocked, resulting in an error message like: `403 You have exceeded a secondary rate limit and have been temporarily blocked from content creation. Please retry your request again later.` > -> 5 MB is a crude estimate as secondary rate limits apply to a user so your user (usually the bot using app token for authentication) will not be able to do anything for some time. Primary limit might still reset quickly but secondary limits will need some cool-off time. +> 5 MB is a crude estimate, as secondary rate limits apply to a user, so your user (usually the bot using an app token for authentication) will not be able to do anything for some time. Primary limits might still reset quickly, but secondary limits will need some cool-off time. -The file method works unless the file itself is very big that after breaking it into chunks of 65k characters, it exceeds the API limit +The file method works unless the file itself is so big that after breaking it into chunks of 65k characters, it exceeds the API limit. ### Outputs @@ -99,20 +99,20 @@ steps: target-repository: repository-name target-repository-owner: owner-id ``` -- if either of `target-repository` or `target-repository-owner` is missing or is an empty string then the issue will be created in the same repository where this step is used. +- If either of `target-repository` or `target-repository-owner` is missing or is an empty string, then the issue will be created in the same repository where this step is used. ### Using Custom Words -GitHub has a rich library of emojis, and these all work in additional approved words or denied words. Some values GitHub will store in their text version - i.e. `:shipit:`. Other emojis, GitHub will store in their unicode emoji form, like ✅. -For a seamless experience, it is recommended that you add the custom words to a GitHub comment, and then copy it back out of the comment into your actions configuration yaml. +GitHub has a rich library of emojis, and these all work in additional approved words or denied words. Some values GitHub will store in their text version - i.e. `:shipit:`. Other emojis, GitHub will store in their Unicode emoji form, like ✅. +For a seamless experience, it is recommended that you add the custom words to a GitHub comment, and then copy it back out of the comment into your actions configuration YAML. ## Org team approver -If you want to have `approvers` set to an org team, then you need to take a different approach. The default [GitHub Actions automatic token](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token) does not have the necessary permissions to list out team members. If you would like to use this then you need to generate a token from a GitHub App with the correct set of permissions. Apart from this the GH app will also need the Issue: Read & Write role. +If you want to have `approvers` set to an org team, then you need to take a different approach. The default [GitHub Actions automatic token](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token) does not have the necessary permissions to list out team members. If you would like to use this, then you need to generate a token from a GitHub App with the correct set of permissions. Apart from this, the GH app will also need the Issue: Read & Write role. Create a GitHub App with **read-only access to organization members**. Once the app is created, add a repo secret with the app ID. In the GitHub App settings, generate a private key and add that as a secret in the repo as well. You can get the app token by using the [`tibdex/github-app-token`](https://github.com/tibdex/github-app-token) GitHub Action: -*Note: The GitHub App tokens expire after 1 hour which implies duration for the approval cannot exceed 60 minutes or the job will fail due to bad credentials. See [docs](https://docs.github.com/en/rest/apps/apps#create-an-installation-access-token-for-an-app).* +*Note: The GitHub App tokens expire after 1 hour, which implies the duration for the approval cannot exceed 60 minutes, or the job will fail due to bad credentials. See [docs](https://docs.github.com/en/rest/apps/apps#create-an-installation-access-token-for-an-app).* ```yaml jobs: @@ -144,7 +144,7 @@ If you'd like to force a timeout of your workflow pause, you can specify `timeou > > If you are currently using `timeout-minutes` as a `manual-approval` input, you may see a warning, but this will not break your action. -For instance, if you want your manual approval step to timeout after an hour you could do the following: +For instance, if you want your manual approval step to timeout after an hour, you could do the following: ```yaml jobs: @@ -181,14 +181,14 @@ For more information on permissions, please look at the [GitHub documentation](h * A paused job is still running compute/instance/virtual machine and will continue to incur costs. * Expirations (also mentioned elsewhere in this document): * A job (including a paused job) will be failed [after 6 hours, and a workflow will be failed after 35 days](https://docs.github.com/en/actions/learn-github-actions/usage-limits-billing-and-administration#usage-limits). - * GitHub App tokens expire after 1 hour which implies duration for the approval cannot exceed 60 minutes or the job will fail due to bad credentials. See [docs](https://docs.github.com/en/rest/apps/apps#create-an-installation-access-token-for-an-app) -* Adding a GH team instead of a user won't circumvent the issue of having a maximum of 10 assignees, github doesn't allow assigning an issue to a team but only to a user. + * GitHub App tokens expire after 1 hour, which implies the duration for the approval cannot exceed 60 minutes, or the job will fail due to bad credentials. See [docs](https://docs.github.com/en/rest/apps/apps#create-an-installation-access-token-for-an-app). +* Adding a GH team instead of a user won't circumvent the issue of having a maximum of 10 assignees, GitHub doesn't allow assigning an issue to a team, only to a user. ## Development ### Running test code -To test out your code in an action, you need to build the image and push it to a different container registry repository. For instance, if I want to test some code I won't build the image with the main image repository. Prior to this, comment out the label binding the image to a repo: +To test out your code in an action, you need to build the image and push it to a different container registry repository. For instance, if I want to test some code, I won't build the image with the main image repository. Prior to this, comment out the label binding the image to a repo: ```dockerfile # LABEL org.opencontainers.image.source https://github.com/trstringer/manual-approval @@ -208,13 +208,13 @@ Push the image to your container registry: VERSION=1.7.1-rc.1 make IMAGE_REPO=ghcr.io/trstringer/manual-approval-test push ``` -To test out the image you will need to modify `action.yaml` so that it points to your new image that you're testing: +To test out the image, you will need to modify `action.yaml` so that it points to your new image that you're testing: ```yaml image: docker://ghcr.io/trstringer/manual-approval-test:1.7.0-rc.1 ``` -Then to test out the image, run a workflow specifying your dev branch: +Then, to test out the image, run a workflow specifying your dev branch: ```yaml - name: Wait for approval @@ -230,10 +230,10 @@ For `uses`, this should point to your repo and dev branch. ### Create a release -1. Build and push the new image: `$ VERSION=1.7.0 make build_push` -2. Create a release branch and modify `action.yaml` to point to the new image -3. Open and merge a PR to add these changes to the default branch -4. Make sure to fetch the new changes into your local repo: `$ git checkout main && git fetch origin && git merge origin main` -5. Delete the `v1` tag locally and remotely: `$ git tag -d v1 && git push --delete origin v1` -6. Create and push new tags: `$ git tag v1.7.0 && git tag v1 && git push origin --tags` -7. Create the GitHub project release +1. Build and push the new image: `$ VERSION=1.7.0 make build_push`. +2. Create a release branch and modify `action.yaml` to point to the new image. +3. Open and merge a PR to add these changes to the default branch. +4. Make sure to fetch the new changes into your local repo: `$ git checkout main && git fetch origin && git merge origin main`. +5. Delete the `v1` tag locally and remotely: `$ git tag -d v1 && git push --delete origin v1`. +6. Create and push new tags: `$ git tag v1.7.0 && git tag v1 && git push origin --tags`. +7. Create the GitHub project release. From 685e33834916cfeef0789ed7795520b71441fe4e Mon Sep 17 00:00:00 2001 From: kjluedke Date: Mon, 1 Sep 2025 21:33:01 -0700 Subject: [PATCH 49/60] Docs: Update readme for org team approvers (#194) * Docs: Update readme for org team approvers * Update README.md --------- Co-authored-by: Sunny <55059942+snskArora@users.noreply.github.com> --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 93162e4..9e5497c 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ For a seamless experience, it is recommended that you add the custom words to a If you want to have `approvers` set to an org team, then you need to take a different approach. The default [GitHub Actions automatic token](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token) does not have the necessary permissions to list out team members. If you would like to use this, then you need to generate a token from a GitHub App with the correct set of permissions. Apart from this, the GH app will also need the Issue: Read & Write role. -Create a GitHub App with **read-only access to organization members**. Once the app is created, add a repo secret with the app ID. In the GitHub App settings, generate a private key and add that as a secret in the repo as well. You can get the app token by using the [`tibdex/github-app-token`](https://github.com/tibdex/github-app-token) GitHub Action: +Create an Organization GitHub App with **read-only access to organization members**. Once the app is created, add a repo secret with the app ID. In the GitHub App settings, generate a private key and add that as a secret in the repo as well. You can get the app token by using the [`actions/create-github-app-token`](https://github.com/actions/create-github-app-token) GitHub Action: *Note: The GitHub App tokens expire after 1 hour, which implies the duration for the approval cannot exceed 60 minutes, or the job will fail due to bad credentials. See [docs](https://docs.github.com/en/rest/apps/apps#create-an-installation-access-token-for-an-app).* @@ -121,10 +121,10 @@ jobs: steps: - name: Generate token id: generate_token - uses: tibdex/github-app-token@v1 + uses: actions/create-github-app-token@v2 with: - app_id: ${{ secrets.APP_ID }} - private_key: ${{ secrets.APP_PRIVATE_KEY }} + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} - name: Wait for approval uses: trstringer/manual-approval@v1 with: From d05ca6dad114979e92f9c0a81728a659b210ca20 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 5 Oct 2025 03:27:09 +0530 Subject: [PATCH 50/60] Bump golang.org/x/oauth2 from 0.30.0 to 0.31.0 (#195) Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.30.0 to 0.31.0. - [Commits](https://github.com/golang/oauth2/compare/v0.30.0...v0.31.0) --- updated-dependencies: - dependency-name: golang.org/x/oauth2 dependency-version: 0.31.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 9c0d645..6dfcfd7 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,10 @@ module github.com/trstringer/manual-approval -go 1.24 +go 1.24.0 require ( github.com/google/go-github/v43 v43.0.0 - golang.org/x/oauth2 v0.30.0 + golang.org/x/oauth2 v0.31.0 ) require ( From 32d182eec237539fa8bd4db6d16bcd2ca9bb5dec Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Oct 2025 17:33:34 +0530 Subject: [PATCH 51/60] Bump golang.org/x/oauth2 from 0.31.0 to 0.32.0 (#198) Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.31.0 to 0.32.0. - [Commits](https://github.com/golang/oauth2/compare/v0.31.0...v0.32.0) --- updated-dependencies: - dependency-name: golang.org/x/oauth2 dependency-version: 0.32.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 6dfcfd7..cce435f 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.24.0 require ( github.com/google/go-github/v43 v43.0.0 - golang.org/x/oauth2 v0.31.0 + golang.org/x/oauth2 v0.32.0 ) require ( From 1afc677e1fc12a5bcfd360e8246d190bf2bbebc4 Mon Sep 17 00:00:00 2001 From: Brian Sneddon Date: Sun, 16 Nov 2025 07:10:04 -0900 Subject: [PATCH 52/60] Add configurable polling interval for GitHub API calls (#197) - Add polling-interval-seconds input parameter to action.yaml (default: 10) - Update polling logic to use configurable interval - Add input validation to ensure positive values - Update README with usage documentation This allows users to customize how frequently the action polls GitHub API for approval status, enabling them to reduce API calls or speed up response times based on their needs. --- README.md | 2 ++ action.yaml | 4 ++++ constants.go | 3 ++- main.go | 19 +++++++++++++++++-- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9e5497c..b65cf17 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ steps: fail-on-denial: true additional-approved-words: '' additional-denied-words: '' + polling-interval-seconds: 10 ``` * `approvers` is a comma-delimited list of all required approvers. An approver can either be a user or an org team. (*Note: Required approvers must have the ability to be set as approvers in the repository. If you add an approver that doesn't have this permission then you would receive an HTTP/402 Validation Failed error when running this action*) @@ -65,6 +66,7 @@ steps: * `fail-on-denial` is a boolean that indicates if the workflow should fail if any approver denies the approval. This is optional and defaults to `true`. Set this to `false` to allow the workflow to continue if any approver denies the approval. * `additional-approved-words` is a comma separated list of strings to expand the dictionary of words that indicate approval. This is optional and defaults to an empty string. * `additional-denied-words` is a comma separated list of strings to expand the dictionary of words that indicate denial. This is optional and defaults to an empty string. +* `polling-interval-seconds` is an integer that sets the number of seconds to wait between polling the GitHub API for approval status. This is optional and defaults to `10` seconds. Increase this value if you want to reduce API calls, or decrease it for faster response times. > [!Note] > 1. If You are using issue-body-file-path then please make sure the file is reachable; for example, if the file is in your repo, then please checkout to your repo in the same job as the approval issue. diff --git a/action.yaml b/action.yaml index a8b661b..cdbf793 100644 --- a/action.yaml +++ b/action.yaml @@ -44,6 +44,10 @@ inputs: description: Whether or not to fail the workflow if the approval is denied required: false default: 'true' + polling-interval-seconds: + description: Number of seconds to wait between polling GitHub API for approval status + required: false + default: '10' outputs: issue-number: description: The number of the issue created diff --git a/constants.go b/constants.go index 88867e3..69c509e 100644 --- a/constants.go +++ b/constants.go @@ -7,7 +7,7 @@ import ( ) const ( - pollingInterval time.Duration = 10 * time.Second + defaultPollingInterval time.Duration = 10 * time.Second envVarRepoFullName string = "GITHUB_REPOSITORY" envVarRunID string = "GITHUB_RUN_ID" @@ -25,6 +25,7 @@ const ( envVarFailOnDenial string = "INPUT_FAIL-ON-DENIAL" envVarTargetRepoOwner string = "INPUT_TARGET-REPOSITORY-OWNER" envVarTargetRepo string = "INPUT_TARGET-REPOSITORY" + envVarPollingIntervalSeconds string = "INPUT_POLLING-INTERVAL-SECONDS" ) var ( diff --git a/main.go b/main.go index e18a395..89305fd 100644 --- a/main.go +++ b/main.go @@ -32,7 +32,7 @@ func handleInterrupt(ctx context.Context, client *github.Client, apprv *approval } } -func newCommentLoopChannel(ctx context.Context, apprv *approvalEnvironment, client *github.Client) chan int { +func newCommentLoopChannel(ctx context.Context, apprv *approvalEnvironment, client *github.Client, pollingInterval time.Duration) chan int { channel := make(chan int) go func() { for { @@ -198,6 +198,21 @@ func main() { } } + pollingInterval := defaultPollingInterval + pollingIntervalSecondsRaw := os.Getenv(envVarPollingIntervalSeconds) + if pollingIntervalSecondsRaw != "" { + pollingIntervalSeconds, err := strconv.Atoi(pollingIntervalSecondsRaw) + if err != nil { + fmt.Printf("error parsing polling interval: %v\n", err) + os.Exit(1) + } + if pollingIntervalSeconds <= 0 { + fmt.Printf("error: polling interval must be greater than 0\n") + os.Exit(1) + } + pollingInterval = time.Duration(pollingIntervalSeconds) * time.Second + } + issueTitle := os.Getenv(envVarIssueTitle) var issueBody string if os.Getenv(envVarIssueBodyFilePath) != "" { @@ -245,7 +260,7 @@ func main() { killSignalChannel := make(chan os.Signal, 1) signal.Notify(killSignalChannel, os.Interrupt) - commentLoopChannel := newCommentLoopChannel(ctx, apprv, client) + commentLoopChannel := newCommentLoopChannel(ctx, apprv, client, pollingInterval) select { case exitCode := <-commentLoopChannel: From ace61e3b4868e19254cd49613b090e70e3752cc8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 16 Nov 2025 21:40:47 +0530 Subject: [PATCH 53/60] Bump golang.org/x/oauth2 from 0.32.0 to 0.33.0 (#200) Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.32.0 to 0.33.0. - [Commits](https://github.com/golang/oauth2/compare/v0.32.0...v0.33.0) --- updated-dependencies: - dependency-name: golang.org/x/oauth2 dependency-version: 0.33.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index cce435f..ab2814d 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.24.0 require ( github.com/google/go-github/v43 v43.0.0 - golang.org/x/oauth2 v0.32.0 + golang.org/x/oauth2 v0.33.0 ) require ( From ab01c89357ed5d3414f344347d4b3168d81812ee Mon Sep 17 00:00:00 2001 From: Sunny <55059942+snskArora@users.noreply.github.com> Date: Mon, 17 Nov 2025 00:32:07 +0530 Subject: [PATCH 54/60] Upgrading the docker image version (#201) --- action.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/action.yaml b/action.yaml index cdbf793..fdd28ae 100644 --- a/action.yaml +++ b/action.yaml @@ -57,4 +57,4 @@ outputs: description: The status of the approval ("approved" or "denied") runs: using: docker - image: docker://ghcr.io/trstringer/manual-approval:1.11.0 + image: docker://ghcr.io/trstringer/manual-approval:1.12.0 From d8289abf875c2b6259d879257d5754b98f5229c8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 28 Nov 2025 23:25:07 +0530 Subject: [PATCH 55/60] Bump golang.org/x/crypto in the go_modules group across 1 directory (#202) Bumps the go_modules group with 1 update in the / directory: [golang.org/x/crypto](https://github.com/golang/crypto). Updates `golang.org/x/crypto` from 0.35.0 to 0.45.0 - [Commits](https://github.com/golang/crypto/compare/v0.35.0...v0.45.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.45.0 dependency-type: indirect dependency-group: go_modules ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index ab2814d..2f1fd03 100644 --- a/go.mod +++ b/go.mod @@ -9,5 +9,5 @@ require ( require ( github.com/google/go-querystring v1.1.0 // indirect - golang.org/x/crypto v0.35.0 // indirect + golang.org/x/crypto v0.45.0 // indirect ) From 9dd64bee80124e11c1c406f94e1a9c57a707a013 Mon Sep 17 00:00:00 2001 From: Chris Pressland Date: Mon, 13 Apr 2026 15:03:44 +0100 Subject: [PATCH 56/60] chore: add support for non-github urls such as forgejo (#210) --- approval.go | 57 ++++++++++++++++++++++++++++++--------------- main.go | 67 ++++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 91 insertions(+), 33 deletions(-) diff --git a/approval.go b/approval.go index 6391ad2..c53aeca 100644 --- a/approval.go +++ b/approval.go @@ -51,11 +51,11 @@ func newApprovalEnvironment(client *github.Client, repoFullName, repoOwner strin } func (a approvalEnvironment) runURL() string { - baseUrl := a.client.BaseURL.String() - if strings.Contains(baseUrl, "github.com") { - baseUrl = "https://github.com/" + serverUrl := os.Getenv("GITHUB_SERVER_URL") + if serverUrl == "" { + serverUrl = "https://github.com" } - return fmt.Sprintf("%s%s/actions/runs/%d", baseUrl, a.repoFullName, a.runID) + return fmt.Sprintf("%s/%s/actions/runs/%d", strings.TrimRight(serverUrl, "/"), a.repoFullName, a.runID) } func (a *approvalEnvironment) createApprovalIssue(ctx context.Context) error { @@ -96,25 +96,44 @@ func (a *approvalEnvironment) createApprovalIssue(ctx context.Context) error { a.issueApprovers, issueBody, ) - a.approvalIssue, _, err = a.client.Issues.Create(ctx, a.targetRepoOwner, a.targetRepoName, &github.IssueRequest{ - Title: &issueTitle, - Body: &issueBody, - Assignees: &a.issueApprovers, - }) + // Use NewRequest+Do with a minimal response struct rather than client.Issues.Create. + // Forgejo's issue response includes "repository.owner" as a plain string, but + // go-github's Repository.Owner is a *User struct, causing an unmarshal error. + // Our minimal struct omits Repository entirely, so the field is ignored. + type createIssueResponse struct { + Number int `json:"number"` + HTMLURL string `json:"html_url"` + } + req, err := a.client.NewRequest("POST", + fmt.Sprintf("repos/%s/%s/issues", a.targetRepoOwner, a.targetRepoName), + &github.IssueRequest{ + Title: &issueTitle, + Body: &issueBody, + Assignees: &a.issueApprovers, + }, + ) if err != nil { return err } - a.approvalIssueNumber = a.approvalIssue.GetNumber() + var created createIssueResponse + if _, err = a.client.Do(ctx, req, &created); err != nil { + return err + } + a.approvalIssueNumber = created.Number + a.approvalIssue = &github.Issue{ + Number: &created.Number, + HTMLURL: &created.HTMLURL, + } - bodyChunks := splitLongString(a.issueBody) - for _, chunk := range bodyChunks { - _, _, err = a.client.Issues.CreateComment(ctx, a.targetRepoOwner, a.targetRepoName, *a.approvalIssue.Number, &github.IssueComment{ - Body: &chunk, - }) - if err != nil { - return fmt.Errorf("failed to add comment chunk to issue: %w", err) - } - } + bodyChunks := splitLongString(a.issueBody) + for _, chunk := range bodyChunks { + _, _, err = a.client.Issues.CreateComment(ctx, a.targetRepoOwner, a.targetRepoName, created.Number, &github.IssueComment{ + Body: &chunk, + }) + if err != nil { + return fmt.Errorf("failed to add comment chunk to issue: %w", err) + } + } fmt.Printf("Issue created: %s\n", a.approvalIssue.GetHTMLURL()) return nil diff --git a/main.go b/main.go index 89305fd..666cfb6 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( "context" "fmt" + "net/url" "os" "os/signal" "strconv" @@ -13,6 +14,22 @@ import ( "golang.org/x/oauth2" ) +// patchIssueState closes or otherwise updates an issue's state without decoding +// the response body into a github.Issue. go-github's Issues.Edit decodes the +// response into github.Issue, which fails against Forgejo because its issue +// response embeds "repository.owner" as a plain string rather than a User object. +func patchIssueState(ctx context.Context, client *github.Client, owner, repo string, number int, state string) error { + req, err := client.NewRequest("PATCH", + fmt.Sprintf("repos/%s/%s/issues/%d", owner, repo, number), + &github.IssueRequest{State: &state}, + ) + if err != nil { + return err + } + _, err = client.Do(ctx, req, nil) + return err +} + func handleInterrupt(ctx context.Context, client *github.Client, apprv *approvalEnvironment) { newState := "closed" closeComment := "Workflow cancelled, closing issue." @@ -25,8 +42,7 @@ func handleInterrupt(ctx context.Context, client *github.Client, apprv *approval fmt.Printf("error commenting on issue: %v\n", err) return } - _, _, err = client.Issues.Edit(ctx, apprv.targetRepoOwner, apprv.targetRepoName, apprv.approvalIssueNumber, &github.IssueRequest{State: &newState}) - if err != nil { + if err = patchIssueState(ctx, client, apprv.targetRepoOwner, apprv.targetRepoName, apprv.approvalIssueNumber, newState); err != nil { fmt.Printf("error closing issue: %v\n", err) return } @@ -41,6 +57,7 @@ func newCommentLoopChannel(ctx context.Context, apprv *approvalEnvironment, clie fmt.Printf("error getting comments: %v\n", err) channel <- 1 close(channel) + return } approved, err := approvalFromComments(comments, apprv.issueApprovers, apprv.minimumApprovals) @@ -48,6 +65,7 @@ func newCommentLoopChannel(ctx context.Context, apprv *approvalEnvironment, clie fmt.Printf("error getting approval from comments: %v\n", err) channel <- 1 close(channel) + return } fmt.Printf("Workflow status: %s\n", approved) switch approved { @@ -61,16 +79,18 @@ func newCommentLoopChannel(ctx context.Context, apprv *approvalEnvironment, clie fmt.Printf("error commenting on issue: %v\n", err) channel <- 1 close(channel) + return } - _, _, err = client.Issues.Edit(ctx, apprv.targetRepoOwner, apprv.targetRepoName, apprv.approvalIssueNumber, &github.IssueRequest{State: &newState}) - if err != nil { + if err = patchIssueState(ctx, client, apprv.targetRepoOwner, apprv.targetRepoName, apprv.approvalIssueNumber, newState); err != nil { fmt.Printf("error closing issue: %v\n", err) channel <- 1 close(channel) + return } channel <- 0 fmt.Println("Workflow manual approval completed") close(channel) + return case approvalStatusDenied: newState := "closed" closeComment := "Request denied. Closing issue " @@ -88,15 +108,17 @@ func newCommentLoopChannel(ctx context.Context, apprv *approvalEnvironment, clie fmt.Printf("error commenting on issue: %v\n", err) channel <- 1 close(channel) + return } - _, _, err = client.Issues.Edit(ctx, apprv.targetRepoOwner, apprv.targetRepoName, apprv.approvalIssueNumber, &github.IssueRequest{State: &newState}) - if err != nil { + if err = patchIssueState(ctx, client, apprv.targetRepoOwner, apprv.targetRepoName, apprv.approvalIssueNumber, newState); err != nil { fmt.Printf("error closing issue: %v\n", err) channel <- 1 close(channel) + return } channel <- 1 close(channel) + return } time.Sleep(pollingInterval) @@ -115,13 +137,30 @@ func newGithubClient(ctx context.Context) (*github.Client, error) { serverUrl, serverUrlPresent := os.LookupEnv("GITHUB_SERVER_URL") apiUrl, apiUrlPresent := os.LookupEnv("GITHUB_API_URL") - if serverUrlPresent { - if !apiUrlPresent { - apiUrl = serverUrl - } - return github.NewEnterpriseClient(apiUrl, serverUrl, tc) + if !serverUrlPresent && !apiUrlPresent { + return github.NewClient(tc), nil } - return github.NewClient(tc), nil + + if !apiUrlPresent { + // Only GITHUB_SERVER_URL is set; assume GitHub Enterprise with the + // default /api/v3 path and let NewEnterpriseClient append it. + return github.NewEnterpriseClient(serverUrl, serverUrl, tc) + } + + // GITHUB_API_URL is set. github.NewEnterpriseClient appends "/api/v3/" to + // any URL whose path doesn't already end with it. This breaks Forgejo/Gitea + // instances whose API lives at "/api/v1/". Instead, set BaseURL directly so + // the URL is used as-is, which works for GitHub.com, GHES, and Forgejo alike. + if !strings.HasSuffix(apiUrl, "/") { + apiUrl += "/" + } + baseURL, err := url.Parse(apiUrl) + if err != nil { + return nil, fmt.Errorf("invalid GITHUB_API_URL %q: %w", apiUrl, err) + } + client := github.NewClient(tc) + client.BaseURL = baseURL + return client, nil } func validateInput() error { @@ -243,7 +282,7 @@ func main() { err = apprv.createApprovalIssue(ctx) if err != nil { - fmt.Printf("error creating issue: %v", err) + fmt.Printf("error creating issue: %v\n", err) os.Exit(1) } @@ -253,7 +292,7 @@ func main() { } _, err = apprv.SetActionOutputs(outputs) if err != nil { - fmt.Printf("error saving output: %v", err) + fmt.Printf("error saving output: %v\n", err) os.Exit(1) } From 3811d88913a9eb8afd9ca8fd9c272776a82315cb Mon Sep 17 00:00:00 2001 From: TakumiMukaiyama <79499429+TakumiMukaiyama@users.noreply.github.com> Date: Mon, 13 Apr 2026 23:03:57 +0900 Subject: [PATCH 57/60] chore: update base image from alpine:3.14 (EOL) to alpine:3.23 (#212) * chore: update base image from alpine:3.14 to alpine:3.21 Alpine 3.14 reached EOL in November 2024 and no longer receives security updates. Update to 3.21 (latest stable) to ensure continued security patch coverage. Closes #211 Co-Authored-By: Claude Sonnet 4.6 * chore: update base image from alpine:3.14 (EOL) to alpine:3.23 Alpine 3.14 reached EOL in November 2024. Update to 3.23 (latest stable, EOL: November 2027) to ensure continued security patch coverage. Closes #211 Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Claude Sonnet 4.6 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 4fa74dd..002be9d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ WORKDIR /var/app RUN go mod tidy RUN CGO_ENABLED=0 go build -o app . -FROM alpine:3.14 +FROM alpine:3.23 LABEL org.opencontainers.image.source=https://github.com/trstringer/manual-approval RUN apk update && apk add ca-certificates COPY --from=builder /var/app/app /var/app/app From 9e098fbb6e4bfbd0b0e3dc75114f29dc24c9820e Mon Sep 17 00:00:00 2001 From: jaime merino Date: Wed, 22 Apr 2026 15:28:23 +0200 Subject: [PATCH 58/60] - Fixed Approver Expansion: Repaired the broken expandGroupFromUser function in approvers.go. It now correctly utilizes the Forgejo SDK to search for teams within an organization and expand them into individual users, while respecting the option to exclude the workflow initiator. - Removed GitHub Dependencies: Excised the unused newGithubClient and its associated dependencies (google/go-github and oauth2) from main.go and go.mod. - Project Infrastructure Updates: - Updated Dockerfile to use golang:1.25 for consistency with go.mod. - Modified action.yaml to run from the local Dockerfile, ensuring that the Forgejo-specific logic is utilized. - Validation: Verified all changes through a successful build and by running the full test suite, all of which passed. --- .github/ISSUE_TEMPLATE/bug_report.md | 29 ---- .github/ISSUE_TEMPLATE/feature_request.md | 22 --- .github/dependabot.yml | 7 - .github/workflows/ci.yaml | 34 ----- .github/workflows/codeql.yml | 39 ----- Dockerfile | 2 +- README.md | 121 +++------------- action.yaml | 22 +-- approval.go | 164 +++++++--------------- approval_test.go | 102 +++++++------- approvers.go | 39 +++-- go.mod | 36 ++++- main.go | 92 +++++------- workflow_dispatcher.md | 57 ++++++++ 14 files changed, 277 insertions(+), 489 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md delete mode 100644 .github/dependabot.yml delete mode 100644 .github/workflows/ci.yaml delete mode 100644 .github/workflows/codeql.yml create mode 100644 workflow_dispatcher.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index d0c25c5..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: '' -assignees: '' - ---- - -- [ ] I have searched through the current issues and did not find any that were related. - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Additional context** -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 6089845..0000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: '' -labels: '' -assignees: '' - ---- - -- [ ] I have searched through the current feature requests and did not find any that were related. - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 21c0165..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,7 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "gomod" - directory: "/" - schedule: - interval: "weekly" - open-pull-requests-limit: 10 # default: 5 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml deleted file mode 100644 index c0d4e03..0000000 --- a/.github/workflows/ci.yaml +++ /dev/null @@ -1,34 +0,0 @@ -name: CI - -on: - workflow_dispatch: - push: - branches: - - main - paths-ignore: - - '**/*.md' - pull_request: - branches: - - main - paths-ignore: - - '**/*.md' - -jobs: - ci: - name: CI - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Refresh module hashsums - run: make tidy - - name: Build - run: make build - env: - VERSION: latest - - - name: Test - run: make test - - name: Lint - run: make lint diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index f5442a1..0000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: "Code Analysis" - -on: - push: - branches: - - "main" - pull_request: - -permissions: - contents: read - -jobs: - CodeQL-Build: - if: github.repository == 'trstringer/manual-approval' - permissions: - actions: read - contents: read - security-events: write - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup Golang - uses: actions/setup-go@v5 - with: - go-version-file: go.mod - - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: go - - - name: Autobuild - uses: github/codeql-action/autobuild@v3 - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 diff --git a/Dockerfile b/Dockerfile index 002be9d..f63013a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.24 AS builder +FROM golang:1.25 AS builder COPY . /var/app WORKDIR /var/app RUN go mod tidy diff --git a/README.md b/README.md index b65cf17..2249cbd 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ # Manual Workflow Approval -[![ci](https://github.com/trstringer/manual-approval/actions/workflows/ci.yaml/badge.svg)](https://github.com/trstringer/manual-approval/actions/workflows/ci.yaml) +Pause a StackitGit Actions workflow and require manual approval from one or more approvers before continuing. -Pause a GitHub Actions workflow and require manual approval from one or more approvers before continuing. +This is a very common feature for a deployment or release pipeline. -This is a very common feature for a deployment or release pipeline, and while [this functionality is available from GitHub](https://docs.github.com/en/actions/managing-workflow-runs/reviewing-deployments), it requires the use of environments, and if you want to use this for private repositories, then you need GitHub Enterprise. This action provides manual approval without the use of environments and is freely available to use on private repositories. +*Note: This approval duration is subject to the broader 4 hours timeout for a workflow, as well as usage costs. So keep that in mind when figuring out how quickly an approver must respond. See [limitations](#limitations) for more information.* -*Note: This approval duration is subject to the broader 35 day timeout for a workflow, as well as usage costs. So keep that in mind when figuring out how quickly an approver must respond. See [limitations](#limitations) for more information.* +*Note: A cheaper but less automatic solution using split jobs and workflow dispatcher can be seen in [here](workflow_dispatcher.md) The way this action works is the following: @@ -21,28 +21,13 @@ The way this action works is the following: These are case insensitive with optional punctuation either a period or an exclamation mark. -In all cases, `manual-approval` will close the initial GitHub issue. - -🖥️ Supported Runners, The action is compatible with the following runner types: -- Linux/amd64 — 64-bit Intel/AMD (x86_64) -- Linux/arm64 — 64-bit ARM (Apple M1) -- Linux/arm/v8 — 64-bit ARM - -🚫 Unsupported -- Windows/amd64 — 64-bit Windows systems are currently not supported. -- Non-Linux runners of any architecture. - -# v1.9.x → v1.10.0 -🚨 Update: Approval Issue Content Handling 🚨 -Starting from v1.10.0, the behaviour for issue contents has changed: -- The issue-body and issue-body-file-path are now added as comments on the issue instead of being set as the issue’s main description/body. -- The issue title is now exactly what is provided as input, instead of being appended to or wrapped in a predefined string. +In all cases, `manual-approval` will close the initial StackitGit issue. ## Usage ```yaml steps: - - uses: trstringer/manual-approval@v1 + - uses: https://stackit.git.onstackit.cloud/actions/manual-approval@v1 with: secret: ${{ github.TOKEN }} approvers: user1,user2,org-team1 @@ -66,7 +51,7 @@ steps: * `fail-on-denial` is a boolean that indicates if the workflow should fail if any approver denies the approval. This is optional and defaults to `true`. Set this to `false` to allow the workflow to continue if any approver denies the approval. * `additional-approved-words` is a comma separated list of strings to expand the dictionary of words that indicate approval. This is optional and defaults to an empty string. * `additional-denied-words` is a comma separated list of strings to expand the dictionary of words that indicate denial. This is optional and defaults to an empty string. -* `polling-interval-seconds` is an integer that sets the number of seconds to wait between polling the GitHub API for approval status. This is optional and defaults to `10` seconds. Increase this value if you want to reduce API calls, or decrease it for faster response times. +* `polling-interval-seconds` is an integer that sets the number of seconds to wait between polling the StackitGit API for approval status. This is optional and defaults to `10` seconds. Increase this value if you want to reduce API calls, or decrease it for faster response times. > [!Note] > 1. If You are using issue-body-file-path then please make sure the file is reachable; for example, if the file is in your repo, then please checkout to your repo in the same job as the approval issue. @@ -88,7 +73,7 @@ The file method works unless the file itself is so big that after breaking it in ```yaml steps: - - uses: trstringer/manual-approval@v1 + - uses: https://stackit.git.onstackit.cloud/actions/manual-approval@v1 with: secret: ${{ github.TOKEN }} approvers: user1,user2,org-team1 @@ -105,16 +90,16 @@ steps: ### Using Custom Words -GitHub has a rich library of emojis, and these all work in additional approved words or denied words. Some values GitHub will store in their text version - i.e. `:shipit:`. Other emojis, GitHub will store in their Unicode emoji form, like ✅. -For a seamless experience, it is recommended that you add the custom words to a GitHub comment, and then copy it back out of the comment into your actions configuration YAML. +StackitGit has a rich library of emojis, and these all work in additional approved words or denied words. Some values StackitGit will store in their text version - i.e. `:shipit:`. Other emojis, StackitGit will store in their Unicode emoji form, like ✅. +For a seamless experience, it is recommended that you add the custom words to a StackitGit comment, and then copy it back out of the comment into your actions configuration YAML. ## Org team approver -If you want to have `approvers` set to an org team, then you need to take a different approach. The default [GitHub Actions automatic token](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token) does not have the necessary permissions to list out team members. If you would like to use this, then you need to generate a token from a GitHub App with the correct set of permissions. Apart from this, the GH app will also need the Issue: Read & Write role. +If you want to have `approvers` set to an org team, then you need to take a different approach. The default [StackitGit Actions automatic token](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token) does not have the necessary permissions to list out team members. If you would like to use this, then you need to generate a token from a StackitGit App with the correct set of permissions. Apart from this, the GH app will also need the Issue: Read & Write role. -Create an Organization GitHub App with **read-only access to organization members**. Once the app is created, add a repo secret with the app ID. In the GitHub App settings, generate a private key and add that as a secret in the repo as well. You can get the app token by using the [`actions/create-github-app-token`](https://github.com/actions/create-github-app-token) GitHub Action: +Create an Organization StackitGit App with **read-only access to organization members**. Once the app is created, add a repo secret with the app ID. In the StackitGit App settings, generate a private key and add that as a secret in the repo as well. You can get the app token by using the [`actions/create-github-app-token`](https://github.com/actions/create-github-app-token) StackitGit Action: -*Note: The GitHub App tokens expire after 1 hour, which implies the duration for the approval cannot exceed 60 minutes, or the job will fail due to bad credentials. See [docs](https://docs.github.com/en/rest/apps/apps#create-an-installation-access-token-for-an-app).* +*Note: The StackitGit App tokens expire after 1 hour, which implies the duration for the approval cannot exceed 60 minutes, or the job will fail due to bad credentials. See [docs](https://docs.github.com/en/rest/apps/apps#create-an-installation-access-token-for-an-app).* ```yaml jobs: @@ -128,7 +113,7 @@ jobs: app-id: ${{ secrets.APP_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} - name: Wait for approval - uses: trstringer/manual-approval@v1 + uses: https://stackit.git.onstackit.cloud/actions/manual-approval@v1 with: secret: ${{ steps.generate_token.outputs.token }} approvers: myteam @@ -152,9 +137,9 @@ For instance, if you want your manual approval step to timeout after an hour, yo jobs: approval: steps: - - uses: trstringer/manual-approval@v1 + - uses: https://stackit.git.onstackit.cloud/actions/manual-approval@v1 timeout-minutes: 60 - ... + # ... ``` or ```yaml @@ -162,80 +147,14 @@ jobs: approval: timeout-minutes: 10 steps: - - uses: trstringer/manual-approval@v1 + - uses: https://stackit.git.onstackit.cloud/actions/manual-approval@v1 ``` -## Permissions - -For the action to create a new issue in your project, please ensure that the action has write permissions on issues. You may have to add the following to your workflow: - -```yaml -permissions: - issues: write -``` - -For more information on permissions, please look at the [GitHub documentation](https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs). - ## Limitations -* While the workflow is paused, it will still continue to consume a concurrent job allocation out of the [max concurrent jobs](https://docs.github.com/en/actions/learn-github-actions/usage-limits-billing-and-administration#usage-limits). +* While the workflow is paused, it will still continue to consume a concurrent job allocation out of the max concurrent job of 20. * A paused job is still running compute/instance/virtual machine and will continue to incur costs. * Expirations (also mentioned elsewhere in this document): - * A job (including a paused job) will be failed [after 6 hours, and a workflow will be failed after 35 days](https://docs.github.com/en/actions/learn-github-actions/usage-limits-billing-and-administration#usage-limits). - * GitHub App tokens expire after 1 hour, which implies the duration for the approval cannot exceed 60 minutes, or the job will fail due to bad credentials. See [docs](https://docs.github.com/en/rest/apps/apps#create-an-installation-access-token-for-an-app). -* Adding a GH team instead of a user won't circumvent the issue of having a maximum of 10 assignees, GitHub doesn't allow assigning an issue to a team, only to a user. - -## Development - -### Running test code - -To test out your code in an action, you need to build the image and push it to a different container registry repository. For instance, if I want to test some code, I won't build the image with the main image repository. Prior to this, comment out the label binding the image to a repo: - -```dockerfile -# LABEL org.opencontainers.image.source https://github.com/trstringer/manual-approval -``` - -Build the image: - -```shell -VERSION=1.7.1-rc.1 make IMAGE_REPO=ghcr.io/trstringer/manual-approval-test build -``` - -*Note: The image version can be whatever you want, as this image wouldn't be pushed to production. It is only for testing.* - -Push the image to your container registry: - -```shell -VERSION=1.7.1-rc.1 make IMAGE_REPO=ghcr.io/trstringer/manual-approval-test push -``` - -To test out the image, you will need to modify `action.yaml` so that it points to your new image that you're testing: - -```yaml - image: docker://ghcr.io/trstringer/manual-approval-test:1.7.0-rc.1 -``` - -Then, to test out the image, run a workflow specifying your dev branch: - -```yaml -- name: Wait for approval - uses: your-github-user/manual-approval@your-dev-branch - with: - secret: ${{ secrets.GITHUB_TOKEN }} - approvers: trstringer -``` - -For `uses`, this should point to your repo and dev branch. - -*Note: To test out the action that uses an approver that is an org team, refer to the [org team approver](#org-team-approver) section for instructions.* - -### Create a release - -1. Build and push the new image: `$ VERSION=1.7.0 make build_push`. -2. Create a release branch and modify `action.yaml` to point to the new image. -3. Open and merge a PR to add these changes to the default branch. -4. Make sure to fetch the new changes into your local repo: `$ git checkout main && git fetch origin && git merge origin main`. -5. Delete the `v1` tag locally and remotely: `$ git tag -d v1 && git push --delete origin v1`. -6. Create and push new tags: `$ git tag v1.7.0 && git tag v1 && git push origin --tags`. -7. Create the GitHub project release. + * A job (including a paused job) will be failed after 4 hours, and a workflow will be failed after 35 days. + * StackitGit App tokens expire after 1 hour, which implies the duration for the approval cannot exceed 60 minutes, or the job will fail due to bad credentials. diff --git a/action.yaml b/action.yaml index fdd28ae..917d16a 100644 --- a/action.yaml +++ b/action.yaml @@ -23,31 +23,33 @@ inputs: description: The file path to a custom body for the issue required: false exclude-workflow-initiator-as-approver: - description: Whether or not to filter out the user who initiated the workflow as an approver if they are in the approvers list + description: Whether or not to filter out the user who initiated the workflow as + an approver if they are in the approvers list required: false - default: 'false' + default: "false" additional-approved-words: - description: Comma separated list of words that can be used to approve beyond the defaults. + description: Comma separated list of words that can be used to approve beyond + the defaults. required: false - default: '' + default: "" additional-denied-words: description: Comma separated list of words that can be used to deny beyond the defaults. required: false - default: '' + default: "" target-repository-owner: description: Owner of the repository in which the issue will be created. - default: '' + default: "" target-repository: description: Name of the repository in which the issue will be created. - default: '' + default: "" fail-on-denial: description: Whether or not to fail the workflow if the approval is denied required: false - default: 'true' + default: "true" polling-interval-seconds: description: Number of seconds to wait between polling GitHub API for approval status required: false - default: '10' + default: "10" outputs: issue-number: description: The number of the issue created @@ -57,4 +59,4 @@ outputs: description: The status of the approval ("approved" or "denied") runs: using: docker - image: docker://ghcr.io/trstringer/manual-approval:1.12.0 + image: Dockerfile \ No newline at end of file diff --git a/approval.go b/approval.go index c53aeca..71c3a4c 100644 --- a/approval.go +++ b/approval.go @@ -7,16 +7,16 @@ import ( "regexp" "strings" - "github.com/google/go-github/v43/github" + "codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v3" ) type approvalEnvironment struct { - client *github.Client + forgejoClient *forgejo.Client repoFullName string repo string repoOwner string runID int - approvalIssue *github.Issue + approvalIssue *forgejo.Issue approvalIssueNumber int issueTitle string issueBody string @@ -27,7 +27,7 @@ type approvalEnvironment struct { failOnDenial bool } -func newApprovalEnvironment(client *github.Client, repoFullName, repoOwner string, runID int, approvers []string, minimumApprovals int, issueTitle, issueBody string, targetRepoOwner string, targetRepoName string, failOnDenial bool) (*approvalEnvironment, error) { +func newApprovalEnvironment(forgejoClient *forgejo.Client, repoFullName, repoOwner string, runID int, approvers []string, minimumApprovals int, issueTitle, issueBody string, targetRepoOwner string, targetRepoName string, failOnDenial bool) (*approvalEnvironment, error) { repoOwnerAndName := strings.Split(repoFullName, "/") if len(repoOwnerAndName) != 2 { return nil, fmt.Errorf("repo owner and name in unexpected format: %s", repoFullName) @@ -35,7 +35,7 @@ func newApprovalEnvironment(client *github.Client, repoFullName, repoOwner strin repo := repoOwnerAndName[1] return &approvalEnvironment{ - client: client, + forgejoClient: forgejoClient, repoFullName: repoFullName, repo: repo, repoOwner: repoOwner, @@ -58,7 +58,7 @@ func (a approvalEnvironment) runURL() string { return fmt.Sprintf("%s/%s/actions/runs/%d", strings.TrimRight(serverUrl, "/"), a.repoFullName, a.runID) } -func (a *approvalEnvironment) createApprovalIssue(ctx context.Context) error { +func (a *approvalEnvironment) createApprovalIssue(_ context.Context) error { issueTitle := fmt.Sprintf("Manual approval required for workflow run %d", a.runID) if a.issueTitle != "" { @@ -85,7 +85,11 @@ func (a *approvalEnvironment) createApprovalIssue(ctx context.Context) error { formatAcceptedWords(deniedWords), ) - issueBody = fmt.Sprintf(">[!NOTE]\n%s", issueBody) + if a.issueBody != "" { + issueBody = fmt.Sprintf("%s\n\n%s", a.issueBody, issueBody) + } else { + issueBody = fmt.Sprintf(">[!NOTE]\n%s", issueBody) + } var err error fmt.Printf( @@ -96,46 +100,20 @@ func (a *approvalEnvironment) createApprovalIssue(ctx context.Context) error { a.issueApprovers, issueBody, ) - // Use NewRequest+Do with a minimal response struct rather than client.Issues.Create. - // Forgejo's issue response includes "repository.owner" as a plain string, but - // go-github's Repository.Owner is a *User struct, causing an unmarshal error. - // Our minimal struct omits Repository entirely, so the field is ignored. - type createIssueResponse struct { - Number int `json:"number"` - HTMLURL string `json:"html_url"` - } - req, err := a.client.NewRequest("POST", - fmt.Sprintf("repos/%s/%s/issues", a.targetRepoOwner, a.targetRepoName), - &github.IssueRequest{ - Title: &issueTitle, - Body: &issueBody, - Assignees: &a.issueApprovers, - }, - ) + + created, _, err := a.forgejoClient.CreateIssue(a.targetRepoOwner, a.targetRepoName, forgejo.CreateIssueOption{ + Title: issueTitle, + Body: issueBody, + Assignees: a.issueApprovers, + }) if err != nil { return err } - var created createIssueResponse - if _, err = a.client.Do(ctx, req, &created); err != nil { - return err - } - a.approvalIssueNumber = created.Number - a.approvalIssue = &github.Issue{ - Number: &created.Number, - HTMLURL: &created.HTMLURL, - } - bodyChunks := splitLongString(a.issueBody) - for _, chunk := range bodyChunks { - _, _, err = a.client.Issues.CreateComment(ctx, a.targetRepoOwner, a.targetRepoName, created.Number, &github.IssueComment{ - Body: &chunk, - }) - if err != nil { - return fmt.Errorf("failed to add comment chunk to issue: %w", err) - } - } + a.approvalIssue = created + a.approvalIssueNumber = int(created.ID) - fmt.Printf("Issue created: %s\n", a.approvalIssue.GetHTMLURL()) + fmt.Printf("Issue created: %s\n", a.approvalIssue.HTMLURL) return nil } @@ -150,9 +128,9 @@ func (a *approvalEnvironment) SetActionOutputs(outputs map[string]string) (bool, return false, err } - defer func() { - _ = f.Close() // Error explicitly ignored as there is nothing to handle if file close fails. - }() + defer func() { + _ = f.Close() // Error explicitly ignored as there is nothing to handle if file close fails. + }() var pairs []string @@ -179,44 +157,47 @@ func (a *approvalEnvironment) SetActionOutputs(outputs map[string]string) (bool, return true, nil } -func approvalFromComments(comments []*github.IssueComment, approvers []string, minimumApprovals int) (approvalStatus, error) { - remainingApprovers := make([]string, len(approvers)) - copy(remainingApprovers, approvers) - +func approvalFromComments(comments []*forgejo.Comment, approvers []string, minimumApprovals int) (approvalStatus, error) { + approvedUsers := []string{} + deniedUsers := []string{} + // If minimum approvals is not set, default to all approvers if minimumApprovals == 0 { minimumApprovals = len(approvers) } for _, comment := range comments { - commentUser := comment.User.GetLogin() - approverIdx := approversIndex(remainingApprovers, commentUser) - if approverIdx < 0 { + commenter := comment.Poster.UserName + if approversIndex(approvers, commenter) == -1 { continue } - commentBody := comment.GetBody() - isApprovalComment, err := isApproved(commentBody) + isApproval, err := isApproved(comment.Body) if err != nil { - return approvalStatusPending, err + return "", err } - if isApprovalComment { - if len(remainingApprovers) == len(approvers)-minimumApprovals+1 { - return approvalStatusApproved, nil - } - remainingApprovers[approverIdx] = remainingApprovers[len(remainingApprovers)-1] - remainingApprovers = remainingApprovers[:len(remainingApprovers)-1] - continue + isDenial, err := isDenied(comment.Body) + if err != nil { + return "", err } - isDenialComment, err := isDenied(commentBody) - if err != nil { - return approvalStatusPending, err - } - if isDenialComment { - return approvalStatusDenied, nil + if isApproval { + approvedUsers = append(approvedUsers, commenter) + } else if isDenial { + deniedUsers = append(deniedUsers, commenter) } } + approvedUsers = deduplicateUsers(approvedUsers) + deniedUsers = deduplicateUsers(deniedUsers) + + if len(deniedUsers) > 0 { + return approvalStatusDenied, nil + } + + if len(approvedUsers) >= minimumApprovals { + return approvalStatusApproved, nil + } + return approvalStatusPending, nil } @@ -298,50 +279,3 @@ func splitLongLine(line string, maxL int) ([]string, bool) { } return result, true } - -func splitLongString(input string) []string { - maxLength := 65536 - var result []string - - lines := strings.Split(input, "\n") - currentChunk := strings.Builder{} - currentLength := 0 - - for i, line := range lines { - lineLength := len(line) - if i < len(lines)-1 { - lineLength++ - } - - if currentLength+lineLength > maxLength { - if currentChunk.Len() > 0 { - result = append(result, currentChunk.String()) - currentChunk.Reset() - currentLength = 0 - } - } - - lineSplit, isLongLine := splitLongLine(line, maxLength) - if isLongLine { - if currentChunk.Len() > 0 { - result = append(result, currentChunk.String()) - currentChunk.Reset() - } - result = append(result, lineSplit[:len(lineSplit)-1]...) - currentChunk.WriteString(lineSplit[len(lineSplit)-1]) - currentLength = len(lineSplit[len(lineSplit)-1]) - } else { - currentChunk.WriteString(line) - currentLength += lineLength - } - - if i < len(lines)-1 { - currentChunk.WriteString("\n") - } - } - if currentChunk.Len() > 0 { - result = append(result, currentChunk.String()) - } - return result -} - diff --git a/approval_test.go b/approval_test.go index 086a6fa..529ec83 100644 --- a/approval_test.go +++ b/approval_test.go @@ -6,7 +6,7 @@ import ( "strings" "testing" - "github.com/google/go-github/v43/github" + "codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v3" ) func TestApprovalFromComments(t *testing.T) { @@ -21,17 +21,17 @@ func TestApprovalFromComments(t *testing.T) { testCases := []struct { name string - comments []*github.IssueComment + comments []*forgejo.Comment approvers []string minimumApprovals int expectedStatus approvalStatus }{ { name: "single_approver_single_comment_approved", - comments: []*github.IssueComment{ + comments: []*forgejo.Comment{ { - User: &github.User{Login: &login1}, - Body: &bodyApproved, + Poster: &forgejo.User{UserName: login1}, + Body: bodyApproved, }, }, approvers: []string{login1}, @@ -39,10 +39,10 @@ func TestApprovalFromComments(t *testing.T) { }, { name: "single_approver_single_comment_denied", - comments: []*github.IssueComment{ + comments: []*forgejo.Comment{ { - User: &github.User{Login: &login1}, - Body: &bodyDenied, + Poster: &forgejo.User{UserName: login1}, + Body: bodyDenied, }, }, approvers: []string{login1}, @@ -50,10 +50,10 @@ func TestApprovalFromComments(t *testing.T) { }, { name: "single_approver_single_comment_pending", - comments: []*github.IssueComment{ + comments: []*forgejo.Comment{ { - User: &github.User{Login: &login1}, - Body: &bodyPending, + Poster: &forgejo.User{UserName: login1}, + Body: bodyPending, }, }, approvers: []string{login1}, @@ -61,14 +61,14 @@ func TestApprovalFromComments(t *testing.T) { }, { name: "single_approver_multi_comment_approved", - comments: []*github.IssueComment{ + comments: []*forgejo.Comment{ { - User: &github.User{Login: &login1}, - Body: &bodyPending, + Poster: &forgejo.User{UserName: login1}, + Body: bodyPending, }, { - User: &github.User{Login: &login1}, - Body: &bodyApproved, + Poster: &forgejo.User{UserName: login1}, + Body: bodyApproved, }, }, approvers: []string{login1}, @@ -76,14 +76,14 @@ func TestApprovalFromComments(t *testing.T) { }, { name: "multi_approver_approved", - comments: []*github.IssueComment{ + comments: []*forgejo.Comment{ { - User: &github.User{Login: &login1}, - Body: &bodyApproved, + Poster: &forgejo.User{UserName: login1}, + Body: bodyApproved, }, { - User: &github.User{Login: &login2}, - Body: &bodyApproved, + Poster: &forgejo.User{UserName: login2}, + Body: bodyApproved, }, }, approvers: []string{login1, login2}, @@ -91,14 +91,14 @@ func TestApprovalFromComments(t *testing.T) { }, { name: "multi_approver_mixed", - comments: []*github.IssueComment{ + comments: []*forgejo.Comment{ { - User: &github.User{Login: &login1}, - Body: &bodyPending, + Poster: &forgejo.User{UserName: login1}, + Body: bodyPending, }, { - User: &github.User{Login: &login2}, - Body: &bodyApproved, + Poster: &forgejo.User{UserName: login2}, + Body: bodyApproved, }, }, approvers: []string{login1, login2}, @@ -106,14 +106,14 @@ func TestApprovalFromComments(t *testing.T) { }, { name: "multi_approver_denied", - comments: []*github.IssueComment{ + comments: []*forgejo.Comment{ { - User: &github.User{Login: &login1}, - Body: &bodyDenied, + Poster: &forgejo.User{UserName: login1}, + Body: bodyDenied, }, { - User: &github.User{Login: &login2}, - Body: &bodyApproved, + Poster: &forgejo.User{UserName: login2}, + Body: bodyApproved, }, }, approvers: []string{login1, login2}, @@ -121,14 +121,14 @@ func TestApprovalFromComments(t *testing.T) { }, { name: "multi_approver_minimum_one_approval", - comments: []*github.IssueComment{ + comments: []*forgejo.Comment{ { - User: &github.User{Login: &login1}, - Body: &bodyPending, + Poster: &forgejo.User{UserName: login1}, + Body: bodyPending, }, { - User: &github.User{Login: &login2}, - Body: &bodyApproved, + Poster: &forgejo.User{UserName: login2}, + Body: bodyApproved, }, }, approvers: []string{login1, login2}, @@ -137,14 +137,14 @@ func TestApprovalFromComments(t *testing.T) { }, { name: "multi_approver_minimum_two_approvals", - comments: []*github.IssueComment{ + comments: []*forgejo.Comment{ { - User: &github.User{Login: &login1}, - Body: &bodyApproved, + Poster: &forgejo.User{UserName: login1}, + Body: bodyApproved, }, { - User: &github.User{Login: &login2}, - Body: &bodyApproved, + Poster: &forgejo.User{UserName: login2}, + Body: bodyApproved, }, }, approvers: []string{login1, login2, login3}, @@ -153,10 +153,10 @@ func TestApprovalFromComments(t *testing.T) { }, { name: "multi_approver_approvals_less_than_minimum", - comments: []*github.IssueComment{ + comments: []*forgejo.Comment{ { - User: &github.User{Login: &login1}, - Body: &bodyApproved, + Poster: &forgejo.User{UserName: login1}, + Body: bodyApproved, }, }, approvers: []string{login1, login2, login3}, @@ -165,10 +165,10 @@ func TestApprovalFromComments(t *testing.T) { }, { name: "single_approver_single_comment_approved_case_insensitive", - comments: []*github.IssueComment{ + comments: []*forgejo.Comment{ { - User: &github.User{Login: &login1u}, - Body: &bodyApproved, + Poster: &forgejo.User{UserName: login1u}, + Body: bodyApproved, }, }, approvers: []string{login1}, @@ -469,7 +469,7 @@ func TestSaveOutput(t *testing.T) { t.Run(testCase.name, func(t *testing.T) { t.Setenv("GITHUB_OUTPUT", testCase.env_github_output) a := approvalEnvironment{ - client: nil, + forgejoClient: nil, repoFullName: "", repo: "", repoOwner: "", @@ -481,9 +481,9 @@ func TestSaveOutput(t *testing.T) { minimumApprovals: 0, } - if err := os.Remove(testCase.env_github_output); err != nil && !os.IsNotExist(err) { - t.Fatalf("failed to remove file: %v", err) - } + if err := os.Remove(testCase.env_github_output); err != nil && !os.IsNotExist(err) { + t.Fatalf("failed to remove file: %v", err) + } actual, err := a.SetActionOutputs(nil) diff --git a/approvers.go b/approvers.go index c3f129f..b191d8e 100644 --- a/approvers.go +++ b/approvers.go @@ -1,16 +1,15 @@ package main import ( - "context" "fmt" "os" "strconv" "strings" - "github.com/google/go-github/v43/github" + "codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v3" ) -func retrieveApprovers(client *github.Client, repoOwner string) ([]string, error) { +func retrieveApprovers(client *forgejo.Client, repoOwner string) ([]string, error) { workflowInitiator := os.Getenv(envVarWorkflowInitiator) shouldExcludeWorkflowInitiatorRaw := os.Getenv(envVarExcludeWorkflowInitiatorAsApprover) shouldExcludeWorkflowInitiator, parseBoolErr := strconv.ParseBool(shouldExcludeWorkflowInitiatorRaw) @@ -28,7 +27,7 @@ func retrieveApprovers(client *github.Client, repoOwner string) ([]string, error for _, approverUser := range requiredApprovers { expandedUsers := expandGroupFromUser(client, repoOwner, approverUser, workflowInitiator, shouldExcludeWorkflowInitiator) - if expandedUsers != nil { + if len(expandedUsers) > 0 { approvers = append(approvers, expandedUsers...) } else if strings.EqualFold(workflowInitiator, approverUser) && shouldExcludeWorkflowInitiator { fmt.Printf("Not adding user '%s' as an approver as they are the workflow initiator\n", approverUser) @@ -56,23 +55,37 @@ func retrieveApprovers(client *github.Client, repoOwner string) ([]string, error return approvers, nil } -func expandGroupFromUser(client *github.Client, org, userOrTeam string, workflowInitiator string, shouldExcludeWorkflowInitiator bool) []string { +func expandGroupFromUser(client *forgejo.Client, org string, userOrTeam string, workflowInitiator string, shouldExcludeWorkflowInitiator bool) []string { fmt.Printf("Attempting to expand user %s/%s as a group (may not succeed)\n", org, userOrTeam) - // GitHub replaces periods in the team name with hyphens. If a period is - // passed to the request it would result in a 404. So we need to replace - // and occurrences with a hyphen. - formattedUserOrTeam := strings.ReplaceAll(userOrTeam, ".", "-") - - users, _, err := client.Teams.ListTeamMembersBySlug(context.Background(), org, formattedUserOrTeam, &github.TeamListTeamMembersOptions{}) + // Try to find a team with this name in the org + teams, _, err := client.SearchOrgTeams(org, &forgejo.SearchTeamsOptions{Query: userOrTeam}) if err != nil { - fmt.Printf("%v\n", err) + fmt.Printf("Error searching teams: %v\n", err) + return nil + } + + var team *forgejo.Team + for _, t := range teams { + if strings.EqualFold(t.Name, userOrTeam) { + team = t + break + } + } + + if team == nil { + return nil + } + + users, _, err := client.ListTeamMembers(team.ID, forgejo.ListTeamMembersOptions{}) + if err != nil { + fmt.Printf("Error listing team members: %v\n", err) return nil } userNames := make([]string, 0, len(users)) for _, user := range users { - userName := user.GetLogin() + userName := user.UserName if strings.EqualFold(userName, workflowInitiator) && shouldExcludeWorkflowInitiator { fmt.Printf("Not adding user '%s' from group '%s' as an approver as they are the workflow initiator\n", userName, userOrTeam) } else { diff --git a/go.mod b/go.mod index 2f1fd03..583cfa2 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,35 @@ module github.com/trstringer/manual-approval -go 1.24.0 +go 1.25 + +require codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v3 v3.0.0 require ( - github.com/google/go-github/v43 v43.0.0 - golang.org/x/oauth2 v0.33.0 -) - -require ( - github.com/google/go-querystring v1.1.0 // indirect + github.com/42wim/httpsig v1.2.3 // indirect + github.com/davidmz/go-pageant v1.0.2 // indirect + github.com/go-fed/httpsig v1.1.0 // indirect + github.com/go-openapi/errors v0.22.6 // indirect + github.com/go-openapi/strfmt v0.25.0 // indirect + github.com/go-openapi/swag v0.25.4 // indirect + github.com/go-openapi/swag/cmdutils v0.25.4 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/fileutils v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/mangling v0.25.4 // indirect + github.com/go-openapi/swag/netutils v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect + github.com/oklog/ulid v1.3.1 // indirect + go.mongodb.org/mongo-driver v1.17.9 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.45.0 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.31.0 // indirect ) diff --git a/main.go b/main.go index 666cfb6..a871c11 100644 --- a/main.go +++ b/main.go @@ -3,40 +3,34 @@ package main import ( "context" "fmt" - "net/url" "os" "os/signal" "strconv" "strings" "time" - "github.com/google/go-github/v43/github" - "golang.org/x/oauth2" + "codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v3" ) // patchIssueState closes or otherwise updates an issue's state without decoding // the response body into a github.Issue. go-github's Issues.Edit decodes the // response into github.Issue, which fails against Forgejo because its issue // response embeds "repository.owner" as a plain string rather than a User object. -func patchIssueState(ctx context.Context, client *github.Client, owner, repo string, number int, state string) error { - req, err := client.NewRequest("PATCH", - fmt.Sprintf("repos/%s/%s/issues/%d", owner, repo, number), - &github.IssueRequest{State: &state}, - ) - if err != nil { - return err - } - _, err = client.Do(ctx, req, nil) +func patchIssueState(_ context.Context, client *forgejo.Client, owner, repo string, number int, state string) error { + issueState := forgejo.StateType(state) + _, _, err := client.EditIssue(owner, repo, int64(number), forgejo.EditIssueOption{ + State: &issueState, + }) return err } -func handleInterrupt(ctx context.Context, client *github.Client, apprv *approvalEnvironment) { +func handleInterrupt(ctx context.Context, client *forgejo.Client, apprv *approvalEnvironment) { newState := "closed" closeComment := "Workflow cancelled, closing issue." fmt.Println(closeComment) - _, _, err := client.Issues.CreateComment(ctx, apprv.targetRepoOwner, apprv.targetRepoName, apprv.approvalIssueNumber, &github.IssueComment{ - Body: &closeComment, + _, _, err := client.CreateIssueComment(apprv.targetRepoOwner, apprv.targetRepoName, int64(apprv.approvalIssueNumber), forgejo.CreateIssueCommentOption{ + Body: closeComment, }) if err != nil { fmt.Printf("error commenting on issue: %v\n", err) @@ -48,11 +42,11 @@ func handleInterrupt(ctx context.Context, client *github.Client, apprv *approval } } -func newCommentLoopChannel(ctx context.Context, apprv *approvalEnvironment, client *github.Client, pollingInterval time.Duration) chan int { +func newCommentLoopChannel(ctx context.Context, apprv *approvalEnvironment, client *forgejo.Client, pollingInterval time.Duration) chan int { channel := make(chan int) go func() { for { - comments, _, err := client.Issues.ListComments(ctx, apprv.targetRepoOwner, apprv.targetRepoName, apprv.approvalIssueNumber, &github.IssueListCommentsOptions{}) + comments, _, err := client.ListIssueComments(apprv.targetRepoOwner, apprv.targetRepoName, int64(apprv.approvalIssueNumber), forgejo.ListIssueCommentOptions{}) if err != nil { fmt.Printf("error getting comments: %v\n", err) channel <- 1 @@ -72,8 +66,8 @@ func newCommentLoopChannel(ctx context.Context, apprv *approvalEnvironment, clie case approvalStatusApproved: newState := "closed" closeComment := fmt.Sprintf("The required number of approvals (%d) has been met; continuing workflow and closing this issue.", apprv.minimumApprovals) - _, _, err := client.Issues.CreateComment(ctx, apprv.targetRepoOwner, apprv.targetRepoName, apprv.approvalIssueNumber, &github.IssueComment{ - Body: &closeComment, + _, _, err := client.CreateIssueComment(apprv.targetRepoOwner, apprv.targetRepoName, int64(apprv.approvalIssueNumber), forgejo.CreateIssueCommentOption{ + Body: closeComment, }) if err != nil { fmt.Printf("error commenting on issue: %v\n", err) @@ -101,8 +95,8 @@ func newCommentLoopChannel(ctx context.Context, apprv *approvalEnvironment, clie } closeComment += " workflow." - _, _, err := client.Issues.CreateComment(ctx, apprv.targetRepoOwner, apprv.targetRepoName, apprv.approvalIssueNumber, &github.IssueComment{ - Body: &closeComment, + _, _, err := client.CreateIssueComment(apprv.targetRepoOwner, apprv.targetRepoName, int64(apprv.approvalIssueNumber), forgejo.CreateIssueCommentOption{ + Body: closeComment, }) if err != nil { fmt.Printf("error commenting on issue: %v\n", err) @@ -127,39 +121,17 @@ func newCommentLoopChannel(ctx context.Context, apprv *approvalEnvironment, clie return channel } -func newGithubClient(ctx context.Context) (*github.Client, error) { +func newForgejoClient() (*forgejo.Client, error) { token := os.Getenv(envVarToken) - ts := oauth2.StaticTokenSource( - &oauth2.Token{AccessToken: token}, - ) - tc := oauth2.NewClient(ctx, ts) - - serverUrl, serverUrlPresent := os.LookupEnv("GITHUB_SERVER_URL") - apiUrl, apiUrlPresent := os.LookupEnv("GITHUB_API_URL") - - if !serverUrlPresent && !apiUrlPresent { - return github.NewClient(tc), nil + serverUrl := os.Getenv("GITHUB_SERVER_URL") + if serverUrl == "" { + return nil, fmt.Errorf("GITHUB_SERVER_URL must be set for Forgejo client") } - if !apiUrlPresent { - // Only GITHUB_SERVER_URL is set; assume GitHub Enterprise with the - // default /api/v3 path and let NewEnterpriseClient append it. - return github.NewEnterpriseClient(serverUrl, serverUrl, tc) - } - - // GITHUB_API_URL is set. github.NewEnterpriseClient appends "/api/v3/" to - // any URL whose path doesn't already end with it. This breaks Forgejo/Gitea - // instances whose API lives at "/api/v1/". Instead, set BaseURL directly so - // the URL is used as-is, which works for GitHub.com, GHES, and Forgejo alike. - if !strings.HasSuffix(apiUrl, "/") { - apiUrl += "/" - } - baseURL, err := url.Parse(apiUrl) + client, err := forgejo.NewClient(serverUrl, forgejo.SetToken(token)) if err != nil { - return nil, fmt.Errorf("invalid GITHUB_API_URL %q: %w", apiUrl, err) + return nil, fmt.Errorf("failed to create forgejo client: %w", err) } - client := github.NewClient(tc) - client.BaseURL = baseURL return client, nil } @@ -215,13 +187,13 @@ func main() { } ctx := context.Background() - client, err := newGithubClient(ctx) + forgejoClient, err := newForgejoClient() if err != nil { - fmt.Printf("error connecting to server: %v\n", err) + fmt.Printf("error connecting to forgejo server: %v\n", err) os.Exit(1) } - approvers, err := retrieveApprovers(client, repoOwner) + approvers, err := retrieveApprovers(forgejoClient, repoOwner) if err != nil { fmt.Printf("error retrieving approvers: %v\n", err) os.Exit(1) @@ -274,7 +246,7 @@ func main() { } } - apprv, err := newApprovalEnvironment(client, repoFullName, repoOwner, runID, approvers, minimumApprovals, issueTitle, issueBody, targetRepoOwner, targetRepoName, failOnDenial) + apprv, err := newApprovalEnvironment(forgejoClient, repoFullName, repoOwner, runID, approvers, minimumApprovals, issueTitle, issueBody, targetRepoOwner, targetRepoName, failOnDenial) if err != nil { fmt.Printf("error creating approval environment: %v\n", err) os.Exit(1) @@ -286,9 +258,9 @@ func main() { os.Exit(1) } - outputs := map[string]string { + outputs := map[string]string{ "issue-number": fmt.Sprintf("%d", apprv.approvalIssueNumber), - "issue-url": apprv.approvalIssue.GetHTMLURL(), + "issue-url": apprv.approvalIssue.HTMLURL, } _, err = apprv.SetActionOutputs(outputs) if err != nil { @@ -299,21 +271,21 @@ func main() { killSignalChannel := make(chan os.Signal, 1) signal.Notify(killSignalChannel, os.Interrupt) - commentLoopChannel := newCommentLoopChannel(ctx, apprv, client, pollingInterval) + commentLoopChannel := newCommentLoopChannel(ctx, apprv, forgejoClient, pollingInterval) select { case exitCode := <-commentLoopChannel: approvalStatus := "" - if (!failOnDenial && exitCode == 1) { + if !failOnDenial && exitCode == 1 { approvalStatus = "denied" exitCode = 0 - } else if (exitCode == 1) { + } else if exitCode == 1 { approvalStatus = "denied" } else { approvalStatus = "approved" } - outputs := map[string]string { + outputs := map[string]string{ "approval-status": approvalStatus, } if _, err := apprv.SetActionOutputs(outputs); err != nil { @@ -322,7 +294,7 @@ func main() { } os.Exit(exitCode) case <-killSignalChannel: - handleInterrupt(ctx, client, apprv) + handleInterrupt(ctx, forgejoClient, apprv) os.Exit(1) } } diff --git a/workflow_dispatcher.md b/workflow_dispatcher.md new file mode 100644 index 0000000..5a98930 --- /dev/null +++ b/workflow_dispatcher.md @@ -0,0 +1,57 @@ +# Forgejo Actions: Manual Approvals Guide + +While StackitGit Actions does not currently feature native environment protection rules that pause a running workflow for a UI approval, the most effective and resource-efficient workaround is to split your CI/CD pipeline into separate workflows using the `workflow_dispatch` event. + +> **Resource Efficiency (Cost Savings):** +> Unlike workarounds that use third-party actions to poll for comments on an Issue, splitting workflows is **significantly cheaper and more efficient**. Pause-and-poll methods keep the original workflow active, *holding the runner hostage* and consuming compute minutes for hours or days while waiting for an approval. By splitting workflows, the runner is immediately freed after the build phase. A new runner is only provisioned when the deployment is explicitly approved. + +## Step 1: The Automated Build & Test Workflow + +Create your primary workflow that runs automatically on every push or pull request. This workflow handles everything up to the point of deployment. + +```yaml +name: 1. Build and Test +on: [push] + +jobs: + build: + runs-on: stackit-ubuntu-22 + steps: + - uses: actions/checkout@v4 + - name: Run tests + run: echo "Testing the code..." + - name: Build artifact + run: echo "Building artifact..." + # Upload artifacts here for the deploy workflow to download +``` + +## Step 2: The Manual Deployment Workflow + +Create a second workflow triggered *only* by `workflow_dispatch`. This generates a "Run Workflow" button in the Forgejo UI, serving as your manual approval gate. + +```yaml +name: 2. Manual Production Deploy +on: + workflow_dispatch: + inputs: + version: + description: 'Version or Branch to deploy' + required: true + default: 'main' + +jobs: + deploy: + runs-on: stackit-ubuntu-22 + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.version }} + - name: Deploy to Production + run: echo "Deploying version ${{ github.event.inputs.version }}..." +``` + +## Workflow Execution + +1. A developer pushes code, triggering the **Build and Test** workflow automatically. +2. The team reviews the workflow results and test logs. The runner completes its job, reports success, and shuts down. +3. When the release is approved, an authorized team member navigates to the Forgejo Actions tab, selects the **Manual Production Deploy** workflow, and clicks "Run Workflow". \ No newline at end of file From 2bcb71ffdf5943584c83dee74bde67dd3752c45c Mon Sep 17 00:00:00 2001 From: jaime merino Date: Thu, 23 Apr 2026 12:51:58 +0200 Subject: [PATCH 59/60] use the repository-specific issue index instead of the global issue ID. --- approval.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/approval.go b/approval.go index 71c3a4c..ce50110 100644 --- a/approval.go +++ b/approval.go @@ -111,7 +111,7 @@ func (a *approvalEnvironment) createApprovalIssue(_ context.Context) error { } a.approvalIssue = created - a.approvalIssueNumber = int(created.ID) + a.approvalIssueNumber = int(created.Index) fmt.Printf("Issue created: %s\n", a.approvalIssue.HTMLURL) return nil From 023907bee418cfbb16bd997e2c4d13e25b168570 Mon Sep 17 00:00:00 2001 From: jaime merino Date: Thu, 23 Apr 2026 15:54:40 +0200 Subject: [PATCH 60/60] Replace mentions to github and forgejo for stackit git --- CONTRIBUTING.md | 4 ++-- README.md | 4 ++-- action.yaml | 2 +- main.go | 8 ++++---- workflow_dispatcher.md | 6 +++--- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bf270b3..c3d2382 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,8 +27,8 @@ Thank you for your interest in contributing to this project! We appreciate your git push origin feature-branch-name ``` -3. Open a PR on GitHub, describing your changes clearly. -4. If your PR is related to an issue, use the [GitHub key words](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests#linking-a-pull-request-to-an-issue). +3. Open a PR on StackitGit, describing your changes clearly. +4. If your PR is related to an issue, use the [StackitGit key words](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests#linking-a-pull-request-to-an-issue). This will ensure that the related issue will automatically close when your change is merged. 5. Address any feedback and update your PR as necessary. diff --git a/README.md b/README.md index 2249cbd..da5526c 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ steps: > [!Note] > 1. If You are using issue-body-file-path then please make sure the file is reachable; for example, if the file is in your repo, then please checkout to your repo in the same job as the approval issue. -> 2. When using issue-body, the content string is passed as an arguent which is limited by github at 10kb. For content >= 10kb, use files for passing the issue body. +> 2. When using issue-body, the content string is passed as an arguent which is limited by StackitGit at 10kb. For content >= 10kb, use files for passing the issue body. > [!CAUTION] > When using a file please make sure that the file size remains under 125 KB (a safe limit, to stay under the threshold). If the file size is huge, then the file content will be broken into multiple chunks, each representing an issue comment. With this many API requests the API rate limit is exceeded, and the actions will be temporarily blocked, resulting in an error message like: `403 You have exceeded a secondary rate limit and have been temporarily blocked from content creation. Please retry your request again later.` @@ -95,7 +95,7 @@ For a seamless experience, it is recommended that you add the custom words to a ## Org team approver -If you want to have `approvers` set to an org team, then you need to take a different approach. The default [StackitGit Actions automatic token](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token) does not have the necessary permissions to list out team members. If you would like to use this, then you need to generate a token from a StackitGit App with the correct set of permissions. Apart from this, the GH app will also need the Issue: Read & Write role. +If you want to have `approvers` set to an org team, then you need to take a different approach. The default [StackitGit Actions automatic token](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token) does not have the necessary permissions to list out team members. If you would like to use this, then you need to generate a token from a StackitGit App with the correct set of permissions. Apart from this, the StackitGit app will also need the Issue: Read & Write role. Create an Organization StackitGit App with **read-only access to organization members**. Once the app is created, add a repo secret with the app ID. In the StackitGit App settings, generate a private key and add that as a secret in the repo as well. You can get the app token by using the [`actions/create-github-app-token`](https://github.com/actions/create-github-app-token) StackitGit Action: diff --git a/action.yaml b/action.yaml index 917d16a..081c2e9 100644 --- a/action.yaml +++ b/action.yaml @@ -47,7 +47,7 @@ inputs: required: false default: "true" polling-interval-seconds: - description: Number of seconds to wait between polling GitHub API for approval status + description: Number of seconds to wait between polling StackitGit API for approval status required: false default: "10" outputs: diff --git a/main.go b/main.go index a871c11..36b616c 100644 --- a/main.go +++ b/main.go @@ -14,7 +14,7 @@ import ( // patchIssueState closes or otherwise updates an issue's state without decoding // the response body into a github.Issue. go-github's Issues.Edit decodes the -// response into github.Issue, which fails against Forgejo because its issue +// response into github.Issue, which fails against StackitGit because its issue // response embeds "repository.owner" as a plain string rather than a User object. func patchIssueState(_ context.Context, client *forgejo.Client, owner, repo string, number int, state string) error { issueState := forgejo.StateType(state) @@ -125,12 +125,12 @@ func newForgejoClient() (*forgejo.Client, error) { token := os.Getenv(envVarToken) serverUrl := os.Getenv("GITHUB_SERVER_URL") if serverUrl == "" { - return nil, fmt.Errorf("GITHUB_SERVER_URL must be set for Forgejo client") + return nil, fmt.Errorf("GITHUB_SERVER_URL must be set for StackitGit client") } client, err := forgejo.NewClient(serverUrl, forgejo.SetToken(token)) if err != nil { - return nil, fmt.Errorf("failed to create forgejo client: %w", err) + return nil, fmt.Errorf("failed to create StackitGit client: %w", err) } return client, nil } @@ -189,7 +189,7 @@ func main() { ctx := context.Background() forgejoClient, err := newForgejoClient() if err != nil { - fmt.Printf("error connecting to forgejo server: %v\n", err) + fmt.Printf("error connecting to StackitGit server: %v\n", err) os.Exit(1) } diff --git a/workflow_dispatcher.md b/workflow_dispatcher.md index 5a98930..b1979c3 100644 --- a/workflow_dispatcher.md +++ b/workflow_dispatcher.md @@ -1,4 +1,4 @@ -# Forgejo Actions: Manual Approvals Guide +# StackitGit Actions: Manual Approvals Guide While StackitGit Actions does not currently feature native environment protection rules that pause a running workflow for a UI approval, the most effective and resource-efficient workaround is to split your CI/CD pipeline into separate workflows using the `workflow_dispatch` event. @@ -27,7 +27,7 @@ jobs: ## Step 2: The Manual Deployment Workflow -Create a second workflow triggered *only* by `workflow_dispatch`. This generates a "Run Workflow" button in the Forgejo UI, serving as your manual approval gate. +Create a second workflow triggered *only* by `workflow_dispatch`. This generates a "Run Workflow" button in the StackitGit UI, serving as your manual approval gate. ```yaml name: 2. Manual Production Deploy @@ -54,4 +54,4 @@ jobs: 1. A developer pushes code, triggering the **Build and Test** workflow automatically. 2. The team reviews the workflow results and test logs. The runner completes its job, reports success, and shuts down. -3. When the release is approved, an authorized team member navigates to the Forgejo Actions tab, selects the **Manual Production Deploy** workflow, and clicks "Run Workflow". \ No newline at end of file +3. When the release is approved, an authorized team member navigates to the StackitGit Actions tab, selects the **Manual Production Deploy** workflow, and clicks "Run Workflow". \ No newline at end of file