From 9640e846e81efe3936f7ccb3f8109b0122f907e2 Mon Sep 17 00:00:00 2001 From: NuclearRedeye Date: Sun, 21 Mar 2021 21:41:16 +0000 Subject: [PATCH] feat: initial commit of really crude labelling script --- package-lock.json | 140 +++++++++++++++++ package.json | 23 +++ utils/repo-labels/labels.js | 245 +++++++++++++++++++++++++++++ utils/repo-labels/labels.md | 55 +++++++ utils/repo-labels/update-labels.js | 161 +++++++++++++++++++ 5 files changed, 624 insertions(+) create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 utils/repo-labels/labels.js create mode 100644 utils/repo-labels/labels.md create mode 100644 utils/repo-labels/update-labels.js diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..83a7d29 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,140 @@ +{ + "name": "tech-team", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@octokit/auth-token": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.4.5.tgz", + "integrity": "sha512-BpGYsPgJt05M7/L/5FoE1PiAbdxXFZkX/3kDYcsvd1v6UhlnE5e96dTDr0ezX/EFwciQxf3cNV0loipsURU+WA==", + "dev": true, + "requires": { + "@octokit/types": "^6.0.3" + } + }, + "@octokit/core": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.2.5.tgz", + "integrity": "sha512-+DCtPykGnvXKWWQI0E1XD+CCeWSBhB6kwItXqfFmNBlIlhczuDPbg+P6BtLnVBaRJDAjv+1mrUJuRsFSjktopg==", + "dev": true, + "requires": { + "@octokit/auth-token": "^2.4.4", + "@octokit/graphql": "^4.5.8", + "@octokit/request": "^5.4.12", + "@octokit/types": "^6.0.3", + "before-after-hook": "^2.1.0", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/endpoint": { + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.11.tgz", + "integrity": "sha512-fUIPpx+pZyoLW4GCs3yMnlj2LfoXTWDUVPTC4V3MUEKZm48W+XYpeWSZCv+vYF1ZABUm2CqnDVf1sFtIYrj7KQ==", + "dev": true, + "requires": { + "@octokit/types": "^6.0.3", + "is-plain-object": "^5.0.0", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/graphql": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.6.0.tgz", + "integrity": "sha512-CJ6n7izLFXLvPZaWzCQDjU/RP+vHiZmWdOunaCS87v+2jxMsW9FB5ktfIxybRBxZjxuJGRnxk7xJecWTVxFUYQ==", + "dev": true, + "requires": { + "@octokit/request": "^5.3.0", + "@octokit/types": "^6.0.3", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/openapi-types": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-4.0.4.tgz", + "integrity": "sha512-31zY8JIuz3h6RAFOnyA8FbOwhILILiBu1qD81RyZZWY7oMBhIdBn6MaAmnnptLhB4jk0g50nkQkUVP4kUzppcA==", + "dev": true + }, + "@octokit/request": { + "version": "5.4.14", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.4.14.tgz", + "integrity": "sha512-VkmtacOIQp9daSnBmDI92xNIeLuSRDOIuplp/CJomkvzt7M18NXgG044Cx/LFKLgjKt9T2tZR6AtJayba9GTSA==", + "dev": true, + "requires": { + "@octokit/endpoint": "^6.0.1", + "@octokit/request-error": "^2.0.0", + "@octokit/types": "^6.7.1", + "deprecation": "^2.0.0", + "is-plain-object": "^5.0.0", + "node-fetch": "^2.6.1", + "once": "^1.4.0", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/request-error": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.0.5.tgz", + "integrity": "sha512-T/2wcCFyM7SkXzNoyVNWjyVlUwBvW3igM3Btr/eKYiPmucXTtkxt2RBsf6gn3LTzaLSLTQtNmvg+dGsOxQrjZg==", + "dev": true, + "requires": { + "@octokit/types": "^6.0.3", + "deprecation": "^2.0.0", + "once": "^1.4.0" + } + }, + "@octokit/types": { + "version": "6.8.5", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.8.5.tgz", + "integrity": "sha512-ZsQawftZoi0kSF2pCsdgLURbOjtVcHnBOXiSxBKSNF56CRjARt5rb/g8WJgqB8vv4lgUEHrv06EdDKYQ22vA9Q==", + "dev": true, + "requires": { + "@octokit/openapi-types": "^4.0.3" + } + }, + "before-after-hook": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.1.1.tgz", + "integrity": "sha512-5ekuQOvO04MDj7kYZJaMab2S8SPjGJbotVNyv7QYFCOAwrGZs/YnoDNlh1U+m5hl7H2D/+n0taaAV/tfyd3KMA==", + "dev": true + }, + "deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", + "dev": true + }, + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true + }, + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "universal-user-agent": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", + "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..35a95cb --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "tech-team", + "version": "1.0.0", + "description": "Everything about the tech team, from team-wide [ADR](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions) that do not fit into single projects, to incident post-mortems.", + "main": "index.js", + "type": "module", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/elifesciences/tech-team.git" + }, + "author": "", + "license": "MIT", + "bugs": { + "url": "https://github.com/elifesciences/tech-team/issues" + }, + "homepage": "https://github.com/elifesciences/tech-team#readme", + "devDependencies": { + "@octokit/core": "^3.2.5" + } +} diff --git a/utils/repo-labels/labels.js b/utils/repo-labels/labels.js new file mode 100644 index 0000000..780069a --- /dev/null +++ b/utils/repo-labels/labels.js @@ -0,0 +1,245 @@ +export const types = { + prefix: 'type', + labels: [ + { + name: 'bug', + description: 'Something that is not working or working incorrectly', + color: 'd73a4a', + match: [':label: bug'] + }, + { + name: 'critical', + description: 'A serious issue that is causing downtime or degraded performance', + color: 'd73a4a', + match: [] + }, + { + name: 'security', + description: 'A potential security vulnerability', + color: 'd73a4a', + match: [] + }, + { + name: 'feature', + description: 'A major feature or improvement', + color: '3e4b9e', + match: ['feature-request', 'Epic'] + }, + { + name: 'enhancement', + description: 'A minor improvement or tweak', + color: '3e4b9e', + match: [] + }, + { + name: 'task', + description: 'An incremental step towards completing a larger goal', + color: '3e4b9e', + match: [] + }, + { + name: 'spike', + description: 'A time-boxed investigation to learn more about a problem or solution', + color: '3e4b9e', + match: [] + }, + { + name: 'chore', + description: 'A trivial or repetative task that requires time but little knowledege', + color: 'BFBFA3', + match: [] + }, + { + name: 'documentation', + description: 'Improvements or additions to documentation', + color: 'BFBFA3', + match: ['docs'] + } + ] +}; + +export const skills = { + prefix: 'skill', + labels: [ + { + name: 'html', + description: 'Solution requires expertise in HTML', + color: '9448dc', + match: ['HTML'] + }, + { + name: 'css', + description: 'Solution requires expertise in CSS', + color: '9448dc', + match: ['CSS'] + }, + { + name: 'javascript', + description: 'Solution requires expertise in JavaScript', + color: '9448dc', + match: ['js'] + }, + { + name: 'typescript', + description: 'Solution requires expertise in TypeScript', + color: '9448dc', + match: ['ts'] + }, + { + name: 'python', + description: 'Solution requires expertise in Python', + color: '9448dc', + match: ['py'] + }, + { + name: 'php', + description: 'Solution requires expertise in PHP', + color: '9448dc', + match: ['PHP'] + }, + { + name: 'docker', + description: 'Solution requires expertise in Docker', + color: '9448dc', + match: [] + }, + { + name: 'jenkins', + description: 'Solution requires expertise in Jenkins', + color: '9448dc', + match: [] + }, + { + name: 'aws', + description: 'Solution requires expertise in Amazon Web Services', + color: '9448dc', + match: [] + }, + { + name: 'kubernetes', + description: 'Solution requires expertise in Kubernetes', + color: '9448dc', + match: [] + }, + { + name: 'salt', + description: 'Solution requires expertise in SaltStack', + color: '9448dc', + match: [] + }, + ] +}; + +export const status = { + prefix: 'status', + labels: [ + { + name: 'blocked', + description: 'Progress on this issue currently impeeded', + color: '000000', + match: ['blocked'] + }, + { + name: 'upstream', + description: 'Progress on this issue currently impeeded due to an upstream dependency', + color: '000000', + match: [] + }, + { + name: 'discussion', + description: 'Currently under debate in order to reach a decision or to exchange ideas', + color: 'd876e3', + match: [] + }, + { + name: 'question', + description: 'Further information is requested or required', + color: 'd876e3', + match: [] + }, + { + name: 'discovery', + description: 'Requires further investigation before being ready to work', + color: 'fbca04', + match: [] + }, + { + name: 'ready to work', + description: 'Issue is well defined, fully scoped and is ready to be worked on', + color: '0e8a16', + match: [] + }, + { + name: 'unplanned', + description: 'Not originally planned for the current sprint, but needs to be addressed', + color: 'fbca04', + match: [] + }, + { + name: 'backlog', + description: 'Work pulled into the current sprint from the backlog', + color: '1d76db', + match: [] + }, + ] +}; + +export const stakeholders = { + prefix: 'stakeholder', + labels: [ + { + name: 'technology', + description: 'Requested or required by the Technology team', + color: 'f48f13', + match: ['engineering', 'tech'] + }, + { + name: 'editorial', + description: 'Requested or required by the Editorial team', + color: 'f48f13', + match: [] + }, + { + name: 'production', + description: 'Requested or required by the Production team', + color: 'f48f13', + match: [] + }, + { + name: 'marketing', + description: 'Requested or required by the Marketing team', + color: 'f48f13', + match: [] + }, + { + name: 'finance', + description: 'Requested or required by the Finance team', + color: 'f48f13', + match: [] + }, + { + name: 'product', + description: 'Requested or required by the Product team', + color: 'f48f13', + match: [] + }, + ] +}; + +export const goals = { + prefix: 'goal', + labels: [ + { + name: 'maintenance', + description: 'Ensure the current state and functionality is preserved', + color: '76D7C4', + match: [] + }, + { + name: 'submissions', + description: 'Attract between 2500 and 3500 submissions per quarter', + color: '76D7C4', + match: [] + }, + ] +} \ No newline at end of file diff --git a/utils/repo-labels/labels.md b/utils/repo-labels/labels.md new file mode 100644 index 0000000..0b16c8a --- /dev/null +++ b/utils/repo-labels/labels.md @@ -0,0 +1,55 @@ +# Types +- security +- bug +- feature +- enhancement +- task +- spike +- chore +- documentation + +# Priority +- retrospective + +# Environments +- production +- staging +- development + +# Skills +- css +- html +- javascript +- typescript +- python +- php +- groovy +- shell + +# Status +- blocked +- upstream +- discussion +- question +- discovery +- ready to work +- unplanned +- backlog + +# Stakeholders +- engineering +- editorial +- production +- marketing +- finance +- product + +# Goals +- sustainability +- readership +- influence +- prc + +# Project +- article-store +- editor-client \ No newline at end of file diff --git a/utils/repo-labels/update-labels.js b/utils/repo-labels/update-labels.js new file mode 100644 index 0000000..da64fc4 --- /dev/null +++ b/utils/repo-labels/update-labels.js @@ -0,0 +1,161 @@ +// Quick and dirty program to update labels in GitHub repos... +// TODO: +// - Tidy the code up, convert to TypeScript. +// - Catch errors. +// - Document. +// - Add usage. +// - Update README. +// - Add to CI. + +import { Octokit } from "@octokit/core"; +import { types, skills, status, stakeholders, goals } from './labels.js'; + +const octokit = new Octokit({ auth: `INSERT TOKEN HERE`}); + +const all = [...types.labels, ...skills.labels, ...status.labels, ...stakeholders.labels, ...goals.labels]; + +const targets = [ + { + name: 'elifesciences', + repositories: [ + 'issues', + 'sciencebeam-issues', + 'data-hub-issues' + ] + } +] + +// Pauses for the desired amount of time. +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +// Gets all the labels for the specified repository. +async function getLabelsForRepository(owner, repo) { + let data = []; + let page = 0; + let response; + const pageSize = 100; + do + { + response = await octokit.request('GET /repos/{owner}/{repo}/labels', { + owner, + repo, + page: ++page, + per_page: pageSize + }); + + data.push(...response.data); + } while (response.status === 200 && response.data.length > 0); + return data; +} + +function getMatch(match, labels) { + let retVal = undefined; + for (let label of labels) { + if (label.name === match.name || match.match.includes(label.name)) { + return label; + } + } + return retVal; +} + +// Compares 2 labels to see if they exactly match +function compareLabel(a, b) { + return (a.name === b.name && a.description === b.description && a.color === b.color); +} + +// Creates a new Create Label Job +function createNewLabelJob(owner, repo, label) { + return { + owner, + repo, + name: label.name, + description: label.description, + color: label.color + } +} + +// Creates a new label +async function createLabel(job) { + const response = await octokit.request('POST /repos/{owner}/{repo}/labels', job); + if (response.status !== 201) { + console.error(`Failed to create label '${job.name}' in '${job.owner}/${job.repo}'`); + } + else { + console.info(`Created label '${job.name}' in '${job.owner}/${job.repo}'`); + } +} + +// Creates a new Update Label Job +function createUpdateJob(owner, repo, name, label) { + return { + owner, + repo, + name, + new_name: label.name, + description: label.description, + color: label.color + } +} + +// Updates the specified label +async function updateLabel(job) { + const response = await octokit.request('PATCH /repos/{owner}/{repo}/labels/{name}', job); + if (response.status !== 200) { + console.error(`Failed to update label '${job.name}' in '${job.owner}/${job.repo}'`); + } + else { + console.info(`Updated label '${job.name}' in '${job.owner}/${job.repo}'`); + } +} + +let createJobs = []; +let updateJobs = []; +for (let org of targets) { + for (let repo of org.repositories) { + console.log(`INFO: Checking labels for '${org.name}/${repo}'`); + + const labels = await getLabelsForRepository(org.name, repo); + + console.log(`INFO: Found '${labels.length}' labels in '${org.name}/${repo}'`); + + // For each label we want in the repository. + for (const label of all) { + + // See if there is already an existing label in the repo that fuzzy matches. + const match = getMatch(label, labels); + if (match === undefined) + { + // Nope, then create it... + createJobs.push(createNewLabelJob(org.name, repo, label)); + } + else + { + // Check if it is an exact match. + if (!compareLabel(label, match)) { + // Nope, then needs updating. + updateJobs.push(createUpdateJob(org.name, repo, match.name, label)); + } + } + } + } +} + +// TODO: Document usage +let test = false; +for (const arg of process.argv) { + if (arg === '-d' || arg === '--dry-run') { + test = true; + } +} + +if (test) { + console.info(`Just a dry run, dumping jobs...`); + createJobs.map(job => console.info(` CREATE label '${job.name}' in '${job.owner}/${job.repo}'`)); + updateJobs.map(job => console.info(` UPDATE label '${job.new_name}' in '${job.owner}/${job.repo}'`)) +} +else { + createJobs.map(job => createLabel(job)); + updateJobs.map(job => updateLabel(job)); +} \ No newline at end of file