diff --git a/bin/get-graphql-schemas.js b/bin/get-graphql-schemas.js index 64aadd00a66..3cb493a992e 100755 --- a/bin/get-graphql-schemas.js +++ b/bin/get-graphql-schemas.js @@ -121,7 +121,7 @@ async function fetchFileForSchema(schema, octokit) { path: schema.pathToFile, ref: branch, }) - console.log(`LFS download via ${download_url}...`) + console.log('Downloading LFS schema content...') content = await fetch(download_url).then(res => { if (!res.ok) { const error = new Error(`HTTP ${res.status}: ${res.statusText}`) diff --git a/bin/github-utils.js b/bin/github-utils.js index ba3f89c1064..622ed6408af 100755 --- a/bin/github-utils.js +++ b/bin/github-utils.js @@ -4,45 +4,58 @@ import {runCommand} from './run-command.js' import {createPullRequest} from 'octokit-plugin-create-pull-request' /** + * @param {typeof runCommand} executeCommand * @returns {Promise} */ -async function getGithubPasswordFromDev() { +async function getGithubTokenFromDev(executeCommand) { try { // Uses token from `dev` - return (await runCommand('/opt/dev/bin/dev', ['github', 'print-auth', '--password'])).trim() - } catch (error) { - console.warn(`Soft-error fetching password from dev: ${error.message}. Try running \`dev github print-auth\` manually.`) - process.exit(0) + const token = ( + await executeCommand('/opt/dev/bin/dev', ['github', 'print-auth', '--password'], {printOutput: false}) + ).trim() + if (!token) throw new Error('The GitHub token returned by dev is empty.') + return token + } catch { + throw new Error('Failed to fetch a GitHub token from dev. Try running `dev github auth`.') } } /** * @param {string} owner - * @param {function(import('@octokit/rest').Octokit): Promise} func - * @returns {Promise} + * @param {{environment?: NodeJS.ProcessEnv, executeCommand?: typeof runCommand, log?: (message: string) => void}} [options] + * @returns {Promise} */ -export async function withOctokit(owner, func) { - let password = undefined - +export async function getGithubToken( + owner, + {environment = process.env, executeCommand = runCommand, log = console.log} = {}, +) { const tokenEnvSources = [ `GITHUB_TOKEN_${owner.toUpperCase()}`, `GH_TOKEN_${owner.toUpperCase()}`, 'GITHUB_TOKEN', 'GH_TOKEN', ] - let tokenFromEnv = undefined + for (const source of tokenEnvSources) { - if (process.env[source]) { - tokenFromEnv = process.env[source] - console.log(`Using token from ${source}: ${tokenFromEnv}`) - break + const token = environment[source] + if (token) { + log(`Using GitHub token from ${source}`) + return token } } - if (!tokenFromEnv) { - password = await getGithubPasswordFromDev() - console.log(`Using password from dev: ${password}`) - } - const authToken = password || tokenFromEnv + + const token = await getGithubTokenFromDev(executeCommand) + log('Using GitHub token from dev') + return token +} + +/** + * @param {string} owner + * @param {function(import('@octokit/rest').Octokit): Promise} func + * @returns {Promise} + */ +export async function withOctokit(owner, func) { + const authToken = await getGithubToken(owner) const OctokitWithPlugin = Octokit.plugin(createPullRequest) const octokit = new OctokitWithPlugin({ diff --git a/bin/github-utils.test.js b/bin/github-utils.test.js new file mode 100644 index 00000000000..acb085dc3fc --- /dev/null +++ b/bin/github-utils.test.js @@ -0,0 +1,50 @@ +import assert from 'node:assert/strict' +import {test} from 'node:test' + +import {getGithubToken} from './github-utils.js' + +test('does not print a GitHub token sourced from the environment', async () => { + const token = 'environment-secret' + const logs = [] + + const result = await getGithubToken('shop', { + environment: {GITHUB_TOKEN_SHOP: token}, + log: (message) => logs.push(message), + }) + + assert.equal(result, token) + assert.deepEqual(logs, ['Using GitHub token from GITHUB_TOKEN_SHOP']) + assert.equal(logs.join(' ').includes(token), false) +}) + +test('captures a GitHub token from dev without printing it', async () => { + const token = 'dev-secret' + const logs = [] + const receivedCommands = [] + + const result = await getGithubToken('shop', { + environment: {}, + executeCommand: (...command) => { + receivedCommands.push(command) + return Promise.resolve(`${token}\n`) + }, + log: (message) => logs.push(message), + }) + + assert.equal(result, token) + assert.deepEqual(receivedCommands, [ + ['/opt/dev/bin/dev', ['github', 'print-auth', '--password'], {printOutput: false}], + ]) + assert.deepEqual(logs, ['Using GitHub token from dev']) + assert.equal(logs.join(' ').includes(token), false) +}) + +test('fails when dev cannot provide a GitHub token', async () => { + await assert.rejects( + getGithubToken('shop', { + environment: {}, + executeCommand: () => Promise.reject(new Error('Authentication failed')), + }), + new Error('Failed to fetch a GitHub token from dev. Try running `dev github auth`.'), + ) +}) diff --git a/bin/run-command.js b/bin/run-command.js index 10b1d09a292..8099aea358f 100644 --- a/bin/run-command.js +++ b/bin/run-command.js @@ -3,9 +3,10 @@ import {spawn} from 'child_process' /** * @param {string} command * @param {string[]} args + * @param {{printOutput?: boolean}} [options] * @returns {Promise} */ -export function runCommand(command, args) { +export function runCommand(command, args, {printOutput = true} = {}) { return new Promise((resolve, reject) => { const child = spawn(command, args, {stdio: ['inherit', 'pipe', 'pipe']}) @@ -13,12 +14,12 @@ export function runCommand(command, args) { let errorOutput = '' child.stdout.on('data', (data) => { - console.log(data.toString()) + if (printOutput) console.log(data.toString()) output += data.toString() }) child.stderr.on('data', (data) => { - console.log(data.toString()) + if (printOutput) console.log(data.toString()) errorOutput += data.toString() }) diff --git a/bin/run-command.test.js b/bin/run-command.test.js new file mode 100644 index 00000000000..039cc898aed --- /dev/null +++ b/bin/run-command.test.js @@ -0,0 +1,16 @@ +import assert from 'node:assert/strict' +import {test} from 'node:test' + +import {runCommand} from './run-command.js' + +test('captures command output without printing it when requested', async (context) => { + const sensitiveOutput = 'sensitive command output' + const consoleLog = context.mock.method(console, 'log', () => {}) + + const output = await runCommand(process.execPath, ['-e', `process.stdout.write('${sensitiveOutput}')`], { + printOutput: false, + }) + + assert.equal(output, sensitiveOutput) + assert.equal(consoleLog.mock.callCount(), 0) +})