UNPKG

wuffle

Version:

A multi-repository task board for GitHub issues

265 lines (210 loc) 5.66 kB
const PermissionLevels = { read: 1, write: 2, none: 0 }; const RequiredPermissions = { checks: 'read', contents: 'read', issues: 'write', metadata: 'read', pull_requests: 'write', statuses: 'read' }; const RequiredEvents = [ 'check_run', 'create', 'issues', 'issue_comment', 'label', 'milestone', 'pull_request', 'pull_request_review', 'repository', 'status' ]; /** * @typedef {import('./types.js').Installation} Installation */ /** * This component validates and exposes * installations of the GitHub app. * * @constructor * * @param {Object} config * @param {import('../../types.js').ProbotApp} app * @param {import('../../types.js').Logger} logger * @param {import('../../types.js').Injector} injector */ export default function GithubApp(config, app, logger, injector) { const log = logger.child({ name: 'wuffle:github-app' }); const { allowedOrgs } = config; // cached data ////////////////// let installations = null; let installationsByLogin = null; let installationsById = null; // reactivity //////////////////// injector.get('webhookEvents').then(webhookEvents => { webhookEvents.on('installation', () => { installations = null; installationsByLogin = null; installationsById = null; }); }); // functionality ///////////////// /** * @return {Promise<import('../../types.js').Octokit>} */ function getAppScopedClient() { return app.auth(); } function getInstallations() { installations = installations || fetchInstallations().then(installations => { const enabledInstallations = installations.filter(i => isLoginEnabled(i.account.login)); validateInstallations(enabledInstallations); return enabledInstallations; }); return installations; } function getInstallationsById() { installationsById = installationsById || getInstallations().then( installations => installations.reduce((byId, installation) => { byId[installation.id] = installation; return byId; }, {}) ); return installationsById; } /** * Get an installation for the given id. * * @param {string} id * * @return {Promise<Installation?>} */ function getInstallationById(id) { return getInstallationsById().then(byId => { return byId[id]; }); } /** * Return map of installations, grouped by org / login. * * @return {Promise<Object<String, Installation>>} */ function getInstallationsByLogin() { installationsByLogin = installationsByLogin || getInstallations().then( installations => installations.reduce((byLogin, installation) => { byLogin[installation.account.login] = installation; return byLogin; }, {}) ); return installationsByLogin; } /** * Get an installation for the given login. * * This method throws if an installation for the given login does not exist. * * @param {string} login * * @return {Promise<Installation>} */ function getInstallationByLogin(login) { return getInstallationsByLogin().then(byLogin => { const installation = byLogin[login]; if (!installation) { throw new Error('not installed for ' + login); } return installation; }); } function isInstallationEnabled(installation) { const { id: installation_id } = installation; return getInstallationById(installation_id).then(installation => !!installation); } /** * @param {string} login * * @return {boolean} */ function isLoginEnabled(login) { if (allowedOrgs) { return allowedOrgs.some(org => org === login); } return true; } /** * @param {string} requested * @param {string} actual * * @return {boolean} */ function isRequiredLevel(requested, actual) { return PermissionLevels[requested] <= PermissionLevels[actual || 'none']; } function validateInstallation(installation) { const { account, permissions, events } = installation; const { login } = account; const missingPermissions = Object.entries(RequiredPermissions).filter( ([ permission, requestedLevel ]) => { const actualLevel = permissions[permission]; return !isRequiredLevel(actualLevel, requestedLevel); } ); const missingEvents = RequiredEvents.filter( (event) => !events.includes(event) ); if (missingPermissions.length) { log.warn({ installation: login, permissions, missingPermissions }, 'missing required permissions'); } if (missingEvents.length) { log.warn({ installation: login, events, missingEvents }, 'missing required event subscriptions'); } } function validateInstallations(installations) { log.debug('validate installations'); installations.map(validateInstallation); log.debug('validated installations'); } /** * Fetch active installations. * * @return {Promise<Array<Installation>>} installations */ function fetchInstallations() { return /** @type Promise<Array<Installation>> */ (getAppScopedClient().then( octokit => octokit.paginate( octokit.apps.listInstallations, { per_page: 100 } ) )); } // api /////////////////// this.getAppScopedClient = getAppScopedClient; this.isInstallationEnabled = isInstallationEnabled; this.getInstallationByLogin = getInstallationByLogin; this.getInstallationById = getInstallationById; this.getInstallations = getInstallations; }