UNPKG

9.62 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3const color_1 = require("@heroku-cli/color");
4const https_1 = require("https");
5const cli_ux_1 = require("cli-ux");
6const phoenix_1 = require("phoenix");
7const io = require("socket.io-client");
8const util_1 = require("util");
9const WebSocket = require("ws");
10const debug = require('debug')('ci');
11const ansiEscapes = require('ansi-escapes');
12const HEROKU_CI_WEBSOCKET_URL = process.env.HEROKU_CI_WEBSOCKET_URL || 'https://simi.heroku.com';
13const HEROKU_CI_WEBSOCKET_PHOENIX = ['1', 'true'].includes(process.env.HEROKU_CI_WEBSOCKET_PHOENIX || '');
14function logStream(url, fn) {
15 return https_1.get(url, fn);
16}
17function stream(url) {
18 return new Promise((resolve, reject) => {
19 const request = logStream(url, output => {
20 output.on('data', data => {
21 if (data.toString() === Buffer.from('').toString()) {
22 request.abort();
23 resolve();
24 }
25 });
26 output.on('end', () => resolve());
27 output.on('error', e => reject(e));
28 output.pipe(process.stdout);
29 });
30 });
31}
32function statusIcon({ status }) {
33 if (!status) {
34 return color_1.default.yellow('-');
35 }
36 switch (status) {
37 case 'pending':
38 case 'creating':
39 case 'building':
40 case 'running':
41 case 'debugging':
42 return color_1.default.yellow('-');
43 case 'errored':
44 return color_1.default.red('!');
45 case 'failed':
46 return color_1.default.red('✗');
47 case 'succeeded':
48 return color_1.default.green('✓');
49 case 'cancelled':
50 return color_1.default.yellow('!');
51 default:
52 return color_1.default.yellow('?');
53 }
54}
55const BUILDING = 'building';
56const RUNNING = 'running';
57const ERRORED = 'errored';
58const FAILED = 'failed';
59const SUCCEEDED = 'succeeded';
60const CANCELLED = 'cancelled';
61const TERMINAL_STATES = [SUCCEEDED, FAILED, ERRORED, CANCELLED];
62const RUNNING_STATES = [RUNNING].concat(TERMINAL_STATES);
63const BUILDING_STATES = [BUILDING, RUNNING].concat(TERMINAL_STATES);
64function printLine(testRun) {
65 return `${statusIcon(testRun)} #${testRun.number} ${testRun.commit_branch}:${testRun.commit_sha.slice(0, 7)} ${testRun.status}`;
66}
67function printLineTestNode(testNode) {
68 return `${statusIcon(testNode)} #${testNode.index} ${testNode.status}`;
69}
70function processExitCode(command, testNode) {
71 if (testNode.exit_code && testNode.exit_code !== 0) {
72 command.exit(testNode.exit_code);
73 }
74}
75function handleTestRunEvent(newTestRun, testRuns) {
76 const previousTestRun = testRuns.find(({ id }) => id === newTestRun.id);
77 if (previousTestRun) {
78 const previousTestRunIndex = testRuns.indexOf(previousTestRun);
79 testRuns.splice(previousTestRunIndex, 1);
80 }
81 testRuns.push(newTestRun);
82 return testRuns;
83}
84function sort(testRuns) {
85 return testRuns.sort((a, b) => a.number < b.number ? 1 : -1);
86}
87function draw(testRuns, watchOption = false, jsonOption = false, count = 15) {
88 const latestTestRuns = sort(testRuns).slice(0, count);
89 if (jsonOption) {
90 cli_ux_1.default.styledJSON(latestTestRuns);
91 return;
92 }
93 if (watchOption) {
94 process.stdout.write(ansiEscapes.eraseDown);
95 }
96 let data = [];
97 latestTestRuns.forEach(testRun => {
98 data.push({
99 iconStatus: `${statusIcon(testRun)}`,
100 number: testRun.number,
101 branch: testRun.commit_branch,
102 sha: testRun.commit_sha.slice(0, 7),
103 status: testRun.status
104 });
105 });
106 cli_ux_1.default.table(data, {
107 printHeader: undefined,
108 columns: [
109 { key: 'iconStatus', width: 1, label: '' },
110 { key: 'number', label: '' },
111 { key: 'branch' },
112 { key: 'sha' },
113 { key: 'status' }
114 ]
115 });
116 if (watchOption) {
117 process.stdout.write(ansiEscapes.cursorUp(latestTestRuns.length));
118 }
119}
120async function renderList(command, testRuns, pipeline, watchOption, jsonOption) {
121 const watchable = (watchOption && !jsonOption ? true : false);
122 if (!jsonOption) {
123 const header = `${watchOption ? 'Watching' : 'Showing'} latest test runs for the ${pipeline.name} pipeline`;
124 cli_ux_1.default.styledHeader(header);
125 }
126 // if (watchable) {
127 // process.stdout.write(ansiEscapes.cursorHide)
128 // }
129 draw(testRuns, watchOption, jsonOption);
130 if (!watchable) {
131 return;
132 }
133 if (HEROKU_CI_WEBSOCKET_PHOENIX) {
134 const socket = new phoenix_1.Socket(HEROKU_CI_WEBSOCKET_URL, {
135 transport: WebSocket,
136 logger: (kind, msg, data) => debug(`${kind}: ${msg}\n${util_1.inspect(data)}`),
137 });
138 socket.connect();
139 const channel = socket.channel(`events:pipelines/${pipeline.id}/test-runs`, { token: command.heroku.auth });
140 channel.join();
141 channel.on('create', ({ data }) => {
142 testRuns = handleTestRunEvent(data, testRuns);
143 draw(testRuns, watchOption);
144 });
145 channel.on('update', ({ data }) => {
146 testRuns = handleTestRunEvent(data, testRuns);
147 draw(testRuns, watchOption);
148 });
149 }
150 else {
151 let socket = io.connect(HEROKU_CI_WEBSOCKET_URL, { transports: ['websocket'] });
152 socket.on('connect', function () {
153 socket.emit('joinRoom', { room: `pipelines/${pipeline.id}/test-runs`, token: command.heroku.auth });
154 });
155 socket.on('disconnect', function () {
156 process.stdout.write(ansiEscapes.cursorShow);
157 });
158 socket.on('create', ({ resource, data }) => {
159 if (resource === 'test-run') {
160 testRuns = handleTestRunEvent(data, testRuns);
161 draw(testRuns, watchOption);
162 }
163 });
164 socket.on('update', ({ resource, data }) => {
165 if (resource === 'test-run') {
166 testRuns = handleTestRunEvent(data, testRuns);
167 draw(testRuns, watchOption);
168 }
169 });
170 }
171}
172exports.renderList = renderList;
173async function renderNodeOutput(command, testRun, testNode) {
174 if (!testNode) {
175 command.error(`Test run ${testRun.number} was ${testRun.status}. No Heroku CI runs found for this pipeline.`);
176 }
177 await stream(testNode.setup_stream_url);
178 await stream(testNode.output_stream_url);
179 command.log();
180 command.log(printLine(testRun));
181}
182async function waitForStates(states, testRun, command) {
183 let newTestRun = testRun;
184 while (!states.includes(newTestRun.status.toString())) {
185 let { body: bodyTestRun } = await command.heroku.get(`/pipelines/${testRun.pipeline.id}/test-runs/${testRun.number}`);
186 newTestRun = bodyTestRun;
187 }
188 return newTestRun;
189}
190async function display(pipeline, number, command) {
191 let { body: testRun } = await command.heroku.get(`/pipelines/${pipeline.id}/test-runs/${number}`);
192 if (testRun) {
193 cli_ux_1.default.action.start('Waiting for build to start');
194 testRun = await waitForStates(BUILDING_STATES, testRun, command);
195 cli_ux_1.default.action.stop();
196 let { body: testNodes } = await command.heroku.get(`/test-runs/${testRun.id}/test-nodes`);
197 let firstTestNode = testNodes[0];
198 if (firstTestNode) {
199 await stream(firstTestNode.setup_stream_url);
200 }
201 if (testRun) {
202 testRun = await waitForStates(RUNNING_STATES, testRun, command);
203 }
204 if (firstTestNode) {
205 await stream(firstTestNode.output_stream_url);
206 }
207 if (testRun) {
208 testRun = await waitForStates(TERMINAL_STATES, testRun, command);
209 }
210 // At this point, we know that testRun has a finished status,
211 // and we can check for exit_code from firstTestNode
212 if (testRun) {
213 let { body: newTestNodes } = await command.heroku.get(`/test-runs/${testRun.id}/test-nodes`);
214 firstTestNode = newTestNodes[0];
215 command.log();
216 command.log(printLine(testRun));
217 }
218 return firstTestNode;
219 }
220}
221async function displayAndExit(pipeline, number, command) {
222 let testNode = await display(pipeline, number, command);
223 testNode ? processExitCode(command, testNode) : command.exit(1);
224}
225exports.displayAndExit = displayAndExit;
226async function displayTestRunInfo(command, testRun, testNodes, nodeArg) {
227 let testNode;
228 if (nodeArg) {
229 const nodeIndex = parseInt(nodeArg, 2);
230 testNode = testNodes.length > 1 ? testNodes[nodeIndex] : testNodes[0];
231 await renderNodeOutput(command, testRun, testNode);
232 if (testNodes.length === 1) {
233 command.log();
234 command.warn('This pipeline doesn\'t have parallel test runs, but you specified a node');
235 command.warn('See https://devcenter.heroku.com/articles/heroku-ci-parallel-test-runs for more info');
236 }
237 processExitCode(command, testNode);
238 }
239 else {
240 if (testNodes.length > 1) {
241 command.log(printLine(testRun));
242 command.log();
243 testNodes.forEach(testNode => {
244 command.log(printLineTestNode(testNode));
245 });
246 }
247 else {
248 testNode = testNodes[0];
249 await renderNodeOutput(command, testRun, testNode);
250 processExitCode(command, testNode);
251 }
252 }
253}
254exports.displayTestRunInfo = displayTestRunInfo;