diff --git a/USAGE.md b/USAGE.md index dd164ca..12826a7 100644 --- a/USAGE.md +++ b/USAGE.md @@ -1,6 +1,27 @@ # Usage +## `wiby clean` + + + +Use this command to clean up branches created by wiby (i.e. branches with the +"wiby-" prefix). + +``` +Options: + + --dependent URL of a dependent [string] + --config Path to the configuration file. By default it will try to load + the configuration from the first file it finds in the current + working directory: `.wiby.json`, `.wiby.js` [string] + --all Remove all branches with "wiby-" prefix. By default, `wiby clean` + will only remove the branch that would be created if `wiby test` + ran in the current repository, on the current branch. + --dry-run Print the list of branches to be removed. +``` + + ## `wiby result` diff --git a/bin/commands/clean.js b/bin/commands/clean.js new file mode 100644 index 0000000..4045193 --- /dev/null +++ b/bin/commands/clean.js @@ -0,0 +1,30 @@ +'use strict' + +const wiby = require('../..') + +exports.desc = 'Use this command to clean up branches created by wiby (i.e. branches with the "wiby-" prefix).' + +exports.builder = (yargs) => yargs + .option('dependent', { + desc: 'URL of a dependent', + type: 'string', + conflicts: 'config' + }) + .option('config', { + desc: 'Path to the configuration file. By default it will try to load the configuration from the first file it finds in the current working directory: `.wiby.json`, `.wiby.js`', + type: 'string' + }) + .option('all', { + desc: 'Remove all branches with "wiby-" prefix. By default, `wiby clean` will only remove the branch that would be created if `wiby test` ran in the current repository, on the current branch.' + }) + .option('dry-run', { + desc: 'Print the list of branches to be removed.' + }) + +exports.handler = async (params) => { + const config = params.dependent + ? { dependents: [{ repository: params.dependent }] } + : wiby.validate({ config: params.config }) + + return wiby.clean(config, params) +} diff --git a/bin/generate-usage.js b/bin/generate-usage.js index 8cf2702..93d0536 100755 --- a/bin/generate-usage.js +++ b/bin/generate-usage.js @@ -1,5 +1,7 @@ #!/usr/bin/env node +'use strict' + /** * Builds new content for USAGE.md page according to wiby --help commands list */ diff --git a/bin/wiby b/bin/wiby index 1c96121..0b05f05 100755 --- a/bin/wiby +++ b/bin/wiby @@ -1,5 +1,12 @@ #!/usr/bin/env node +'use strict' + +process.on('unhandledRejection', (err) => { + + throw err; +}); + require('dotenv').config() const chalk = require('chalk') diff --git a/lib/clean.js b/lib/clean.js new file mode 100644 index 0000000..2355b51 --- /dev/null +++ b/lib/clean.js @@ -0,0 +1,44 @@ +'use strict' + +const github = require('./github') +const gitURLParse = require('git-url-parse') +const logger = require('./logger') + +// setup logger namespace +const cleanCommandNamespace = 'wiby:clean' +const debug = logger(cleanCommandNamespace) + +module.exports = async ({ dependents }, { all, dryRun }) => { + // enable log output for clean command without DEBUG env + logger.enableLogs(cleanCommandNamespace) + + const parentPkgJSON = await require('./test').getLocalPackageJSON() + const parentPkgInfo = gitURLParse(parentPkgJSON.repository.url) + debug(`Parent module: ${parentPkgInfo.owner}/${parentPkgJSON.name}`) + + console.log(dryRun ? 'Branches to be deleted:' : 'Branches deleted:') + + for (const { repository: url } of dependents) { + const dependentPkgInfo = gitURLParse(url) + + let branches + + const branch = await require('./result').getBranchName(parentPkgJSON.name) + + if (all) { + branches = await github.getWibyBranches(dependentPkgInfo.owner, dependentPkgInfo.name) + } else if (await github.getBranch(dependentPkgInfo.owner, dependentPkgInfo.name, branch)) { + branches = [branch] + } else { + branches = [] + } + + if (!dryRun) { + for (const branch of branches) { + await github.deleteBranch(dependentPkgInfo.owner, dependentPkgInfo.name, branch) + } + } + + console.log(`- ${dependentPkgInfo}: ${branches.length ? branches.join(', ') : '(none)'}`) + } +} diff --git a/lib/github.js b/lib/github.js index 02239d2..0eb78f4 100644 --- a/lib/github.js +++ b/lib/github.js @@ -1,5 +1,10 @@ +'use strict' + const { graphql } = require('@octokit/graphql') const { Octokit } = require('@octokit/rest') + +const queries = require('./graphql') + const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN }) @@ -13,15 +18,7 @@ const graphqlWithAuth = graphql.defaults({ module.exports.getPackageJson = async function getPackageJson (owner, repo) { try { const resp = await graphqlWithAuth({ - query: `query($owner: String!, $repo: String!) { - repository(name: $repo, owner: $owner) { - object(expression: "HEAD:package.json") { - ... on Blob { - text - } - } - } - }`, + query: queries.getPackageJson, owner: owner, repo: repo }) @@ -35,6 +32,16 @@ module.exports.getPackageJson = async function getPackageJson (owner, repo) { } } +module.exports.getWibyBranches = async function (owner, repo) { + const resp = await graphqlWithAuth({ + query: queries.getWibyBranches, + owner: owner, + repo: repo + }) + const edges = resp.organization.repository.refs.edges + return edges.map(({ node: { branchName } }) => branchName) +} + module.exports.getShas = async function getShas (owner, repo) { const resp = await octokit.repos.listCommits({ owner, @@ -91,6 +98,29 @@ module.exports.createBranch = async function createBranch (owner, repo, commitSh }) } +module.exports.deleteBranch = async function deleteBranch (owner, repo, branch) { + await octokit.git.deleteRef({ + owner, + repo, + ref: `heads/${branch}` + }) +} + +module.exports.getBranch = async function getBranch (owner, repo, branch) { + try { + return await octokit.repos.getBranch({ + owner, + repo, + branch + }) + } catch (err) { + if (err.status === 404) { + return undefined + } + throw err + } +} + module.exports.getChecks = async function getChecks (owner, repo, branch) { return octokit.checks.listForRef({ owner, diff --git a/lib/graphql/getPackageJson.graphql b/lib/graphql/getPackageJson.graphql new file mode 100644 index 0000000..1a736e9 --- /dev/null +++ b/lib/graphql/getPackageJson.graphql @@ -0,0 +1,9 @@ +query($owner: String!, $repo: String!) { + repository(name: $repo, owner: $owner) { + object(expression: "HEAD:package.json") { + ... on Blob { + text + } + } + } +} diff --git a/lib/graphql/getWibyBranches.graphql b/lib/graphql/getWibyBranches.graphql new file mode 100644 index 0000000..965bf1c --- /dev/null +++ b/lib/graphql/getWibyBranches.graphql @@ -0,0 +1,18 @@ +query getExistingRepoBranches($owner: String!, $repo: String!) { + organization(login: $owner) { + repository(name: $repo) { + id + name + refs(refPrefix: "refs/heads/", query: "wiby-", first: 100) { + edges { + node { + branchName: name + } + } + pageInfo { + endCursor + } + } + } + } +} diff --git a/lib/graphql/index.js b/lib/graphql/index.js new file mode 100644 index 0000000..b6d4973 --- /dev/null +++ b/lib/graphql/index.js @@ -0,0 +1,8 @@ +'use strict' + +const fs = require('fs') +const path = require('path') + +exports.getPackageJson = fs.readFileSync(path.join(__dirname, 'getPackageJson.graphql')).toString() + +exports.getWibyBranches = fs.readFileSync(path.join(__dirname, 'getWibyBranches.graphql')).toString() diff --git a/lib/index.js b/lib/index.js index b4fc80c..6c2a341 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,5 +1,7 @@ 'use strict' +exports.clean = require('./clean') + exports.test = require('./test') exports.result = require('./result') diff --git a/lib/logger.js b/lib/logger.js index 248e0ae..40c8285 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -1,3 +1,5 @@ +'use strict' + const debugPkg = require('debug') /** diff --git a/lib/result.js b/lib/result.js index 5ed9e21..1853cb2 100644 --- a/lib/result.js +++ b/lib/result.js @@ -1,3 +1,5 @@ +'use strict' + const test = require('./test') const github = require('./github') const gitURLParse = require('git-url-parse') diff --git a/lib/test.js b/lib/test.js index ded9785..c375c49 100644 --- a/lib/test.js +++ b/lib/test.js @@ -1,3 +1,5 @@ +'use strict' + const fsPromises = require('fs').promises const github = require('./github') const gitURLParse = require('git-url-parse') diff --git a/test/clean.js b/test/clean.js new file mode 100644 index 0000000..8580023 --- /dev/null +++ b/test/clean.js @@ -0,0 +1,38 @@ +'use strict' + +require('dotenv').config() +const tap = require('tap') +const nock = require('nock') +const CONFIG = require('./fixtures/config') +const wiby = require('..') + +tap.beforeEach(async () => { + nock.disableNetConnect() +}) + +tap.afterEach(async () => { + nock.cleanAll() + nock.enableNetConnect() +}) + +tap.test('wiby.clean()', async (tap) => { + tap.test('should check if the wiby branch exists', async (tap) => { + nock('https://api.github.com') + .get(`/repos/wiby-test/${CONFIG.DEP_REPO}/branches/wiby-wiby`) + .reply(404) + + await wiby.clean({ dependents: [{ repository: `https://www.github.com/${CONFIG.DEP_ORG}/${CONFIG.DEP_REPO}` }] }, {}) + + // implied assertion - no DELETE requests expected - we don't need to delete the missing `wiby-wiby` branch + }) + + tap.test('should rethrow when github API inaccessible during branch check', async (tap) => { + nock('https://api.github.com') + .get(`/repos/wiby-test/${CONFIG.DEP_REPO}/branches/wiby-wiby`) + .reply(500) + + await tap.rejects( + wiby.clean({ dependents: [{ repository: `https://www.github.com/${CONFIG.DEP_ORG}/${CONFIG.DEP_REPO}` }] }, {}) + ) + }) +}) diff --git a/test/cli.js b/test/cli.js index 6f2c26a..9ab80aa 100644 --- a/test/cli.js +++ b/test/cli.js @@ -1,3 +1,5 @@ +'use strict' + const tap = require('tap') const childProcess = require('child_process') const path = require('path') @@ -26,7 +28,7 @@ tap.test('test command', async (tap) => { } }).toString() - tap.equal(true, result.includes('Changes pushed to https://github.com/wiby-test/fakeRepo/blob/wiby-wiby/package.json')) + tap.includes(result, 'Changes pushed to https://github.com/wiby-test/fakeRepo/blob/wiby-wiby/package.json') }) tap.test('test command should call test module with all deps from .wiby.json', async (tap) => { @@ -37,9 +39,9 @@ tap.test('test command', async (tap) => { } }).toString() - tap.equal(true, result.includes('Changes pushed to https://github.com/wiby-test/pass/blob/wiby-wiby/package.json')) - tap.equal(true, result.includes('Changes pushed to https://github.com/wiby-test/fail/blob/wiby-wiby/package.json')) - tap.equal(true, result.includes('Changes pushed to https://github.com/wiby-test/partial/blob/wiby-wiby/package.json')) + tap.includes(result, 'Changes pushed to https://github.com/wiby-test/pass/blob/wiby-wiby/package.json') + tap.includes(result, 'Changes pushed to https://github.com/wiby-test/fail/blob/wiby-wiby/package.json') + tap.includes(result, 'Changes pushed to https://github.com/wiby-test/partial/blob/wiby-wiby/package.json') }) }) @@ -146,8 +148,63 @@ tap.test('result command', async (tap) => { tap.test('validate command', async (tap) => { tap.test('should pass on wiby itself', async (tap) => { - const result = childProcess.execSync(`${wibyCommand} validate`, { cwd: cwd }).toString() - console.info(result) + childProcess.execSync(`${wibyCommand} validate`, { cwd: cwd }) tap.end() }) }) + +tap.test('clean command', async (tap) => { + tap.test('should delete test branch in all configured test modules', async (tap) => { + const result = childProcess.execSync(`${wibyCommand} clean`, { + cwd: cwd, + env: { + NODE_OPTIONS: '-r ./test/fixtures/http/clean-command.js' + } + }).toString() + + tap.includes(result, 'Branches deleted:') + tap.includes(result, '- https://github.com/wiby-test/partial: wiby-wiby') + tap.includes(result, '- git://github.com/wiby-test/fail: wiby-wiby') + tap.includes(result, '- git+https://github.com/wiby-test/pass: wiby-wiby') + }) + + tap.test('should delete test branch in the test module at dependent URI', async (tap) => { + const result = childProcess.execSync(`${wibyCommand} clean --dependent="https://github.com/wiby-test/fakeRepo"`, { + cwd: cwd, + env: { + NODE_OPTIONS: '-r ./test/fixtures/http/clean-command.js' + } + }).toString() + + tap.includes(result, 'Branches deleted:') + tap.includes(result, '- https://github.com/wiby-test/fakeRepo: wiby-wiby') + }) + + tap.test('should delete all wiby-* branches in all configured test modules', async (tap) => { + const result = childProcess.execSync(`${wibyCommand} clean --all`, { + cwd: cwd, + env: { + NODE_OPTIONS: '-r ./test/fixtures/http/clean-command-all.js' + } + }).toString() + + tap.includes(result, 'Branches deleted:') + tap.includes(result, '- https://github.com/wiby-test/partial: wiby-partial-one, wiby-partial-two') + tap.includes(result, '- git://github.com/wiby-test/fail: wiby-fail-one, wiby-fail-two') + tap.includes(result, '- git+https://github.com/wiby-test/pass: wiby-pass-one, wiby-pass-two') + }) + + tap.test('should not delete during dry-run', async (tap) => { + const result = childProcess.execSync(`${wibyCommand} clean --dry-run`, { + cwd: cwd, + env: { + NODE_OPTIONS: '-r ./test/fixtures/http/clean-command-dry.js' + } + }).toString() + + tap.includes(result, 'Branches to be deleted:') + tap.includes(result, '- https://github.com/wiby-test/partial: wiby-wiby') + tap.includes(result, '- git://github.com/wiby-test/fail: wiby-wiby') + tap.includes(result, '- git+https://github.com/wiby-test/pass: wiby-wiby') + }) +}) diff --git a/test/fixtures/checks.js b/test/fixtures/checks.js index 9a0d6cb..f9e329a 100644 --- a/test/fixtures/checks.js +++ b/test/fixtures/checks.js @@ -1,3 +1,5 @@ +'use strict' + module.exports = [ { status: 'queued', diff --git a/test/fixtures/config.js b/test/fixtures/config.js index 4326a67..18275b2 100644 --- a/test/fixtures/config.js +++ b/test/fixtures/config.js @@ -1,3 +1,5 @@ +'use strict' + module.exports.PKGJSON = require('./pass/package.json') module.exports.PATCHED_PKGJSON = Object.assign({}, module.exports.PKGJSON, { dependencies: Object.assign( diff --git a/test/fixtures/http/clean-command-all.js b/test/fixtures/http/clean-command-all.js new file mode 100644 index 0000000..177b19b --- /dev/null +++ b/test/fixtures/http/clean-command-all.js @@ -0,0 +1,64 @@ +'use strict' + +/** + * Mocks of HTTP calls for "wiby clean" command + */ +const nock = require('nock') + +nock.disableNetConnect() + +function nockRepo (nockInstance, repo) { + return nockInstance + .post('/graphql') + .reply(200, (uri, body) => { + const repo = body.variables.repo + + return { + data: { + organization: { + repository: { + name: 'pass', + refs: { + edges: [ + { + node: { + branchName: `wiby-${repo}-one` + } + }, + { + node: { + branchName: `wiby-${repo}-two` + } + } + ] + } + } + } + } + } + }) + .delete(`/repos/wiby-test/${repo}/git/refs/heads%2Fwiby-pass-one`) + .reply(200) + .delete(`/repos/wiby-test/${repo}/git/refs/heads%2Fwiby-pass-two`) + .reply(200) + .delete(`/repos/wiby-test/${repo}/git/refs/heads%2Fwiby-fail-one`) + .reply(200) + .delete(`/repos/wiby-test/${repo}/git/refs/heads%2Fwiby-fail-two`) + .reply(200) + .delete(`/repos/wiby-test/${repo}/git/refs/heads%2Fwiby-partial-one`) + .reply(200) + .delete(`/repos/wiby-test/${repo}/git/refs/heads%2Fwiby-partial-two`) + .reply(200) +} + +function buildNock () { + let nockInstance = nock('https://api.github.com') + + nockInstance = nockRepo(nockInstance, 'fail') + nockInstance = nockRepo(nockInstance, 'pass') + nockInstance = nockRepo(nockInstance, 'partial') + + return nockInstance +} + +buildNock() diff --git a/test/fixtures/http/clean-command-dry.js b/test/fixtures/http/clean-command-dry.js new file mode 100644 index 0000000..3e66a37 --- /dev/null +++ b/test/fixtures/http/clean-command-dry.js @@ -0,0 +1,26 @@ +'use strict' + +/** + * Mocks of HTTP calls for "wiby clean" command + */ +const nock = require('nock') + +nock.disableNetConnect() + +function nockRepo (nockInstance, repo) { + return nockInstance + .get(`/repos/wiby-test/${repo}/branches/wiby-wiby`) + .reply(200) +} + +function buildNock () { + let nockInstance = nock('https://api.github.com') + + nockInstance = nockRepo(nockInstance, 'fail') + nockInstance = nockRepo(nockInstance, 'pass') + nockInstance = nockRepo(nockInstance, 'partial') + + return nockInstance +} + +buildNock() diff --git a/test/fixtures/http/clean-command.js b/test/fixtures/http/clean-command.js new file mode 100644 index 0000000..59d5a79 --- /dev/null +++ b/test/fixtures/http/clean-command.js @@ -0,0 +1,29 @@ +'use strict' + +/** + * Mocks of HTTP calls for "wiby clean" command + */ +const nock = require('nock') + +nock.disableNetConnect() + +function nockRepo (nockInstance, repo) { + return nockInstance + .get(`/repos/wiby-test/${repo}/branches/wiby-wiby`) + .reply(200) + .delete(`/repos/wiby-test/${repo}/git/refs/heads%2Fwiby-wiby`) + .reply(200) +} + +function buildNock () { + let nockInstance = nock('https://api.github.com') + + nockInstance = nockRepo(nockInstance, 'fakeRepo') + nockInstance = nockRepo(nockInstance, 'fail') + nockInstance = nockRepo(nockInstance, 'pass') + nockInstance = nockRepo(nockInstance, 'partial') + + return nockInstance +} + +buildNock() diff --git a/test/fixtures/http/result-command-empty-branch-checks.js b/test/fixtures/http/result-command-empty-branch-checks.js index 9700874..622686c 100644 --- a/test/fixtures/http/result-command-empty-branch-checks.js +++ b/test/fixtures/http/result-command-empty-branch-checks.js @@ -1,8 +1,12 @@ +'use strict' + /** * Mocks of HTTP calls for "wiby result" command flow with empty response from check-runs */ const nock = require('nock') +nock.disableNetConnect() + nock('https://api.github.com') // get package json .post('/graphql') diff --git a/test/fixtures/http/result-command-positive-checks-failed.js b/test/fixtures/http/result-command-positive-checks-failed.js index 1bec118..c205489 100644 --- a/test/fixtures/http/result-command-positive-checks-failed.js +++ b/test/fixtures/http/result-command-positive-checks-failed.js @@ -1,9 +1,13 @@ +'use strict' + /** * Mocks of HTTP calls for "wiby result" command * Checks returned with status failure */ const nock = require('nock') +nock.disableNetConnect() + nock('https://api.github.com') // get package json .post('/graphql') diff --git a/test/fixtures/http/result-command-positive.js b/test/fixtures/http/result-command-positive.js index 233b7b3..8e39693 100644 --- a/test/fixtures/http/result-command-positive.js +++ b/test/fixtures/http/result-command-positive.js @@ -1,8 +1,12 @@ +'use strict' + /** * Mocks of HTTP calls for "wiby result" command positive flow */ const nock = require('nock') +nock.disableNetConnect() + nock('https://api.github.com') // get package json .post('/graphql') diff --git a/test/fixtures/http/test-command-positive.js b/test/fixtures/http/test-command-positive.js index b66dee5..23f6740 100644 --- a/test/fixtures/http/test-command-positive.js +++ b/test/fixtures/http/test-command-positive.js @@ -1,8 +1,12 @@ +'use strict' + /** * Mocks of HTTP calls for "wiby test" command positive flow */ const nock = require('nock') +nock.disableNetConnect() + function nockPkgjsWiby (nockInstance) { return nockInstance // get package json diff --git a/test/github-integration.js b/test/github-integration.js index f69407d..dfcea90 100644 --- a/test/github-integration.js +++ b/test/github-integration.js @@ -1,3 +1,5 @@ +'use strict' + require('dotenv').config() const tap = require('tap') const github = require('../lib/github') diff --git a/test/github.js b/test/github.js index d7e5eae..b06cf7d 100644 --- a/test/github.js +++ b/test/github.js @@ -1,3 +1,5 @@ +'use strict' + require('dotenv').config() const nock = require('nock') const tap = require('tap') diff --git a/test/logger.js b/test/logger.js index 7bff53b..d2e1a7e 100644 --- a/test/logger.js +++ b/test/logger.js @@ -1,3 +1,5 @@ +'use strict' + const tap = require('tap') const logger = require('./../lib/logger') diff --git a/test/result.js b/test/result.js index 4c61225..a031a93 100644 --- a/test/result.js +++ b/test/result.js @@ -1,3 +1,5 @@ +'use strict' + require('dotenv').config() const tap = require('tap') const nock = require('nock') diff --git a/test/test.js b/test/test.js index 9ceffdb..c8a2321 100644 --- a/test/test.js +++ b/test/test.js @@ -1,3 +1,5 @@ +'use strict' + require('dotenv').config() const fs = require('fs').promises const tap = require('tap')