diff --git a/src/act.ts b/src/act.ts index 254d480..043ff66 100644 --- a/src/act.ts +++ b/src/act.ts @@ -2,6 +2,7 @@ import * as child_process from 'child_process'; import * as path from "path"; import { commands, CustomExecution, env, EventEmitter, Pseudoterminal, ShellExecution, TaskDefinition, TaskGroup, TaskPanelKind, TaskRevealKind, tasks, TaskScope, TerminalDimensions, window, workspace } from "vscode"; import { ComponentsManager } from "./componentsManager"; +import { workflowsTreeDataProvider } from './extension'; import { SettingsManager } from './settingsManager'; import { Workflow, WorkflowsManager } from "./workflowsManager"; @@ -42,12 +43,73 @@ export enum EventTrigger { } export enum Option { - Workflows = '-W', - Variable = '-var' + Workflows = '--workflows', + Variable = '--var', + Json = "--json" +} + +export interface WorkflowLog { + name: string, + status: WorkflowStatus + jobLogs: JobLog[] +} + +export interface JobLog { + name: string, + status: JobStatus, + stepLogs: StepLog[] +} + +export interface StepLog { + name: string, + status: StepStatus +} + +export interface RawLog { + dryrun: boolean, + job: string, + jobID: string, + level: string, //TODO: Could be an enum? + matrix: any, + msg: string, + time: string, + + stage: string, + step: string, + stepID: string[], + + jobResult: string, //TODO: Could be an enum? +} + +export enum WorkflowStatus { + Queued = 'queued', + InProgress = 'inProgress', + Success = 'success', + Failed = 'failed', + Cancelled = 'cancelled' +} + +export enum JobStatus { + Queued = 'queued', + InProgress = 'inProgress', + Skipped = 'skipped', + Success = 'success', + Failed = 'failed', + Cancelled = 'cancelled' +} + +export enum StepStatus { + Queued = 'queued', + InProgress = 'inProgress', + Skipped = 'skipped', + Success = 'success', + Failed = 'failed', + Cancelled = 'cancelled' } export class Act { private static base: string = 'act'; + workflowLogs: { [path: string]: WorkflowLog[] }; componentsManager: ComponentsManager; workflowsManager: WorkflowsManager; settingsManager: SettingsManager; @@ -55,6 +117,7 @@ export class Act { prebuiltExecutables: { [architecture: string]: string }; constructor() { + this.workflowLogs = {}; this.componentsManager = new ComponentsManager(); this.workflowsManager = new WorkflowsManager(); this.settingsManager = new SettingsManager(); @@ -82,7 +145,7 @@ export class Act { 'MacPorts': 'sudo port install act', 'GitHub CLI': 'gh extension install https://github.com/nektos/gh-act' }; - + this.prebuiltExecutables = { 'macOS 64-bit (Apple Silicon)': 'https://github.com/nektos/act/releases/latest/download/act_Darwin_arm64.tar.gz', 'macOS 64-bit (Intel)': 'https://github.com/nektos/act/releases/latest/download/act_Darwin_x86_64.tar.gz' @@ -96,7 +159,7 @@ export class Act { 'COPR': 'dnf copr enable goncalossilva/act && dnf install act-cli', 'GitHub CLI': 'gh extension install https://github.com/nektos/gh-act' }; - + this.prebuiltExecutables = { 'Linux 64-bit (arm64/aarch64)': 'https://github.com/nektos/act/releases/latest/download/act_Linux_arm64.tar.gz', 'Linux 64-bit (amd64/x86_64)': 'https://github.com/nektos/act/releases/latest/download/act_Linux_x86_64.tar.gz', @@ -120,7 +183,7 @@ export class Act { } async runWorkflow(workflow: Workflow) { - return await this.runCommand(workflow, `${Act.base} ${Option.Workflows} ".github/workflows/${path.parse(workflow.uri.fsPath).base}"`); + return await this.runCommand(workflow, `${Act.base} ${Option.Json} ${Option.Workflows} ".github/workflows/${path.parse(workflow.uri.fsPath).base}"`); } async runCommand(workflow: Workflow, command: string) { @@ -140,6 +203,29 @@ export class Act { return; } + if (!this.workflowLogs[workflow.uri.fsPath]) { + this.workflowLogs[workflow.uri.fsPath] = []; + } + + this.workflowLogs[workflow.uri.fsPath].push({ + name: `${workflow.name} #${this.workflowLogs[workflow.uri.fsPath].length + 1}`, + status: WorkflowStatus.Queued, + jobLogs: Object.entries(workflow.yaml.jobs).map(([key, value]) => { + return { + name: value.name ? value.name : key, + status: JobStatus.Queued, + stepLogs: Object.entries(workflow.yaml.jobs[key].steps).map(([key, value]) => { + return { + name: value.name ? value.name : key, + status: StepStatus.Queued, + stepLogs: [] + } + }) + } + }) + }); + workflowsTreeDataProvider.refresh(); + await tasks.executeTask({ name: workflow.name, detail: 'Run workflow', @@ -178,8 +264,25 @@ export class Act { const exec = child_process.spawn(command, { cwd: workspaceFolder.uri.fsPath, shell: env.shell }); exec.stdout.on('data', (data) => { - const output = data.toString().replaceAll('\n', '\r\n'); - writeEmitter.fire(output); + const lines = data.toString().split('\n'); + for (const line of lines) { + const rawLog: RawLog = JSON.parse(line); + + if (rawLog.stepID) { + + } else if (rawLog.jobID) { + // this.workflowLogs[workflow.uri.fsPath][Object.values(this.workflowLogs).length - 1].jobLogs.push({ + // name: '', + // status: '', + // stepLogs: [] + // }); + } else if (rawLog.jobResult) { + + } else { + + } + } + writeEmitter.fire(lines); }); exec.stderr.on('data', (data) => { diff --git a/src/componentsManager.ts b/src/componentsManager.ts index 3579bfd..64982d8 100644 --- a/src/componentsManager.ts +++ b/src/componentsManager.ts @@ -1,6 +1,6 @@ import * as child_process from "child_process"; -import { commands, env, extensions, QuickPickItemKind, ShellExecution, TaskGroup, TaskPanelKind, TaskRevealKind, tasks, TaskScope, ThemeIcon, Uri, window } from "vscode"; -import { act } from "./extension"; +import { env, extensions, QuickPickItemKind, ShellExecution, TaskGroup, TaskPanelKind, TaskRevealKind, tasks, TaskScope, ThemeIcon, Uri, window } from "vscode"; +import { act, componentsTreeDataProvider } from "./extension"; export interface Component { name: string, @@ -102,7 +102,7 @@ export class ComponentsManager { await env.openExternal(Uri.parse(selectedPrebuiltExecutable.link)); window.showInformationMessage('Unpack and run the executable in the terminal specifying the full path or add it to one of the paths in your PATH environment variable. Once nektos/act is successfully installed, refresh the components view.', 'Refresh').then(async value => { if (value === 'Refresh') { - await commands.executeCommand('githubLocalActions.refreshComponents'); + componentsTreeDataProvider.refresh(); } }); } @@ -110,7 +110,7 @@ export class ComponentsManager { await env.openExternal(Uri.parse(selectedInstallationMethod.link)); window.showInformationMessage('Once nektos/act is successfully installed, refresh the components view.', 'Refresh').then(async value => { if (value === 'Refresh') { - await commands.executeCommand('githubLocalActions.refreshComponents'); + componentsTreeDataProvider.refresh(); } }); } else { @@ -170,7 +170,7 @@ export class ComponentsManager { window.showInformationMessage('Once Docker Engine is successfully started, refresh the components view.', 'Refresh').then(async value => { if (value === 'Refresh') { - await commands.executeCommand('githubLocalActions.refreshComponents'); + componentsTreeDataProvider.refresh(); } }); } @@ -229,7 +229,7 @@ export class ComponentsManager { }); } } else { - if(checksIfRunning) { + if (checksIfRunning) { resolve({ version: version ? version[1] : undefined, status: CliStatus.Running diff --git a/src/extension.ts b/src/extension.ts index 3a1b4a3..e77ad90 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -7,6 +7,9 @@ import SettingsTreeDataProvider from './views/settings/settingsTreeDataProvider' import WorkflowsTreeDataProvider from './views/workflows/workflowsTreeDataProvider'; export let act: Act; +export let componentsTreeDataProvider: ComponentsTreeDataProvider; +export let workflowsTreeDataProvider: WorkflowsTreeDataProvider; +export let settingsTreeDataProvider: SettingsTreeDataProvider; export function activate(context: vscode.ExtensionContext) { console.log('Congratulations, your extension "github-local-actions" is now active!'); @@ -14,11 +17,11 @@ export function activate(context: vscode.ExtensionContext) { act = new Act(); const decorationProvider = new DecorationProvider(); - const componentsTreeDataProvider = new ComponentsTreeDataProvider(context); + componentsTreeDataProvider = new ComponentsTreeDataProvider(context); const componentsTreeView = window.createTreeView(ComponentsTreeDataProvider.VIEW_ID, { treeDataProvider: componentsTreeDataProvider }); - const workflowsTreeDataProvider = new WorkflowsTreeDataProvider(context); + workflowsTreeDataProvider = new WorkflowsTreeDataProvider(context); const workflowsTreeView = window.createTreeView(WorkflowsTreeDataProvider.VIEW_ID, { treeDataProvider: workflowsTreeDataProvider }); - const settingsTreeDataProvider = new SettingsTreeDataProvider(context); + settingsTreeDataProvider = new SettingsTreeDataProvider(context); const settingsTreeView = window.createTreeView(SettingsTreeDataProvider.VIEW_ID, { treeDataProvider: settingsTreeDataProvider }); context.subscriptions.push( componentsTreeView, diff --git a/src/views/workflows/jobLog.ts b/src/views/workflows/jobLog.ts new file mode 100644 index 0000000..2c4cf0c --- /dev/null +++ b/src/views/workflows/jobLog.ts @@ -0,0 +1,30 @@ +import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from "vscode"; +import { JobLog } from "../../act"; +import { GithubLocalActionsTreeItem } from "../githubLocalActionsTreeItem"; +import StepTreeItem from "./stepLog"; + +export default class JobLogTreeItem extends TreeItem implements GithubLocalActionsTreeItem { + static contextValue = 'githubLocalActions.jobLog'; + jobLog: JobLog; + + constructor(jobLog: JobLog) { + super(jobLog.name, TreeItemCollapsibleState.Collapsed); + this.jobLog = jobLog; + this.contextValue = JobLogTreeItem.contextValue; + this.iconPath = new ThemeIcon('pass-filled'); + // this.tooltip = `Name: ${workflow.name}\n` + + // `Path: ${workflow.uri.fsPath}\n` + + // (workflow.error ? `Error: ${workflow.error}` : ``); + + // TODO: Add tooltip and resourceUri + } + + async getChildren(): Promise { + const stepLogs = this.jobLog.stepLogs; + if (stepLogs) { + return this.jobLog.stepLogs.map(step => new StepTreeItem(step)); + } else { + return []; + } + } +} \ No newline at end of file diff --git a/src/views/workflows/stepLog.ts b/src/views/workflows/stepLog.ts new file mode 100644 index 0000000..63772a8 --- /dev/null +++ b/src/views/workflows/stepLog.ts @@ -0,0 +1,24 @@ +import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from "vscode"; +import { StepLog } from "../../act"; +import { GithubLocalActionsTreeItem } from "../githubLocalActionsTreeItem"; + +export default class StepLogTreeItem extends TreeItem implements GithubLocalActionsTreeItem { + static contextValue = 'githubLocalActions.stepLog'; + stepLog: StepLog; + + constructor(stepLog: StepLog) { + super(stepLog.name, TreeItemCollapsibleState.None); + this.stepLog = stepLog; + this.contextValue = StepLogTreeItem.contextValue; + this.iconPath = new ThemeIcon('pass-filled'); + // this.tooltip = `Name: ${workflow.name}\n` + + // `Path: ${workflow.uri.fsPath}\n` + + // (workflow.error ? `Error: ${workflow.error}` : ``); + + // TODO: Add tooltip and resourceUri + } + + async getChildren(): Promise { + return []; + } +} \ No newline at end of file diff --git a/src/views/workflows/workflow.ts b/src/views/workflows/workflow.ts index b262d00..81f6106 100644 --- a/src/views/workflows/workflow.ts +++ b/src/views/workflows/workflow.ts @@ -1,6 +1,8 @@ import { ThemeIcon, TreeItem, TreeItemCollapsibleState, Uri } from "vscode"; +import { act } from "../../extension"; import { Workflow } from "../../workflowsManager"; import { GithubLocalActionsTreeItem } from "../githubLocalActionsTreeItem"; +import WorkflowLogTreeItem from "./workflowLog"; export default class WorkflowTreeItem extends TreeItem implements GithubLocalActionsTreeItem { static contextValue = 'githubLocalActions.workflow'; @@ -15,12 +17,17 @@ export default class WorkflowTreeItem extends TreeItem implements GithubLocalAct `Path: ${workflow.uri.fsPath}\n` + (workflow.error ? `Error: ${workflow.error}` : ``); - if(workflow.error) { + if (workflow.error) { this.resourceUri = Uri.parse(`${WorkflowTreeItem.contextValue}:${workflow.name}?error=${workflow.error}`, true); } } async getChildren(): Promise { - return []; + const workflowLogs = act.workflowLogs[this.workflow.uri.fsPath]; + if (workflowLogs) { + return workflowLogs.map(workflowLog => new WorkflowLogTreeItem(workflowLog)); + } else { + return []; + } } } \ No newline at end of file diff --git a/src/views/workflows/workflowLog.ts b/src/views/workflows/workflowLog.ts new file mode 100644 index 0000000..15d5367 --- /dev/null +++ b/src/views/workflows/workflowLog.ts @@ -0,0 +1,25 @@ +import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from "vscode"; +import { WorkflowLog } from "../../act"; +import { GithubLocalActionsTreeItem } from "../githubLocalActionsTreeItem"; +import JobLogTreeItem from "./jobLog"; + +export default class WorkflowLogTreeItem extends TreeItem implements GithubLocalActionsTreeItem { + static contextValue = 'githubLocalActions.workflowLog'; + workflowLog: WorkflowLog; + + constructor(workflowLog: WorkflowLog) { + super(workflowLog.name, TreeItemCollapsibleState.Collapsed); + this.workflowLog = workflowLog; + this.contextValue = WorkflowLogTreeItem.contextValue; + this.iconPath = new ThemeIcon('pass-filled'); + // this.tooltip = `Name: ${workflow.name}\n` + + // `Path: ${workflow.uri.fsPath}\n` + + // (workflow.error ? `Error: ${workflow.error}` : ``); + + // TODO: Add tooltip and resourceUri + } + + async getChildren(): Promise { + return this.workflowLog.jobLogs.map(jobLog => new JobLogTreeItem(jobLog)); + } +} \ No newline at end of file diff --git a/src/workflowsManager.ts b/src/workflowsManager.ts index 0436be8..b40e7bc 100644 --- a/src/workflowsManager.ts +++ b/src/workflowsManager.ts @@ -11,44 +11,7 @@ export interface Workflow { error?: string } -export interface WorkflowLog { - workflow: Workflow, - status: WorkflowStatus -} - -export enum WorkflowStatus { - Queued = 'queued', - InProgress = 'inProgress', - success = 'success', - failed = 'failed', - Cancelled = 'cancelled' -} - -export enum JobStatus { - Queued = 'queued', - InProgress = 'inProgress', - Skipped = 'skipped', - success = 'success', - failed = 'failed', - Cancelled = 'cancelled' -} - -export enum StepStatus { - Queued = 'queued', - InProgress = 'inProgress', - Skipped = 'skipped', - success = 'success', - failed = 'failed', - Cancelled = 'cancelled' -} - export class WorkflowsManager { - private workflowLogs: WorkflowLog[] = []; - - constructor() { - - } - async getWorkflows(): Promise { const workflows: Workflow[] = [];