eas-cli
Version:
EAS command line tool
326 lines (325 loc) • 14 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.workflowRunExitCodes = exports.showWorkflowStatusAsync = exports.maybeReadStdinAsync = exports.fileExistsAsync = exports.infoForFailedWorkflowRunAsync = exports.infoForActiveWorkflowRunAsync = exports.fetchAndProcessLogsFromJobAsync = exports.processWorkflowRuns = exports.choicesFromWorkflowLogs = exports.choiceFromWorkflowJob = exports.choiceFromWorkflowRun = exports.computeTriggerInfoForWorkflowRun = void 0;
const tslib_1 = require("tslib");
const chalk_1 = tslib_1.__importDefault(require("chalk"));
const fs = tslib_1.__importStar(require("node:fs"));
const fetchLogs_1 = require("./fetchLogs");
const types_1 = require("./types");
const generated_1 = require("../../graphql/generated");
const WorkflowRunQuery_1 = require("../../graphql/queries/WorkflowRunQuery");
const log_1 = tslib_1.__importDefault(require("../../log"));
const ora_1 = require("../../ora");
const formatFields_1 = tslib_1.__importDefault(require("../../utils/formatFields"));
const promise_1 = require("../../utils/promise");
function computeTriggerInfoForWorkflowRun(run) {
let triggerType = types_1.WorkflowTriggerType.OTHER;
let trigger = '';
if (run.actor?.__typename === 'Robot') {
if (run.actor.firstName?.startsWith('GitHub App · ')) {
trigger = `${run.requestedGitRef ?? ''}@${run.gitCommitHash?.substring(0, 12) ?? ''}`;
}
}
else if (run.actor?.__typename === 'User') {
trigger = run.actor.username;
}
switch (run.triggerEventType) {
case generated_1.WorkflowRunTriggerEventType.Manual:
triggerType = types_1.WorkflowTriggerType.MANUAL;
break;
case generated_1.WorkflowRunTriggerEventType.GithubPullRequestLabeled:
case generated_1.WorkflowRunTriggerEventType.GithubPullRequestOpened:
case generated_1.WorkflowRunTriggerEventType.GithubPullRequestReopened:
case generated_1.WorkflowRunTriggerEventType.GithubPullRequestSynchronize:
case generated_1.WorkflowRunTriggerEventType.GithubPush:
triggerType = types_1.WorkflowTriggerType.GITHUB;
break;
case generated_1.WorkflowRunTriggerEventType.Schedule:
triggerType = types_1.WorkflowTriggerType.SCHEDULED;
trigger = run.triggeringSchedule ?? '';
break;
}
return { triggerType, trigger };
}
exports.computeTriggerInfoForWorkflowRun = computeTriggerInfoForWorkflowRun;
function choiceFromWorkflowRun(run) {
const titleArray = [
run.workflowFileName,
run.status,
run.startedAt,
run.triggerType,
run.trigger,
];
return {
title: titleArray.join(' - '),
value: run.id,
description: `ID: ${run.id}, Message: ${run.gitCommitMessage?.split('\n')[0] ?? ''}`,
};
}
exports.choiceFromWorkflowRun = choiceFromWorkflowRun;
function choiceFromWorkflowJob(job, index) {
return {
title: `${job.name} - ${job.status}`,
value: index,
description: `ID: ${job.id}`,
};
}
exports.choiceFromWorkflowJob = choiceFromWorkflowJob;
function choicesFromWorkflowLogs(logs) {
return Array.from(logs.keys())
.map(step => {
const logLines = logs.get(step);
const stepStatus = logLines?.filter((line) => line.marker === 'end-step')[0]?.result ?? '';
return {
title: `${step} - ${stepStatus}`,
name: step,
status: stepStatus,
value: step,
logLines,
};
})
.filter(step => step.status !== 'skipped');
}
exports.choicesFromWorkflowLogs = choicesFromWorkflowLogs;
function processWorkflowRuns(runs) {
return runs.map(run => {
const finishedAt = run.status === generated_1.WorkflowRunStatus.InProgress ? null : run.updatedAt;
const { triggerType, trigger } = computeTriggerInfoForWorkflowRun(run);
return {
id: run.id,
status: run.status,
gitCommitMessage: run.gitCommitMessage?.split('\n')[0] ?? null,
gitCommitHash: run.gitCommitHash ?? null,
startedAt: run.createdAt,
finishedAt,
triggerType,
trigger,
workflowId: run.workflow.id,
workflowName: run.workflow.name ?? null,
workflowFileName: run.workflow.fileName,
};
});
}
exports.processWorkflowRuns = processWorkflowRuns;
async function fetchAndProcessLogsFromJobAsync(state, job) {
let rawLogs;
switch (job.type) {
case generated_1.WorkflowJobType.Build:
case generated_1.WorkflowJobType.Repack:
rawLogs = await (0, fetchLogs_1.fetchRawLogsForBuildJobAsync)(state, job);
break;
default:
rawLogs = await (0, fetchLogs_1.fetchRawLogsForCustomJobAsync)(job);
break;
}
if (!rawLogs) {
return null;
}
log_1.default.debug(`rawLogs = ${JSON.stringify(rawLogs, null, 2)}`);
const logs = new Map();
const logKeys = new Set();
rawLogs.split('\n').forEach((line, index) => {
log_1.default.debug(`line ${index} = ${JSON.stringify(line, null, 2)}`);
try {
const parsedLine = JSON.parse(line);
const { buildStepDisplayName, buildStepInternalId, time, msg, result, marker, err } = parsedLine;
const stepId = buildStepDisplayName ?? buildStepInternalId;
if (stepId) {
if (!logKeys.has(stepId)) {
logKeys.add(stepId);
logs.set(stepId, []);
}
logs.get(stepId)?.push({ time, msg, result, marker, err });
}
}
catch { }
});
return logs;
}
exports.fetchAndProcessLogsFromJobAsync = fetchAndProcessLogsFromJobAsync;
function descriptionForJobStatus(status) {
switch (status) {
case generated_1.WorkflowJobStatus.New:
return 'Waiting for worker';
case generated_1.WorkflowJobStatus.InProgress:
return 'In progress';
case generated_1.WorkflowJobStatus.Success:
return 'Completed successfully';
case generated_1.WorkflowJobStatus.Failure:
return 'Failed';
case generated_1.WorkflowJobStatus.Canceled:
return 'Canceled';
case generated_1.WorkflowJobStatus.Skipped:
return 'Skipped';
case generated_1.WorkflowJobStatus.ActionRequired:
return 'Waiting for action';
case generated_1.WorkflowJobStatus.PendingCancel:
return 'Pending cancel';
}
}
async function infoForActiveWorkflowRunAsync(graphqlClient, workflowRun, maxLogLines = 5 // -1 means no limit
) {
const statusLines = [];
const statusValues = [];
for (const job of workflowRun.jobs) {
statusValues.push({ label: '', value: '' });
statusValues.push({ label: ' Job', value: job.name });
statusValues.push({ label: ' Status', value: descriptionForJobStatus(job.status) });
if (job.status !== generated_1.WorkflowJobStatus.InProgress) {
continue;
}
const logs = await fetchAndProcessLogsFromJobAsync({ graphqlClient }, job);
const steps = logs ? choicesFromWorkflowLogs(logs) : [];
if (steps.length > 0) {
const currentStep = steps[steps.length - 1];
statusValues.push({ label: ' Current step', value: currentStep.name });
if (currentStep?.logLines?.length) {
statusValues.push({ label: ' Current logs', value: '' });
const currentLogs = currentStep.logLines
?.map(line => line.msg)
.filter((_, index) => {
if (maxLogLines === -1) {
return true;
}
return index > (currentStep.logLines?.length ?? 0) - maxLogLines;
}) ?? [];
for (const log of currentLogs) {
statusValues.push({ label: '', value: log });
}
}
}
}
statusValues.push({ label: '', value: '' });
statusLines.push((0, formatFields_1.default)(statusValues));
return statusLines.join('\n');
}
exports.infoForActiveWorkflowRunAsync = infoForActiveWorkflowRunAsync;
async function infoForFailedWorkflowRunAsync(graphqlClient, workflowRun, maxLogLines = -1 // -1 means no limit
) {
const statusLines = [];
const statusValues = [];
const logLinesToKeep = maxLogLines === -1 ? Infinity : maxLogLines;
for (const job of workflowRun.jobs) {
if (job.status !== generated_1.WorkflowJobStatus.Failure) {
continue;
}
const logs = await fetchAndProcessLogsFromJobAsync({ graphqlClient }, job);
const steps = logs ? choicesFromWorkflowLogs(logs) : [];
statusValues.push({ label: '', value: '' });
statusValues.push({ label: ' Failed job', value: job.name });
if (steps.length > 0) {
const failedStep = steps.find(step => step.status === 'fail');
if (failedStep) {
const logs = failedStep.logLines?.map(line => line.msg).slice(-logLinesToKeep) ?? [];
statusValues.push({ label: ' Failed step', value: failedStep.name });
statusValues.push({
label: ' Logs for failed step',
value: '',
});
for (const log of logs) {
statusValues.push({ label: '', value: log });
}
}
}
}
statusValues.push({ label: '', value: '' });
statusLines.push((0, formatFields_1.default)(statusValues));
return statusLines.join('\n');
}
exports.infoForFailedWorkflowRunAsync = infoForFailedWorkflowRunAsync;
async function fileExistsAsync(filePath) {
return await fs.promises
.access(filePath, fs.constants.F_OK)
.then(() => true)
.catch(() => false);
}
exports.fileExistsAsync = fileExistsAsync;
async function maybeReadStdinAsync() {
// Check if there's data on stdin
if (process.stdin.isTTY) {
return null;
}
return await new Promise((resolve, reject) => {
let data = '';
process.stdin.setEncoding('utf8');
process.stdin.on('readable', () => {
let chunk;
while ((chunk = process.stdin.read()) !== null) {
data += chunk;
}
});
process.stdin.on('end', () => {
const trimmedData = data.trim();
resolve(trimmedData || null);
});
process.stdin.on('error', err => {
reject(err);
});
});
}
exports.maybeReadStdinAsync = maybeReadStdinAsync;
async function showWorkflowStatusAsync(graphqlClient, { workflowRunId, spinnerUsesStdErr, waitForCompletion = true, }) {
log_1.default.log('Waiting for workflow run to complete. You can press Ctrl+C to exit.');
const spinner = (0, ora_1.ora)({
stream: spinnerUsesStdErr ? process.stderr : process.stdout,
text: '',
}).start();
spinner.prefixText = (0, chalk_1.default) `{bold.yellow Workflow run is waiting to start:}`;
let failedFetchesCount = 0;
while (true) {
try {
const workflowRun = await WorkflowRunQuery_1.WorkflowRunQuery.withJobsByIdAsync(graphqlClient, workflowRunId, {
useCache: false,
});
failedFetchesCount = 0;
switch (workflowRun.status) {
case generated_1.WorkflowRunStatus.New:
break;
case generated_1.WorkflowRunStatus.InProgress: {
spinner.prefixText = (0, chalk_1.default) `{bold.green Workflow run is in progress:}`;
spinner.text = await infoForActiveWorkflowRunAsync(graphqlClient, workflowRun, 5);
break;
}
case generated_1.WorkflowRunStatus.ActionRequired:
spinner.prefixText = (0, chalk_1.default) `{bold.yellow Workflow run is waiting for action:}`;
break;
case generated_1.WorkflowRunStatus.Canceled:
spinner.prefixText = (0, chalk_1.default) `{bold.yellow Workflow has been canceled.}`;
spinner.stopAndPersist();
return workflowRun;
case generated_1.WorkflowRunStatus.Failure: {
spinner.prefixText = (0, chalk_1.default) `{bold.red Workflow has failed.}`;
const failedInfo = await infoForFailedWorkflowRunAsync(graphqlClient, workflowRun, 30);
spinner.fail(failedInfo);
return workflowRun;
}
case generated_1.WorkflowRunStatus.Success:
spinner.prefixText = (0, chalk_1.default) `{bold.green Workflow has completed successfully.}`;
spinner.text = '';
spinner.succeed('');
return workflowRun;
}
if (!waitForCompletion) {
if (spinner.isSpinning) {
spinner.stopAndPersist();
}
return workflowRun;
}
}
catch {
spinner.text = '⚠ Failed to fetch the workflow run status. Check your network connection.';
failedFetchesCount += 1;
if (failedFetchesCount > 6) {
spinner.fail('Failed to fetch the workflow run status 6 times in a row. Aborting wait.');
process.exit(exports.workflowRunExitCodes.WAIT_ABORTED);
}
}
await (0, promise_1.sleepAsync)(10 /* seconds */ * 1000 /* milliseconds */);
}
}
exports.showWorkflowStatusAsync = showWorkflowStatusAsync;
exports.workflowRunExitCodes = {
WORKFLOW_FAILED: 11,
WORKFLOW_CANCELED: 12,
WAIT_ABORTED: 13,
};