UNPKG

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