├── .github └── stale.yml ├── .gitignore ├── LICENSE ├── README.md ├── fixtures └── installation-created.json ├── index.js ├── package.json └── test.js /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Configuration for https://github.com/probot/stale 2 | _extends: .github 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | package-lock.json 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2016, Brandon Keepers 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⚠️ This project is archived 2 | 3 | We recommend you use GitHub Actions with the [`schedule`](https://docs.github.com/en/actions/reference/events-that-trigger-workflows#scheduled-events) trigger instead, it's a much more reliably solution to the same problem. 4 | 5 | You can authenticate as your GitHub app when running code in a GitHub Action using the [`@octokit/auth-app`](https://github.com/octokit/auth-app.js/) authentication strategy. 6 | 7 | ## Probot: Scheduler 8 | 9 | [![npm](https://img.shields.io/npm/v/probot-scheduler.svg)](https://www.npmjs.com/package/probot-scheduler) 10 | 11 | A [Probot](https://github.com/probot/probot) extension to trigger events on an hourly schedule. 12 | 13 | ## Usage 14 | 15 | ```shell 16 | $ npm install probot-scheduler 17 | ``` 18 | 19 | ```js 20 | const createScheduler = require('probot-scheduler') 21 | 22 | module.exports = (robot) => { 23 | createScheduler(robot) 24 | robot.on('schedule.repository', context => { 25 | // this event is triggered on an interval, which is 1 hr by default 26 | }) 27 | } 28 | ``` 29 | 30 | ## Configuration 31 | 32 | There are a few environment variables that can change the behavior of the scheduler: 33 | 34 | - `DISABLE_DELAY=true` - Perform the schedule immediately on startup, instead of waiting for the random delay between 0 and 59:59 for each repository, which exists to avoid all schedules being performed at the same time. 35 | 36 | - `IGNORED_ACCOUNTS=comma,separated,list` - GitHub usernames to ignore when scheduling. These are typically spammy or abusive accounts. 37 | 38 | 39 | ## Options 40 | 41 | There are a few runtime options you can pass that can change the behavior of the scheduler: 42 | 43 | * `delay` - when `false`, the schedule will be performed immediately on startup. When `true`, there will be a random delay between 0 and `interval` for each repository to avoid all schedules being performed at the same time. Default: `true` unless the `DISABLE_DELAY` environment variable is set. 44 | 45 | * `interval` - the number of milliseconds to schedule each repository. Default: 1 hour (`60 * 60 * 1000`) 46 | 47 | For example, if you want your app to be triggered *once every day* with *delay enabled on first run*: 48 | 49 | ```js 50 | const createScheduler = require('probot-scheduler') 51 | 52 | module.exports = (robot) => { 53 | createScheduler(robot, { 54 | delay: !!process.env.DISABLE_DELAY, // delay is enabled on first run 55 | interval: 24 * 60 * 60 * 1000 // 1 day 56 | }) 57 | 58 | robot.on('schedule.repository', context => { 59 | // this event is triggered once every day, with a random delay 60 | }) 61 | } 62 | ``` 63 | -------------------------------------------------------------------------------- /fixtures/installation-created.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": "created", 3 | "installation": { 4 | "id": 2, 5 | "account": { 6 | "login": "octocat", 7 | "id": 1, 8 | "node_id": "MDQ6VXNlcjE=", 9 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 10 | "gravatar_id": "", 11 | "url": "https://api.github.com/users/octocat", 12 | "html_url": "https://github.com/octocat", 13 | "followers_url": "https://api.github.com/users/octocat/followers", 14 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 15 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 16 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 17 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 18 | "organizations_url": "https://api.github.com/users/octocat/orgs", 19 | "repos_url": "https://api.github.com/users/octocat/repos", 20 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 21 | "received_events_url": "https://api.github.com/users/octocat/received_events", 22 | "type": "User", 23 | "site_admin": false 24 | }, 25 | "repository_selection": "selected", 26 | "access_tokens_url": "https://api.github.com/installations/2/access_tokens", 27 | "repositories_url": "https://api.github.com/installation/repositories", 28 | "html_url": "https://github.com/settings/installations/2", 29 | "app_id": 5725, 30 | "target_id": 3880403, 31 | "target_type": "User", 32 | "permissions": { 33 | "metadata": "read", 34 | "contents": "read", 35 | "issues": "write" 36 | }, 37 | "events": [ 38 | "push", 39 | "pull_request" 40 | ], 41 | "created_at": 1525109898, 42 | "updated_at": 1525109899, 43 | "single_file_name": "config.yml" 44 | }, 45 | "repositories": [ 46 | { 47 | "id": 1296269, 48 | "name": "Hello-World", 49 | "full_name": "octocat/Hello-World", 50 | "private": false 51 | } 52 | ], 53 | "sender": { 54 | "login": "octocat", 55 | "id": 1, 56 | "node_id": "MDQ6VXNlcjE=", 57 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 58 | "gravatar_id": "", 59 | "url": "https://api.github.com/users/octocat", 60 | "html_url": "https://github.com/octocat", 61 | "followers_url": "https://api.github.com/users/octocat/followers", 62 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 63 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 64 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 65 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 66 | "organizations_url": "https://api.github.com/users/octocat/orgs", 67 | "repos_url": "https://api.github.com/users/octocat/repos", 68 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 69 | "received_events_url": "https://api.github.com/users/octocat/received_events", 70 | "type": "User", 71 | "site_admin": false 72 | } 73 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const Bottleneck = require('bottleneck') 2 | 3 | const limiter = new Bottleneck({ maxConcurrent: 1, minTime: 0 }) 4 | const ignoredAccounts = (process.env.IGNORED_ACCOUNTS || '') 5 | .toLowerCase() 6 | .split(',') 7 | 8 | const defaults = { 9 | delay: !process.env.DISABLE_DELAY, // Should the first run be put on a random delay? 10 | interval: 60 * 60 * 1000 // 1 hour 11 | } 12 | 13 | module.exports = (app, options) => { 14 | options = Object.assign({}, defaults, options || {}) 15 | const intervals = {} 16 | 17 | // https://developer.github.com/v3/activity/events/types/#installationrepositoriesevent 18 | app.on('installation.created', async event => { 19 | const installation = event.payload.installation 20 | 21 | eachRepository(installation, repository => { 22 | schedule(installation, repository) 23 | }) 24 | }) 25 | 26 | // https://developer.github.com/v3/activity/events/types/#installationrepositoriesevent 27 | app.on('installation_repositories.added', async event => { 28 | return setupInstallation(event.payload.installation) 29 | }) 30 | 31 | setup() 32 | 33 | function setup () { 34 | return eachInstallation(setupInstallation) 35 | } 36 | 37 | function setupInstallation (installation) { 38 | if (ignoredAccounts.includes(installation.account.login.toLowerCase())) { 39 | app.log.info({ installation }, 'Installation is ignored') 40 | return 41 | } 42 | 43 | limiter.schedule(eachRepository, installation, repository => { 44 | schedule(installation, repository) 45 | }) 46 | } 47 | 48 | function schedule (installation, repository) { 49 | if (intervals[repository.id]) { 50 | return 51 | } 52 | 53 | // Wait a random delay to more evenly distribute requests 54 | const delay = options.delay ? options.interval * Math.random() : 0 55 | 56 | app.log.debug({ repository, delay, interval: options.interval }, `Scheduling interval`) 57 | 58 | intervals[repository.id] = setTimeout(() => { 59 | const event = { 60 | name: 'schedule', 61 | payload: { action: 'repository', installation, repository } 62 | } 63 | 64 | // Trigger events on this repository on an interval 65 | intervals[repository.id] = setInterval( 66 | () => app.receive(event), 67 | options.interval 68 | ) 69 | 70 | // Trigger the first event now 71 | app.receive(event) 72 | }, delay) 73 | } 74 | 75 | async function eachInstallation (callback) { 76 | app.log.trace('Fetching installations') 77 | const github = await app.auth() 78 | 79 | const installations = await github.paginate( 80 | github.apps.listInstallations.endpoint.merge({ per_page: 100 }) 81 | ) 82 | 83 | const filteredInstallations = options.filter 84 | ? installations.filter(inst => options.filter(inst)) 85 | : installations 86 | return filteredInstallations.forEach(callback) 87 | } 88 | 89 | async function eachRepository (installation, callback) { 90 | app.log.trace({ installation }, 'Fetching repositories for installation') 91 | const github = await app.auth(installation.id) 92 | 93 | const repositories = await github.paginate( 94 | github.apps.listRepos.endpoint.merge({ per_page: 100 }), 95 | response => { 96 | return response.data.repositories 97 | } 98 | ) 99 | 100 | const filteredRepositories = options.filter 101 | ? repositories.filter(repo => options.filter(installation, repo)) 102 | : repositories 103 | 104 | return filteredRepositories.forEach(async repository => 105 | callback(repository, github) 106 | ) 107 | } 108 | 109 | function stop (repository) { 110 | app.log.info({ repository }, `Canceling interval`) 111 | 112 | clearInterval(intervals[repository.id]) 113 | } 114 | 115 | return { stop } 116 | } 117 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "probot-scheduler", 3 | "version": "2.0.0-beta.1", 4 | "description": "Probot extension to trigger events on a periodic schedule", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest && standard" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/probot/scheduler.git" 12 | }, 13 | "keywords": [ 14 | "probot" 15 | ], 16 | "author": "Brandon Keepers", 17 | "license": "ISC", 18 | "bugs": { 19 | "url": "https://github.com/probot/scheduler/issues" 20 | }, 21 | "homepage": "https://github.com/probot/scheduler#readme", 22 | "peerDependencies": { 23 | "probot": ">=9.2.0" 24 | }, 25 | "standard": { 26 | "env": [ 27 | "mocha" 28 | ] 29 | }, 30 | "dependencies": { 31 | "bottleneck": "^2.0.1" 32 | }, 33 | "devDependencies": { 34 | "jest": "^24.5.0", 35 | "nock": "^10.0.6", 36 | "probot": "^9.2.0", 37 | "standard": "^12.0.1" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | process.env.PRIVATE_KEY = 'testkey' 2 | 3 | const nock = require('nock') 4 | const createScheduler = require('./') 5 | const { Probot } = require('probot') 6 | 7 | const payload = require('./fixtures/installation-created.json') 8 | 9 | nock.disableNetConnect() 10 | 11 | const testApp = (app) => { 12 | createScheduler(app) 13 | 14 | app.on('schedule.repository', () => {}) 15 | } 16 | 17 | describe('Schedules intervals for a repository', () => { 18 | let probot 19 | let app 20 | 21 | beforeEach(() => { 22 | probot = new Probot({}) 23 | app = probot.load(testApp) 24 | 25 | app.app = () => 'test' 26 | }) 27 | 28 | it('gets a page of repositories', async (done) => { 29 | nock('https://api.github.com') 30 | .get('/app/installations') 31 | .query({ per_page: 1 }) 32 | .reply(200, [{ id: 1 }], { 33 | 'Link': '; rel="next"', 34 | 'X-GitHub-Media-Type': 'github.v3; format=json' 35 | }) 36 | .get('/installation/repositories') 37 | .query({ page: 2, per_page: 1 }) 38 | .reply(200, [{ id: 2 }]) 39 | .persist() 40 | 41 | await app.receive({ name: 'installation', payload }) 42 | await done() 43 | }) 44 | }) 45 | --------------------------------------------------------------------------------