1 | "use strict";
|
2 | Object.defineProperty(exports, "__esModule", { value: true });
|
3 | const color_1 = require("@heroku-cli/color");
|
4 | const https_1 = require("https");
|
5 | const cli_ux_1 = require("cli-ux");
|
6 | const phoenix_1 = require("phoenix");
|
7 | const io = require("socket.io-client");
|
8 | const util_1 = require("util");
|
9 | const WebSocket = require("ws");
|
10 | const debug = require('debug')('ci');
|
11 | const ansiEscapes = require('ansi-escapes');
|
12 | const HEROKU_CI_WEBSOCKET_URL = process.env.HEROKU_CI_WEBSOCKET_URL || 'https://simi.heroku.com';
|
13 | const HEROKU_CI_WEBSOCKET_PHOENIX = ['1', 'true'].includes(process.env.HEROKU_CI_WEBSOCKET_PHOENIX || '');
|
14 | function logStream(url, fn) {
|
15 | return https_1.get(url, fn);
|
16 | }
|
17 | function 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 | }
|
32 | function 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 | }
|
55 | const BUILDING = 'building';
|
56 | const RUNNING = 'running';
|
57 | const ERRORED = 'errored';
|
58 | const FAILED = 'failed';
|
59 | const SUCCEEDED = 'succeeded';
|
60 | const CANCELLED = 'cancelled';
|
61 | const TERMINAL_STATES = [SUCCEEDED, FAILED, ERRORED, CANCELLED];
|
62 | const RUNNING_STATES = [RUNNING].concat(TERMINAL_STATES);
|
63 | const BUILDING_STATES = [BUILDING, RUNNING].concat(TERMINAL_STATES);
|
64 | function printLine(testRun) {
|
65 | return `${statusIcon(testRun)} #${testRun.number} ${testRun.commit_branch}:${testRun.commit_sha.slice(0, 7)} ${testRun.status}`;
|
66 | }
|
67 | function printLineTestNode(testNode) {
|
68 | return `${statusIcon(testNode)} #${testNode.index} ${testNode.status}`;
|
69 | }
|
70 | function processExitCode(command, testNode) {
|
71 | if (testNode.exit_code && testNode.exit_code !== 0) {
|
72 | command.exit(testNode.exit_code);
|
73 | }
|
74 | }
|
75 | function 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 | }
|
84 | function sort(testRuns) {
|
85 | return testRuns.sort((a, b) => a.number < b.number ? 1 : -1);
|
86 | }
|
87 | function 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 | }
|
120 | async 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 |
|
127 |
|
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 | }
|
172 | exports.renderList = renderList;
|
173 | async 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 | }
|
182 | async 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 | }
|
190 | async 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 |
|
211 |
|
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 | }
|
221 | async function displayAndExit(pipeline, number, command) {
|
222 | let testNode = await display(pipeline, number, command);
|
223 | testNode ? processExitCode(command, testNode) : command.exit(1);
|
224 | }
|
225 | exports.displayAndExit = displayAndExit;
|
226 | async 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 | }
|
254 | exports.displayTestRunInfo = displayTestRunInfo;
|