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