diff --git a/package-lock.json b/package-lock.json index a90cc4a..43c4d45 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1", "license": "Apache-2.0", "dependencies": { + "child_process": "^1.0.2", "yaml": "^2.5.1" }, "devDependencies": { @@ -1197,6 +1198,11 @@ "node": ">=8" } }, + "node_modules/child_process": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/child_process/-/child_process-1.0.2.tgz", + "integrity": "sha512-Wmza/JzL0SiWz7kl6MhIKT5ceIlnFPJX+lwUGj7Clhy5MMldsSoJR0+uvRzOS5Kv45Mq7t1PoE8TsOA9bzvb6g==" + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", diff --git a/package.json b/package.json index 25de732..cc589f7 100644 --- a/package.json +++ b/package.json @@ -213,6 +213,7 @@ "test": "vscode-test" }, "dependencies": { + "child_process": "^1.0.2", "yaml": "^2.5.1" }, "devDependencies": { @@ -229,4 +230,4 @@ "webpack": "^5.94.0", "webpack-cli": "^5.1.4" } -} \ No newline at end of file +} diff --git a/src/act.ts b/src/act.ts index 22cba2a..539a38f 100644 --- a/src/act.ts +++ b/src/act.ts @@ -1,5 +1,6 @@ +import * as child_process from 'child_process'; import * as path from "path"; -import { commands, ShellExecution, TaskGroup, TaskPanelKind, TaskRevealKind, tasks, TaskScope, window } from "vscode"; +import { commands, CustomExecution, EventEmitter, Pseudoterminal, TaskDefinition, TaskGroup, TaskPanelKind, TaskRevealKind, tasks, TaskScope, TerminalDimensions, window, workspace } from "vscode"; import { ComponentManager } from "./componentManager"; import { Workflow } from "./workflowManager"; @@ -51,15 +52,14 @@ export class Act { } static async runEvent(eventTrigger: EventTrigger) { - return await Act.runCommand(`${Act.base} ${eventTrigger}`); + // return await Act.runCommand(`${Act.base} ${eventTrigger}`); } static async runWorkflow(workflow: Workflow) { - return await Act.runCommand(`${Act.base} ${Option.Workflows} '.github/workflows/${path.parse(workflow.uri.fsPath).base}'`, workflow); + return await Act.runCommand(workflow, `${Act.base} ${Option.Workflows} ".github/workflows/${path.parse(workflow.uri.fsPath).base}"`); } - static async runCommand(command: string, workflow?: Workflow) { - + static async runCommand(workflow: Workflow, command: string) { const unreadyComponents = await ComponentManager.getUnreadyComponents(); if (unreadyComponents.length > 0) { window.showErrorMessage(`The following required components are not ready: ${unreadyComponents.map(component => component.name).join(', ')}`, 'Fix...').then(async value => { @@ -70,12 +70,18 @@ export class Act { return; } + const workspaceFolder = workspace.getWorkspaceFolder(workflow.uri); + if (!workspaceFolder) { + window.showErrorMessage('Failed to detect workspace folder'); + return; + } + await tasks.executeTask({ - name: workflow?.name || 'act', + name: workflow.name, detail: 'Run workflow', definition: { type: 'GitHub Local Actions' }, source: 'GitHub Local Actions', - scope: TaskScope.Workspace, + scope: workspaceFolder || TaskScope.Workspace, isBackground: true, presentationOptions: { reveal: TaskRevealKind.Always, @@ -89,7 +95,42 @@ export class Act { problemMatchers: [], runOptions: {}, group: TaskGroup.Build, - execution: new ShellExecution(command) + execution: new CustomExecution(async (resolvedDefinition: TaskDefinition): Promise => { + const writeEmitter = new EventEmitter(); + const closeEmitter = new EventEmitter(); + + return { + onDidWrite: writeEmitter.event, + onDidClose: closeEmitter.event, + open: async (initialDimensions: TerminalDimensions | undefined): Promise => { + writeEmitter.fire(`Workflow: ${workflow.name}\r\nPath: ${workflow.uri.fsPath}\r\nCommand: ${command}\r\nTimestamp: ${new Date().toLocaleTimeString()}\r\n\r\n`); + + const exec = child_process.spawn(command, { cwd: workspaceFolder.uri.fsPath, shell: '/usr/bin/bash' }); + exec.stdout.on('data', (data) => { + const output = data.toString(); + writeEmitter.fire(output); + + if (output.includes('success')) { + window.showInformationMessage('Command succeeded!'); + } + }); + + exec.stderr.on('data', (data) => { + const error = data.toString(); + writeEmitter.fire(error); + }); + + exec.on('close', (code) => { + closeEmitter.fire(code || 0); + }); + }, + + close: () => { + // TODO: + } + }; + } + ) }); } } \ No newline at end of file diff --git a/src/componentManager.ts b/src/componentManager.ts index ed4c55c..0a9ad82 100644 --- a/src/componentManager.ts +++ b/src/componentManager.ts @@ -1,12 +1,12 @@ export interface Component { name: string, icon: string, - status: Status, + status: ComponentStatus, required: boolean message?: string } -export enum Status { +export enum ComponentStatus { Enabled = 'Enabled', Warning = 'Warning', Disabled = 'Disabled' @@ -18,26 +18,26 @@ export class ComponentManager { { name: 'nektos/act', icon: 'package', - status: Status.Enabled, + status: ComponentStatus.Enabled, required: true }, { name: 'Docker Engine', icon: 'dashboard', - status: Status.Enabled, + status: ComponentStatus.Enabled, required: true }, { name: 'GitHub Actions Extension', icon: 'extensions', - status: Status.Warning, + status: ComponentStatus.Warning, required: false, message: 'GitHub Actions extension is not required, but is recommended to take advantage of workflow editor features.' }, { name: 'GitHub CLI', icon: 'terminal', - status: Status.Warning, + status: ComponentStatus.Warning, required: false, message: 'GitHub CLI is not required, but is recommended if you plan to use it to retrieve GitHub tokens.' } @@ -46,6 +46,6 @@ export class ComponentManager { static async getUnreadyComponents(): Promise { const components = await ComponentManager.getComponents(); - return components.filter(component => component.required && component.status !== Status.Enabled); + return components.filter(component => component.required && component.status !== ComponentStatus.Enabled); } } \ No newline at end of file diff --git a/src/views/decorationProvider.ts b/src/views/decorationProvider.ts index e0634e2..9e177d3 100644 --- a/src/views/decorationProvider.ts +++ b/src/views/decorationProvider.ts @@ -1,5 +1,5 @@ import { CancellationToken, Event, FileDecoration, FileDecorationProvider, ProviderResult, ThemeColor, Uri } from "vscode"; -import { Status } from "../componentManager"; +import { ComponentStatus } from "../componentManager"; import ComponentTreeItem from "./components/component"; import WorkflowTreeItem from "./workflows/workflow"; @@ -9,17 +9,17 @@ export class DecorationProvider implements FileDecorationProvider { const params = new URLSearchParams(uri.query); if (uri.scheme === ComponentTreeItem.contextValue) { - if (params.get('status') === Status.Enabled) { + if (params.get('status') === ComponentStatus.Enabled) { return { badge: '✅', color: new ThemeColor('GitHubLocalActions.green') }; - } else if (params.get('status') === Status.Warning) { + } else if (params.get('status') === ComponentStatus.Warning) { return { badge: '⚠️', color: new ThemeColor('GitHubLocalActions.yellow') }; - } else if (params.get('status') === Status.Disabled) { + } else if (params.get('status') === ComponentStatus.Disabled) { return { badge: '❌', color: new ThemeColor('GitHubLocalActions.red') diff --git a/src/views/workflows/workflow.ts b/src/views/workflows/workflow.ts index 64e479f..6c18879 100644 --- a/src/views/workflows/workflow.ts +++ b/src/views/workflows/workflow.ts @@ -7,7 +7,7 @@ export default class WorkflowTreeItem extends TreeItem implements GithubLocalAct workflow: Workflow; constructor(workflow: Workflow) { - super(workflow.content.name || workflow.name, workflow.error ? TreeItemCollapsibleState.None : TreeItemCollapsibleState.Collapsed); + super(workflow.name, workflow.error ? TreeItemCollapsibleState.None : TreeItemCollapsibleState.Collapsed); this.workflow = workflow; this.contextValue = WorkflowTreeItem.contextValue; this.iconPath = new ThemeIcon('layers'); diff --git a/src/workflowManager.ts b/src/workflowManager.ts index baa32d6..173d985 100644 --- a/src/workflowManager.ts +++ b/src/workflowManager.ts @@ -10,7 +10,44 @@ 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 WorkflowManager { + private workflowLogs: WorkflowLog[] = []; + + constructor() { + + } + static async getWorkflows(): Promise { const workflows: Workflow[] = []; @@ -19,17 +56,20 @@ export class WorkflowManager { const workflowFileUris = await workspace.findFiles(`.github/workflows/*.{yml,yaml}`); for await (const workflowFileUri of workflowFileUris) { + let yamlContent: any | undefined; + try { const fileContent = await fs.readFile(workflowFileUri.fsPath, 'utf8'); + yamlContent = yaml.parse(fileContent); workflows.push({ - name: path.parse(workflowFileUri.fsPath).name, + name: yamlContent.name || path.parse(workflowFileUri.fsPath).name, uri: workflowFileUri, content: yaml.parse(fileContent) }); } catch (error) { workflows.push({ - name: path.parse(workflowFileUri.fsPath).name, + name: (yamlContent ? yamlContent.name : undefined) || path.parse(workflowFileUri.fsPath).name, uri: workflowFileUri, error: 'Failed to parse workflow file' });