Skip to content

Commit

Permalink
AG-32057 Get logs for workflow runs
Browse files Browse the repository at this point in the history
Merge in CI/github-actions-runner from feature/logs to master

* commit '57ed162a92581292efb31efaefaefb9a6a972fd7':
  improve logs
  add current status info
  get logs for workflow runs
  • Loading branch information
maximtop committed Apr 22, 2024
2 parents 65b3015 + 57ed162 commit 33bea3f
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 10 deletions.
12 changes: 11 additions & 1 deletion src/lib/GitHubActionsRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,11 +158,18 @@ export class GitHubActionsRunner {
await this.githubApiManager.waitForBranch(branch, branchTimeoutMs);

const customWorkflowRunId = await this.githubApiManager.triggerWorkflow(workflow, branch);
await this.githubApiManager.waitForWorkflowRunCreation(
const workflowRunInfo = await this.githubApiManager.waitForWorkflowRunCreation(
branch,
customWorkflowRunId,
workflowRunCreationTimeoutMs,
);

if (!workflowRunInfo) {
throw new Error('Workflow run not found.');
}

logger.info(`Link to workflow run: "${workflowRunInfo.html_url}"`);

const workflowRun = await this.githubApiManager.waitForWorkflowRunCompletion(
branch,
customWorkflowRunId,
Expand All @@ -173,6 +180,9 @@ export class GitHubActionsRunner {
throw new Error('Workflow run not found.');
}

const logs = await this.githubApiManager.fetchWorkflowRunLogs(workflowRun.id);
logger.info(logs);

if (artifactsPath) {
await this.githubApiManager.downloadArtifacts(workflowRun, artifactsPath);
}
Expand Down
22 changes: 18 additions & 4 deletions src/lib/github/GithubApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import { WORKFLOW_CREATED_WITHIN_MS } from '../constants';
const PER_PAGE = 100;

/**
* Type definition for the artifact download response.
* ResponseWithDownloadUrl interface.
*/
interface ArtifactDownloadResponse {
interface ResponseWithDownloadUrl {
status: number;
url: string;
}
Expand Down Expand Up @@ -157,7 +157,7 @@ export class GithubApiClient {
* @param artifactId The id of the artifact.
* @returns A promise that resolves to the download URL.
*/
async getArtifactDownloadUrl(artifactId: number): Promise<ArtifactDownloadResponse> {
async getArtifactDownloadUrl(artifactId: number): Promise<ResponseWithDownloadUrl> {
return await this.octokit.request(
'GET /repos/{owner}/{repo}/actions/artifacts/{artifact_id}/{archive_format}',
{
Expand All @@ -166,6 +166,20 @@ export class GithubApiClient {
artifact_id: artifactId,
archive_format: ARCHIVE_FORMAT,
},
) as ArtifactDownloadResponse;
) as ResponseWithDownloadUrl;
}

/**
* Gets the logs download url for a specific workflow run.
* The download URL is valid for 1 minute.
* @param workflowRunId The id of the workflow run.
* @returns A promise that resolves to the object with download URL and status.
*/
async getWorkflowRunLogsUrl(workflowRunId: number): Promise<ResponseWithDownloadUrl> {
return await this.octokit.request('GET /repos/{owner}/{repo}/actions/runs/{run_id}/logs', {
owner: this.owner,
repo: this.repo,
run_id: workflowRunId,
}) as ResponseWithDownloadUrl;
}
}
89 changes: 84 additions & 5 deletions src/lib/github/GithubApiManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { nanoid } from 'nanoid';
import axios, { HttpStatusCode } from 'axios';
import path from 'path';
import type { RestEndpointMethodTypes } from '@octokit/plugin-rest-endpoint-methods';
import { pipeline } from 'stream';
import { pipeline, Writable } from 'stream';
import { promisify } from 'util';
import * as unzipper from 'unzipper';
import { ensureDir } from 'fs-extra';
Expand Down Expand Up @@ -250,13 +250,13 @@ export class GithubApiManager {
const checkIfWorkflowRunCreated = async (): Promise<WorkflowRun | null> => {
const workflowRun = await this.getWorkflowRun(branch, customWorkflowRunId);
if (workflowRun) {
logger.info(`Workflow run found: ${workflowRun.name}`);
logger.info(`Workflow run found: "${workflowRun.name}"`);
return workflowRun;
}

// Check if the workflowRunCreationTimeoutMs has been reached
if (Date.now() - startTime > workflowRunCreationTimeoutMs) {
throw new Error('Timeout reached waiting for workflow run completion');
throw new Error('Timeout reached waiting for workflow run creation');
}

// Wait for the defined intervalMs then check again
Expand All @@ -268,6 +268,26 @@ export class GithubApiManager {
return result;
}

/**
* Logs how much time has passed since the workflow run started.
* @param workflowRun The workflow run info to log the time for.
*/
private static logHowMuchTimePassed(workflowRun: WorkflowRun): void {
if (!workflowRun.run_started_at) {
// impossible here, since we log after workflow run has started
logger.error(`Workflow run has not started yet, status: ${workflowRun.status}}`);
return;
}

const startedAt = workflowRun.run_started_at;
const currentTime = new Date();
const workflowStartTime = new Date(startedAt);
const durationSeconds = Math.floor((currentTime.getTime() - workflowStartTime.getTime()) / 1000);

// Log the time the build has been running and its current status
logger.info(`Build is running for: ${durationSeconds} seconds, current status is: "${workflowRun.status}"`);
}

/**
* Waits for a specific workflow run to complete with a workflowRunCompletionTimeoutMs.
*
Expand Down Expand Up @@ -314,6 +334,8 @@ export class GithubApiManager {
const checkIfWorkflowRunCompleted = async (): Promise<WorkflowRun | null> => {
const workflowRun = await this.getWorkflowRun(branch, customWorkflowRunId);
if (workflowRun) {
GithubApiManager.logHowMuchTimePassed(workflowRun);

if (workflowRun.status) {
if (!IN_PROGRESS_STATUSES[workflowRun.status as keyof Statuses]) {
logger.info(`Workflow run completed with status: "${workflowRun.status}"`);
Expand All @@ -325,12 +347,12 @@ export class GithubApiManager {
}
}

// Check if the timeoutMs has been reached
// Check if the workflowRunCompletionTimeoutMs has been reached
if (Date.now() - startTime > workflowRunCompletionTimeoutMs) {
throw new Error('Timeout reached waiting for workflow run completion');
}

// Wait for the defined intervalMs then check again
// Wait for the defined interval and then check again
await sleep(POLLING_INTERVAL_MS);
return checkIfWorkflowRunCompleted();
};
Expand Down Expand Up @@ -434,4 +456,61 @@ export class GithubApiManager {
return this.downloadArtifactToPath(artifact, artifactsPath);
}));
}

/**
* Fetches the logs for a given workflow run.
* The logs are fetched from the GitHub API and returned as a string.
* @param workflowRunId The ID of the workflow run.
* @returns A promise that resolves to the logs for the workflow run.
*/
async fetchWorkflowRunLogs(workflowRunId: number): Promise<string> {
/**
* In the archive with logs there are several files, separated by jobs,
* but we are interested in the whole log, which is in the file with the name "0_<job_name>.txt".
*/
const WHOLE_LOG_PATH_BEGINNING = '0_';
const LOG_EXTENSION = '.txt';

const logContent: string[] = [];

try {
// Fetch the URL for the workflow logs.
const response = await this.githubApiClient.getWorkflowRunLogsUrl(workflowRunId);
if (!response || !response.url) {
throw new Error(`Unable to retrieve log URL or URL is undefined for workflowRunId: ${workflowRunId}`);
}

const { data: logsStream } = await axios.get(
response.url,
{ responseType: 'stream' },
);

// Using a stream pipeline to process the stream and print the log data.
await pipelinePromise(
logsStream,
unzipper.Parse(),
new Writable({
objectMode: true,
async write(entry, encoding, callback): Promise<void> {
if (entry.path.startsWith(WHOLE_LOG_PATH_BEGINNING) && entry.path.endsWith(LOG_EXTENSION)) {
for await (const chunk of entry) {
logContent.push(chunk);
}
callback();
} else {
// Skip non-required files.
entry.autodrain().on('finish', callback);
}
},
}),
);
return [
'\n----GITHUB WORKFLOW RUN LOGS START----\n',
logContent.join(''),
'----GITHUB WORKFLOW RUN LOGS END----\n',
].join('');
} catch (e) {
throw new Error(`Failed to fetch logs: ${e}`);
}
}
}

0 comments on commit 33bea3f

Please sign in to comment.