UNPKG

8.47 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 io = require("socket.io-client");
7const ansiEscapes = require('ansi-escapes');
8const SIMI_URL = 'https://simi.heroku.com';
9function logStream(url, fn) {
10 return https_1.get(url, fn);
11}
12function 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}
27function 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}
50const BUILDING = 'building';
51const RUNNING = 'running';
52const ERRORED = 'errored';
53const FAILED = 'failed';
54const SUCCEEDED = 'succeeded';
55const CANCELLED = 'cancelled';
56const TERMINAL_STATES = [SUCCEEDED, FAILED, ERRORED, CANCELLED];
57const RUNNING_STATES = [RUNNING].concat(TERMINAL_STATES);
58const BUILDING_STATES = [BUILDING, RUNNING].concat(TERMINAL_STATES);
59function printLine(testRun) {
60 return `${statusIcon(testRun)} #${testRun.number} ${testRun.commit_branch}:${testRun.commit_sha.slice(0, 7)} ${testRun.status}`;
61}
62function printLineTestNode(testNode) {
63 return `${statusIcon(testNode)} #${testNode.index} ${testNode.status}`;
64}
65function processExitCode(command, testNode) {
66 if (testNode.exit_code && testNode.exit_code !== 0) {
67 command.exit(testNode.exit_code);
68 }
69}
70function 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}
79function sort(testRuns) {
80 return testRuns.sort((a, b) => a.number < b.number ? 1 : -1);
81}
82function 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}
115async 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}
148exports.renderList = renderList;
149async 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}
158async 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}
166async 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 // At this point, we know that testRun has a finished status,
187 // and we can check for exit_code from firstTestNode
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}
197async function displayAndExit(pipeline, number, command) {
198 let testNode = await display(pipeline, number, command);
199 testNode ? processExitCode(command, testNode) : command.exit(1);
200}
201exports.displayAndExit = displayAndExit;
202async 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}
230exports.displayTestRunInfo = displayTestRunInfo;