Update installation and start of components

Signed-off-by: Sanjula Ganepola <sanjulagane@gmail.com>
This commit is contained in:
Sanjula Ganepola
2024-09-30 21:30:32 -04:00
parent 499886721f
commit ef1a7145ce
6 changed files with 309 additions and 45 deletions

View File

@@ -72,6 +72,18 @@
"title": "Information", "title": "Information",
"icon": "$(info)" "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", "category": "GitHub Local Actions",
"command": "githubLocalActions.runAllWorkflows", "command": "githubLocalActions.runAllWorkflows",
@@ -119,6 +131,14 @@
"command": "githubLocalActions.information", "command": "githubLocalActions.information",
"when": "never" "when": "never"
}, },
{
"command": "githubLocalActions.installComponent",
"when": "never"
},
{
"command": "githubLocalActions.startComponent",
"when": "never"
},
{ {
"command": "githubLocalActions.runAllWorkflows", "command": "githubLocalActions.runAllWorkflows",
"when": "never" "when": "never"
@@ -177,6 +197,16 @@
"when": "view == components && viewItem =~ /^githubLocalActions.component.*/", "when": "view == components && viewItem =~ /^githubLocalActions.component.*/",
"group": "inline@0" "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", "command": "githubLocalActions.openWorkflow",
"when": "view == workflows && viewItem =~ /^githubLocalActions.workflow.*/", "when": "view == workflows && viewItem =~ /^githubLocalActions.workflow.*/",

View File

@@ -1,6 +1,6 @@
import * as child_process from 'child_process'; import * as child_process from 'child_process';
import * as path from "path"; 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 { ComponentsManager } from "./componentsManager";
import { SettingsManager } from './settingsManager'; import { SettingsManager } from './settingsManager';
import { Workflow, WorkflowsManager } from "./workflowsManager"; import { Workflow, WorkflowsManager } from "./workflowsManager";
@@ -51,11 +51,64 @@ export class Act {
componentsManager: ComponentsManager; componentsManager: ComponentsManager;
workflowsManager: WorkflowsManager; workflowsManager: WorkflowsManager;
settingsManager: SettingsManager; settingsManager: SettingsManager;
installationCommands: { [packageManager: string]: string };
prebuiltExecutables: { [architecture: string]: string };
constructor() { constructor() {
this.componentsManager = new ComponentsManager(); this.componentsManager = new ComponentsManager();
this.workflowsManager = new WorkflowsManager(); this.workflowsManager = new WorkflowsManager();
this.settingsManager = new SettingsManager(); 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() { async runAllWorkflows() {
@@ -97,7 +150,7 @@ export class Act {
presentationOptions: { presentationOptions: {
reveal: TaskRevealKind.Always, reveal: TaskRevealKind.Always,
focus: false, focus: false,
clear: false, clear: true,
close: false, close: false,
echo: true, echo: true,
panel: TaskPanelKind.Dedicated, panel: TaskPanelKind.Dedicated,
@@ -123,14 +176,10 @@ export class Act {
writeEmitter.fire(`Timestamp: ${new Date().toLocaleTimeString()}\r\n`); writeEmitter.fire(`Timestamp: ${new Date().toLocaleTimeString()}\r\n`);
writeEmitter.fire(`\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) => { exec.stdout.on('data', (data) => {
const output = data.toString(); const output = data.toString().replaceAll('\n', '\r\n');
writeEmitter.fire(output); writeEmitter.fire(output);
if (output.includes('success')) {
window.showInformationMessage('Command succeeded!');
}
}); });
exec.stderr.on('data', (data) => { exec.stderr.on('data', (data) => {
@@ -143,12 +192,36 @@ export class Act {
}); });
}, },
close: () => { close: () => { }
// TODO:
}
}; };
} })
)
}); });
} }
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)
});
}
}
} }

View File

@@ -1,19 +1,24 @@
import * as child_process from "child_process"; 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<T extends CliStatus | ExtensionStatus> { export interface Component<T extends CliStatus | ExtensionStatus> {
name: string, name: string,
icon: string, icon: string,
version?: string, version?: string,
status: T, status: T,
required: boolean,
information: string, information: string,
required: boolean installation: () => Promise<void>,
start?: () => Promise<void>,
message?: string message?: string
} }
export enum CliStatus { export enum CliStatus {
Installed = 'Installed', Installed = 'Installed',
NotInstalled = 'Not Installed' NotInstalled = 'Not Installed',
Running = 'Running',
NotRunning = 'Not Running'
} }
export enum ExtensionStatus { export enum ExtensionStatus {
@@ -25,26 +30,150 @@ export class ComponentsManager {
async getComponents(): Promise<Component<CliStatus | ExtensionStatus>[]> { async getComponents(): Promise<Component<CliStatus | ExtensionStatus>[]> {
const components: Component<CliStatus | ExtensionStatus>[] = []; const components: Component<CliStatus | ExtensionStatus>[] = [];
const actCliInfo = await this.getCliInfo('act', /act version (.+)/); const actCliInfo = await this.getCliInfo('act --version', /act version (.+)/, false, false);
components.push({ components.push({
name: 'nektos/act CLI', name: 'nektos/act CLI',
icon: 'terminal', icon: 'terminal',
version: actCliInfo.version, version: actCliInfo.version,
status: actCliInfo.status, status: actCliInfo.status,
required: true,
information: 'https://github.com/nektos/act', 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 dockerCliInfo = await this.getCliInfo('docker version', /Client:\n.+\n\sVersion:\s+(.+)/, true, true);
const dockerEngineVersion = '2.0.0';
const dockerEngineStatus = CliStatus.Installed;
components.push({ components.push({
name: 'Docker Engine', name: 'Docker Engine',
icon: 'dashboard', icon: 'dashboard',
version: dockerEngineVersion, version: dockerCliInfo.version,
status: dockerEngineStatus, status: dockerCliInfo.status,
required: true,
information: 'https://docs.docker.com/engine', 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'); const githubActionsInfo = await this.getExtensionInfo('github.vscode-github-actions');
@@ -53,19 +182,25 @@ export class ComponentsManager {
icon: 'extensions', icon: 'extensions',
version: githubActionsInfo.version, version: githubActionsInfo.version,
status: githubActionsInfo.status, status: githubActionsInfo.status,
information: 'https://marketplace.visualstudio.com/items?itemName=GitHub.vscode-github-actions',
required: false, 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.' 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({ components.push({
name: 'GitHub CLI', name: 'GitHub CLI',
icon: 'terminal', icon: 'terminal',
version: githubCliInfo.version, version: githubCliInfo.version,
status: githubCliInfo.status, status: githubCliInfo.status,
information: 'https://cli.github.com',
required: false, 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.' 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)); 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) => { return new Promise<{ version?: string, status: CliStatus }>((resolve, reject) => {
child_process.exec(`${component} --version`, (error, stdout, stderr) => { child_process.exec(command, (error, stdout, stderr) => {
if (error) { const version = stdout?.match(versionRegex);
resolve({
status: CliStatus.NotInstalled
});
} else {
const version = stdout.match(versionRegex);
resolve({ if (error) {
version: version ? version[1] : undefined, if (ignoreError && version) {
status: CliStatus.Installed 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
});
}
} }
}); });
}); });

View File

@@ -9,8 +9,8 @@ export default class ComponentTreeItem extends TreeItem implements GithubLocalAc
constructor(component: Component<CliStatus | ExtensionStatus>) { constructor(component: Component<CliStatus | ExtensionStatus>) {
super(component.name, TreeItemCollapsibleState.None); super(component.name, TreeItemCollapsibleState.None);
this.component = component; this.component = component;
this.description = component.version; this.description = component.version ? `(${component.version}) - ${component.status}` : `${component.status}`;
this.contextValue = ComponentTreeItem.contextValue; this.contextValue = `${ComponentTreeItem.contextValue}_${component.status}`;
this.iconPath = new ThemeIcon(component.icon); this.iconPath = new ThemeIcon(component.icon);
this.resourceUri = Uri.parse(`${ComponentTreeItem.contextValue}:${component.name}?status=${component.status}&required=${component.required}`, true); this.resourceUri = Uri.parse(`${ComponentTreeItem.contextValue}:${component.name}?status=${component.status}&required=${component.required}`, true);
this.tooltip = `Name: ${component.name}\n` + this.tooltip = `Name: ${component.name}\n` +

View File

@@ -18,7 +18,19 @@ export default class ComponentsTreeDataProvider implements TreeDataProvider<Gith
this.refresh(); this.refresh();
}), }),
commands.registerCommand('githubLocalActions.information', async (componentTreeItem: ComponentTreeItem) => { commands.registerCommand('githubLocalActions.information', async (componentTreeItem: ComponentTreeItem) => {
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();
}) })
); );
} }

View File

@@ -12,17 +12,17 @@ export class DecorationProvider implements FileDecorationProvider {
const status = params.get('status'); const status = params.get('status');
const required = params.get('required') === 'true'; const required = params.get('required') === 'true';
if (status === CliStatus.Installed || status === ExtensionStatus.Activated) { if (status === CliStatus.Installed || status === CliStatus.Running || status === ExtensionStatus.Activated) {
return { return {
badge: '✅', badge: '✅',
color: new ThemeColor('GitHubLocalActions.green') 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 { return {
badge: '⚠️', badge: '⚠️',
color: new ThemeColor('GitHubLocalActions.yellow') 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 { return {
badge: '❌', badge: '❌',
color: new ThemeColor('GitHubLocalActions.red') color: new ThemeColor('GitHubLocalActions.red')