├── .github ├── actions.yml ├── renovate.json └── workflows │ ├── branch-cleanup.yml │ ├── pr-opened-command.yml │ ├── rebuild-readme-command.yml │ ├── terraform-fmt-command.yml │ ├── terratest-command.yml │ └── test-command.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── codefresh ├── pipeline-creator │ ├── Dockerfile │ ├── README.md │ ├── action.yml │ └── entrypoint.sh └── pipeline-runner │ ├── Dockerfile │ ├── action.yml │ └── entrypoint.sh ├── github ├── Makefile ├── auto-approve │ ├── .gitattributes │ ├── .github │ │ └── workflows │ │ │ └── ci.yml │ ├── .gitignore │ ├── .vscode │ │ └── settings.json │ ├── LICENSE │ ├── README.md │ ├── action.yml │ ├── dist │ │ └── index.js │ ├── jest.config.js │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── approve.test.ts │ │ ├── approve.ts │ │ ├── main.test.ts │ │ └── main.ts │ └── tsconfig.json ├── auto-assign │ ├── .github │ │ ├── FUNDING.yml │ │ ├── auto_assign.yml │ │ ├── release-drafter.yml │ │ └── workflows │ │ │ ├── pull-request.yml │ │ │ ├── release-note.yml │ │ │ └── test.yml │ ├── .gitignore │ ├── .husky │ │ └── .gitignore │ ├── .node-version │ ├── .prettierrc.yml │ ├── LICENCE │ ├── README.md │ ├── __tests__ │ │ ├── handler.test.ts │ │ ├── run.test.ts │ │ ├── tsconfig.jest.json │ │ └── utils.test.ts │ ├── action.yml │ ├── dist │ │ ├── index.js │ │ ├── index.js.map │ │ ├── licenses.txt │ │ └── sourcemap-register.js │ ├── jest.config.js │ ├── package-lock.json │ ├── package.json │ ├── renovate.json5 │ ├── src │ │ ├── handler.ts │ │ ├── main.ts │ │ ├── pull_request.ts │ │ ├── run.ts │ │ ├── types.ts │ │ └── utils.ts │ └── tsconfig.json ├── auto-merge │ ├── .circleci │ │ └── config.yml │ ├── .eslintignore │ ├── .eslintrc.json │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── action.yml │ ├── bin │ │ └── automerge.js │ ├── dist │ │ ├── LICENSE │ │ └── index.js │ ├── docs │ │ └── screenshot.svg │ ├── it │ │ └── it.js │ ├── lib │ │ ├── api.js │ │ ├── common.js │ │ ├── git.js │ │ ├── merge.js │ │ ├── update.js │ │ └── util.js │ ├── package.json │ ├── test │ │ ├── api.test.js │ │ ├── common.js │ │ ├── common.test.js │ │ ├── git.test.js │ │ ├── merge.test.js │ │ ├── update.test.js │ │ └── util.test.js │ └── yarn.lock ├── branch-cleanup │ ├── .github │ │ └── FUNDING.yml │ ├── .travis.yml │ ├── Dockerfile │ ├── LICENSE │ ├── Makefile │ ├── README.md │ ├── cleanup-pr-branch │ ├── demo.png │ └── test.sh ├── create-or-update-comment │ ├── .eslintrc.json │ ├── .github │ │ ├── FUNDING.yml │ │ ├── comment-body-addition.md │ │ ├── comment-body.md │ │ ├── comment-template.md │ │ └── workflows │ │ │ ├── ci.yml │ │ │ ├── slash-command-dispatch.yml │ │ │ └── test-command.yml │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── action.yml │ ├── dist │ │ └── index.js │ ├── index.js │ ├── package-lock.json │ └── package.json ├── create-pull-request │ ├── .eslintignore │ ├── .eslintrc.json │ ├── .github │ │ ├── FUNDING.yml │ │ ├── ISSUE_TEMPLATE.md │ │ └── workflows │ │ │ ├── ci.yml │ │ │ ├── cpr-example-command.yml │ │ │ └── slash-command-dispatch.yml │ ├── .gitignore │ ├── .prettierignore │ ├── .prettierrc.json │ ├── LICENSE │ ├── README.md │ ├── __test__ │ │ ├── create-or-update-branch.int.test.ts │ │ ├── entrypoint.sh │ │ ├── git-auth-helper.int.test.ts │ │ ├── integration-tests.sh │ │ └── utils.unit.test.ts │ ├── action.yml │ ├── dist │ │ ├── bridge.js │ │ ├── events.js │ │ ├── index.js │ │ ├── setup-node-sandbox.js │ │ └── setup-sandbox.js │ ├── docs │ │ ├── assets │ │ │ ├── cpr-gitgraph.htm │ │ │ ├── cpr-gitgraph.png │ │ │ ├── logo.svg │ │ │ └── pull-request-example.png │ │ ├── concepts-guidelines.md │ │ ├── examples.md │ │ └── updating.md │ ├── jest.config.js │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── create-or-update-branch.ts │ │ ├── create-pull-request.ts │ │ ├── git-auth-helper.ts │ │ ├── git-command-manager.ts │ │ ├── github-helper.ts │ │ ├── main.ts │ │ ├── octokit-client.ts │ │ └── utils.ts │ └── tsconfig.json ├── crud │ ├── Dockerfile │ ├── action.yaml │ └── entrypoint.sh ├── git-push │ ├── Dockerfile │ ├── action.yml │ └── entrypoint.sh ├── release-assets │ ├── Dockerfile │ ├── action.yml │ └── entrypoint.sh ├── repository-dispatch │ ├── .eslintignore │ ├── .eslintrc.json │ ├── .github │ │ ├── FUNDING.yml │ │ └── workflows │ │ │ ├── ci.yml │ │ │ ├── on-repository-dispatch.yml │ │ │ └── slash-command-dispatch.yml │ ├── .gitignore │ ├── .prettierignore │ ├── .prettierrc.json │ ├── LICENSE │ ├── README.md │ ├── action.yml │ ├── dist │ │ └── index.js │ ├── jest.config.js │ ├── package-lock.json │ ├── package.json │ ├── src │ │ └── main.ts │ └── tsconfig.json └── slash-command-dispatch │ ├── .eslintignore │ ├── .eslintrc.json │ ├── .github │ ├── FUNDING.yml │ ├── slash-command-dispatch.json │ └── workflows │ │ ├── ci.yml │ │ ├── hello-world-command.yml │ │ ├── ping-command.yml │ │ └── slash-command-dispatch.yml │ ├── .gitignore │ ├── .prettierignore │ ├── .prettierrc.json │ ├── LICENSE │ ├── README.md │ ├── __test__ │ ├── command-helper.unit.test.ts │ └── github-helper.int.test.ts │ ├── action.yml │ ├── dist │ └── index.js │ ├── docs │ ├── advanced-configuration.md │ ├── assets │ │ ├── comment-parsing.png │ │ ├── error-message-output.png │ │ ├── example-command.png │ │ └── slash-command-dispatch.png │ ├── examples.md │ ├── getting-started.md │ ├── updating.md │ └── workflow-dispatch.md │ ├── jest.config.js │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── command-helper.ts │ ├── github-helper.ts │ ├── main.ts │ ├── octokit-client.ts │ └── utils.ts │ └── tsconfig.json └── go └── build ├── Dockerfile ├── action.yml └── entrypoint.sh /.github/actions.yml: -------------------------------------------------------------------------------- 1 | codefresh/pipeline-creator: codefresh/pipeline-creator/** 2 | codefresh/pipeline-runner: codefresh/pipeline-runner/** 3 | github/auto-approve: github/auto-approve/** 4 | github/auto-assign: github/auto-assign/** 5 | github/auto-merge: github/auto-merge/** 6 | github/branch-cleanup: github/branch-cleanup/** 7 | github/create-or-update-comment: github/create-or-update-comment/** 8 | github/create-pull-request: github/create-pull-request/** 9 | github/git-push: github/git-push/** 10 | github/release-assets: github/release-assets/** 11 | github/repository-dispatch: github/repository-dispatch/** 12 | github/slash-command-dispatch: github/slash-command-dispatch/** 13 | go/build: go/build/** 14 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "npm": { 6 | "enabled": false 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.github/workflows/branch-cleanup.yml: -------------------------------------------------------------------------------- 1 | name: branch-cleanup 2 | on: 3 | pull_request: 4 | branches: 5 | - master 6 | jobs: 7 | cleanup-branch: 8 | name: Auto delete branch on merge 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: cloudposse/actions/github/branch-cleanup@0.28.0 12 | env: 13 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 14 | NO_BRANCH_DELETED_EXIT_CODE: 0 15 | -------------------------------------------------------------------------------- /.github/workflows/pr-opened-command.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request Opened Command 2 | on: 3 | repository_dispatch: 4 | types: [pr-opened-command] 5 | 6 | jobs: 7 | greet-contributor: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Pull Request Opened Command 11 | uses: actions/github-script@0.9.0 12 | with: 13 | github-token: ${{secrets.REPO_ACCESS_TOKEN}} 14 | script: | 15 | 16 | const issueNumber = ${{ github.event.client_payload.github.event.pull_request.number }}; 17 | const owner = '${{ github.event.client_payload.github.event.repository.owner.login }}'; 18 | const repo = '${{ github.event.client_payload.github.event.repository.name }}'; 19 | const creator = '${{ github.event.client_payload.github.event.pull_request.user.login }}'; 20 | 21 | // Greet contributor 22 | console.log('Executing Greet contributor script'); 23 | 24 | let firstTimeContributor = true; 25 | let message = ''; 26 | 27 | const opts = github.issues.listForRepo.endpoint.merge({ 28 | owner: owner, 29 | repo: repo, 30 | creator: creator, 31 | state: 'all' 32 | }); 33 | 34 | const issues = await github.paginate(opts); 35 | 36 | for (const issue of issues) { 37 | if (issue.number === issueNumber) { 38 | continue; 39 | } 40 | 41 | if (issue.pull_request) { 42 | firstTimeContributor = false; 43 | break; 44 | } 45 | } 46 | 47 | if (firstTimeContributor) { 48 | console.log('PR creator is a first time contributor'); 49 | 50 | message = ` 51 | Welcome @${creator} 52 | 53 | Thank you for submitting this PR! If you haven't already [joined our slack community](https://slack.sweetops.com), then we invite you to do so. 54 | 55 | We receive an overwhelming number of contributions. By joining our slack, we'll be able to review your PR faster. 56 | 57 | [![Slack Community](https://slack.cloudposse.com/badge.svg)](https://slack.cloudposse.com) 58 | `; 59 | } 60 | else { 61 | console.log('PR creator is already a contributor'); 62 | 63 | message = ` 64 | @${creator} thanks again for your contribution! 65 | 66 | We will review this PR shortly. 67 | `; 68 | } 69 | 70 | await github.issues.createComment({ 71 | issue_number: issueNumber, 72 | owner: owner, 73 | repo: repo, 74 | body: message 75 | }); 76 | 77 | // Assign reviewers 78 | console.log('Executing Assign reviewers script'); 79 | 80 | const all_reviewers = ['osterman', 'aknysh', 'goruha']; 81 | 82 | // Exclude the PR author from the reviewer list 83 | const reviewers = all_reviewers.filter((val) => val !== creator); 84 | 85 | await github.pulls.createReviewRequest({ 86 | pull_number: issueNumber, 87 | owner: owner, 88 | repo: repo, 89 | reviewers: reviewers 90 | }); 91 | 92 | await github.issues.addAssignees({ 93 | issue_number: issueNumber, 94 | owner: owner, 95 | repo: repo, 96 | assignees: [creator] 97 | }); 98 | -------------------------------------------------------------------------------- /.github/workflows/rebuild-readme-command.yml: -------------------------------------------------------------------------------- 1 | # TODO deprecate by adding a deprecation message back to PR 2 | name: Rebuild README 3 | on: 4 | repository_dispatch: 5 | types: [rebuild-readme-command] 6 | 7 | jobs: 8 | rebuild-readme: 9 | runs-on: ubuntu-latest 10 | steps: 11 | # Checkout the pull request branch 12 | - uses: actions/checkout@v2 13 | with: 14 | token: ${{ secrets.REPO_ACCESS_TOKEN }} 15 | repository: ${{ github.event.client_payload.pull_request.head.repo.full_name }} 16 | ref: ${{ github.event.client_payload.pull_request.head.ref }} 17 | 18 | # Rebuild README 19 | - name: Rebuild README 20 | shell: bash 21 | run: | 22 | make init 23 | make readme/deps 24 | make readme 25 | 26 | # Commit changes (if any) to the PR branch 27 | - name: Commit changes to the PR branch 28 | shell: bash 29 | run: | 30 | set -x 31 | output=$(git diff --name-only) 32 | 33 | if [ -n "$output" ]; then 34 | echo "Changes detected. Pushing to the PR branch" 35 | git config --global user.name 'actions-bot' 36 | git config --global user.email '58130806+actions-bot@users.noreply.github.com' 37 | git add -A 38 | git commit -m "Updated README.md" 39 | git push 40 | else 41 | echo "No changes detected" 42 | fi 43 | 44 | # Add reaction to the original comment 45 | - name: Add reaction to the original comment 46 | uses: cloudposse/actions/github/create-or-update-comment@0.28.0 47 | with: 48 | token: ${{ secrets.REPO_ACCESS_TOKEN }} 49 | repository: ${{ github.event.client_payload.github.payload.repository.full_name }} 50 | comment-id: ${{ github.event.client_payload.github.payload.comment.id }} 51 | reactions: hooray 52 | -------------------------------------------------------------------------------- /.github/workflows/terraform-fmt-command.yml: -------------------------------------------------------------------------------- 1 | # TODO deprecate by adding a deprecation message back to PR 2 | name: Format terraform files 3 | on: 4 | repository_dispatch: 5 | types: [terraform-fmt-command] 6 | 7 | jobs: 8 | terraform-fmt: 9 | runs-on: ubuntu-latest 10 | container: cloudposse/build-harness:slim-latest 11 | steps: 12 | # Checkout the pull request branch 13 | - uses: actions/checkout@v2 14 | with: 15 | token: ${{ secrets.REPO_ACCESS_TOKEN }} 16 | repository: ${{ github.event.client_payload.pull_request.head.repo.full_name }} 17 | ref: ${{ github.event.client_payload.pull_request.head.ref }} 18 | 19 | # Format Terraform files 20 | - name: Format Terraform files 21 | shell: bash 22 | run: /usr/bin/make BUILD_HARNESS_PATH=/build-harness PACKAGES_PREFER_HOST=true -f /build-harness/templates/Makefile.build-harness terraform/fmt 23 | 24 | # Commit changes (if any) to the PR branch 25 | - name: Commit changes to the PR branch 26 | shell: bash 27 | run: | 28 | set -x 29 | output=$(git diff --name-only) 30 | 31 | if [ -n "$output" ]; then 32 | echo "Changes detected. Pushing to the PR branch" 33 | git config --global user.name 'actions-bot' 34 | git config --global user.email '58130806+actions-bot@users.noreply.github.com' 35 | git add -A 36 | git commit -m "Executed 'terraform fmt'" 37 | git push 38 | else 39 | echo "No changes detected" 40 | fi 41 | 42 | # Add reaction to the original comment 43 | - name: Add reaction to the original comment 44 | uses: cloudposse/actions/github/create-or-update-comment@0.28.0 45 | with: 46 | token: ${{ secrets.REPO_ACCESS_TOKEN }} 47 | repository: ${{ github.event.client_payload.github.payload.repository.full_name }} 48 | comment-id: ${{ github.event.client_payload.github.payload.comment.id }} 49 | reactions: hooray 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ACTIONS = $(filter %/, $(sort $(wildcard github/*/ go/*/ codefresh/*/))) 2 | 3 | labeler: 4 | for action in $(ACTIONS); do \ 5 | echo "$${action%/}: $${action}**"; \ 6 | done > .github/actions.yml 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # actions [![Auto Assign](https://github.com/cloudposse/actions/workflows/Auto%20Assign/badge.svg)](https://github.com/cloudposse/actions/actions?query=workflow%3A%22Auto+Assign%22) [![Labeler](https://github.com/cloudposse/actions/workflows/Labeler/badge.svg)](https://github.com/cloudposse/actions/actions?query=workflow%3A%22Labeler%22) [![branch-cleanup](https://github.com/cloudposse/actions/workflows/branch-cleanup/badge.svg)](https://github.com/cloudposse/actions/actions?query=workflow%3A%22branch-cleanup%22) 2 | 3 | GitHub Actions 4 | -------------------------------------------------------------------------------- /codefresh/pipeline-creator/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM codefresh/cli:0.74.9 2 | 3 | LABEL maintainer="Cloud Posse " 4 | 5 | LABEL "com.github.actions.name"="Create Codefresh pipelines" 6 | LABEL "com.github.actions.description"="GitHub action that creates Codefresh pipelines" 7 | LABEL "com.github.actions.icon"="activity" 8 | LABEL "com.github.actions.color"="green" 9 | 10 | RUN apk add --no-cache \ 11 | gomplate \ 12 | git 13 | 14 | COPY entrypoint.sh / 15 | 16 | RUN chmod 755 /entrypoint.sh 17 | 18 | ENTRYPOINT ["/entrypoint.sh"] 19 | -------------------------------------------------------------------------------- /codefresh/pipeline-creator/README.md: -------------------------------------------------------------------------------- 1 | # Codefresh Pipeline Creator GitHub Action 2 | 3 | Automatically creates Codefresh pipelines from pipeline specs. 4 | 5 | 6 | ## Usage 7 | 8 | In your application repo, create a workflow file (e.g. `.github/workflows/codefresh.yml`) similar to this: 9 | 10 | ```yaml 11 | name: codefresh 12 | on: 13 | push: 14 | branches: 15 | - main 16 | paths: 17 | # When this file is merged to the default branch, then perform codefresh CRUD 18 | - '.github/workflows/codefresh.yml' 19 | # Synchronize pipelines with Codefresh nightly 20 | schedule: 21 | - cron: '0 0 * * *' 22 | 23 | jobs: 24 | pipeline-creator: 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: cloudposse/actions/codefresh/pipeline-creator@0.25.0 28 | with: 29 | # GitHub owner and repository name of the application repository 30 | repo: "${{ github.repository }}" 31 | # Codefresh project name to host the pipelines 32 | cf_project: "${{ github.event.repository.name }}" 33 | # URL of the repository that contains Codefresh pipelines and pipeline specs 34 | cf_repo_url: "https://github.com/cloudposse/codefresh.git" 35 | # Version of the repository that contains Codefresh pipelines and pipeline specs 36 | cf_repo_version: "0.1.0" 37 | # Pipeline spec type (microservice, spa, serverless) 38 | cf_spec_type: "microservice" 39 | # A comma separated list of pipeline specs to create the pipelines from 40 | cf_specs: "preview,build,deploy,release,destroy" 41 | env: 42 | GITHUB_USER: "xxxxxxxxx-bot" 43 | # Global organization secrets 44 | GITHUB_TOKEN: "${{ secrets.CF_GITHUB_TOKEN }}" 45 | CF_API_KEY: "${{ secrets.CF_API_KEY }}" 46 | ``` 47 | 48 | 49 | ## Why? 50 | 51 | - Every application that uses Codefresh as a CI/CD platform, needs to define a set of Codefresh pipelines 52 | (e.g. `build`, `deploy`, `release`, `destroy`), 53 | which are usually different for different types of applications (`microservice`, `SPA`, `severless`, etc.). 54 | 55 | 56 | - Instead of copying the same Codefresh pipeline definitions into every application (which would require maintaining them in many places), 57 | and creating the pipelines manually in the Codefresh UI, 58 | define the pipeline steps and pipeline specs in your company's centralized catalog of Codefresh pipelines 59 | (e.g. in `https://github.com/my-company/codefresh` repo), and just copy one 60 | file with the GitHub Actions workflow described above into your application. 61 | 62 | 63 | - The workflow calls the `pipeline-creator` action, which combines the pipeline steps with pipeline spec templates 64 | into the final specs, and provisions the pipelines in Codefresh automatically. 65 | -------------------------------------------------------------------------------- /codefresh/pipeline-creator/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Codefresh pipelines creator' 2 | description: 'GitHub Action that creates Codefresh pipelines from templated spec files' 3 | author: 'Cloud Posse ' 4 | runs: 5 | using: 'docker' 6 | image: 'Dockerfile' 7 | branding: 8 | icon: 'activity' 9 | color: 'green' 10 | inputs: 11 | repo: 12 | description: 'GitHub owner and repository name of the application repository.' 13 | required: true 14 | cf_project: 15 | description: 'Codefresh project name to host the pipelines.' 16 | required: true 17 | cf_repo_url: 18 | description: 'URL of the repository that contains Codefresh pipelines and pipeline specs.' 19 | required: true 20 | cf_repo_version: 21 | description: 'Version of the repository that contains Codefresh pipelines and pipeline specs.' 22 | required: true 23 | cf_spec_type: 24 | description: 'Pipeline spec type (microservice, spa, serverless).' 25 | required: true 26 | cf_specs: 27 | description: 'A comma separated list of pipeline specs to create the pipelines from.' 28 | required: true 29 | -------------------------------------------------------------------------------- /codefresh/pipeline-creator/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -o pipefail 4 | 5 | export PROJECT="${INPUT_CF_PROJECT}" 6 | export REPO="${INPUT_REPO}" 7 | 8 | NETRC="$HOME/.netrc" 9 | printf "machine github.com\n" > "$NETRC" 10 | printf "login %s\n" "$GITHUB_USER" >> "$NETRC" 11 | printf "password %s\n" "$GITHUB_TOKEN" >> "$NETRC" 12 | 13 | echo "Cloning repo ${INPUT_CF_REPO_URL} version ${INPUT_CF_REPO_VERSION}" 14 | git clone -c advice.detachedHead=false --depth=1 -b "${INPUT_CF_REPO_VERSION}" "${INPUT_CF_REPO_URL}" codefresh 15 | cd ./codefresh 16 | 17 | for spec in $(tr , ' ' <<<"${INPUT_CF_SPECS}"); do 18 | echo "Updating '${spec}' pipeline..." 19 | input_pipeline_file=./pipelines/"${INPUT_CF_SPEC_TYPE}"/"${spec}".yaml 20 | input_spec_file=./specs/"${INPUT_CF_SPEC_TYPE}"/"${spec}".yaml 21 | output_file=./specs/"${INPUT_CF_SPEC_TYPE}"/"${spec}"-rendered.yaml 22 | gomplate -d pipeline="$input_pipeline_file" -f "$input_spec_file" -o "$output_file" 23 | codefresh get project "${INPUT_CF_PROJECT}" || codefresh create project "${INPUT_CF_PROJECT}" 24 | codefresh replace pipeline -f "$output_file" || codefresh create pipeline -f "$output_file" 25 | done 26 | -------------------------------------------------------------------------------- /codefresh/pipeline-runner/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM codefresh/cli:0.74.9 2 | 3 | LABEL maintainer="Cloud Posse " 4 | 5 | LABEL "com.github.actions.name"="Run Codefresh pipeline" 6 | LABEL "com.github.actions.description"="GitHub action that runs Codefresh pipeline" 7 | LABEL "com.github.actions.icon"="activity" 8 | LABEL "com.github.actions.color"="green" 9 | 10 | COPY entrypoint.sh / 11 | 12 | RUN chmod 755 /entrypoint.sh 13 | 14 | ENTRYPOINT ["/entrypoint.sh"] 15 | -------------------------------------------------------------------------------- /codefresh/pipeline-runner/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Run Codefresh pipeline' 2 | description: 'GitHub action that runs Codefresh pipeline' 3 | author: 'Cloud Posse ' 4 | runs: 5 | using: 'docker' 6 | image: 'Dockerfile' 7 | branding: 8 | icon: 'activity' 9 | color: 'green' 10 | -------------------------------------------------------------------------------- /codefresh/pipeline-runner/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -o pipefail 4 | 5 | codefresh auth create-context context 6 | codefresh auth use-contex context 7 | 8 | if [ -n "$TRIGGER_NAME" ]; then 9 | codefresh run "$PIPELINE_NAME" --trigger="$TRIGGER_NAME" --branch="$BRANCH" 10 | else 11 | codefresh run "$PIPELINE_NAME" --branch="$BRANCH" 12 | fi 13 | -------------------------------------------------------------------------------- /github/Makefile: -------------------------------------------------------------------------------- 1 | all: branch-cleanup auto-approve auto-merge auto-assign create-pull-request slash-command-dispatch repository-dispatch create-or-update-comment 2 | 3 | clone(%): 4 | rm -rf $% 5 | git clone $(REPO) $% 6 | git -c advice.detachedHead=false -C $% checkout $(REF) 7 | rm -rf $%/.git 8 | 9 | .PHONY : branch-cleanup 10 | branch-cleanup: REPO=git@github.com:jessfraz/branch-cleanup-action.git 11 | branch-cleanup: REF=150e4a88d2d9838299a348c631cfc3ad2ea8560b 12 | branch-cleanup: clone(branch-cleanup) 13 | 14 | .PHONY : auto-approve 15 | auto-approve: REPO=git@github.com:hmarr/auto-approve-action.git 16 | auto-approve: REF=v3.2.0 17 | auto-approve: clone(auto-approve) 18 | 19 | .PHONY : auto-merge 20 | auto-merge: REPO=git@github.com:pascalgn/automerge-action.git 21 | auto-merge: REF=v0.15.6 22 | auto-merge: clone(auto-merge) 23 | 24 | .PHONY : auto-assign 25 | auto-assign: REPO=git@github.com:kentaro-m/auto-assign-action.git 26 | auto-assign: REF=v1.2.5 27 | auto-assign: clone(auto-assign) 28 | 29 | .PHONY : create-pull-request 30 | create-pull-request: REPO=git@github.com:peter-evans/create-pull-request.git 31 | create-pull-request: REF=v4.2.3 32 | create-pull-request: clone(create-pull-request) 33 | 34 | .PHONY : slash-command-dispatch 35 | slash-command-dispatch: REPO=git@github.com:peter-evans/slash-command-dispatch.git 36 | slash-command-dispatch: REF=v3.0.1 37 | slash-command-dispatch: clone(slash-command-dispatch) 38 | 39 | .PHONY : repository-dispatch 40 | repository-dispatch: REPO=git@github.com:peter-evans/repository-dispatch.git 41 | repository-dispatch: REF=v2.1.1 42 | repository-dispatch: clone(repository-dispatch) 43 | 44 | .PHONY : create-or-update-comment 45 | create-or-update-comment: REPO=git@github.com:peter-evans/create-or-update-comment.git 46 | create-or-update-comment: REF=v2.1.1 47 | create-or-update-comment: clone(create-or-update-comment) 48 | -------------------------------------------------------------------------------- /github/auto-approve/.gitattributes: -------------------------------------------------------------------------------- 1 | dist/index.js linguist-generated=true 2 | -------------------------------------------------------------------------------- /github/auto-approve/.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | test: 6 | name: Lint and test 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Check out code 10 | uses: actions/checkout@v3 11 | 12 | - name: Setup nodejs 13 | uses: actions/setup-node@v3 14 | with: 15 | node-version: '18' 16 | 17 | - name: Install dependencies 18 | run: npm ci 19 | 20 | - name: Check style with prettier 21 | run: npm run format-check 22 | 23 | - name: Run tests 24 | run: npm test 25 | 26 | - name: Compare the expected and actual dist/ directories 27 | run: | 28 | npm run build 29 | if [ "$(git diff --ignore-blank-lines --ignore-space-at-eol dist/ | wc -l)" -gt "0" ]; then 30 | echo "Detected uncommitted changes after build. See status below:" 31 | git diff 32 | exit 1 33 | fi 34 | -------------------------------------------------------------------------------- /github/auto-approve/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | -------------------------------------------------------------------------------- /github/auto-approve/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[typescript]": { 3 | "editor.defaultFormatter": "esbenp.prettier-vscode" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /github/auto-approve/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Harry Marr 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /github/auto-approve/README.md: -------------------------------------------------------------------------------- 1 | # Auto Approve GitHub Action 2 | 3 | [![CI](https://github.com/hmarr/auto-approve-action/actions/workflows/ci.yml/badge.svg?event=push)](https://github.com/hmarr/auto-approve-action/actions/workflows/ci.yml) 4 | 5 | **Name:** `hmarr/auto-approve-action` 6 | 7 | Automatically approve GitHub pull requests. 8 | 9 | **Important:** use v3 or later, as v2 uses Node.js 12, which is deprecated. If you're on an old version of GHES (earlier than 3.4) you may need to use v2 until you can upgrade. v1 was designed for the initial GitHub Actions beta, and no longer works. 10 | 11 | ## Usage instructions 12 | 13 | Create a workflow file (e.g. `.github/workflows/auto-approve.yml`) that contains a step that `uses: hmarr/auto-approve-action@v3`. Here's an example workflow file: 14 | 15 | ```yaml 16 | name: Auto approve 17 | on: pull_request_target 18 | 19 | jobs: 20 | build: 21 | runs-on: ubuntu-latest 22 | permissions: 23 | pull-requests: write 24 | steps: 25 | - uses: hmarr/auto-approve-action@v3 26 | ``` 27 | 28 | Combine with an `if` clause to only auto-approve certain users. For example, to auto-approve [Dependabot][dependabot] pull requests, use: 29 | 30 | ```yaml 31 | name: Auto approve 32 | 33 | on: pull_request_target 34 | 35 | jobs: 36 | auto-approve: 37 | runs-on: ubuntu-latest 38 | permissions: 39 | pull-requests: write 40 | if: github.actor == 'dependabot[bot]' 41 | steps: 42 | - uses: hmarr/auto-approve-action@v3 43 | ``` 44 | 45 | If you want to use this action from a workflow file that doesn't run on the `pull_request` or `pull_request_target` events, use the `pull-request-number` input: 46 | 47 | ```yaml 48 | name: Auto approve 49 | 50 | on: 51 | workflow_dispatch: 52 | inputs: pullRequestNumber 53 | description: Pull request number to auto-approve 54 | required: false 55 | 56 | jobs: 57 | auto-approve: 58 | runs-on: ubuntu-latest 59 | permissions: 60 | pull-requests: write 61 | steps: 62 | - uses: hmarr/auto-approve-action@v3 63 | with: 64 | pull-request-number: ${{ github.event.inputs.pullRequestNumber }} 65 | ``` 66 | 67 | Optionally, you can provide a message for the review: 68 | 69 | ```yaml 70 | name: Auto approve 71 | 72 | on: pull_request_target 73 | 74 | jobs: 75 | auto-approve: 76 | runs-on: ubuntu-latest 77 | permissions: 78 | pull-requests: write 79 | if: github.actor == 'dependabot[bot]' 80 | steps: 81 | - uses: hmarr/auto-approve-action@v3 82 | with: 83 | review-message: "Auto approved automated PR" 84 | ``` 85 | 86 | ### Approving on behalf of a different user 87 | 88 | By default, this will use the [automatic GitHub token](https://docs.github.com/en/actions/security-guides/automatic-token-authentication) that's provided to the workflow. This means the approval will come from the "github-actions" bot user. Make sure you enable the `pull-requests: write` permission in your workflow. 89 | 90 | To approve the pull request as a different user, pass a GitHub Personal Access Token into the `github-token` input. In order to approve the pull request, the token needs the `repo` scope enabled. 91 | 92 | ```yaml 93 | name: Auto approve 94 | 95 | on: pull_request_target 96 | 97 | jobs: 98 | auto-approve: 99 | runs-on: ubuntu-latest 100 | steps: 101 | - uses: hmarr/auto-approve-action@v3 102 | with: 103 | github-token: ${{ secrets.SOME_USERS_PAT }} 104 | ``` 105 | 106 | ### Approving Dependabot pull requests 107 | 108 | When a workflow is run in response to a Dependabot pull request using the `pull_request` event, the workflow won't have access to secrets. If you're trying to use a Personal Access Token (as above) but getting an error on Dependabot pull requests, this is probably why. 109 | 110 | Fortunately the fix is simple: use the `pull_request_target` event instead of `pull_request`. This runs the workflow in the context of the base branch of the pull request, which does have access to secrets. 111 | 112 | ## Why? 113 | 114 | GitHub lets you prevent merges of unapproved pull requests. However, it's occasionally useful to selectively circumvent this restriction - for instance, some people want Dependabot's automated pull requests to not require approval. 115 | 116 | [dependabot]: https://github.com/marketplace/dependabot 117 | 118 | ## Code owners 119 | 120 | If you're using a [CODEOWNERS file](https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners), you'll need to give this action a personal access token for a user listed as a code owner. Rather than using a real user's personal access token, you're probably better off creating a dedicated bot user, and adding it to a team which you assign as the code owner. That way you can restrict the bot user's permissions as much as possible, and your workflow won't break when people leave the team. 121 | 122 | ## Development and release process 123 | 124 | Each major version corresponds to a branch (e.g. `v2`, `v3`). The latest major version (`v3` at the time of writing) is the repository's default branch. Releases are tagged with semver-style version numbers (e.g. `v1.2.3`). 125 | -------------------------------------------------------------------------------- /github/auto-approve/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Auto Approve' 2 | description: 'Automatically approve pull requests' 3 | branding: 4 | icon: 'check-circle' 5 | color: 'green' 6 | inputs: 7 | github-token: 8 | default: ${{ github.token }} 9 | description: 'The GITHUB_TOKEN secret' 10 | required: false 11 | pull-request-number: 12 | description: '(optional) The ID of a pull request to auto-approve. By default, this action tries to use the pull_request event payload.' 13 | required: false 14 | review-message: 15 | description: '(optional) The message of the pull request review.' 16 | required: false 17 | runs: 18 | using: 'node16' 19 | main: 'dist/index.js' 20 | -------------------------------------------------------------------------------- /github/auto-approve/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | transform: { 4 | '^.+\\.(ts|tsx)?$': 'ts-jest' 5 | } 6 | } -------------------------------------------------------------------------------- /github/auto-approve/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "auto-approve-action", 3 | "version": "3.1.0", 4 | "description": "Automatically approve pull requests", 5 | "main": "dist/main.ts", 6 | "scripts": { 7 | "build": "ncc build src/main.ts", 8 | "format": "prettier --write **/*.ts", 9 | "format-check": "prettier --check **/*.ts", 10 | "test": "jest", 11 | "test:watch": "jest --watchAll" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/hmarr/auto-approve-action.git" 16 | }, 17 | "keywords": [ 18 | "actions" 19 | ], 20 | "author": "hmarr", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/hmarr/auto-approve-action/issues" 24 | }, 25 | "homepage": "https://github.com/hmarr/auto-approve-action#readme", 26 | "dependencies": { 27 | "@actions/core": "^1.9.1", 28 | "@actions/github": "^5.0.0" 29 | }, 30 | "devDependencies": { 31 | "@types/jest": "^27.4.1", 32 | "@types/node": "^17.0.23", 33 | "@vercel/ncc": "^0.33.3", 34 | "jest": "^27.5.1", 35 | "nock": "^13.2.4", 36 | "prettier": "^2.6.1", 37 | "ts-jest": "^27.1.4", 38 | "typescript": "^4.6.3" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /github/auto-approve/src/approve.ts: -------------------------------------------------------------------------------- 1 | import * as core from "@actions/core"; 2 | import * as github from "@actions/github"; 3 | import { RequestError } from "@octokit/request-error"; 4 | import { Context } from "@actions/github/lib/context"; 5 | import { GitHub } from "@actions/github/lib/utils"; 6 | 7 | export async function approve( 8 | token: string, 9 | context: Context, 10 | prNumber?: number, 11 | reviewMessage?: string 12 | ) { 13 | if (!prNumber) { 14 | prNumber = context.payload.pull_request?.number; 15 | } 16 | 17 | if (!prNumber) { 18 | core.setFailed( 19 | "Event payload missing `pull_request` key, and no `pull-request-number` provided as input." + 20 | "Make sure you're triggering this action on the `pull_request` or `pull_request_target` events." 21 | ); 22 | return; 23 | } 24 | 25 | const client = github.getOctokit(token); 26 | 27 | try { 28 | const { owner, repo } = context.repo; 29 | 30 | core.info(`Fetching user, pull request information, and existing reviews`); 31 | const [login, { data: pr }, { data: reviews }] = await Promise.all([ 32 | getLoginForToken(client), 33 | client.rest.pulls.get({ owner, repo, pull_number: prNumber }), 34 | client.rest.pulls.listReviews({ owner, repo, pull_number: prNumber }), 35 | ]); 36 | 37 | core.info(`Current user is ${login}`); 38 | 39 | const prHead = pr.head.sha; 40 | core.info(`Commit SHA is ${prHead}`); 41 | 42 | const alreadyReviewed = reviews.some( 43 | ({ user, state }) => user?.login === login && state === "APPROVED" 44 | ); 45 | const outstandingReviewRequest = pr.requested_reviewers?.some( 46 | (reviewer) => reviewer.login == login 47 | ); 48 | if (alreadyReviewed && !outstandingReviewRequest) { 49 | core.info( 50 | `Current user already approved pull request #${prNumber}, nothing to do` 51 | ); 52 | return; 53 | } 54 | 55 | core.info( 56 | `Pull request #${prNumber} has not been approved yet, creating approving review` 57 | ); 58 | await client.rest.pulls.createReview({ 59 | owner: context.repo.owner, 60 | repo: context.repo.repo, 61 | pull_number: prNumber, 62 | body: reviewMessage, 63 | event: "APPROVE", 64 | }); 65 | core.info(`Approved pull request #${prNumber}`); 66 | } catch (error) { 67 | if (error instanceof RequestError) { 68 | switch (error.status) { 69 | case 401: 70 | core.setFailed( 71 | `${error.message}. Please check that the \`github-token\` input ` + 72 | "parameter is set correctly." 73 | ); 74 | break; 75 | case 403: 76 | core.setFailed( 77 | `${error.message}. In some cases, the GitHub token used for actions triggered ` + 78 | "from `pull_request` events are read-only, which can cause this problem. " + 79 | "Switching to the `pull_request_target` event typically resolves this issue." 80 | ); 81 | break; 82 | case 404: 83 | core.setFailed( 84 | `${error.message}. This typically means the token you're using doesn't have ` + 85 | "access to this repository. Use the built-in `${{ secrets.GITHUB_TOKEN }}` token " + 86 | "or review the scopes assigned to your personal access token." 87 | ); 88 | break; 89 | case 422: 90 | core.setFailed( 91 | `${error.message}. This typically happens when you try to approve the pull ` + 92 | "request with the same user account that created the pull request. Try using " + 93 | "the built-in `${{ secrets.GITHUB_TOKEN }}` token, or if you're using a personal " + 94 | "access token, use one that belongs to a dedicated bot account." 95 | ); 96 | break; 97 | default: 98 | core.setFailed(`Error (code ${error.status}): ${error.message}`); 99 | } 100 | return; 101 | } 102 | 103 | if (error instanceof Error) { 104 | core.setFailed(error); 105 | } else { 106 | core.setFailed("Unknown error"); 107 | } 108 | return; 109 | } 110 | } 111 | 112 | async function getLoginForToken( 113 | client: InstanceType 114 | ): Promise { 115 | try { 116 | const { data: user } = await client.rest.users.getAuthenticated(); 117 | return user.login; 118 | } catch (error) { 119 | if (error instanceof RequestError) { 120 | // If you use the GITHUB_TOKEN provided by GitHub Actions to fetch the current user 121 | // you get a 403. For now we'll assume any 403 means this is an Actions token. 122 | if (error.status === 403) { 123 | return "github-actions[bot]"; 124 | } 125 | } 126 | throw error; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /github/auto-approve/src/main.test.ts: -------------------------------------------------------------------------------- 1 | import * as core from "@actions/core"; 2 | import * as github from "@actions/github"; 3 | import { Context } from "@actions/github/lib/context"; 4 | import nock from "nock"; 5 | import { approve } from "./approve"; 6 | import { run } from "./main"; 7 | 8 | jest.mock("./approve"); 9 | const mockedApprove = jest.mocked(approve, true); 10 | 11 | jest.mock("@actions/github"); 12 | const mockedGithub = jest.mocked(github, true); 13 | 14 | afterAll(() => { 15 | jest.unmock("./approve"); 16 | jest.unmock("@actions/github"); 17 | }); 18 | 19 | const originalEnv = process.env; 20 | 21 | beforeEach(() => { 22 | jest.restoreAllMocks(); 23 | mockedApprove.mockReset(); 24 | jest.spyOn(core, "setFailed").mockImplementation(jest.fn()); 25 | nock.disableNetConnect(); 26 | 27 | process.env = { 28 | GITHUB_REPOSITORY: "hmarr/test", 29 | "INPUT_GITHUB-TOKEN": "tok-xyz", 30 | }; 31 | }); 32 | 33 | afterEach(() => { 34 | nock.enableNetConnect(); 35 | process.env = originalEnv; 36 | }); 37 | 38 | test("passes the review message to approve", async () => { 39 | mockedGithub.context = ghContext(); 40 | process.env["INPUT_REVIEW-MESSAGE"] = "LGTM"; 41 | await run(); 42 | expect(mockedApprove).toHaveBeenCalledWith( 43 | "tok-xyz", 44 | expect.anything(), 45 | 101, 46 | "LGTM" 47 | ); 48 | }); 49 | 50 | test("calls approve when no PR number is provided", async () => { 51 | mockedGithub.context = ghContext(); 52 | await run(); 53 | expect(mockedApprove).toHaveBeenCalledWith( 54 | "tok-xyz", 55 | expect.anything(), 56 | 101, 57 | undefined 58 | ); 59 | }); 60 | 61 | test("calls approve when a valid PR number is provided", async () => { 62 | process.env["INPUT_PULL-REQUEST-NUMBER"] = "456"; 63 | await run(); 64 | expect(mockedApprove).toHaveBeenCalledWith( 65 | "tok-xyz", 66 | expect.anything(), 67 | 456, 68 | undefined 69 | ); 70 | }); 71 | 72 | test("errors when an invalid PR number is provided", async () => { 73 | process.env["INPUT_PULL-REQUEST-NUMBER"] = "not a number"; 74 | await run(); 75 | expect(mockedApprove).not.toHaveBeenCalled(); 76 | }); 77 | 78 | function ghContext(): Context { 79 | const ctx = new Context(); 80 | ctx.payload = { 81 | pull_request: { 82 | number: 101, 83 | }, 84 | }; 85 | return ctx; 86 | } 87 | -------------------------------------------------------------------------------- /github/auto-approve/src/main.ts: -------------------------------------------------------------------------------- 1 | import * as core from "@actions/core"; 2 | import * as github from "@actions/github"; 3 | import { approve } from "./approve"; 4 | 5 | export async function run() { 6 | try { 7 | const token = core.getInput("github-token"); 8 | const reviewMessage = core.getInput("review-message"); 9 | await approve( 10 | token, 11 | github.context, 12 | prNumber(), 13 | reviewMessage || undefined 14 | ); 15 | } catch (error) { 16 | if (error instanceof Error) { 17 | core.setFailed(error.message); 18 | } else { 19 | core.setFailed("Unknown error"); 20 | } 21 | } 22 | } 23 | 24 | function prNumber(): number { 25 | if (core.getInput("pull-request-number") !== "") { 26 | const prNumber = parseInt(core.getInput("pull-request-number"), 10); 27 | if (Number.isNaN(prNumber)) { 28 | throw new Error("Invalid `pull-request-number` value"); 29 | } 30 | return prNumber; 31 | } 32 | 33 | if (!github.context.payload.pull_request) { 34 | throw new Error( 35 | "This action must be run using a `pull_request` event or " + 36 | "have an explicit `pull-request-number` provided" 37 | ); 38 | } 39 | return github.context.payload.pull_request.number; 40 | } 41 | 42 | if (require.main === module) { 43 | run(); 44 | } 45 | -------------------------------------------------------------------------------- /github/auto-approve/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "outDir": "./lib", 6 | "rootDir": "./src", 7 | "strict": true, 8 | "noImplicitAny": false, 9 | "esModuleInterop": true 10 | }, 11 | "exclude": ["node_modules"] 12 | } 13 | -------------------------------------------------------------------------------- /github/auto-assign/.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: kentaro-m 2 | -------------------------------------------------------------------------------- /github/auto-assign/.github/auto_assign.yml: -------------------------------------------------------------------------------- 1 | # Set to true to add reviewers to pull requests 2 | addReviewers: false 3 | 4 | # Set to true to add assignees to pull requests 5 | addAssignees: author 6 | 7 | # A list of assignees, overrides reviewers if set 8 | assignees: 9 | - kentaro-m 10 | 11 | # A number of assignees to add to the pull request 12 | # Set to 0 to add all of the assignees. 13 | # Uses numberOfReviewers if unset. 14 | numberOfAssignees: 0 15 | 16 | # A list of keywords to be skipped the process that add reviewers if pull requests include it 17 | skipKeywords: 18 | - wip -------------------------------------------------------------------------------- /github/auto-assign/.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | template: | 2 | ## Changes 3 | 4 | $CHANGES -------------------------------------------------------------------------------- /github/auto-assign/.github/workflows/pull-request.yml: -------------------------------------------------------------------------------- 1 | name: 'Auto Assign' 2 | on: pull_request 3 | 4 | jobs: 5 | add-reviews: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: kentaro-m/auto-assign-action@v1.2.5 9 | -------------------------------------------------------------------------------- /github/auto-assign/.github/workflows/release-note.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | update_release_draft: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: release-drafter/release-drafter@v5 13 | env: 14 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /github/auto-assign/.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: 'Test' 2 | 3 | on: push 4 | 5 | jobs: 6 | build-and-test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | 11 | - name: setup node 12 | uses: actions/setup-node@v3 13 | with: 14 | node-version-file: ./.node-version 15 | 16 | - name: install 17 | run: npm install 18 | 19 | - name: build 20 | run: npm run build 21 | 22 | - name: test 23 | run: npm test 24 | 25 | - name: format check 26 | run: npm run format-check -------------------------------------------------------------------------------- /github/auto-assign/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib/ 3 | coverage/ 4 | .idea/ -------------------------------------------------------------------------------- /github/auto-assign/.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /github/auto-assign/.node-version: -------------------------------------------------------------------------------- 1 | 16.19.1 2 | -------------------------------------------------------------------------------- /github/auto-assign/.prettierrc.yml: -------------------------------------------------------------------------------- 1 | trailingComma: "es5" 2 | tabWidth: 2 3 | semi: false 4 | singleQuote: true -------------------------------------------------------------------------------- /github/auto-assign/LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Kentaro Matsushita 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /github/auto-assign/README.md: -------------------------------------------------------------------------------- 1 | # Auto Assign Action 2 | 3 | An action which adds reviewers to the pull request when the pull request is opened. 4 | 5 | ## :arrow_forward: Usage 6 | 7 | Create a workflow (e.g. `.github/workflows/action.yml` For more detail, refer to [Configuring a workflow](https://help.github.com/en/articles/configuring-a-workflow#creating-a-workflow-file)) for running the auto-assign action. 8 | 9 | ```yml 10 | name: 'Auto Assign' 11 | on: 12 | pull_request: 13 | types: [opened, ready_for_review] 14 | 15 | jobs: 16 | add-reviews: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: kentaro-m/auto-assign-action@v1.2.5 20 | with: 21 | configuration-path: '.github/some_name_for_configs.yml' # Only needed if you use something other than .github/auto_assign.yml 22 | ``` 23 | 24 | Change event that triggers a workflow to the `pull_request_target` if you want to enable the auto-assign action when opening pull requests from fork repositories or bots like Dependabot. 25 | 26 | Using dangerous misuse of the `pull_request_target` event can be a security risk, so make sure you understand pros and cons before using it. 27 | 28 | See below for details: 29 | 30 | - [Events that trigger workflows / Pull request target - GitHub Docs](https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#pull_request_target) 31 | - [Events that trigger workflows / Pull request events for forked repositories - GitHub Docs](https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#pull-request-events-for-forked-repositories) 32 | 33 | ```diff 34 | name: 'Auto Assign' 35 | on: 36 | - pull_request: 37 | + pull_request_target: 38 | types: [opened, ready_for_review] 39 | 40 | jobs: 41 | ``` 42 | 43 | Create a separate configuration file for the auto-assign action (e.g. `.github/auto_assign.yml`). 44 | 45 | ### Single Reviewers List 46 | 47 | Add reviewers/assignees to the pull request based on single reviewers list. 48 | 49 | ```yaml 50 | # Set to true to add reviewers to pull requests 51 | addReviewers: true 52 | 53 | # Set to true to add assignees to pull requests 54 | addAssignees: false 55 | 56 | # A list of reviewers to be added to pull requests (GitHub user name) 57 | reviewers: 58 | - reviewerA 59 | - reviewerB 60 | - reviewerC 61 | 62 | # A number of reviewers added to the pull request 63 | # Set 0 to add all the reviewers (default: 0) 64 | numberOfReviewers: 0 65 | # A list of assignees, overrides reviewers if set 66 | # assignees: 67 | # - assigneeA 68 | 69 | # A number of assignees to add to the pull request 70 | # Set to 0 to add all of the assignees. 71 | # Uses numberOfReviewers if unset. 72 | # numberOfAssignees: 2 73 | 74 | # A list of keywords to be skipped the process that add reviewers if pull requests include it 75 | # skipKeywords: 76 | # - wip 77 | ``` 78 | 79 | ### Multiple Reviewers List 80 | 81 | Add reviewers/assignees to the pull request based on multiple reviewers list. 82 | 83 | If you and peers work at the separate office or they work at the separate team by roles like frontend and backend, you might be good to use adding reviewers from each group. 84 | 85 | ```yaml 86 | # Set to true to add reviewers to pull requests 87 | addReviewers: true 88 | 89 | # Set to true to add assignees to pull requests 90 | addAssignees: false 91 | 92 | # A number of reviewers added to the pull request 93 | # Set 0 to add all the reviewers (default: 0) 94 | numberOfReviewers: 1 95 | 96 | # A number of assignees to add to the pull request 97 | # Set to 0 to add all of the assignees. 98 | # Uses numberOfReviewers if unset. 99 | # numberOfAssignees: 2 100 | 101 | # Set to true to add reviewers from different groups to pull requests 102 | useReviewGroups: true 103 | 104 | # A list of reviewers, split into different groups, to be added to pull requests (GitHub user name) 105 | reviewGroups: 106 | groupA: 107 | - reviewerA 108 | - reviewerB 109 | - reviewerC 110 | groupB: 111 | - reviewerD 112 | - reviewerE 113 | - reviewerF 114 | 115 | # Set to true to add assignees from different groups to pull requests 116 | useAssigneeGroups: false 117 | # A list of assignees, split into different froups, to be added to pull requests (GitHub user name) 118 | # assigneeGroups: 119 | # groupA: 120 | # - assigneeA 121 | # - assigneeB 122 | # - assigneeC 123 | # groupB: 124 | # - assigneeD 125 | # - assigneeE 126 | # - assigneeF 127 | 128 | # A list of keywords to be skipped the process that add reviewers if pull requests include it 129 | # skipKeywords: 130 | # - wip 131 | ``` 132 | 133 | ### Assign Author as Assignee 134 | 135 | Add the PR creator as the assignee of the pull request. 136 | 137 | ```yaml 138 | # Set addAssignees to 'author' to set the PR creator as the assignee. 139 | addAssignees: author 140 | ``` 141 | 142 | ### Filter by label 143 | 144 | The action will only run if the PR meets the specified filters 145 | 146 | ```yaml 147 | filterLabels: 148 | # Run 149 | include: 150 | - my_label 151 | - another_label 152 | # Not run 153 | exclude: 154 | - wip 155 | ``` 156 | 157 | ### Filter draft PRs 158 | 159 | The action will only run for non-draft PRs. If you want to run for all PRs, you need to enable it to run on drafts. 160 | 161 | ```yaml 162 | runOnDraft: true 163 | ``` 164 | 165 | ## :memo: Licence 166 | 167 | MIT 168 | -------------------------------------------------------------------------------- /github/auto-assign/__tests__/run.test.ts: -------------------------------------------------------------------------------- 1 | import { mocked } from 'jest-mock' 2 | import * as core from '@actions/core' 3 | import * as github from '@actions/github' 4 | import { run } from '../src/run' 5 | import * as utils from '../src/utils' 6 | import * as handler from '../src/handler' 7 | 8 | jest.mock('@actions/core') 9 | jest.mock('@actions/github') 10 | jest.mock('../src/utils') 11 | jest.mock('../src/handler') 12 | 13 | const mockedUtils = mocked(utils) 14 | const coreMocked = mocked(core) 15 | const mockedHandler = mocked(handler) 16 | 17 | describe.only('run', () => { 18 | beforeEach(() => { 19 | // @ts-ignore 20 | github.context = { 21 | eventName: '', 22 | workflow: '', 23 | action: '', 24 | actor: '', 25 | payload: { 26 | action: 'opened', 27 | number: '1', 28 | pull_request: { 29 | number: 1, 30 | title: 'test', 31 | user: { 32 | login: 'pr-creator', 33 | }, 34 | }, 35 | repository: { 36 | name: 'auto-assign', 37 | owner: { 38 | login: 'kentaro-m', 39 | }, 40 | }, 41 | }, 42 | repo: { 43 | owner: 'kentaro-m', 44 | repo: 'auto-assign', 45 | }, 46 | issue: { 47 | owner: 'kentaro-m', 48 | repo: 'auto-assign', 49 | number: 1, 50 | }, 51 | sha: '', 52 | ref: '', 53 | } 54 | }) 55 | 56 | test('succeeds the process', async () => { 57 | coreMocked.getInput.mockImplementation((name) => { 58 | switch (name) { 59 | case 'repo-token': 60 | return 'token' 61 | case 'configuration-path': 62 | return '.github/auto_assign.yml' 63 | default: 64 | return '' 65 | } 66 | }) 67 | 68 | mockedUtils.fetchConfigurationFile.mockImplementation(async () => ({ 69 | addAssignees: false, 70 | addReviewers: true, 71 | reviewers: ['reviewerA', 'reviewerB', 'reviewerC'], 72 | })) 73 | 74 | mockedHandler.handlePullRequest.mockImplementation(async () => {}) 75 | 76 | await run() 77 | 78 | expect(mockedHandler.handlePullRequest).toBeCalled() 79 | }) 80 | }) 81 | -------------------------------------------------------------------------------- /github/auto-assign/__tests__/tsconfig.jest.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "strictNullChecks": false 5 | } 6 | } -------------------------------------------------------------------------------- /github/auto-assign/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Auto Assign Action' 2 | description: 'Add reviewers to pull requests when pull requests are opened.' 3 | author: 'Kentaro Matsushita' 4 | inputs: 5 | repo-token: 6 | description: 'A token for the repo' 7 | default: ${{ github.token }} 8 | required: false 9 | configuration-path: 10 | description: 'A path for the auto-assign configuration' 11 | default: '.github/auto_assign.yml' 12 | runs: 13 | using: 'node16' 14 | main: 'dist/index.js' 15 | branding: 16 | icon: 'user-plus' 17 | color: 'blue' 18 | -------------------------------------------------------------------------------- /github/auto-assign/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | moduleFileExtensions: ['js', 'ts'], 4 | testEnvironment: 'node', 5 | testMatch: ['**/*.test.ts'], 6 | testRunner: 'jest-circus/runner', 7 | transform: { 8 | '^.+\\.ts$': 'ts-jest' 9 | }, 10 | verbose: true, 11 | transform: { 12 | '^.+\\.(ts|tsx)$': 'ts-jest' 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /github/auto-assign/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "auto-assign-action", 3 | "version": "1.2.5", 4 | "description": "Add reviewers to pull requests when pull requests are opened.", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/kentaro-m/auto-assign-action.git" 8 | }, 9 | "main": "lib/main.js", 10 | "scripts": { 11 | "build": "tsc", 12 | "test": "jest", 13 | "format": "prettier --write **/*.ts", 14 | "format-check": "prettier --check **/*.ts", 15 | "package": "ncc build --source-map --license licenses.txt", 16 | "prepare": "husky install" 17 | }, 18 | "author": "Kentaro Matsushita", 19 | "license": "MIT", 20 | "dependencies": { 21 | "@actions/core": "^1.6.0", 22 | "@actions/github": "^5.0.0", 23 | "js-yaml": "^3.13.1", 24 | "lodash": "^4.17.20" 25 | }, 26 | "devDependencies": { 27 | "@octokit/webhooks-types": "6.10.0", 28 | "@types/jest": "29.4.0", 29 | "@types/js-yaml": "3.12.1", 30 | "@types/lodash": "4.14.141", 31 | "@types/node": "12.7.8", 32 | "@vercel/ncc": "0.36.1", 33 | "husky": "8.0.3", 34 | "jest": "29.4.3", 35 | "jest-circus": "29.4.3", 36 | "jest-mock": "29.4.3", 37 | "lint-staged": "13.1.2", 38 | "prettier": "2.8.4", 39 | "ts-jest": "29.0.5", 40 | "typescript": "4.9.5" 41 | }, 42 | "lint-staged": { 43 | "*.ts": "prettier --write" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /github/auto-assign/renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "github>kentaro-m/renovate-config:default.json5" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /github/auto-assign/src/handler.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import { Context } from '@actions/github/lib/context' 3 | import * as utils from './utils' 4 | import { PullRequest } from './pull_request' 5 | import { Client } from './types' 6 | import { PullRequestEvent } from '@octokit/webhooks-types' 7 | 8 | export interface Config { 9 | addReviewers: boolean 10 | addAssignees: boolean | string 11 | reviewers: string[] 12 | assignees: string[] 13 | filterLabels?: { 14 | include?: string[] 15 | exclude?: string[] 16 | } 17 | numberOfAssignees: number 18 | numberOfReviewers: number 19 | skipKeywords: string[] 20 | useReviewGroups: boolean 21 | useAssigneeGroups: boolean 22 | reviewGroups: { [key: string]: string[] } 23 | assigneeGroups: { [key: string]: string[] } 24 | runOnDraft?: boolean 25 | } 26 | 27 | export async function handlePullRequest( 28 | client: Client, 29 | context: Context, 30 | config: Config 31 | ) { 32 | if (!context.payload.pull_request) { 33 | throw new Error('the webhook payload is not exist') 34 | } 35 | 36 | const { pull_request: event } = context.payload as PullRequestEvent 37 | const { title, draft, user, number } = event 38 | const { 39 | skipKeywords, 40 | useReviewGroups, 41 | useAssigneeGroups, 42 | reviewGroups, 43 | assigneeGroups, 44 | addReviewers, 45 | addAssignees, 46 | filterLabels, 47 | runOnDraft, 48 | } = config 49 | 50 | if (skipKeywords && utils.includesSkipKeywords(title, skipKeywords)) { 51 | core.info( 52 | 'Skips the process to add reviewers/assignees since PR title includes skip-keywords' 53 | ) 54 | return 55 | } 56 | if (!runOnDraft && draft) { 57 | core.info( 58 | 'Skips the process to add reviewers/assignees since PR type is draft' 59 | ) 60 | return 61 | } 62 | 63 | if (useReviewGroups && !reviewGroups) { 64 | throw new Error( 65 | "Error in configuration file to do with using review groups. Expected 'reviewGroups' variable to be set because the variable 'useReviewGroups' = true." 66 | ) 67 | } 68 | 69 | if (useAssigneeGroups && !assigneeGroups) { 70 | throw new Error( 71 | "Error in configuration file to do with using review groups. Expected 'assigneeGroups' variable to be set because the variable 'useAssigneeGroups' = true." 72 | ) 73 | } 74 | 75 | const owner = user.login 76 | const pr = new PullRequest(client, context) 77 | 78 | if (filterLabels !== undefined) { 79 | if (filterLabels.include !== undefined && filterLabels.include.length > 0) { 80 | const hasLabels = pr.hasAnyLabel(filterLabels.include) 81 | if (!hasLabels) { 82 | core.info( 83 | 'Skips the process to add reviewers/assignees since PR is not tagged with any of the filterLabels.include' 84 | ) 85 | return 86 | } 87 | } 88 | 89 | if (filterLabels.exclude !== undefined && filterLabels.exclude.length > 0) { 90 | const hasLabels = pr.hasAnyLabel(filterLabels.exclude) 91 | if (hasLabels) { 92 | core.info( 93 | 'Skips the process to add reviewers/assignees since PR is tagged with any of the filterLabels.exclude' 94 | ) 95 | return 96 | } 97 | } 98 | } 99 | 100 | if (addReviewers) { 101 | try { 102 | const reviewers = utils.chooseReviewers(owner, config) 103 | 104 | if (reviewers.length > 0) { 105 | await pr.addReviewers(reviewers) 106 | core.info(`Added reviewers to PR #${number}: ${reviewers.join(', ')}`) 107 | } 108 | } catch (error) { 109 | if (error instanceof Error) { 110 | core.warning(error.message) 111 | } 112 | } 113 | } 114 | 115 | if (addAssignees) { 116 | try { 117 | const assignees = utils.chooseAssignees(owner, config) 118 | 119 | if (assignees.length > 0) { 120 | await pr.addAssignees(assignees) 121 | core.info(`Added assignees to PR #${number}: ${assignees.join(', ')}`) 122 | } 123 | } catch (error) { 124 | if (error instanceof Error) { 125 | core.warning(error.message) 126 | } 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /github/auto-assign/src/main.ts: -------------------------------------------------------------------------------- 1 | import { run } from './run' 2 | 3 | run() 4 | -------------------------------------------------------------------------------- /github/auto-assign/src/pull_request.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import { Context } from '@actions/github/lib/context' 3 | import { Client } from './types' 4 | 5 | export class PullRequest { 6 | private client: Client 7 | private context: Context 8 | 9 | constructor(client: Client, context: Context) { 10 | this.client = client 11 | this.context = context 12 | } 13 | 14 | async addReviewers(reviewers: string[]): Promise { 15 | const { owner, repo, number: pull_number } = this.context.issue 16 | const result = await this.client.rest.pulls.requestReviewers({ 17 | owner, 18 | repo, 19 | pull_number, 20 | reviewers, 21 | }) 22 | core.debug(JSON.stringify(result)) 23 | } 24 | 25 | async addAssignees(assignees: string[]): Promise { 26 | const { owner, repo, number: issue_number } = this.context.issue 27 | const result = await this.client.rest.issues.addAssignees({ 28 | owner, 29 | repo, 30 | issue_number, 31 | assignees, 32 | }) 33 | core.debug(JSON.stringify(result)) 34 | } 35 | 36 | hasAnyLabel(labels: string[]): boolean { 37 | if (!this.context.payload.pull_request) { 38 | return false 39 | } 40 | const { labels: pullRequestLabels = [] } = this.context.payload.pull_request 41 | return pullRequestLabels.some((label) => labels.includes(label.name)) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /github/auto-assign/src/run.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import * as github from '@actions/github' 3 | import * as utils from './utils' 4 | import * as handler from './handler' 5 | 6 | export async function run() { 7 | try { 8 | const token = core.getInput('repo-token', { required: true }) 9 | const configPath = core.getInput('configuration-path', { 10 | required: true, 11 | }) 12 | 13 | const client = github.getOctokit(token) 14 | const { repo, sha } = github.context 15 | const config = await utils.fetchConfigurationFile(client, { 16 | owner: repo.owner, 17 | repo: repo.repo, 18 | path: configPath, 19 | ref: sha, 20 | }) 21 | 22 | await handler.handlePullRequest(client, github.context, config) 23 | } catch (error) { 24 | if (error instanceof Error) { 25 | core.setFailed(error.message) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /github/auto-assign/src/types.ts: -------------------------------------------------------------------------------- 1 | import * as github from '@actions/github' 2 | 3 | export type Client = ReturnType 4 | -------------------------------------------------------------------------------- /github/auto-assign/src/utils.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import * as yaml from 'js-yaml' 3 | import { Config } from './handler' 4 | import { Client } from './types' 5 | 6 | export function chooseReviewers(owner: string, config: Config): string[] { 7 | const { useReviewGroups, reviewGroups, numberOfReviewers, reviewers } = config 8 | let chosenReviewers: string[] = [] 9 | const useGroups: boolean = 10 | useReviewGroups && Object.keys(reviewGroups).length > 0 11 | 12 | if (useGroups) { 13 | chosenReviewers = chooseUsersFromGroups( 14 | owner, 15 | reviewGroups, 16 | numberOfReviewers 17 | ) 18 | } else { 19 | chosenReviewers = chooseUsers(reviewers, numberOfReviewers, owner) 20 | } 21 | 22 | return chosenReviewers 23 | } 24 | 25 | export function chooseAssignees(owner: string, config: Config): string[] { 26 | const { 27 | useAssigneeGroups, 28 | assigneeGroups, 29 | addAssignees, 30 | numberOfAssignees, 31 | numberOfReviewers, 32 | assignees, 33 | reviewers, 34 | } = config 35 | let chosenAssignees: string[] = [] 36 | 37 | const useGroups: boolean = 38 | useAssigneeGroups && Object.keys(assigneeGroups).length > 0 39 | 40 | if (typeof addAssignees === 'string') { 41 | if (addAssignees !== 'author') { 42 | throw new Error( 43 | "Error in configuration file to do with using addAssignees. Expected 'addAssignees' variable to be either boolean or 'author'" 44 | ) 45 | } 46 | chosenAssignees = [owner] 47 | } else if (useGroups) { 48 | chosenAssignees = chooseUsersFromGroups( 49 | owner, 50 | assigneeGroups, 51 | numberOfAssignees || numberOfReviewers 52 | ) 53 | } else { 54 | const candidates = assignees ? assignees : reviewers 55 | chosenAssignees = chooseUsers( 56 | candidates, 57 | numberOfAssignees || numberOfReviewers, 58 | owner 59 | ) 60 | } 61 | 62 | return chosenAssignees 63 | } 64 | 65 | export function chooseUsers( 66 | candidates: string[], 67 | desiredNumber: number, 68 | filterUser: string = '' 69 | ): string[] { 70 | const filteredCandidates = candidates.filter((reviewer: string): boolean => { 71 | return reviewer.toLowerCase() !== filterUser.toLowerCase() 72 | }) 73 | 74 | // all-assign 75 | if (desiredNumber === 0) { 76 | return filteredCandidates 77 | } 78 | 79 | return _.sampleSize(filteredCandidates, desiredNumber) 80 | } 81 | 82 | export function includesSkipKeywords( 83 | title: string, 84 | skipKeywords: string[] 85 | ): boolean { 86 | for (const skipKeyword of skipKeywords) { 87 | if (title.toLowerCase().includes(skipKeyword.toLowerCase()) === true) { 88 | return true 89 | } 90 | } 91 | 92 | return false 93 | } 94 | 95 | export function chooseUsersFromGroups( 96 | owner: string, 97 | groups: { [key: string]: string[] } | undefined, 98 | desiredNumber: number 99 | ): string[] { 100 | let users: string[] = [] 101 | for (const group in groups) { 102 | users = users.concat(chooseUsers(groups[group], desiredNumber, owner)) 103 | } 104 | return users 105 | } 106 | 107 | export async function fetchConfigurationFile(client: Client, options) { 108 | const { owner, repo, path, ref } = options 109 | const result = await client.rest.repos.getContent({ 110 | owner, 111 | repo, 112 | path, 113 | ref, 114 | }) 115 | 116 | const data: any = result.data 117 | 118 | if (!data.content) { 119 | throw new Error('the configuration file is not found') 120 | } 121 | 122 | const configString = Buffer.from(data.content, 'base64').toString() 123 | const config = yaml.safeLoad(configString) 124 | 125 | return config 126 | } 127 | -------------------------------------------------------------------------------- /github/auto-assign/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "outDir": "./lib", 6 | "rootDir": "./src", 7 | "strict": true, 8 | "noImplicitAny": false, 9 | "esModuleInterop": true 10 | }, 11 | "exclude": ["node_modules", "**/*.test.ts"] 12 | } -------------------------------------------------------------------------------- /github/auto-merge/.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: cimg/node:16.13.1 6 | steps: 7 | - checkout 8 | - run: 9 | name: yarn 10 | command: yarn 11 | -------------------------------------------------------------------------------- /github/auto-merge/.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | -------------------------------------------------------------------------------- /github/auto-merge/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 2017, 4 | "sourceType": "module" 5 | }, 6 | "extends": ["eslint:recommended"], 7 | "plugins": ["jest"], 8 | "env": { 9 | "node": true, 10 | "es6": true, 11 | "jest/globals": true 12 | }, 13 | "rules": { 14 | "semi": "error", 15 | "no-tabs": "error", 16 | "no-console": "off" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /github/auto-merge/.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .env 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /github/auto-merge/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright 2019 Pascal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /github/auto-merge/action.yml: -------------------------------------------------------------------------------- 1 | name: "Merge pull requests (automerge-action)" 2 | description: "Automatically merge pull requests that are ready" 3 | runs: 4 | using: "node16" 5 | main: "dist/index.js" 6 | branding: 7 | icon: "git-pull-request" 8 | color: "blue" 9 | -------------------------------------------------------------------------------- /github/auto-merge/bin/automerge.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const process = require("process"); 4 | 5 | const fse = require("fs-extra"); 6 | const { ArgumentParser } = require("argparse"); 7 | const { Octokit } = require("@octokit/rest"); 8 | 9 | const { ClientError, logger, createConfig } = require("../lib/common"); 10 | const { executeLocally, executeGitHubAction } = require("../lib/api"); 11 | 12 | const pkg = require("../package.json"); 13 | 14 | const OLD_CONFIG = [ 15 | "MERGE_LABEL", 16 | "UPDATE_LABEL", 17 | "LABELS", 18 | "AUTOMERGE", 19 | "AUTOREBASE", 20 | "COMMIT_MESSAGE_TEMPLATE", 21 | "TOKEN" 22 | ]; 23 | 24 | async function main() { 25 | const parser = new ArgumentParser({ 26 | prog: pkg.name, 27 | add_help: true, 28 | description: pkg.description 29 | }); 30 | parser.add_argument("-v", "--version", { 31 | action: "version", 32 | version: pkg.version, 33 | help: "Show version number and exit" 34 | }); 35 | parser.add_argument("url", { 36 | metavar: "", 37 | nargs: "?", 38 | help: "GitHub URL to process instead of environment variables" 39 | }); 40 | 41 | const args = parser.parse_args(); 42 | 43 | if (process.env.LOG === "TRACE") { 44 | logger.level = "trace"; 45 | } else if (process.env.LOG === "DEBUG") { 46 | logger.level = "debug"; 47 | } else if (process.env.LOG && process.env.LOG.length > 0) { 48 | logger.error("Invalid log level:", process.env.LOG); 49 | } 50 | 51 | checkOldConfig(); 52 | 53 | const token = env("GITHUB_TOKEN"); 54 | 55 | const octokit = new Octokit({ 56 | auth: `token ${token}`, 57 | userAgent: "pascalgn/automerge-action" 58 | }); 59 | 60 | const config = createConfig(process.env); 61 | logger.debug("Configuration:", config); 62 | 63 | const context = { token, octokit, config }; 64 | 65 | if (args.url) { 66 | await executeLocally(context, args.url); 67 | } else { 68 | const eventPath = env("GITHUB_EVENT_PATH"); 69 | const eventName = env("GITHUB_EVENT_NAME"); 70 | 71 | const eventDataStr = await fse.readFile(eventPath, "utf8"); 72 | const eventData = JSON.parse(eventDataStr); 73 | 74 | await executeGitHubAction(context, eventName, eventData); 75 | } 76 | } 77 | 78 | function checkOldConfig() { 79 | let error = false; 80 | for (const old of OLD_CONFIG) { 81 | if (process.env[old] != null) { 82 | logger.error("Old configuration option present:", old); 83 | error = true; 84 | } 85 | } 86 | if (error) { 87 | logger.error( 88 | "You have passed configuration options that were used by an old " + 89 | "version of this action. Please see " + 90 | "https://github.com/pascalgn/automerge-action for the latest " + 91 | "documentation of the configuration options!" 92 | ); 93 | throw new Error(`old configuration present!`); 94 | } 95 | } 96 | 97 | function env(name) { 98 | const val = process.env[name]; 99 | if (!val || !val.length) { 100 | throw new ClientError(`environment variable ${name} not set!`); 101 | } 102 | return val; 103 | } 104 | 105 | if (require.main === module) { 106 | main().catch(e => { 107 | if (e instanceof ClientError) { 108 | process.exitCode = 2; 109 | logger.error(e); 110 | } else { 111 | process.exitCode = 1; 112 | logger.error(e); 113 | } 114 | }); 115 | } 116 | -------------------------------------------------------------------------------- /github/auto-merge/it/it.js: -------------------------------------------------------------------------------- 1 | const { Octokit } = require("@octokit/rest"); 2 | 3 | const { executeLocally } = require("../lib/api"); 4 | const { createConfig } = require("../lib/common"); 5 | 6 | async function main() { 7 | require("dotenv").config(); 8 | 9 | const token = process.env.GITHUB_TOKEN; 10 | 11 | const octokit = new Octokit({ 12 | auth: `token ${token}`, 13 | userAgent: "pascalgn/automerge-action-it" 14 | }); 15 | 16 | const config = createConfig({ 17 | UPDATE_LABELS: "it-update", 18 | MERGE_LABELS: "it-merge", 19 | MERGE_REQUIRED_APPROVALS: "0", 20 | MERGE_REMOVE_LABELS: "it-merge", 21 | MERGE_RETRIES: "3", 22 | MERGE_RETRY_SLEEP: "2000", 23 | MERGE_ERROR_FAIL: "true" 24 | }); 25 | 26 | const context = { token, octokit, config }; 27 | 28 | await executeLocally(context, process.env.URL); 29 | } 30 | 31 | main().catch(e => { 32 | process.exitCode = 1; 33 | console.error(e); 34 | }); 35 | -------------------------------------------------------------------------------- /github/auto-merge/lib/git.js: -------------------------------------------------------------------------------- 1 | const { spawn } = require("child_process"); 2 | 3 | const { TimeoutError, logger } = require("./common"); 4 | 5 | class ExitError extends Error { 6 | constructor(message, code) { 7 | super(message); 8 | this.code = code; 9 | } 10 | } 11 | 12 | const FETCH_DEPTH = 10; 13 | 14 | const COMMON_ARGS = [ 15 | "-c", 16 | "user.name=GitHub", 17 | "-c", 18 | "user.email=noreply@github.com" 19 | ]; 20 | 21 | function git(cwd, ...args) { 22 | const stdio = [ 23 | "ignore", 24 | "pipe", 25 | logger.level === "trace" || logger.level === "debug" ? "inherit" : "ignore" 26 | ]; 27 | // the URL passed to the clone command could contain a password! 28 | const command = args.includes("clone") 29 | ? "git clone" 30 | : `git ${args.join(" ")}`; 31 | logger.debug("Executing", command); 32 | return new Promise((resolve, reject) => { 33 | const proc = spawn( 34 | "git", 35 | COMMON_ARGS.concat(args.filter(a => a !== null)), 36 | { cwd, stdio } 37 | ); 38 | const buffers = []; 39 | proc.stdout.on("data", data => buffers.push(data)); 40 | proc.on("error", () => { 41 | reject(new Error(`command failed: ${command}`)); 42 | }); 43 | proc.on("exit", code => { 44 | if (code === 0) { 45 | const data = Buffer.concat(buffers); 46 | resolve(data.toString("utf8").trim()); 47 | } else { 48 | reject( 49 | new ExitError(`command failed with code ${code}: ${command}`, code) 50 | ); 51 | } 52 | }); 53 | }); 54 | } 55 | 56 | async function clone(from, to, branch) { 57 | await git( 58 | ".", 59 | "clone", 60 | "--quiet", 61 | "--shallow-submodules", 62 | "--branch", 63 | branch, 64 | "--depth", 65 | FETCH_DEPTH, 66 | from, 67 | to 68 | ); 69 | } 70 | 71 | async function fetch(dir, branch) { 72 | await git( 73 | dir, 74 | "fetch", 75 | "--quiet", 76 | "--depth", 77 | FETCH_DEPTH, 78 | "origin", 79 | `${branch}:refs/remotes/origin/${branch}` 80 | ); 81 | } 82 | 83 | async function fetchUntilMergeBase(dir, branch, timeout) { 84 | const maxTime = new Date().getTime() + timeout; 85 | const ref = `refs/remotes/origin/${branch}`; 86 | while (new Date().getTime() < maxTime) { 87 | const base = await mergeBase(dir, "HEAD", ref); 88 | if (base) { 89 | const bases = [base]; 90 | const parents = await mergeCommits(dir, ref); 91 | let fetchMore = false; 92 | for (const parent of parents.flat()) { 93 | const b = await mergeBase(dir, parent, ref); 94 | if (b) { 95 | if (!bases.includes(b)) { 96 | bases.push(b); 97 | } 98 | } else { 99 | // we found a commit which does not have a common ancestor with 100 | // the branch we want to merge, so we need to fetch more 101 | fetchMore = true; 102 | break; 103 | } 104 | } 105 | if (!fetchMore) { 106 | const commonBase = await mergeBase(dir, ...bases); 107 | if (!commonBase) { 108 | throw new Error(`failed to find common base for ${bases}`); 109 | } 110 | return commonBase; 111 | } 112 | } 113 | await fetchDeepen(dir); 114 | } 115 | throw new TimeoutError(); 116 | } 117 | 118 | async function fetchDeepen(dir) { 119 | await git(dir, "fetch", "--quiet", "--deepen", FETCH_DEPTH); 120 | } 121 | 122 | async function mergeBase(dir, ...refs) { 123 | if (refs.length === 1) { 124 | return refs[0]; 125 | } else if (refs.length < 1) { 126 | throw new Error("empty refs!"); 127 | } 128 | let todo = refs; 129 | try { 130 | while (todo.length > 1) { 131 | const base = await git(dir, "merge-base", todo[0], todo[1]); 132 | todo = [base].concat(todo.slice(2)); 133 | } 134 | return todo[0]; 135 | } catch (e) { 136 | if (e instanceof ExitError && e.code === 1) { 137 | return null; 138 | } else { 139 | throw e; 140 | } 141 | } 142 | } 143 | 144 | async function mergeCommits(dir, ref) { 145 | return (await git(dir, "rev-list", "--parents", `${ref}..HEAD`)) 146 | .split(/\n/g) 147 | .map(line => line.split(/ /g).slice(1)) 148 | .filter(commit => commit.length > 1); 149 | } 150 | 151 | async function head(dir) { 152 | return await git(dir, "show-ref", "--head", "-s", "/HEAD"); 153 | } 154 | 155 | async function sha(dir, branch) { 156 | return await git(dir, "show-ref", "-s", `refs/remotes/origin/${branch}`); 157 | } 158 | 159 | async function rebase(dir, branch) { 160 | return await git(dir, "rebase", "--quiet", "--autosquash", branch); 161 | } 162 | 163 | async function push(dir, force, branch) { 164 | return await git( 165 | dir, 166 | "push", 167 | "--quiet", 168 | force ? "--force-with-lease" : null, 169 | "origin", 170 | branch 171 | ); 172 | } 173 | 174 | module.exports = { 175 | ExitError, 176 | git, 177 | clone, 178 | fetch, 179 | fetchUntilMergeBase, 180 | fetchDeepen, 181 | mergeBase, 182 | mergeCommits, 183 | head, 184 | sha, 185 | rebase, 186 | push 187 | }; 188 | -------------------------------------------------------------------------------- /github/auto-merge/lib/util.js: -------------------------------------------------------------------------------- 1 | function branchName(ref) { 2 | const branchPrefix = "refs/heads/"; 3 | if (ref.startsWith(branchPrefix)) { 4 | return ref.substr(branchPrefix.length); 5 | } 6 | } 7 | 8 | module.exports = { branchName }; 9 | -------------------------------------------------------------------------------- /github/auto-merge/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "automerge-action", 3 | "version": "0.15.6", 4 | "description": "GitHub action to automatically merge pull requests", 5 | "main": "lib/api.js", 6 | "author": "Pascal", 7 | "license": "MIT", 8 | "private": true, 9 | "bin": { 10 | "automerge-action": "./bin/automerge.js" 11 | }, 12 | "scripts": { 13 | "test": "jest", 14 | "it": "node it/it.js", 15 | "lint": "prettier -l lib/** test/** && eslint .", 16 | "compile": "ncc build bin/automerge.js --license LICENSE -o dist", 17 | "prepublish": "yarn lint && yarn test && yarn compile" 18 | }, 19 | "dependencies": { 20 | "@actions/core": "^1.10.0", 21 | "@octokit/rest": "^19.0.7", 22 | "argparse": "^2.0.1", 23 | "fs-extra": "^11.1.0", 24 | "object-resolve-path": "^1.1.1", 25 | "tmp": "^0.2.1" 26 | }, 27 | "devDependencies": { 28 | "@vercel/ncc": "^0.36.1", 29 | "dotenv": "^16.0.3", 30 | "eslint": "^8.34.0", 31 | "eslint-plugin-jest": "^27.2.1", 32 | "jest": "^29.4.3", 33 | "prettier": "^2.8.4" 34 | }, 35 | "prettier": { 36 | "trailingComma": "none", 37 | "arrowParens": "avoid" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /github/auto-merge/test/api.test.js: -------------------------------------------------------------------------------- 1 | const api = require("../lib/api"); 2 | const { createConfig } = require("../lib/common"); 3 | const { pullRequest } = require("./common"); 4 | 5 | let octokit; 6 | 7 | test("forked PR check_suite/check_run updates are handled", async () => { 8 | // GIVEN 9 | const head_sha = "1234abcd"; 10 | const pr = pullRequest(); 11 | pr.labels = [{ name: "automerge" }]; 12 | pr.head.sha = head_sha; 13 | 14 | const config = createConfig({}); 15 | 16 | let merged = false; 17 | octokit = { 18 | pulls: { 19 | list: jest.fn(() => ({ data: [pr] })), 20 | merge: jest.fn(() => (merged = true)), 21 | listReviews: jest.fn(() => ({ data: [] })) 22 | } 23 | }; 24 | 25 | const event = { 26 | action: "completed", 27 | repository: { owner: { login: "other-username" }, name: "repository" }, 28 | check_suite: { conclusion: "success", head_sha, pull_requests: [] } 29 | }; 30 | 31 | // WHEN 32 | await api.executeGitHubAction({ config, octokit }, "check_suite", event); 33 | expect(merged).toEqual(true); 34 | }); 35 | 36 | test("only merge PRs with required approvals", async () => { 37 | // GIVEN 38 | const head_sha = "1234abcd"; 39 | const pr = pullRequest(); 40 | pr.labels = [{ name: "automerge" }]; 41 | pr.head.sha = head_sha; 42 | 43 | const config = createConfig({}); 44 | config.mergeRequiredApprovals = 2; // let's only merge, if there are two independent approvals 45 | 46 | let merged = false; 47 | octokit = { 48 | pulls: { 49 | list: jest.fn(() => ({ data: [pr] })), 50 | merge: jest.fn(() => (merged = true)), 51 | listReviews: Symbol("listReviews") 52 | }, 53 | paginate: jest.fn(() => []) 54 | }; 55 | 56 | const event = { 57 | action: "completed", 58 | repository: { owner: { login: "other-username" }, name: "repository" }, 59 | check_suite: { conclusion: "success", head_sha, pull_requests: [] } 60 | }; 61 | 62 | // WHEN 63 | await api.executeGitHubAction({ config, octokit }, "check_suite", event); 64 | expect(merged).toEqual(false); // if there's no approval, it should fail 65 | 66 | merged = false; 67 | octokit.paginate.mockReturnValueOnce([ 68 | { state: "APPROVED", user: { login: "approval_user" } }, 69 | { state: "APPROVED", user: { login: "approval_user2" } } 70 | ]); 71 | 72 | // WHEN 73 | await api.executeGitHubAction({ config, octokit }, "check_suite", event); 74 | expect(merged).toEqual(true); // if there are two approvals, it should succeed 75 | 76 | merged = false; 77 | octokit.paginate.mockReturnValueOnce([ 78 | { state: "APPROVED", user: { login: "approval_user" } }, 79 | { state: "APPROVED", user: { login: "approval_user" } } 80 | ]); 81 | 82 | // WHEN a user has given 83 | await api.executeGitHubAction({ config, octokit }, "check_suite", event); 84 | expect(merged).toEqual(false); // if there are only two approvals from the same user, it should fail 85 | }); 86 | -------------------------------------------------------------------------------- /github/auto-merge/test/common.js: -------------------------------------------------------------------------------- 1 | function pullRequest() { 2 | return { 3 | number: 1, 4 | title: "Update README", 5 | body: "This PR updates the README", 6 | state: "open", 7 | locked: false, 8 | merged: false, 9 | mergeable: true, 10 | rebaseable: true, 11 | mergeable_state: "clean", 12 | commits: 2, 13 | labels: [{ name: "automerge" }], 14 | head: { 15 | ref: "patch-1", 16 | sha: "2c3b4d5", 17 | user: { login: "username" }, 18 | repo: { 19 | name: "repository", 20 | full_name: "username/repository", 21 | owner: { login: "username" } 22 | } 23 | }, 24 | base: { 25 | ref: "master", 26 | sha: "45600fe", 27 | user: { login: "username" }, 28 | repo: { 29 | name: "repository", 30 | full_name: "username/repository", 31 | owner: { login: "username" } 32 | } 33 | } 34 | }; 35 | } 36 | 37 | module.exports = { pullRequest }; 38 | -------------------------------------------------------------------------------- /github/auto-merge/test/common.test.js: -------------------------------------------------------------------------------- 1 | const { createConfig } = require("../lib/common"); 2 | 3 | test("createConfig", () => { 4 | const config = createConfig({ 5 | UPDATE_LABELS: " required1,! block1, ! ,required2, !block2 ", 6 | MERGE_LABELS: "", 7 | MERGE_RETRIES: "3", 8 | BASE_BRANCHES: "dev,main" 9 | }); 10 | const expected = { 11 | mergeMethod: "merge", 12 | mergeFilterAuthor: "", 13 | mergeLabels: { 14 | blocking: [], 15 | required: [] 16 | }, 17 | mergeMethodLabels: [], 18 | mergeMethodLabelRequired: false, 19 | mergeForks: true, 20 | mergeCommitMessage: "automatic", 21 | mergeCommitMessageRegex: "", 22 | mergeDeleteBranch: false, 23 | mergeDeleteBranchFilter: [], 24 | mergeErrorFail: false, 25 | mergeReadyState: ["clean", "has_hooks", "unknown", "unstable"], 26 | mergeRetries: 3, 27 | mergeRetrySleep: 5000, 28 | mergeRequiredApprovals: 0, 29 | mergeRemoveLabels: [], 30 | updateMethod: "merge", 31 | updateLabels: { 32 | blocking: ["block1", "block2"], 33 | required: ["required1", "required2"] 34 | }, 35 | updateRetries: 1, 36 | updateRetrySleep: 5000, 37 | baseBranches: ["dev", "main"], 38 | pullRequest: null 39 | }; 40 | expect(config).toEqual(expected); 41 | }); 42 | 43 | test("createConfig with arbitrary pull request (as string)", () => { 44 | const config = createConfig({ 45 | UPDATE_LABELS: " required1,! block1, ! ,required2, !block2 ", 46 | MERGE_LABELS: "", 47 | MERGE_RETRIES: "3", 48 | PULL_REQUEST: "144" 49 | }); 50 | const expected = { 51 | mergeMethod: "merge", 52 | mergeFilterAuthor: "", 53 | mergeLabels: { 54 | blocking: [], 55 | required: [] 56 | }, 57 | mergeMethodLabels: [], 58 | mergeMethodLabelRequired: false, 59 | mergeForks: true, 60 | mergeCommitMessage: "automatic", 61 | mergeCommitMessageRegex: "", 62 | mergeDeleteBranch: false, 63 | mergeDeleteBranchFilter: [], 64 | mergeErrorFail: false, 65 | mergeReadyState: ["clean", "has_hooks", "unknown", "unstable"], 66 | mergeRetries: 3, 67 | mergeRetrySleep: 5000, 68 | mergeRequiredApprovals: 0, 69 | mergeRemoveLabels: [], 70 | updateMethod: "merge", 71 | updateLabels: { 72 | blocking: ["block1", "block2"], 73 | required: ["required1", "required2"] 74 | }, 75 | updateRetries: 1, 76 | updateRetrySleep: 5000, 77 | baseBranches: [], 78 | pullRequest: { 79 | pullRequestNumber: 144 80 | } 81 | }; 82 | expect(config).toEqual(expected); 83 | }); 84 | 85 | test("createConfig with arbitrary pull request (as number)", () => { 86 | const config = createConfig({ 87 | UPDATE_LABELS: " required1,! block1, ! ,required2, !block2 ", 88 | MERGE_LABELS: "", 89 | MERGE_RETRIES: "3", 90 | PULL_REQUEST: 144 91 | }); 92 | const expected = { 93 | mergeMethod: "merge", 94 | mergeFilterAuthor: "", 95 | mergeLabels: { 96 | blocking: [], 97 | required: [] 98 | }, 99 | mergeMethodLabels: [], 100 | mergeMethodLabelRequired: false, 101 | mergeForks: true, 102 | mergeCommitMessage: "automatic", 103 | mergeCommitMessageRegex: "", 104 | mergeDeleteBranch: false, 105 | mergeDeleteBranchFilter: [], 106 | mergeErrorFail: false, 107 | mergeReadyState: ["clean", "has_hooks", "unknown", "unstable"], 108 | mergeRetries: 3, 109 | mergeRetrySleep: 5000, 110 | mergeRequiredApprovals: 0, 111 | mergeRemoveLabels: [], 112 | updateMethod: "merge", 113 | updateLabels: { 114 | blocking: ["block1", "block2"], 115 | required: ["required1", "required2"] 116 | }, 117 | updateRetries: 1, 118 | updateRetrySleep: 5000, 119 | baseBranches: [], 120 | pullRequest: { 121 | pullRequestNumber: 144 122 | } 123 | }; 124 | expect(config).toEqual(expected); 125 | }); 126 | 127 | test("createConfig with arbitrary pull request in another repo", () => { 128 | const config = createConfig({ 129 | UPDATE_LABELS: " required1,! block1, ! ,required2, !block2 ", 130 | MERGE_LABELS: "", 131 | MERGE_RETRIES: "3", 132 | PULL_REQUEST: "pascalgn/automerge-action/144" 133 | }); 134 | const expected = { 135 | mergeMethod: "merge", 136 | mergeFilterAuthor: "", 137 | mergeLabels: { 138 | blocking: [], 139 | required: [] 140 | }, 141 | mergeMethodLabels: [], 142 | mergeMethodLabelRequired: false, 143 | mergeForks: true, 144 | mergeCommitMessage: "automatic", 145 | mergeCommitMessageRegex: "", 146 | mergeDeleteBranch: false, 147 | mergeDeleteBranchFilter: [], 148 | mergeErrorFail: false, 149 | mergeReadyState: ["clean", "has_hooks", "unknown", "unstable"], 150 | mergeRetries: 3, 151 | mergeRetrySleep: 5000, 152 | mergeRequiredApprovals: 0, 153 | mergeRemoveLabels: [], 154 | updateMethod: "merge", 155 | updateLabels: { 156 | blocking: ["block1", "block2"], 157 | required: ["required1", "required2"] 158 | }, 159 | updateRetries: 1, 160 | updateRetrySleep: 5000, 161 | baseBranches: [], 162 | pullRequest: { 163 | repoOwner: "pascalgn", 164 | repoName: "automerge-action", 165 | pullRequestNumber: 144 166 | } 167 | }; 168 | expect(config).toEqual(expected); 169 | }); 170 | -------------------------------------------------------------------------------- /github/auto-merge/test/git.test.js: -------------------------------------------------------------------------------- 1 | const fse = require("fs-extra"); 2 | 3 | const git = require("../lib/git"); 4 | const { tmpdir } = require("../lib/common"); 5 | 6 | async function init(dir) { 7 | await fse.mkdirs(dir); 8 | await git.git(dir, "init"); 9 | await git.git(dir, "checkout", "-b", "main"); 10 | } 11 | 12 | async function commit(dir, message = "C%d", count = 1) { 13 | for (let i = 1; i <= count; i++) { 14 | await git.git( 15 | dir, 16 | "commit", 17 | "--allow-empty", 18 | "-m", 19 | message.replace(/%d/g, i) 20 | ); 21 | } 22 | } 23 | 24 | test("clone creates the target directory", async () => { 25 | await tmpdir(async path => { 26 | await init(`${path}/origin`); 27 | await commit(`${path}/origin`); 28 | await git.clone(`${path}/origin`, `${path}/ws`, "main", 1); 29 | expect(await fse.exists(`${path}/ws`)).toBe(true); 30 | }); 31 | }); 32 | 33 | test("fetchUntilMergeBase finds the correct merge base", async () => { 34 | await tmpdir(async path => { 35 | const origin = `${path}/origin`; 36 | await init(origin); 37 | await commit(origin, "base %d", 10); 38 | const base = await git.head(origin); 39 | await git.git(origin, "checkout", "-b", "br1"); 40 | await commit(origin, "br1 %d", 20); 41 | await git.git(origin, "checkout", "main"); 42 | await commit(origin, "main %d", 20); 43 | 44 | const ws = `${path}/ws`; 45 | await git.clone(`${path}/origin`, ws, "br1"); 46 | await git.fetch(ws, "main"); 47 | expect(await git.fetchUntilMergeBase(ws, "main", 10000)).toBe(base); 48 | }); 49 | }, 15000); 50 | 51 | test("fetchUntilMergeBase finds the earliest merge base 1", async () => { 52 | await tmpdir(async path => { 53 | const origin = `${path}/origin`; 54 | await init(origin); 55 | await commit(origin, "base %d", 10); 56 | const base = await git.head(origin); 57 | await git.git(origin, "branch", "br1"); 58 | await commit(origin, "main %d", 10); 59 | await git.git(origin, "checkout", "br1"); 60 | await commit(origin, "br1 before merge %d", 5); 61 | await git.git(origin, "merge", "--no-ff", "main"); 62 | await commit(origin, "br1 after merge %d", 10); 63 | await git.git(origin, "checkout", "main"); 64 | await commit(origin, "main after merge %d", 10); 65 | 66 | const ws = `${path}/ws`; 67 | await git.clone(`${path}/origin`, ws, "br1"); 68 | await git.fetch(ws, "main"); 69 | expect(await git.fetchUntilMergeBase(ws, "main", 10000)).toBe(base); 70 | }); 71 | }, 15000); 72 | 73 | test("fetchUntilMergeBase finds the earliest merge base 2", async () => { 74 | await tmpdir(async path => { 75 | const origin = `${path}/origin`; 76 | await init(origin); 77 | await commit(origin, "base a%d", 5); 78 | const base = await git.head(origin); 79 | await commit(origin, "base b%d", 5); 80 | await git.git(origin, "branch", "br1"); 81 | await commit(origin, "main %d", 10); 82 | await git.git(origin, "checkout", "br1"); 83 | await commit(origin, "br1 before merge %d", 5); 84 | await git.git(origin, "merge", "--no-ff", "main"); 85 | await commit(origin, "br1 after merge %d", 10); 86 | await git.git(origin, "checkout", "main"); 87 | await commit(origin, "main after merge %d", 10); 88 | await git.git(origin, "checkout", "-b", "br2", base); 89 | await commit(origin, "br2"); 90 | await git.git(origin, "checkout", "br1"); 91 | await git.git(origin, "merge", "--no-ff", "br2"); 92 | 93 | const ws = `${path}/ws`; 94 | await git.clone(`${path}/origin`, ws, "br1"); 95 | await git.fetch(ws, "main"); 96 | expect(await git.fetchUntilMergeBase(ws, "main", 10000)).toBe(base); 97 | }); 98 | }, 15000); 99 | 100 | test("mergeCommits returns the correct commits", async () => { 101 | await tmpdir(async path => { 102 | await init(path); 103 | await commit(path, "main %d", 2); 104 | const head1 = await git.head(path); 105 | await git.git(path, "checkout", "-b", "branch", "HEAD^"); 106 | const head2 = await git.head(path); 107 | await git.git(path, "merge", "--no-ff", "main"); 108 | 109 | const commits = await git.mergeCommits(path, "HEAD^"); 110 | expect(commits).toHaveLength(1); 111 | expect(commits[0][0]).toBe(head2); 112 | expect(commits[0][1]).toBe(head1); 113 | }); 114 | }); 115 | -------------------------------------------------------------------------------- /github/auto-merge/test/update.test.js: -------------------------------------------------------------------------------- 1 | const { update } = require("../lib/update"); 2 | const { createConfig } = require("../lib/common"); 3 | const { pullRequest } = require("./common"); 4 | 5 | test("update will only run when the label matches", async () => { 6 | const pr = pullRequest(); 7 | const config = createConfig({ UPDATE_LABELS: "none" }); 8 | expect(await update({ config }, pr)).toEqual(false); 9 | }); 10 | -------------------------------------------------------------------------------- /github/auto-merge/test/util.test.js: -------------------------------------------------------------------------------- 1 | const { branchName } = require("../lib/util"); 2 | 3 | describe("branchName", () => { 4 | it("returns the branch name from a reference referring to a branch", async () => { 5 | expect(branchName("refs/heads/main")).toEqual("main"); 6 | expect(branchName("refs/heads/features/branch_with_slashes")).toEqual( 7 | "features/branch_with_slashes" 8 | ); 9 | }); 10 | 11 | it("is falsey for other kinds of git references", async () => { 12 | expect(branchName("refs/tags/v1.0")).toBeUndefined(); 13 | expect(branchName("refs/remotes/origin/main")).toBeUndefined(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /github/branch-cleanup/.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # You can add one username per supported platform and one custom link 2 | patreon: jessfraz 3 | -------------------------------------------------------------------------------- /github/branch-cleanup/.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | notifications: 3 | email: true 4 | services: 5 | - docker 6 | script: 7 | - make test 8 | -------------------------------------------------------------------------------- /github/branch-cleanup/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | LABEL maintainer="Jessica Frazelle " 3 | 4 | LABEL "com.github.actions.name"="Branch Cleanup" 5 | LABEL "com.github.actions.description"="Delete the branch after a pull request has been merged" 6 | LABEL "com.github.actions.icon"="activity" 7 | LABEL "com.github.actions.color"="red" 8 | 9 | RUN apk add --no-cache \ 10 | bash \ 11 | ca-certificates \ 12 | curl \ 13 | jq 14 | 15 | COPY cleanup-pr-branch /usr/bin/cleanup-pr-branch 16 | 17 | ENTRYPOINT ["cleanup-pr-branch"] 18 | -------------------------------------------------------------------------------- /github/branch-cleanup/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Jessie Frazelle 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /github/branch-cleanup/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | test: shellcheck ## Runs the tests on the repository. 3 | 4 | # if this session isn't interactive, then we don't want to allocate a 5 | # TTY, which would fail, but if it is interactive, we do want to attach 6 | # so that the user can send e.g. ^C through. 7 | INTERACTIVE := $(shell [ -t 0 ] && echo 1 || echo 0) 8 | ifeq ($(INTERACTIVE), 1) 9 | DOCKER_FLAGS += -t 10 | endif 11 | 12 | .PHONY: shellcheck 13 | shellcheck: ## Runs the shellcheck tests on the scripts. 14 | docker run --rm -i $(DOCKER_FLAGS) \ 15 | --name shellcheck \ 16 | -v $(CURDIR):/usr/src:ro \ 17 | --workdir /usr/src \ 18 | r.j3ss.co/shellcheck ./test.sh 19 | 20 | .PHONY: help 21 | help: 22 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 23 | -------------------------------------------------------------------------------- /github/branch-cleanup/README.md: -------------------------------------------------------------------------------- 1 | # Branch Cleanup Action 2 | 3 | [![Travis CI](https://img.shields.io/travis/jessfraz/branch-cleanup-action.svg?style=for-the-badge)](https://travis-ci.org/jessfraz/branch-cleanup-action) 4 | 5 | A GitHub action to automatically delete the branch after a pull request has been merged. Here's [a blog post](https://blog.jessfraz.com/post/the-life-of-a-github-action/) describing this action in more detail. 6 | 7 | > **NOTE:** This will **never** delete the repository's default branch or protected branches. If the pull request is closed _without_ merging, it will **not** delete it. 8 | 9 | **Table of Contents** 10 | 11 | 12 | 13 | - [Usage](#usage) 14 | - [Contributing](#contributing) 15 | * [Running the tests](#running-the-tests) 16 | 17 | 18 | 19 | ## Usage 20 | 21 | ``` 22 | workflow "on pull request merge, delete the branch" { 23 | on = "pull_request" 24 | resolves = ["branch cleanup"] 25 | } 26 | 27 | action "branch cleanup" { 28 | uses = "jessfraz/branch-cleanup-action@master" 29 | secrets = ["GITHUB_TOKEN"] 30 | } 31 | ``` 32 | 33 | If you include this in another Workflow, you may want to configure the environment variable `NO_BRANCH_DELETED_EXIT_CODE`. The default value for this is `78`, as Github Actions will mark a check as "neutral" (neither failed/succeeded) when you exit with code 78. This will however **cancel** any other actions running in parallel in this workflow. 34 | 35 | If you don't want it to cancel, configure your workflow as follows: 36 | 37 | ``` 38 | action "branch cleanup" { 39 | uses = "jessfraz/branch-cleanup-action@master" 40 | secrets = ["GITHUB_TOKEN"] 41 | 42 | env = { 43 | NO_BRANCH_DELETED_EXIT_CODE = "0" 44 | } 45 | } 46 | ``` 47 | 48 | ![demo](demo.png) 49 | 50 | ## Contributing 51 | 52 | ### Running the tests 53 | 54 | The tests use [shellcheck](https://github.com/koalaman/shellcheck). You don't need to install anything (assuming you have [docker](https://www.docker.com) installed). The tests run in a container. 55 | 56 | ```console 57 | $ make test 58 | ``` 59 | -------------------------------------------------------------------------------- /github/branch-cleanup/cleanup-pr-branch: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | set -o pipefail 4 | 5 | if [[ -n "$TOKEN" ]]; then 6 | GITHUB_TOKEN=$TOKEN 7 | fi 8 | 9 | if [[ -z "$GITHUB_TOKEN" ]]; then 10 | echo "Set the GITHUB_TOKEN env variable." 11 | exit 1 12 | fi 13 | 14 | URI=https://api.github.com 15 | API_VERSION=v3 16 | API_HEADER="Accept: application/vnd.github.${API_VERSION}+json" 17 | AUTH_HEADER="Authorization: token ${GITHUB_TOKEN}" 18 | 19 | # Github Actions uses either status code 0 for success or any other code for failure. 20 | # Docs: https://help.github.com/en/articles/virtual-environments-for-github-actions#exit-codes-and-statuses 21 | NO_BRANCH_DELETED_EXIT_CODE=${NO_BRANCH_DELETED_EXIT_CODE:-0} 22 | 23 | main(){ 24 | action=$(jq --raw-output .action "$GITHUB_EVENT_PATH") 25 | merged=$(jq --raw-output .pull_request.merged "$GITHUB_EVENT_PATH") 26 | 27 | echo "DEBUG -> action: $action merged: $merged" 28 | 29 | if [[ "$action" != "closed" ]] || [[ "$merged" != "true" ]]; then 30 | exit "$NO_BRANCH_DELETED_EXIT_CODE" 31 | fi 32 | 33 | # delete the branch. 34 | ref=$(jq --raw-output .pull_request.head.ref "$GITHUB_EVENT_PATH") 35 | owner=$(jq --raw-output .pull_request.head.repo.owner.login "$GITHUB_EVENT_PATH") 36 | repo=$(jq --raw-output .pull_request.head.repo.name "$GITHUB_EVENT_PATH") 37 | default_branch=$( 38 | curl -XGET -fsSL \ 39 | -H "${AUTH_HEADER}" \ 40 | -H "${API_HEADER}" \ 41 | "${URI}/repos/${owner}/${repo}" | jq .default_branch 42 | ) 43 | 44 | if [[ "$ref" == "$default_branch" ]]; then 45 | # Never delete the default branch. 46 | echo "Will not delete default branch (${default_branch}) for ${owner}/${repo}, exiting." 47 | exit 0 48 | fi 49 | is_protected=$( 50 | curl -XGET -fsSL \ 51 | -H "${AUTH_HEADER}" \ 52 | -H "${API_HEADER}" \ 53 | "${URI}/repos/${owner}/${repo}/branches/${ref}" | jq .protected 54 | ) 55 | 56 | if [[ "$is_protected" == "true" ]]; then 57 | # Never delete protected branches 58 | echo "Will not delete protected branch (${ref}) for ${owner}/${repo}, exiting." 59 | exit 0 60 | fi 61 | 62 | pulls_with_ref_as_base=$( 63 | curl -XGET -fsSL \ 64 | -H "${AUTH_HEADER}" \ 65 | -H "${API_HEADER}" \ 66 | "${URI}/repos/${owner}/${repo}/pulls?state=open&base=${ref}" 67 | ) 68 | has_pulls_with_ref_as_base=$(echo "$pulls_with_ref_as_base" | jq 'has(0)') 69 | 70 | 71 | if [[ "$has_pulls_with_ref_as_base" != false ]]; then 72 | # Do not delete if the branch is a base branch of another pull request 73 | pr=$(echo "$pulls_with_ref_as_base" | jq '.[0].number') 74 | echo "${ref} is the base branch of PR #${pr} for ${owner}/${repo}, exiting." 75 | exit 0 76 | fi 77 | 78 | echo "Deleting branch ref $ref for owner ${owner}/${repo}..." 79 | response=$( 80 | curl -XDELETE -sSL \ 81 | -H "${AUTH_HEADER}" \ 82 | -H "${API_HEADER}" \ 83 | --output /dev/null \ 84 | --write-out "%{http_code}" \ 85 | "${URI}/repos/${owner}/${repo}/git/refs/heads/${ref}" 86 | ) 87 | 88 | if [[ ${response} -eq 422 ]]; then 89 | echo "The branch is already gone!" 90 | elif [[ ${response} -eq 204 ]]; then 91 | echo "Branch delete success!" 92 | else 93 | echo "Something unexpected happened!" 94 | exit "$NO_BRANCH_DELETED_EXIT_CODE" 95 | fi 96 | 97 | exit 0 98 | } 99 | 100 | main "$@" 101 | -------------------------------------------------------------------------------- /github/branch-cleanup/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudposse-archives/actions/6a5182ba6c68f4d4a9f1d473629a47f6cd01dabd/github/branch-cleanup/demo.png -------------------------------------------------------------------------------- /github/branch-cleanup/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | set -o pipefail 4 | 5 | ERRORS=() 6 | 7 | # find all executables and run `shellcheck` 8 | for f in $(find . -type f -not -iwholename '*.git*' | sort -u); do 9 | if file "$f" | grep --quiet shell; then 10 | { 11 | shellcheck "$f" && echo "[OK]: sucessfully linted $f" 12 | } || { 13 | # add to errors 14 | ERRORS+=("$f") 15 | } 16 | fi 17 | done 18 | 19 | if [ ${#ERRORS[@]} -eq 0 ]; then 20 | echo "No errors, hooray" 21 | else 22 | echo "These files failed shellcheck: ${ERRORS[*]}" 23 | exit 1 24 | fi 25 | -------------------------------------------------------------------------------- /github/create-or-update-comment/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "globals": { 9 | "Atomics": "readonly", 10 | "SharedArrayBuffer": "readonly" 11 | }, 12 | "parserOptions": { 13 | "ecmaVersion": 2018 14 | }, 15 | "rules": { 16 | } 17 | } -------------------------------------------------------------------------------- /github/create-or-update-comment/.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: peter-evans -------------------------------------------------------------------------------- /github/create-or-update-comment/.github/comment-body-addition.md: -------------------------------------------------------------------------------- 1 | **Edit:** Some additional info 2 | -------------------------------------------------------------------------------- /github/create-or-update-comment/.github/comment-body.md: -------------------------------------------------------------------------------- 1 | This is a multi-line test comment read from a file. 2 | - With GitHub **Markdown** :sparkles: 3 | - Created by [create-or-update-comment][1] 4 | 5 | [1]: https://github.com/peter-evans/create-or-update-comment 6 | -------------------------------------------------------------------------------- /github/create-or-update-comment/.github/comment-template.md: -------------------------------------------------------------------------------- 1 | This is a test comment template 2 | Render template variables such as {{ .foo }} and {{ .bar }}. 3 | -------------------------------------------------------------------------------- /github/create-or-update-comment/.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: [main] 5 | paths-ignore: 6 | - 'README.md' 7 | - 'docs/**' 8 | pull_request: 9 | branches: [main] 10 | paths-ignore: 11 | - 'README.md' 12 | - 'docs/**' 13 | 14 | permissions: 15 | issues: write 16 | pull-requests: write 17 | contents: write 18 | 19 | jobs: 20 | build: 21 | runs-on: ubuntu-latest 22 | outputs: 23 | issue-number: ${{ steps.vars.outputs.issue-number }} 24 | steps: 25 | - uses: actions/checkout@v3 26 | - uses: actions/setup-node@v3 27 | with: 28 | node-version: 16.x 29 | cache: npm 30 | - run: npm ci 31 | - run: npm run test 32 | - run: npm run package 33 | - uses: actions/upload-artifact@v3 34 | with: 35 | name: dist 36 | path: dist 37 | - uses: actions/upload-artifact@v3 38 | with: 39 | name: action.yml 40 | path: action.yml 41 | - id: vars 42 | run: | 43 | if [[ "${{ github.event_name }}" == "pull_request" ]]; then \ 44 | echo "issue-number=${{ github.event.number }}" >> $GITHUB_OUTPUT; \ 45 | else \ 46 | echo "issue-number=1" >> $GITHUB_OUTPUT; \ 47 | fi 48 | 49 | test: 50 | if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository 51 | needs: [build] 52 | runs-on: ubuntu-latest 53 | strategy: 54 | matrix: 55 | target: [built, committed] 56 | steps: 57 | - uses: actions/checkout@v3 58 | - if: matrix.target == 'built' || github.event_name == 'pull_request' 59 | uses: actions/download-artifact@v3 60 | with: 61 | name: dist 62 | path: dist 63 | - if: matrix.target == 'built' || github.event_name == 'pull_request' 64 | uses: actions/download-artifact@v3 65 | with: 66 | name: action.yml 67 | path: . 68 | 69 | - name: Test create comment 70 | uses: ./ 71 | id: couc 72 | with: 73 | issue-number: ${{ needs.build.outputs.issue-number }} 74 | body: | 75 | This is a multi-line test comment 76 | - With GitHub **Markdown** :sparkles: 77 | - Created by [create-or-update-comment][1] 78 | 79 | [1]: https://github.com/peter-evans/create-or-update-comment 80 | reactions: '+1' 81 | 82 | - name: Test update comment 83 | uses: ./ 84 | with: 85 | comment-id: ${{ steps.couc.outputs.comment-id }} 86 | body: | 87 | **Edit:** Some additional info 88 | reactions: eyes 89 | 90 | - name: Test add reactions 91 | uses: ./ 92 | with: 93 | comment-id: ${{ steps.couc.outputs.comment-id }} 94 | reactions: heart, hooray, laugh 95 | 96 | - name: Test create comment from file 97 | uses: ./ 98 | id: couc2 99 | with: 100 | issue-number: ${{ needs.build.outputs.issue-number }} 101 | body-file: .github/comment-body.md 102 | reactions: '+1' 103 | 104 | - name: Test update comment from file 105 | uses: ./ 106 | with: 107 | comment-id: ${{ steps.couc2.outputs.comment-id }} 108 | body-file: .github/comment-body-addition.md 109 | reactions: eyes 110 | 111 | package: 112 | if: github.event_name == 'push' && github.ref == 'refs/heads/main' 113 | needs: [test] 114 | runs-on: ubuntu-latest 115 | steps: 116 | - uses: actions/checkout@v3 117 | - uses: actions/download-artifact@v3 118 | with: 119 | name: dist 120 | path: dist 121 | - name: Create Pull Request 122 | uses: peter-evans/create-pull-request@v6 123 | with: 124 | token: ${{ secrets.ACTIONS_BOT_TOKEN }} 125 | commit-message: Update distribution 126 | title: Update distribution 127 | body: | 128 | - Updates the distribution for changes on `main` 129 | 130 | Auto-generated by [create-pull-request][1] 131 | 132 | [1]: https://github.com/peter-evans/create-pull-request 133 | branch: update-distribution 134 | -------------------------------------------------------------------------------- /github/create-or-update-comment/.github/workflows/slash-command-dispatch.yml: -------------------------------------------------------------------------------- 1 | name: Slash Command Dispatch 2 | on: 3 | issue_comment: 4 | types: [created] 5 | jobs: 6 | slashCommandDispatch: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Slash Command Dispatch 10 | uses: peter-evans/slash-command-dispatch@v3 11 | with: 12 | token: ${{ secrets.ACTIONS_BOT_TOKEN }} 13 | config: > 14 | [ 15 | { 16 | "command": "rebase", 17 | "permission": "admin", 18 | "repository": "peter-evans/slash-command-dispatch-processor", 19 | "issue_type": "pull-request" 20 | }, 21 | { 22 | "command": "test", 23 | "permission": "admin", 24 | "named_args": true 25 | } 26 | ] 27 | -------------------------------------------------------------------------------- /github/create-or-update-comment/.github/workflows/test-command.yml: -------------------------------------------------------------------------------- 1 | name: Test Command 2 | on: 3 | repository_dispatch: 4 | types: [test-command] 5 | jobs: 6 | testCreateOrUpdateComment: 7 | runs-on: ubuntu-latest 8 | steps: 9 | # Get the target repository and branch 10 | - name: Get the target repository and branch 11 | id: vars 12 | run: | 13 | repository=${{ github.event.client_payload.slash_command.repository }} 14 | if [[ -z "$repository" ]]; then repository=${{ github.repository }}; fi 15 | echo "repository=$repository" >> $GITHUB_OUTPUT 16 | branch=${{ github.event.client_payload.slash_command.branch }} 17 | if [[ -z "$branch" ]]; then branch="main"; fi 18 | echo "branch=$branch" >> $GITHUB_OUTPUT 19 | 20 | # Checkout the branch to test 21 | - uses: actions/checkout@v3 22 | with: 23 | repository: ${{ steps.vars.outputs.repository }} 24 | ref: ${{ steps.vars.outputs.branch }} 25 | 26 | # Test create 27 | - name: Create comment 28 | uses: ./ 29 | id: couc 30 | with: 31 | issue-number: 1 32 | body: | 33 | This is a multi-line test comment 34 | - With GitHub **Markdown** :sparkles: 35 | - Created by [create-or-update-comment][1] 36 | 37 | [1]: https://github.com/peter-evans/create-or-update-comment 38 | reactions: '+1' 39 | 40 | # Test update 41 | - name: Update comment 42 | uses: ./ 43 | with: 44 | comment-id: ${{ steps.couc.outputs.comment-id }} 45 | body: | 46 | **Edit:** Some additional info 47 | reactions: eyes 48 | 49 | # Test add reactions 50 | - name: Add reactions 51 | uses: ./ 52 | with: 53 | comment-id: ${{ steps.couc.outputs.comment-id }} 54 | reactions: heart, hooray, laugh 55 | 56 | - name: Add reaction 57 | uses: peter-evans/create-or-update-comment@v2 58 | with: 59 | repository: ${{ github.event.client_payload.github.payload.repository.full_name }} 60 | comment-id: ${{ github.event.client_payload.github.payload.comment.id }} 61 | reactions: hooray 62 | 63 | # Test create with body from file 64 | - name: Create comment 65 | uses: ./ 66 | with: 67 | issue-number: 1 68 | body-file: .github/comment-body.md 69 | 70 | # Test create from template 71 | - name: Render template 72 | id: template 73 | uses: chuhlomin/render-template@v1.6 74 | with: 75 | template: .github/comment-template.md 76 | vars: | 77 | foo: this 78 | bar: that 79 | 80 | - name: Create comment 81 | uses: ./ 82 | with: 83 | issue-number: 1 84 | body: ${{ steps.template.outputs.result }} 85 | -------------------------------------------------------------------------------- /github/create-or-update-comment/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /github/create-or-update-comment/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Peter Evans 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /github/create-or-update-comment/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Create or Update Comment' 2 | description: 'Create or update an issue or pull request comment' 3 | inputs: 4 | token: 5 | description: 'GITHUB_TOKEN or a repo scoped PAT.' 6 | default: ${{ github.token }} 7 | repository: 8 | description: 'The full name of the repository in which to create or update a comment.' 9 | issue-number: 10 | description: 'The number of the issue or pull request in which to create a comment.' 11 | comment-id: 12 | description: 'The id of the comment to update.' 13 | body: 14 | description: 'The comment body. Cannot be used in conjunction with `body-file`.' 15 | body-file: 16 | description: 'The path to a file containing the comment body. Cannot be used in conjunction with `body`.' 17 | edit-mode: 18 | description: 'The mode when updating a comment, "replace" or "append".' 19 | reaction-type: 20 | description: 'Deprecated in favour of `reactions`' 21 | reactions: 22 | description: 'A comma separated list of reactions to add to the comment.' 23 | outputs: 24 | comment-id: 25 | description: 'The id of the created comment' 26 | runs: 27 | using: 'node16' 28 | main: 'dist/index.js' 29 | branding: 30 | icon: 'message-square' 31 | color: 'gray-dark' 32 | -------------------------------------------------------------------------------- /github/create-or-update-comment/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-or-update-comment", 3 | "version": "2.0.0", 4 | "description": "Create or update an issue or pull request comment", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "eslint index.js", 8 | "package": "ncc build index.js -o dist", 9 | "test": "eslint index.js && jest --passWithNoTests" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/peter-evans/create-or-update-comment.git" 14 | }, 15 | "keywords": [], 16 | "author": "Peter Evans", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/peter-evans/create-or-update-comment/issues" 20 | }, 21 | "homepage": "https://github.com/peter-evans/create-or-update-comment#readme", 22 | "dependencies": { 23 | "@actions/core": "^1.10.0", 24 | "@actions/github": "^5.1.1" 25 | }, 26 | "devDependencies": { 27 | "@vercel/ncc": "^0.36.1", 28 | "eslint": "^8.33.0", 29 | "jest": "^29.4.1" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /github/create-pull-request/.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /github/create-pull-request/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { "node": true, "jest": true }, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { "ecmaVersion": 9, "sourceType": "module" }, 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/eslint-recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "plugin:import/errors", 10 | "plugin:import/warnings", 11 | "plugin:import/typescript", 12 | "plugin:prettier/recommended" 13 | ], 14 | "plugins": ["@typescript-eslint"], 15 | "rules": { 16 | "@typescript-eslint/camelcase": "off" 17 | }, 18 | "settings": { 19 | "import/resolver": { 20 | "typescript": {} 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /github/create-pull-request/.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: peter-evans -------------------------------------------------------------------------------- /github/create-pull-request/.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Subject of the issue 2 | 3 | Describe your issue here. 4 | 5 | ### Steps to reproduce 6 | 7 | If this issue is describing a possible bug please provide (or link to) your GitHub Actions workflow. 8 | -------------------------------------------------------------------------------- /github/create-pull-request/.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: [main] 5 | paths-ignore: 6 | - 'README.md' 7 | - 'docs/**' 8 | pull_request: 9 | branches: [main] 10 | paths-ignore: 11 | - 'README.md' 12 | - 'docs/**' 13 | 14 | permissions: 15 | pull-requests: write 16 | contents: write 17 | 18 | jobs: 19 | build: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v3 23 | - uses: actions/setup-node@v3 24 | with: 25 | node-version: 16.x 26 | cache: npm 27 | - run: npm ci 28 | - run: npm run build 29 | - run: npm run format-check 30 | - run: npm run lint 31 | - run: npm run test 32 | - uses: actions/upload-artifact@v3 33 | with: 34 | name: dist 35 | path: dist 36 | - uses: actions/upload-artifact@v3 37 | with: 38 | name: action.yml 39 | path: action.yml 40 | 41 | test: 42 | if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository 43 | needs: [build] 44 | runs-on: ubuntu-latest 45 | strategy: 46 | matrix: 47 | target: [built, committed] 48 | steps: 49 | - uses: actions/checkout@v3 50 | with: 51 | ref: main 52 | - if: matrix.target == 'built' || github.event_name == 'pull_request' 53 | uses: actions/download-artifact@v3 54 | with: 55 | name: dist 56 | path: dist 57 | - if: matrix.target == 'built' || github.event_name == 'pull_request' 58 | uses: actions/download-artifact@v3 59 | with: 60 | name: action.yml 61 | path: . 62 | 63 | - name: Create change 64 | run: date +%s > report.txt 65 | 66 | - name: Create Pull Request 67 | id: cpr 68 | uses: ./ 69 | with: 70 | commit-message: '[CI] test ${{ matrix.target }}' 71 | committer: GitHub 72 | author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com> 73 | title: '[CI] test ${{ matrix.target }}' 74 | body: | 75 | - CI test case for target '${{ matrix.target }}' 76 | 77 | Auto-generated by [create-pull-request][1] 78 | 79 | [1]: https://github.com/peter-evans/create-pull-request 80 | branch: ci-test-${{ matrix.target }} 81 | 82 | - name: Close Pull 83 | uses: peter-evans/close-pull@v2 84 | with: 85 | pull-request-number: ${{ steps.cpr.outputs.pull-request-number }} 86 | comment: '[CI] test ${{ matrix.target }}' 87 | delete-branch: true 88 | 89 | commentTestSuiteHelp: 90 | if: github.event_name == 'pull_request' 91 | needs: [test] 92 | runs-on: ubuntu-latest 93 | steps: 94 | - name: Find Comment 95 | uses: peter-evans/find-comment@v2 96 | id: fc 97 | with: 98 | issue-number: ${{ github.event.number }} 99 | comment-author: 'github-actions[bot]' 100 | body-includes: Full test suite slash command 101 | 102 | - if: steps.fc.outputs.comment-id == '' 103 | name: Create comment 104 | uses: peter-evans/create-or-update-comment@v2 105 | with: 106 | issue-number: ${{ github.event.number }} 107 | body: | 108 | Full test suite slash command (repository admin only) 109 | ``` 110 | /test repository=${{ github.event.pull_request.head.repo.full_name }} ref=${{ github.event.pull_request.head.ref }} build=true 111 | ``` 112 | 113 | package: 114 | if: github.event_name == 'push' && github.ref == 'refs/heads/main' 115 | needs: [test] 116 | runs-on: ubuntu-latest 117 | steps: 118 | - uses: actions/checkout@v3 119 | - uses: actions/download-artifact@v3 120 | with: 121 | name: dist 122 | path: dist 123 | - name: Create Pull Request 124 | uses: peter-evans/create-pull-request@v6 125 | with: 126 | commit-message: 'build: update distribution' 127 | title: Update distribution 128 | body: | 129 | - Updates the distribution for changes on `main` 130 | 131 | Auto-generated by [create-pull-request][1] 132 | 133 | [1]: https://github.com/peter-evans/create-pull-request 134 | branch: update-distribution 135 | -------------------------------------------------------------------------------- /github/create-pull-request/.github/workflows/cpr-example-command.yml: -------------------------------------------------------------------------------- 1 | name: Create Pull Request Example Command 2 | on: 3 | repository_dispatch: 4 | types: [cpr-example-command] 5 | jobs: 6 | createPullRequest: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | 11 | - name: Make changes to pull request 12 | run: date +%s > report.txt 13 | 14 | - name: Create Pull Request 15 | id: cpr 16 | uses: ./ 17 | with: 18 | commit-message: Update report 19 | committer: GitHub 20 | author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com> 21 | signoff: false 22 | title: '[Example] Update report' 23 | body: | 24 | Update report 25 | - Updated with *today's* date 26 | - Auto-generated by [create-pull-request][1] 27 | 28 | [1]: https://github.com/peter-evans/create-pull-request 29 | labels: | 30 | report 31 | automated pr 32 | assignees: peter-evans 33 | reviewers: peter-evans 34 | milestone: 1 35 | draft: false 36 | branch: example-patches 37 | delete-branch: true 38 | 39 | - name: Check output 40 | run: | 41 | echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}" 42 | echo "Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}" 43 | 44 | - name: Add reaction 45 | uses: peter-evans/create-or-update-comment@v2 46 | with: 47 | repository: ${{ github.event.client_payload.github.payload.repository.full_name }} 48 | comment-id: ${{ github.event.client_payload.github.payload.comment.id }} 49 | reaction-type: hooray 50 | -------------------------------------------------------------------------------- /github/create-pull-request/.github/workflows/slash-command-dispatch.yml: -------------------------------------------------------------------------------- 1 | name: Slash Command Dispatch 2 | on: 3 | issue_comment: 4 | types: [created] 5 | jobs: 6 | slashCommandDispatch: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Slash Command Dispatch 10 | uses: peter-evans/slash-command-dispatch@v3 11 | with: 12 | token: ${{ secrets.ACTIONS_BOT_TOKEN }} 13 | config: > 14 | [ 15 | { 16 | "command": "test", 17 | "permission": "admin", 18 | "repository": "peter-evans/create-pull-request-tests", 19 | "named_args": true 20 | }, 21 | { 22 | "command": "clean", 23 | "permission": "admin", 24 | "repository": "peter-evans/create-pull-request-tests" 25 | }, 26 | { 27 | "command": "cpr-example", 28 | "permission": "admin", 29 | "issue_type": "issue" 30 | }, 31 | { 32 | "command": "rebase", 33 | "permission": "admin", 34 | "repository": "peter-evans/slash-command-dispatch-processor", 35 | "issue_type": "pull-request" 36 | } 37 | ] 38 | -------------------------------------------------------------------------------- /github/create-pull-request/.gitignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | node_modules/ 3 | 4 | .DS_Store 5 | .idea 6 | -------------------------------------------------------------------------------- /github/create-pull-request/.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /github/create-pull-request/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": false, 6 | "singleQuote": true, 7 | "trailingComma": "none", 8 | "bracketSpacing": false, 9 | "arrowParens": "avoid", 10 | "parser": "typescript" 11 | } -------------------------------------------------------------------------------- /github/create-pull-request/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Peter Evans 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /github/create-pull-request/__test__/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -l 2 | set -euo pipefail 3 | 4 | # Save the working directory 5 | WORKINGDIR=$PWD 6 | 7 | # Create and serve a remote repo 8 | mkdir -p /git/remote 9 | git config --global init.defaultBranch main 10 | git init --bare /git/remote/test-base.git 11 | git daemon --verbose --enable=receive-pack --base-path=/git/remote --export-all /git/remote &>/dev/null & 12 | 13 | # Give the daemon time to start 14 | sleep 2 15 | 16 | # Create a local clone and make an initial commit 17 | mkdir -p /git/local 18 | git clone git://127.0.0.1/test-base.git /git/local/test-base 19 | cd /git/local/test-base 20 | git config --global user.email "you@example.com" 21 | git config --global user.name "Your Name" 22 | echo "#test-base" > README.md 23 | git add . 24 | git commit -m "initial commit" 25 | git push -u 26 | git log -1 --pretty=oneline 27 | git config --global --unset user.email 28 | git config --global --unset user.name 29 | git config -l 30 | 31 | # Clone a server-side fork of the base repo 32 | cd $WORKINGDIR 33 | git clone --mirror git://127.0.0.1/test-base.git /git/remote/test-fork.git 34 | cd /git/remote/test-fork.git 35 | git log -1 --pretty=oneline 36 | 37 | # Restore the working directory 38 | cd $WORKINGDIR 39 | 40 | # Execute integration tests 41 | jest int --runInBand 42 | -------------------------------------------------------------------------------- /github/create-pull-request/__test__/git-auth-helper.int.test.ts: -------------------------------------------------------------------------------- 1 | import {GitCommandManager} from '../lib/git-command-manager' 2 | import {GitAuthHelper} from '../lib/git-auth-helper' 3 | 4 | const REPO_PATH = '/git/local/test-base' 5 | 6 | const extraheaderConfigKey = 'http.https://github.com/.extraheader' 7 | 8 | describe('git-auth-helper tests', () => { 9 | let git: GitCommandManager 10 | let gitAuthHelper: GitAuthHelper 11 | 12 | beforeAll(async () => { 13 | git = await GitCommandManager.create(REPO_PATH) 14 | gitAuthHelper = new GitAuthHelper(git) 15 | }) 16 | 17 | it('tests save and restore with no persisted auth', async () => { 18 | await gitAuthHelper.savePersistedAuth() 19 | await gitAuthHelper.restorePersistedAuth() 20 | }) 21 | 22 | it('tests configure and removal of auth', async () => { 23 | await gitAuthHelper.configureToken('github-token') 24 | expect(await git.configExists(extraheaderConfigKey)).toBeTruthy() 25 | expect(await git.getConfigValue(extraheaderConfigKey)).toEqual( 26 | 'AUTHORIZATION: basic eC1hY2Nlc3MtdG9rZW46Z2l0aHViLXRva2Vu' 27 | ) 28 | 29 | await gitAuthHelper.removeAuth() 30 | expect(await git.configExists(extraheaderConfigKey)).toBeFalsy() 31 | }) 32 | 33 | it('tests save and restore of persisted auth', async () => { 34 | const extraheaderConfigValue = 'AUTHORIZATION: basic ***persisted-auth***' 35 | await git.config(extraheaderConfigKey, extraheaderConfigValue) 36 | 37 | await gitAuthHelper.savePersistedAuth() 38 | 39 | const exists = await git.configExists(extraheaderConfigKey) 40 | expect(exists).toBeFalsy() 41 | 42 | await gitAuthHelper.restorePersistedAuth() 43 | 44 | const configValue = await git.getConfigValue(extraheaderConfigKey) 45 | expect(configValue).toEqual(extraheaderConfigValue) 46 | 47 | await gitAuthHelper.removeAuth() 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /github/create-pull-request/__test__/integration-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | IMAGE="cpr-integration-tests:latest" 5 | ARG1=${1:-} 6 | 7 | if [[ "$(docker images -q $IMAGE 2> /dev/null)" == "" || $ARG1 == "build" ]]; then 8 | echo "Building Docker image $IMAGE ..." 9 | 10 | cat > Dockerfile << EOF 11 | FROM node:16-alpine 12 | RUN apk --no-cache add git git-daemon 13 | RUN npm install jest jest-environment-jsdom --global 14 | WORKDIR /cpr 15 | COPY __test__/entrypoint.sh /entrypoint.sh 16 | ENTRYPOINT ["/entrypoint.sh"] 17 | EOF 18 | 19 | docker build --no-cache -t $IMAGE . 20 | rm Dockerfile 21 | fi 22 | 23 | docker run -v $PWD:/cpr $IMAGE 24 | -------------------------------------------------------------------------------- /github/create-pull-request/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Create Pull Request' 2 | description: 'Creates a pull request for changes to your repository in the actions workspace' 3 | inputs: 4 | token: 5 | description: 'GITHUB_TOKEN or a `repo` scoped Personal Access Token (PAT)' 6 | default: ${{ github.token }} 7 | path: 8 | description: > 9 | Relative path under $GITHUB_WORKSPACE to the repository. 10 | Defaults to $GITHUB_WORKSPACE. 11 | add-paths: 12 | description: > 13 | A comma or newline-separated list of file paths to commit. 14 | Paths should follow git's pathspec syntax. 15 | Defaults to adding all new and modified files. 16 | commit-message: 17 | description: 'The message to use when committing changes.' 18 | default: '[create-pull-request] automated change' 19 | committer: 20 | description: > 21 | The committer name and email address in the format `Display Name `. 22 | Defaults to the GitHub Actions bot user. 23 | default: 'GitHub ' 24 | author: 25 | description: > 26 | The author name and email address in the format `Display Name `. 27 | Defaults to the user who triggered the workflow run. 28 | default: '${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>' 29 | signoff: 30 | description: 'Add `Signed-off-by` line by the committer at the end of the commit log message.' 31 | default: false 32 | branch: 33 | description: 'The pull request branch name.' 34 | default: 'create-pull-request/patch' 35 | delete-branch: 36 | description: > 37 | Delete the `branch` when closing pull requests, and when undeleted after merging. 38 | Recommend `true`. 39 | default: false 40 | branch-suffix: 41 | description: 'The branch suffix type when using the alternative branching strategy.' 42 | base: 43 | description: > 44 | The pull request base branch. 45 | Defaults to the branch checked out in the workflow. 46 | push-to-fork: 47 | description: > 48 | A fork of the checked out parent repository to which the pull request branch will be pushed. 49 | e.g. `owner/repo-fork`. 50 | The pull request will be created to merge the fork's branch into the parent's base. 51 | title: 52 | description: 'The title of the pull request.' 53 | default: 'Changes by create-pull-request action' 54 | body: 55 | description: 'The body of the pull request.' 56 | default: 'Automated changes by [create-pull-request](https://github.com/peter-evans/create-pull-request) GitHub action' 57 | labels: 58 | description: 'A comma or newline separated list of labels.' 59 | assignees: 60 | description: 'A comma or newline separated list of assignees (GitHub usernames).' 61 | reviewers: 62 | description: 'A comma or newline separated list of reviewers (GitHub usernames) to request a review from.' 63 | team-reviewers: 64 | description: > 65 | A comma or newline separated list of GitHub teams to request a review from. 66 | Note that a `repo` scoped Personal Access Token (PAT) may be required. 67 | milestone: 68 | description: 'The number of the milestone to associate the pull request with.' 69 | draft: 70 | description: 'Create a draft pull request. It is not possible to change draft status after creation except through the web interface' 71 | default: false 72 | outputs: 73 | pull-request-number: 74 | description: 'The pull request number' 75 | pull-request-url: 76 | description: 'The URL of the pull request.' 77 | pull-request-operation: 78 | description: 'The pull request operation performed by the action, `created`, `updated` or `closed`.' 79 | pull-request-head-sha: 80 | description: 'The commit SHA of the pull request branch.' 81 | runs: 82 | using: 'node16' 83 | main: 'dist/index.js' 84 | branding: 85 | icon: 'git-pull-request' 86 | color: 'gray-dark' 87 | -------------------------------------------------------------------------------- /github/create-pull-request/docs/assets/cpr-gitgraph.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | create-pull-request GitHub action 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /github/create-pull-request/docs/assets/cpr-gitgraph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudposse-archives/actions/6a5182ba6c68f4d4a9f1d473629a47f6cd01dabd/github/create-pull-request/docs/assets/cpr-gitgraph.png -------------------------------------------------------------------------------- /github/create-pull-request/docs/assets/logo.svg: -------------------------------------------------------------------------------- 1 | git-pull-request 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /github/create-pull-request/docs/assets/pull-request-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudposse-archives/actions/6a5182ba6c68f4d4a9f1d473629a47f6cd01dabd/github/create-pull-request/docs/assets/pull-request-example.png -------------------------------------------------------------------------------- /github/create-pull-request/docs/updating.md: -------------------------------------------------------------------------------- 1 | ## Updating from `v3` to `v4` 2 | 3 | ### Breaking changes 4 | 5 | - The `add-paths` input no longer accepts `-A` as a valid value. When committing all new and modified files the `add-paths` input should be omitted. 6 | 7 | - If using self-hosted runners or GitHub Enterprise Server, there are minimum requirements for `v4` to run. See "What's new" below for details. 8 | 9 | ### What's new 10 | 11 | - Updated runtime to Node.js 16 12 | - The action now requires a minimum version of v2.285.0 for the [Actions Runner](https://github.com/actions/runner/releases/tag/v2.285.0). 13 | - If using GitHub Enterprise Server, the action requires [GHES 3.4](https://docs.github.com/en/enterprise-server@3.4/admin/release-notes) or later. 14 | 15 | ## Updating from `v2` to `v3` 16 | 17 | ### Breaking changes 18 | 19 | - The `author` input now defaults to the user who triggered the workflow run. This default is set via [action.yml](../action.yml) as `${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>`, where `github.actor` is the GitHub user account associated with the run. For example, `peter-evans `. 20 | 21 | To continue to use the `v2` default, set the `author` input as follows. 22 | ```yaml 23 | - uses: peter-evans/create-pull-request@v6 24 | with: 25 | author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> 26 | ``` 27 | 28 | - The `author` and `committer` inputs are no longer cross-used if only one is supplied. Additionally, when neither input is set, the `author` and `committer` are no longer determined from an existing identity set in git config. In both cases, the inputs will fall back to their default set in [action.yml](../action.yml). 29 | 30 | - Deprecated inputs `project` and `project-column` have been removed in favour of an additional action step. See [Create a project card](https://github.com/peter-evans/create-pull-request#create-a-project-card) for details. 31 | 32 | - Deprecated output `pr_number` has been removed in favour of `pull-request-number`. 33 | 34 | - Input `request-to-parent` has been removed in favour of `push-to-fork`. This greatly simplifies pushing the pull request branch to a fork of the parent repository. See [Push pull request branches to a fork](concepts-guidelines.md#push-pull-request-branches-to-a-fork) for details. 35 | 36 | e.g. 37 | ```yaml 38 | - uses: actions/checkout@v2 39 | 40 | # Make changes to pull request here 41 | 42 | - uses: peter-evans/create-pull-request@v6 43 | with: 44 | token: ${{ secrets.MACHINE_USER_PAT }} 45 | push-to-fork: machine-user/fork-of-repository 46 | ``` 47 | 48 | ### What's new 49 | 50 | - The action has been converted to Typescript giving it a significant performance improvement. 51 | 52 | - If you run this action in a container, or on [self-hosted runners](https://docs.github.com/en/actions/hosting-your-own-runners), `python` and `pip` are no longer required dependencies. See [Running in a container or on self-hosted runners](concepts-guidelines.md#running-in-a-container-or-on-self-hosted-runners) for details. 53 | 54 | - Inputs `labels`, `assignees`, `reviewers` and `team-reviewers` can now be newline separated, or comma separated. 55 | e.g. 56 | ```yml 57 | labels: | 58 | chore 59 | dependencies 60 | automated 61 | ``` 62 | 63 | ## Updating from `v1` to `v2` 64 | 65 | ### Breaking changes 66 | 67 | - `v2` now expects repositories to be checked out with `actions/checkout@v2` 68 | 69 | To use `actions/checkout@v1` the following step to checkout the branch is necessary. 70 | ```yml 71 | - uses: actions/checkout@v1 72 | - name: Checkout branch 73 | run: git checkout "${GITHUB_REF:11}" 74 | ``` 75 | 76 | - The two branch naming strategies have been swapped. Fixed-branch naming strategy is now the default. i.e. `branch-suffix: none` is now the default and should be removed from configuration if set. 77 | 78 | - `author-name`, `author-email`, `committer-name`, `committer-email` have been removed in favour of `author` and `committer`. 79 | They can both be set in the format `Display Name ` 80 | 81 | If neither `author` or `committer` are set the action will default to making commits as the GitHub Actions bot user. 82 | 83 | ### What's new 84 | 85 | - Unpushed commits made during the workflow before the action runs will now be considered as changes to be raised in the pull request. See [Create your own commits](https://github.com/peter-evans/create-pull-request#create-your-own-commits) for details. 86 | - New commits made to the pull request base will now be taken into account when pull requests are updated. 87 | - If an updated pull request no longer differs from its base it will automatically be closed and the pull request branch deleted. 88 | -------------------------------------------------------------------------------- /github/create-pull-request/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | moduleFileExtensions: ['js', 'ts'], 4 | testEnvironment: 'node', 5 | testMatch: ['**/*.test.ts'], 6 | testRunner: 'jest-circus/runner', 7 | transform: { 8 | '^.+\\.ts$': 'ts-jest' 9 | }, 10 | verbose: true 11 | } 12 | -------------------------------------------------------------------------------- /github/create-pull-request/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-pull-request", 3 | "version": "4.0.0", 4 | "private": true, 5 | "description": "Creates a pull request for changes to your repository in the actions workspace", 6 | "main": "lib/main.js", 7 | "scripts": { 8 | "build": "tsc && ncc build", 9 | "format": "prettier --write '**/*.ts'", 10 | "format-check": "prettier --check '**/*.ts'", 11 | "lint": "eslint src/**/*.ts", 12 | "test:unit": "jest unit", 13 | "test:int": "__test__/integration-tests.sh", 14 | "test": "npm run test:unit && npm run test:int" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/peter-evans/create-pull-request.git" 19 | }, 20 | "keywords": [ 21 | "actions", 22 | "pull", 23 | "request" 24 | ], 25 | "author": "Peter Evans", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/peter-evans/create-pull-request/issues" 29 | }, 30 | "homepage": "https://github.com/peter-evans/create-pull-request", 31 | "dependencies": { 32 | "@actions/core": "^1.10.0", 33 | "@actions/exec": "^1.1.1", 34 | "@octokit/core": "^3.5.1", 35 | "@octokit/plugin-paginate-rest": "^2.17.0", 36 | "@octokit/plugin-rest-endpoint-methods": "^5.13.0", 37 | "proxy-agent": "^5.0.0", 38 | "uuid": "^8.3.2" 39 | }, 40 | "devDependencies": { 41 | "@types/jest": "^27.5.0", 42 | "@types/node": "^16.11.11", 43 | "@typescript-eslint/parser": "^5.5.0", 44 | "@vercel/ncc": "^0.32.0", 45 | "eslint": "^8.3.0", 46 | "eslint-import-resolver-typescript": "^2.5.0", 47 | "eslint-plugin-github": "^4.3.5", 48 | "eslint-plugin-import": "^2.25.3", 49 | "eslint-plugin-jest": "^26.1.5", 50 | "jest": "^28.1.0", 51 | "jest-circus": "^28.1.0", 52 | "jest-environment-jsdom": "^28.1.0", 53 | "js-yaml": "^4.1.0", 54 | "prettier": "^2.5.0", 55 | "ts-jest": "^28.0.2", 56 | "typescript": "^4.5.2" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /github/create-pull-request/src/git-auth-helper.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import * as fs from 'fs' 3 | import {GitCommandManager} from './git-command-manager' 4 | import * as path from 'path' 5 | import {URL} from 'url' 6 | import * as utils from './utils' 7 | 8 | export class GitAuthHelper { 9 | private git: GitCommandManager 10 | private gitConfigPath: string 11 | private extraheaderConfigKey: string 12 | private extraheaderConfigPlaceholderValue = 'AUTHORIZATION: basic ***' 13 | private extraheaderConfigValueRegex = '^AUTHORIZATION:' 14 | private persistedExtraheaderConfigValue = '' 15 | 16 | constructor(git: GitCommandManager) { 17 | this.git = git 18 | this.gitConfigPath = path.join( 19 | this.git.getWorkingDirectory(), 20 | '.git', 21 | 'config' 22 | ) 23 | const serverUrl = this.getServerUrl() 24 | this.extraheaderConfigKey = `http.${serverUrl.origin}/.extraheader` 25 | } 26 | 27 | async savePersistedAuth(): Promise { 28 | // Save and unset persisted extraheader credential in git config if it exists 29 | this.persistedExtraheaderConfigValue = await this.getAndUnset() 30 | } 31 | 32 | async restorePersistedAuth(): Promise { 33 | if (this.persistedExtraheaderConfigValue) { 34 | try { 35 | await this.setExtraheaderConfig(this.persistedExtraheaderConfigValue) 36 | core.info('Persisted git credentials restored') 37 | } catch (e) { 38 | core.warning(utils.getErrorMessage(e)) 39 | } 40 | } 41 | } 42 | 43 | async configureToken(token: string): Promise { 44 | // Encode and configure the basic credential for HTTPS access 45 | const basicCredential = Buffer.from( 46 | `x-access-token:${token}`, 47 | 'utf8' 48 | ).toString('base64') 49 | core.setSecret(basicCredential) 50 | const extraheaderConfigValue = `AUTHORIZATION: basic ${basicCredential}` 51 | await this.setExtraheaderConfig(extraheaderConfigValue) 52 | } 53 | 54 | async removeAuth(): Promise { 55 | await this.getAndUnset() 56 | } 57 | 58 | private async setExtraheaderConfig( 59 | extraheaderConfigValue: string 60 | ): Promise { 61 | // Configure a placeholder value. This approach avoids the credential being captured 62 | // by process creation audit events, which are commonly logged. For more information, 63 | // refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing 64 | // See https://github.com/actions/checkout/blob/main/src/git-auth-helper.ts#L267-L274 65 | await this.git.config( 66 | this.extraheaderConfigKey, 67 | this.extraheaderConfigPlaceholderValue 68 | ) 69 | // Replace the placeholder 70 | await this.gitConfigStringReplace( 71 | this.extraheaderConfigPlaceholderValue, 72 | extraheaderConfigValue 73 | ) 74 | } 75 | 76 | private async getAndUnset(): Promise { 77 | let configValue = '' 78 | // Save and unset persisted extraheader credential in git config if it exists 79 | if ( 80 | await this.git.configExists( 81 | this.extraheaderConfigKey, 82 | this.extraheaderConfigValueRegex 83 | ) 84 | ) { 85 | configValue = await this.git.getConfigValue( 86 | this.extraheaderConfigKey, 87 | this.extraheaderConfigValueRegex 88 | ) 89 | if ( 90 | await this.git.tryConfigUnset( 91 | this.extraheaderConfigKey, 92 | this.extraheaderConfigValueRegex 93 | ) 94 | ) { 95 | core.info(`Unset config key '${this.extraheaderConfigKey}'`) 96 | } else { 97 | core.warning( 98 | `Failed to unset config key '${this.extraheaderConfigKey}'` 99 | ) 100 | } 101 | } 102 | return configValue 103 | } 104 | 105 | private async gitConfigStringReplace( 106 | find: string, 107 | replace: string 108 | ): Promise { 109 | let content = (await fs.promises.readFile(this.gitConfigPath)).toString() 110 | const index = content.indexOf(find) 111 | if (index < 0 || index != content.lastIndexOf(find)) { 112 | throw new Error(`Unable to replace '${find}' in ${this.gitConfigPath}`) 113 | } 114 | content = content.replace(find, replace) 115 | await fs.promises.writeFile(this.gitConfigPath, content) 116 | } 117 | 118 | private getServerUrl(): URL { 119 | // todo: remove GITHUB_URL after support for GHES Alpha is no longer needed 120 | // See https://github.com/actions/checkout/blob/main/src/url-helper.ts#L22-L29 121 | return new URL( 122 | process.env['GITHUB_SERVER_URL'] || 123 | process.env['GITHUB_URL'] || 124 | 'https://github.com' 125 | ) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /github/create-pull-request/src/main.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import {Inputs, createPullRequest} from './create-pull-request' 3 | import {inspect} from 'util' 4 | import * as utils from './utils' 5 | 6 | async function run(): Promise { 7 | try { 8 | const inputs: Inputs = { 9 | token: core.getInput('token'), 10 | path: core.getInput('path'), 11 | addPaths: utils.getInputAsArray('add-paths'), 12 | commitMessage: core.getInput('commit-message'), 13 | committer: core.getInput('committer'), 14 | author: core.getInput('author'), 15 | signoff: core.getBooleanInput('signoff'), 16 | branch: core.getInput('branch'), 17 | deleteBranch: core.getBooleanInput('delete-branch'), 18 | branchSuffix: core.getInput('branch-suffix'), 19 | base: core.getInput('base'), 20 | pushToFork: core.getInput('push-to-fork'), 21 | title: core.getInput('title'), 22 | body: core.getInput('body'), 23 | labels: utils.getInputAsArray('labels'), 24 | assignees: utils.getInputAsArray('assignees'), 25 | reviewers: utils.getInputAsArray('reviewers'), 26 | teamReviewers: utils.getInputAsArray('team-reviewers'), 27 | milestone: Number(core.getInput('milestone')), 28 | draft: core.getBooleanInput('draft') 29 | } 30 | core.debug(`Inputs: ${inspect(inputs)}`) 31 | 32 | await createPullRequest(inputs) 33 | } catch (error) { 34 | core.setFailed(utils.getErrorMessage(error)) 35 | } 36 | } 37 | 38 | run() 39 | -------------------------------------------------------------------------------- /github/create-pull-request/src/octokit-client.ts: -------------------------------------------------------------------------------- 1 | import {Octokit as Core} from '@octokit/core' 2 | import {paginateRest} from '@octokit/plugin-paginate-rest' 3 | import {restEndpointMethods} from '@octokit/plugin-rest-endpoint-methods' 4 | import ProxyAgent from 'proxy-agent' 5 | export {RestEndpointMethodTypes} from '@octokit/plugin-rest-endpoint-methods' 6 | export {OctokitOptions} from '@octokit/core/dist-types/types' 7 | 8 | export const Octokit = Core.plugin( 9 | paginateRest, 10 | restEndpointMethods, 11 | autoProxyAgent 12 | ) 13 | 14 | // Octokit plugin to support the standard environment variables http_proxy, https_proxy and no_proxy 15 | function autoProxyAgent(octokit: Core) { 16 | const proxy = 17 | process.env.https_proxy || 18 | process.env.HTTPS_PROXY || 19 | process.env.http_proxy || 20 | process.env.HTTP_PROXY 21 | 22 | if (!proxy) return 23 | 24 | const agent = new ProxyAgent() 25 | octokit.hook.before('request', options => { 26 | options.request.agent = agent 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /github/create-pull-request/src/utils.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import * as fs from 'fs' 3 | import * as path from 'path' 4 | 5 | export function getInputAsArray( 6 | name: string, 7 | options?: core.InputOptions 8 | ): string[] { 9 | return getStringAsArray(core.getInput(name, options)) 10 | } 11 | 12 | export function getStringAsArray(str: string): string[] { 13 | return str 14 | .split(/[\n,]+/) 15 | .map(s => s.trim()) 16 | .filter(x => x !== '') 17 | } 18 | 19 | export function getRepoPath(relativePath?: string): string { 20 | let githubWorkspacePath = process.env['GITHUB_WORKSPACE'] 21 | if (!githubWorkspacePath) { 22 | throw new Error('GITHUB_WORKSPACE not defined') 23 | } 24 | githubWorkspacePath = path.resolve(githubWorkspacePath) 25 | core.debug(`githubWorkspacePath: ${githubWorkspacePath}`) 26 | 27 | let repoPath = githubWorkspacePath 28 | if (relativePath) repoPath = path.resolve(repoPath, relativePath) 29 | 30 | core.debug(`repoPath: ${repoPath}`) 31 | return repoPath 32 | } 33 | 34 | interface RemoteDetail { 35 | hostname: string 36 | protocol: string 37 | repository: string 38 | } 39 | 40 | export function getRemoteDetail(remoteUrl: string): RemoteDetail { 41 | // Parse the protocol and github repository from a URL 42 | // e.g. HTTPS, peter-evans/create-pull-request 43 | const githubUrl = process.env['GITHUB_SERVER_URL'] || 'https://github.com' 44 | 45 | const githubServerMatch = githubUrl.match(/^https?:\/\/(.+)$/i) 46 | if (!githubServerMatch) { 47 | throw new Error('Could not parse GitHub Server name') 48 | } 49 | 50 | const hostname = githubServerMatch[1] 51 | 52 | const httpsUrlPattern = new RegExp( 53 | '^https?://.*@?' + hostname + '/(.+/.+?)(\\.git)?$', 54 | 'i' 55 | ) 56 | const sshUrlPattern = new RegExp('^git@' + hostname + ':(.+/.+)\\.git$', 'i') 57 | 58 | const httpsMatch = remoteUrl.match(httpsUrlPattern) 59 | if (httpsMatch) { 60 | return { 61 | hostname, 62 | protocol: 'HTTPS', 63 | repository: httpsMatch[1] 64 | } 65 | } 66 | 67 | const sshMatch = remoteUrl.match(sshUrlPattern) 68 | if (sshMatch) { 69 | return { 70 | hostname, 71 | protocol: 'SSH', 72 | repository: sshMatch[1] 73 | } 74 | } 75 | 76 | throw new Error( 77 | `The format of '${remoteUrl}' is not a valid GitHub repository URL` 78 | ) 79 | } 80 | 81 | export function getRemoteUrl( 82 | protocol: string, 83 | hostname: string, 84 | repository: string 85 | ): string { 86 | return protocol == 'HTTPS' 87 | ? `https://${hostname}/${repository}` 88 | : `git@${hostname}:${repository}.git` 89 | } 90 | 91 | export function secondsSinceEpoch(): number { 92 | const now = new Date() 93 | return Math.round(now.getTime() / 1000) 94 | } 95 | 96 | export function randomString(): string { 97 | return Math.random().toString(36).substr(2, 7) 98 | } 99 | 100 | interface DisplayNameEmail { 101 | name: string 102 | email: string 103 | } 104 | 105 | export function parseDisplayNameEmail( 106 | displayNameEmail: string 107 | ): DisplayNameEmail { 108 | // Parse the name and email address from a string in the following format 109 | // Display Name 110 | const pattern = /^([^<]+)\s*<([^>]+)>$/i 111 | 112 | // Check we have a match 113 | const match = displayNameEmail.match(pattern) 114 | if (!match) { 115 | throw new Error( 116 | `The format of '${displayNameEmail}' is not a valid email address with display name` 117 | ) 118 | } 119 | 120 | // Check that name and email are not just whitespace 121 | const name = match[1].trim() 122 | const email = match[2].trim() 123 | if (!name || !email) { 124 | throw new Error( 125 | `The format of '${displayNameEmail}' is not a valid email address with display name` 126 | ) 127 | } 128 | 129 | return { 130 | name: name, 131 | email: email 132 | } 133 | } 134 | 135 | export function fileExistsSync(path: string): boolean { 136 | if (!path) { 137 | throw new Error("Arg 'path' must not be empty") 138 | } 139 | 140 | let stats: fs.Stats 141 | try { 142 | stats = fs.statSync(path) 143 | } catch (error) { 144 | if (hasErrorCode(error) && error.code === 'ENOENT') { 145 | return false 146 | } 147 | 148 | throw new Error( 149 | `Encountered an error when checking whether path '${path}' exists: ${getErrorMessage( 150 | error 151 | )}` 152 | ) 153 | } 154 | 155 | if (!stats.isDirectory()) { 156 | return true 157 | } 158 | 159 | return false 160 | } 161 | 162 | /* eslint-disable @typescript-eslint/no-explicit-any */ 163 | function hasErrorCode(error: any): error is {code: string} { 164 | return typeof (error && error.code) === 'string' 165 | } 166 | 167 | export function getErrorMessage(error: unknown) { 168 | if (error instanceof Error) return error.message 169 | return String(error) 170 | } 171 | -------------------------------------------------------------------------------- /github/create-pull-request/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es6" 7 | ], 8 | "outDir": "./lib", 9 | "rootDir": "./src", 10 | "declaration": true, 11 | "strict": true, 12 | "noImplicitAny": false, 13 | "esModuleInterop": true 14 | }, 15 | "exclude": ["__test__", "lib", "node_modules"] 16 | } 17 | -------------------------------------------------------------------------------- /github/crud/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.16 2 | 3 | LABEL maintainer="Cloud Posse " 4 | 5 | LABEL "com.github.actions.name"="CRUD" 6 | LABEL "com.github.actions.description"="Synchronize Github workflows to a repository" 7 | LABEL "com.github.actions.icon"="activity" 8 | LABEL "com.github.actions.color"="blue" 9 | 10 | COPY entrypoint.sh / 11 | 12 | RUN chmod 755 /entrypoint.sh \ 13 | && apk --no-cache add bash git 14 | 15 | ENTRYPOINT ["/entrypoint.sh"] 16 | -------------------------------------------------------------------------------- /github/crud/action.yaml: -------------------------------------------------------------------------------- 1 | name: 'crud' 2 | description: 'Synchronize Github workflows to a repository' 3 | author: 'Cloud Posse ' 4 | inputs: 5 | repo: 6 | required: true 7 | catalog: 8 | required: true 9 | version: 10 | required: false 11 | default: master 12 | workflows: 13 | required: true 14 | runs: 15 | using: 'docker' 16 | image: 'Dockerfile' 17 | branding: 18 | icon: 'activity' 19 | color: 'blue' -------------------------------------------------------------------------------- /github/crud/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eu 4 | 5 | echo REPO: $INPUT_REPO 6 | echo VERSION: $INPUT_VERSION 7 | echo WORKFLOWS: $INPUT_WORKFLOWS 8 | echo CATALOG: $INPUT_CATALOG 9 | 10 | git clone --branch ${INPUT_VERSION} ${INPUT_REPO} ../actions 11 | 12 | for workflow in $(tr , ' ' <<<${INPUT_WORKFLOWS}); do 13 | # Install the workflow 14 | cp -a ../actions/${INPUT_CATALOG}/${workflow}.yaml .github/workflows/ 15 | done 16 | -------------------------------------------------------------------------------- /github/git-push/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.16 2 | 3 | LABEL maintainer="Cloud Posse " 4 | 5 | LABEL "com.github.actions.name"="Git Push" 6 | LABEL "com.github.actions.description"="Push Changes to Origin" 7 | LABEL "com.github.actions.icon"="activity" 8 | LABEL "com.github.actions.color"="blue" 9 | 10 | RUN apk add --no-cache \ 11 | bash \ 12 | ca-certificates \ 13 | curl \ 14 | jq \ 15 | git 16 | 17 | COPY entrypoint.sh / 18 | 19 | RUN chmod 755 /entrypoint.sh 20 | 21 | ENTRYPOINT ["/entrypoint.sh"] 22 | -------------------------------------------------------------------------------- /github/git-push/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Git Push' 2 | description: 'Push Changes to Origin' 3 | author: 'Cloud Posse ' 4 | runs: 5 | using: 'docker' 6 | image: 'Dockerfile' 7 | branding: 8 | icon: 'activity' 9 | color: 'blue' 10 | -------------------------------------------------------------------------------- /github/git-push/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -x 4 | export GIT_COMMIT_MESSAGE="${GIT_COMMIT_MESSAGE:-autocommit}" 5 | export GIT_DIRECTORY="${GIT_DIRECTORY:-$(pwd)}" 6 | export GIT_BRANCH="${GIT_BRANCH:-$GITHUB_HEAD_REF}" 7 | 8 | # Testing for either presence of untracked files or presence of modifications in tracked files. 9 | if [[ "$(git ls-files --others --directory --exclude-standard | sed q | wc -l)" -ne 0 || "$(git ls-files --others --directory --exclude-standard | sed q | wc -l)" -ne 0 ]]; then 10 | echo "Changes detected." 11 | git -C ${GIT_DIRECTORY} config user.name "$(git -C ${GIT_DIRECTORY} --no-pager log --format=format:'%an' -n 1)" 12 | git -C ${GIT_DIRECTORY} config user.email "$(git -C ${GIT_DIRECTORY} --no-pager log --format=format:'%ae' -n 1)" 13 | 14 | git -C ${GIT_DIRECTORY} remote set-url origin "https://${GITHUB_ACTOR}:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" 15 | git -C ${GIT_DIRECTORY} remote -v 16 | git -C ${GIT_DIRECTORY} add . 17 | git -C ${GIT_DIRECTORY} commit -m "${GIT_COMMIT_MESSAGE}" 18 | git -C ${GIT_DIRECTORY} push origin HEAD:${GIT_BRANCH} 19 | else 20 | echo "No changes detected." 21 | fi 22 | -------------------------------------------------------------------------------- /github/release-assets/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.16 2 | 3 | LABEL maintainer="Cloud Posse " 4 | 5 | LABEL "com.github.actions.name"="Release Assets" 6 | LABEL "com.github.actions.description"="Attach assets to GitHub Release" 7 | LABEL "com.github.actions.icon"="activity" 8 | LABEL "com.github.actions.color"="blue" 9 | 10 | RUN apk add --no-cache \ 11 | bash \ 12 | ca-certificates \ 13 | curl \ 14 | jq \ 15 | git 16 | 17 | COPY entrypoint.sh / 18 | 19 | RUN chmod 755 /entrypoint.sh 20 | 21 | ENTRYPOINT ["/entrypoint.sh"] 22 | -------------------------------------------------------------------------------- /github/release-assets/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Release Assets' 2 | description: 'Attach assets to GitHub Release' 3 | author: 'Cloud Posse ' 4 | runs: 5 | using: 'docker' 6 | image: 'Dockerfile' 7 | branding: 8 | icon: 'activity' 9 | color: 'blue' 10 | -------------------------------------------------------------------------------- /github/release-assets/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -x 4 | 5 | if [[ -z "$GITHUB_TOKEN" ]]; then 6 | echo "Missing GITHUB_TOKEN env variable" 7 | exit 1 8 | fi 9 | 10 | if [[ -z "$INPUT_PATH" ]]; then 11 | echo "Missing INPUT_PATH env variable" 12 | exit 1 13 | fi 14 | 15 | RELEASE_ID=$(jq --raw-output '.release.id' "$GITHUB_EVENT_PATH") 16 | if [ -z "$RELEASE_ID" ]; then 17 | echo "Release ID is not set, will not upload binaries" 18 | exit 0 19 | fi 20 | 21 | IS_DRAFT=$(jq --raw-output '.release.draft' "$GITHUB_EVENT_PATH") 22 | if [ "$IS_DRAFT" = true ]; then 23 | echo "This is a draft release, will not upload binaries" 24 | exit 0 25 | fi 26 | 27 | AUTH_HEADER="Authorization: token ${GITHUB_TOKEN}" 28 | 29 | for file in ${INPUT_PATH}; do 30 | echo "Uploading file ${file}" 31 | 32 | if [[ ! -f "$file" || ! -s "$file" ]]; then 33 | echo "WARNING: File ${file} does not exist or is empty" 34 | continue 35 | fi 36 | 37 | FILENAME=$(basename "${file}") 38 | UPLOAD_URL="https://uploads.github.com/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}/assets?name=${FILENAME}" 39 | tmp=$(mktemp) 40 | 41 | response=$(curl \ 42 | -sSL \ 43 | -XPOST \ 44 | -H "${AUTH_HEADER}" \ 45 | --upload-file "${file}" \ 46 | --header "Content-Type:application/octet-stream" \ 47 | --write-out "%{http_code}" \ 48 | --output $tmp \ 49 | "${UPLOAD_URL}") 50 | 51 | if [ "$?" -ne 0 ]; then 52 | echo "ERROR: 'curl' returned error" 53 | cat $tmp 54 | rm $tmp 55 | exit 1 56 | fi 57 | 58 | if [ "$response" -ge 400 ]; then 59 | echo "ERROR: Upload was not successful. HTTP status: $response" 60 | cat $tmp 61 | rm $tmp 62 | exit 1 63 | fi 64 | 65 | rm $tmp 66 | 67 | done 68 | -------------------------------------------------------------------------------- /github/repository-dispatch/.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /github/repository-dispatch/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { "node": true, "jest": true }, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { "ecmaVersion": 9, "sourceType": "module" }, 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/eslint-recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "plugin:import/errors", 10 | "plugin:import/warnings", 11 | "plugin:import/typescript", 12 | "plugin:prettier/recommended" 13 | ], 14 | "plugins": ["@typescript-eslint"], 15 | "rules": { 16 | "@typescript-eslint/camelcase": "off" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /github/repository-dispatch/.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: peter-evans -------------------------------------------------------------------------------- /github/repository-dispatch/.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: [main] 5 | paths-ignore: 6 | - 'README.md' 7 | - 'docs/**' 8 | pull_request: 9 | branches: [main] 10 | paths-ignore: 11 | - 'README.md' 12 | - 'docs/**' 13 | 14 | permissions: 15 | pull-requests: write 16 | contents: write 17 | 18 | jobs: 19 | build: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v3 23 | - uses: actions/setup-node@v3 24 | with: 25 | node-version: 16.x 26 | - run: npm ci 27 | - run: npm run build 28 | - run: npm run format-check 29 | - run: npm run lint 30 | - run: npm run test 31 | - uses: actions/upload-artifact@v3 32 | with: 33 | name: dist 34 | path: dist 35 | - uses: actions/upload-artifact@v3 36 | with: 37 | name: action.yml 38 | path: action.yml 39 | 40 | test: 41 | if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository 42 | needs: [build] 43 | runs-on: ubuntu-latest 44 | strategy: 45 | matrix: 46 | target: [built, committed] 47 | steps: 48 | - uses: actions/checkout@v3 49 | - if: matrix.target == 'built' || github.event_name == 'pull_request' 50 | uses: actions/download-artifact@v3 51 | with: 52 | name: dist 53 | path: dist 54 | - if: matrix.target == 'built' || github.event_name == 'pull_request' 55 | uses: actions/download-artifact@v3 56 | with: 57 | name: action.yml 58 | path: . 59 | 60 | - name: Test repository dispatch 61 | uses: ./ 62 | with: 63 | event-type: tests 64 | client-payload: '{"ref": "${{ github.ref }}", "sha": "${{ github.sha }}"}' 65 | 66 | - name: Test repository dispatch (default payload) 67 | uses: ./ 68 | with: 69 | event-type: tests 70 | 71 | package: 72 | if: github.event_name == 'push' && github.ref == 'refs/heads/main' 73 | needs: [test] 74 | runs-on: ubuntu-latest 75 | steps: 76 | - uses: actions/checkout@v3 77 | - uses: actions/download-artifact@v3 78 | with: 79 | name: dist 80 | path: dist 81 | - name: Create Pull Request 82 | uses: peter-evans/create-pull-request@v6 83 | with: 84 | commit-message: Update distribution 85 | title: Update distribution 86 | body: | 87 | - Updates the distribution for changes on `main` 88 | 89 | Auto-generated by [create-pull-request][1] 90 | 91 | [1]: https://github.com/peter-evans/create-pull-request 92 | branch: update-distribution 93 | -------------------------------------------------------------------------------- /github/repository-dispatch/.github/workflows/on-repository-dispatch.yml: -------------------------------------------------------------------------------- 1 | name: Repository Dispatch 2 | on: 3 | repository_dispatch: 4 | types: [tests] 5 | jobs: 6 | tests: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Dump the client payload context 10 | env: 11 | PAYLOAD_CONTEXT: ${{ toJson(github.event.client_payload) }} 12 | run: echo "$PAYLOAD_CONTEXT" 13 | 14 | - uses: actions/checkout@v3 15 | if: github.event.client_payload.ref != '' 16 | with: 17 | ref: ${{ github.event.client_payload.ref }} 18 | 19 | - run: echo ${{ github.event.client_payload.sha }} 20 | -------------------------------------------------------------------------------- /github/repository-dispatch/.github/workflows/slash-command-dispatch.yml: -------------------------------------------------------------------------------- 1 | name: Slash Command Dispatch 2 | on: 3 | issue_comment: 4 | types: [created] 5 | jobs: 6 | slashCommandDispatch: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Slash Command Dispatch 10 | uses: peter-evans/slash-command-dispatch@v3 11 | with: 12 | token: ${{ secrets.ACTIONS_BOT_TOKEN }} 13 | config: > 14 | [ 15 | { 16 | "command": "rebase", 17 | "permission": "admin", 18 | "repository": "peter-evans/slash-command-dispatch-processor", 19 | "issue_type": "pull-request" 20 | } 21 | ] 22 | -------------------------------------------------------------------------------- /github/repository-dispatch/.gitignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /github/repository-dispatch/.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /github/repository-dispatch/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": false, 6 | "singleQuote": true, 7 | "trailingComma": "none", 8 | "bracketSpacing": false, 9 | "arrowParens": "avoid", 10 | "parser": "typescript" 11 | } -------------------------------------------------------------------------------- /github/repository-dispatch/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Peter Evans 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /github/repository-dispatch/README.md: -------------------------------------------------------------------------------- 1 | # Repository Dispatch 2 | [![CI](https://github.com/peter-evans/repository-dispatch/workflows/CI/badge.svg)](https://github.com/peter-evans/repository-dispatch/actions?query=workflow%3ACI) 3 | [![GitHub Marketplace](https://img.shields.io/badge/Marketplace-Repository%20Dispatch-blue.svg?colorA=24292e&colorB=0366d6&style=flat&longCache=true&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAM6wAADOsB5dZE0gAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAERSURBVCiRhZG/SsMxFEZPfsVJ61jbxaF0cRQRcRJ9hlYn30IHN/+9iquDCOIsblIrOjqKgy5aKoJQj4O3EEtbPwhJbr6Te28CmdSKeqzeqr0YbfVIrTBKakvtOl5dtTkK+v4HfA9PEyBFCY9AGVgCBLaBp1jPAyfAJ/AAdIEG0dNAiyP7+K1qIfMdonZic6+WJoBJvQlvuwDqcXadUuqPA1NKAlexbRTAIMvMOCjTbMwl1LtI/6KWJ5Q6rT6Ht1MA58AX8Apcqqt5r2qhrgAXQC3CZ6i1+KMd9TRu3MvA3aH/fFPnBodb6oe6HM8+lYHrGdRXW8M9bMZtPXUji69lmf5Cmamq7quNLFZXD9Rq7v0Bpc1o/tp0fisAAAAASUVORK5CYII=)](https://github.com/marketplace/actions/repository-dispatch) 4 | 5 | A GitHub action to create a repository dispatch event. 6 | 7 | ## Usage 8 | 9 | Dispatch an event to the current repository. 10 | ```yml 11 | - name: Repository Dispatch 12 | uses: peter-evans/repository-dispatch@v2 13 | with: 14 | event-type: my-event 15 | ``` 16 | 17 | Dispatch an event to a remote repository using a `repo` scoped [Personal Access Token (PAT)](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token). 18 | ```yml 19 | - name: Repository Dispatch 20 | uses: peter-evans/repository-dispatch@v2 21 | with: 22 | token: ${{ secrets.PAT }} 23 | event-type: my-event 24 | ``` 25 | 26 | ### Action inputs 27 | 28 | | Name | Description | Default | 29 | | --- | --- | --- | 30 | | `token` | `GITHUB_TOKEN` (permissions `contents: write`) or a `repo` scoped [Personal Access Token (PAT)](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token). See [token](#token) for further details. | `GITHUB_TOKEN` | 31 | | `repository` | The full name of the repository to send the dispatch. | `github.repository` (current repository) | 32 | | `event-type` | (**required**) A custom webhook event name. | | 33 | | `client-payload` | JSON payload with extra information about the webhook event that your action or workflow may use. | `{}` | 34 | 35 | #### Token 36 | 37 | This action creates [`repository_dispatch`](https://docs.github.com/en/rest/repos/repos#create-a-repository-dispatch-event) events. 38 | The default `GITHUB_TOKEN` token can only be used if you are dispatching the same repository that the workflow is executing in. 39 | 40 | To dispatch to a remote repository you must create a [Personal Access Token (PAT)](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) with the `repo` scope and store it as a secret. 41 | If you will be dispatching to a public repository then you can use the more limited `public_repo` scope. 42 | 43 | ## Example 44 | 45 | Here is an example setting all of the input parameters. 46 | 47 | ```yml 48 | - name: Repository Dispatch 49 | uses: peter-evans/repository-dispatch@v2 50 | with: 51 | token: ${{ secrets.PAT }} 52 | repository: username/my-repo 53 | event-type: my-event 54 | client-payload: '{"ref": "${{ github.ref }}", "sha": "${{ github.sha }}"}' 55 | ``` 56 | 57 | Here is an example `on: repository_dispatch` workflow to receive the event. 58 | Note that repository dispatch events will only trigger a workflow run if the workflow is committed to the default branch. 59 | 60 | ```yml 61 | name: Repository Dispatch 62 | on: 63 | repository_dispatch: 64 | types: [my-event] 65 | jobs: 66 | myEvent: 67 | runs-on: ubuntu-latest 68 | steps: 69 | - uses: actions/checkout@v3 70 | with: 71 | ref: ${{ github.event.client_payload.ref }} 72 | - run: echo ${{ github.event.client_payload.sha }} 73 | ``` 74 | 75 | ### Dispatch to multiple repositories 76 | 77 | You can dispatch to multiple repositories by using a [matrix strategy](https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstrategymatrix). In the following example, after the `build` job succeeds, an event is dispatched to three different repositories. 78 | 79 | ```yml 80 | jobs: 81 | build: 82 | # Main workflow job that builds, tests, etc. 83 | 84 | dispatch: 85 | needs: build 86 | strategy: 87 | matrix: 88 | repo: ['my-org/repo1', 'my-org/repo2', 'my-org/repo3'] 89 | runs-on: ubuntu-latest 90 | steps: 91 | - name: Repository Dispatch 92 | uses: peter-evans/repository-dispatch@v2 93 | with: 94 | token: ${{ secrets.PAT }} 95 | repository: ${{ matrix.repo }} 96 | event-type: my-event 97 | ``` 98 | 99 | ## Client payload 100 | 101 | The GitHub API allows a maximum of 10 top-level properties in the `client-payload` JSON. 102 | If you use more than that you will see an error message like the following. 103 | 104 | ``` 105 | No more than 10 properties are allowed; 14 were supplied. 106 | ``` 107 | 108 | For example, this payload will fail because it has more than 10 top-level properties. 109 | 110 | ```yml 111 | client-payload: ${{ toJson(github) }} 112 | ``` 113 | 114 | To solve this you can simply wrap the payload in a single top-level property. 115 | The following payload will succeed. 116 | 117 | ```yml 118 | client-payload: '{"github": ${{ toJson(github) }}}' 119 | ``` 120 | 121 | Additionally, there is a limitation on the total data size of the `client-payload`. A very large payload may result in a `client_payload is too large` error. 122 | 123 | ## License 124 | 125 | [MIT](LICENSE) 126 | -------------------------------------------------------------------------------- /github/repository-dispatch/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Repository Dispatch' 2 | description: 'Create a repository dispatch event' 3 | inputs: 4 | token: 5 | description: 'GITHUB_TOKEN or a `repo` scoped Personal Access Token (PAT)' 6 | default: ${{ github.token }} 7 | repository: 8 | description: 'The full name of the repository to send the dispatch.' 9 | default: ${{ github.repository }} 10 | event-type: 11 | description: 'A custom webhook event name.' 12 | required: true 13 | client-payload: 14 | description: 'JSON payload with extra information about the webhook event that your action or worklow may use.' 15 | default: '{}' 16 | runs: 17 | using: 'node16' 18 | main: 'dist/index.js' 19 | branding: 20 | icon: 'target' 21 | color: 'gray-dark' 22 | -------------------------------------------------------------------------------- /github/repository-dispatch/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | moduleFileExtensions: ['js', 'ts'], 4 | testEnvironment: 'node', 5 | testMatch: ['**/*.test.ts'], 6 | testRunner: 'jest-circus/runner', 7 | transform: { 8 | '^.+\\.ts$': 'ts-jest' 9 | }, 10 | verbose: true 11 | } 12 | -------------------------------------------------------------------------------- /github/repository-dispatch/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "repository-dispatch", 3 | "version": "2.0.0", 4 | "private": true, 5 | "description": "Create a repository dispatch event", 6 | "main": "lib/main.js", 7 | "scripts": { 8 | "build": "tsc && ncc build", 9 | "format": "prettier --write '**/*.ts'", 10 | "format-check": "prettier --check '**/*.ts'", 11 | "lint": "eslint src/**/*.ts", 12 | "test": "jest --passWithNoTests" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/peter-evans/repository-dispatch.git" 17 | }, 18 | "keywords": [ 19 | "actions", 20 | "repository", 21 | "dispatch" 22 | ], 23 | "author": "Peter Evans", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/peter-evans/repository-dispatch/issues" 27 | }, 28 | "homepage": "https://github.com/peter-evans/repository-dispatch#readme", 29 | "dependencies": { 30 | "@actions/core": "^1.10.0", 31 | "@actions/github": "^5.1.1" 32 | }, 33 | "devDependencies": { 34 | "@types/jest": "^27.0.3", 35 | "@types/node": "^18.11.11", 36 | "@typescript-eslint/parser": "^5.5.0", 37 | "@vercel/ncc": "^0.36.0", 38 | "eslint": "^8.3.0", 39 | "eslint-plugin-github": "^4.6.0", 40 | "eslint-plugin-jest": "^25.3.0", 41 | "jest": "^27.4.3", 42 | "jest-circus": "^29.3.1", 43 | "js-yaml": "^4.1.0", 44 | "prettier": "^2.8.1", 45 | "ts-jest": "^27.0.7", 46 | "typescript": "^4.5.2" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /github/repository-dispatch/src/main.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import * as github from '@actions/github' 3 | import {inspect} from 'util' 4 | 5 | /* eslint-disable @typescript-eslint/no-explicit-any */ 6 | function hasErrorStatus(error: any): error is {status: number} { 7 | return typeof error.code === 'number' 8 | } 9 | 10 | function getErrorMessage(error: unknown) { 11 | if (error instanceof Error) return error.message 12 | return String(error) 13 | } 14 | 15 | async function run(): Promise { 16 | try { 17 | const inputs = { 18 | token: core.getInput('token'), 19 | repository: core.getInput('repository'), 20 | eventType: core.getInput('event-type'), 21 | clientPayload: core.getInput('client-payload') 22 | } 23 | core.debug(`Inputs: ${inspect(inputs)}`) 24 | 25 | const [owner, repo] = inputs.repository.split('/') 26 | 27 | const octokit = github.getOctokit(inputs.token) 28 | 29 | await octokit.rest.repos.createDispatchEvent({ 30 | owner: owner, 31 | repo: repo, 32 | event_type: inputs.eventType, 33 | client_payload: JSON.parse(inputs.clientPayload) 34 | }) 35 | } catch (error) { 36 | core.debug(inspect(error)) 37 | if (hasErrorStatus(error) && error.status == 404) { 38 | core.setFailed( 39 | 'Repository not found, OR token has insufficient permissions.' 40 | ) 41 | } else { 42 | core.setFailed(getErrorMessage(error)) 43 | } 44 | } 45 | } 46 | 47 | run() 48 | -------------------------------------------------------------------------------- /github/repository-dispatch/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es6" 7 | ], 8 | "outDir": "./lib", 9 | "rootDir": "./src", 10 | "declaration": true, 11 | "strict": true, 12 | "noImplicitAny": false, 13 | "esModuleInterop": true 14 | }, 15 | "exclude": ["__test__", "lib", "node_modules"] 16 | } 17 | -------------------------------------------------------------------------------- /github/slash-command-dispatch/.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /github/slash-command-dispatch/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { "node": true, "jest": true }, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { "ecmaVersion": 9, "sourceType": "module" }, 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/eslint-recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "plugin:import/errors", 10 | "plugin:import/warnings", 11 | "plugin:import/typescript", 12 | "plugin:prettier/recommended" 13 | ], 14 | "plugins": ["@typescript-eslint"], 15 | "rules": { 16 | "@typescript-eslint/camelcase": "off" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /github/slash-command-dispatch/.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: peter-evans -------------------------------------------------------------------------------- /github/slash-command-dispatch/.github/slash-command-dispatch.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "command": "create", 4 | "permission": "write", 5 | "issue_type": "issue", 6 | "event_type_suffix": "-cmd" 7 | }, 8 | { 9 | "command": "delete", 10 | "permission": "write", 11 | "issue_type": "both", 12 | "allow_edits": true, 13 | "static_args": [ 14 | "some-unnamed-arg", 15 | "foo=bar" 16 | ] 17 | }, 18 | { 19 | "command": "update", 20 | "permission": "write", 21 | "issue_type": "issue", 22 | "dispatch_type": "workflow" 23 | }, 24 | { 25 | "command": "do-something-remotely", 26 | "permission": "write", 27 | "issue_type": "both", 28 | "repository": "peter-evans/slash-command-dispatch-processor", 29 | "event_type_suffix": "-cmd" 30 | }, 31 | { 32 | "command": "send-to-multiple-repos", 33 | "repository": "peter-evans/slash-command-dispatch-processor" 34 | }, 35 | { 36 | "command": "send-to-multiple-repos", 37 | "repository": "peter-evans/slash-command-dispatch" 38 | }, 39 | { 40 | "command": "analyze", 41 | "permission": "admin", 42 | "issue_type": "pull-request", 43 | "allow_edits": true, 44 | "event_type_suffix": "-cmd" 45 | } 46 | ] 47 | -------------------------------------------------------------------------------- /github/slash-command-dispatch/.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: [main] 5 | paths-ignore: 6 | - 'README.md' 7 | - 'docs/**' 8 | pull_request: 9 | branches: [main] 10 | paths-ignore: 11 | - 'README.md' 12 | - 'docs/**' 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: actions/setup-node@v3 19 | with: 20 | node-version: 16.x 21 | - run: npm ci 22 | - run: npm run build 23 | - run: npm run format-check 24 | - run: npm run lint 25 | - run: npm run test 26 | - uses: actions/upload-artifact@v3 27 | with: 28 | name: dist 29 | path: dist 30 | 31 | package: 32 | if: github.event_name == 'push' && github.ref == 'refs/heads/main' 33 | needs: [build] 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v3 37 | - uses: actions/download-artifact@v3 38 | with: 39 | name: dist 40 | path: dist 41 | - name: Create Pull Request 42 | uses: peter-evans/create-pull-request@v6 43 | with: 44 | commit-message: 'build: update distribution' 45 | title: Update distribution 46 | body: | 47 | - Updates the distribution for changes on `main` 48 | 49 | Auto-generated by [create-pull-request][1] 50 | 51 | [1]: https://github.com/peter-evans/create-pull-request 52 | branch: update-distribution 53 | -------------------------------------------------------------------------------- /github/slash-command-dispatch/.github/workflows/hello-world-command.yml: -------------------------------------------------------------------------------- 1 | name: Hello World Command 2 | on: 3 | repository_dispatch: 4 | types: [hello-world-local-command] 5 | jobs: 6 | helloWorld: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Add reaction 10 | uses: peter-evans/create-or-update-comment@v2 11 | with: 12 | comment-id: ${{ github.event.client_payload.github.payload.comment.id }} 13 | reaction-type: hooray 14 | 15 | - name: Create URL to the run output 16 | id: vars 17 | run: echo "run-url=https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" >> $GITHUB_OUTPUT 18 | 19 | - name: Create comment 20 | uses: peter-evans/create-or-update-comment@v2 21 | with: 22 | issue-number: ${{ github.event.client_payload.github.payload.issue.number }} 23 | body: | 24 | Hello @${{ github.event.client_payload.github.actor }}! 25 | 26 | [Click here to see the command run output][1] 27 | 28 | [1]: ${{ steps.vars.outputs.run-url }} 29 | -------------------------------------------------------------------------------- /github/slash-command-dispatch/.github/workflows/ping-command.yml: -------------------------------------------------------------------------------- 1 | name: Ping Command 2 | on: 3 | repository_dispatch: 4 | types: [ping-local-command] 5 | jobs: 6 | helloWorld: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Update comment 10 | uses: peter-evans/create-or-update-comment@v2 11 | with: 12 | comment-id: ${{ github.event.client_payload.github.payload.comment.id }} 13 | body: | 14 | >pong ${{ github.event.client_payload.slash_command.args.all }} 15 | reaction-type: hooray 16 | -------------------------------------------------------------------------------- /github/slash-command-dispatch/.github/workflows/slash-command-dispatch.yml: -------------------------------------------------------------------------------- 1 | name: Slash Command Dispatch 2 | on: 3 | issue_comment: 4 | # Type "edited" added here for test purposes. Where possible, avoid 5 | # using to prevent processing unnecessary events. 6 | types: [created, edited] 7 | jobs: 8 | slashCommandDispatch: 9 | runs-on: ubuntu-latest 10 | steps: 11 | # Checkout is necessary here due to referencing a local action. 12 | # It's also necessary when using the 'config-from-file' option. 13 | # Otherwise, avoid using checkout to keep this workflow fast. 14 | - uses: actions/checkout@v3 15 | 16 | # Basic configuration 17 | - name: Slash Command Dispatch 18 | uses: ./ 19 | with: 20 | token: ${{ secrets.REPO_ACCESS_TOKEN }} 21 | commands: | 22 | hello-world-local 23 | ping-local 24 | permission: none 25 | issue-type: issue 26 | 27 | # Advanced JSON configuration 28 | - name: Slash Command Dispatch (JSON) 29 | id: scd 30 | uses: ./ 31 | with: 32 | token: ${{ secrets.REPO_ACCESS_TOKEN }} 33 | config: > 34 | [ 35 | { 36 | "command": "rebase", 37 | "permission": "admin", 38 | "repository": "peter-evans/slash-command-dispatch-processor", 39 | "issue_type": "pull-request" 40 | }, 41 | { 42 | "command": "help", 43 | "permission": "none", 44 | "issue_type": "issue", 45 | "repository": "peter-evans/slash-command-dispatch-processor" 46 | }, 47 | { 48 | "command": "help", 49 | "permission": "none", 50 | "issue_type": "pull-request", 51 | "repository": "peter-evans/slash-command-dispatch-processor", 52 | "event_type_suffix": "-pr-command" 53 | }, 54 | { 55 | "command": "example", 56 | "permission": "none", 57 | "issue_type": "issue", 58 | "repository": "peter-evans/slash-command-dispatch-processor" 59 | }, 60 | { 61 | "command": "hello-world", 62 | "permission": "none", 63 | "issue_type": "issue", 64 | "repository": "peter-evans/slash-command-dispatch-processor" 65 | }, 66 | { 67 | "command": "hello-world", 68 | "permission": "none", 69 | "issue_type": "pull-request", 70 | "repository": "peter-evans/slash-command-dispatch-processor", 71 | "event_type_suffix": "-pr-command" 72 | }, 73 | { 74 | "command": "hello-workflow", 75 | "permission": "none", 76 | "issue_type": "issue", 77 | "repository": "peter-evans/slash-command-dispatch-processor", 78 | "static_args": [ 79 | "repository=${{ github.repository }}", 80 | "comment-id=${{ github.event.comment.id }}", 81 | "issue-number=${{ github.event.issue.number }}", 82 | "actor=${{ github.actor }}" 83 | ], 84 | "dispatch_type": "workflow" 85 | }, 86 | { 87 | "command": "ping", 88 | "permission": "none", 89 | "issue_type": "issue", 90 | "repository": "peter-evans/slash-command-dispatch-processor" 91 | }, 92 | { 93 | "command": "black", 94 | "permission": "none", 95 | "issue_type": "pull-request", 96 | "repository": "peter-evans/slash-command-dispatch-processor" 97 | }, 98 | { 99 | "command": "reset-demo", 100 | "permission": "none", 101 | "issue_type": "pull-request", 102 | "repository": "peter-evans/slash-command-dispatch-processor" 103 | } 104 | ] 105 | 106 | - name: Edit comment with error message 107 | if: steps.scd.outputs.error-message 108 | uses: peter-evans/create-or-update-comment@v2 109 | with: 110 | comment-id: ${{ github.event.comment.id }} 111 | body: | 112 | > ${{ steps.scd.outputs.error-message }} 113 | 114 | # Advanced JSON configuration from file 115 | # (These commands do not do anything and are just a reference example) 116 | - name: Slash Command Dispatch (JSON file) 117 | uses: ./ 118 | with: 119 | token: ${{ secrets.REPO_ACCESS_TOKEN }} 120 | reactions: false 121 | config-from-file: .github/slash-command-dispatch.json 122 | -------------------------------------------------------------------------------- /github/slash-command-dispatch/.gitignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /github/slash-command-dispatch/.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /github/slash-command-dispatch/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": false, 6 | "singleQuote": true, 7 | "trailingComma": "none", 8 | "bracketSpacing": false, 9 | "arrowParens": "avoid", 10 | "parser": "typescript" 11 | } -------------------------------------------------------------------------------- /github/slash-command-dispatch/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Peter Evans 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /github/slash-command-dispatch/__test__/github-helper.int.test.ts: -------------------------------------------------------------------------------- 1 | import {GitHubHelper} from '../lib/github-helper' 2 | 3 | const token: string = process.env['REPO_SCOPED_PAT'] || 'not set' 4 | 5 | describe('github-helper tests', () => { 6 | it('tests getActorPermission returns "none" for non-existent collaborators', async () => { 7 | const githubHelper = new GitHubHelper(token) 8 | const actorPermission = await githubHelper.getActorPermission( 9 | {owner: 'peter-evans', repo: 'slash-command-dispatch'}, 10 | 'collaborator-does-not-exist' 11 | ) 12 | expect(actorPermission).toEqual('none') 13 | }) 14 | 15 | it('tests getActorPermission returns "admin"', async () => { 16 | const githubHelper = new GitHubHelper(token) 17 | const actorPermission = await githubHelper.getActorPermission( 18 | {owner: 'peter-evans', repo: 'slash-command-dispatch'}, 19 | 'peter-evans' 20 | ) 21 | expect(actorPermission).toEqual('admin') 22 | }) 23 | 24 | it('tests getActorPermission returns "write"', async () => { 25 | const githubHelper = new GitHubHelper(token) 26 | const actorPermission = await githubHelper.getActorPermission( 27 | {owner: 'peter-evans', repo: 'slash-command-dispatch'}, 28 | 'actions-bot' 29 | ) 30 | expect(actorPermission).toEqual('write') 31 | }) 32 | 33 | it('tests getActorPermission returns "triage" for an org repository collaborator', async () => { 34 | const githubHelper = new GitHubHelper(token) 35 | const actorPermission = await githubHelper.getActorPermission( 36 | {owner: 'slash-command-dispatch', repo: 'integration-test-fixture'}, 37 | 'test-case-machine-user' 38 | ) 39 | expect(actorPermission).toEqual('triage') 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /github/slash-command-dispatch/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Slash Command Dispatch' 2 | description: 'Facilitates "ChatOps" by creating dispatch events for slash commands' 3 | inputs: 4 | token: 5 | description: 'A repo scoped GitHub Personal Access Token.' 6 | required: true 7 | reaction-token: 8 | description: 'An optional GitHub token to use for reactions.' 9 | default: ${{ github.token }} 10 | reactions: 11 | description: 'Add reactions to comments containing commands.' 12 | default: true 13 | commands: 14 | description: 'A comma or newline separated list of commands.' 15 | required: true 16 | permission: 17 | description: 'The repository permission level required by the user to dispatch commands.' 18 | default: write 19 | issue-type: 20 | description: 'The issue type required for commands.' 21 | default: both 22 | allow-edits: 23 | description: 'Allow edited comments to trigger command dispatches.' 24 | default: false 25 | repository: 26 | description: 'The full name of the repository to send the dispatch events.' 27 | default: ${{ github.repository }} 28 | event-type-suffix: 29 | description: 'The repository dispatch event type suffix for the commands.' 30 | default: -command 31 | static-args: 32 | description: 'A comma or newline separated list of arguments that will be dispatched with every command.' 33 | dispatch-type: 34 | description: 'The dispatch type; `repository` or `workflow`.' 35 | default: repository 36 | config: 37 | description: 'JSON configuration for commands.' 38 | config-from-file: 39 | description: 'JSON configuration from a file for commands.' 40 | outputs: 41 | error-message: 42 | description: 'Validation errors when using `workflow` dispatch.' 43 | runs: 44 | using: 'node20' 45 | main: 'dist/index.js' 46 | branding: 47 | icon: 'target' 48 | color: 'gray-dark' 49 | -------------------------------------------------------------------------------- /github/slash-command-dispatch/docs/advanced-configuration.md: -------------------------------------------------------------------------------- 1 | # Advanced configuration 2 | 3 | ## What is advanced configuration? 4 | 5 | Due to the limitations of YAML based action inputs, basic configuration is not adequate to support unique configuration *per command*. 6 | 7 | For example, the following basic configuration means that all commands must have the same `admin` permission. 8 | 9 | ```yml 10 | - name: Slash Command Dispatch 11 | uses: peter-evans/slash-command-dispatch@v3 12 | with: 13 | token: ${{ secrets.PAT }} 14 | commands: | 15 | deploy 16 | integration-test 17 | build-docs 18 | permission: admin 19 | ``` 20 | 21 | To solve this issue, advanced JSON configuration allows each command to be configured individually. 22 | 23 | ## Dispatching commands 24 | 25 | There are two ways to specify JSON configuration for command dispatch. Directly in the workflow via the `config` input, OR, specifying a JSON config file via the `config-from-file` input. 26 | 27 | **Note**: It is recommended to write the JSON configuration directly in the workflow rather than use a file. Using the `config-from-file` input will be slightly slower due to requiring the repository to be checked out with `actions/checkout` so the file can be accessed. 28 | 29 | Here is a reference example workflow. Take care to use the correct [JSON property names](#advanced-action-inputs). 30 | 31 | ```yml 32 | name: Slash Command Dispatch 33 | on: 34 | issue_comment: 35 | types: [created] 36 | jobs: 37 | slashCommandDispatch: 38 | runs-on: ubuntu-latest 39 | steps: 40 | - name: Slash Command Dispatch 41 | uses: peter-evans/slash-command-dispatch@v3 42 | with: 43 | token: ${{ secrets.PAT }} 44 | config: > 45 | [ 46 | { 47 | "command": "rebase", 48 | "permission": "admin", 49 | "issue_type": "pull-request", 50 | "repository": "peter-evans/slash-command-dispatch-processor" 51 | }, 52 | { 53 | "command": "integration-test", 54 | "permission": "write", 55 | "issue_type": "both", 56 | "repository": "peter-evans/slash-command-dispatch-processor", 57 | "static_args": [ 58 | "production", 59 | "region=us-east-1" 60 | ] 61 | }, 62 | { 63 | "command": "create-ticket", 64 | "permission": "write", 65 | "issue_type": "issue", 66 | "allow_edits": true, 67 | "event_type_suffix": "-cmd", 68 | "dispatch_type": "workflow" 69 | } 70 | ] 71 | ``` 72 | 73 | The following workflow is an example using the `config-from-file` input to set JSON configuration. 74 | Note that `actions/checkout` is required to access the file. 75 | 76 | ```yml 77 | name: Slash Command Dispatch 78 | on: 79 | issue_comment: 80 | types: [created] 81 | jobs: 82 | slashCommandDispatch: 83 | runs-on: ubuntu-latest 84 | steps: 85 | - uses: actions/checkout@v3 86 | - name: Slash Command Dispatch 87 | uses: peter-evans/slash-command-dispatch@v3 88 | with: 89 | token: ${{ secrets.PAT }} 90 | config-from-file: .github/slash-command-dispatch.json 91 | ``` 92 | 93 | ## Advanced action inputs 94 | 95 | Advanced configuration requires a combination of YAML based inputs and JSON configuration. 96 | 97 | | Input | JSON Property | Description | Default | 98 | | --- | --- | --- | --- | 99 | | `token` | | (**required**) A `repo` scoped [Personal Access Token (PAT)](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token). Note: `GITHUB_TOKEN` *does not* work here. See [token](https://github.com/peter-evans/slash-command-dispatch#token) for further details. | | 100 | | `reaction-token` | | `GITHUB_TOKEN` or a `repo` scoped [Personal Access Token (PAT)](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token). See [reaction-token](https://github.com/peter-evans/slash-command-dispatch#reaction-token) for further details. | `GITHUB_TOKEN` | 101 | | `reactions` | | Add reactions. :eyes: = seen, :rocket: = dispatched | `true` | 102 | | | `command` | (**required**) The slash command. | | 103 | | | `permission` | The repository permission level required by the user to dispatch the command. (`none`, `read`, `triage`, `write`, `maintain`, `admin`) | `write` | 104 | | | `issue_type` | The issue type required for the command. (`issue`, `pull-request`, `both`) | `both` | 105 | | | `allow_edits` | Allow edited comments to trigger command dispatches. | `false` | 106 | | | `repository` | The full name of the repository to send the dispatch events. | Current repository | 107 | | | `event_type_suffix` | The repository dispatch event type suffix for the command. | `-command` | 108 | | | `static_args` | A string array of arguments that will be dispatched with the command. | `[]` | 109 | | | `dispatch_type` | The dispatch type; `repository` or `workflow`. | `repository` | 110 | | `config` | | JSON configuration for commands. | | 111 | | `config-from-file` | | JSON configuration from a file for commands. | | 112 | -------------------------------------------------------------------------------- /github/slash-command-dispatch/docs/assets/comment-parsing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudposse-archives/actions/6a5182ba6c68f4d4a9f1d473629a47f6cd01dabd/github/slash-command-dispatch/docs/assets/comment-parsing.png -------------------------------------------------------------------------------- /github/slash-command-dispatch/docs/assets/error-message-output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudposse-archives/actions/6a5182ba6c68f4d4a9f1d473629a47f6cd01dabd/github/slash-command-dispatch/docs/assets/error-message-output.png -------------------------------------------------------------------------------- /github/slash-command-dispatch/docs/assets/example-command.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudposse-archives/actions/6a5182ba6c68f4d4a9f1d473629a47f6cd01dabd/github/slash-command-dispatch/docs/assets/example-command.png -------------------------------------------------------------------------------- /github/slash-command-dispatch/docs/assets/slash-command-dispatch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudposse-archives/actions/6a5182ba6c68f4d4a9f1d473629a47f6cd01dabd/github/slash-command-dispatch/docs/assets/slash-command-dispatch.png -------------------------------------------------------------------------------- /github/slash-command-dispatch/docs/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | Follow this guide to get started with a working `/example` command. 4 | 5 | ## Command processing setup 6 | 7 | 1. Create a new repository called, for example, `slash-command-processor`. 8 | This will be the repository that commands are dispatched to for processing. 9 | 10 | 2. In your new repository, create the following workflow at `.github/workflows/example-command.yml`. 11 | 12 | ```yml 13 | name: example-command 14 | on: 15 | repository_dispatch: 16 | types: [example-command] 17 | jobs: 18 | example: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Add reaction 22 | uses: peter-evans/create-or-update-comment@v2 23 | with: 24 | token: ${{ secrets.PAT }} 25 | repository: ${{ github.event.client_payload.github.payload.repository.full_name }} 26 | comment-id: ${{ github.event.client_payload.github.payload.comment.id }} 27 | reaction-type: hooray 28 | ``` 29 | 30 | 3. Create a `repo` scoped Personal Access Token (PAT) by following [this guide](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token). 31 | 32 | 4. Go to your repository `Settings` -> `Secrets` and `Add a new secret`. 33 | 34 | **Name**: `PAT` 35 | 36 | **Value**: (The PAT created in step 3) 37 | 38 | Command processing setup is complete! Now we need to setup command dispatch for our `/example` command. 39 | 40 | ## Command dispatch setup 41 | 42 | 1. Choose a repository or create a new repository to dispatch commands from. 43 | This will be the repository where issue and pull request comments will be monitored for slash commands. 44 | 45 | In the repository, create the following workflow at `.github/workflows/slash-command-dispatch.yml`. 46 | 47 | **Note**: Change `your-github-username/slash-command-processor` to reference your command processor repository created in the [previous section](#command-processing-setup). 48 | 49 | ```yml 50 | name: Slash Command Dispatch 51 | on: 52 | issue_comment: 53 | types: [created] 54 | jobs: 55 | slashCommandDispatch: 56 | runs-on: ubuntu-latest 57 | steps: 58 | - name: Slash Command Dispatch 59 | uses: peter-evans/slash-command-dispatch@v3 60 | with: 61 | token: ${{ secrets.PAT }} 62 | commands: example 63 | repository: your-github-username/slash-command-processor 64 | ``` 65 | 66 | 2. Create a new `repo` scoped [PAT](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token), OR, use the one created at step 3 of the [previous section](#command-processing-setup). 67 | 68 | 3. Go to your repository `Settings` -> `Secrets` and `Add a new secret`. 69 | 70 | **Name**: `PAT` 71 | 72 | **Value**: (The PAT created in step 2) 73 | 74 | Command dispatch setup is complete! Now let's test our `/example` command. 75 | 76 | ## Testing the command 77 | 78 | 1. Create a new GitHub Issue in the repository you chose to dispatch commands from. 79 | 80 | 2. Add a new comment with the text `/example`. 81 | 82 | Once the command completes you should see all three reactions on your comment. 83 | 84 | ![Example Command](assets/example-command.png) 85 | 86 | Now you can start to tweak the command and make it do something useful! 87 | -------------------------------------------------------------------------------- /github/slash-command-dispatch/docs/updating.md: -------------------------------------------------------------------------------- 1 | ## Updating from `v2` to `v3` 2 | 3 | ### Breaking changes 4 | 5 | - If using self-hosted runners or GitHub Enterprise Server, there are minimum requirements for `v4` to run. See "What's new" below for details. 6 | 7 | ### What's new 8 | 9 | - Updated runtime to Node.js 16 10 | - The action now requires a minimum version of v2.285.0 for the [Actions Runner](https://github.com/actions/runner/releases/tag/v2.285.0). 11 | - If using GitHub Enterprise Server, the action requires [GHES 3.4](https://docs.github.com/en/enterprise-server@3.4/admin/release-notes) or later. 12 | 13 | ## Updating from `v1` to `v2` 14 | 15 | ### Breaking changes 16 | 17 | - The format of the `slash_command` context has been changed to prevent an issue where named arguments can overwrite other properties of the payload. 18 | 19 | The following diff shows how the `slash_command` context has changed for the example command `/deploy branch=main smoke-test dry-run reason="new feature"`. 20 | 21 | ```diff 22 | "slash_command": { 23 | "command": "deploy", 24 | - "args": "branch=main smoke-test dry-run reason=\"new feature\"", 25 | - "unnamed_args": "smoke-test dry-run", 26 | - "arg1": "smoke-test", 27 | - "arg2": "dry-run" 28 | - "branch": "main", 29 | - "reason": "new feature" 30 | + "args": { 31 | + "all": "branch=main smoke-test dry-run reason=\"new feature\"", 32 | + "unnamed": { 33 | + "all": "smoke-test dry-run", 34 | + "arg1": "smoke-test", 35 | + "arg2": "dry-run" 36 | + }, 37 | + "named": { 38 | + "branch": "main", 39 | + "reason": "new feature" 40 | + }, 41 | + } 42 | } 43 | ``` 44 | 45 | - The `named-args` input (standard configuration) and `named_args` JSON property (advanced configuration) have been removed. Named arguments will now always be parsed and added to the `slash_command` context. 46 | 47 | - The `client_payload.github.payload.issue.body` and `client_payload.pull_request.body` context properties will now be truncated if they exceed 1000 characters. 48 | 49 | ### New features 50 | 51 | - Commands can now be dispatched via the new [workflow_dispatch](https://docs.github.com/en/actions/reference/events-that-trigger-workflows#workflow_dispatch) event. For standard configuration, set the new `dispatch-type` input to `workflow`. For advanced configuration, set the `dispatch_type` JSON property of a command to `workflow`. 52 | There are significant differences in the action's behaviour when using `workflow` dispatch. See [workflow dispatch](workflow-dispatch.md) for usage details. 53 | 54 | - Added a new input `static-args` (standard configuration), and a new JSON property `static_args` (advanced configuration). This is a list of arguments that will always be dispatched with commands. 55 | 56 | Standard configuration: 57 | ```yml 58 | static-args: | 59 | production 60 | region=us-east-1 61 | ``` 62 | Advanced configuration: 63 | ```json 64 | "static_args": [ 65 | "production", 66 | "region=us-east-1" 67 | ] 68 | ``` 69 | 70 | - Slash command arguments can now be double-quoted to allow for argument values containing spaces. 71 | 72 | e.g. 73 | ``` 74 | /deploy branch=main dry-run reason="new feature" 75 | ``` 76 | ``` 77 | /send "hello world!" 78 | ``` 79 | 80 | - The `commands` input can now be newline separated, or comma-separated. 81 | 82 | e.g. 83 | ```yml 84 | commands: | 85 | deploy 86 | integration-test 87 | build-docs 88 | ``` 89 | -------------------------------------------------------------------------------- /github/slash-command-dispatch/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | moduleFileExtensions: ['js', 'ts'], 4 | testEnvironment: 'node', 5 | testMatch: ['**/*.test.ts'], 6 | testRunner: 'jest-circus/runner', 7 | transform: { 8 | '^.+\\.ts$': 'ts-jest' 9 | }, 10 | verbose: true 11 | } 12 | process.env = Object.assign(process.env, { 13 | GITHUB_REPOSITORY: "peter-evans/slash-command-dispatch" 14 | }) 15 | -------------------------------------------------------------------------------- /github/slash-command-dispatch/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "slash-command-dispatch", 3 | "version": "3.0.0", 4 | "private": true, 5 | "description": "Facilitates 'ChatOps' by creating dispatch events for slash commands", 6 | "main": "lib/main.js", 7 | "scripts": { 8 | "build": "tsc && ncc build", 9 | "format": "prettier --write '**/*.ts'", 10 | "format-check": "prettier --check '**/*.ts'", 11 | "lint": "eslint src/**/*.ts", 12 | "test": "jest unit", 13 | "test:int": "jest int" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/peter-evans/slash-command-dispatch.git" 18 | }, 19 | "keywords": [ 20 | "slash", 21 | "command", 22 | "dispatch" 23 | ], 24 | "author": "Peter Evans", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/peter-evans/slash-command-dispatch/issues" 28 | }, 29 | "homepage": "https://github.com/peter-evans/slash-command-dispatch#readme", 30 | "dependencies": { 31 | "@actions/core": "^1.10.0", 32 | "@actions/github": "^5.1.1", 33 | "@octokit/core": "^3.5.1", 34 | "@octokit/plugin-paginate-rest": "^2.17.0", 35 | "@octokit/plugin-rest-endpoint-methods": "^5.13.0", 36 | "http-proxy-agent": "^5.0.0", 37 | "https-proxy-agent": "^5.0.0" 38 | }, 39 | "devDependencies": { 40 | "@types/jest": "^27.0.3", 41 | "@types/node": "^16.11.11", 42 | "@typescript-eslint/parser": "^5.5.0", 43 | "@vercel/ncc": "^0.32.0", 44 | "eslint": "^8.3.0", 45 | "eslint-plugin-github": "^4.3.5", 46 | "eslint-plugin-jest": "^25.3.0", 47 | "jest": "^27.4.3", 48 | "jest-circus": "^27.4.2", 49 | "js-yaml": "^4.1.0", 50 | "prettier": "^2.5.0", 51 | "ts-jest": "^27.0.7", 52 | "typescript": "^4.5.2" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /github/slash-command-dispatch/src/octokit-client.ts: -------------------------------------------------------------------------------- 1 | import {Octokit as Core} from '@octokit/core' 2 | import {paginateRest} from '@octokit/plugin-paginate-rest' 3 | import { 4 | restEndpointMethods, 5 | RestEndpointMethodTypes 6 | } from '@octokit/plugin-rest-endpoint-methods' 7 | import {HttpProxyAgent} from 'http-proxy-agent' 8 | import {HttpsProxyAgent} from 'https-proxy-agent' 9 | 10 | export const Octokit = Core.plugin( 11 | paginateRest, 12 | restEndpointMethods, 13 | autoProxyAgent 14 | ) 15 | 16 | export type PullsGetResponseData = 17 | RestEndpointMethodTypes['pulls']['get']['response']['data'] 18 | 19 | // Octokit plugin to support the http_proxy and https_proxy environment variable 20 | function autoProxyAgent(octokit: Core) { 21 | const http_proxy_address = 22 | process.env['http_proxy'] || process.env['HTTP_PROXY'] 23 | const https_proxy_address = 24 | process.env['https_proxy'] || process.env['HTTPS_PROXY'] 25 | 26 | octokit.hook.before('request', options => { 27 | if (options.baseUrl.startsWith('http://') && http_proxy_address) { 28 | options.request.agent = new HttpProxyAgent(http_proxy_address) 29 | } else if (options.baseUrl.startsWith('https://') && https_proxy_address) { 30 | options.request.agent = new HttpsProxyAgent(https_proxy_address) 31 | } 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /github/slash-command-dispatch/src/utils.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | 3 | export function getInputAsArray( 4 | name: string, 5 | options?: core.InputOptions 6 | ): string[] { 7 | return getStringAsArray(core.getInput(name, options)) 8 | } 9 | 10 | export function getStringAsArray(str: string): string[] { 11 | return str 12 | .split(/[\n,]+/) 13 | .map(s => s.trim()) 14 | .filter(x => x !== '') 15 | } 16 | -------------------------------------------------------------------------------- /github/slash-command-dispatch/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es6" 7 | ], 8 | "outDir": "./lib", 9 | "rootDir": "./src", 10 | "declaration": true, 11 | "strict": true, 12 | "noImplicitAny": false, 13 | "esModuleInterop": true 14 | }, 15 | "exclude": ["__test__", "lib", "node_modules"] 16 | } 17 | -------------------------------------------------------------------------------- /go/build/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.20-buster 2 | 3 | LABEL maintainer="Cloud Posse " 4 | 5 | LABEL "com.github.actions.name"="Build" 6 | LABEL "com.github.actions.description"="Build Go Binaries" 7 | LABEL "com.github.actions.icon"="activity" 8 | LABEL "com.github.actions.color"="blue" 9 | 10 | COPY entrypoint.sh / 11 | 12 | RUN chmod 755 /entrypoint.sh 13 | 14 | ENTRYPOINT ["/entrypoint.sh"] 15 | -------------------------------------------------------------------------------- /go/build/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Build' 2 | description: 'Build Go Binaries' 3 | author: 'Cloud Posse ' 4 | runs: 5 | using: 'docker' 6 | image: 'Dockerfile' 7 | branding: 8 | icon: 'activity' 9 | color: 'blue' 10 | -------------------------------------------------------------------------------- /go/build/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -x 4 | 5 | export GO111MODULE="${GO111MODULE:-on}" 6 | export CGO_ENABLED=0 7 | export GOBIN=$GOPATH/bin 8 | export PATH=$PATH:$GOBIN 9 | export GOX_OSARCH="${GOX_OSARCH:-linux/386 linux/amd64}" 10 | 11 | if [[ -z "$OUTPUT_PATH" ]]; then 12 | echo "Missing OUTPUT_PATH env variable" 13 | exit 1 14 | fi 15 | 16 | pwd 17 | ls 18 | 19 | if [[ ! -z "$WORKING_DIR" ]]; then 20 | mkdir -p "${WORKING_DIR}" || exit 1 21 | cp -r . "${WORKING_DIR}" || exit 1 22 | cd "${WORKING_DIR}" || exit 1 23 | pwd 24 | ls 25 | fi 26 | 27 | go get -u github.com/mitchellh/gox 28 | 29 | if [[ "$GO111MODULE" != "on" ]]; then 30 | go mod download 31 | fi 32 | 33 | if [[ -z "$LDFLAGS" ]]; then 34 | gox -osarch="${GOX_OSARCH}" -output "${OUTPUT_PATH}{{.OS}}_{{.Arch}}" 35 | else 36 | gox -osarch="${GOX_OSARCH}" -output "${OUTPUT_PATH}{{.OS}}_{{.Arch}}" -ldflags="${LDFLAGS}" 37 | fi 38 | --------------------------------------------------------------------------------