diff --git a/package.json b/package.json index ab14ad6..1ab30a4 100644 --- a/package.json +++ b/package.json @@ -156,6 +156,12 @@ "title": "Start", "icon": "$(debug-start)" }, + { + "category": "GitHub Local Actions", + "command": "githubLocalActions.fixPermissions", + "title": "Fix Permissions", + "icon": "$(unlock)" + }, { "category": "GitHub Local Actions", "command": "githubLocalActions.runAllWorkflows", @@ -366,6 +372,10 @@ "command": "githubLocalActions.startComponent", "when": "never" }, + { + "command": "githubLocalActions.fixPermissions", + "when": "never" + }, { "command": "githubLocalActions.runAllWorkflows", "when": "never" @@ -555,6 +565,11 @@ "when": "view == components && viewItem =~ /^githubLocalActions.component_Not Running.*/", "group": "inline@1" }, + { + "command": "githubLocalActions.fixPermissions", + "when": "view == components && viewItem =~ /^githubLocalActions.component_Invalid Permissions.*/", + "group": "inline@1" + }, { "command": "githubLocalActions.runAllWorkflows", "when": "view == workflows && viewItem =~ /^githubLocalActions.workspaceFolderWorkflows.*/ && workspaceFolderCount > 1", diff --git a/src/act.ts b/src/act.ts index 0e6f989..c4b9139 100644 --- a/src/act.ts +++ b/src/act.ts @@ -2,7 +2,7 @@ import * as childProcess from "child_process"; import * as fs from "fs/promises"; import * as path from "path"; import sanitize from "sanitize-filename"; -import { CustomExecution, env, EventEmitter, ExtensionContext, Pseudoterminal, ShellExecution, TaskDefinition, TaskGroup, TaskPanelKind, TaskRevealKind, tasks, TaskScope, TerminalDimensions, Uri, window, workspace, WorkspaceFolder } from "vscode"; +import { commands, CustomExecution, env, EventEmitter, ExtensionContext, Pseudoterminal, ShellExecution, TaskDefinition, TaskGroup, TaskPanelKind, TaskRevealKind, tasks, TaskScope, TerminalDimensions, Uri, window, workspace, WorkspaceFolder } from "vscode"; import { ComponentsManager } from "./componentsManager"; import { ConfigurationManager, Platform, Section } from "./configurationManager"; import { componentsTreeDataProvider, historyTreeDataProvider } from './extension'; @@ -70,8 +70,8 @@ export interface CommandArgs { } export class Act { - static command: string = 'act'; - static githubCliCommand: string = 'gh act'; + static defaultActCommand: string = 'act'; + static githubCliActCommand: string = 'gh act'; context: ExtensionContext; storageManager: StorageManager; secretManager: SecretManager; @@ -127,6 +127,7 @@ export class Act { this.installationCommands = { 'Homebrew': 'brew install act', 'Nix': 'nix run nixpkgs#act', + 'Arch': 'pacman -Syu act', 'AUR': 'yay -Syu act', 'COPR': 'dnf copr enable goncalossilva/act && dnf install act-cli', 'GitHub CLI': '(gh auth status || gh auth login) && gh extension install https://github.com/nektos/gh-act' @@ -174,20 +175,29 @@ export class Act { tasks.onDidEndTaskProcess(async e => { const taskDefinition = e.execution.task.definition; if (taskDefinition.type === 'nektos/act installation' && e.exitCode === 0) { - // Update base act command based on installation method - if (taskDefinition.ghCliInstall) { - await ConfigurationManager.set(Section.actCommand, Act.githubCliCommand); - } else { - await ConfigurationManager.set(Section.actCommand, Act.command); - } - + this.updateActCommand(taskDefinition.ghCliInstall ? Act.githubCliActCommand : Act.defaultActCommand); componentsTreeDataProvider.refresh(); } }); } static getActCommand() { - return ConfigurationManager.get(Section.actCommand) || Act.command; + return ConfigurationManager.get(Section.actCommand) || Act.defaultActCommand; + } + + updateActCommand(newActCommand: string) { + const actCommand = ConfigurationManager.get(Section.actCommand); + + if (newActCommand !== actCommand) { + window.showInformationMessage(`The act command is currently set to "${actCommand}". Once the installation is complete, it is recommended to update this to "${newActCommand}" for this selected installation method.`, 'Proceed', 'Manually Edit').then(async value => { + if (value === 'Proceed') { + await ConfigurationManager.set(Section.actCommand, newActCommand); + componentsTreeDataProvider.refresh(); + } else if (value === 'Manually Edit') { + await commands.executeCommand('workbench.action.openSettings', ConfigurationManager.getSearchTerm(Section.actCommand)); + } + }); + } } async runAllWorkflows(workspaceFolder: WorkspaceFolder) { @@ -454,7 +464,10 @@ export class Act { await tasks.executeTask({ name: 'nektos/act', detail: 'Install nektos/act', - definition: { type: 'nektos/act installation', ghCliInstall: command.includes('gh-act') }, + definition: { + type: 'nektos/act installation', + ghCliInstall: command.includes('gh-act') + }, source: 'GitHub Local Actions', scope: TaskScope.Workspace, isBackground: true, diff --git a/src/componentsManager.ts b/src/componentsManager.ts index d0cd127..1ace6dd 100644 --- a/src/componentsManager.ts +++ b/src/componentsManager.ts @@ -15,6 +15,7 @@ export interface Component { information: string, installation: () => Promise, start?: () => Promise, + fixPermissions?: () => Promise, message?: string } @@ -22,7 +23,8 @@ export enum CliStatus { Installed = 'Installed', NotInstalled = 'Not Installed', Running = 'Running', - NotRunning = 'Not Running' + NotRunning = 'Not Running', + InvalidPermissions = 'Invalid Permissions' } export enum ExtensionStatus { @@ -31,10 +33,13 @@ export enum ExtensionStatus { } export class ComponentsManager { + static actVersionRegExp: RegExp = /act version (.+)/; + static dockerVersionRegExp: RegExp = /Docker Engine Version:\s(.+)/; + async getComponents(): Promise[]> { const components: Component[] = []; - const actCliInfo = await this.getCliInfo(`${Act.getActCommand()} --version`, /act version (.+)/, false, false); + const actCliInfo = await this.getCliInfo(`${Act.getActCommand()} --version`, ComponentsManager.actVersionRegExp, false, false); components.push({ name: 'nektos/act', icon: 'terminal', @@ -110,6 +115,8 @@ export class ComponentsManager { } }); } + + act.updateActCommand(Act.defaultActCommand); } else if (selectedInstallationMethod.link) { await env.openExternal(Uri.parse(selectedInstallationMethod.link)); window.showInformationMessage('Once nektos/act is successfully installed, add it to your shell\'s PATH and then refresh the components view.', 'Refresh').then(async value => { @@ -117,6 +124,8 @@ export class ComponentsManager { componentsTreeDataProvider.refresh(); } }); + + act.updateActCommand(Act.defaultActCommand); } else { await act.install(selectedInstallationMethod.label); } @@ -124,7 +133,7 @@ export class ComponentsManager { } }); - const dockerCliInfo = await this.getCliInfo('docker version', /Client:\n.+\n\sVersion:\s+(.+)/, true, true); + const dockerCliInfo = await this.getCliInfo(`docker version --format "Docker Engine Version: {{.Client.Version}}"`, ComponentsManager.dockerVersionRegExp, true, true); const dockerDesktopPath = ConfigurationManager.get(Section.dockerDesktopPath); components.push({ name: 'Docker Engine', @@ -146,7 +155,9 @@ export class ComponentsManager { await tasks.executeTask({ name: 'Docker Engine', detail: 'Start Docker Engine', - definition: { type: 'GitHub Local Actions' }, + definition: { + type: 'Start Docker Engine' + }, source: 'GitHub Local Actions', scope: TaskScope.Workspace, isBackground: true, @@ -162,7 +173,7 @@ export class ComponentsManager { problemMatchers: [], runOptions: {}, group: TaskGroup.Build, - execution: new ShellExecution('sudo dockerd', { executable: env.shell }) + execution: new ShellExecution('systemctl start docker', { executable: env.shell }) }); } else { window.showErrorMessage(`Invalid environment: ${process.platform}`, 'Report an Issue').then(async value => { @@ -180,7 +191,7 @@ export class ComponentsManager { await delay(4000); // Check again for docker status - const newDockerCliInfo = await this.getCliInfo('docker version', /Client:\n.+\n\sVersion:\s+(.+)/, true, true); + const newDockerCliInfo = await this.getCliInfo(`docker version --format "Docker Engine Version: {{.Client.Version}}"`, ComponentsManager.dockerVersionRegExp, true, true); if (dockerCliInfo.status !== newDockerCliInfo.status) { componentsTreeDataProvider.refresh(); } else { @@ -199,6 +210,55 @@ export class ComponentsManager { }); } }); + }, + fixPermissions: async () => { + if (process.platform === Platform.linux) { + window.showInformationMessage('By default, the Docker daemon binds to a Unix socket owned by the root user. To manage Docker as a non-root user, a Unix group called "docker" should be created with your user added to it.', 'Proceed', 'Learn More').then(async value => { + if (value === 'Proceed') { + await tasks.executeTask({ + name: 'Docker Engine', + detail: 'Fix Docker Engine Permissions', + definition: { + type: 'Fix Docker Engine Permissions' + }, + 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 groupadd docker; sudo usermod -aG docker $USER') + }); + + window.withProgress({ location: { viewId: ComponentsTreeDataProvider.VIEW_ID } }, async () => { + // Delay 4 seconds for Docker to be started + const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + await delay(4000); + + // Check again for docker status + const newDockerCliInfo = await this.getCliInfo(`docker version --format "Docker Engine Version: {{.Client.Version}}"`, ComponentsManager.dockerVersionRegExp, true, true); + if (dockerCliInfo.status !== newDockerCliInfo.status) { + componentsTreeDataProvider.refresh(); + } else { + window.showInformationMessage('You may need to restart your PC for these changes to take affect.'); + } + }); + } else if (value === 'Learn More') { + await env.openExternal(Uri.parse('https://docs.docker.com/engine/install/linux-postinstall/#manage-docker-as-a-non-root-user')); + } + }); + } else { + window.showErrorMessage(`Permissions cannot be automatically fixed for ${process.platform} environment.`); + } } }); @@ -235,7 +295,7 @@ export class ComponentsManager { async getUnreadyComponents(): Promise[]> { const components = await this.getComponents(); - return components.filter(component => component.required && [CliStatus.NotInstalled, CliStatus.NotRunning, ExtensionStatus.NotActivated].includes(component.status)); + return components.filter(component => component.required && [CliStatus.NotInstalled, CliStatus.NotRunning, CliStatus.InvalidPermissions, ExtensionStatus.NotActivated].includes(component.status)); } async getCliInfo(command: string, versionRegex: RegExp, ignoreError: boolean, checksIfRunning: boolean): Promise<{ version?: string, status: CliStatus }> { @@ -247,7 +307,9 @@ export class ComponentsManager { if (ignoreError && version) { resolve({ version: version[1], - status: CliStatus.NotRunning + status: (process.platform === Platform.linux && error.message.toLowerCase().includes('permission denied')) ? + CliStatus.InvalidPermissions : + CliStatus.NotRunning }); } else { resolve({ diff --git a/src/configurationManager.ts b/src/configurationManager.ts index e6c8c84..4d38acd 100644 --- a/src/configurationManager.ts +++ b/src/configurationManager.ts @@ -35,7 +35,7 @@ export namespace ConfigurationManager { let actCommand = ConfigurationManager.get(Section.actCommand); if (!actCommand) { - await ConfigurationManager.set(Section.actCommand, Act.command); + await ConfigurationManager.set(Section.actCommand, Act.defaultActCommand); } } diff --git a/src/views/components/componentsTreeDataProvider.ts b/src/views/components/componentsTreeDataProvider.ts index 725f931..99f5413 100644 --- a/src/views/components/componentsTreeDataProvider.ts +++ b/src/views/components/componentsTreeDataProvider.ts @@ -27,9 +27,15 @@ export default class ComponentsTreeDataProvider implements TreeDataProvider { + const fixPermissions = componentTreeItem.component.fixPermissions; + if (fixPermissions) { + await fixPermissions(); + this.refresh(); } - - this.refresh(); }) ); } diff --git a/src/views/decorationProvider.ts b/src/views/decorationProvider.ts index a35d255..5654579 100644 --- a/src/views/decorationProvider.ts +++ b/src/views/decorationProvider.ts @@ -22,12 +22,12 @@ export class DecorationProvider implements FileDecorationProvider { badge: '✅', color: new ThemeColor('GitHubLocalActions.green') }; - } else if (!required && (status === CliStatus.NotInstalled || status === CliStatus.NotRunning || status === ExtensionStatus.NotActivated)) { + } else if (!required && (status === CliStatus.NotInstalled || status === CliStatus.NotRunning || status === CliStatus.InvalidPermissions || status === ExtensionStatus.NotActivated)) { return { badge: '⚠️', color: new ThemeColor('GitHubLocalActions.yellow') }; - } else if (required && (status === CliStatus.NotInstalled || status === CliStatus.NotRunning || status === ExtensionStatus.NotActivated)) { + } else if (required && (status === CliStatus.NotInstalled || status === CliStatus.NotRunning || status === CliStatus.InvalidPermissions || status === ExtensionStatus.NotActivated)) { return { badge: '❌', color: new ThemeColor('GitHubLocalActions.red')