Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for job and step level execution status #95

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
14 changes: 11 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -745,23 +745,31 @@
"colors": [
{
"id": "GitHubLocalActions.green",
"description": "Color for green in GitHub Local Actions extension",
"description": "Color for green in the GitHub Local Actions extension",
"defaults": {
"dark": "#89d185",
"light": "#89d185"
}
},
{
"id": "GitHubLocalActions.yellow",
"description": "Color for yellow in GitHub Local Actions extension",
"description": "Color for yellow in the GitHub Local Actions extension",
"defaults": {
"dark": "#cca700",
"light": "#cca700"
}
},
{
"id": "GitHubLocalActions.purple",
"description": "Color for purple in the GitHub Local Actions extension",
"defaults": {
"dark": "#d6bcfa",
"light": "#d6bcfa"
}
},
{
"id": "GitHubLocalActions.red",
"description": "Color for red in GitHub Local Actions extension",
"description": "Color for red in the GitHub Local Actions extension",
"defaults": {
"dark": "#f48771",
"light": "#f48771"
Expand Down
127 changes: 118 additions & 9 deletions src/act.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ export class Act {
if (taskDefinition.type === 'GitHub Local Actions') {
this.runningTaskCount--;

if (this.refreshInterval && this.runningTaskCount == 0) {
if (this.refreshInterval && this.runningTaskCount === 0) {
clearInterval(this.refreshInterval);
this.refreshInterval = undefined;
}
Expand Down Expand Up @@ -344,7 +344,7 @@ export class Act {
const actCommand = Act.getActCommand();
const settings = await this.settingsManager.getSettings(workspaceFolder, true);
const command =
`${actCommand} ${commandArgs.options}` +
`${actCommand} ${Option.Json} ${commandArgs.options}` +
(settings.secrets.length > 0 ? ` ${Option.Secret} ${settings.secrets.map(secret => secret.key).join(` ${Option.Secret} `)}` : ``) +
(settings.secretFiles.length > 0 ? ` ${Option.SecretFile} "${settings.secretFiles[0].path}"` : ` ${Option.SecretFile} ""`) +
(settings.variables.length > 0 ? ` ${Option.Var} ${settings.variables.map(variable => `${variable.key}=${variable.value}`).join(` ${Option.Var} `)}` : ``) +
Expand Down Expand Up @@ -394,7 +394,8 @@ export class Act {
},
taskExecution: taskExecution,
commandArgs: commandArgs,
logPath: logPath
logPath: logPath,
jobs: []
});
historyTreeDataProvider.refresh();
this.storageManager.update(StorageKey.WorkspaceHistory, this.historyManager.workspaceHistory);
Expand All @@ -417,11 +418,100 @@ export class Act {
});

const handleIO = (data: any) => {
const lines: string[] = data.toString().split('\n').filter((line: string) => line != '');
const lines: string[] = data.toString().split('\n').filter((line: string) => line !== '');
for (const line of lines) {
writeEmitter.fire(`${line.trimEnd()}\r\n`);
const dateString = new Date().toString();

let message: string;
try {
const parsedMessage = JSON.parse(line);
if (parsedMessage.msg) {
message = `${parsedMessage.job ? `[${parsedMessage.job}] ` : ``}${parsedMessage.msg}`;
} else {
message = line;
}

// Update job and step status in workspace history
if (parsedMessage.jobID) {
let jobIndex = this.historyManager.workspaceHistory[commandArgs.path][historyIndex].jobs!
.findIndex(job => job.name === parsedMessage.jobID);
if (jobIndex < 0) {
// Add new job with setup step
this.historyManager.workspaceHistory[commandArgs.path][historyIndex].jobs!.push({
name: parsedMessage.jobID,
status: HistoryStatus.Running,
date: {
start: dateString
},
steps: [
{
id: -1, // Special id for setup job
name: 'Setup Job',
status: HistoryStatus.Running,
date: {
start: dateString
}
}
]
});
jobIndex = this.historyManager.workspaceHistory[commandArgs.path][historyIndex].jobs!.length - 1;
}

const isCompleteJobStep = this.historyManager.workspaceHistory[commandArgs.path][historyIndex].jobs![jobIndex].steps!.length > 1;
if (parsedMessage.stepID || isCompleteJobStep) {
let stepName: string;
let stepId: number;
if (!parsedMessage.stepID && isCompleteJobStep) {
stepName = 'Complete Job';
stepId = -2; // Special Id for complete job
} else {
stepName = parsedMessage.stage !== 'Main' ? `${parsedMessage.stage} ${parsedMessage.step}` : parsedMessage.step;
stepId = parseInt(parsedMessage.stepID[0]);
SanjulaGanepola marked this conversation as resolved.
Show resolved Hide resolved
}

if (this.historyManager.workspaceHistory[commandArgs.path][historyIndex].jobs![jobIndex].steps![0].status === HistoryStatus.Running) {
// TODO: How to know if setup job step failed?
this.historyManager.workspaceHistory[commandArgs.path][historyIndex].jobs![jobIndex].steps![0].status = HistoryStatus.Success;
this.historyManager.workspaceHistory[commandArgs.path][historyIndex].jobs![jobIndex].steps![0].date.end = dateString;
}

let stepIndex = this.historyManager.workspaceHistory[commandArgs.path][historyIndex].jobs![jobIndex].steps!
.findIndex(step => step.id === stepId && step.name === stepName);
if (stepIndex < 0) {
// Add new step
this.historyManager.workspaceHistory[commandArgs.path][historyIndex].jobs![jobIndex].steps!.push({
id: stepId,
name: stepName,
status: HistoryStatus.Running,
date: {
start: dateString
}
});
stepIndex = this.historyManager.workspaceHistory[commandArgs.path][historyIndex].jobs![jobIndex].steps!.length - 1;
}

if (parsedMessage.stepResult) {
this.historyManager.workspaceHistory[commandArgs.path][historyIndex].jobs![jobIndex].steps![stepIndex].status =
parsedMessage.stepResult === 'success' ? HistoryStatus.Success : HistoryStatus.Failed;
this.historyManager.workspaceHistory[commandArgs.path][historyIndex].jobs![jobIndex].steps![stepIndex].date.end = dateString;
}
}

if (parsedMessage.jobResult) {
this.historyManager.workspaceHistory[commandArgs.path][historyIndex].jobs![jobIndex].status =
parsedMessage.jobResult === 'success' ? HistoryStatus.Success : HistoryStatus.Failed;
this.historyManager.workspaceHistory[commandArgs.path][historyIndex].jobs![jobIndex].date.end =
dateString;
}
}
} catch (error) {
message = line;
}
writeEmitter.fire(`${message.trimEnd()}\r\n`);
historyTreeDataProvider.refresh();
this.storageManager.update(StorageKey.WorkspaceHistory, this.historyManager.workspaceHistory);
}
}
};

let shell = env.shell;
switch (process.platform) {
Expand Down Expand Up @@ -455,8 +545,28 @@ export class Act {
exec.stdout.on('data', handleIO);
exec.stderr.on('data', handleIO);
exec.on('exit', (code, signal) => {
const dateString = new Date().toString();

// Set execution status and end time in workspace history
if (this.historyManager.workspaceHistory[commandArgs.path][historyIndex].status === HistoryStatus.Running) {
const jobAndStepStatus = (!code && code !== 0) ? HistoryStatus.Cancelled : HistoryStatus.Unknown;
this.historyManager.workspaceHistory[commandArgs.path][historyIndex].jobs?.forEach((job, jobIndex) => {
this.historyManager.workspaceHistory[commandArgs.path][historyIndex].jobs![jobIndex].steps?.forEach((step, stepIndex) => {
if (step.status === HistoryStatus.Running) {
// Update status of all running steps
this.historyManager.workspaceHistory[commandArgs.path][historyIndex].jobs![jobIndex].steps![stepIndex].status = jobAndStepStatus;
this.historyManager.workspaceHistory[commandArgs.path][historyIndex].jobs![jobIndex].steps![stepIndex].date.end = dateString;
}
});

if (job.status === HistoryStatus.Running) {
// Update status of all running jobs
this.historyManager.workspaceHistory[commandArgs.path][historyIndex].jobs![jobIndex].status = jobAndStepStatus;
this.historyManager.workspaceHistory[commandArgs.path][historyIndex].jobs![jobIndex].date.end = dateString;
}
});

// Update history status
if (code === 0) {
this.historyManager.workspaceHistory[commandArgs.path][historyIndex].status = HistoryStatus.Success;
} else if (!code) {
Expand All @@ -465,7 +575,7 @@ export class Act {
this.historyManager.workspaceHistory[commandArgs.path][historyIndex].status = HistoryStatus.Failed;
}
}
this.historyManager.workspaceHistory[commandArgs.path][historyIndex].date.end = new Date().toString();
this.historyManager.workspaceHistory[commandArgs.path][historyIndex].date.end = dateString;
historyTreeDataProvider.refresh();
this.storageManager.update(StorageKey.WorkspaceHistory, this.historyManager.workspaceHistory);

Expand Down Expand Up @@ -494,7 +604,7 @@ export class Act {
exec.stdin.destroy();
exec.stderr.destroy();
} else {
exec.stdin.write(data === '\r' ? '\r\n' : data)
exec.stdin.write(data === '\r' ? '\r\n' : data);
}
},
close: () => {
Expand All @@ -506,7 +616,6 @@ export class Act {
};
})
});
this.storageManager.update(StorageKey.WorkspaceHistory, this.historyManager.workspaceHistory);
}

async install(packageManager: string) {
Expand Down
60 changes: 56 additions & 4 deletions src/historyManager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TaskExecution, Uri, window, workspace, WorkspaceFolder } from "vscode";
import { TaskExecution, ThemeColor, ThemeIcon, Uri, window, workspace, WorkspaceFolder } from "vscode";
import { CommandArgs } from "./act";
import { act, historyTreeDataProvider } from "./extension";
import { StorageKey, StorageManager } from "./storageManager";
Expand All @@ -12,16 +12,38 @@ export interface History {
start: string,
end?: string,
},
taskExecution?: TaskExecution,
commandArgs: CommandArgs,
logPath: string
logPath: string,
taskExecution?: TaskExecution,
jobs?: Job[],
}

export interface Job {
name: string,
status: HistoryStatus,
date: {
start: string,
end?: string,
},
steps?: Step[]
}

export interface Step {
id: number,
name: string,
status: HistoryStatus,
date: {
start: string,
end?: string,
}
}

export enum HistoryStatus {
Running = 'Running',
Success = 'Success',
Failed = 'Failed',
Cancelled = 'Cancelled'
Cancelled = 'Cancelled',
Unknown = 'Unknown'
}

export class HistoryManager {
Expand All @@ -33,6 +55,21 @@ export class HistoryManager {
const workspaceHistory = this.storageManager.get<{ [path: string]: History[] }>(StorageKey.WorkspaceHistory) || {};
for (const [path, historyLogs] of Object.entries(workspaceHistory)) {
workspaceHistory[path] = historyLogs.map(history => {
history.jobs?.forEach((job, jobIndex) => {
history.jobs![jobIndex].steps?.forEach((step, stepIndex) => {
// Update status of all running steps
if (step.status === HistoryStatus.Running) {
history.jobs![jobIndex].steps![stepIndex].status = HistoryStatus.Cancelled;
}
});

// Update status of all running jobs
if (job.status === HistoryStatus.Running) {
history.jobs![jobIndex].status = HistoryStatus.Cancelled;
}
});

// Update history status
if (history.status === HistoryStatus.Running) {
history.status = HistoryStatus.Cancelled;
}
Expand Down Expand Up @@ -84,4 +121,19 @@ export class HistoryManager {
} catch (error: any) { }
}
}

static statusToIcon(status: HistoryStatus) {
switch (status) {
case HistoryStatus.Running:
return new ThemeIcon('loading~spin');
case HistoryStatus.Success:
return new ThemeIcon('pass', new ThemeColor('GitHubLocalActions.green'));
case HistoryStatus.Failed:
return new ThemeIcon('error', new ThemeColor('GitHubLocalActions.red'));
case HistoryStatus.Cancelled:
return new ThemeIcon('circle-slash', new ThemeColor('GitHubLocalActions.yellow'));
case HistoryStatus.Unknown:
return new ThemeIcon('question', new ThemeColor('GitHubLocalActions.purple'));
}
}
}
34 changes: 11 additions & 23 deletions src/views/history/history.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import * as path from "path";
import { ThemeColor, ThemeIcon, TreeItem, TreeItemCollapsibleState, WorkspaceFolder } from "vscode";
import { History, HistoryStatus } from "../../historyManager";
import { TreeItem, TreeItemCollapsibleState, WorkspaceFolder } from "vscode";
import { History, HistoryManager, HistoryStatus } from "../../historyManager";
import { Utils } from "../../utils";
import { GithubLocalActionsTreeItem } from "../githubLocalActionsTreeItem";
import JobTreeItem from "./job";

export default class HistoryTreeItem extends TreeItem implements GithubLocalActionsTreeItem {
static contextValue = 'githubLocalActions.history';
history: History;

constructor(public workspaceFolder: WorkspaceFolder, history: History) {
super(`${history.name} #${history.count}`, TreeItemCollapsibleState.None);
super(`${history.name} #${history.count}`, TreeItemCollapsibleState.Collapsed);
this.history = history;

let endTime: string | undefined;
Expand All @@ -24,20 +25,7 @@ export default class HistoryTreeItem extends TreeItem implements GithubLocalActi

this.description = totalDuration;
this.contextValue = `${HistoryTreeItem.contextValue}_${history.status}`;
switch (history.status) {
case HistoryStatus.Running:
this.iconPath = new ThemeIcon('loading~spin');
break;
case HistoryStatus.Success:
this.iconPath = new ThemeIcon('pass', new ThemeColor('GitHubLocalActions.green'));
break;
case HistoryStatus.Failed:
this.iconPath = new ThemeIcon('error', new ThemeColor('GitHubLocalActions.red'));
break;
case HistoryStatus.Cancelled:
this.iconPath = new ThemeIcon('circle-slash', new ThemeColor('GitHubLocalActions.yellow'));
break;
}
this.iconPath = HistoryManager.statusToIcon(history.status);
this.tooltip = `Name: ${history.name} #${history.count}\n` +
`${history.commandArgs.extraHeader.map(header => `${header.key}: ${header.value}`).join('\n')}\n` +
`Path: ${history.commandArgs.path}\n` +
Expand All @@ -46,14 +34,14 @@ export default class HistoryTreeItem extends TreeItem implements GithubLocalActi
`Started: ${Utils.getDateString(history.date.start)}\n` +
`Ended: ${endTime ? Utils.getDateString(endTime) : 'N/A'}\n` +
`Total Duration: ${totalDuration ? totalDuration : 'N/A'}`;
this.command = {
title: 'Focus Task',
command: 'githubLocalActions.focusTask',
arguments: [this]
};
// this.command = {
// title: 'Focus Task',
// command: 'githubLocalActions.focusTask',
// arguments: [this]
// };
}

async getChildren(): Promise<GithubLocalActionsTreeItem[]> {
return [];
return this.history.jobs?.map(job => new JobTreeItem(this.workspaceFolder, job)) || [];
}
}
Loading