1 | "use strict";
|
2 | Object.defineProperty(exports, "__esModule", { value: true });
|
3 | const color_1 = require("@heroku-cli/color");
|
4 | const cli_ux_1 = require("cli-ux");
|
5 | const https_1 = require("https");
|
6 | const phoenix_1 = require("phoenix");
|
7 | const util_1 = require("util");
|
8 | const WebSocket = require("ws");
|
9 | const debug = require('debug')('ci');
|
10 | const ansiEscapes = require('ansi-escapes');
|
11 | const HEROKU_CI_WEBSOCKET_URL = process.env.HEROKU_CI_WEBSOCKET_URL || 'wss://particleboard.heroku.com/socket';
|
12 | function logStream(url, fn) {
|
13 | return https_1.get(url, fn);
|
14 | }
|
15 | function 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 | }
|
30 | function 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 | }
|
53 | const BUILDING = 'building';
|
54 | const RUNNING = 'running';
|
55 | const ERRORED = 'errored';
|
56 | const FAILED = 'failed';
|
57 | const SUCCEEDED = 'succeeded';
|
58 | const CANCELLED = 'cancelled';
|
59 | const TERMINAL_STATES = [SUCCEEDED, FAILED, ERRORED, CANCELLED];
|
60 | const RUNNING_STATES = [RUNNING].concat(TERMINAL_STATES);
|
61 | const BUILDING_STATES = [BUILDING, RUNNING].concat(TERMINAL_STATES);
|
62 | function printLine(testRun) {
|
63 | return `${statusIcon(testRun)} #${testRun.number} ${testRun.commit_branch}:${testRun.commit_sha.slice(0, 7)} ${testRun.status}`;
|
64 | }
|
65 | function printLineTestNode(testNode) {
|
66 | return `${statusIcon(testNode)} #${testNode.index} ${testNode.status}`;
|
67 | }
|
68 | function processExitCode(command, testNode) {
|
69 | if (testNode.exit_code && testNode.exit_code !== 0) {
|
70 | command.exit(testNode.exit_code);
|
71 | }
|
72 | }
|
73 | function 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 | }
|
82 | function sort(testRuns) {
|
83 | return testRuns.sort((a, b) => a.number < b.number ? 1 : -1);
|
84 | }
|
85 | function 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 | }
|
118 | async 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 | }
|
144 | exports.renderList = renderList;
|
145 | async 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 | }
|
154 | async function waitForStates(states, testRun, command) {
|
155 | let newTestRun = testRun;
|
156 | while (!states.includes(newTestRun.status.toString())) {
|
157 |
|
158 | const { body: bodyTestRun } = await command.heroku.get(`/pipelines/${testRun.pipeline.id}/test-runs/${testRun.number}`);
|
159 | newTestRun = bodyTestRun;
|
160 | }
|
161 | return newTestRun;
|
162 | }
|
163 | async 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 |
|
184 |
|
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 | }
|
194 | async function displayAndExit(pipeline, number, command) {
|
195 | const testNode = await display(pipeline, number, command);
|
196 | testNode ? processExitCode(command, testNode) : command.exit(1);
|
197 | }
|
198 | exports.displayAndExit = displayAndExit;
|
199 | async 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 | }
|
225 | exports.displayTestRunInfo = displayTestRunInfo;
|