├── .editorconfig ├── .github ├── CODEOWNERS └── workflows │ ├── add-labels.yml │ ├── add-late-review-label.yml │ ├── add-pr-approval-label.yml │ ├── approvals-satisfied.yml │ ├── approve-pr.yml │ ├── are-reviewers-required.yml │ ├── assign-pr-reviewers.yml │ ├── check-merge-safety.yml │ ├── check-pr-title.yml │ ├── close-pr.yml │ ├── create-pr-comment.yml │ ├── create-pr.yml │ ├── create-project-card.yml │ ├── delete-stale-branches.yml │ ├── deployments.yml │ ├── filter-paths.yml │ ├── generate-matrix.yml │ ├── generate-path-matrix.yml │ ├── get-changed-files.yml │ ├── get-email-on-user-profile.yml │ ├── get-merge-queue-position.yml │ ├── is-user-core-member.yml │ ├── is-user-in-team.yml │ ├── manage-issue-due-dates.yml │ ├── manage-merge-queue.yml │ ├── move-project-card.yml │ ├── notify-pipeline-complete.yml │ ├── prepare-queued-pr-for-merge.yml │ ├── publish.yaml │ ├── release.yml │ ├── remove-label.yml │ ├── remove-pr-from-merge-queue.yml │ ├── reopen-pr.yml │ ├── rerun-pr-checks.yml │ ├── set-commit-status.yml │ ├── set-latest-pipeline-status.yml │ ├── tests.yml │ └── update-check-result.yml ├── .gitignore ├── .husky └── pre-commit ├── .prettierignore ├── .prettierrc.json ├── .releaserc.yaml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── action.yml ├── bun.lockb ├── dist ├── 0.index.js ├── 0.index.js.map ├── 101.index.js ├── 101.index.js.map ├── 110.index.js ├── 110.index.js.map ├── 12.index.js ├── 12.index.js.map ├── 124.index.js ├── 124.index.js.map ├── 148.index.js ├── 148.index.js.map ├── 150.index.js ├── 150.index.js.map ├── 153.index.js ├── 153.index.js.map ├── 180.index.js ├── 180.index.js.map ├── 208.index.js ├── 208.index.js.map ├── 209.index.js ├── 209.index.js.map ├── 228.index.js ├── 228.index.js.map ├── 250.index.js ├── 250.index.js.map ├── 263.index.js ├── 263.index.js.map ├── 264.index.js ├── 264.index.js.map ├── 271.index.js ├── 271.index.js.map ├── 280.index.js ├── 280.index.js.map ├── 284.index.js ├── 284.index.js.map ├── 318.index.js ├── 318.index.js.map ├── 338.index.js ├── 338.index.js.map ├── 351.index.js ├── 351.index.js.map ├── 366.index.js ├── 366.index.js.map ├── 374.index.js ├── 374.index.js.map ├── 400.index.js ├── 400.index.js.map ├── 404.index.js ├── 404.index.js.map ├── 409.index.js ├── 409.index.js.map ├── 419.index.js ├── 419.index.js.map ├── 420.index.js ├── 420.index.js.map ├── 431.index.js ├── 431.index.js.map ├── 440.index.js ├── 440.index.js.map ├── 445.index.js ├── 445.index.js.map ├── 461.index.js ├── 461.index.js.map ├── 465.index.js ├── 465.index.js.map ├── 467.index.js ├── 467.index.js.map ├── 471.index.js ├── 471.index.js.map ├── 491.index.js ├── 491.index.js.map ├── 522.index.js ├── 522.index.js.map ├── 533.index.js ├── 533.index.js.map ├── 551.index.js ├── 551.index.js.map ├── 573.index.js ├── 573.index.js.map ├── 579.index.js ├── 579.index.js.map ├── 59.index.js ├── 59.index.js.map ├── 598.index.js ├── 598.index.js.map ├── 604.index.js ├── 604.index.js.map ├── 61.index.js ├── 61.index.js.map ├── 654.index.js ├── 654.index.js.map ├── 655.index.js ├── 655.index.js.map ├── 676.index.js ├── 676.index.js.map ├── 682.index.js ├── 682.index.js.map ├── 689.index.js ├── 689.index.js.map ├── 710.index.js ├── 710.index.js.map ├── 720.index.js ├── 720.index.js.map ├── 766.index.js ├── 766.index.js.map ├── 783.index.js ├── 783.index.js.map ├── 785.index.js ├── 785.index.js.map ├── 79.index.js ├── 79.index.js.map ├── 794.index.js ├── 794.index.js.map ├── 800.index.js ├── 800.index.js.map ├── 807.index.js ├── 807.index.js.map ├── 83.index.js ├── 83.index.js.map ├── 830.index.js ├── 830.index.js.map ├── 832.index.js ├── 832.index.js.map ├── 839.index.js ├── 839.index.js.map ├── 843.index.js ├── 843.index.js.map ├── 850.index.js ├── 850.index.js.map ├── 862.index.js ├── 862.index.js.map ├── 867.index.js ├── 867.index.js.map ├── 905.index.js ├── 905.index.js.map ├── 913.index.js ├── 913.index.js.map ├── 928.index.js ├── 928.index.js.map ├── 939.index.js ├── 939.index.js.map ├── 943.index.js ├── 943.index.js.map ├── 944.index.js ├── 944.index.js.map ├── 946.index.js ├── 946.index.js.map ├── 95.index.js ├── 95.index.js.map ├── 950.index.js ├── 950.index.js.map ├── 956.index.js ├── 956.index.js.map ├── 960.index.js ├── 960.index.js.map ├── 974.index.js ├── 974.index.js.map ├── 98.index.js ├── 98.index.js.map ├── action.yml ├── index.js ├── index.js.map ├── licenses.txt ├── package.json └── sourcemap-register.cjs ├── eslint.config.js ├── package.json ├── plopfile.js ├── renovate.json ├── reset.d.ts ├── scripts ├── dev-setup.sh ├── generate-helper-inputs.ts ├── verify-file-headers.ts └── verify-pr-title-has-valid-descriptor.ts ├── src ├── constants.ts ├── helpers │ ├── add-labels.ts │ ├── add-late-review-label.ts │ ├── add-pr-approval-label.ts │ ├── approvals-satisfied.ts │ ├── approve-pr.ts │ ├── are-reviewers-required.ts │ ├── assign-pr-reviewers.ts │ ├── check-merge-safety.ts │ ├── check-pr-title.ts │ ├── close-pr.ts │ ├── create-pr-comment.ts │ ├── create-pr.ts │ ├── create-project-card.ts │ ├── delete-deployment.ts │ ├── delete-stale-branches.ts │ ├── filter-paths.ts │ ├── generate-matrix.ts │ ├── generate-path-matrix.ts │ ├── get-changed-files.ts │ ├── get-email-on-user-profile.ts │ ├── get-merge-queue-position.ts │ ├── initiate-deployment.ts │ ├── is-user-core-member.ts │ ├── is-user-in-team.ts │ ├── manage-issue-due-dates.ts │ ├── manage-merge-queue.ts │ ├── move-project-card.ts │ ├── notify-pipeline-complete.ts │ ├── prepare-queued-pr-for-merge.ts │ ├── remove-label.ts │ ├── remove-pr-from-merge-queue.ts │ ├── reopen-pr.ts │ ├── rerun-pr-checks.ts │ ├── set-commit-status.ts │ ├── set-deployment-status.ts │ ├── set-latest-pipeline-status.ts │ └── update-check-result.ts ├── main.ts ├── octokit.ts ├── types │ ├── generated.ts │ └── github.ts └── utils │ ├── add-due-date-comment.ts │ ├── convert-to-team-slug.ts │ ├── get-action-inputs.ts │ ├── get-changed-filepaths.ts │ ├── get-core-member-logins.ts │ ├── get-default-branch.ts │ ├── get-inputs-from-file.ts │ ├── get-project-columns.ts │ ├── merge-queue.ts │ ├── notify-user.ts │ ├── paginate-all-branches.ts │ ├── paginate-all-reviews.ts │ ├── paginate-comments-on-issue.ts │ ├── paginate-members-in-org.ts │ ├── paginate-open-pull-requests.ts │ ├── paginate-prioritized-issues.ts │ └── update-merge-queue.ts ├── templates ├── docs.hbs ├── helper.hbs ├── test.hbs └── workflow.hbs ├── test ├── constants.test.ts ├── helpers │ ├── add-labels.test.ts │ ├── add-late-review-label.test.ts │ ├── add-pr-approval-label.test.ts │ ├── approvals-satisfied.test.ts │ ├── approve-pr.test.ts │ ├── are-reviewers-required.test.ts │ ├── assign-pr-reviewers.test.ts │ ├── check-merge-safety.test.ts │ ├── check-pr-title.test.ts │ ├── close-pr.test.ts │ ├── create-pr-comment.test.ts │ ├── create-pr.test.ts │ ├── create-project-card.test.ts │ ├── delete-deployment.test.ts │ ├── delete-stale-branches.test.ts │ ├── filter-paths.test.ts │ ├── generate-matrix.test.ts │ ├── generate-path-matrix.test.ts │ ├── get-changed-files.test.ts │ ├── get-email-on-user-profile.test.ts │ ├── get-merge-queue-position.test.ts │ ├── initiate-deployment.test.ts │ ├── is-user-core-member.test.ts │ ├── is-user-in-team.test.ts │ ├── manage-issue-due-dates.test.ts │ ├── manage-merge-queue.test.ts │ ├── move-project-card.test.ts │ ├── notify-pipeline-complete.test.ts │ ├── prepare-queued-pr-for-merge.test.ts │ ├── remove-label.test.ts │ ├── remove-pr-from-merge-queue.test.ts │ ├── reopen-pr.test.ts │ ├── rerun-pr-checks.test.ts │ ├── set-commit-status.test.ts │ ├── set-deployment-status.test.ts │ ├── set-latest-pipeline-status.test.ts │ └── update-check-result.test.ts ├── main.test.ts ├── types.d.ts └── utils │ ├── get-action-inputs.test.ts │ ├── get-core-member-logins.test.ts │ ├── get-inputs-from-file.test.ts │ ├── notify-user.test.ts │ ├── paginate-members-in-org.test.ts │ └── update-merge-queue.test.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | 12 | [*.{yaml,yml}] 13 | indent_size = 2 14 | 15 | [*.{ts,js,json}] 16 | indent_size = 2 17 | 18 | [LICENSE] 19 | insert_final_newline = false 20 | 21 | [Makefile] 22 | indent_style = tab 23 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Order is important; the last matching pattern takes the most precedence. 2 | # These owners will be the default owners for everything in 3 | # the repo. Unless a later match takes precedence 4 | 5 | * @ExpediaGroup/github-helpers-committers 6 | 7 | /file/path/1 @ExpediaGroup/test-owners-1 8 | /file/path/2 @ExpediaGroup/test-owners-2 9 | /file/path/shared @ExpediaGroup/test-shared-owners-1 @ExpediaGroup/test-shared-owners-2 10 | /file/path/users @user1 @user2 11 | -------------------------------------------------------------------------------- /.github/workflows/add-labels.yml: -------------------------------------------------------------------------------- 1 | name: Add Labels 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | paths: 7 | - 'src/helpers/add-labels.ts' 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - uses: ./ 17 | with: 18 | helper: add-labels 19 | labels: Test Label 20 | -------------------------------------------------------------------------------- /.github/workflows/add-late-review-label.yml: -------------------------------------------------------------------------------- 1 | name: Add Late Review Label 2 | 3 | on: 4 | pull_request_review: 5 | types: [submitted] 6 | paths: 7 | - 'src/helpers/add-late-review-label.ts' 8 | 9 | jobs: 10 | test: 11 | if: github.event.review.state == 'blocked' 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | 17 | - uses: ./ 18 | with: 19 | helper: add-late-review-label 20 | login: ${{ github.event.review.user.login }} 21 | github_token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} # must have read:org permission 22 | -------------------------------------------------------------------------------- /.github/workflows/add-pr-approval-label.yml: -------------------------------------------------------------------------------- 1 | name: Add PR Approval Label 2 | 3 | on: 4 | pull_request_review: 5 | types: [submitted] 6 | paths: 7 | - 'src/helpers/add-pr-approval-label.ts' 8 | 9 | jobs: 10 | test: 11 | if: github.event.review.state == 'approved' 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | 17 | - uses: ./ 18 | with: 19 | helper: add-pr-approval-label 20 | login: ${{ github.event.review.user.login }} 21 | github_token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} # must have read:org permission 22 | -------------------------------------------------------------------------------- /.github/workflows/approvals-satisfied.yml: -------------------------------------------------------------------------------- 1 | name: Approvals Satisfied 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | paths: 7 | - 'src/helpers/approvals-satisfied.ts' 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - uses: ./ 17 | id: approvals-satisfied 18 | with: 19 | helper: approvals-satisfied 20 | github_token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} 21 | 22 | - if: steps.approvals-satisfied.outputs.output == 'true' 23 | run: echo "PR approvals have been satisfied!" 24 | -------------------------------------------------------------------------------- /.github/workflows/approve-pr.yml: -------------------------------------------------------------------------------- 1 | name: Auto Approve PR 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | paths: 7 | - 'src/helpers/approve-pr.ts' 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - uses: ./ 17 | with: 18 | helper: approve-pr 19 | github_token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} # must have write:repo permission 20 | -------------------------------------------------------------------------------- /.github/workflows/are-reviewers-required.yml: -------------------------------------------------------------------------------- 1 | name: Are Reviewers Requested 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | paths: 7 | - 'src/helpers/are-reviewers-required.ts' 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - uses: ./ 17 | with: 18 | helper: are-reviewers-required 19 | teams: '@ExpediaGroup/github-helpers-committers' 20 | -------------------------------------------------------------------------------- /.github/workflows/assign-pr-reviewers.yml: -------------------------------------------------------------------------------- 1 | name: Assign PR Reviewer 2 | 3 | on: 4 | pull_request_review: 5 | types: [submitted] 6 | paths: 7 | - 'src/helpers/assign-pr-reviewers.ts' 8 | 9 | jobs: 10 | test: 11 | if: github.event.review.state == 'approved' 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | 17 | - uses: ./ 18 | with: 19 | helper: assign-pr-reviewers 20 | login: ${{ github.event.review.user.login }} 21 | github_token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} # must have read:org permission 22 | -------------------------------------------------------------------------------- /.github/workflows/check-merge-safety.yml: -------------------------------------------------------------------------------- 1 | name: Check Merge Safety 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | paths: 9 | - 'src/helpers/check-merge-safety.ts' 10 | 11 | jobs: 12 | test: 13 | name: Merge Safety 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | 19 | - uses: ./ 20 | with: 21 | helper: check-merge-safety 22 | override_filter_paths: | 23 | package.json 24 | package-lock.json 25 | -------------------------------------------------------------------------------- /.github/workflows/check-pr-title.yml: -------------------------------------------------------------------------------- 1 | name: Check PR Title 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | types: [edited, opened, reopened, synchronize] 7 | 8 | jobs: 9 | test: 10 | name: PR Title Check 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - name: Setup Bun 17 | uses: oven-sh/setup-bun@v2 18 | with: 19 | bun-version-file: package.json 20 | 21 | - name: Install dependencies 22 | run: bun install 23 | 24 | - name: Validate PR Title Contains Valid Descriptor 25 | run: bun ./scripts/verify-pr-title-has-valid-descriptor.ts 26 | env: 27 | TITLE: ${{ github.event.pull_request.title }} 28 | 29 | - uses: ./ 30 | with: 31 | helper: 32 | check-pr-title 33 | # pattern: 'my-regex-pattern' (optional: pattern must be wrapped in single quotes) 34 | -------------------------------------------------------------------------------- /.github/workflows/close-pr.yml: -------------------------------------------------------------------------------- 1 | name: Close Pr 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | types: [opened, reopened] 7 | paths: 8 | - 'src/helpers/close-pr.ts' 9 | 10 | permissions: write-all 11 | 12 | jobs: 13 | test: 14 | if: contains(github.event.pull_request.labels.*.name, 'CLOSE ME') 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | 20 | - uses: ./ 21 | with: 22 | helper: close-pr 23 | body: Closing this PR for testing purposes! 24 | -------------------------------------------------------------------------------- /.github/workflows/create-pr-comment.yml: -------------------------------------------------------------------------------- 1 | name: Create PR Comment 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | paths: 7 | - 'src/helpers/create-pr-comment.ts' 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - uses: ./ 17 | with: 18 | helper: create-pr-comment 19 | body: Test Comment 20 | -------------------------------------------------------------------------------- /.github/workflows/create-pr.yml: -------------------------------------------------------------------------------- 1 | name: Open Pull Request 2 | 3 | on: 4 | push: 5 | branches: 6 | - create-pull-request 7 | 8 | permissions: write-all 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | 17 | - uses: ./ 18 | with: 19 | helper: create-pr 20 | title: New PR 21 | body: Implemented new feature. Added tests. 22 | -------------------------------------------------------------------------------- /.github/workflows/create-project-card.yml: -------------------------------------------------------------------------------- 1 | name: Create Project Card 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | paths: 7 | - 'src/helpers/create-project-card.ts' 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - uses: ./ 17 | with: 18 | helper: create-project-card 19 | project_name: Test Project 20 | project_destination_column_name: Test Column 1 21 | -------------------------------------------------------------------------------- /.github/workflows/delete-stale-branches.yml: -------------------------------------------------------------------------------- 1 | name: Delete Stale Branches 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | paths: 7 | - 'src/helpers/delete-stale-branches.ts' 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - uses: ./ 17 | with: 18 | helper: delete-stale-branches 19 | -------------------------------------------------------------------------------- /.github/workflows/deployments.yml: -------------------------------------------------------------------------------- 1 | name: Deployments 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | paths: 7 | - 'src/helpers/delete-deployment.ts' 8 | - 'src/helpers/initiate-deployment.ts' 9 | - 'src/helpers/set-deployment-status.ts' 10 | 11 | jobs: 12 | initiate-deployment: 13 | name: Initiate Deployment 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | 19 | - uses: ./ 20 | with: 21 | helper: initiate-deployment 22 | sha: ${{ github.event.pull_request.head.sha }} 23 | environment: test 24 | description: PR#${{ github.event.pull_request.number }} has been merged; pipeline in progress... 25 | 26 | set-deployment-status: 27 | name: Set Deployment Status 28 | needs: [initiate-deployment] 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v4 33 | 34 | - uses: ./ 35 | with: 36 | helper: set-deployment-status 37 | sha: ${{ github.event.pull_request.head.sha }} 38 | environment: test 39 | state: success 40 | description: Deployment succeeded. 41 | target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} 42 | 43 | delete-deployment: 44 | name: Delete Deployment 45 | needs: [initiate-deployment, set-deployment-status] 46 | runs-on: ubuntu-latest 47 | steps: 48 | - name: Checkout 49 | uses: actions/checkout@v4 50 | 51 | - uses: ./ 52 | with: 53 | helper: delete-deployment 54 | sha: ${{ github.event.pull_request.head.sha }} 55 | environment: test 56 | github_token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} 57 | -------------------------------------------------------------------------------- /.github/workflows/filter-paths.yml: -------------------------------------------------------------------------------- 1 | name: Filter Paths 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | paths: 7 | - 'src/helpers/filter-paths.ts' 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - uses: ./ 17 | id: paths 18 | with: 19 | helper: filter-paths 20 | paths: | 21 | src 22 | package.json 23 | yarn.lock 24 | 25 | - if: steps.paths.outputs.output == 'true' 26 | run: echo "One of those file paths changed!" 27 | -------------------------------------------------------------------------------- /.github/workflows/generate-matrix.yml: -------------------------------------------------------------------------------- 1 | name: Generate Matrix 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | paths: 7 | - 'src/helpers/generate-matrix.ts' 8 | 9 | jobs: 10 | scheduler: 11 | name: Determine packages to build 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | 17 | - uses: ./ 18 | id: matrix 19 | with: 20 | helper: generate-matrix 21 | paths: | 22 | test/path/1 23 | test/path/2 24 | test/path/3 25 | 26 | outputs: 27 | matrix: ${{ steps.matrix.outputs.output }} 28 | 29 | build: 30 | runs-on: ubuntu-latest 31 | needs: scheduler 32 | strategy: 33 | matrix: ${{ fromJson(needs.scheduler.outputs.matrix) }} 34 | steps: 35 | - run: echo "Run each job using ${{ matrix.path }}" 36 | 37 | build-status: 38 | runs-on: ubuntu-latest 39 | if: always() 40 | needs: build 41 | steps: 42 | - name: Check build status 43 | run: exit ${{ (needs.build.result == 'failure' || needs.build.result == 'cancelled') && 1 || 0 }} 44 | -------------------------------------------------------------------------------- /.github/workflows/generate-path-matrix.yml: -------------------------------------------------------------------------------- 1 | name: Generate Path Matrix 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | paths: 7 | - 'src/helpers/generate-path-matrix.ts' 8 | 9 | jobs: 10 | scheduler: 11 | name: Determine packages to build 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | 17 | - uses: ./ 18 | id: path_matrix 19 | with: 20 | helper: generate-path-matrix 21 | paths: | 22 | src/helpers/generate-path-matrix.ts 23 | package/two 24 | package/three 25 | 26 | outputs: 27 | matrix: ${{ steps.path_matrix.outputs.output }} 28 | 29 | build: 30 | runs-on: ubuntu-latest 31 | needs: scheduler 32 | strategy: 33 | matrix: ${{ fromJson(needs.scheduler.outputs.matrix) }} 34 | steps: 35 | - run: echo "Run each job using ${{ matrix.path }}" 36 | 37 | build-status: 38 | runs-on: ubuntu-latest 39 | if: always() 40 | needs: build 41 | steps: 42 | - name: Check build status 43 | run: exit ${{ (needs.build.result == 'failure' || needs.build.result == 'cancelled') && 1 || 0 }} 44 | -------------------------------------------------------------------------------- /.github/workflows/get-changed-files.yml: -------------------------------------------------------------------------------- 1 | name: Get Changed Files 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | paths: 7 | - 'src/helpers/get-changed-files.ts' 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - uses: ./ 17 | id: changed 18 | with: 19 | helper: get-changed-files 20 | 21 | - run: echo "Changed files are ${{ steps.changed.outputs.output }}" 22 | -------------------------------------------------------------------------------- /.github/workflows/get-email-on-user-profile.yml: -------------------------------------------------------------------------------- 1 | name: Get Email on User Profile 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | paths: 7 | - 'src/helpers/get-email-on-user-profile.ts' 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - uses: ./ 17 | id: get-email-on-user-profile 18 | with: 19 | helper: get-email-on-user-profile 20 | login: ${{ github.actor }} 21 | 22 | - run: echo "Email is ${{ steps.get-email-on-user-profile.outputs.output }}" 23 | -------------------------------------------------------------------------------- /.github/workflows/get-merge-queue-position.yml: -------------------------------------------------------------------------------- 1 | name: Get Merge Queue Position 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | paths: 7 | - 'src/helpers/get-merge-queue-position.ts' 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - uses: ./ 17 | with: 18 | helper: get-merge-queue-position 19 | sha: ${{ github.sha }} 20 | -------------------------------------------------------------------------------- /.github/workflows/is-user-core-member.yml: -------------------------------------------------------------------------------- 1 | name: Is User Core Member 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | paths: 7 | - 'src/helpers/is-user-core-member.ts' 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - uses: ./ 17 | id: is-user-core-member 18 | with: 19 | helper: is-user-core-member 20 | pull_number: ${{ github.event.pull_request.number }} 21 | login: ${{ github.actor }} # Optional, defaults to the GitHub actor 22 | github_token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} 23 | 24 | - run: echo ${{ steps.is-user-core-member.outputs.output }} 25 | -------------------------------------------------------------------------------- /.github/workflows/is-user-in-team.yml: -------------------------------------------------------------------------------- 1 | name: Is User In Team 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | paths: 7 | - 'src/helpers/is-user-in-team.ts' 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - uses: ./ 17 | id: is-user-in-team 18 | with: 19 | helper: is-user-in-team 20 | login: ${{ github.actor }} 21 | team: github-helpers-committers 22 | github_token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} # must have read:org permission 23 | 24 | - run: echo ${{ steps.is-user-in-team.outputs.output }} 25 | -------------------------------------------------------------------------------- /.github/workflows/manage-issue-due-dates.yml: -------------------------------------------------------------------------------- 1 | name: Manage Issue Due Dates 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v4 13 | 14 | - uses: ./ 15 | with: 16 | helper: manage-issue-due-dates 17 | login: ${{ github.event.review.user.login }} 18 | github_token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} # must have read:org permission 19 | -------------------------------------------------------------------------------- /.github/workflows/manage-merge-queue.yml: -------------------------------------------------------------------------------- 1 | name: Manage Merge Queue 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | types: [labeled, unlabeled, closed] 7 | 8 | jobs: 9 | test: 10 | if: | 11 | github.event.action == 'closed' || 12 | github.event.label.name == 'READY FOR MERGE' || 13 | github.event.label.name == 'JUMP THE QUEUE' 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | 19 | - uses: ./ 20 | with: 21 | helper: manage-merge-queue 22 | -------------------------------------------------------------------------------- /.github/workflows/move-project-card.yml: -------------------------------------------------------------------------------- 1 | name: Move Project Card 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | paths: 7 | - 'src/helpers/move-project-card.ts' 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - uses: ./ 17 | with: 18 | helper: move-project-card 19 | project_name: Test Project 20 | project_origin_column_name: Test Column 1 21 | project_destination_column_name: Test Column 2 22 | -------------------------------------------------------------------------------- /.github/workflows/notify-pipeline-complete.yml: -------------------------------------------------------------------------------- 1 | name: Notify Pipeline Complete 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | paths: 7 | - 'src/helpers/notify-pipeline-complete.ts' 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - uses: ./ 17 | with: 18 | helper: notify-pipeline-complete 19 | -------------------------------------------------------------------------------- /.github/workflows/prepare-queued-pr-for-merge.yml: -------------------------------------------------------------------------------- 1 | name: Prepare Queued PR For Merge 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | paths: 7 | - 'src/helpers/prepare-queued-pr-for-merge.ts' 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - uses: ./ 17 | with: 18 | helper: prepare-queued-pr-for-merge 19 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: [edited, published] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Update Major Version 13 | run: | 14 | MAJOR_VERSION=$(echo "${GITHUB_REF}" | cut -d "/" -f3 | cut -d "." -f1) 15 | echo "New version: ${MAJOR_VERSION}" 16 | git tag ${MAJOR_VERSION} 17 | git push --tags -f 18 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | release: 9 | name: Release 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | 15 | - name: Setup Bun 16 | uses: oven-sh/setup-bun@v2 17 | with: 18 | bun-version-file: package.json 19 | 20 | - name: Setup Node 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: latest 24 | 25 | - name: Create Release 26 | run: bunx semantic-release 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} 29 | -------------------------------------------------------------------------------- /.github/workflows/remove-label.yml: -------------------------------------------------------------------------------- 1 | name: Remove Label 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | paths: 7 | - 'src/helpers/remove-label.ts' 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - uses: ./ 17 | with: 18 | helper: remove-label 19 | label: Test Label 20 | -------------------------------------------------------------------------------- /.github/workflows/remove-pr-from-merge-queue.yml: -------------------------------------------------------------------------------- 1 | name: Remove Pr From Merge Queue 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | paths: 7 | - 'src/helpers/remove-pr-from-merge-queue.ts' 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - uses: ./ 17 | with: 18 | helper: remove-pr-from-merge-queue 19 | seconds: 3600 20 | -------------------------------------------------------------------------------- /.github/workflows/reopen-pr.yml: -------------------------------------------------------------------------------- 1 | name: Reopen Pr 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | paths: 7 | - 'src/helpers/reopen-pr.ts' 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - uses: ./ 17 | with: 18 | helper: reopen-pr 19 | -------------------------------------------------------------------------------- /.github/workflows/rerun-pr-checks.yml: -------------------------------------------------------------------------------- 1 | name: Rerun PR Checks 2 | 3 | on: 4 | pull_request: 5 | types: [labeled] 6 | 7 | jobs: 8 | test: 9 | if: ${{ github.event.label.name == 'RE-RUN PR CHECKS' }} 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | 15 | - uses: ./ 16 | with: 17 | helper: rerun-pr-checks 18 | 19 | - uses: ./ 20 | with: 21 | helper: remove-label 22 | label: RE-RUN PR CHECKS 23 | -------------------------------------------------------------------------------- /.github/workflows/set-commit-status.yml: -------------------------------------------------------------------------------- 1 | name: Set Commit Status 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | paths: 7 | - 'src/helpers/set-commit-status.ts' 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - uses: ./ 17 | with: 18 | helper: set-commit-status 19 | sha: ${{ github.event.pull_request.head.sha }} 20 | context: Commit Status Test 21 | state: success 22 | description: set-commit-status is working! 23 | -------------------------------------------------------------------------------- /.github/workflows/set-latest-pipeline-status.yml: -------------------------------------------------------------------------------- 1 | name: Set Latest Pipeline Status 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | paths: 7 | - 'src/helpers/set-latest-pipeline-status.ts' 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - uses: ./ 17 | with: 18 | helper: set-latest-pipeline-status 19 | sha: ${{ github.event.pull_request.head.sha }} 20 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: PR Checks 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | 7 | jobs: 8 | test: 9 | name: Build and Test 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | with: 15 | ref: ${{ github.event.pull_request.head.ref }} 16 | repository: ${{ github.event.pull_request.head.repo.full_name }} 17 | token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN || secrets.GITHUB_TOKEN }} 18 | 19 | - name: Validate package.json 20 | uses: ExpediaGroup/package-json-validator@v1 21 | with: 22 | rules: ranges 23 | dependency-types: | 24 | dependencies 25 | devDependencies 26 | 27 | - name: Setup Bun 28 | uses: oven-sh/setup-bun@v2 29 | with: 30 | bun-version-file: package.json 31 | 32 | - name: Install Dependencies 33 | run: bun install 34 | 35 | - name: Validate Copyright Headers 36 | run: bun ./scripts/verify-file-headers.ts 37 | 38 | - name: Run Prettier 39 | run: bun format-check 40 | 41 | - name: Run Lint 42 | run: bun lint 43 | 44 | - name: Run Unit Tests 45 | run: bun run test 46 | 47 | - name: Package 48 | run: bun package 49 | 50 | - name: Compare the expected and actual dist/ directories 51 | run: | 52 | if [[ $(git status --porcelain) ]]; then 53 | echo "Detected uncommitted changes after build. Please run npm run package and commit the changes!" 54 | git status 55 | git config user.name "${{ github.actor }}" 56 | git config user.email "${{ github.actor }}@users.noreply.github.com" 57 | git add . 58 | git commit -m "chore: committing generated dist" --no-verify 59 | git push 60 | exit 1 61 | fi 62 | -------------------------------------------------------------------------------- /.github/workflows/update-check-result.yml: -------------------------------------------------------------------------------- 1 | name: Update Check Result 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | paths: 7 | - 'src/helpers/update-check-result.ts' 8 | 9 | jobs: 10 | test: 11 | name: Update Check Result 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | 17 | - uses: ./ 18 | with: 19 | helper: update-check-result 20 | context: Update Check Result 21 | sha: ${{ github.event.pull_request.head.sha }} 22 | state: success 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS generated files # 2 | ###################### 3 | .DS_Store 4 | .DS_Store? 5 | ._* 6 | .Spotlight-V100 7 | .Trashes 8 | Icon? 9 | ehthumbs.db 10 | Thumbs.db 11 | 12 | node_modules 13 | .idea 14 | package-lock.json 15 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | bun generate && bun lint && bun format && bun package && git add . 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | node_modules/ 4 | scripts/ 5 | templates/ 6 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 140, 3 | "useTabs": false, 4 | "singleQuote": true, 5 | "trailingComma": "none", 6 | "bracketSpacing": true, 7 | "arrowParens": "avoid" 8 | } 9 | -------------------------------------------------------------------------------- /.releaserc.yaml: -------------------------------------------------------------------------------- 1 | plugins: 2 | - - '@semantic-release/commit-analyzer' 3 | - preset: angular 4 | releaseRules: 5 | - breaking: true 6 | release: major 7 | - type: breaking 8 | release: major 9 | - type: docs 10 | release: patch 11 | - type: refactor 12 | release: patch 13 | - scope: no-release 14 | release: false 15 | - '@semantic-release/release-notes-generator' 16 | - '@semantic-release/github' 17 | branches: 18 | - main 19 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We'd love to accept your patches and contributions to this project. There are just a few guidelines you need to follow which are described in detail below. 4 | 5 | ## How To Contribute 6 | 7 | ### 1. Fork this repo 8 | 9 | You should create a fork of this project in your account and work from there. You can create a fork by clicking the fork button in GitHub. 10 | 11 | ### 2. One feature, one branch 12 | 13 | Work for each new feature/issue should occur in its own branch. To create a new branch from the command line: 14 | 15 | ```shell 16 | git checkout -b my-new-feature 17 | ``` 18 | 19 | where "my-new-feature" describes what you're working on. 20 | 21 | ### 3. Get set up locally 22 | 23 | This script will install all dependencies for local development. 24 | 25 | ```shell 26 | npm run setup 27 | ``` 28 | 29 | ### 4. If you are creating a new helper 30 | 31 | ```shell 32 | npm run create-helper 33 | ``` 34 | 35 | ### 5. Add tests for any bug fixes or new functionality 36 | 37 | All functions must be tested with a unit test. Please follow the existing convention of one exported function per file with a corresponding file to test it. Run tests using `npm run test`, or using the [Jest CLI](https://jestjs.io/docs/cli). 38 | 39 | There are also integration tests present in the [workflow](./.github/workflows) directory, which will actually run each Github Action using the code from this repository. This allows you to test your changes right within the pull request you make. 40 | 41 | Unfortunately, Github Action projects cannot be run locally. You must rely on the unit and integration tests to run your code. 42 | 43 | ### 6. Check code style 44 | 45 | Before opening a pull request, ensure that you have installed all dependencies so the pre-commit hooks will run. 46 | These hooks will run ESLint according to the [.eslintrc.json](./.eslintrc.json), 47 | style the code according to the [.prettierrc.json](./.prettierrc.json), and package the code into the `dist` directory which is required for Github Actions code. 48 | 49 | ### 7. Add documentation for new or updated functionality 50 | 51 | Please review all of the .md files in this project to see if they are impacted by your change and update them accordingly. 52 | 53 | ### 8. Format Commits 54 | 55 | This project uses [Semantic Release](https://github.com/semantic-release/semantic-release) for versioning. As such, commits need to follow the format: `(): `. All fields are required. 56 | 57 | ### 9. Submit Pull Request and describe the change 58 | 59 | Push your changes to your branch and open a pull request against the parent repo on GitHub. The project administrators will review your pull request and respond with feedback. 60 | 61 | ## How Your Contribution Gets Merged 62 | 63 | Upon Pull Request submission, your code will be reviewed by the maintainers. They will confirm at least the following: 64 | 65 | - Tests run successfully (unit, coverage, integration, style). 66 | - Contribution policy has been followed. 67 | 68 | One (human) reviewer will need to sign off on your Pull Request before it can be merged. 69 | 70 | ### 10. Release to Github Marketplace 71 | 72 | Once your change has been merged, a new release will be created to the repository. In order to allow others to consume this change, you will need to publish the change as a separate release to the Github Marketplace. Follow [these instructions](https://docs.github.com/en/actions/creating-actions/publishing-actions-in-github-marketplace) to do so. 73 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExpediaGroup/github-helpers/fd12b8207a941757f3175ac5c7747726dacee65c/bun.lockb -------------------------------------------------------------------------------- /dist/522.index.js: -------------------------------------------------------------------------------- 1 | export const id = 522; 2 | export const ids = [522]; 3 | export const modules = { 4 | 5 | /***/ 6522: 6 | /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { 7 | 8 | __webpack_require__.r(__webpack_exports__); 9 | /* harmony export */ __webpack_require__.d(__webpack_exports__, { 10 | /* harmony export */ "approvePr": () => (/* binding */ approvePr) 11 | /* harmony export */ }); 12 | /* harmony import */ var _actions_github__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(5438); 13 | /* harmony import */ var _actions_github__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_actions_github__WEBPACK_IMPORTED_MODULE_0__); 14 | /* harmony import */ var _octokit__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(6161); 15 | /* 16 | Copyright 2021 Expedia, Inc. 17 | Licensed under the Apache License, Version 2.0 (the "License"); 18 | you may not use this file except in compliance with the License. 19 | You may obtain a copy of the License at 20 | https://www.apache.org/licenses/LICENSE-2.0 21 | Unless required by applicable law or agreed to in writing, software 22 | distributed under the License is distributed on an "AS IS" BASIS, 23 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 24 | See the License for the specific language governing permissions and 25 | limitations under the License. 26 | */ 27 | 28 | 29 | const approvePr = async () => _octokit__WEBPACK_IMPORTED_MODULE_1__/* .octokit.pulls.createReview */ .K.pulls.createReview({ 30 | pull_number: _actions_github__WEBPACK_IMPORTED_MODULE_0__.context.issue.number, 31 | body: 'Approved by bot', 32 | event: 'APPROVE', 33 | ..._actions_github__WEBPACK_IMPORTED_MODULE_0__.context.repo 34 | }); 35 | 36 | 37 | /***/ }), 38 | 39 | /***/ 6161: 40 | /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { 41 | 42 | /* harmony export */ __webpack_require__.d(__webpack_exports__, { 43 | /* harmony export */ "K": () => (/* binding */ octokit), 44 | /* harmony export */ "o": () => (/* binding */ octokitGraphql) 45 | /* harmony export */ }); 46 | /* harmony import */ var _actions_core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2186); 47 | /* harmony import */ var _actions_core__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_actions_core__WEBPACK_IMPORTED_MODULE_0__); 48 | /* harmony import */ var _adobe_node_fetch_retry__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(3006); 49 | /* harmony import */ var _adobe_node_fetch_retry__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_adobe_node_fetch_retry__WEBPACK_IMPORTED_MODULE_1__); 50 | /* harmony import */ var _actions_github__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(5438); 51 | /* harmony import */ var _actions_github__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(_actions_github__WEBPACK_IMPORTED_MODULE_2__); 52 | /* 53 | Copyright 2021 Expedia, Inc. 54 | Licensed under the Apache License, Version 2.0 (the "License"); 55 | you may not use this file except in compliance with the License. 56 | You may obtain a copy of the License at 57 | https://www.apache.org/licenses/LICENSE-2.0 58 | Unless required by applicable law or agreed to in writing, software 59 | distributed under the License is distributed on an "AS IS" BASIS, 60 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 61 | See the License for the specific language governing permissions and 62 | limitations under the License. 63 | */ 64 | 65 | 66 | 67 | const githubToken = _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('github_token', { required: true }); 68 | const { rest: octokit, graphql: octokitGraphql } = (0,_actions_github__WEBPACK_IMPORTED_MODULE_2__.getOctokit)(githubToken, { request: { fetch: _adobe_node_fetch_retry__WEBPACK_IMPORTED_MODULE_1__ } }); 69 | 70 | 71 | /***/ }) 72 | 73 | }; 74 | 75 | //# sourceMappingURL=522.index.js.map -------------------------------------------------------------------------------- /dist/522.index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"522.index.js","mappings":";;;;;;;;;;;;;;AAAA;;;;;;;;;;;AAWA;AAEA;AACA;AAEA;AAEA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;ACtBA;;;;;;;;;;;AAWA;AAEA;AACA;AACA;AAEA;AACA","sources":[".././src/helpers/approve-pr.ts",".././src/octokit.ts"],"sourcesContent":["/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport { context } from '@actions/github';\nimport { octokit } from '../octokit';\n\nexport const approvePr = async () =>\n octokit.pulls.createReview({\n pull_number: context.issue.number,\n body: 'Approved by bot',\n event: 'APPROVE',\n ...context.repo\n });\n","/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport * as core from '@actions/core';\nimport * as fetch from '@adobe/node-fetch-retry';\nimport { getOctokit } from '@actions/github';\n\nconst githubToken = core.getInput('github_token', { required: true });\nexport const { rest: octokit, graphql: octokitGraphql } = getOctokit(githubToken, { request: { fetch } });\n"],"names":[],"sourceRoot":""} -------------------------------------------------------------------------------- /dist/579.index.js: -------------------------------------------------------------------------------- 1 | export const id = 579; 2 | export const ids = [579]; 3 | export const modules = { 4 | 5 | /***/ 2579: 6 | /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { 7 | 8 | __webpack_require__.r(__webpack_exports__); 9 | /* harmony export */ __webpack_require__.d(__webpack_exports__, { 10 | /* harmony export */ approvePr: () => (/* binding */ approvePr) 11 | /* harmony export */ }); 12 | /* harmony import */ var _actions_github__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3228); 13 | /* harmony import */ var _actions_github__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_actions_github__WEBPACK_IMPORTED_MODULE_0__); 14 | /* harmony import */ var _octokit__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(6590); 15 | /* 16 | Copyright 2021 Expedia, Inc. 17 | Licensed under the Apache License, Version 2.0 (the "License"); 18 | you may not use this file except in compliance with the License. 19 | You may obtain a copy of the License at 20 | https://www.apache.org/licenses/LICENSE-2.0 21 | Unless required by applicable law or agreed to in writing, software 22 | distributed under the License is distributed on an "AS IS" BASIS, 23 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 24 | See the License for the specific language governing permissions and 25 | limitations under the License. 26 | */ 27 | 28 | 29 | const approvePr = async () => _octokit__WEBPACK_IMPORTED_MODULE_1__/* .octokit */ .A.pulls.createReview({ 30 | pull_number: _actions_github__WEBPACK_IMPORTED_MODULE_0__.context.issue.number, 31 | body: 'Approved by bot', 32 | event: 'APPROVE', 33 | ..._actions_github__WEBPACK_IMPORTED_MODULE_0__.context.repo 34 | }); 35 | 36 | 37 | /***/ }), 38 | 39 | /***/ 6590: 40 | /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { 41 | 42 | /* harmony export */ __webpack_require__.d(__webpack_exports__, { 43 | /* harmony export */ A: () => (/* binding */ octokit), 44 | /* harmony export */ n: () => (/* binding */ octokitGraphql) 45 | /* harmony export */ }); 46 | /* harmony import */ var _actions_core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(7484); 47 | /* harmony import */ var _actions_core__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_actions_core__WEBPACK_IMPORTED_MODULE_0__); 48 | /* harmony import */ var _adobe_node_fetch_retry__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(1806); 49 | /* harmony import */ var _adobe_node_fetch_retry__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_adobe_node_fetch_retry__WEBPACK_IMPORTED_MODULE_1__); 50 | /* harmony import */ var _actions_github__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(3228); 51 | /* harmony import */ var _actions_github__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(_actions_github__WEBPACK_IMPORTED_MODULE_2__); 52 | /* 53 | Copyright 2021 Expedia, Inc. 54 | Licensed under the Apache License, Version 2.0 (the "License"); 55 | you may not use this file except in compliance with the License. 56 | You may obtain a copy of the License at 57 | https://www.apache.org/licenses/LICENSE-2.0 58 | Unless required by applicable law or agreed to in writing, software 59 | distributed under the License is distributed on an "AS IS" BASIS, 60 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 61 | See the License for the specific language governing permissions and 62 | limitations under the License. 63 | */ 64 | 65 | 66 | 67 | const githubToken = _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('github_token', { required: true }); 68 | const { rest: octokit, graphql: octokitGraphql } = (0,_actions_github__WEBPACK_IMPORTED_MODULE_2__.getOctokit)(githubToken, { request: { fetch: _adobe_node_fetch_retry__WEBPACK_IMPORTED_MODULE_1__ } }); 69 | 70 | 71 | /***/ }) 72 | 73 | }; 74 | 75 | //# sourceMappingURL=579.index.js.map -------------------------------------------------------------------------------- /dist/579.index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"579.index.js","mappings":";;;;;;;;;;;;;;AAAA;;;;;;;;;;;AAWA;AAEA;AACA;AAEA;AAEA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;ACtBA;;;;;;;;;;;AAWA;AAEA;AACA;AACA;AAEA;AACA","sources":[".././src/helpers/approve-pr.ts",".././src/octokit.ts"],"sourcesContent":["/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport { context } from '@actions/github';\nimport { octokit } from '../octokit';\n\nexport const approvePr = async () =>\n octokit.pulls.createReview({\n pull_number: context.issue.number,\n body: 'Approved by bot',\n event: 'APPROVE',\n ...context.repo\n });\n","/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport * as core from '@actions/core';\nimport * as fetch from '@adobe/node-fetch-retry';\nimport { getOctokit } from '@actions/github';\n\nconst githubToken = core.getInput('github_token', { required: true });\nexport const { rest: octokit, graphql: octokitGraphql } = getOctokit(githubToken, { request: { fetch } });\n"],"names":[],"sourceRoot":""} -------------------------------------------------------------------------------- /dist/682.index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"682.index.js","mappings":";;;;;;;;;;;;;;;;;;AAAA;;;;;;;;;;;AAWA;AAEA;AACA;AACA;AACA;AAEA;AACA;AACA;AASA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;ACtDA;;;;;;;;;;;AAWA;AAEA;AACA;AACA;AAEA;AACA","sources":[".././src/helpers/rerun-pr-checks.ts",".././src/octokit.ts"],"sourcesContent":["/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport * as core from '@actions/core';\nimport { context } from '@actions/github';\nimport { map } from 'bluebird';\nimport { octokit } from '../octokit';\n\nexport const rerunPrChecks = async () => {\n /** grab owner in case of fork branch */\n const {\n data: {\n head: {\n user: { login: owner },\n sha: latestHash,\n ref: branch\n }\n }\n } = await octokit.pulls.get({\n pull_number: context.issue.number,\n ...context.repo\n });\n const workflowRunResponses = await map(['pull_request', 'pull_request_target'], event =>\n octokit.actions.listWorkflowRunsForRepo({\n branch,\n ...context.repo,\n owner,\n event,\n per_page: 100,\n status: 'completed'\n })\n );\n const workflowRuns = workflowRunResponses.map(response => response.data.workflow_runs).flat();\n if (!workflowRuns.length) {\n core.info(`No workflow runs found on branch ${branch} on ${owner}/${context.repo.repo}`);\n return;\n }\n const latestWorkflowRuns = workflowRuns.filter(({ head_sha }) => head_sha === latestHash);\n core.info(`There are ${latestWorkflowRuns.length} checks associated with the latest commit, triggering reruns...`);\n\n return map(latestWorkflowRuns, async ({ id, name }) => {\n core.info(`- Rerunning ${name} (${id})`);\n await octokit.actions.reRunWorkflow({ run_id: id, ...context.repo });\n });\n};\n","/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport * as core from '@actions/core';\nimport * as fetch from '@adobe/node-fetch-retry';\nimport { getOctokit } from '@actions/github';\n\nconst githubToken = core.getInput('github_token', { required: true });\nexport const { rest: octokit, graphql: octokitGraphql } = getOctokit(githubToken, { request: { fetch } });\n"],"names":[],"sourceRoot":""} -------------------------------------------------------------------------------- /dist/766.index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"766.index.js","mappings":";;;;;;;;;;;;;;;;;;AAAA;;;;;;;;;;;AAWA;AAEA;AACA;AACA;AACA;AAEA;AACA;AACA;AASA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;ACtDA;;;;;;;;;;;AAWA;AAEA;AACA;AACA;AAEA;AACA","sources":[".././src/helpers/rerun-pr-checks.ts",".././src/octokit.ts"],"sourcesContent":["/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport * as core from '@actions/core';\nimport { context } from '@actions/github';\nimport { map } from 'bluebird';\nimport { octokit } from '../octokit';\n\nexport const rerunPrChecks = async () => {\n /** grab owner in case of fork branch */\n const {\n data: {\n head: {\n user: { login: owner },\n sha: latestHash,\n ref: branch\n }\n }\n } = await octokit.pulls.get({\n pull_number: context.issue.number,\n ...context.repo\n });\n const workflowRunResponses = await map(['pull_request', 'pull_request_target'], event =>\n octokit.actions.listWorkflowRunsForRepo({\n branch,\n ...context.repo,\n owner,\n event,\n per_page: 100,\n status: 'completed'\n })\n );\n const workflowRuns = workflowRunResponses.map(response => response.data.workflow_runs).flat();\n if (!workflowRuns.length) {\n core.info(`No workflow runs found on branch ${branch} on ${owner}/${context.repo.repo}`);\n return;\n }\n const latestWorkflowRuns = workflowRuns.filter(({ head_sha }) => head_sha === latestHash);\n core.info(`There are ${latestWorkflowRuns.length} checks associated with the latest commit, triggering reruns...`);\n\n return map(latestWorkflowRuns, async ({ id, name }) => {\n core.info(`- Rerunning ${name} (${id})`);\n await octokit.actions.reRunWorkflow({ run_id: id, ...context.repo });\n });\n};\n","/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport * as core from '@actions/core';\nimport * as fetch from '@adobe/node-fetch-retry';\nimport { getOctokit } from '@actions/github';\n\nconst githubToken = core.getInput('github_token', { required: true });\nexport const { rest: octokit, graphql: octokitGraphql } = getOctokit(githubToken, { request: { fetch } });\n"],"names":[],"sourceRoot":""} -------------------------------------------------------------------------------- /dist/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import typescriptEslint from 'typescript-eslint'; 2 | 3 | export default [ 4 | ...typescriptEslint.configs.recommended, 5 | { 6 | ignores: ['dist'] 7 | } 8 | ]; 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-helpers", 3 | "packageManager": "bun@1.2.2", 4 | "main": "src/main.ts", 5 | "type": "module", 6 | "private": true, 7 | "dependencies": { 8 | "@actions/core": "1.11.1", 9 | "@actions/github": "6.0.1", 10 | "@adobe/node-fetch-retry": "2.2.0", 11 | "@octokit/graphql-schema": "15.26.0", 12 | "axios": "1.9.0", 13 | "bluebird": "3.7.2", 14 | "codeowners-utils": "1.0.2", 15 | "js-yaml": "4.1.0", 16 | "lodash.camelcase": "4.3.0", 17 | "lodash.chunk": "4.2.0", 18 | "lodash.samplesize": "4.2.0", 19 | "lodash.union": "4.6.0", 20 | "lodash.uniq": "4.5.0", 21 | "micromatch": "4.0.8", 22 | "simple-git": "3.27.0" 23 | }, 24 | "devDependencies": { 25 | "@swc/jest": "0.2.38", 26 | "@total-typescript/ts-reset": "0.6.1", 27 | "@types/bluebird": "3.5.42", 28 | "@types/glob": "8.1.0", 29 | "@types/jest": "29.5.14", 30 | "@types/js-yaml": "4.0.9", 31 | "@types/lodash.camelcase": "4.3.9", 32 | "@types/lodash.chunk": "4.2.9", 33 | "@types/lodash.samplesize": "4.2.9", 34 | "@types/lodash.union": "4.6.9", 35 | "@types/lodash.uniq": "4.5.9", 36 | "@types/micromatch": "4.0.9", 37 | "@vercel/ncc": "0.38.3", 38 | "bun-types": "1.2.14", 39 | "eslint": "9.27.0", 40 | "husky": "9.1.7", 41 | "jest": "29.7.0", 42 | "plop": "4.0.1", 43 | "prettier": "3.5.3", 44 | "typescript": "5.8.3", 45 | "typescript-eslint": "8.32.1" 46 | }, 47 | "jest": { 48 | "clearMocks": true, 49 | "transform": { 50 | "^.+\\.(t|j)sx?$": "@swc/jest" 51 | }, 52 | "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$" 53 | }, 54 | "scripts": { 55 | "create-helper": "plop helper --force", 56 | "format": "prettier --write .", 57 | "format-check": "prettier --check .", 58 | "generate": "bun scripts/generate-helper-inputs.ts", 59 | "lint": "eslint --quiet --fix ./**/*.ts src/**/*.ts test/**/*.ts", 60 | "package": "ncc build --source-map --license licenses.txt", 61 | "prepare": "husky", 62 | "setup": "./scripts/dev-setup.sh", 63 | "test": "bun jest" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /plopfile.js: -------------------------------------------------------------------------------- 1 | export default plop => { 2 | plop.setHelper('spaceSeparatedCase', helper => helper.split('-').join(' ')); 3 | 4 | plop.setGenerator('helper', { 5 | description: 'new github helper', 6 | prompts: [ 7 | { 8 | type: 'input', 9 | name: 'helper', 10 | message: 'New helper name (e.g. my-new-helper):' 11 | }, 12 | { 13 | type: 'input', 14 | name: 'description', 15 | message: 'Enter a detailed description of what the helper does and how it can be used:' 16 | } 17 | ], 18 | actions: [ 19 | { 20 | type: 'add', 21 | path: 'src/helpers/{{ helper }}.ts', 22 | templateFile: 'templates/helper.hbs' 23 | }, 24 | { 25 | type: 'add', 26 | path: 'test/helpers/{{ helper }}.test.ts', 27 | templateFile: 'templates/test.hbs' 28 | }, 29 | { 30 | type: 'add', 31 | path: '.github/workflows/{{ helper }}.yml', 32 | templateFile: 'templates/workflow.hbs' 33 | }, 34 | { 35 | type: 'modify', 36 | path: 'README.md', 37 | pattern: /Each of the following helpers are defined in a file of the same name in `src\/helpers`:/g, 38 | templateFile: 'templates/docs.hbs' 39 | } 40 | ] 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["local>ExpediaGroup/renovate-config"], 4 | "assignAutomerge": true, 5 | "assigneesFromCodeOwners": true, 6 | "packageRules": [ 7 | { 8 | "matchDepTypes": ["dependencies", "devDependencies"], 9 | "matchUpdateTypes": ["patch", "minor"], 10 | "groupName": "dependencies", 11 | "automerge": true 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /reset.d.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/total-typescript/ts-reset 2 | import '@total-typescript/ts-reset'; 3 | -------------------------------------------------------------------------------- /scripts/dev-setup.sh: -------------------------------------------------------------------------------- 1 | curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash 2 | curl -fsSL https://bun.sh/install | bash 3 | 4 | export NVM_DIR="$HOME/.nvm" 5 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm 6 | [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion 7 | 8 | nvm install 9 | nvm use 10 | npm install 11 | -------------------------------------------------------------------------------- /scripts/generate-helper-inputs.ts: -------------------------------------------------------------------------------- 1 | import { load } from 'js-yaml'; 2 | import { COPYRIGHT_HEADER } from '../src/constants'; 3 | 4 | const yamlContents = await Bun.file('action.yml').text(); 5 | const inputs = (load(yamlContents) as { inputs: { [input: string]: { description: string; required: boolean }} }).inputs 6 | 7 | const newContents = `${COPYRIGHT_HEADER} 8 | 9 | export class HelperInputs { 10 | ${Object.keys(inputs).map(input => ` declare ${input}?: string;`).join('\n')} 11 | } 12 | ` 13 | 14 | await Bun.write('src/types/generated.ts', newContents); 15 | -------------------------------------------------------------------------------- /scripts/verify-file-headers.ts: -------------------------------------------------------------------------------- 1 | import { sync } from 'glob'; 2 | import { filter } from 'bluebird'; 3 | 4 | const filePaths = sync('{src,test}/**/*.ts'); 5 | const filesWithoutCopyrightHeader = await filter(filePaths, async filePath => { 6 | const fileContents = await Bun.file(filePath).text(); 7 | return !fileContents.startsWith('/*\nCopyright') 8 | }); 9 | 10 | if (filesWithoutCopyrightHeader.length) { 11 | console.error(`\nThe following files are missing a valid copyright header:${filesWithoutCopyrightHeader.map(file => `\n • ${file}`).join()}`); 12 | process.exit(1); 13 | } 14 | 15 | console.info('All files contain a valid copyright header!'); 16 | -------------------------------------------------------------------------------- /scripts/verify-pr-title-has-valid-descriptor.ts: -------------------------------------------------------------------------------- 1 | import { sync } from 'glob'; 2 | 3 | const title = process.env.TITLE; 4 | if (!title) throw new Error('process.env.TITLE is required'); 5 | 6 | const helpers = sync('src/helpers/**/*.ts') 7 | .map(file => file.match(/(?<=src\/helpers\/)(.*)(?=.ts)/)?.find(Boolean)); 8 | const validDescriptors = helpers.concat(['repo', 'deps', 'deps-dev']); 9 | 10 | const prTitleHasValidDescriptor = title.match(new RegExp(`\((${validDescriptors.join('|')})\)`, 'g')); 11 | 12 | if (!prTitleHasValidDescriptor) { 13 | console.error(`\nPR title is missing a valid descriptor inside parentheses. Must be one of the following:\n${validDescriptors.map(descriptor => `\n • ${descriptor}`).join('\n')}`); 14 | process.exit(1); 15 | } 16 | 17 | console.info('PR title contains a valid descriptor!'); 18 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | // These extra headers are for experimental API features on Github Enterprise. See https://docs.github.com/en/enterprise-server@3.0/rest/overview/api-previews for details. 15 | const PREVIEWS = ['ant-man', 'flash', 'groot', 'inertia', 'starfox']; 16 | export const GITHUB_OPTIONS = { 17 | headers: { 18 | accept: PREVIEWS.map(preview => `application/vnd.github.${preview}-preview+json`).join() 19 | } 20 | }; 21 | 22 | export const SECONDS_IN_A_DAY = 86400000; 23 | export const DEFAULT_PIPELINE_STATUS = 'Pipeline Status'; 24 | export const DEFAULT_PIPELINE_DESCRIPTION = 'Pipeline clear.'; 25 | export const PRODUCTION_ENVIRONMENT = 'production'; 26 | export const LATE_REVIEW = 'Late Review'; 27 | export const OVERDUE_ISSUE = 'Overdue'; 28 | export const ALMOST_OVERDUE_ISSUE = 'Due Soon'; 29 | export const PRIORITY_1 = 'Priority: Critical'; 30 | export const PRIORITY_2 = 'Priority: High'; 31 | export const PRIORITY_3 = 'Priority: Medium'; 32 | export const PRIORITY_4 = 'Priority: Low'; 33 | export const PRIORITY_LABELS = [PRIORITY_1, PRIORITY_2, PRIORITY_3, PRIORITY_4] as const; 34 | export const PRIORITY_TO_DAYS_MAP = { 35 | [PRIORITY_1]: 2, 36 | [PRIORITY_2]: 14, 37 | [PRIORITY_3]: 45, 38 | [PRIORITY_4]: 90 39 | }; 40 | export const CORE_APPROVED_PR_LABEL = 'CORE APPROVED'; 41 | export const PEER_APPROVED_PR_LABEL = 'PEER APPROVED'; 42 | export const READY_FOR_MERGE_PR_LABEL = 'READY FOR MERGE'; 43 | export const MERGE_QUEUE_STATUS = 'QUEUE CHECKER'; 44 | export const QUEUED_FOR_MERGE_PREFIX = 'QUEUED FOR MERGE'; 45 | export const FIRST_QUEUED_PR_LABEL = `${QUEUED_FOR_MERGE_PREFIX} #1`; 46 | export const JUMP_THE_QUEUE_PR_LABEL = 'JUMP THE QUEUE'; 47 | export const DEFAULT_PR_TITLE_REGEX = '^(build|ci|chore|docs|feat|fix|perf|refactor|style|test|revert|Revert|BREAKING CHANGE)((.*))?: .+$'; 48 | export const COPYRIGHT_HEADER = `/* 49 | Copyright 2021 Expedia, Inc. 50 | Licensed under the Apache License, Version 2.0 (the "License"); 51 | you may not use this file except in compliance with the License. 52 | You may obtain a copy of the License at 53 | https://www.apache.org/licenses/LICENSE-2.0 54 | Unless required by applicable law or agreed to in writing, software 55 | distributed under the License is distributed on an "AS IS" BASIS, 56 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 57 | See the License for the specific language governing permissions and 58 | limitations under the License. 59 | */`; 60 | -------------------------------------------------------------------------------- /src/helpers/add-labels.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { HelperInputs } from '../types/generated'; 15 | import { context } from '@actions/github'; 16 | import { octokit } from '../octokit'; 17 | 18 | export class AddLabels extends HelperInputs { 19 | labels = ''; 20 | } 21 | 22 | export const addLabels = ({ labels }: AddLabels) => 23 | octokit.issues.addLabels({ 24 | labels: labels.split('\n'), 25 | issue_number: context.issue.number, 26 | ...context.repo 27 | }); 28 | -------------------------------------------------------------------------------- /src/helpers/add-late-review-label.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { LATE_REVIEW, SECONDS_IN_A_DAY } from '../constants'; 15 | import { HelperInputs } from '../types/generated'; 16 | import { context } from '@actions/github'; 17 | import { octokit } from '../octokit'; 18 | import { map } from 'bluebird'; 19 | import { paginateAllOpenPullRequests } from '../utils/paginate-open-pull-requests'; 20 | import { SinglePullRequest } from '../types/github'; 21 | 22 | export class AddLateReviewLabel extends HelperInputs { 23 | declare days?: string; 24 | } 25 | 26 | export const addLateReviewLabel = async ({ days = '1' }: AddLateReviewLabel) => { 27 | const openPullRequests = await paginateAllOpenPullRequests(); 28 | 29 | return map(openPullRequests, pr => { 30 | if (!isLabelNeeded(pr, Number(days))) { 31 | return; 32 | } 33 | 34 | return octokit.issues.addLabels({ 35 | labels: [LATE_REVIEW], 36 | issue_number: pr.number, 37 | ...context.repo 38 | }); 39 | }); 40 | }; 41 | 42 | const isLabelNeeded = ({ requested_reviewers, requested_teams, updated_at }: SinglePullRequest, days: number) => { 43 | const last_updated = new Date(updated_at); 44 | const now = new Date(); 45 | const timeSinceLastUpdated = now.getTime() - last_updated.getTime(); 46 | const dayThreshold = days * SECONDS_IN_A_DAY; 47 | const isWaitingOnReviewers = Boolean(requested_reviewers || requested_teams); 48 | return timeSinceLastUpdated > dayThreshold && isWaitingOnReviewers; 49 | }; 50 | -------------------------------------------------------------------------------- /src/helpers/add-pr-approval-label.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { CORE_APPROVED_PR_LABEL, PEER_APPROVED_PR_LABEL } from '../constants'; 15 | import { HelperInputs } from '../types/generated'; 16 | import { context } from '@actions/github'; 17 | import { getCoreMemberLogins } from '../utils/get-core-member-logins'; 18 | import { octokit } from '../octokit'; 19 | 20 | export class AddPrApprovalLabel extends HelperInputs { 21 | login = ''; 22 | declare teams?: string; 23 | } 24 | 25 | export const addPrApprovalLabel = async ({ teams, login }: AddPrApprovalLabel) => { 26 | const coreMemberLogins = await getCoreMemberLogins(context.issue.number, teams?.split('\n')); 27 | const approvalLabel = coreMemberLogins.includes(login) ? CORE_APPROVED_PR_LABEL : PEER_APPROVED_PR_LABEL; 28 | return octokit.issues.addLabels({ 29 | labels: [approvalLabel], 30 | issue_number: context.issue.number, 31 | ...context.repo 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /src/helpers/approve-pr.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { context } from '@actions/github'; 15 | import { octokit } from '../octokit'; 16 | 17 | export const approvePr = async () => 18 | octokit.pulls.createReview({ 19 | pull_number: context.issue.number, 20 | body: 'Approved by bot', 21 | event: 'APPROVE', 22 | ...context.repo 23 | }); 24 | -------------------------------------------------------------------------------- /src/helpers/are-reviewers-required.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import * as core from '@actions/core'; 15 | import { HelperInputs } from '../types/generated'; 16 | import { getRequiredCodeOwnersEntries } from '../utils/get-core-member-logins'; 17 | import { context } from '@actions/github'; 18 | 19 | export class AreReviewersRequired extends HelperInputs { 20 | teams = ''; 21 | } 22 | 23 | export const areReviewersRequired = async ({ teams }: AreReviewersRequired) => { 24 | const prNumber = context.issue.number; 25 | const teamsList = teams?.split('\n'); 26 | const requiredCodeOwnersEntries = (await getRequiredCodeOwnersEntries(prNumber)).map(({ owners }) => owners).flat(); 27 | const notRequiredTeams = teamsList.filter(team => !requiredCodeOwnersEntries.includes(team)); 28 | if (notRequiredTeams.length) { 29 | core.info(`${notRequiredTeams.join(', ')} not in list of required reviewers (${requiredCodeOwnersEntries.join(', ')})`); 30 | return false; 31 | } 32 | return true; 33 | }; 34 | -------------------------------------------------------------------------------- /src/helpers/assign-pr-reviewers.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import * as core from '@actions/core'; 15 | import { HelperInputs } from '../types/generated'; 16 | import { context } from '@actions/github'; 17 | import { getCoreMemberLogins } from '../utils/get-core-member-logins'; 18 | import { map } from 'bluebird'; 19 | import { notifyUser } from '../utils/notify-user'; 20 | import { octokit } from '../octokit'; 21 | import { sampleSize } from 'lodash'; 22 | import { CORE_APPROVED_PR_LABEL } from '../constants'; 23 | 24 | export class AssignPrReviewer extends HelperInputs { 25 | declare teams?: string; 26 | declare login?: string; 27 | declare number_of_assignees?: string; 28 | declare slack_webhook_url?: string; 29 | declare pull_number?: string; 30 | } 31 | 32 | export const assignPrReviewers = async ({ 33 | teams, 34 | login, 35 | number_of_assignees = '1', 36 | slack_webhook_url, 37 | pull_number = String(context.issue.number) 38 | }: AssignPrReviewer) => { 39 | const coreMemberLogins = await getCoreMemberLogins(context.issue.number, teams?.split('\n')); 40 | const { 41 | data: { user, labels } 42 | } = await octokit.pulls.get({ pull_number: context.issue.number, ...context.repo }); 43 | 44 | if (login && coreMemberLogins.includes(login)) { 45 | core.info('Already a core member, no need to assign.'); 46 | return; 47 | } 48 | 49 | if (labels?.find(label => label.name === CORE_APPROVED_PR_LABEL)) { 50 | core.info('Already approved by a core member, no need to assign.'); 51 | return; 52 | } 53 | const prAuthorUsername = user?.login; 54 | const filteredCoreMemberLogins = coreMemberLogins.filter(userName => userName !== prAuthorUsername); 55 | const assignees = sampleSize(filteredCoreMemberLogins, Number(number_of_assignees)); 56 | 57 | await octokit.issues.addAssignees({ 58 | assignees, 59 | issue_number: Number(pull_number), 60 | ...context.repo 61 | }); 62 | 63 | if (slack_webhook_url) { 64 | await map( 65 | assignees, 66 | async assignee => 67 | notifyUser({ 68 | login: assignee, 69 | pull_number: Number(pull_number), 70 | slack_webhook_url 71 | }), 72 | { concurrency: 1 } 73 | ); 74 | } 75 | }; 76 | -------------------------------------------------------------------------------- /src/helpers/check-pr-title.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { DEFAULT_PR_TITLE_REGEX } from '../constants'; 15 | import { HelperInputs } from '../types/generated'; 16 | import { context } from '@actions/github'; 17 | import { octokit } from '../octokit'; 18 | import { setFailed } from '@actions/core'; 19 | 20 | export class CheckPrTitle extends HelperInputs { 21 | declare pattern?: string; 22 | } 23 | 24 | export const checkPrTitle = async ({ pattern = DEFAULT_PR_TITLE_REGEX }: CheckPrTitle) => { 25 | const regex = new RegExp(pattern); 26 | const { 27 | data: { title } 28 | } = await octokit.pulls.get({ 29 | pull_number: context.issue.number, 30 | ...context.repo 31 | }); 32 | if (regex.test(title)) { 33 | return true; 34 | } 35 | setFailed(`Pull request title does not meet requirements. The title must match the following regex: ${pattern}`); 36 | return false; 37 | }; 38 | -------------------------------------------------------------------------------- /src/helpers/close-pr.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { HelperInputs } from '../types/generated'; 15 | import { context } from '@actions/github'; 16 | import { octokit } from '../octokit'; 17 | import { createPrComment } from './create-pr-comment'; 18 | 19 | export class ClosePr extends HelperInputs { 20 | declare body?: string; 21 | declare pull_number?: string; 22 | declare repo_name?: string; 23 | declare repo_owner_name?: string; 24 | } 25 | 26 | export const closePr = async ({ body, pull_number, repo_name, repo_owner_name }: ClosePr = {}) => { 27 | if ((repo_name || repo_owner_name) && !pull_number) { 28 | throw new Error('pull_number is required when repo_name or repo_owner_name is provided'); 29 | } 30 | if (body) { 31 | await createPrComment({ body, pull_number, repo_name, repo_owner_name }); 32 | } 33 | 34 | return octokit.pulls.update({ 35 | pull_number: pull_number ? Number(pull_number) : context.issue.number, 36 | repo: repo_name ?? context.repo.repo, 37 | owner: repo_owner_name ?? context.repo.owner, 38 | state: 'closed' 39 | }); 40 | }; 41 | -------------------------------------------------------------------------------- /src/helpers/create-pr-comment.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { GITHUB_OPTIONS } from '../constants'; 15 | import { HelperInputs } from '../types/generated'; 16 | import { context } from '@actions/github'; 17 | import { octokit } from '../octokit'; 18 | 19 | export class CreatePrComment extends HelperInputs { 20 | body = ''; 21 | declare sha?: string; 22 | declare login?: string; 23 | declare pull_number?: string; 24 | declare repo_name?: string; 25 | declare repo_owner_name?: string; 26 | } 27 | 28 | const emptyResponse = { data: [] }; 29 | 30 | const getFirstPrByCommit = async (sha?: string, repo_name?: string, repo_owner_name?: string) => { 31 | const prs = 32 | (sha && 33 | (await octokit.repos.listPullRequestsAssociatedWithCommit({ 34 | commit_sha: sha, 35 | repo: repo_name ?? context.repo.repo, 36 | owner: repo_owner_name ?? context.repo.owner, 37 | ...GITHUB_OPTIONS 38 | }))) || 39 | emptyResponse; 40 | 41 | return prs.data.find(Boolean)?.number; 42 | }; 43 | 44 | const getCommentByUser = async (login?: string, pull_number?: string, repo_name?: string, repo_owner_name?: string) => { 45 | const comments = 46 | (login && 47 | (await octokit.issues.listComments({ 48 | issue_number: pull_number ? Number(pull_number) : context.issue.number, 49 | repo: repo_name ?? context.repo.repo, 50 | owner: repo_owner_name ?? context.repo.owner 51 | }))) || 52 | emptyResponse; 53 | 54 | return comments.data.find(comment => comment?.user?.login === login)?.id; 55 | }; 56 | 57 | export const createPrComment = async ({ body, sha, login, pull_number, repo_name, repo_owner_name }: CreatePrComment) => { 58 | const defaultPrNumber = context.issue.number; 59 | 60 | if (!sha && !login) { 61 | return octokit.issues.createComment({ 62 | body, 63 | issue_number: pull_number ? Number(pull_number) : defaultPrNumber, 64 | repo: repo_name ?? context.repo.repo, 65 | owner: repo_owner_name ?? context.repo.owner 66 | }); 67 | } 68 | 69 | const prNumber = (await getFirstPrByCommit(sha, repo_name, repo_owner_name)) ?? (pull_number ? Number(pull_number) : defaultPrNumber); 70 | const commentId = await getCommentByUser(login, pull_number, repo_name, repo_owner_name); 71 | 72 | if (commentId) { 73 | return octokit.issues.updateComment({ 74 | comment_id: commentId, 75 | body, 76 | repo: repo_name ?? context.repo.repo, 77 | owner: repo_owner_name ?? context.repo.owner 78 | }); 79 | } else { 80 | return octokit.issues.createComment({ 81 | body, 82 | issue_number: prNumber, 83 | repo: repo_name ?? context.repo.repo, 84 | owner: repo_owner_name ?? context.repo.owner 85 | }); 86 | } 87 | }; 88 | -------------------------------------------------------------------------------- /src/helpers/create-pr.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { HelperInputs } from '../types/generated'; 15 | import { context } from '@actions/github'; 16 | import { octokit } from '../octokit'; 17 | import { getDefaultBranch } from '../utils/get-default-branch'; 18 | import simpleGit from 'simple-git'; 19 | 20 | export class CreatePR extends HelperInputs { 21 | title = ''; 22 | body = ''; 23 | declare commit_message?: string; 24 | declare head?: string; 25 | declare base?: string; 26 | declare return_full_payload?: string; 27 | declare branch_name?: string; 28 | } 29 | 30 | export const createPr = async ({ title, body, head, base, return_full_payload, branch_name, commit_message }: CreatePR) => { 31 | const resolvedHead = await getOrCreateHeadBranch({ head, branch_name, commit_message }); 32 | 33 | const pr_base = base || (await getDefaultBranch()); 34 | await updateHeadWithBaseBranch(pr_base, resolvedHead); 35 | const { data } = await octokit.pulls.create({ 36 | title, 37 | head: resolvedHead, 38 | base: pr_base, 39 | body, 40 | maintainer_can_modify: true, 41 | ...context.repo 42 | }); 43 | return return_full_payload === 'true' ? data : data.number; 44 | }; 45 | 46 | const getOrCreateHeadBranch = async ({ head, branch_name, commit_message }: Partial): Promise => { 47 | if (branch_name && commit_message) { 48 | const git = simpleGit(); 49 | 50 | await git.addConfig('user.name', 'github-actions[bot]'); 51 | await git.addConfig('user.email', 'github-actions[bot]@users.noreply.github.com'); 52 | 53 | await git.checkoutLocalBranch(branch_name); 54 | await git.add('.'); 55 | await git.commit(commit_message); 56 | await git.push('origin', branch_name); 57 | 58 | return branch_name; 59 | } 60 | 61 | return head || context.ref.replace('refs/heads/', ''); 62 | }; 63 | 64 | const updateHeadWithBaseBranch = (base: string, head: string) => 65 | octokit.repos.merge({ 66 | base: head, 67 | head: base, 68 | ...context.repo 69 | }); 70 | -------------------------------------------------------------------------------- /src/helpers/create-project-card.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import * as core from '@actions/core'; 15 | import { SingleColumn, getDestinationColumn, getProjectColumns } from '../utils/get-project-columns'; 16 | import { GITHUB_OPTIONS } from '../constants'; 17 | import { HelperInputs } from '../types/generated'; 18 | import { context } from '@actions/github'; 19 | import { octokit } from '../octokit'; 20 | 21 | export class CreateProjectCardProps extends HelperInputs { 22 | project_name = ''; 23 | project_destination_column_name = ''; 24 | declare note?: string; 25 | } 26 | 27 | export const createProjectCard = async ({ project_name, project_destination_column_name, note }: CreateProjectCardProps) => { 28 | const columnsList = await getProjectColumns({ project_name }); 29 | 30 | if (!columnsList?.data?.length) { 31 | core.error(`There are no columns associated to ${project_name} project.`); 32 | return; 33 | } 34 | 35 | const destinationColumn = getDestinationColumn(columnsList, project_destination_column_name); 36 | 37 | if (!destinationColumn) { 38 | core.info('No destination column was found'); 39 | return; 40 | } 41 | const cardParams = await generateCardParams(destinationColumn, note); 42 | 43 | return octokit.projects.createCard(cardParams); 44 | }; 45 | 46 | const generateCardParams = async (filteredColumn: SingleColumn, note?: string) => { 47 | const getResponse = await octokit.pulls.get({ pull_number: context.issue.number, ...context.repo }); 48 | const pullRequest = getResponse.data; 49 | if (note) { 50 | return { 51 | column_id: filteredColumn?.id, 52 | note, 53 | ...context.repo, 54 | ...GITHUB_OPTIONS 55 | }; 56 | } 57 | 58 | return { 59 | column_id: filteredColumn.id, 60 | content_id: pullRequest.id, 61 | content_type: 'PullRequest', 62 | ...context.repo, 63 | ...GITHUB_OPTIONS 64 | }; 65 | }; 66 | -------------------------------------------------------------------------------- /src/helpers/delete-deployment.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import * as core from '@actions/core'; 15 | import { GITHUB_OPTIONS } from '../constants'; 16 | import { HelperInputs } from '../types/generated'; 17 | import { context } from '@actions/github'; 18 | import { octokit } from '../octokit'; 19 | import { map } from 'bluebird'; 20 | 21 | const DEFAULT_MAP_CONCURRENCY = 5; 22 | 23 | class DeleteDeploymentResponse { 24 | deploymentsDeleted = 0; 25 | deploymentsFound = 0; 26 | message = ''; 27 | environmentDeleted = false; 28 | 29 | constructor(init?: Partial) { 30 | Object.assign(this, init); 31 | } 32 | } 33 | 34 | export class DeleteDeployment extends HelperInputs { 35 | environment = ''; 36 | } 37 | 38 | const deactivateDeployments = async (deployments: number[]) => { 39 | const statusResponse = await map( 40 | deployments, 41 | async (deploymentId: number) => { 42 | return octokit.repos.createDeploymentStatus({ 43 | state: 'inactive', 44 | deployment_id: deploymentId, 45 | ...context.repo, 46 | ...GITHUB_OPTIONS 47 | }); 48 | }, 49 | { concurrency: DEFAULT_MAP_CONCURRENCY } 50 | ); 51 | 52 | const deletionMatch = statusResponse.filter(result => result.data.state === 'success').length === deployments.length; 53 | if (!deletionMatch) { 54 | core.info(`Not all deployments were successfully deactivated. Some may still be active.`); 55 | } 56 | }; 57 | 58 | const deleteDeployments = async (deployments: number[]) => { 59 | return await map( 60 | deployments, 61 | async (deploymentId: number) => { 62 | return octokit.repos.deleteDeployment({ 63 | deployment_id: deploymentId, 64 | ...context.repo, 65 | ...GITHUB_OPTIONS 66 | }); 67 | }, 68 | { concurrency: DEFAULT_MAP_CONCURRENCY } 69 | ); 70 | }; 71 | 72 | export const deleteDeployment = async ({ sha, environment }: DeleteDeployment): Promise => { 73 | const { data } = await octokit.repos.listDeployments({ 74 | sha, 75 | environment, 76 | ...context.repo, 77 | ...GITHUB_OPTIONS 78 | }); 79 | 80 | if (!data.length) { 81 | return new DeleteDeploymentResponse({ 82 | message: `No deployments found for environment ${environment}` 83 | }); 84 | } 85 | 86 | const deployments = data.map(deployment => deployment.id); 87 | 88 | await deactivateDeployments(deployments); 89 | 90 | const reqResults = await deleteDeployments(deployments); 91 | 92 | const envDelResult = await octokit.repos 93 | .deleteAnEnvironment({ 94 | environment_name: environment, 95 | ...context.repo, 96 | ...GITHUB_OPTIONS 97 | }) 98 | .catch(() => null); 99 | 100 | const deploymentsDeleted = reqResults.filter(result => result.status === 204).length; 101 | const environmentDeleted = envDelResult?.status === 204; 102 | 103 | return new DeleteDeploymentResponse({ 104 | deploymentsDeleted, 105 | deploymentsFound: data.length, 106 | environmentDeleted, 107 | message: `Deleted ${deploymentsDeleted} deployments for environment ${environment}` 108 | }); 109 | }; 110 | -------------------------------------------------------------------------------- /src/helpers/delete-stale-branches.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { HelperInputs } from '../types/generated'; 15 | import { context } from '@actions/github'; 16 | import * as core from '@actions/core'; 17 | import { octokit } from '../octokit'; 18 | import { map } from 'bluebird'; 19 | import { paginateAllOpenPullRequests } from '../utils/paginate-open-pull-requests'; 20 | import { getDefaultBranch } from '../utils/get-default-branch'; 21 | import { SECONDS_IN_A_DAY } from '../constants'; 22 | import { paginateAllBranches } from '../utils/paginate-all-branches'; 23 | 24 | export class DeleteStaleBranches extends HelperInputs { 25 | declare days?: string; 26 | } 27 | 28 | export const deleteStaleBranches = async ({ days = '30' }: DeleteStaleBranches = {}) => { 29 | const openPullRequests = await paginateAllOpenPullRequests(); 30 | const openPullRequestBranches = new Set(openPullRequests.map(pr => pr.head.ref)); 31 | const unprotectedBranches = await paginateAllBranches({ protectedBranches: false }); 32 | const defaultBranch = await getDefaultBranch(); 33 | const featureBranchesWithNoOpenPullRequest = unprotectedBranches.filter( 34 | ({ name }) => !openPullRequestBranches.has(name) && name !== defaultBranch 35 | ); 36 | const branchesWithUpdatedDates = await map( 37 | featureBranchesWithNoOpenPullRequest, 38 | async ({ name, commit: { sha } }) => { 39 | const { 40 | data: { 41 | committer: { date } 42 | } 43 | } = await octokit.git.getCommit({ 44 | commit_sha: sha, 45 | ...context.repo 46 | }); 47 | return { 48 | name, 49 | date 50 | }; 51 | }, 52 | { concurrency: 5 } 53 | ); 54 | 55 | const branchesToDelete = branchesWithUpdatedDates.filter(({ date }) => branchIsTooOld(date, days)).map(({ name }) => name); 56 | await map( 57 | branchesToDelete, 58 | async branch => { 59 | core.info(`Deleting branch ${branch}...`); 60 | await octokit.git.deleteRef({ 61 | ref: `heads/${branch}`, 62 | ...context.repo 63 | }); 64 | }, 65 | { concurrency: 5 } 66 | ); 67 | }; 68 | 69 | const branchIsTooOld = (dateLastUpdated: string, daysThreshold: string) => { 70 | const lastUpdated = new Date(dateLastUpdated); 71 | const now = Date.now(); 72 | const timeSinceLastUpdated = now - lastUpdated.getTime(); 73 | const threshold = Number(daysThreshold) * SECONDS_IN_A_DAY; 74 | 75 | return timeSinceLastUpdated > threshold; 76 | }; 77 | -------------------------------------------------------------------------------- /src/helpers/filter-paths.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import * as core from '@actions/core'; 15 | import { HelperInputs } from '../types/generated'; 16 | import { context } from '@actions/github'; 17 | import micromatch from 'micromatch'; 18 | import { octokit } from '../octokit'; 19 | import { getPrNumberFromMergeQueueRef } from '../utils/merge-queue'; 20 | import { ChangedFilesList } from '../types/github'; 21 | 22 | export class FilterPaths extends HelperInputs { 23 | declare paths?: string; 24 | declare globs?: string; 25 | declare sha?: string; 26 | declare packages?: string; 27 | declare merge_queue_enabled?: string; 28 | } 29 | 30 | export const filterPaths = async ({ paths, globs, sha, packages, merge_queue_enabled }: FilterPaths) => { 31 | if (!paths && !globs && !packages) { 32 | core.error('Must pass `globs` or `paths` or `packages` for filtering'); 33 | return false; 34 | } 35 | 36 | let pull_number: number; 37 | if (context.eventName === 'merge_group') { 38 | pull_number = getPrNumberFromMergeQueueRef(); 39 | } else if (sha && merge_queue_enabled === 'true') { 40 | const branchesResult = sha 41 | ? await octokit.repos.listBranchesForHeadCommit({ 42 | commit_sha: sha, 43 | ...context.repo 44 | }) 45 | : undefined; 46 | const branchName = branchesResult?.data[0]?.name; 47 | pull_number = getPrNumberFromMergeQueueRef(branchName); 48 | } else if (sha) { 49 | const listPrsResult = await octokit.repos.listPullRequestsAssociatedWithCommit({ 50 | commit_sha: sha, 51 | ...context.repo 52 | }); 53 | const prFromSha = listPrsResult?.data.find(Boolean); 54 | if (!prFromSha) throw new Error(`No PR found for commit ${sha}`); 55 | pull_number = prFromSha.number; 56 | } else { 57 | pull_number = context.issue.number; 58 | } 59 | 60 | const { data } = await octokit.pulls.listFiles({ 61 | per_page: 100, 62 | pull_number, 63 | ...context.repo 64 | }); 65 | 66 | if (packages && hasRelevantPackageChanged(data, packages)) { 67 | return true; 68 | } 69 | 70 | const fileNames = data.map(file => file.filename); 71 | if (globs) { 72 | if (paths) core.info('`paths` and `globs` inputs found, defaulting to use `globs` for filtering'); 73 | return micromatch(fileNames, globs.split('\n')).length > 0; 74 | } else if (paths) { 75 | const filePaths = paths.split('\n'); 76 | return fileNames.some(changedFile => filePaths.some(filePath => changedFile.startsWith(filePath))); 77 | } 78 | }; 79 | 80 | const hasRelevantPackageChanged = (files: ChangedFilesList, packages: string) => { 81 | const packageJson = files.find(file => file.filename === 'package.json'); 82 | if (!packageJson) { 83 | return false; 84 | } 85 | 86 | return packages.split('\n').some(pkg => new RegExp(`(-|\\+)\\s*\\"${pkg}\\"`).test(packageJson.patch ?? '')); 87 | }; 88 | -------------------------------------------------------------------------------- /src/helpers/generate-matrix.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { HelperInputs } from '../types/generated'; 15 | import { chunk, sum } from 'lodash'; 16 | 17 | export class GenerateMatrix extends HelperInputs { 18 | paths = ''; 19 | declare batches?: string; 20 | declare load_balancing_sizes?: string; 21 | declare use_basic_matrix_configuration?: string; 22 | } 23 | 24 | export const generateMatrix = ({ 25 | paths, 26 | batches: _batches = '1', 27 | load_balancing_sizes, 28 | use_basic_matrix_configuration = '' 29 | }: GenerateMatrix) => { 30 | const matrixValues = paths.split(/[\n,]/); 31 | const batches = Number(_batches); 32 | let result; 33 | if (!load_balancing_sizes || matrixValues.length <= batches) { 34 | const chunkedList = chunk(matrixValues, Math.ceil(matrixValues.length / batches)).map(chunk => chunk.join(',')); 35 | if (use_basic_matrix_configuration === 'true') result = { path: chunkedList }; 36 | else result = { include: chunkedList.map(chunk => ({ path: chunk })) }; 37 | } else { 38 | const loadBalancingSizes = load_balancing_sizes.split(/[\n,]/).map(size => Number(size)); 39 | if (loadBalancingSizes.length !== matrixValues.length) 40 | throw new Error('load_balancing_sizes input must have the same length as paths input'); 41 | const targetLoadSize = sum(loadBalancingSizes) / batches; 42 | const loadBalancedPaths: string[] = []; 43 | let currentLoadSize = 0; 44 | let currentBatch: string[] = []; 45 | matrixValues.forEach((path, index) => { 46 | if (Number.isNaN(loadBalancingSizes[index])) throw new Error('load_balancing_sizes input must contain values'); 47 | // we've already validated that a value exists at this index above, but TS really _needs_ to see us validate it againgit 48 | const loadAtIndex = (loadBalancingSizes[index] !== undefined ? loadBalancingSizes[index] : 0) as number; 49 | const possibleLoadSize = currentLoadSize + loadAtIndex; 50 | if (Math.abs(possibleLoadSize - targetLoadSize) <= Math.abs(loadAtIndex - targetLoadSize)) { 51 | currentLoadSize += loadAtIndex; 52 | currentBatch.push(path); 53 | } else { 54 | loadBalancedPaths.push(currentBatch.join(',')); 55 | currentBatch = [path]; 56 | currentLoadSize = loadAtIndex; 57 | } 58 | if (currentLoadSize >= targetLoadSize) { 59 | loadBalancedPaths.push(currentBatch.join(',')); 60 | currentBatch = []; 61 | currentLoadSize = 0; 62 | } 63 | }); 64 | if (currentBatch.length > 0) loadBalancedPaths.push(currentBatch.join(',')); 65 | if (use_basic_matrix_configuration === 'true') result = { path: loadBalancedPaths }; 66 | else result = { include: loadBalancedPaths.map(path => ({ path })) }; 67 | } 68 | return result; 69 | }; 70 | -------------------------------------------------------------------------------- /src/helpers/generate-path-matrix.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import * as core from '@actions/core'; 15 | import { HelperInputs } from '../types/generated'; 16 | import { chunk, uniq } from 'lodash'; 17 | import { context } from '@actions/github'; 18 | import { getChangedFilepaths } from '../utils/get-changed-filepaths'; 19 | import micromatch from 'micromatch'; 20 | 21 | export class GeneratePathMatrix extends HelperInputs { 22 | declare paths?: string; 23 | declare globs?: string; 24 | declare override_filter_paths?: string; 25 | declare override_filter_globs?: string; 26 | declare paths_no_filter?: string; 27 | declare batches?: string; 28 | } 29 | 30 | export const generatePathMatrix = async ({ 31 | paths, 32 | globs, 33 | /** paths that override the changed files filter, causing the action to return all paths */ 34 | override_filter_paths, 35 | override_filter_globs, 36 | /** paths that will be returned regardless of their adherence to the filter */ 37 | paths_no_filter, 38 | /** number of evenly-sized batches to separate matching paths into (returns comma-separated result) */ 39 | batches 40 | }: GeneratePathMatrix) => { 41 | const pathsToUse = paths || globs; 42 | if (!pathsToUse) { 43 | core.error('Must supply one of paths, globs'); 44 | throw new Error(); 45 | } 46 | const changedFiles = await getChangedFilepaths(context.issue.number); 47 | const shouldOverrideFilter = override_filter_globs 48 | ? micromatch(changedFiles, override_filter_globs.split('\n')).length > 0 49 | : changedFiles.some(changedFile => override_filter_paths?.split(/[\n,]/).includes(changedFile)); 50 | const splitPaths = pathsToUse.split(/[\n,]/); 51 | const basePaths = shouldOverrideFilter 52 | ? splitPaths 53 | : paths 54 | ? splitPaths.filter(path => changedFiles.some(changedFile => changedFile.startsWith(path))) 55 | : splitPaths.filter(glob => micromatch(changedFiles, glob).length > 0); 56 | const extraPaths: string[] = paths_no_filter?.split(/[\n,]/) ?? []; 57 | const matrixValues = uniq(basePaths.concat(extraPaths)); 58 | if (batches) { 59 | return { 60 | include: chunk(matrixValues, Math.ceil(matrixValues.length / Number(batches))).map(chunk => ({ path: chunk.join(',') })) 61 | }; 62 | } 63 | 64 | return { 65 | include: matrixValues.map(path => ({ path })) 66 | }; 67 | }; 68 | -------------------------------------------------------------------------------- /src/helpers/get-changed-files.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { HelperInputs } from '../types/generated'; 15 | import { context } from '@actions/github'; 16 | import { getChangedFilepaths } from '../utils/get-changed-filepaths'; 17 | import { getPrNumberFromMergeQueueRef } from '../utils/merge-queue'; 18 | 19 | export class GetChangedFiles extends HelperInputs { 20 | declare pattern?: string; 21 | declare delimiter?: string; 22 | declare ignore_deleted?: string; 23 | } 24 | 25 | export const getChangedFiles = async ({ pattern, delimiter = ',', ignore_deleted }: GetChangedFiles) => { 26 | const pullNumber = context.eventName === 'merge_group' ? getPrNumberFromMergeQueueRef() : context.issue.number; 27 | const filePaths = await getChangedFilepaths(pullNumber, Boolean(ignore_deleted)); 28 | const filteredFilePaths = pattern ? filePaths.filter(fileName => fileName.match(pattern)) : filePaths; 29 | return filteredFilePaths.join(delimiter); 30 | }; 31 | -------------------------------------------------------------------------------- /src/helpers/get-email-on-user-profile.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { HelperInputs } from '../types/generated'; 15 | import { octokit } from '../octokit'; 16 | import { setFailed } from '@actions/core'; 17 | 18 | export class GetEmailOnUserProfile extends HelperInputs { 19 | login = ''; 20 | declare pattern?: string; 21 | } 22 | 23 | export const getEmailOnUserProfile = async ({ login, pattern }: GetEmailOnUserProfile) => { 24 | const { 25 | data: { email } 26 | } = await octokit.users.getByUsername({ username: login }); 27 | 28 | if (!email) { 29 | setFailed(`User ${login} does not have an email address on their GitHub profile!`); 30 | return; 31 | } 32 | 33 | if (pattern && !new RegExp(pattern).test(email)) { 34 | setFailed( 35 | `Email ${email} does not match regex pattern ${pattern}. Please update the email on your GitHub profile to match this pattern!` 36 | ); 37 | return; 38 | } 39 | 40 | return email; 41 | }; 42 | -------------------------------------------------------------------------------- /src/helpers/get-merge-queue-position.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { HelperInputs } from '../types/generated'; 15 | import { context } from '@actions/github'; 16 | import { octokitGraphql } from '../octokit'; 17 | import { Repository } from '@octokit/graphql-schema'; 18 | import { getPrNumberFromMergeQueueRef } from '../utils/merge-queue'; 19 | 20 | export class GetMergeQueuePosition extends HelperInputs { 21 | declare max_queue_size?: string; 22 | } 23 | 24 | export const getMergeQueuePosition = async ({ max_queue_size = '10' }: GetMergeQueuePosition) => { 25 | const { repository } = await octokitGraphql<{ repository: Repository }>(` 26 | query { 27 | repository(owner: "${context.repo.owner}", name: "${context.repo.repo}") { 28 | mergeQueue { 29 | entries(first: ${max_queue_size}) { 30 | nodes { 31 | pullRequest { 32 | number 33 | } 34 | position 35 | } 36 | } 37 | } 38 | } 39 | } 40 | `); 41 | const prNumberFromMergeQueueRef = getPrNumberFromMergeQueueRef(); 42 | const mergeQueueEntries = repository.mergeQueue?.entries?.nodes; 43 | return mergeQueueEntries?.find(entry => entry?.pullRequest?.number === prNumberFromMergeQueueRef)?.position; 44 | }; 45 | -------------------------------------------------------------------------------- /src/helpers/initiate-deployment.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { DeploymentState } from '../types/github'; 15 | import { DEFAULT_PIPELINE_STATUS, GITHUB_OPTIONS } from '../constants'; 16 | import { HelperInputs } from '../types/generated'; 17 | import { context as githubContext } from '@actions/github'; 18 | import { octokit } from '../octokit'; 19 | import { getMergeQueueCommitHashes } from '../utils/merge-queue'; 20 | import { map } from 'bluebird'; 21 | 22 | export class InitiateDeployment extends HelperInputs { 23 | sha = ''; 24 | environment = ''; 25 | declare state?: DeploymentState; 26 | declare environment_url?: string; 27 | declare description?: string; 28 | declare target_url?: string; 29 | declare context?: string; 30 | declare merge_queue_enabled?: string; 31 | } 32 | 33 | export const initiateDeployment = async ({ 34 | sha, 35 | state = 'in_progress', 36 | environment, 37 | environment_url, 38 | description, 39 | target_url, 40 | context = DEFAULT_PIPELINE_STATUS, 41 | merge_queue_enabled 42 | }: InitiateDeployment) => { 43 | const { data } = await octokit.repos.createDeployment({ 44 | ref: sha, 45 | environment, 46 | auto_merge: false, 47 | required_contexts: [], 48 | ...githubContext.repo, 49 | ...GITHUB_OPTIONS 50 | }); 51 | const deployment_id = 'ref' in data ? data.id : undefined; 52 | if (!deployment_id) return; 53 | 54 | await octokit.repos.createDeploymentStatus({ 55 | state, 56 | deployment_id, 57 | description, 58 | environment_url, 59 | target_url, 60 | ...githubContext.repo, 61 | ...GITHUB_OPTIONS 62 | }); 63 | 64 | if (merge_queue_enabled === 'true') { 65 | const mergeQueueCommitHashes = await getMergeQueueCommitHashes(); 66 | return map(mergeQueueCommitHashes, async sha => 67 | octokit.repos.createCommitStatus({ 68 | sha, 69 | context, 70 | state: 'pending', 71 | description, 72 | target_url, 73 | ...githubContext.repo 74 | }) 75 | ); 76 | } 77 | }; 78 | -------------------------------------------------------------------------------- /src/helpers/is-user-core-member.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { HelperInputs } from '../types/generated'; 15 | import { context } from '@actions/github'; 16 | import * as core from '@actions/core'; 17 | import { getCoreMemberLogins } from '../utils/get-core-member-logins'; 18 | 19 | export class IsUserCoreMember extends HelperInputs {} 20 | 21 | export const isUserCoreMember = async ({ pull_number, login = context.actor }: IsUserCoreMember) => { 22 | const pullNumber = Number(pull_number); 23 | const coreMembers = await getCoreMemberLogins(pullNumber); 24 | core.info(`Checking if ${login} is a core member for pull request ${pullNumber}`); 25 | core.info(`Core members: ${coreMembers.join(', ')}`); 26 | return coreMembers.includes(login); 27 | }; 28 | -------------------------------------------------------------------------------- /src/helpers/is-user-in-team.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { HelperInputs } from '../types/generated'; 15 | import { context } from '@actions/github'; 16 | import { octokit } from '../octokit'; 17 | import * as core from '@actions/core'; 18 | import { MembersInOrg } from '../types/github'; 19 | 20 | export class IsUserInTeam extends HelperInputs { 21 | team = ''; 22 | declare login?: string; 23 | } 24 | 25 | export const isUserInTeam = async ({ login = context.actor, team }: IsUserInTeam) => { 26 | const members = await paginateAllMembersInOrg(team); 27 | core.info(`Checking if ${login} is in team ${team}`); 28 | core.info(`Team members: ${members.map(({ login }) => login).join(', ')}`); 29 | return members.some(({ login: memberLogin }) => memberLogin === login); 30 | }; 31 | 32 | async function paginateAllMembersInOrg(team: string, page = 1): Promise { 33 | const response = await octokit.teams.listMembersInOrg({ 34 | org: context.repo.owner, 35 | team_slug: team, 36 | page, 37 | per_page: 100 38 | }); 39 | if (!response.data.length) { 40 | return []; 41 | } 42 | return response.data.concat(await paginateAllMembersInOrg(team, page + 1)); 43 | } 44 | -------------------------------------------------------------------------------- /src/helpers/manage-issue-due-dates.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { OVERDUE_ISSUE, ALMOST_OVERDUE_ISSUE, PRIORITY_LABELS, PRIORITY_TO_DAYS_MAP, SECONDS_IN_A_DAY } from '../constants'; 15 | import { HelperInputs } from '../types/generated'; 16 | import { paginateAllPrioritizedIssues } from '../utils/paginate-prioritized-issues'; 17 | import { addDueDateComment, pingAssigneesForDueDate } from '../utils/add-due-date-comment'; 18 | import { IssueList, IssueLabels } from '../types/github'; 19 | import { map } from 'bluebird'; 20 | import { octokit } from '../octokit'; 21 | import { context } from '@actions/github'; 22 | import { removeLabelIfExists } from './remove-label'; 23 | 24 | export class ManageIssueDueDates extends HelperInputs { 25 | declare days?: string; 26 | } 27 | 28 | export const manageIssueDueDates = async ({ days = '7' }: ManageIssueDueDates) => { 29 | const openIssues: IssueList = await paginateAllPrioritizedIssues(); 30 | 31 | const warningThreshold = Number(days); 32 | 33 | const getFirstPriorityLabelFoundOnIssue = (issueLabels: IssueLabels) => 34 | PRIORITY_LABELS.find(priorityLabel => 35 | issueLabels.some(issueLabel => { 36 | const labelName = typeof issueLabel === 'string' ? issueLabel : issueLabel.name; 37 | return labelName === priorityLabel; 38 | }) 39 | ); 40 | 41 | await map(openIssues, async issue => { 42 | const { labels, created_at, assignees, number: issue_number } = issue; 43 | const priority = getFirstPriorityLabelFoundOnIssue(labels); 44 | const alreadyHasOverdueLabel = Boolean( 45 | labels.find(label => { 46 | const overdueLabels = [OVERDUE_ISSUE]; 47 | const labelName: string = typeof label === 'string' ? label : label.name || ''; 48 | return overdueLabels.includes(labelName); 49 | }) 50 | ); 51 | 52 | if (!priority || alreadyHasOverdueLabel) { 53 | return; 54 | } 55 | const createdDate = new Date(created_at); 56 | const daysSinceCreation = Math.ceil((Date.now() - createdDate.getTime()) / SECONDS_IN_A_DAY); 57 | const deadline = PRIORITY_TO_DAYS_MAP[priority]; 58 | await addDueDateComment(deadline, createdDate, issue_number); 59 | const labelToAdd = 60 | daysSinceCreation > deadline ? OVERDUE_ISSUE : daysSinceCreation > deadline - warningThreshold ? ALMOST_OVERDUE_ISSUE : undefined; 61 | if (labelToAdd) { 62 | if (assignees) { 63 | await pingAssigneesForDueDate(assignees, labelToAdd, issue_number); 64 | } 65 | if (labelToAdd === OVERDUE_ISSUE) { 66 | await removeLabelIfExists(ALMOST_OVERDUE_ISSUE, issue_number); 67 | } 68 | await octokit.issues.addLabels({ 69 | labels: [labelToAdd], 70 | issue_number, 71 | ...context.repo 72 | }); 73 | } 74 | }); 75 | }; 76 | -------------------------------------------------------------------------------- /src/helpers/move-project-card.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import * as core from '@actions/core'; 15 | import { getDestinationColumn, getProjectColumns } from '../utils/get-project-columns'; 16 | import { ColumnListResponse } from '../types/github'; 17 | import { GITHUB_OPTIONS } from '../constants'; 18 | import { HelperInputs } from '../types/generated'; 19 | import { context } from '@actions/github'; 20 | import { octokit } from '../octokit'; 21 | 22 | export class MoveProjectCardProps extends HelperInputs { 23 | project_destination_column_name = ''; 24 | project_name = ''; 25 | project_origin_column_name = ''; 26 | } 27 | 28 | export const moveProjectCard = async ({ 29 | project_destination_column_name, 30 | project_origin_column_name, 31 | project_name 32 | }: MoveProjectCardProps) => { 33 | const columnsList = await getProjectColumns({ project_name }); 34 | 35 | if (!columnsList?.data?.length) { 36 | core.error(`There are no columns associated to ${project_name} project.`); 37 | return; 38 | } 39 | 40 | const destinationColumn = getDestinationColumn(columnsList, project_destination_column_name); 41 | const originColumn = getOriginColumn(columnsList, project_origin_column_name); 42 | 43 | if (!originColumn) { 44 | core.info(`No origin column was found for the name ${project_origin_column_name}`); 45 | return; 46 | } 47 | 48 | const cardToMove = await getCardToMove(originColumn); 49 | 50 | if (cardToMove && destinationColumn) { 51 | return octokit.projects.moveCard({ card_id: cardToMove.id, column_id: destinationColumn.id, position: 'top', ...GITHUB_OPTIONS }); 52 | } else { 53 | core.info('No destination column or card to move was found'); 54 | return; 55 | } 56 | }; 57 | 58 | const getCardToMove = async (originColumn: OriginColumn) => { 59 | const { 60 | data: { issue_url } 61 | } = await octokit.pulls.get({ pull_number: context.issue.number, ...context.repo }); 62 | const cardsResponse = await octokit.projects.listCards({ column_id: originColumn.id, ...GITHUB_OPTIONS }); 63 | 64 | return cardsResponse.data.find(card => card.content_url === issue_url); 65 | }; 66 | 67 | interface OriginColumn { 68 | id: number; 69 | } 70 | 71 | const getOriginColumn = (columns: ColumnListResponse, project_origin_column_name: string) => 72 | columns.data.find(column => column.name === project_origin_column_name); 73 | -------------------------------------------------------------------------------- /src/helpers/notify-pipeline-complete.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { DEFAULT_PIPELINE_DESCRIPTION, DEFAULT_PIPELINE_STATUS, GITHUB_OPTIONS, PRODUCTION_ENVIRONMENT } from '../constants'; 15 | import { HelperInputs } from '../types/generated'; 16 | import { context as githubContext } from '@actions/github'; 17 | import { map } from 'bluebird'; 18 | import { octokit } from '../octokit'; 19 | import { getMergeQueueCommitHashes } from '../utils/merge-queue'; 20 | 21 | export class NotifyPipelineComplete extends HelperInputs { 22 | declare context?: string; 23 | declare description?: string; 24 | declare environment?: string; 25 | declare target_url?: string; 26 | declare merge_queue_enabled?: string; 27 | } 28 | 29 | export const notifyPipelineComplete = async ({ 30 | context = DEFAULT_PIPELINE_STATUS, 31 | description = DEFAULT_PIPELINE_DESCRIPTION, 32 | environment = PRODUCTION_ENVIRONMENT, 33 | target_url, 34 | merge_queue_enabled 35 | }: NotifyPipelineComplete) => { 36 | const { data: deployments } = await octokit.repos.listDeployments({ 37 | environment, 38 | ...githubContext.repo, 39 | ...GITHUB_OPTIONS 40 | }); 41 | const deployment_id = deployments.find(Boolean)?.id; 42 | if (!deployment_id) return; 43 | await octokit.repos.createDeploymentStatus({ 44 | environment, 45 | deployment_id, 46 | state: 'success', 47 | description, 48 | target_url, 49 | ...githubContext.repo, 50 | ...GITHUB_OPTIONS 51 | }); 52 | 53 | if (merge_queue_enabled === 'true') { 54 | const mergeQueueCommitHashes = await getMergeQueueCommitHashes(); 55 | return map(mergeQueueCommitHashes, async sha => 56 | octokit.repos.createCommitStatus({ 57 | sha, 58 | context, 59 | state: 'success', 60 | description, 61 | target_url, 62 | ...githubContext.repo 63 | }) 64 | ); 65 | } 66 | 67 | const { data: pullRequests } = await octokit.pulls.list({ 68 | state: 'open', 69 | per_page: 100, 70 | ...githubContext.repo 71 | }); 72 | const commitHashesForOpenPullRequests = pullRequests.map(pullRequest => pullRequest.head.sha); 73 | return map(commitHashesForOpenPullRequests, async sha => 74 | octokit.repos.createCommitStatus({ 75 | sha, 76 | context, 77 | state: 'success', 78 | description, 79 | target_url, 80 | ...githubContext.repo 81 | }) 82 | ); 83 | }; 84 | -------------------------------------------------------------------------------- /src/helpers/prepare-queued-pr-for-merge.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import * as core from '@actions/core'; 15 | import { FIRST_QUEUED_PR_LABEL, JUMP_THE_QUEUE_PR_LABEL, READY_FOR_MERGE_PR_LABEL } from '../constants'; 16 | import { GithubError, PullRequest, PullRequestList, SinglePullRequest } from '../types/github'; 17 | import { context } from '@actions/github'; 18 | import { octokit } from '../octokit'; 19 | import { removePrFromQueue } from './manage-merge-queue'; 20 | 21 | export const prepareQueuedPrForMerge = async () => { 22 | const { data } = await octokit.pulls.list({ 23 | state: 'open', 24 | per_page: 100, 25 | ...context.repo 26 | }); 27 | const pullRequest = findNextPrToMerge(data); 28 | if (pullRequest) { 29 | return updatePrWithDefaultBranch(pullRequest as PullRequest); 30 | } 31 | }; 32 | 33 | const findNextPrToMerge = (pullRequests: PullRequestList) => 34 | pullRequests.find(pr => hasRequiredLabels(pr, [READY_FOR_MERGE_PR_LABEL, JUMP_THE_QUEUE_PR_LABEL])) ?? 35 | pullRequests.find(pr => hasRequiredLabels(pr, [READY_FOR_MERGE_PR_LABEL, FIRST_QUEUED_PR_LABEL])); 36 | 37 | const hasRequiredLabels = (pr: SinglePullRequest, requiredLabels: string[]) => 38 | requiredLabels.every(mergeQueueLabel => pr.labels.some(label => label.name === mergeQueueLabel)); 39 | 40 | export const updatePrWithDefaultBranch = async (pullRequest: PullRequest) => { 41 | if (pullRequest.head.user?.login && pullRequest.base.user?.login && pullRequest.head.user?.login !== pullRequest.base.user?.login) { 42 | try { 43 | // update fork default branch with upstream 44 | await octokit.repos.mergeUpstream({ 45 | ...context.repo, 46 | branch: pullRequest.base.repo.default_branch 47 | }); 48 | } catch (error) { 49 | if ((error as GithubError).status === 409) { 50 | core.setFailed('Attempt to update fork branch with upstream failed; conflict on default branch between fork and upstream.'); 51 | } else core.setFailed((error as GithubError).message); 52 | } 53 | } 54 | try { 55 | await octokit.repos.merge({ 56 | base: pullRequest.head.ref, 57 | head: 'HEAD', 58 | ...context.repo 59 | }); 60 | } catch (error) { 61 | const noEvictUponConflict = core.getInput('no_evict_upon_conflict'); 62 | const githubError = error as GithubError; 63 | if (githubError.status !== 409) { 64 | core.setFailed(githubError.message); 65 | return; 66 | } 67 | if (noEvictUponConflict === 'true') { 68 | core.info('The first PR in the queue has a merge conflict. PR was not removed from the queue due to no_evict_upon_conflict input.'); 69 | return; 70 | } 71 | 72 | await removePrFromQueue(pullRequest); 73 | core.setFailed('The first PR in the queue has a merge conflict, and it was removed from the queue.'); 74 | } 75 | }; 76 | -------------------------------------------------------------------------------- /src/helpers/remove-label.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import * as core from '@actions/core'; 15 | import { GithubError } from '../types/github'; 16 | import { HelperInputs } from '../types/generated'; 17 | import { context } from '@actions/github'; 18 | import { octokit } from '../octokit'; 19 | 20 | export class RemoveLabel extends HelperInputs { 21 | label = ''; 22 | } 23 | 24 | export const removeLabel = async ({ label }: RemoveLabel) => removeLabelIfExists(label, context.issue.number); 25 | 26 | export const removeLabelIfExists = async (labelName: string, issue_number: number) => { 27 | try { 28 | await octokit.issues.removeLabel({ 29 | name: labelName, 30 | issue_number, 31 | ...context.repo 32 | }); 33 | } catch (error) { 34 | if ((error as GithubError).status === 404) { 35 | core.info('Label is not present on PR.'); 36 | } 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /src/helpers/remove-pr-from-merge-queue.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import * as core from '@actions/core'; 15 | import { orderBy, groupBy } from 'lodash'; 16 | import { FIRST_QUEUED_PR_LABEL, QUEUED_FOR_MERGE_PREFIX, READY_FOR_MERGE_PR_LABEL } from '../constants'; 17 | import { HelperInputs } from '../types/generated'; 18 | import { context } from '@actions/github'; 19 | import { octokit } from '../octokit'; 20 | import { removeLabelIfExists } from './remove-label'; 21 | import { map } from 'bluebird'; 22 | 23 | export class RemovePrFromMergeQueue extends HelperInputs { 24 | seconds = ''; 25 | } 26 | 27 | export const removePrFromMergeQueue = async ({ seconds }: RemovePrFromMergeQueue) => { 28 | const { data: pullRequests } = await octokit.pulls.list({ 29 | state: 'open', 30 | per_page: 100, 31 | ...context.repo 32 | }); 33 | const firstQueuedPr = pullRequests.find(pr => pr.labels.some(label => label.name === FIRST_QUEUED_PR_LABEL)); 34 | if (!firstQueuedPr) { 35 | core.info('No PR is first in the merge queue.'); 36 | 37 | return map(pullRequests, async pr => { 38 | const readyForMergeLabel = pr.labels.find(label => label.name.startsWith(READY_FOR_MERGE_PR_LABEL)); 39 | const queueLabel = pr.labels.find(label => label.name.startsWith(QUEUED_FOR_MERGE_PREFIX)); 40 | if (readyForMergeLabel || queueLabel) { 41 | core.info(`Cleaning up queued PR #${pr.number}...`); 42 | await removeLabelIfExists(READY_FOR_MERGE_PR_LABEL, pr.number); 43 | if (queueLabel) { 44 | await removeLabelIfExists(queueLabel.name, pr.number); 45 | } 46 | } 47 | }); 48 | } 49 | 50 | const { 51 | number, 52 | head: { sha } 53 | } = firstQueuedPr; 54 | const { data } = await octokit.repos.listCommitStatusesForRef({ 55 | ref: sha, 56 | ...context.repo 57 | }); 58 | const statusesPerContext = groupBy(data, 'context'); 59 | const someContextHasLatestStatusPending = Object.keys(statusesPerContext).some(context => { 60 | const mostRecentStatus = orderBy(statusesPerContext[context], 'created_at', 'desc')[0]; 61 | return mostRecentStatus?.state === 'pending'; 62 | }); 63 | if (someContextHasLatestStatusPending) { 64 | return; 65 | } 66 | const mostRecentStatus = orderBy(data, 'created_at', 'desc')[0]; 67 | if (mostRecentStatus && timestampIsStale(mostRecentStatus.created_at, seconds)) { 68 | core.info('Removing stale PR from first queued position...'); 69 | return Promise.all([removeLabelIfExists(READY_FOR_MERGE_PR_LABEL, number), removeLabelIfExists(FIRST_QUEUED_PR_LABEL, number)]); 70 | } 71 | }; 72 | 73 | const timestampIsStale = (timestamp: string, seconds: string) => { 74 | const ageOfTimestampInMiliseconds = Date.now() - new Date(timestamp).getTime(); 75 | const milisecondsConsideredStale = Number(seconds) * 1000; 76 | return ageOfTimestampInMiliseconds > milisecondsConsideredStale; 77 | }; 78 | -------------------------------------------------------------------------------- /src/helpers/reopen-pr.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { HelperInputs } from '../types/generated'; 15 | import { context } from '@actions/github'; 16 | import { octokit } from '../octokit'; 17 | 18 | export class ReopenPr extends HelperInputs { 19 | declare pull_number?: string; 20 | declare repo_name?: string; 21 | declare repo_owner_name?: string; 22 | } 23 | 24 | export const reopenPr = async ({ pull_number, repo_name, repo_owner_name }: ReopenPr = {}) => { 25 | if ((repo_name || repo_owner_name) && !pull_number) { 26 | throw new Error('pull_number is required when repo_name or repo_owner_name is provided'); 27 | } 28 | 29 | return octokit.pulls.update({ 30 | pull_number: pull_number ? Number(pull_number) : context.issue.number, 31 | repo: repo_name ?? context.repo.repo, 32 | owner: repo_owner_name ?? context.repo.owner, 33 | state: 'open' 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /src/helpers/rerun-pr-checks.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import * as core from '@actions/core'; 15 | import { context } from '@actions/github'; 16 | import { map } from 'bluebird'; 17 | import { octokit } from '../octokit'; 18 | 19 | export const rerunPrChecks = async () => { 20 | /** grab owner in case of fork branch */ 21 | const { 22 | data: { 23 | head: { 24 | user: { login: owner }, 25 | sha: latestHash, 26 | ref: branch 27 | } 28 | } 29 | } = await octokit.pulls.get({ 30 | pull_number: context.issue.number, 31 | ...context.repo 32 | }); 33 | const workflowRunResponses = await map(['pull_request', 'pull_request_target'], event => 34 | octokit.actions.listWorkflowRunsForRepo({ 35 | branch, 36 | ...context.repo, 37 | owner, 38 | event, 39 | per_page: 100, 40 | status: 'completed' 41 | }) 42 | ); 43 | const workflowRuns = workflowRunResponses.map(response => response.data.workflow_runs).flat(); 44 | if (!workflowRuns.length) { 45 | core.info(`No workflow runs found on branch ${branch} on ${owner}/${context.repo.repo}`); 46 | return; 47 | } 48 | const latestWorkflowRuns = workflowRuns.filter(({ head_sha }) => head_sha === latestHash); 49 | core.info(`There are ${latestWorkflowRuns.length} checks associated with the latest commit, triggering reruns...`); 50 | 51 | return map(latestWorkflowRuns, async ({ id, name }) => { 52 | core.info(`- Rerunning ${name} (${id})`); 53 | await octokit.actions.reRunWorkflow({ run_id: id, ...context.repo }); 54 | }); 55 | }; 56 | -------------------------------------------------------------------------------- /src/helpers/set-commit-status.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import * as core from '@actions/core'; 15 | import { PipelineState } from '../types/github'; 16 | import { HelperInputs } from '../types/generated'; 17 | import { context as githubContext } from '@actions/github'; 18 | import { map } from 'bluebird'; 19 | import { octokit } from '../octokit'; 20 | 21 | export class SetCommitStatus extends HelperInputs { 22 | sha = ''; 23 | context = ''; 24 | state = ''; 25 | declare description?: string; 26 | declare target_url?: string; 27 | declare skip_if_already_set?: string; 28 | } 29 | 30 | export const setCommitStatus = async ({ sha, context, state, description, target_url, skip_if_already_set }: SetCommitStatus) => { 31 | await map(context.split('\n').filter(Boolean), async context => { 32 | if (skip_if_already_set === 'true') { 33 | const check_runs = await octokit.checks.listForRef({ 34 | ...githubContext.repo, 35 | ref: sha 36 | }); 37 | const run = check_runs.data.check_runs.find(({ name }) => name === context); 38 | const runCompletedAndIsValid = run?.status === 'completed' && (run?.conclusion === 'failure' || run?.conclusion === 'success'); 39 | if (runCompletedAndIsValid) { 40 | core.info(`${context} already completed with a ${run.conclusion} conclusion.`); 41 | return; 42 | } 43 | } 44 | 45 | octokit.repos.createCommitStatus({ 46 | sha, 47 | context, 48 | state: state as PipelineState, 49 | description, 50 | target_url, 51 | ...githubContext.repo 52 | }); 53 | }); 54 | }; 55 | -------------------------------------------------------------------------------- /src/helpers/set-deployment-status.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { DeploymentState } from '../types/github'; 15 | import { HelperInputs } from '../types/generated'; 16 | import { GITHUB_OPTIONS } from '../constants'; 17 | import { context } from '@actions/github'; 18 | import { octokit } from '../octokit'; 19 | 20 | export class SetDeploymentStatus extends HelperInputs { 21 | state = ''; 22 | environment = ''; 23 | declare sha?: string; 24 | declare description?: string; 25 | declare target_url?: string; 26 | declare environment_url?: string; 27 | } 28 | 29 | export const setDeploymentStatus = async ({ sha, state, environment, description, target_url, environment_url }: SetDeploymentStatus) => { 30 | const { data } = await octokit.repos.listDeployments({ 31 | sha, 32 | environment, 33 | ...context.repo, 34 | ...GITHUB_OPTIONS 35 | }); 36 | const deployment_id = data.find(Boolean)?.id; 37 | if (deployment_id) { 38 | return octokit.repos.createDeploymentStatus({ 39 | state: state as DeploymentState, 40 | deployment_id, 41 | description, 42 | target_url, 43 | environment_url, 44 | ...context.repo, 45 | ...GITHUB_OPTIONS 46 | }); 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /src/helpers/set-latest-pipeline-status.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import * as core from '@actions/core'; 15 | import { DEFAULT_PIPELINE_STATUS, GITHUB_OPTIONS, PRODUCTION_ENVIRONMENT } from '../constants'; 16 | import { PipelineState } from '../types/github'; 17 | import { HelperInputs } from '../types/generated'; 18 | import { context as githubContext } from '@actions/github'; 19 | import { octokit } from '../octokit'; 20 | 21 | export class SetLatestPipelineStatus extends HelperInputs { 22 | sha = ''; 23 | declare context?: string; 24 | declare environment?: string; 25 | } 26 | 27 | export const setLatestPipelineStatus = async ({ 28 | sha, 29 | context = DEFAULT_PIPELINE_STATUS, 30 | environment = PRODUCTION_ENVIRONMENT 31 | }: SetLatestPipelineStatus) => { 32 | const { data: deployments } = await octokit.repos.listDeployments({ 33 | environment, 34 | ...githubContext.repo, 35 | ...GITHUB_OPTIONS 36 | }); 37 | const deployment_id = deployments.find(Boolean)?.id; 38 | if (!deployment_id) { 39 | core.info('No deployments found. Pipeline is clear!'); 40 | return; 41 | } 42 | const { data: deploymentStatuses } = await octokit.repos.listDeploymentStatuses({ 43 | deployment_id, 44 | ...githubContext.repo, 45 | ...GITHUB_OPTIONS 46 | }); 47 | const deploymentStatus = deploymentStatuses.find(Boolean); 48 | if (!deploymentStatus) { 49 | return octokit.repos.createCommitStatus({ 50 | sha, 51 | context, 52 | state: 'pending', 53 | ...githubContext.repo 54 | }); 55 | } 56 | const { state, description, target_url } = deploymentStatus; 57 | return octokit.repos.createCommitStatus({ 58 | sha, 59 | context, 60 | state: deploymentStateToPipelineStateMap[state] ?? 'pending', 61 | description, 62 | target_url, 63 | ...githubContext.repo 64 | }); 65 | }; 66 | 67 | const deploymentStateToPipelineStateMap: { [deploymentState: string]: PipelineState } = { 68 | in_progress: 'pending', 69 | success: 'success', 70 | failure: 'failure', 71 | inactive: 'error' 72 | }; 73 | -------------------------------------------------------------------------------- /src/helpers/update-check-result.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { HelperInputs } from '../types/generated'; 15 | import { context as githubContext } from '@actions/github'; 16 | import { octokit } from '../octokit'; 17 | import { ChecksUpdateConclusion } from '../types/github'; 18 | 19 | export class UpdateCheckResult extends HelperInputs { 20 | context = ''; 21 | sha = ''; 22 | state = ''; 23 | declare description?: string; 24 | } 25 | 26 | export const updateCheckResult = async ({ context, sha, state, description }: UpdateCheckResult) => { 27 | const checks = await octokit.checks.listForRef({ 28 | ref: sha, 29 | check_name: context, 30 | ...githubContext.repo 31 | }); 32 | const check_run_id = checks.data.check_runs[0]?.id; 33 | if (!check_run_id) { 34 | throw new Error('Check run not found'); 35 | } 36 | 37 | return octokit.checks.update({ 38 | check_run_id, 39 | conclusion: state as ChecksUpdateConclusion, 40 | output: { 41 | title: description ?? `Check updated to ${state}`, 42 | summary: 'Check updated via update-check-result helper' 43 | }, 44 | ...githubContext.repo 45 | }); 46 | }; 47 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import * as core from '@actions/core'; 15 | import { camelCase, upperFirst } from 'lodash'; 16 | import { getActionInputs } from './utils/get-action-inputs'; 17 | 18 | export const run = async () => { 19 | try { 20 | const helper = core.getInput('helper', { required: true }); 21 | const { [camelCase(helper)]: method, [upperFirst(camelCase(helper))]: HelperInterface } = await import(`./helpers/${helper}`); 22 | const requiredInputs = HelperInterface ? Object.keys(new HelperInterface()) : []; 23 | const actionInputs = getActionInputs(requiredInputs); 24 | const output = await method(actionInputs); 25 | core.setOutput('output', output); 26 | } catch (error) { 27 | core.setFailed(error as Error); 28 | } 29 | }; 30 | 31 | run(); 32 | -------------------------------------------------------------------------------- /src/octokit.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import * as core from '@actions/core'; 15 | import * as fetch from '@adobe/node-fetch-retry'; 16 | import { getOctokit } from '@actions/github'; 17 | 18 | const githubToken = core.getInput('github_token', { required: true }); 19 | export const { rest: octokit, graphql: octokitGraphql } = getOctokit(githubToken, { request: { fetch } }); 20 | -------------------------------------------------------------------------------- /src/types/generated.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | export class HelperInputs { 15 | declare helper?: string; 16 | declare github_token?: string; 17 | declare body?: string; 18 | declare project_name?: string; 19 | declare project_destination_column_name?: string; 20 | declare note?: string; 21 | declare project_origin_column_name?: string; 22 | declare sha?: string; 23 | declare context?: string; 24 | declare state?: string; 25 | declare description?: string; 26 | declare target_url?: string; 27 | declare environment?: string; 28 | declare environment_url?: string; 29 | declare label?: string; 30 | declare labels?: string; 31 | declare paths?: string; 32 | declare ignore_globs?: string; 33 | declare override_filter_paths?: string; 34 | declare batches?: string; 35 | declare pattern?: string; 36 | declare teams?: string; 37 | declare users?: string; 38 | declare login?: string; 39 | declare paths_no_filter?: string; 40 | declare slack_webhook_url?: string; 41 | declare number_of_assignees?: string; 42 | declare number_of_reviewers?: string; 43 | declare globs?: string; 44 | declare override_filter_globs?: string; 45 | declare title?: string; 46 | declare seconds?: string; 47 | declare pull_number?: string; 48 | declare base?: string; 49 | declare head?: string; 50 | declare days?: string; 51 | declare no_evict_upon_conflict?: string; 52 | declare skip_if_already_set?: string; 53 | declare delimiter?: string; 54 | declare team?: string; 55 | declare ignore_deleted?: string; 56 | declare return_full_payload?: string; 57 | declare skip_auto_merge?: string; 58 | declare repo_name?: string; 59 | declare repo_owner_name?: string; 60 | declare load_balancing_sizes?: string; 61 | declare required_review_overrides?: string; 62 | declare codeowners_overrides?: string; 63 | declare max_queue_size?: string; 64 | declare allow_only_for_maintainers?: string; 65 | declare use_basic_matrix_configuration?: string; 66 | declare merge_queue_enabled?: string; 67 | declare packages?: string; 68 | declare branch_name?: string; 69 | declare commit_message?: string; 70 | } 71 | -------------------------------------------------------------------------------- /src/types/github.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { RestEndpointMethodTypes } from '@octokit/plugin-rest-endpoint-methods/dist-types'; 15 | import { octokit } from '../octokit'; 16 | 17 | export type PipelineState = RestEndpointMethodTypes['repos']['createCommitStatus']['parameters']['state']; 18 | export type DeploymentState = RestEndpointMethodTypes['repos']['createDeploymentStatus']['parameters']['state']; 19 | export type PullRequest = Awaited>['data']; 20 | export type PullRequestList = RestEndpointMethodTypes['pulls']['list']['response']['data']; 21 | export type IssueList = RestEndpointMethodTypes['issues']['listForRepo']['response']['data']; 22 | export type CommentList = RestEndpointMethodTypes['issues']['listComments']['response']['data']; 23 | export type SingleComment = CommentList[number]; 24 | export type IssueAssignees = IssueList[number]['assignees']; 25 | export type PullRequestReviewList = RestEndpointMethodTypes['pulls']['listReviews']['response']['data']; 26 | export type SinglePullRequest = PullRequestList[number]; 27 | export type IssueLabels = IssueList[number]['labels']; 28 | export type PullRequestBranchesList = RestEndpointMethodTypes['repos']['listBranches']['response']['data']; 29 | export type ChangedFilesList = RestEndpointMethodTypes['pulls']['listFiles']['response']['data']; 30 | export type ProjectListResponse = RestEndpointMethodTypes['projects']['listForRepo']['response']; 31 | export type ColumnListResponse = RestEndpointMethodTypes['projects']['listColumns']['response']; 32 | export type ChecksUpdateConclusion = RestEndpointMethodTypes['checks']['update']['parameters']['conclusion']; 33 | export type MembersInOrg = RestEndpointMethodTypes['teams']['listMembersInOrg']['response']['data']; 34 | export type MembersInOrgParams = RestEndpointMethodTypes['teams']['listMembersInOrg']['parameters']; 35 | 36 | export type GithubError = { 37 | status: number; 38 | message: string; 39 | }; 40 | -------------------------------------------------------------------------------- /src/utils/add-due-date-comment.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { IssueAssignees, CommentList, SingleComment } from '../types/github'; 15 | import { paginateAllCommentsOnIssue } from './paginate-comments-on-issue'; 16 | import { context } from '@actions/github'; 17 | import { octokit } from '../octokit'; 18 | import { SECONDS_IN_A_DAY } from '../constants'; 19 | 20 | export const addDueDateComment = async (deadline: number, createdDate: Date, issue_number: number) => { 21 | const commentList: CommentList = await paginateAllCommentsOnIssue(issue_number); 22 | if (!commentList?.find((comment: SingleComment) => comment.body?.startsWith('This issue is due on'))) { 23 | const dueDate = new Date(createdDate.getTime() + deadline * SECONDS_IN_A_DAY); 24 | 25 | await octokit.issues.createComment({ 26 | issue_number, 27 | body: `This issue is due on ${dueDate.toDateString()}`, 28 | ...context.repo 29 | }); 30 | } 31 | }; 32 | 33 | export const pingAssigneesForDueDate = async (assignees: IssueAssignees, labelToAdd: string, issue_number: number) => { 34 | const commentList: CommentList = await paginateAllCommentsOnIssue(issue_number); 35 | 36 | assignees?.map(async assignee => { 37 | const commentToAdd = `@${assignee.name || assignee.login}, this issue assigned to you is now ${labelToAdd.toLowerCase()}`; 38 | if (!commentList?.find((comment: SingleComment) => comment.body === commentToAdd)) { 39 | await octokit.issues.createComment({ 40 | issue_number, 41 | body: commentToAdd, 42 | ...context.repo 43 | }); 44 | } 45 | }); 46 | }; 47 | -------------------------------------------------------------------------------- /src/utils/convert-to-team-slug.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | export const convertToTeamSlug = (codeOwner: string) => codeOwner.substring(codeOwner.indexOf('/') + 1); 15 | -------------------------------------------------------------------------------- /src/utils/get-action-inputs.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { getInput } from '@actions/core'; 15 | import { getInputsFromFile } from './get-inputs-from-file'; 16 | import { pickBy } from 'lodash'; 17 | import { readFileSync } from 'fs'; 18 | import { join } from 'path'; 19 | 20 | export const getActionInputs = (requiredInputs: string[] = []) => { 21 | const yamlContents = readFileSync(join(process.cwd(), 'action.yml')).toString(); 22 | const inputsFromFile = getInputsFromFile(yamlContents).reduce((acc, current) => { 23 | const trimWhitespaceOptions = current === 'delimiter' ? { trimWhitespace: false } : {}; 24 | return { 25 | ...acc, 26 | [current]: getInput(current, { required: requiredInputs.includes(current), ...trimWhitespaceOptions }) 27 | }; 28 | }, {}); 29 | 30 | return pickBy(inputsFromFile); 31 | }; 32 | -------------------------------------------------------------------------------- /src/utils/get-changed-filepaths.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { ChangedFilesList } from '../types/github'; 15 | import { context } from '@actions/github'; 16 | import { octokit } from '../octokit'; 17 | 18 | export const getChangedFilepaths = async (pull_number: number, ignore_deleted?: boolean) => { 19 | const changedFiles = await paginateAllChangedFilepaths(pull_number); 20 | const renamedPreviousFilenames = changedFiles 21 | .filter(({ status }) => status === 'renamed') 22 | .map(({ previous_filename }) => previous_filename) 23 | .filter(Boolean); // GitHub should always include previous_filename for renamed files, but just in case 24 | const processedFilenames = (ignore_deleted ? changedFiles.filter(({ status }) => status !== 'removed') : changedFiles).map( 25 | ({ filename }) => filename 26 | ); 27 | return processedFilenames.concat(renamedPreviousFilenames); 28 | }; 29 | 30 | const paginateAllChangedFilepaths = async (pull_number: number, page = 1): Promise => { 31 | const response = await octokit.pulls.listFiles({ 32 | pull_number, 33 | per_page: 100, 34 | page, 35 | ...context.repo 36 | }); 37 | if (!response.data.length) { 38 | return []; 39 | } 40 | return response.data.concat(await paginateAllChangedFilepaths(pull_number, page + 1)); 41 | }; 42 | -------------------------------------------------------------------------------- /src/utils/get-default-branch.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { octokit } from '../octokit'; 15 | import { context } from '@actions/github'; 16 | 17 | export const getDefaultBranch = async () => { 18 | const { 19 | data: { default_branch } 20 | } = await octokit.repos.get({ ...context.repo }); 21 | return default_branch; 22 | }; 23 | -------------------------------------------------------------------------------- /src/utils/get-inputs-from-file.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import * as yaml from 'js-yaml'; 15 | 16 | export const getInputsFromFile = (yamlContents: string) => 17 | Object.keys((yaml.load(yamlContents) as { inputs: Record }).inputs); 18 | -------------------------------------------------------------------------------- /src/utils/get-project-columns.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { ColumnListResponse, ProjectListResponse } from '../types/github'; 15 | import { GITHUB_OPTIONS } from '../constants'; 16 | import { context } from '@actions/github'; 17 | import { octokit } from '../octokit'; 18 | 19 | interface GetProjectColumns { 20 | project_name: string; 21 | } 22 | 23 | export interface SingleColumn { 24 | url: string; 25 | project_url: string; 26 | cards_url: string; 27 | id: number; 28 | node_id: string; 29 | name: string; 30 | created_at: string; 31 | updated_at: string; 32 | } 33 | 34 | export const getProjectColumns = async ({ project_name }: GetProjectColumns) => { 35 | const projectList = await octokit.projects.listForRepo({ state: 'open', per_page: 100, ...context.repo, ...GITHUB_OPTIONS }); 36 | const project = findProjectToModify(projectList, project_name); 37 | 38 | if (!project) { 39 | return null; 40 | } 41 | 42 | return octokit.projects.listColumns({ project_id: project.id, per_page: 100, ...GITHUB_OPTIONS }); 43 | }; 44 | 45 | const findProjectToModify = (projectsResponse: ProjectListResponse, project_name: string) => 46 | projectsResponse.data.find(project => project.name === project_name); 47 | 48 | export const getDestinationColumn = (columns: ColumnListResponse, project_destination_column_name: string) => 49 | columns.data.find(column => column.name === project_destination_column_name); 50 | -------------------------------------------------------------------------------- /src/utils/merge-queue.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { paginateAllBranches } from './paginate-all-branches'; 15 | import { context } from '@actions/github'; 16 | 17 | export const getMergeQueueCommitHashes = async () => { 18 | const branches = await paginateAllBranches(); 19 | const mergeQueueBranches = branches.filter(branch => branch.name.startsWith('gh-readonly-queue/')); 20 | return mergeQueueBranches.map(branch => branch.commit.sha); 21 | }; 22 | 23 | export const getPrNumberFromMergeQueueRef = (ref = context.ref) => { 24 | const prNumber = Number( 25 | ref 26 | .split('/') 27 | .find(part => part.includes('pr-')) 28 | ?.match(/\d+/)?.[0] 29 | ); 30 | if (isNaN(prNumber)) { 31 | throw new Error('Could not find PR number in merge queue ref.'); 32 | } 33 | return prNumber; 34 | }; 35 | -------------------------------------------------------------------------------- /src/utils/notify-user.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import * as core from '@actions/core'; 15 | import axios from 'axios'; 16 | import { context } from '@actions/github'; 17 | import { octokit } from '../octokit'; 18 | import { getEmailOnUserProfile } from '../helpers/get-email-on-user-profile'; 19 | 20 | interface NotifyUser { 21 | login: string; 22 | pull_number: number; 23 | slack_webhook_url: string; 24 | queuePosition?: number; 25 | } 26 | 27 | export const notifyUser = async ({ login, pull_number, slack_webhook_url, queuePosition }: NotifyUser) => { 28 | const email = await getEmailOnUserProfile({ login }); 29 | if (!email) { 30 | return; 31 | } 32 | core.info(`Notifying user ${login}...`); 33 | const { 34 | data: { title, html_url } 35 | } = await octokit.pulls.get({ pull_number, ...context.repo }); 36 | 37 | const result = await axios.post(slack_webhook_url, { 38 | assignee: email, 39 | title, 40 | html_url, 41 | repo: context.repo.repo, 42 | queuePosition 43 | }); 44 | if (result.status !== 200) { 45 | core.error(result.statusText); 46 | core.setFailed(`User notification failed for login: ${login} and email: ${email}`); 47 | } 48 | return result; 49 | }; 50 | -------------------------------------------------------------------------------- /src/utils/paginate-all-branches.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { PullRequestBranchesList } from '../types/github'; 15 | import { octokit } from '../octokit'; 16 | import { context } from '@actions/github'; 17 | 18 | export const paginateAllBranches = async ({ 19 | protectedBranches, 20 | page = 1 21 | }: { protectedBranches?: boolean; page?: number } = {}): Promise => { 22 | const response = await octokit.repos.listBranches({ 23 | protected: protectedBranches, 24 | per_page: 100, 25 | page, 26 | ...context.repo 27 | }); 28 | if (!response.data.length) { 29 | return []; 30 | } 31 | return [...response.data, ...(await paginateAllBranches({ protectedBranches, page: page + 1 }))]; 32 | }; 33 | -------------------------------------------------------------------------------- /src/utils/paginate-all-reviews.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { PullRequestReviewList } from '../types/github'; 15 | import { octokit } from '../octokit'; 16 | import { context } from '@actions/github'; 17 | 18 | export const paginateAllReviews = async (prNumber: number, page = 1): Promise => { 19 | const response = await octokit.pulls.listReviews({ 20 | pull_number: prNumber, 21 | per_page: 100, 22 | page, 23 | ...context.repo 24 | }); 25 | if (!response.data.length) { 26 | return []; 27 | } 28 | return response.data.concat(await paginateAllReviews(prNumber, page + 1)); 29 | }; 30 | -------------------------------------------------------------------------------- /src/utils/paginate-comments-on-issue.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { CommentList } from '../types/github'; 15 | import { octokit } from '../octokit'; 16 | import { context } from '@actions/github'; 17 | 18 | export const paginateAllCommentsOnIssue = async (issue_number: number, page = 1): Promise => { 19 | const response = await octokit.issues.listComments({ 20 | issue_number, 21 | sort: 'created', 22 | direction: 'desc', 23 | per_page: 100, 24 | page, 25 | ...context.repo 26 | }); 27 | if (!response?.data?.length) { 28 | return []; 29 | } 30 | return response.data.concat(await paginateAllCommentsOnIssue(issue_number, page + 1)); 31 | }; 32 | -------------------------------------------------------------------------------- /src/utils/paginate-members-in-org.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { MembersInOrg } from '../types/github'; 15 | import { octokit } from '../octokit'; 16 | import { convertToTeamSlug } from './convert-to-team-slug'; 17 | import { context } from '@actions/github'; 18 | 19 | export const paginateMembersInOrg = async (team: string, page: number = 1): Promise => { 20 | const response = await octokit.teams.listMembersInOrg({ 21 | org: context.repo.owner, 22 | team_slug: convertToTeamSlug(team), 23 | per_page: 100, 24 | page 25 | }); 26 | if (!response?.data?.length) { 27 | return []; 28 | } 29 | // If the response size is less than 100, we have reached the end of the pagination 30 | if (response.data.length < 100) { 31 | return response.data; 32 | } 33 | return [...response.data, ...(await paginateMembersInOrg(team, page + 1))]; 34 | }; 35 | -------------------------------------------------------------------------------- /src/utils/paginate-open-pull-requests.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { PullRequestList } from '../types/github'; 15 | import { octokit } from '../octokit'; 16 | import { context } from '@actions/github'; 17 | 18 | export const paginateAllOpenPullRequests = async (page = 1): Promise => { 19 | const response = await octokit.pulls.list({ 20 | state: 'open', 21 | sort: 'updated', 22 | direction: 'desc', 23 | per_page: 100, 24 | page, 25 | ...context.repo 26 | }); 27 | if (!response.data.length) { 28 | return []; 29 | } 30 | return response.data.concat(await paginateAllOpenPullRequests(page + 1)); 31 | }; 32 | -------------------------------------------------------------------------------- /src/utils/paginate-prioritized-issues.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { IssueList } from '../types/github'; 15 | import { octokit } from '../octokit'; 16 | import { context } from '@actions/github'; 17 | import { PRIORITY_LABELS } from '../constants'; 18 | import { map } from 'bluebird'; 19 | 20 | export const paginateAllPrioritizedIssues = async () => 21 | (await map(PRIORITY_LABELS, async label => await paginateIssuesOfSpecificPriority(label))).flat(); 22 | 23 | export const paginateIssuesOfSpecificPriority = async (label: string, page = 1): Promise => { 24 | const response = await octokit.issues.listForRepo({ 25 | state: 'open', 26 | sort: 'created', 27 | direction: 'desc', 28 | per_page: 100, 29 | labels: label, 30 | page, 31 | ...context.repo 32 | }); 33 | if (!response?.data?.length) { 34 | return []; 35 | } 36 | return response.data.concat(await paginateIssuesOfSpecificPriority(label, page + 1)); 37 | }; 38 | -------------------------------------------------------------------------------- /templates/docs.hbs: -------------------------------------------------------------------------------- 1 | Each of the following helpers are defined in a file of the same name in `src/helpers`: 2 | ### [{{ helper }}](.github/workflows/{{ helper }}.yml) 3 | * {{ description }} 4 | -------------------------------------------------------------------------------- /templates/helper.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { HelperInputs } from '../types/generated'; 15 | import { context } from '@actions/github'; 16 | import { octokit } from '../octokit'; 17 | 18 | export class {{ properCase helper }} extends HelperInputs { 19 | requiredInput = ''; 20 | declare optionalInput?: string; 21 | } 22 | 23 | export const {{ camelCase helper }} = async ({ requiredInput, optionalInput }: {{ properCase helper }}) => { 24 | 25 | }; 26 | -------------------------------------------------------------------------------- /templates/test.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { Mocktokit } from '../types'; 15 | import { context } from '@actions/github'; 16 | import { {{ camelCase helper }} } from '../../src/helpers/{{ dashCase helper }}'; 17 | import { octokit } from '../../src/octokit'; 18 | 19 | jest.mock('@actions/core'); 20 | jest.mock('@actions/github', () => ({ 21 | context: { repo: { repo: 'repo', owner: 'owner' }, issue: { number: 123 } }, 22 | getOctokit: jest.fn(() => ({ 23 | rest: { 24 | 25 | } 26 | })) 27 | })); 28 | 29 | describe('{{ camelCase helper }}', () => { 30 | beforeEach(() => { 31 | {{ camelCase helper }}({ requiredInput: '', optionalInput: '' }); 32 | }); 33 | 34 | it('should pass', () => { 35 | expect(false).toBe(true); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /templates/workflow.hbs: -------------------------------------------------------------------------------- 1 | name: {{ spaceSeparatedCase ( titleCase helper ) }} 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | paths: 7 | - 'src/helpers/{{ dashCase helper }}.ts' 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - uses: ./ 17 | with: 18 | helper: {{ dashCase helper }} 19 | -------------------------------------------------------------------------------- /test/constants.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { GITHUB_OPTIONS } from '../src/constants'; 15 | 16 | describe('constants', () => { 17 | it('should produce correct github options', () => { 18 | expect(GITHUB_OPTIONS).toEqual({ 19 | headers: { 20 | accept: 21 | 'application/vnd.github.ant-man-preview+json,application/vnd.github.flash-preview+json,application/vnd.github.groot-preview+json,application/vnd.github.inertia-preview+json,application/vnd.github.starfox-preview+json' 22 | } 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/helpers/add-labels.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { addLabels } from '../../src/helpers/add-labels'; 15 | import { context } from '@actions/github'; 16 | import { octokit } from '../../src/octokit'; 17 | 18 | jest.mock('@actions/core'); 19 | jest.mock('@actions/github', () => ({ 20 | context: { repo: { repo: 'repo', owner: 'owner' }, issue: { number: 123 } }, 21 | getOctokit: jest.fn(() => ({ rest: { issues: { addLabels: jest.fn() } } })) 22 | })); 23 | 24 | describe('addLabels', () => { 25 | const labels = 'Needs a11y review\nExempt 👻'; 26 | 27 | beforeEach(() => { 28 | addLabels({ labels }); 29 | }); 30 | 31 | it('should call addLabels with correct params', () => { 32 | expect(octokit.issues.addLabels).toHaveBeenCalledWith({ 33 | labels: labels.split('\n'), 34 | issue_number: 123, 35 | ...context.repo 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/helpers/add-late-review-label.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { LATE_REVIEW } from '../../src/constants'; 15 | import { addLateReviewLabel } from '../../src/helpers/add-late-review-label'; 16 | import { context } from '@actions/github'; 17 | import { octokit } from '../../src/octokit'; 18 | import { Mocktokit } from '../types'; 19 | 20 | jest.mock('@actions/core'); 21 | jest.mock('@actions/github', () => ({ 22 | context: { repo: { repo: 'repo', owner: 'owner' } }, 23 | getOctokit: jest.fn(() => ({ rest: { issues: { addLabels: jest.fn() }, pulls: { list: jest.fn() } } })) 24 | })); 25 | 26 | jest.spyOn(Date, 'now').mockImplementation(() => new Date('2022-08-04T10:00:00Z').getTime()); 27 | 28 | describe('addLateReviewLabel', () => { 29 | describe('Late Review', () => { 30 | beforeEach(() => { 31 | (octokit.pulls.list as unknown as Mocktokit) 32 | .mockResolvedValueOnce({ 33 | status: '200', 34 | data: [ 35 | { 36 | number: 123, 37 | requested_reviewers: [{ id: 234 }], 38 | updated_at: '2022-07-25T20:09:21Z' 39 | } 40 | ] 41 | }) 42 | .mockResolvedValueOnce({ 43 | status: '200', 44 | data: [] 45 | }); 46 | }); 47 | 48 | it('should add Late Review label to the pr', async () => { 49 | await addLateReviewLabel({ 50 | days: '1', 51 | ...context.repo 52 | }); 53 | 54 | expect(octokit.pulls.list).toHaveBeenCalledWith({ 55 | page: 1, 56 | per_page: 100, 57 | sort: 'updated', 58 | direction: 'desc', 59 | state: 'open', 60 | ...context.repo 61 | }); 62 | 63 | expect(octokit.pulls.list).toHaveBeenCalledWith({ 64 | page: 2, 65 | per_page: 100, 66 | sort: 'updated', 67 | direction: 'desc', 68 | state: 'open', 69 | ...context.repo 70 | }); 71 | 72 | expect(octokit.issues.addLabels).toHaveBeenCalledWith({ 73 | labels: [LATE_REVIEW], 74 | issue_number: 123, 75 | ...context.repo 76 | }); 77 | }); 78 | 79 | it('should not add any labels to the pr', async () => { 80 | await addLateReviewLabel({ 81 | days: '1', 82 | ...context.repo 83 | }); 84 | 85 | expect(octokit.pulls.list).toHaveBeenCalledWith({ 86 | page: 1, 87 | per_page: 100, 88 | sort: 'updated', 89 | direction: 'desc', 90 | state: 'open', 91 | ...context.repo 92 | }); 93 | 94 | expect(octokit.pulls.list).toHaveBeenCalledWith({ 95 | page: 2, 96 | per_page: 100, 97 | sort: 'updated', 98 | direction: 'desc', 99 | state: 'open', 100 | ...context.repo 101 | }); 102 | 103 | expect(octokit.issues.addLabels).not.toHaveBeenCalledWith(); 104 | }); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /test/helpers/add-pr-approval-label.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { CORE_APPROVED_PR_LABEL, PEER_APPROVED_PR_LABEL } from '../../src/constants'; 15 | import { addPrApprovalLabel } from '../../src/helpers/add-pr-approval-label'; 16 | import { context } from '@actions/github'; 17 | import { getCoreMemberLogins } from '../../src/utils/get-core-member-logins'; 18 | import { octokit } from '../../src/octokit'; 19 | 20 | jest.mock('@actions/core'); 21 | jest.mock('@actions/github', () => ({ 22 | context: { repo: { repo: 'repo', owner: 'owner' }, issue: { number: 123 } }, 23 | getOctokit: jest.fn(() => ({ rest: { issues: { addLabels: jest.fn() } } })) 24 | })); 25 | jest.mock('../../src/utils/get-core-member-logins'); 26 | 27 | (getCoreMemberLogins as jest.Mock).mockResolvedValue(['user1', 'user2', 'user3']); 28 | 29 | const teams = 'team1\nteam2'; 30 | 31 | describe('addPrApprovalLabel', () => { 32 | describe('core approver case', () => { 33 | const login = 'user1'; 34 | 35 | beforeEach(async () => { 36 | await addPrApprovalLabel({ 37 | teams, 38 | login 39 | }); 40 | }); 41 | 42 | it('should call getCoreMemberLogins with correct params', () => { 43 | expect(getCoreMemberLogins).toHaveBeenCalledWith(123, ['team1', 'team2']); 44 | }); 45 | 46 | it('should add core approved label to the pr', () => { 47 | expect(octokit.issues.addLabels).toHaveBeenCalledWith({ 48 | labels: [CORE_APPROVED_PR_LABEL], 49 | issue_number: 123, 50 | ...context.repo 51 | }); 52 | }); 53 | }); 54 | 55 | describe('peer approver case', () => { 56 | const login = 'user6'; 57 | 58 | beforeEach(async () => { 59 | await addPrApprovalLabel({ 60 | teams, 61 | login 62 | }); 63 | }); 64 | 65 | it('should add core approved label to the pr', () => { 66 | expect(octokit.issues.addLabels).toHaveBeenCalledWith({ 67 | labels: [PEER_APPROVED_PR_LABEL], 68 | issue_number: 123, 69 | ...context.repo 70 | }); 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /test/helpers/approve-pr.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { approvePr } from '../../src/helpers/approve-pr'; 15 | import { context } from '@actions/github'; 16 | import { octokit } from '../../src/octokit'; 17 | 18 | jest.mock('@actions/core'); 19 | jest.mock('@actions/github', () => ({ 20 | context: { repo: { repo: 'repo', owner: 'owner' }, issue: { number: 123 } }, 21 | getOctokit: jest.fn(() => ({ rest: { pulls: { createReview: jest.fn() } } })) 22 | })); 23 | 24 | describe('approvePr', () => { 25 | beforeEach(() => { 26 | approvePr(); 27 | }); 28 | 29 | it('should call createReview with correct params', () => { 30 | expect(octokit.pulls.createReview).toHaveBeenCalledWith({ 31 | pull_number: 123, 32 | body: 'Approved by bot', 33 | event: 'APPROVE', 34 | ...context.repo 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /test/helpers/are-reviewers-required.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { areReviewersRequired } from '../../src/helpers/are-reviewers-required'; 15 | import { getRequiredCodeOwnersEntries } from '../../src/utils/get-core-member-logins'; 16 | 17 | jest.mock('@actions/core'); 18 | jest.mock('@actions/github', () => ({ 19 | context: { repo: { repo: 'repo', owner: 'owner' }, issue: { number: 123 } }, 20 | getOctokit: jest.fn(() => ({ 21 | rest: {} 22 | })) 23 | })); 24 | jest.mock('../../src/utils/get-core-member-logins'); 25 | 26 | describe('AreReviewersRequired', () => { 27 | beforeEach(() => { 28 | (getRequiredCodeOwnersEntries as jest.Mock).mockResolvedValue([{ owners: ['@ExpediaGroup/team1', '@ExpediaGroup/team2'] }]); 29 | }); 30 | 31 | it('should return true when all teams are required reviewers', async () => { 32 | const result = await areReviewersRequired({ teams: '@ExpediaGroup/team1\n@ExpediaGroup/team2' }); 33 | expect(result).toBe(true); 34 | }); 35 | 36 | it('should return false when not all teams are required reviewers', async () => { 37 | const result = await areReviewersRequired({ teams: '@ExpediaGroup/team1\n@ExpediaGroup/team3' }); 38 | expect(result).toBe(false); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /test/helpers/check-pr-title.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { Mocktokit } from '../types'; 15 | import { checkPrTitle } from '../../src/helpers/check-pr-title'; 16 | import { octokit } from '../../src/octokit'; 17 | 18 | jest.mock('@actions/core'); 19 | jest.mock('@actions/github', () => ({ 20 | context: { repo: { repo: 'repo', owner: 'owner' }, issue: { number: 123 } }, 21 | getOctokit: jest.fn(() => ({ rest: { pulls: { get: jest.fn() } } })) 22 | })); 23 | 24 | describe('checkPrTitle', () => { 25 | it('should pass as the PR title conforms to the regex', async () => { 26 | (octokit.pulls.get as unknown as Mocktokit).mockImplementation(async () => ({ 27 | data: { 28 | id: 1, 29 | number: 123, 30 | state: 'open', 31 | title: 'feat: added feature to project' 32 | } 33 | })); 34 | 35 | const result = await checkPrTitle({}); 36 | 37 | expect(result).toBe(true); 38 | }); 39 | 40 | it('should fail as the PR title does not conform to the regex', async () => { 41 | (octokit.pulls.get as unknown as Mocktokit).mockImplementation(async () => ({ 42 | data: { 43 | id: 1, 44 | number: 123, 45 | state: 'open', 46 | title: 'this title will fail' 47 | } 48 | })); 49 | 50 | const result = await checkPrTitle({}); 51 | 52 | expect(result).toBe(false); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /test/helpers/delete-stale-branches.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { context } from '@actions/github'; 15 | import { deleteStaleBranches } from '../../src/helpers/delete-stale-branches'; 16 | import { octokit } from '../../src/octokit'; 17 | import { paginateAllOpenPullRequests } from '../../src/utils/paginate-open-pull-requests'; 18 | 19 | jest.mock('../../src/utils/paginate-open-pull-requests'); 20 | jest.mock('@actions/core'); 21 | jest.mock('@actions/github', () => ({ 22 | context: { repo: { repo: 'repo', owner: 'owner' } }, 23 | getOctokit: jest.fn(() => ({ 24 | rest: { 25 | git: { 26 | deleteRef: jest.fn(), 27 | getCommit: jest.fn().mockImplementation(({ commit_sha }) => 28 | commit_sha === '123' 29 | ? { 30 | data: { committer: { date: '2023-02-23T11:00:00Z' } } 31 | } 32 | : { 33 | data: { committer: { date: '2023-02-23T09:00:00Z' } } 34 | } 35 | ) 36 | }, 37 | repos: { 38 | get: jest.fn().mockReturnValue({ data: { default_branch: 'main' } }), 39 | listBranches: jest.fn().mockImplementation(({ page }) => 40 | page === 1 41 | ? { 42 | data: [ 43 | { name: 'main', commit: { sha: 'main sha' } }, 44 | { name: 'new-branch-no-open-pr', commit: { sha: '123' } }, 45 | { name: 'old-branch-with-no-open-pr', commit: { sha: '456' } }, 46 | { name: 'branch-with-open-pr', commit: { sha: '789' } } 47 | ] 48 | } 49 | : { data: [] } 50 | ) 51 | } 52 | } 53 | })) 54 | })); 55 | (paginateAllOpenPullRequests as jest.Mock).mockResolvedValue([ 56 | { head: { ref: 'branch-with-open-pr' } }, 57 | { head: { ref: 'some-other-branch' } } 58 | ]); 59 | jest.spyOn(Date, 'now').mockImplementation(() => new Date('2023-02-24T10:00:00Z').getTime()); 60 | 61 | describe('deleteStaleBranches', () => { 62 | it('should call octokit deleteRef with correct branch names', async () => { 63 | await deleteStaleBranches({ days: '1' }); 64 | 65 | expect(octokit.git.deleteRef).not.toHaveBeenCalledWith({ 66 | ref: 'heads/main', 67 | ...context.repo 68 | }); 69 | expect(octokit.git.deleteRef).not.toHaveBeenCalledWith({ 70 | ref: 'heads/new-branch-no-open-pr', 71 | ...context.repo 72 | }); 73 | expect(octokit.git.deleteRef).not.toHaveBeenCalledWith({ 74 | ref: 'heads/branch-with-open-pr', 75 | ...context.repo 76 | }); 77 | expect(octokit.git.deleteRef).toHaveBeenCalledWith({ 78 | ref: 'heads/old-branch-with-no-open-pr', 79 | ...context.repo 80 | }); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /test/helpers/get-email-on-user-profile.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { getEmailOnUserProfile } from '../../src/helpers/get-email-on-user-profile'; 15 | import * as core from '@actions/core'; 16 | import { octokit } from '../../src/octokit'; 17 | import { Mocktokit } from '../types'; 18 | 19 | jest.mock('@actions/core'); 20 | jest.mock('@actions/github', () => ({ 21 | context: { repo: { repo: 'repo', owner: 'owner' }, issue: { number: 123 } }, 22 | getOctokit: jest.fn(() => ({ 23 | rest: { 24 | users: { 25 | getByUsername: jest.fn(() => ({ data: { email: 'example@github.com' } })) 26 | } 27 | } 28 | })) 29 | })); 30 | 31 | describe('getEmailOnUserProfile', () => { 32 | it('should retrieve user email', async () => { 33 | const result = await getEmailOnUserProfile({ login: 'example' }); 34 | expect(result).toBe('example@github.com'); 35 | }); 36 | 37 | it('should fail if user has no email on their profile', async () => { 38 | (octokit.users.getByUsername as unknown as Mocktokit).mockImplementationOnce(() => ({ data: { email: null } })); 39 | const result = await getEmailOnUserProfile({ login: 'example' }); 40 | expect(core.setFailed).toHaveBeenCalled(); 41 | expect(result).toBeUndefined(); 42 | }); 43 | 44 | it('should retrieve user email that matches regex pattern', async () => { 45 | const result = await getEmailOnUserProfile({ login: 'example', pattern: '@github.com' }); 46 | expect(core.setFailed).not.toHaveBeenCalled(); 47 | expect(result).toBe('example@github.com'); 48 | }); 49 | 50 | it('should fail if resulting email does not match regex pattern', async () => { 51 | const result = await getEmailOnUserProfile({ login: 'example', pattern: '@expediagroup.com' }); 52 | expect(core.setFailed).toHaveBeenCalled(); 53 | expect(result).toBeUndefined(); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /test/helpers/get-merge-queue-position.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { Mocktokit } from '../types'; 15 | import { getMergeQueuePosition } from '../../src/helpers/get-merge-queue-position'; 16 | import { octokitGraphql } from '../../src/octokit'; 17 | import { MergeQueueEntry } from '@octokit/graphql-schema'; 18 | import { context } from '@actions/github'; 19 | 20 | jest.mock('@actions/core'); 21 | jest.mock('@actions/github', () => ({ 22 | context: { repo: { repo: 'repo', owner: 'owner' } }, 23 | getOctokit: jest.fn(() => ({ 24 | graphql: jest.fn() 25 | })) 26 | })); 27 | 28 | type RecursivePartial = { 29 | [P in keyof T]?: RecursivePartial; 30 | }; 31 | function mockGraphQLResponse(mergeQueueEntries: RecursivePartial[]) { 32 | (octokitGraphql as unknown as Mocktokit).mockImplementation(async () => ({ 33 | repository: { 34 | mergeQueue: { 35 | entries: { 36 | nodes: mergeQueueEntries 37 | } 38 | } 39 | } 40 | })); 41 | } 42 | 43 | describe('getMergeQueuePosition', () => { 44 | it('should return 1 for PR 1st in the queue', async () => { 45 | context.ref = 'refs/heads/gh-readonly-queue/default-branch/pr-123-f0d9a4cb862b13cdaab6522f72d6dc17e4336b7f'; 46 | mockGraphQLResponse([ 47 | { position: 1, pullRequest: { number: 123 } }, 48 | { position: 2, pullRequest: { number: 456 } } 49 | ]); 50 | const result = await getMergeQueuePosition({}); 51 | expect(result).toBe(1); 52 | }); 53 | 54 | it('should return 3 for PR 3rd in the queue', async () => { 55 | context.ref = 'refs/heads/gh-readonly-queue/default-branch/pr-789-f0d9a4cb862b13cdaab6522f72d6dc17e4336b7f'; 56 | mockGraphQLResponse([ 57 | { position: 1, pullRequest: { number: 123 } }, 58 | { position: 2, pullRequest: { number: 456 } }, 59 | { position: 3, pullRequest: { number: 789 } } 60 | ]); 61 | const result = await getMergeQueuePosition({}); 62 | expect(result).toBe(3); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /test/helpers/is-user-core-member.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { isUserCoreMember } from '../../src/helpers/is-user-core-member'; 15 | import { getCoreMemberLogins } from '../../src/utils/get-core-member-logins'; 16 | 17 | jest.mock('@actions/core'); 18 | jest.mock('@actions/github', () => ({ 19 | context: { repo: { repo: 'repo', owner: 'owner' }, issue: { number: 123 }, actor: 'admin' } 20 | })); 21 | jest.mock('../../src/utils/get-core-member-logins', () => ({ 22 | getCoreMemberLogins: jest.fn() 23 | })); 24 | 25 | describe('isUserCoreMember', () => { 26 | const login = 'octocat'; 27 | const pull_number = '123'; 28 | 29 | beforeEach(() => { 30 | jest.clearAllMocks(); 31 | }); 32 | 33 | it('should call isUserCoreMember with correct params and find user as core member', async () => { 34 | (getCoreMemberLogins as jest.Mock).mockResolvedValue(['octocat', 'admin']); 35 | 36 | const response = await isUserCoreMember({ login, pull_number }); 37 | 38 | expect(getCoreMemberLogins).toHaveBeenCalledWith(Number(pull_number)); 39 | expect(response).toBe(true); 40 | }); 41 | 42 | it('should call isUserCoreMember with correct params and find user as core member for context actor', async () => { 43 | (getCoreMemberLogins as jest.Mock).mockResolvedValue(['admin']); 44 | 45 | const response = await isUserCoreMember({ pull_number }); 46 | 47 | expect(getCoreMemberLogins).toHaveBeenCalledWith(Number(pull_number)); 48 | expect(response).toBe(true); 49 | }); 50 | 51 | it('should call isUserCoreMember with correct params and find user not as core member', async () => { 52 | (getCoreMemberLogins as jest.Mock).mockResolvedValue(['admin']); 53 | 54 | const response = await isUserCoreMember({ login, pull_number }); 55 | 56 | expect(getCoreMemberLogins).toHaveBeenCalledWith(Number(pull_number)); 57 | expect(response).toBe(false); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /test/helpers/is-user-in-team.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { isUserInTeam } from '../../src/helpers/is-user-in-team'; 15 | import { context } from '@actions/github'; 16 | import { octokit } from '../../src/octokit'; 17 | import { Mocktokit } from '../types'; 18 | 19 | jest.mock('@actions/core'); 20 | jest.mock('@actions/github', () => ({ 21 | context: { repo: { repo: 'repo', owner: 'owner' }, issue: { number: 123 }, actor: 'admin' }, 22 | getOctokit: jest.fn(() => ({ rest: { teams: { listMembersInOrg: jest.fn() } } })) 23 | })); 24 | 25 | (octokit.teams.listMembersInOrg as unknown as Mocktokit).mockImplementation(async ({ page, team_slug }) => { 26 | if (page === 1) { 27 | return { 28 | data: team_slug === 'users' ? [{ login: 'octocat' }, { login: 'admin' }] : [{ login: 'admin' }] 29 | }; 30 | } 31 | return { data: [] }; 32 | }); 33 | 34 | describe('isUserInTeam', () => { 35 | const login = 'octocat'; 36 | 37 | it('should call isUserInTeam with correct params and find user in team', async () => { 38 | const response = await isUserInTeam({ login, team: 'users' }); 39 | expect(octokit.teams.listMembersInOrg).toHaveBeenCalledWith({ 40 | org: context.repo.owner, 41 | page: 1, 42 | per_page: 100, 43 | team_slug: 'users' 44 | }); 45 | expect(response).toBe(true); 46 | }); 47 | 48 | it('should call isUserInTeam with correct params and find user in team for context actor', async () => { 49 | const response = await isUserInTeam({ team: 'users' }); 50 | expect(octokit.teams.listMembersInOrg).toHaveBeenCalledWith({ 51 | org: context.repo.owner, 52 | page: 1, 53 | per_page: 100, 54 | team_slug: 'users' 55 | }); 56 | expect(response).toBe(true); 57 | }); 58 | 59 | it('should call isUserInTeam with correct params and find user not in team', async () => { 60 | const response = await isUserInTeam({ login, team: 'core' }); 61 | expect(octokit.teams.listMembersInOrg).toHaveBeenCalledWith({ 62 | org: context.repo.owner, 63 | page: 1, 64 | per_page: 100, 65 | team_slug: 'core' 66 | }); 67 | expect(response).toBe(false); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /test/helpers/remove-label.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { Mocktokit } from '../types'; 15 | import { context } from '@actions/github'; 16 | import { octokit } from '../../src/octokit'; 17 | import { removeLabel } from '../../src/helpers/remove-label'; 18 | 19 | jest.mock('@actions/core'); 20 | jest.mock('@actions/github', () => ({ 21 | context: { repo: { repo: 'repo', owner: 'owner' }, issue: { number: 123 } }, 22 | getOctokit: jest.fn(() => ({ rest: { issues: { removeLabel: jest.fn() } } })) 23 | })); 24 | (octokit.issues.removeLabel as unknown as Mocktokit).mockImplementation(async () => 'label removed!'); 25 | 26 | describe('removeLabel', () => { 27 | const label = 'Needs a11y review'; 28 | 29 | beforeEach(() => { 30 | removeLabel({ label }); 31 | }); 32 | 33 | it('should call addLabels with correct params', () => { 34 | expect(octokit.issues.removeLabel).toHaveBeenCalledWith({ 35 | name: label, 36 | issue_number: 123, 37 | ...context.repo 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /test/helpers/reopen-pr.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { context } from '@actions/github'; 15 | import { reopenPr } from '../../src/helpers/reopen-pr'; 16 | import { octokit } from '../../src/octokit'; 17 | 18 | jest.mock('@actions/core'); 19 | jest.mock('@actions/github', () => ({ 20 | context: { repo: { repo: 'repo', owner: 'owner' }, issue: { number: 123 } }, 21 | getOctokit: jest.fn(() => ({ 22 | rest: { 23 | pulls: { update: jest.fn() } 24 | } 25 | })) 26 | })); 27 | 28 | describe('reopenPr', () => { 29 | beforeEach(() => { 30 | reopenPr(); 31 | }); 32 | 33 | it('should call pulls.update with correct params', () => { 34 | expect(octokit.pulls.update).toHaveBeenCalledWith({ 35 | pull_number: 123, 36 | state: 'open', 37 | ...context.repo 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /test/helpers/set-latest-pipeline-status.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { DEFAULT_PIPELINE_STATUS, GITHUB_OPTIONS, PRODUCTION_ENVIRONMENT } from '../../src/constants'; 15 | import { Mocktokit } from '../types'; 16 | import { context } from '@actions/github'; 17 | import { octokit } from '../../src/octokit'; 18 | import { setLatestPipelineStatus } from '../../src/helpers/set-latest-pipeline-status'; 19 | 20 | jest.mock('@actions/core'); 21 | jest.mock('@actions/github', () => ({ 22 | context: { repo: { repo: 'repo', owner: 'owner' } }, 23 | getOctokit: jest.fn(() => ({ 24 | rest: { 25 | repos: { 26 | createCommitStatus: jest.fn(), 27 | listDeployments: jest.fn(), 28 | listDeploymentStatuses: jest.fn() 29 | } 30 | } 31 | })) 32 | })); 33 | 34 | const deployment_id = 123; 35 | (octokit.repos.listDeployments as unknown as Mocktokit).mockImplementation(async () => ({ 36 | data: [ 37 | { 38 | id: deployment_id 39 | }, 40 | { 41 | id: 456 42 | } 43 | ] 44 | })); 45 | 46 | describe('setLatestDeploymentStatus', () => { 47 | const sha = 'sha'; 48 | describe('deployment status found', () => { 49 | beforeEach(() => { 50 | (octokit.repos.listDeploymentStatuses as unknown as Mocktokit).mockImplementation(async () => ({ 51 | data: [ 52 | { 53 | state: 'success', 54 | description: 'description', 55 | target_url: 'url' 56 | }, 57 | { 58 | state: 'pending', 59 | description: 'other description' 60 | } 61 | ] 62 | })); 63 | setLatestPipelineStatus({ sha }); 64 | }); 65 | 66 | it('should call listDeployments with correct params', () => { 67 | expect(octokit.repos.listDeployments).toHaveBeenCalledWith({ 68 | environment: PRODUCTION_ENVIRONMENT, 69 | ...context.repo, 70 | ...GITHUB_OPTIONS 71 | }); 72 | }); 73 | 74 | it('should call listDeploymentStatuses with correct params', () => { 75 | expect(octokit.repos.listDeploymentStatuses).toHaveBeenCalledWith({ 76 | deployment_id, 77 | ...context.repo, 78 | ...GITHUB_OPTIONS 79 | }); 80 | }); 81 | 82 | it('should call createCommitStatus with correct params', () => { 83 | expect(octokit.repos.createCommitStatus).toHaveBeenCalledWith({ 84 | sha, 85 | context: DEFAULT_PIPELINE_STATUS, 86 | state: 'success', 87 | description: 'description', 88 | target_url: 'url', 89 | ...context.repo 90 | }); 91 | }); 92 | }); 93 | 94 | describe('deployment status not found', () => { 95 | beforeEach(() => { 96 | (octokit.repos.listDeploymentStatuses as unknown as Mocktokit).mockImplementation(async () => ({ 97 | data: [] 98 | })); 99 | setLatestPipelineStatus({ sha }); 100 | }); 101 | 102 | it('should call createCommitStatus with correct params', () => { 103 | expect(octokit.repos.createCommitStatus).toHaveBeenCalledWith({ 104 | sha, 105 | context: DEFAULT_PIPELINE_STATUS, 106 | state: 'pending', 107 | ...context.repo 108 | }); 109 | }); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /test/helpers/update-check-result.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { context } from '@actions/github'; 15 | import { updateCheckResult } from '../../src/helpers/update-check-result'; 16 | import { octokit } from '../../src/octokit'; 17 | 18 | jest.mock('@actions/core'); 19 | jest.mock('@actions/github', () => ({ 20 | context: { repo: { repo: 'repo', owner: 'owner' }, issue: { number: 123 } }, 21 | getOctokit: jest.fn(() => ({ 22 | rest: { 23 | checks: { 24 | listForRef: jest.fn(() => ({ data: { check_runs: [{ id: 123 }] } })), 25 | update: jest.fn() 26 | } 27 | } 28 | })) 29 | })); 30 | 31 | describe('updateCheckResult', () => { 32 | beforeEach(async () => { 33 | await updateCheckResult({ context: 'My PR Check', sha: 'sha', state: 'success' }); 34 | }); 35 | 36 | it('should call checks.update with correct params', () => { 37 | expect(octokit.checks.listForRef).toHaveBeenCalledWith({ 38 | ref: 'sha', 39 | check_name: 'My PR Check', 40 | ...context.repo 41 | }); 42 | }); 43 | 44 | it('should call checks.update with correct params', () => { 45 | expect(octokit.checks.update).toHaveBeenCalledWith({ 46 | check_run_id: 123, 47 | conclusion: 'success', 48 | output: { 49 | title: 'Check updated to success', 50 | summary: 'Check updated via update-check-result helper' 51 | }, 52 | ...context.repo 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /test/main.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import * as core from '@actions/core'; 15 | import * as helperModule from '../src/helpers/create-pr-comment'; 16 | import { getActionInputs } from '../src/utils/get-action-inputs'; 17 | import { getInput } from '@actions/core'; 18 | import { run } from '../src/main'; 19 | 20 | jest.mock('../src/helpers/create-pr-comment', () => { 21 | return { 22 | __esModule: true, 23 | ...jest.requireActual('../src/helpers/create-pr-comment') 24 | }; 25 | }); 26 | jest.mock('@actions/core'); 27 | jest.mock('@actions/github', () => ({ 28 | context: { repo: { repo: 'repo', owner: 'owner' } }, 29 | getOctokit: jest.fn(() => ({ rest: { issues: { createComment: jest.fn() } } })) 30 | })); 31 | jest.mock('../src/utils/get-action-inputs'); 32 | const helperSpy = jest.spyOn(helperModule, 'createPrComment'); 33 | const helper = 'create-pr-comment'; 34 | const otherInputs = { 35 | my: 'input', 36 | another: 'input' 37 | }; 38 | const output = 'some output'; 39 | (getInput as jest.Mock).mockReturnValue(helper); 40 | (getActionInputs as jest.Mock).mockReturnValue(otherInputs); 41 | (helperSpy as jest.Mock).mockResolvedValue(output); 42 | 43 | describe('main', () => { 44 | beforeEach(async () => { 45 | await run(); 46 | }); 47 | 48 | it('should call getActionInputs with correct params', () => { 49 | const requiredInputs = ['body']; 50 | expect(getActionInputs).toHaveBeenCalledWith(requiredInputs); 51 | }); 52 | 53 | it('should call helper with all inputs', () => { 54 | expect(helperSpy).toHaveBeenCalledWith(otherInputs); 55 | }); 56 | 57 | it('should set output', () => { 58 | expect(core.setOutput).toHaveBeenCalledWith('output', output); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /test/types.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { SimpleGit } from 'simple-git'; 15 | 16 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 17 | export type Mocktokit = jest.MockInstance; 18 | 19 | export type MockSimpleGit = jest.MockedFunction< 20 | () => jest.Mocked> 21 | > & { 22 | __mockGitInstance: jest.Mocked>; 23 | }; 24 | -------------------------------------------------------------------------------- /test/utils/get-action-inputs.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { getActionInputs } from '../../src/utils/get-action-inputs'; 15 | import { getInput } from '@actions/core'; 16 | import { getInputsFromFile } from '../../src/utils/get-inputs-from-file'; 17 | 18 | jest.mock('../../src/utils/get-inputs-from-file'); 19 | jest.mock('@actions/core', () => ({ 20 | getInput: jest.fn(input => (input === 'input2' ? '' : input)) 21 | })); 22 | jest.mock('fs', () => ({ 23 | promises: { 24 | access: jest.fn() 25 | }, 26 | readFileSync: jest.fn(() => ({ 27 | toString: jest.fn() 28 | })) 29 | })); 30 | 31 | describe('getActionInputs', () => { 32 | const requiredInputs = ['input1']; 33 | 34 | it('should call getInput with correct params and return expected inputs', () => { 35 | (getInputsFromFile as jest.Mock).mockReturnValue(['input1', 'input2', 'input3']); 36 | const result = getActionInputs(requiredInputs); 37 | 38 | expect(getInput).toHaveBeenCalledWith('input1', { required: true }); 39 | expect(getInput).toHaveBeenCalledWith('input2', { required: false }); 40 | expect(getInput).toHaveBeenCalledWith('input3', { required: false }); 41 | expect(result).toEqual({ 42 | input1: 'input1', 43 | input3: 'input3' 44 | }); 45 | }); 46 | 47 | it('should call getInput with trimWhiteSpace false for delimiter input', () => { 48 | (getInputsFromFile as jest.Mock).mockReturnValue(['input1', 'input2', 'delimiter']); 49 | const result = getActionInputs(requiredInputs); 50 | 51 | expect(getInput).toHaveBeenCalledWith('input1', { required: true }); 52 | expect(getInput).toHaveBeenCalledWith('input2', { required: false }); 53 | expect(getInput).toHaveBeenCalledWith('delimiter', { required: false, trimWhitespace: false }); 54 | expect(result).toEqual({ 55 | input1: 'input1', 56 | delimiter: 'delimiter' 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /test/utils/get-inputs-from-file.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { getInputsFromFile } from '../../src/utils/get-inputs-from-file'; 15 | 16 | describe('getInputsFromFile', () => { 17 | const yamlContents = ` 18 | name: Create PR Comment 19 | description: 'Creates a new issue comment for a pull request' 20 | inputs: 21 | input1: 22 | description: 'The github helper to invoke' 23 | required: true 24 | input2: 25 | description: 'The comment body' 26 | required: false 27 | `; 28 | 29 | it('should return expected inputs', () => { 30 | expect(getInputsFromFile(yamlContents)).toEqual(['input1', 'input2']); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/utils/notify-user.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Expedia, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { Mocktokit } from '../types'; 15 | import axios from 'axios'; 16 | import { context } from '@actions/github'; 17 | import { notifyUser } from '../../src/utils/notify-user'; 18 | import { octokit } from '../../src/octokit'; 19 | 20 | jest.mock('../../src/helpers/create-pr-comment'); 21 | jest.mock('@actions/core'); 22 | jest.mock('@actions/github', () => ({ 23 | context: { repo: { repo: 'repo', owner: 'owner' } }, 24 | getOctokit: jest.fn(() => ({ 25 | rest: { 26 | pulls: { get: jest.fn() }, 27 | users: { getByUsername: jest.fn() } 28 | } 29 | })) 30 | })); 31 | jest.mock('axios'); 32 | 33 | const login = 'octocat'; 34 | const assigneeEmail = 'assignee@github.com'; 35 | const title = 'title'; 36 | const html_url = 'url'; 37 | (octokit.pulls.get as unknown as Mocktokit).mockImplementation(async () => ({ 38 | data: { title, html_url } 39 | })); 40 | (axios.post as jest.Mock).mockResolvedValue({ data: 'request succeeded' }); 41 | 42 | describe('notifyUser', () => { 43 | const pull_number = 123; 44 | const slack_webhook_url = 'https://hooks.slack.com/workflows/1234567890'; 45 | beforeEach(async () => { 46 | (octokit.users.getByUsername as unknown as Mocktokit).mockImplementation(async () => ({ 47 | data: { 48 | email: assigneeEmail 49 | } 50 | })); 51 | await notifyUser({ login, pull_number, slack_webhook_url }); 52 | }); 53 | 54 | it('should call getByUsername with correct params', () => { 55 | expect(octokit.users.getByUsername).toHaveBeenCalledWith({ username: login }); 56 | }); 57 | 58 | it('should call pulls with correct params', () => { 59 | expect(octokit.pulls.get).toHaveBeenCalledWith({ pull_number: 123, ...context.repo }); 60 | }); 61 | 62 | it('should call axios with correct params', () => { 63 | expect(axios.post).toHaveBeenCalledWith(slack_webhook_url, { 64 | assignee: assigneeEmail, 65 | title, 66 | html_url, 67 | repo: context.repo.repo 68 | }); 69 | }); 70 | }); 71 | 72 | describe('notifyUser with a PR comment', () => { 73 | const pull_number = 123; 74 | const slack_webhook_url = 'https://hooks.slack.com/workflows/1234567890'; 75 | 76 | beforeEach(async () => { 77 | (octokit.users.getByUsername as unknown as Mocktokit).mockImplementation(async () => ({ 78 | data: { 79 | email: null 80 | } 81 | })); 82 | 83 | await notifyUser({ login, pull_number, slack_webhook_url }); 84 | }); 85 | 86 | it('should do nothing when email is not found', () => { 87 | expect(octokit.users.getByUsername).toHaveBeenCalledWith({ username: login }); 88 | expect(axios.post).not.toHaveBeenCalled(); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "noEmit": true, 6 | "strict": true, 7 | "noImplicitAny": true, 8 | "noUncheckedIndexedAccess": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "sourceMap": true, 12 | "moduleResolution": "node", 13 | "types": ["bun-types", "jest"] 14 | }, 15 | "exclude": ["node_modules"] 16 | } 17 | --------------------------------------------------------------------------------