UNPKG

eas-cli

Version:

EAS command line tool

326 lines (325 loc) 14 kB
"use strict"; 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, };