From ef1a7145cee3def5b98e812e3a4d166d71209a0f Mon Sep 17 00:00:00 2001 From: Sanjula Ganepola Date: Mon, 30 Sep 2024 21:30:32 -0400 Subject: [PATCH] Update installation and start of components Signed-off-by: Sanjula Ganepola --- package.json | 30 +++ src/act.ts | 99 +++++++-- src/componentsManager.ts | 201 +++++++++++++++--- src/views/components/component.ts | 4 +- .../components/componentsTreeDataProvider.ts | 14 +- src/views/decorationProvider.ts | 6 +- 6 files changed, 309 insertions(+), 45 deletions(-) diff --git a/package.json b/package.json index 0289b42..3b46151 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,18 @@ "title": "Information", "icon": "$(info)" }, + { + "category": "GitHub Local Actions", + "command": "githubLocalActions.installComponent", + "title": "Install", + "icon": "$(desktop-download)" + }, + { + "category": "GitHub Local Actions", + "command": "githubLocalActions.startComponent", + "title": "Start", + "icon": "$(debug-start)" + }, { "category": "GitHub Local Actions", "command": "githubLocalActions.runAllWorkflows", @@ -119,6 +131,14 @@ "command": "githubLocalActions.information", "when": "never" }, + { + "command": "githubLocalActions.installComponent", + "when": "never" + }, + { + "command": "githubLocalActions.startComponent", + "when": "never" + }, { "command": "githubLocalActions.runAllWorkflows", "when": "never" @@ -177,6 +197,16 @@ "when": "view == components && viewItem =~ /^githubLocalActions.component.*/", "group": "inline@0" }, + { + "command": "githubLocalActions.installComponent", + "when": "view == components && viewItem =~ /^githubLocalActions.component_(Not Installed|Not Activated).*/", + "group": "inline@1" + }, + { + "command": "githubLocalActions.startComponent", + "when": "view == components && viewItem =~ /^githubLocalActions.component_Not Running.*/", + "group": "inline@1" + }, { "command": "githubLocalActions.openWorkflow", "when": "view == workflows && viewItem =~ /^githubLocalActions.workflow.*/", diff --git a/src/act.ts b/src/act.ts index cab63b2..254d480 100644 --- a/src/act.ts +++ b/src/act.ts @@ -1,6 +1,6 @@ import * as child_process from 'child_process'; import * as path from "path"; -import { commands, CustomExecution, EventEmitter, Pseudoterminal, TaskDefinition, TaskGroup, TaskPanelKind, TaskRevealKind, tasks, TaskScope, TerminalDimensions, window, workspace } from "vscode"; +import { commands, CustomExecution, env, EventEmitter, Pseudoterminal, ShellExecution, TaskDefinition, TaskGroup, TaskPanelKind, TaskRevealKind, tasks, TaskScope, TerminalDimensions, window, workspace } from "vscode"; import { ComponentsManager } from "./componentsManager"; import { SettingsManager } from './settingsManager'; import { Workflow, WorkflowsManager } from "./workflowsManager"; @@ -51,11 +51,64 @@ export class Act { componentsManager: ComponentsManager; workflowsManager: WorkflowsManager; settingsManager: SettingsManager; + installationCommands: { [packageManager: string]: string }; + prebuiltExecutables: { [architecture: string]: string }; constructor() { this.componentsManager = new ComponentsManager(); this.workflowsManager = new WorkflowsManager(); this.settingsManager = new SettingsManager(); + + switch (process.platform) { + case 'win32': + this.installationCommands = { + 'Chocolatey': 'choco install act-cli', + 'Winget': 'winget install nektos.act', + 'Scoop': 'scoop install act', + 'GitHub CLI': 'gh extension install https://github.com/nektos/gh-act' + }; + + this.prebuiltExecutables = { + 'Windows 64-bit (arm64/aarch64)': 'https://github.com/nektos/act/releases/latest/download/act_Windows_arm64.zip', + 'Windows 64-bit (amd64/x86_64)': 'https://github.com/nektos/act/releases/latest/download/act_Windows_x86_64.zip', + 'Windows 32-bit (armv7)': 'https://github.com/nektos/act/releases/latest/download/act_Windows_armv7.zip', + 'Windows 32-bit (i386/x86)': 'https://github.com/nektos/act/releases/latest/download/act_Windows_i386.zip' + }; + break; + case 'darwin': + this.installationCommands = { + 'Homebrew': 'brew install act', + 'Nix': 'nix run nixpkgs#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' + }; + break; + case 'linux': + this.installationCommands = { + 'Homebrew': 'brew install act', + 'Nix': 'nix run nixpkgs#act', + 'AUR': 'yay -Syu 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', + 'Linux 32-bit (armv7)': 'https://github.com/nektos/act/releases/latest/download/act_Linux_armv7.tar.gz', + 'Linux 32-bit (armv6)': 'https://github.com/nektos/act/releases/latest/download/act_Linux_armv6.tar.gz', + 'Linux 32-bit (i386/x86)': 'https://github.com/nektos/act/releases/latest/download/act_Linux_i386.tar.gz', + }; + break; + default: + this.installationCommands = {}; + this.prebuiltExecutables = {}; + } } async runAllWorkflows() { @@ -97,7 +150,7 @@ export class Act { presentationOptions: { reveal: TaskRevealKind.Always, focus: false, - clear: false, + clear: true, close: false, echo: true, panel: TaskPanelKind.Dedicated, @@ -123,14 +176,10 @@ export class Act { writeEmitter.fire(`Timestamp: ${new Date().toLocaleTimeString()}\r\n`); writeEmitter.fire(`\r\n`); - const exec = child_process.spawn(command, { cwd: workspaceFolder.uri.fsPath, shell: '/usr/bin/bash' }); + const exec = child_process.spawn(command, { cwd: workspaceFolder.uri.fsPath, shell: env.shell }); exec.stdout.on('data', (data) => { - const output = data.toString(); + const output = data.toString().replaceAll('\n', '\r\n'); writeEmitter.fire(output); - - if (output.includes('success')) { - window.showInformationMessage('Command succeeded!'); - } }); exec.stderr.on('data', (data) => { @@ -143,12 +192,36 @@ export class Act { }); }, - close: () => { - // TODO: - } + close: () => { } }; - } - ) + }) }); } + + async install(packageManager: string) { + const command = this.installationCommands[packageManager]; + if (command) { + await tasks.executeTask({ + name: 'nektos/act', + detail: 'Install nektos/act', + definition: { type: 'GitHub Local Actions' }, + source: 'GitHub Local Actions', + scope: TaskScope.Workspace, + isBackground: true, + presentationOptions: { + reveal: TaskRevealKind.Always, + focus: false, + clear: true, + close: false, + echo: true, + panel: TaskPanelKind.Shared, + showReuseMessage: false + }, + problemMatchers: [], + runOptions: {}, + group: TaskGroup.Build, + execution: new ShellExecution(command) + }); + } + } } \ No newline at end of file diff --git a/src/componentsManager.ts b/src/componentsManager.ts index 7e66408..3579bfd 100644 --- a/src/componentsManager.ts +++ b/src/componentsManager.ts @@ -1,19 +1,24 @@ import * as child_process from "child_process"; -import { extensions } from "vscode"; +import { commands, env, extensions, QuickPickItemKind, ShellExecution, TaskGroup, TaskPanelKind, TaskRevealKind, tasks, TaskScope, ThemeIcon, Uri, window } from "vscode"; +import { act } from "./extension"; export interface Component { name: string, icon: string, version?: string, status: T, + required: boolean, information: string, - required: boolean + installation: () => Promise, + start?: () => Promise, message?: string } export enum CliStatus { Installed = 'Installed', - NotInstalled = 'Not Installed' + NotInstalled = 'Not Installed', + Running = 'Running', + NotRunning = 'Not Running' } export enum ExtensionStatus { @@ -25,26 +30,150 @@ export class ComponentsManager { async getComponents(): Promise[]> { const components: Component[] = []; - const actCliInfo = await this.getCliInfo('act', /act version (.+)/); + const actCliInfo = await this.getCliInfo('act --version', /act version (.+)/, false, false); components.push({ name: 'nektos/act CLI', icon: 'terminal', version: actCliInfo.version, status: actCliInfo.status, + required: true, information: 'https://github.com/nektos/act', - required: true + installation: async () => { + const installationMethods: any[] = [ + { + label: 'Software Package Managers', + kind: QuickPickItemKind.Separator + } + ]; + + Object.entries(act.installationCommands).map(([packageManager, command]) => { + installationMethods.push({ + label: packageManager, + description: command, + iconPath: new ThemeIcon('terminal'), + }); + }); + + installationMethods.push( + { + label: 'Pre-built Artifacts', + kind: QuickPickItemKind.Separator + }, + { + label: 'Install Pre-built Executable', + description: 'Install pre-built executable', + iconPath: new ThemeIcon('package') + }, + { + label: 'Bash Script Installation', + description: 'Install pre-built act executable using bash script', + iconPath: new ThemeIcon('code'), + link: 'https://nektosact.com/installation/index.html#bash-script' + }, + { + label: 'Build From Source', + description: 'Build nektos/act yourself', + iconPath: new ThemeIcon('tools'), + link: 'https://nektosact.com/installation/index.html#build-from-source' + } + ); + + const selectedInstallationMethod = await window.showQuickPick(installationMethods, { + title: 'Select the method of installation', + placeHolder: 'Installation Method' + }); + + if (selectedInstallationMethod) { + if (selectedInstallationMethod.label === 'Install Pre-built Executable') { + const prebuiltExecutables = Object.entries(act.prebuiltExecutables).map(([architecture, link]) => { + return { + label: architecture, + iconPath: new ThemeIcon('package'), + link: link + }; + }); + + const selectedPrebuiltExecutable = await window.showQuickPick(prebuiltExecutables, { + title: 'Select the prebuilt executable to download', + placeHolder: 'Prebuilt executable' + }); + + if (selectedPrebuiltExecutable) { + 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'); + } + }); + } + } else if (selectedInstallationMethod.link) { + 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'); + } + }); + } else { + await act.install(selectedInstallationMethod.label); + } + } + } }); - // TODO: Fix docker status - const dockerEngineVersion = '2.0.0'; - const dockerEngineStatus = CliStatus.Installed; + const dockerCliInfo = await this.getCliInfo('docker version', /Client:\n.+\n\sVersion:\s+(.+)/, true, true); components.push({ name: 'Docker Engine', icon: 'dashboard', - version: dockerEngineVersion, - status: dockerEngineStatus, + version: dockerCliInfo.version, + status: dockerCliInfo.status, + required: true, information: 'https://docs.docker.com/engine', - required: true + installation: async () => { + await env.openExternal(Uri.parse('https://docs.docker.com/engine/install')); + }, + start: async () => { + //TODO: Make the below win32 and darwin paths customizable + switch (process.platform) { + case 'win32': + await env.openExternal(Uri.parse('C:/Program Files/Docker/Docker/Docker Desktop.exe')); + break; + case 'darwin': + await env.openExternal(Uri.parse('/Applications/Docker.app')); + break; + case 'linux': + await tasks.executeTask({ + name: 'Docker Engine', + detail: 'Start Docker Engine', + definition: { type: 'GitHub Local Actions' }, + source: 'GitHub Local Actions', + scope: TaskScope.Workspace, + isBackground: true, + presentationOptions: { + reveal: TaskRevealKind.Always, + focus: false, + clear: true, + close: false, + echo: true, + panel: TaskPanelKind.Shared, + showReuseMessage: false + }, + problemMatchers: [], + runOptions: {}, + group: TaskGroup.Build, + execution: new ShellExecution('sudo dockerd') + }); + break; + default: + window.showErrorMessage(`Invalid environment: ${process.platform}`); + return; + } + + window.showInformationMessage('Once Docker Engine is successfully started, refresh the components view.', 'Refresh').then(async value => { + if (value === 'Refresh') { + await commands.executeCommand('githubLocalActions.refreshComponents'); + } + }); + } }); const githubActionsInfo = await this.getExtensionInfo('github.vscode-github-actions'); @@ -53,19 +182,25 @@ export class ComponentsManager { icon: 'extensions', version: githubActionsInfo.version, status: githubActionsInfo.status, - information: 'https://marketplace.visualstudio.com/items?itemName=GitHub.vscode-github-actions', required: false, + information: 'https://marketplace.visualstudio.com/items?itemName=GitHub.vscode-github-actions', + installation: async () => { + await env.openExternal(Uri.parse('https://marketplace.visualstudio.com/items?itemName=GitHub.vscode-github-actions')); + }, message: 'GitHub Actions extension is not required, but is recommended to take advantage of workflow editor features.' }); - const githubCliInfo = await this.getCliInfo('gh', /gh version (.+)/); + const githubCliInfo = await this.getCliInfo('gh', /gh version (.+)/, false, false); components.push({ name: 'GitHub CLI', icon: 'terminal', version: githubCliInfo.version, status: githubCliInfo.status, - information: 'https://cli.github.com', required: false, + information: 'https://cli.github.com', + installation: async () => { + await env.openExternal(Uri.parse('https://cli.github.com')); + }, message: 'GitHub CLI is not required, but is recommended if you plan to use it to retrieve GitHub tokens.' }); @@ -77,20 +212,34 @@ export class ComponentsManager { return components.filter(component => component.required && (component.status === CliStatus.NotInstalled || component.status === ExtensionStatus.NotActivated)); } - async getCliInfo(component: string, versionRegex: RegExp): Promise<{ version?: string, status: CliStatus }> { + async getCliInfo(command: string, versionRegex: RegExp, ignoreError: boolean, checksIfRunning: boolean): Promise<{ version?: string, status: CliStatus }> { return new Promise<{ version?: string, status: CliStatus }>((resolve, reject) => { - child_process.exec(`${component} --version`, (error, stdout, stderr) => { - if (error) { - resolve({ - status: CliStatus.NotInstalled - }); - } else { - const version = stdout.match(versionRegex); + child_process.exec(command, (error, stdout, stderr) => { + const version = stdout?.match(versionRegex); - resolve({ - version: version ? version[1] : undefined, - status: CliStatus.Installed - }); + if (error) { + if (ignoreError && version) { + resolve({ + version: version[1], + status: CliStatus.NotRunning + }); + } else { + resolve({ + status: CliStatus.NotInstalled + }); + } + } else { + if(checksIfRunning) { + resolve({ + version: version ? version[1] : undefined, + status: CliStatus.Running + }); + } else { + resolve({ + version: version ? version[1] : undefined, + status: CliStatus.Installed + }); + } } }); }); diff --git a/src/views/components/component.ts b/src/views/components/component.ts index 433feab..13853b2 100644 --- a/src/views/components/component.ts +++ b/src/views/components/component.ts @@ -9,8 +9,8 @@ export default class ComponentTreeItem extends TreeItem implements GithubLocalAc constructor(component: Component) { super(component.name, TreeItemCollapsibleState.None); this.component = component; - this.description = component.version; - this.contextValue = ComponentTreeItem.contextValue; + this.description = component.version ? `(${component.version}) - ${component.status}` : `${component.status}`; + this.contextValue = `${ComponentTreeItem.contextValue}_${component.status}`; this.iconPath = new ThemeIcon(component.icon); this.resourceUri = Uri.parse(`${ComponentTreeItem.contextValue}:${component.name}?status=${component.status}&required=${component.required}`, true); this.tooltip = `Name: ${component.name}\n` + diff --git a/src/views/components/componentsTreeDataProvider.ts b/src/views/components/componentsTreeDataProvider.ts index 1f84946..39a9b5c 100644 --- a/src/views/components/componentsTreeDataProvider.ts +++ b/src/views/components/componentsTreeDataProvider.ts @@ -18,7 +18,19 @@ export default class ComponentsTreeDataProvider implements TreeDataProvider { - env.openExternal(Uri.parse(componentTreeItem.component.information)); + await env.openExternal(Uri.parse(componentTreeItem.component.information)); + }), + commands.registerCommand('githubLocalActions.installComponent', async (componentTreeItem: ComponentTreeItem) => { + await componentTreeItem.component.installation(); + this.refresh(); + }), + commands.registerCommand('githubLocalActions.startComponent', async (componentTreeItem: ComponentTreeItem) => { + const start = componentTreeItem.component.start; + if (start) { + await start(); + } + + this.refresh(); }) ); } diff --git a/src/views/decorationProvider.ts b/src/views/decorationProvider.ts index 9e4181c..7f0dd98 100644 --- a/src/views/decorationProvider.ts +++ b/src/views/decorationProvider.ts @@ -12,17 +12,17 @@ export class DecorationProvider implements FileDecorationProvider { const status = params.get('status'); const required = params.get('required') === 'true'; - if (status === CliStatus.Installed || status === ExtensionStatus.Activated) { + if (status === CliStatus.Installed || status === CliStatus.Running || status === ExtensionStatus.Activated) { return { badge: '✅', color: new ThemeColor('GitHubLocalActions.green') }; - } else if (!required && (status === CliStatus.NotInstalled || status === ExtensionStatus.NotActivated)) { + } else if (!required && (status === CliStatus.NotInstalled || status === CliStatus.NotRunning|| status === ExtensionStatus.NotActivated)) { return { badge: '⚠️', color: new ThemeColor('GitHubLocalActions.yellow') }; - } else if (required && (status === CliStatus.NotInstalled || status === ExtensionStatus.NotActivated)) { + } else if (required && (status === CliStatus.NotInstalled || status === CliStatus.NotRunning || status === ExtensionStatus.NotActivated)) { return { badge: '❌', color: new ThemeColor('GitHubLocalActions.red')