├── .circleci └── config.yml ├── .env.example ├── .gitignore ├── LICENSE.txt ├── README.md ├── app.yml ├── index.js ├── lib ├── delete-old-unused-labels.js ├── handle-merge-failure.js ├── label-of-the-day.js ├── merge-scheduled-posts.js ├── pulls-with-label.js └── run.js ├── package-lock.json ├── package.json └── test ├── fauxbot.js ├── helper.js └── index.test.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/node:10 11 | 12 | # Specify service dependencies here if necessary 13 | # CircleCI maintains a library of pre-built images 14 | # documented at https://circleci.com/docs/2.0/circleci-images/ 15 | # - image: circleci/mongo:3.4.4 16 | 17 | working_directory: ~/repo 18 | 19 | steps: 20 | - checkout 21 | 22 | # Download and cache dependencies 23 | - restore_cache: 24 | keys: 25 | - v1-dependencies-{{ checksum "package.json" }} 26 | # fallback to using the latest cache if no exact match is found 27 | - v1-dependencies- 28 | 29 | - run: npm install 30 | 31 | - save_cache: 32 | paths: 33 | - node_modules 34 | key: v1-dependencies-{{ checksum "package.json" }} 35 | 36 | # run tests! 37 | - run: npm test 38 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # The ID of your GitHub App 2 | APP_ID= 3 | WEBHOOK_SECRET=development 4 | 5 | # Use `trace` to get verbose logging or `info` to show less 6 | LOG_LEVEL=debug 7 | 8 | # Go to https://smee.io/new set this to the URL that you are redirected to. 9 | WEBHOOK_PROXY_URL= 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .env 3 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Test Double, LLC. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # scheduled-merge 2 | 3 | A GitHub App built with [Probot](https://github.com/probot/probot) that Merges 4 | PRs on a specified date using Labels. 5 | 6 | Each time the app is run, will merge any open pull requests with a label 7 | matching pattern `merge-YYYY-MM-DD` (e.g. `merge-2019-05-07`). If automated 8 | merge fails, the app will leave a comment with the error. 9 | 10 | (The initial implementation is very naive, and will run 24 times (once per 11 | hour), which could lead to a lot of comments on a lot of PRs if they aren't 12 | mergeable!) 13 | 14 | To deploy your own instance, check out [the Probot docs](https://probot.github.io/docs/deployment/#heroku) 15 | -------------------------------------------------------------------------------- /app.yml: -------------------------------------------------------------------------------- 1 | # This is a GitHub App Manifest. These settings will be used by default when 2 | # initially configuring your GitHub App. 3 | # 4 | # NOTE: changing this file will not update your GitHub App settings. 5 | # You must visit github.com/settings/apps/your-app-name to edit them. 6 | # 7 | # Read more about configuring your GitHub App: 8 | # https://probot.github.io/docs/development/#configuring-a-github-app 9 | # 10 | # Read more about GitHub App Manifests: 11 | # https://developer.github.com/apps/building-github-apps/creating-github-apps-from-a-manifest/ 12 | 13 | # The list of events the GitHub App subscribes to. 14 | # Uncomment the event names below to enable them. 15 | default_events: [] 16 | # - check_run 17 | # - check_suite 18 | # - commit_comment 19 | # - create 20 | # - delete 21 | # - deployment 22 | # - deployment_status 23 | # - fork 24 | # - gollum 25 | # - issue_comment 26 | # - issues 27 | # - label 28 | # - milestone 29 | # - member 30 | # - membership 31 | # - org_block 32 | # - organization 33 | # - page_build 34 | # - project 35 | # - project_card 36 | # - project_column 37 | # - public 38 | # - pull_request 39 | # - pull_request_review 40 | # - pull_request_review_comment 41 | # - push 42 | # - release 43 | # - repository 44 | # - repository_import 45 | # - status 46 | # - team 47 | # - team_add 48 | # - watch 49 | 50 | # The set of permissions needed by the GitHub App. The format of the object uses 51 | # the permission name for the key (for example, issues) and the access type for 52 | # the value (for example, write). 53 | # Valid values are `read`, `write`, and `none` 54 | default_permissions: 55 | # Repository creation, deletion, settings, teams, and collaborators. 56 | # https://developer.github.com/v3/apps/permissions/#permission-on-administration 57 | # administration: read 58 | 59 | # Checks on code. 60 | # https://developer.github.com/v3/apps/permissions/#permission-on-checks 61 | # checks: read 62 | 63 | # Repository contents, commits, branches, downloads, releases, and merges. 64 | # https://developer.github.com/v3/apps/permissions/#permission-on-contents 65 | # contents: read 66 | 67 | # Deployments and deployment statuses. 68 | # https://developer.github.com/v3/apps/permissions/#permission-on-deployments 69 | # deployments: read 70 | 71 | # Issues and related comments, assignees, labels, and milestones. 72 | # https://developer.github.com/v3/apps/permissions/#permission-on-issues 73 | # issues: write 74 | 75 | # Search repositories, list collaborators, and access repository metadata. 76 | # https://developer.github.com/v3/apps/permissions/#metadata-permissions 77 | metadata: read 78 | 79 | # Retrieve Pages statuses, configuration, and builds, as well as create new builds. 80 | # https://developer.github.com/v3/apps/permissions/#permission-on-pages 81 | # pages: read 82 | 83 | # Pull requests and related comments, assignees, labels, milestones, and merges. 84 | # https://developer.github.com/v3/apps/permissions/#permission-on-pull-requests 85 | pull_requests: write 86 | 87 | # Manage the post-receive hooks for a repository. 88 | # https://developer.github.com/v3/apps/permissions/#permission-on-repository-hooks 89 | # repository_hooks: read 90 | 91 | # Manage repository projects, columns, and cards. 92 | # https://developer.github.com/v3/apps/permissions/#permission-on-repository-projects 93 | # repository_projects: read 94 | 95 | # Retrieve security vulnerability alerts. 96 | # https://developer.github.com/v4/object/repositoryvulnerabilityalert/ 97 | # vulnerability_alerts: read 98 | 99 | # Commit statuses. 100 | # https://developer.github.com/v3/apps/permissions/#permission-on-statuses 101 | # statuses: read 102 | 103 | # Organization members and teams. 104 | # https://developer.github.com/v3/apps/permissions/#permission-on-members 105 | # members: read 106 | 107 | # View and manage users blocked by the organization. 108 | # https://developer.github.com/v3/apps/permissions/#permission-on-organization-user-blocking 109 | # organization_user_blocking: read 110 | 111 | # Manage organization projects, columns, and cards. 112 | # https://developer.github.com/v3/apps/permissions/#permission-on-organization-projects 113 | # organization_projects: read 114 | 115 | # Manage team discussions and related comments. 116 | # https://developer.github.com/v3/apps/permissions/#permission-on-team-discussions 117 | # team_discussions: read 118 | 119 | # Manage the post-receive hooks for an organization. 120 | # https://developer.github.com/v3/apps/permissions/#permission-on-organization-hooks 121 | # organization_hooks: read 122 | 123 | # Get notified of, and update, content references. 124 | # https://developer.github.com/v3/apps/permissions/ 125 | # organization_administration: read 126 | 127 | 128 | # The name of the GitHub App. Defaults to the name specified in package.json 129 | # name: My Probot App 130 | 131 | # The homepage of your GitHub App. 132 | # url: https://example.com/ 133 | 134 | # A description of the GitHub App. 135 | # description: A description of my awesome app 136 | 137 | # Set to true when your GitHub App is available to the public or false when it is only accessible to the owner of the app. 138 | # Default: true 139 | # public: false 140 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const probotScheduler = require('probot-scheduler') 2 | 3 | const run = require('./lib/run') 4 | 5 | module.exports = app => { 6 | probotScheduler(app, { 7 | delay: process.env.NODE_ENV === 'production' 8 | }) 9 | app.on('schedule.repository', function (context) { 10 | const { owner, repo } = context.repo({ logger: app.log }) 11 | return run({ 12 | owner, 13 | repo, 14 | github: context.github, 15 | log: app.log 16 | }) 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /lib/delete-old-unused-labels.js: -------------------------------------------------------------------------------- 1 | const labelOfTheDay = require('./label-of-the-day') 2 | const pullsWithLabel = require('./pulls-with-label') 3 | 4 | module.exports = async function ({ github, owner, repo, log }) { 5 | const todaysLabelName = labelOfTheDay() 6 | const labels = await repoLabels({ github, owner, repo, log }) 7 | const oldMergeLabels = (labels || []).filter((label) => ( 8 | label.name.startsWith('merge-') && label.name < todaysLabelName 9 | )) 10 | 11 | const oldMergeLabelPulls = await Promise.all(oldMergeLabels.map((label) => { 12 | return pullsWithLabel({ labelName: label.name, github, owner, repo, log }) 13 | })) 14 | 15 | const oldUnusedMergeLabels = oldMergeLabels.filter((label, index) => { 16 | const pulls = oldMergeLabelPulls[index].data 17 | return (!pulls || !pulls.length) 18 | }) 19 | 20 | return Promise.all(oldUnusedMergeLabels.map((label) => { 21 | return deleteLabel({ labelName: label.name, github, owner, repo, log }) 22 | })) 23 | } 24 | 25 | function repoLabels ({ github, owner, repo, log }) { 26 | log.info(`Listing all labels for ${owner}/${repo}`) 27 | return github.paginate(github.issues.listLabelsForRepo.endpoint.merge({ 28 | owner, repo 29 | })).catch(() => { log.info('Error fetching labels') }) 30 | } 31 | 32 | function deleteLabel ({ labelName, github, owner, repo, log }) { 33 | log.info(`Deleting label ${labelName}`) 34 | return github.issues.deleteLabel({ 35 | owner, repo, name: labelName 36 | }).catch(() => { log.info(`Deletion of label ${labelName} failed`) }) 37 | } 38 | -------------------------------------------------------------------------------- /lib/handle-merge-failure.js: -------------------------------------------------------------------------------- 1 | module.exports = async function ({ error, pull, github, owner, repo }) { 2 | await ensureFailureLabelExists({ github, owner, repo }) 3 | await assignLabel({ pull, github, owner, repo }) 4 | return addComment({ error, pull, github, owner, repo }) 5 | } 6 | 7 | function ensureFailureLabelExists ({ github, owner, repo }) { 8 | return github.issues.getLabel({ owner, repo, name: 'merge-failed' }).catch(() => { 9 | return github.issues.createLabel({ owner, repo, name: 'merge-failed', color: 'cc0000' }) 10 | }) 11 | } 12 | 13 | function assignLabel ({ pull, github, owner, repo }) { 14 | return github.issues.addLabels({ 15 | owner, 16 | repo, 17 | issue_number: pull.number, 18 | labels: ['merge-failed'] 19 | }) 20 | } 21 | 22 | function addComment ({ error, pull, github, owner, repo }) { 23 | return github.issues.createComment({ 24 | owner, 25 | repo, 26 | issue_number: pull.number, 27 | body: `Failed to automatically merge with error: **${error.message}**` 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /lib/label-of-the-day.js: -------------------------------------------------------------------------------- 1 | // Ripped right off SO: 2 | // https://stackoverflow.com/questions/23593052/format-javascript-date-to-yyyy-mm-dd 3 | module.exports = function () { 4 | const d = new Date() 5 | let month = String(d.getMonth() + 1) 6 | let day = String(d.getDate()) 7 | 8 | if (month.length < 2) month = '0' + month 9 | if (day.length < 2) day = '0' + day 10 | 11 | return `merge-${d.getFullYear()}-${month}-${day}` 12 | } 13 | -------------------------------------------------------------------------------- /lib/merge-scheduled-posts.js: -------------------------------------------------------------------------------- 1 | const labelOfTheDay = require('./label-of-the-day') 2 | const handleMergeFailure = require('./handle-merge-failure') 3 | const pullsWithLabel = require('./pulls-with-label') 4 | 5 | module.exports = async function ({ github, owner, repo, log }) { 6 | const label = await findTodaysLabel({ github, owner, repo, log }) 7 | if (label) { 8 | const pulls = await pullsWithLabel({ 9 | labelName: label.data.name, 10 | github, 11 | owner, 12 | repo, 13 | log 14 | }) 15 | await Promise.all(pulls.data.map(async pull => { 16 | return mergePull({ pull, github, owner, repo, log }) 17 | })) 18 | } 19 | } 20 | 21 | function findTodaysLabel ({ github, owner, repo, log }) { 22 | const labelName = labelOfTheDay() 23 | log.info(`Searching for open PRs for ${owner}/${repo} with label ${labelName}`) 24 | return github.issues.getLabel({ 25 | owner, repo, name: labelName 26 | }).catch(() => { log.info(`No label named ${labelName}`) }) 27 | } 28 | 29 | async function mergePull ({ pull, github, owner, repo, log }) { 30 | log.info(`Merging PR: ${pull.url}`) 31 | if (pull.labels.find(l => l.name === 'merge-failed')) { 32 | log.info('Skipping because `merge-failed` label was applied.') 33 | return false 34 | } else { 35 | return github.pulls.merge({ 36 | owner, 37 | repo, 38 | pull_number: pull.number 39 | }).catch(error => { 40 | return handleMergeFailure({ error, pull, github, owner, repo, log }) 41 | }) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/pulls-with-label.js: -------------------------------------------------------------------------------- 1 | module.exports = async function pullsWithLabel ({ labelName, github, owner, repo, log }) { 2 | log.info(`Searching for PRs labeled ${labelName}`) 3 | return github.issues.listForRepo({ 4 | owner, repo, labels: labelName, state: 'open' 5 | }).catch(() => { log.info('No open PRs found') }) 6 | } 7 | -------------------------------------------------------------------------------- /lib/run.js: -------------------------------------------------------------------------------- 1 | const deleteOldUnusedLabels = require('./delete-old-unused-labels') 2 | const mergeScheduledPosts = require('./merge-scheduled-posts') 3 | 4 | module.exports = function ({ github, owner, repo, log }) { 5 | return Promise.all([ 6 | deleteOldUnusedLabels({ github, owner, repo, log }), 7 | mergeScheduledPosts({ github, owner, repo, log }) 8 | ]) 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scheduled-merge", 3 | "version": "1.0.0", 4 | "description": "Merge PRs on a specified date using Labels", 5 | "author": "Justin Searls (https://github.com/testdouble/scheduled-merge)", 6 | "license": "MIT", 7 | "repository": "https://github.com//scheduled-merge.git", 8 | "homepage": "https://github.com//scheduled-merge", 9 | "bugs": "https://github.com//scheduled-merge/issues", 10 | "keywords": [ 11 | "probot", 12 | "github", 13 | "probot-app", 14 | "scheduled posts" 15 | ], 16 | "scripts": { 17 | "dev": "nodemon", 18 | "start": "probot run ./index.js", 19 | "debug": "node --inspect-brk node_modules/probot/bin/probot.js run ./index.js", 20 | "lint": "standard --fix", 21 | "test": "teenytest && standard" 22 | }, 23 | "dependencies": { 24 | "probot": "^9.11.5", 25 | "probot-scheduler": "^2.0.0-beta.1" 26 | }, 27 | "devDependencies": { 28 | "nock": "^13.0.2", 29 | "nodemon": "^2.0.4", 30 | "ought": "0.0.6", 31 | "smee-client": "^1.1.0", 32 | "standard": "^14.3.4", 33 | "teenytest": "^6.0.2", 34 | "teenytest-promise": "^1.0.0", 35 | "testdouble": "^3.16.1", 36 | "testdouble-jest": "^2.0.0" 37 | }, 38 | "engines": { 39 | "node": ">= 8.3.0" 40 | }, 41 | "standard": { 42 | "globals": [ 43 | "td", 44 | "nock", 45 | "ought" 46 | ] 47 | }, 48 | "nodemonConfig": { 49 | "exec": "npm start", 50 | "watch": [ 51 | ".env", 52 | "." 53 | ] 54 | }, 55 | "teenytest": { 56 | "testLocator": "test/*.test.js", 57 | "helper": "test/helper.js", 58 | "timeout": 1000, 59 | "plugins": [ 60 | "teenytest-promise" 61 | ] 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /test/fauxbot.js: -------------------------------------------------------------------------------- 1 | const { Probot } = require('probot') 2 | const octokit = require('@octokit/rest') 3 | 4 | module.exports = class Fauxbot { 5 | constructor ({ appFn, probotScheduler }) { 6 | this.probot = new Probot({ 7 | Octokit: octokit, // prevent retries 8 | githubToken: 'test' // make probot-scheduler work 9 | }) 10 | this.probotScheduler = probotScheduler 11 | this.app = this.probot.load(appFn) 12 | } 13 | 14 | trigger () { 15 | return this.probot.receive({ 16 | name: 'schedule.repository', 17 | payload: { 18 | repository: { 19 | name: 'stuff', 20 | owner: { 21 | login: 'fake' 22 | } 23 | } 24 | } 25 | }) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/helper.js: -------------------------------------------------------------------------------- 1 | global.td = require('testdouble') 2 | global.nock = require('nock') 3 | global.ought = require('ought') 4 | 5 | module.exports = { 6 | beforeAll () { 7 | nock.disableNetConnect() 8 | 9 | process.on('unhandledRejection', error => { 10 | process.stderr.write(error.stack) 11 | process.exit(1) 12 | }) 13 | }, 14 | 15 | beforeEach () { 16 | nock.cleanAll() 17 | }, 18 | 19 | afterEach () { 20 | ought.equal(nock.pendingMocks(), []) 21 | td.reset() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | const labelOfTheDay = require('../lib/label-of-the-day') 2 | const Fauxbot = require('./fauxbot') 3 | 4 | let fauxbot, label, api 5 | module.exports = { 6 | beforeEach () { 7 | api = nock('https://api.github.com') 8 | fauxbot = new Fauxbot({ 9 | probotScheduler: td.replace('probot-scheduler'), 10 | appFn: require('..') 11 | }) 12 | label = labelOfTheDay() 13 | }, 14 | 15 | async 'starts probot-scheduler' () { 16 | td.verify(fauxbot.probotScheduler(fauxbot.app, { 17 | delay: false 18 | })) 19 | }, 20 | 21 | 'merge-scheduled-posts': { 22 | async 'does nothing when no label is found' () { 23 | api.get(`/repos/fake/stuff/labels/${label}`) 24 | .reply(404, { 25 | message: 'Not Found', 26 | documentation_url: 'https://developer.github.com/v3/issues/labels/#get-a-single-label' 27 | }) 28 | 29 | await fauxbot.trigger() 30 | }, 31 | 32 | async 'does nothing when label is found but no pulls are open' () { 33 | api.get(`/repos/fake/stuff/labels/${label}`).reply(200, { name: label }) 34 | 35 | api.get('/repos/fake/stuff/issues') 36 | .query({ labels: label, state: 'open' }) 37 | .reply(200, []) 38 | 39 | await fauxbot.trigger() 40 | }, 41 | 42 | async 'does nothing when `merge-failed` label is applied' () { 43 | api.get(`/repos/fake/stuff/labels/${label}`).reply(200, { name: label }) 44 | 45 | api.get('/repos/fake/stuff/issues') 46 | .query({ labels: label, state: 'open' }) 47 | .reply(200, [{ 48 | url: 'fake pull url', 49 | number: 999, 50 | labels: [{ name: label }, { name: 'merge-failed' }], 51 | state: 'open' 52 | }]) 53 | 54 | await fauxbot.trigger() 55 | }, 56 | 57 | async 'merges PR when labeled, open, and mergeable' () { 58 | api.get(`/repos/fake/stuff/labels/${label}`).reply(200, { name: label }) 59 | 60 | api.get('/repos/fake/stuff/issues') 61 | .query({ labels: label, state: 'open' }) 62 | .reply(200, [{ 63 | url: 'fake pull url', 64 | number: 999, 65 | labels: [{ name: label }], 66 | state: 'open' 67 | }]) 68 | 69 | api.put('/repos/fake/stuff/pulls/999/merge').reply(200) 70 | 71 | await fauxbot.trigger() 72 | }, 73 | 74 | async 'leaves a comment when a PR is not mergeable & labels merge-failed' () { 75 | api.get(`/repos/fake/stuff/labels/${label}`).reply(200, { name: label }) 76 | 77 | api.get('/repos/fake/stuff/issues') 78 | .query({ labels: label, state: 'open' }) 79 | .reply(200, [{ number: 999, labels: [] }]) 80 | 81 | api.put('/repos/fake/stuff/pulls/999/merge') 82 | .reply(405, { message: 'Pull Request is not mergeable' }) 83 | 84 | api.get('/repos/fake/stuff/labels/merge-failed').reply(404) 85 | 86 | api.post('/repos/fake/stuff/labels', { 87 | name: 'merge-failed', 88 | color: 'cc0000' 89 | }).reply(201, { name: 'merge-failed' }) 90 | 91 | api.post('/repos/fake/stuff/issues/999/labels', { labels: ['merge-failed'] }) 92 | .reply(200) 93 | 94 | api.post('/repos/fake/stuff/issues/999/comments', { 95 | body: 'Failed to automatically merge with error: **Pull Request is not mergeable**' 96 | }).reply(201) 97 | 98 | await fauxbot.trigger() 99 | } 100 | }, 101 | 102 | 'delete-old-unused-labels': { 103 | async 'does nothing when there are no old merge labels' () { 104 | api.get('/repos/fake/stuff/labels') 105 | .reply(200, [ 106 | { name: 'merge-failed' }, 107 | { name: 'merge-2100-01-01' }, 108 | { name: 'some other label' } 109 | ]) 110 | 111 | await fauxbot.trigger() 112 | }, 113 | async 'does nothing when old merge labels are applied to open PRs' () { 114 | const oldLabel = 'merge-2003-03-03' 115 | 116 | api.get('/repos/fake/stuff/labels') 117 | .reply(200, [{ name: oldLabel }]) 118 | 119 | api.get('/repos/fake/stuff/issues') 120 | .query({ labels: oldLabel, state: 'open' }) 121 | .reply(200, [{ number: 999, labels: [] }]) 122 | 123 | await fauxbot.trigger() 124 | }, 125 | async 'deletes old merge labels that have no open PRs' () { 126 | api.get('/repos/fake/stuff/labels') 127 | .reply( 128 | 200, 129 | [{ name: 'merge-2003-03-03' }], 130 | { Link: '; rel="next", ; rel="last"' } 131 | ) 132 | api.get('/repos/fake/stuff/labels?page=2') 133 | .reply( 134 | 200, 135 | [{ name: 'merge-2003-03-04' }], 136 | { Link: '; rel="prev", ; rel="next", ; rel="last", ; rel="first"' } 137 | ) 138 | api.get('/repos/fake/stuff/labels?page=3') 139 | .reply( 140 | 200, 141 | [{ name: 'merge-2003-03-05' }], 142 | { Link: '; rel="prev", ; rel="first"' } 143 | ) 144 | 145 | api.get('/repos/fake/stuff/issues') 146 | .query({ labels: 'merge-2003-03-03', state: 'open' }) 147 | .reply(200, []) 148 | api.get('/repos/fake/stuff/issues') 149 | .query({ labels: 'merge-2003-03-04', state: 'open' }) 150 | .reply(200, [{ number: 999, labels: [] }]) 151 | api.get('/repos/fake/stuff/issues') 152 | .query({ labels: 'merge-2003-03-05', state: 'open' }) 153 | .reply(200, []) 154 | 155 | api.delete('/repos/fake/stuff/labels/merge-2003-03-03') 156 | .reply(204) 157 | api.delete('/repos/fake/stuff/labels/merge-2003-03-05') 158 | .reply(204) 159 | 160 | await fauxbot.trigger() 161 | } 162 | } 163 | } 164 | --------------------------------------------------------------------------------