Allow the same act options to be added multiple times (#142)

* extract act options from cli command

* remove special options handled by the tree view

* Refactor logic to get act options (#1)

* Refactor get options logic
* Remove bool type options from having descriptions
* Force option description to be uppercase

Signed-off-by: Sanjula Ganepola <sanjulagane@gmail.com>

* disable generic early exit options as well

* filter out stringArray default value

* this removes default values like `[]` regardless if they are sent

* fix quote consitency

* Fix lint errors and move default options to separate func

Signed-off-by: Sanjula Ganepola <sanjulagane@gmail.com>

* Allow multiple of the same options to support stringArray type

Signed-off-by: Sanjula Ganepola <sanjulagane@gmail.com>

* Fix comment

Signed-off-by: Sanjula Ganepola <sanjulagane@gmail.com>

* Exclude list options

Signed-off-by: Sanjula Ganepola <sanjulagane@gmail.com>

---------

Signed-off-by: Sanjula Ganepola <sanjulagane@gmail.com>
Co-authored-by: Christopher Homberger <christopher.homberger@web.de>
This commit is contained in:
Sanjula Ganepola
2025-01-18 19:22:10 -05:00
committed by GitHub
parent dc301e47e0
commit 375ca40178
4 changed files with 229 additions and 219 deletions

View File

@@ -733,7 +733,7 @@
}, },
{ {
"command": "githubLocalActions.removeCustomSetting", "command": "githubLocalActions.removeCustomSetting",
"when": "view == settings && viewItem =~ /^githubLocalActions.((secret|variable|input|payload|)File|option(?!s)).*/", "when": "view == settings && viewItem =~ /^githubLocalActions.((secret|variable|input|payload)File|option(?!s)).*/",
"group": "inline@1" "group": "inline@1"
}, },
{ {

View File

@@ -1,5 +1,6 @@
import * as childProcess from "child_process"; import * as childProcess from "child_process";
import * as fs from "fs/promises"; import * as fs from "fs/promises";
import * as os from "os";
import * as path from "path"; import * as path from "path";
import sanitize from "sanitize-filename"; import sanitize from "sanitize-filename";
import { commands, 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";
@@ -81,6 +82,7 @@ export enum Option {
Job = "--job", Job = "--job",
Json = "--json", Json = "--json",
List = "--list", List = "--list",
ListOptions = "--list-options",
LocalRepository = "--local-repository", LocalRepository = "--local-repository",
LogPrefixJobId = "--log-prefix-job-id", LogPrefixJobId = "--log-prefix-job-id",
ManPage = "--man-page", ManPage = "--man-page",
@@ -121,10 +123,10 @@ export interface CommandArgs {
} }
export interface ActOption { export interface ActOption {
default: string;
name: string, name: string,
description: string description: string
type: string type: string,
default: string
} }
export class Act { export class Act {
@@ -323,7 +325,7 @@ export class Act {
} }
if (!eventExists) { if (!eventExists) {
window.showErrorMessage(`No workflows triggered by the ${event} event.`) window.showErrorMessage(`No workflows triggered by the ${event} event.`);
} }
} else { } else {
window.showErrorMessage('No workflows found.'); window.showErrorMessage('No workflows found.');
@@ -333,21 +335,225 @@ export class Act {
getAllOptions(): Promise<ActOption[]> { getAllOptions(): Promise<ActOption[]> {
return new Promise<ActOption[]>((resolve, reject) => { return new Promise<ActOption[]>((resolve, reject) => {
const exec = childProcess.spawn( const exec = childProcess.spawn(
`${Act.getActCommand()} --list-options`, `${Act.getActCommand()} ${Option.ListOptions}`,
{ {
shell: true, shell: true,
} }
); );
let options: string = ""
exec.stdout.on('data', b => options += b.toString()); let options: string = "";
exec.stdout.on('data', data => {
options += data.toString();
});
exec.on('exit', async (code, signal) => { exec.on('exit', async (code, signal) => {
if (code === 0) { if (code === 0) {
resolve(JSON.parse(options)); resolve(JSON.parse(options));
} else { } else {
reject(new Error("Not supported by this binary")); reject(new Error(`The ${Option.ListOptions} option is not supported by this binary`));
} }
}); });
}) });
}
/**
* This is to be used until act adopts "--list-options"
* https://github.com/nektos/act/pull/2557
*/
getDefaultOptions() {
return [
{
label: Option.ActionCachePath,
description: this.getCacheDirectory(['act']),
detail: 'Defines the path where the actions get cached and host workspaces are created.'
},
{
label: Option.ActionOfflineMode,
detail: 'If action contents exists, it will not be fetched and pulled again. If this is turned on, it will turn off force pull.'
},
{
label: Option.Actor,
description: 'nektos/act',
detail: 'User that triggered the event.'
},
{
label: Option.ArtifactServerAddr,
description: '',
detail: 'Defines the address to which the artifact server binds. If not set, nektos/act will use the outbound IP address of this machine. This means that it will try to access the internet and return the local IP address of the connection. If the machine cannot access the internet, it returns a preferred IP address from network interfaces. If no IP address is found, this will not be set.'
},
{
label: Option.ArtifactServerPath,
description: '',
detail: 'Defines the path where the artifact server stores uploads and retrieves downloads from. If not specified, the artifact server will not start.'
},
{
label: Option.ArtifactServerPort,
description: '34567',
detail: 'Defines the port where the artifact server listens.'
},
{
label: Option.Bind,
detail: 'Bind working directory to container, rather than copy.'
},
{
label: Option.CacheServerAddr,
description: '',
detail: 'Defines the address to which the cache server binds. If not set, nektos/act will use the outbound IP address of this machine. This means that it will try to access the internet and return the local IP address of the connection. If the machine cannot access the internet, it returns a preferred IP address from network interfaces. If no IP address is found, this will not be set.'
},
{
label: Option.CacheServerPath,
description: this.getCacheDirectory(['actcache']),
detail: 'Defines the path where the cache server stores caches.'
},
{
label: Option.CacheServerPort,
description: '0',
detail: 'Defines the port where the artifact server listens. 0 means a randomly available port.'
},
{
label: Option.ContainerArchitecture,
description: '',
detail: 'The architecture which should be used to run containers (e.g.: linux/amd64). If not specified, the host default architecture will be used. This requires Docker server API Version 1.41+ (ignored on earlier Docker server platforms).'
},
{
label: Option.ContainerCapAdd,
description: '',
detail: 'Kernel capabilities to add to the workflow containers (e.g. SYS_PTRACE).'
},
{
label: Option.ContainerCapDrop,
description: '',
detail: 'Kernel capabilities to remove from the workflow containers (e.g. SYS_PTRACE).'
},
{
label: Option.ContainerDaemonSocket,
description: '',
detail: 'URI to Docker Engine socket (e.g.: unix://~/.docker/run/docker.sock or - to disable bind mounting the socket).'
},
{
label: Option.ContainerOptions,
description: '',
detail: 'Custom docker container options for the job container without an options property in the job definition.'
},
{
label: Option.DefaultBranch,
description: '',
detail: 'The name of the main branch.'
},
{
label: Option.DetectEvent,
detail: 'Use first event type from workflow as event that triggered the workflow.'
},
{
label: Option.Directory,
description: '.',
detail: 'The working directory used when running a nektos/act command.'
},
{
label: Option.DryRun,
detail: 'Disable container creation and validate only workflow correctness.'
},
{
label: Option.GithubInstance,
description: 'github.com',
detail: 'The GitHub instance to use. Only use this when using GitHub Enterprise Server.'
},
{
label: Option.InsecureSecrets,
detail: 'Show secrets while printing logs (NOT RECOMMENDED!).'
},
{
label: Option.Json,
detail: 'Output logs in json format.'
},
{
label: Option.LocalRepository,
description: '',
detail: 'Replaces the specified repository and ref with a local folder (e.g. https://github.com/test/test@v0=/home/act/test or test/test@v0=/home/act/test, the latter matches any hosts or protocols).'
},
{
label: Option.LogPrefixJobId,
detail: 'Output the job id within non-json logs instead of the entire name.'
},
{
label: Option.Network,
description: 'host',
detail: 'Sets a docker network name.'
},
{
label: Option.NoCacheServer,
detail: 'Disable cache server.'
},
{
label: Option.NoRecurse,
detail: 'Flag to disable running workflows from subdirectories of specified path in --workflows/-W flag.'
},
{
label: Option.NoSkipCheckout,
detail: 'Do not skip actions/checkout.'
},
{
label: Option.Privileged,
detail: 'Use privileged mode.'
},
{
label: Option.Pull,
detail: 'Pull docker image(s) even if already present.'
},
{
label: Option.Quiet,
detail: 'Disable logging of output from steps.'
},
{
label: Option.Rebuild,
detail: 'Rebuild local action docker image(s) even if already present.'
},
{
label: Option.RemoteName,
description: 'origin',
detail: 'Git remote name that will be used to retrieve the URL of Git repo.'
},
{
label: Option.ReplaceGheActionTokenWithGithubCom,
description: '',
detail: 'If you are using replace-ghe-action-with-github-com and you want to use private actions on GitHub, you have to set a personal access token.'
},
{
label: Option.ReplaceGheActionWithGithubCom,
description: '',
detail: 'If you are using GitHub Enterprise Server and allow specified actions from GitHub (github.com), you can set actions on this.'
},
{
label: Option.Reuse,
detail: 'Don\'t remove container(s) on successfully completed workflow(s) to maintain state between runs.'
},
{
label: Option.Rm,
detail: 'Automatically remove container(s)/volume(s) after a workflow(s) failure.'
},
{
label: Option.UseGitignore,
detail: 'Controls whether paths specified in a .gitignore file should be copied into the container.'
},
{
label: Option.UseNewActionCache,
detail: 'Enable using the new Action Cache for storing Actions locally.'
},
{
label: Option.Userns,
description: '',
detail: 'User namespace to use.'
},
{
label: Option.Verbose,
detail: 'Enable verbose output.'
}
];
}
getCacheDirectory(paths: string[]) {
const userHomeDir = os.homedir();
const cacheHomeDir = process.env.XDG_CACHE_HOME || path.join(userHomeDir, '.cache');
return path.join(cacheHomeDir, ...paths);
} }
async runCommand(commandArgs: CommandArgs) { async runCommand(commandArgs: CommandArgs) {
@@ -594,7 +800,7 @@ export class Act {
historyTreeDataProvider.refresh(); historyTreeDataProvider.refresh();
} }
await this.storageManager.update(StorageKey.WorkspaceHistory, this.historyManager.workspaceHistory); await this.storageManager.update(StorageKey.WorkspaceHistory, this.historyManager.workspaceHistory);
} };
}; };
let shell = env.shell; let shell = env.shell;

View File

@@ -214,7 +214,7 @@ export class SettingsManager {
} }
} }
async editCustomSetting(workspaceFolder: WorkspaceFolder, newCustomSetting: CustomSetting, storageKey: StorageKey) { async editCustomSetting(workspaceFolder: WorkspaceFolder, newCustomSetting: CustomSetting, storageKey: StorageKey, forceAppend: boolean = false) {
const existingCustomSettings = this.storageManager.get<{ [path: string]: CustomSetting[] }>(storageKey) || {}; const existingCustomSettings = this.storageManager.get<{ [path: string]: CustomSetting[] }>(storageKey) || {};
if (existingCustomSettings[workspaceFolder.uri.fsPath]) { if (existingCustomSettings[workspaceFolder.uri.fsPath]) {
const index = existingCustomSettings[workspaceFolder.uri.fsPath] const index = existingCustomSettings[workspaceFolder.uri.fsPath]
@@ -223,7 +223,7 @@ export class SettingsManager {
customSetting.name === newCustomSetting.name : customSetting.name === newCustomSetting.name :
customSetting.path === newCustomSetting.path customSetting.path === newCustomSetting.path
); );
if (index > -1) { if (index > -1 && !forceAppend) {
existingCustomSettings[workspaceFolder.uri.fsPath][index] = newCustomSetting; existingCustomSettings[workspaceFolder.uri.fsPath][index] = newCustomSetting;
} else { } else {
existingCustomSettings[workspaceFolder.uri.fsPath].push(newCustomSetting); existingCustomSettings[workspaceFolder.uri.fsPath].push(newCustomSetting);
@@ -240,7 +240,7 @@ export class SettingsManager {
if (existingCustomSettings[workspaceFolder.uri.fsPath]) { if (existingCustomSettings[workspaceFolder.uri.fsPath]) {
const index = existingCustomSettings[workspaceFolder.uri.fsPath].findIndex(customSetting => const index = existingCustomSettings[workspaceFolder.uri.fsPath].findIndex(customSetting =>
storageKey === StorageKey.Options ? storageKey === StorageKey.Options ?
customSetting.name === existingCustomSetting.name : (customSetting.name === existingCustomSetting.name && customSetting.path === existingCustomSetting.path) :
customSetting.path === existingCustomSetting.path customSetting.path === existingCustomSetting.path
); );
if (index > -1) { if (index > -1) {

View File

@@ -1,5 +1,3 @@
import * as os from "os";
import * as path from "path";
import { CancellationToken, commands, EventEmitter, ExtensionContext, QuickPickItem, QuickPickItemKind, ThemeIcon, TreeCheckboxChangeEvent, TreeDataProvider, TreeItem, TreeItemCheckboxState, Uri, window, workspace } from "vscode"; import { CancellationToken, commands, EventEmitter, ExtensionContext, QuickPickItem, QuickPickItemKind, ThemeIcon, TreeCheckboxChangeEvent, TreeDataProvider, TreeItem, TreeItemCheckboxState, Uri, window, workspace } from "vscode";
import { Option } from "../../act"; import { Option } from "../../act";
import { act } from "../../extension"; import { act } from "../../extension";
@@ -121,7 +119,8 @@ export default class SettingsTreeDataProvider implements TreeDataProvider<Github
try { try {
const allOptions = await act.getAllOptions(); const allOptions = await act.getAllOptions();
const specialOptions: string[] = [ const excludeOptions: string[] = [
// The following options can be added directly from the Settings view
Option.Input, Option.Input,
Option.InputFile, Option.InputFile,
Option.Var, Option.Var,
@@ -135,202 +134,16 @@ export default class SettingsTreeDataProvider implements TreeDataProvider<Github
Option.BugReport, Option.BugReport,
Option.Watch, Option.Watch,
Option.List, Option.List,
Option.Version Option.Version,
Option.ListOptions
]; ];
options = allOptions.map(opt => ({ options = allOptions.map(opt => ({
label: "--" + opt.name, label: "--" + opt.name,
description: opt.type !== 'bool' ? opt.type === 'stringArray' ? '' : opt.default : undefined, description: opt.type !== 'bool' ? (opt.type === 'stringArray' ? '' : opt.default) : undefined,
detail: opt.description.charAt(0).toUpperCase() + opt.description.slice(1) detail: opt.description ? (opt.description.charAt(0).toUpperCase() + opt.description.slice(1)) : undefined
})).filter(opt => !specialOptions.includes(opt.label)); })).filter(opt => !excludeOptions.includes(opt.label));
} catch (error: any) { } catch (error: any) {
options = [ options = act.getDefaultOptions();
{
label: Option.ActionCachePath,
description: this.getCacheDirectory(['act']),
detail: 'Defines the path where the actions get cached and host workspaces are created.'
},
{
label: Option.ActionOfflineMode,
detail: 'If action contents exists, it will not be fetched and pulled again. If this is turned on, it will turn off force pull.'
},
{
label: Option.Actor,
description: 'nektos/act',
detail: 'User that triggered the event.'
},
{
label: Option.ArtifactServerAddr,
description: '',
detail: 'Defines the address to which the artifact server binds. If not set, nektos/act will use the outbound IP address of this machine. This means that it will try to access the internet and return the local IP address of the connection. If the machine cannot access the internet, it returns a preferred IP address from network interfaces. If no IP address is found, this will not be set.'
},
{
label: Option.ArtifactServerPath,
description: '',
detail: 'Defines the path where the artifact server stores uploads and retrieves downloads from. If not specified, the artifact server will not start.'
},
{
label: Option.ArtifactServerPort,
description: '34567',
detail: 'Defines the port where the artifact server listens.'
},
{
label: Option.Bind,
detail: 'Bind working directory to container, rather than copy.'
},
{
label: Option.CacheServerAddr,
description: '',
detail: 'Defines the address to which the cache server binds. If not set, nektos/act will use the outbound IP address of this machine. This means that it will try to access the internet and return the local IP address of the connection. If the machine cannot access the internet, it returns a preferred IP address from network interfaces. If no IP address is found, this will not be set.'
},
{
label: Option.CacheServerPath,
description: this.getCacheDirectory(['actcache']),
detail: 'Defines the path where the cache server stores caches.'
},
{
label: Option.CacheServerPort,
description: '0',
detail: 'Defines the port where the artifact server listens. 0 means a randomly available port.'
},
{
label: Option.ContainerArchitecture,
description: '',
detail: 'The architecture which should be used to run containers (e.g.: linux/amd64). If not specified, the host default architecture will be used. This requires Docker server API Version 1.41+ (ignored on earlier Docker server platforms).'
},
{
label: Option.ContainerCapAdd,
description: '',
detail: 'Kernel capabilities to add to the workflow containers (e.g. SYS_PTRACE).'
},
{
label: Option.ContainerCapDrop,
description: '',
detail: 'Kernel capabilities to remove from the workflow containers (e.g. SYS_PTRACE).'
},
{
label: Option.ContainerDaemonSocket,
description: '',
detail: 'URI to Docker Engine socket (e.g.: unix://~/.docker/run/docker.sock or - to disable bind mounting the socket).'
},
{
label: Option.ContainerOptions,
description: '',
detail: 'Custom docker container options for the job container without an options property in the job definition.'
},
{
label: Option.DefaultBranch,
description: '',
detail: 'The name of the main branch.'
},
{
label: Option.DetectEvent,
detail: 'Use first event type from workflow as event that triggered the workflow.'
},
{
label: Option.Directory,
description: '.',
detail: 'The working directory used when running a nektos/act command.'
},
{
label: Option.DryRun,
detail: 'Disable container creation and validate only workflow correctness.'
},
{
label: Option.GithubInstance,
description: 'github.com',
detail: 'The GitHub instance to use. Only use this when using GitHub Enterprise Server.'
},
{
label: Option.InsecureSecrets,
detail: 'Show secrets while printing logs (NOT RECOMMENDED!).'
},
{
label: Option.Json,
detail: 'Output logs in json format.'
},
{
label: Option.LocalRepository,
description: '',
detail: 'Replaces the specified repository and ref with a local folder (e.g. https://github.com/test/test@v0=/home/act/test or test/test@v0=/home/act/test, the latter matches any hosts or protocols).'
},
{
label: Option.LogPrefixJobId,
detail: 'Output the job id within non-json logs instead of the entire name.'
},
{
label: Option.Network,
description: 'host',
detail: 'Sets a docker network name.'
},
{
label: Option.NoCacheServer,
detail: 'Disable cache server.'
},
{
label: Option.NoRecurse,
detail: 'Flag to disable running workflows from subdirectories of specified path in --workflows/-W flag.'
},
{
label: Option.NoSkipCheckout,
detail: 'Do not skip actions/checkout.'
},
{
label: Option.Privileged,
detail: 'Use privileged mode.'
},
{
label: Option.Pull,
detail: 'Pull docker image(s) even if already present.'
},
{
label: Option.Quiet,
detail: 'Disable logging of output from steps.'
},
{
label: Option.Rebuild,
detail: 'Rebuild local action docker image(s) even if already present.'
},
{
label: Option.RemoteName,
description: 'origin',
detail: 'Git remote name that will be used to retrieve the URL of Git repo.'
},
{
label: Option.ReplaceGheActionTokenWithGithubCom,
description: '',
detail: 'If you are using replace-ghe-action-with-github-com and you want to use private actions on GitHub, you have to set a personal access token.'
},
{
label: Option.ReplaceGheActionWithGithubCom,
description: '',
detail: 'If you are using GitHub Enterprise Server and allow specified actions from GitHub (github.com), you can set actions on this.'
},
{
label: Option.Reuse,
detail: 'Don\'t remove container(s) on successfully completed workflow(s) to maintain state between runs.'
},
{
label: Option.Rm,
detail: 'Automatically remove container(s)/volume(s) after a workflow(s) failure.'
},
{
label: Option.UseGitignore,
detail: 'Controls whether paths specified in a .gitignore file should be copied into the container.'
},
{
label: Option.UseNewActionCache,
detail: 'Enable using the new Action Cache for storing Actions locally.'
},
{
label: Option.Userns,
description: '',
detail: 'User namespace to use.'
},
{
label: Option.Verbose,
detail: 'Enable verbose output.'
}
];
} }
options.forEach((option, index) => { options.forEach((option, index) => {
@@ -338,10 +151,6 @@ export default class SettingsTreeDataProvider implements TreeDataProvider<Github
options[index].iconPath = new ThemeIcon('symbol-property'); options[index].iconPath = new ThemeIcon('symbol-property');
}); });
const settings = await act.settingsManager.getSettings(optionsTreeItem.workspaceFolder, false);
const optionNames = settings.options.map(option => option.name);
options = options.filter(option => !optionNames.includes(option.label));
const selectedOption = await window.showQuickPick(options, { const selectedOption = await window.showQuickPick(options, {
title: 'Select the option to add', title: 'Select the option to add',
placeHolder: 'Option', placeHolder: 'Option',
@@ -374,7 +183,8 @@ export default class SettingsTreeDataProvider implements TreeDataProvider<Github
default: selectedOption.description, default: selectedOption.description,
description: selectedOption.detail description: selectedOption.detail
}, },
optionsTreeItem.storageKey optionsTreeItem.storageKey,
true
); );
this.refresh(); this.refresh();
} }
@@ -648,10 +458,4 @@ export default class SettingsTreeDataProvider implements TreeDataProvider<Github
return items; return items;
} }
} }
getCacheDirectory(paths: string[]) {
const userHomeDir = os.homedir();
const cacheHomeDir = process.env.XDG_CACHE_HOME || path.join(userHomeDir, '.cache');
return path.join(cacheHomeDir, ...paths);
}
} }