"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const color_1 = require("@heroku-cli/color"); const https_1 = require("https"); const cli_ux_1 = require("cli-ux"); const io = require("socket.io-client"); const ansiEscapes = require('ansi-escapes'); const SIMI_URL = 'https://simi.heroku.com'; function logStream(url, fn) { return https_1.get(url, fn); } function stream(url) { return new Promise((resolve, reject) => { const request = logStream(url, output => { output.on('data', data => { if (data.toString() === Buffer.from('').toString()) { request.abort(); resolve(); } }); output.on('end', () => resolve()); output.on('error', e => reject(e)); output.pipe(process.stdout); }); }); } function statusIcon({ status }) { if (!status) { return color_1.default.yellow('-'); } switch (status) { case 'pending': case 'creating': case 'building': case 'running': case 'debugging': return color_1.default.yellow('-'); case 'errored': return color_1.default.red('!'); case 'failed': return color_1.default.red('✗'); case 'succeeded': return color_1.default.green('✓'); case 'cancelled': return color_1.default.yellow('!'); default: return color_1.default.yellow('?'); } } const BUILDING = 'building'; const RUNNING = 'running'; const ERRORED = 'errored'; const FAILED = 'failed'; const SUCCEEDED = 'succeeded'; const CANCELLED = 'cancelled'; const TERMINAL_STATES = [SUCCEEDED, FAILED, ERRORED, CANCELLED]; const RUNNING_STATES = [RUNNING].concat(TERMINAL_STATES); const BUILDING_STATES = [BUILDING, RUNNING].concat(TERMINAL_STATES); function printLine(testRun) { return `${statusIcon(testRun)} #${testRun.number} ${testRun.commit_branch}:${testRun.commit_sha.slice(0, 7)} ${testRun.status}`; } function printLineTestNode(testNode) { return `${statusIcon(testNode)} #${testNode.index} ${testNode.status}`; } function processExitCode(command, testNode) { if (testNode.exit_code && testNode.exit_code !== 0) { command.exit(testNode.exit_code); } } function handleTestRunEvent(newTestRun, testRuns) { const previousTestRun = testRuns.find(({ id }) => id === newTestRun.id); if (previousTestRun) { const previousTestRunIndex = testRuns.indexOf(previousTestRun); testRuns.splice(previousTestRunIndex, 1); } testRuns.push(newTestRun); return testRuns; } function sort(testRuns) { return testRuns.sort((a, b) => a.number < b.number ? 1 : -1); } function draw(testRuns, watchOption = false, jsonOption = false, count = 15) { const latestTestRuns = sort(testRuns).slice(0, count); if (jsonOption) { cli_ux_1.default.styledJSON(latestTestRuns); return; } if (watchOption) { process.stdout.write(ansiEscapes.eraseDown); } let data = []; latestTestRuns.forEach(testRun => { data.push({ iconStatus: `${statusIcon(testRun)}`, number: testRun.number, branch: testRun.commit_branch, sha: testRun.commit_sha.slice(0, 7), status: testRun.status }); }); cli_ux_1.default.table(data, { printHeader: undefined, columns: [ { key: 'iconStatus', width: 1, label: '' }, { key: 'number', label: '' }, { key: 'branch' }, { key: 'sha' }, { key: 'status' } ] }); if (watchOption) { process.stdout.write(ansiEscapes.cursorUp(latestTestRuns.length)); } } async function renderList(command, testRuns, pipeline, watchOption, jsonOption) { const watchable = (watchOption && !jsonOption ? true : false); if (!jsonOption) { const header = `${watchOption ? 'Watching' : 'Showing'} latest test runs for the ${pipeline.name} pipeline`; cli_ux_1.default.styledHeader(header); } if (watchable) { process.stdout.write(ansiEscapes.cursorHide); } draw(testRuns, watchOption, jsonOption); if (!watchable) { return; } let socket = io.connect(SIMI_URL, { transports: ['websocket'] }); socket.on('connect', function () { socket.emit('joinRoom', { room: `pipelines/${pipeline.id}/test-runs`, token: command.heroku.auth }); }); socket.on('disconnect', function () { process.stdout.write(ansiEscapes.cursorShow); }); socket.on('create', ({ resource, data }) => { if (resource === 'test-run') { testRuns = handleTestRunEvent(data, testRuns); draw(testRuns, watchOption); } }); socket.on('update', ({ resource, data }) => { if (resource === 'test-run') { testRuns = handleTestRunEvent(data, testRuns); draw(testRuns, watchOption); } }); } exports.renderList = renderList; async function renderNodeOutput(command, testRun, testNode) { if (!testNode) { command.error(`Test run ${testRun.number} was ${testRun.status}. No Heroku CI runs found for this pipeline.`); } await stream(testNode.setup_stream_url); await stream(testNode.output_stream_url); command.log(); command.log(printLine(testRun)); } async function waitForStates(states, testRun, command) { let newTestRun = testRun; while (!states.includes(newTestRun.status.toString())) { let { body: bodyTestRun } = await command.heroku.get(`/pipelines/${testRun.pipeline.id}/test-runs/${testRun.number}`); newTestRun = bodyTestRun; } return newTestRun; } async function display(pipeline, number, command) { let { body: testRun } = await command.heroku.get(`/pipelines/${pipeline.id}/test-runs/${number}`); if (testRun) { cli_ux_1.default.action.start('Waiting for build to start'); testRun = await waitForStates(BUILDING_STATES, testRun, command); cli_ux_1.default.action.stop(); let { body: testNodes } = await command.heroku.get(`/test-runs/${testRun.id}/test-nodes`); let firstTestNode = testNodes[0]; if (firstTestNode) { await stream(firstTestNode.setup_stream_url); } if (testRun) { testRun = await waitForStates(RUNNING_STATES, testRun, command); } if (firstTestNode) { await stream(firstTestNode.output_stream_url); } if (testRun) { testRun = await waitForStates(TERMINAL_STATES, testRun, command); } // At this point, we know that testRun has a finished status, // and we can check for exit_code from firstTestNode if (testRun) { let { body: newTestNodes } = await command.heroku.get(`/test-runs/${testRun.id}/test-nodes`); firstTestNode = newTestNodes[0]; command.log(); command.log(printLine(testRun)); } return firstTestNode; } } async function displayAndExit(pipeline, number, command) { let testNode = await display(pipeline, number, command); testNode ? processExitCode(command, testNode) : command.exit(1); } exports.displayAndExit = displayAndExit; async function displayTestRunInfo(command, testRun, testNodes, nodeArg) { let testNode; if (nodeArg) { const nodeIndex = parseInt(nodeArg, 2); testNode = testNodes.length > 1 ? testNodes[nodeIndex] : testNodes[0]; await renderNodeOutput(command, testRun, testNode); if (testNodes.length === 1) { command.log(); command.warn('This pipeline doesn\'t have parallel test runs, but you specified a node'); command.warn('See https://devcenter.heroku.com/articles/heroku-ci-parallel-test-runs for more info'); } processExitCode(command, testNode); } else { if (testNodes.length > 1) { command.log(printLine(testRun)); command.log(); testNodes.forEach(testNode => { command.log(printLineTestNode(testNode)); }); } else { testNode = testNodes[0]; await renderNodeOutput(command, testRun, testNode); processExitCode(command, testNode); } } } exports.displayTestRunInfo = displayTestRunInfo;