From 64b16d4e39aeb39ca2b1f765fb768198a1edc57f Mon Sep 17 00:00:00 2001 From: Sanjula Ganepola <32170854+SanjulaGanepola@users.noreply.github.com> Date: Tue, 11 Feb 2025 22:02:35 -0500 Subject: [PATCH] Add support for auto-generating github cli token (#165) * Add support for auto-generating github cli token Signed-off-by: Sanjula Ganepola * Autogenerate Github CLI token when executing act command Signed-off-by: Sanjula Ganepola --------- Signed-off-by: Sanjula Ganepola --- package.json | 36 ++++++++++++++-- src/act.ts | 29 ++++++++----- src/githubManager.ts | 43 ++++++++++++++++++- src/settingsManager.ts | 32 +++++++++++--- src/views/settings/setting.ts | 10 +++-- .../settings/settingsTreeDataProvider.ts | 19 +++++++- 6 files changed, 144 insertions(+), 25 deletions(-) diff --git a/package.json b/package.json index ea3188c..7ccaa7f 100644 --- a/package.json +++ b/package.json @@ -282,6 +282,18 @@ "title": "Hide", "icon": "$(eye-closed)" }, + { + "category": "GitHub Local Actions", + "command": "githubLocalActions.enableGithubCLIToken", + "title": "Enable GitHub CLI Token", + "icon": "$(sync)" + }, + { + "category": "GitHub Local Actions", + "command": "githubLocalActions.disableGithubCLIToken", + "title": "Disable GitHub CLI Token", + "icon": "$(sync-ignored)" + }, { "category": "GitHub Local Actions", "command": "githubLocalActions.createVariableFile", @@ -472,6 +484,14 @@ "command": "githubLocalActions.hide", "when": "never" }, + { + "command": "githubLocalActions.enableGithubCLIToken", + "when": "never" + }, + { + "command": "githubLocalActions.disableGithubCLIToken", + "when": "never" + }, { "command": "githubLocalActions.createVariableFile", "when": "never" @@ -673,14 +693,24 @@ }, { "command": "githubLocalActions.show", - "when": "view == settings && viewItem =~ /^githubLocalActions.secret(?!s)_hide.*/", + "when": "view == settings && viewItem =~ /^githubLocalActions.secret(?!s)_hide(?!_generate).*/", "group": "inline@0" }, { "command": "githubLocalActions.hide", - "when": "view == settings && viewItem =~ /^githubLocalActions.secret(?!s)_show.*/", + "when": "view == settings && viewItem =~ /^githubLocalActions.secret(?!s)_show(?!_generate).*/", "group": "inline@0" }, + { + "command": "githubLocalActions.enableGithubCLIToken", + "when": "view == settings && viewItem =~ /^githubLocalActions.secret(?!s).*_manual.*/", + "group": "inline@2" + }, + { + "command": "githubLocalActions.disableGithubCLIToken", + "when": "view == settings && viewItem =~ /^githubLocalActions.secret(?!s).*_generate.*/", + "group": "inline@2" + }, { "command": "githubLocalActions.createVariableFile", "when": "view == settings && viewItem =~ /^githubLocalActions.variables.*/", @@ -743,7 +773,7 @@ }, { "command": "githubLocalActions.editSetting", - "when": "view == settings && viewItem =~ /^githubLocalActions.(secret|variable|input|runner)(?!(File|s)).*/", + "when": "view == settings && viewItem =~ /^githubLocalActions.(secret|variable|input|runner)(?!(File|s))(?!_(show|hide)_generate).*/", "group": "inline@1" } ] diff --git a/src/act.ts b/src/act.ts index 07b6ab2..0f607a9 100644 --- a/src/act.ts +++ b/src/act.ts @@ -9,7 +9,7 @@ import { ConfigurationManager, Platform, Section } from "./configurationManager" import { componentsTreeDataProvider, historyTreeDataProvider } from './extension'; import { HistoryManager, HistoryStatus } from './historyManager'; import { SecretManager } from "./secretManager"; -import { SettingsManager } from './settingsManager'; +import { Mode, SettingsManager } from './settingsManager'; import { StorageKey, StorageManager } from './storageManager'; import { Utils } from "./utils"; import { Job, Workflow, WorkflowsManager } from "./workflowsManager"; @@ -836,20 +836,29 @@ export class Act { break; } + // Process environment variables for child process + const processedSecrets: Record = {}; + for (const secret of settings.secrets) { + if (secret.key === 'GITHUB_TOKEN' && secret.mode === Mode.generate) { + const token = await this.settingsManager.githubManager.getGithubCLIToken(); + if (token) { + processedSecrets[secret.key] = token; + } + } else { + processedSecrets[secret.key] = secret.value!; + } + } + const envVars = { + ...process.env, + ...processedSecrets + }; + const exec = childProcess.spawn( command, { cwd: commandArgs.path, shell: shell, - env: { - ...process.env, - ...settings.secrets - .filter(secret => secret.value) - .reduce((previousValue, currentValue) => { - previousValue[currentValue.key] = currentValue.value; - return previousValue; - }, {} as Record) - } + env: envVars } ); exec.stdout.on('data', handleIO()); diff --git a/src/githubManager.ts b/src/githubManager.ts index 8b56e3a..22080d8 100644 --- a/src/githubManager.ts +++ b/src/githubManager.ts @@ -1,6 +1,7 @@ +import * as childProcess from "child_process"; import { Octokit } from "octokit"; import * as path from "path"; -import { authentication, AuthenticationSession, commands, extensions, window, WorkspaceFolder } from "vscode"; +import { authentication, AuthenticationSession, commands, extensions, ShellExecution, TaskGroup, TaskPanelKind, TaskRevealKind, tasks, TaskScope, window, WorkspaceFolder } from "vscode"; import { GitExtension } from "./import/git"; export interface Response { @@ -152,4 +153,44 @@ export class GitHubManager { return; } } + + public async getGithubCLIToken(): Promise { + return new Promise((resolve, reject) => { + childProcess.exec('gh auth token', (error, stdout, stderr) => { + if (error) { + const errorMessage = (String(stderr).charAt(0).toUpperCase() + String(stderr).slice(1)).trim(); + window.showErrorMessage(`${errorMessage}. Authenticate to GitHub and try again.`, 'Authenticate').then(async value => { + if (value === 'Authenticate') { + await tasks.executeTask({ + name: 'GitHub CLI', + detail: 'Authenticate with a GitHub host', + definition: { + type: 'Authenticate with a GitHub host' + }, + 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('gh auth login') + }); + } + }); + resolve(undefined); + } else { + resolve(stdout.trim()); + } + }); + }); + } } \ No newline at end of file diff --git a/src/settingsManager.ts b/src/settingsManager.ts index 47700c2..c84cd0f 100644 --- a/src/settingsManager.ts +++ b/src/settingsManager.ts @@ -23,7 +23,8 @@ export interface Setting { value: string, password: boolean, selected: boolean, - visible: Visibility + visible: Visibility, + mode: Mode } // This is either a secret/variable/input/payload file or an option @@ -41,6 +42,11 @@ export enum Visibility { hide = 'hide' } +export enum Mode { + generate = 'generate', + manual = 'manual' +} + export enum SettingFileName { secretFile = '.secrets', envFile = '.env', @@ -65,7 +71,17 @@ export class SettingsManager { } async getSettings(workspaceFolder: WorkspaceFolder, isUserSelected: boolean): Promise { - const secrets = (await this.getSetting(workspaceFolder, SettingsManager.secretsRegExp, StorageKey.Secrets, true, Visibility.hide)).filter(secret => !isUserSelected || (secret.selected && secret.value)); + const defaultSecrets: Setting[] = [ + { + key: 'GITHUB_TOKEN', + value: '', + password: true, + selected: false, + visible: Visibility.hide, + mode: Mode.manual + } + ]; + const secrets = (await this.getSetting(workspaceFolder, SettingsManager.secretsRegExp, StorageKey.Secrets, true, Visibility.hide, defaultSecrets)).filter(secret => !isUserSelected || (secret.selected && (secret.value || secret.mode === Mode.generate))); const secretFiles = (await this.getCustomSettings(workspaceFolder, StorageKey.SecretFiles)).filter(secretFile => !isUserSelected || secretFile.selected); const variables = (await this.getSetting(workspaceFolder, SettingsManager.variablesRegExp, StorageKey.Variables, false, Visibility.show)).filter(variable => !isUserSelected || (variable.selected && variable.value)); const variableFiles = (await this.getCustomSettings(workspaceFolder, StorageKey.VariableFiles)).filter(variableFile => !isUserSelected || variableFile.selected); @@ -90,8 +106,8 @@ export class SettingsManager { }; } - async getSetting(workspaceFolder: WorkspaceFolder, regExp: RegExp, storageKey: StorageKey, password: boolean, visible: Visibility): Promise { - const settings: Setting[] = []; + async getSetting(workspaceFolder: WorkspaceFolder, regExp: RegExp, storageKey: StorageKey, password: boolean, visible: Visibility, defaultSettings: Setting[] = []): Promise { + const settings: Setting[] = defaultSettings; const workflows = await act.workflowsManager.getWorkflows(workspaceFolder); for (const workflow of workflows) { @@ -125,7 +141,8 @@ export class SettingsManager { value: value, password: existingSetting.password, selected: existingSetting.selected, - visible: existingSetting.visible + visible: existingSetting.visible, + mode: existingSetting.mode || Mode.manual }; } } @@ -161,7 +178,8 @@ export class SettingsManager { value: '', password: false, selected: false, - visible: Visibility.show + visible: Visibility.show, + mode: Mode.manual }); } } @@ -298,7 +316,7 @@ export class SettingsManager { const matches = content.matchAll(regExp); for (const match of matches) { - results.push({ key: match[1], value: '', password: password, selected: false, visible: visible }); + results.push({ key: match[1], value: '', password: password, selected: false, visible: visible, mode: Mode.manual }); } return results; diff --git a/src/views/settings/setting.ts b/src/views/settings/setting.ts index 45df970..716b142 100644 --- a/src/views/settings/setting.ts +++ b/src/views/settings/setting.ts @@ -1,5 +1,5 @@ import { ThemeIcon, TreeItem, TreeItemCheckboxState, TreeItemCollapsibleState, Uri, WorkspaceFolder } from "vscode"; -import { Setting, Visibility } from "../../settingsManager"; +import { Mode, Setting, Visibility } from "../../settingsManager"; import { StorageKey } from "../../storageManager"; import { GithubLocalActionsTreeItem } from "../githubLocalActionsTreeItem"; @@ -19,11 +19,15 @@ export default class SettingTreeItem extends TreeItem implements GithubLocalActi this.setting = setting; this.storageKey = storageKey; if (setting.password) { - this.description = (setting.visible === Visibility.hide && setting.value) ? '••••••••' : setting.value; + if (setting.mode === Mode.generate) { + this.description = 'Generated by GitHub CLI'; + } else { + this.description = (setting.visible === Visibility.hide && setting.value) ? '••••••••' : setting.value; + } } else { this.description = setting.value; } - this.contextValue = `${treeItem.contextValue}_${setting.password ? setting.visible : ''}`; + this.contextValue = `${treeItem.contextValue}${setting.password ? `_${setting.visible}` : ''}${setting.key === 'GITHUB_TOKEN' ? `_${setting.mode}` : ''}`; this.iconPath = treeItem.iconPath; this.checkboxState = setting.selected ? TreeItemCheckboxState.Checked : TreeItemCheckboxState.Unchecked; this.resourceUri = Uri.parse(`${treeItem.contextValue}:${setting.key}?isSelected=${setting.selected}&hasValue=${setting.value !== ''}`, true); diff --git a/src/views/settings/settingsTreeDataProvider.ts b/src/views/settings/settingsTreeDataProvider.ts index 022d1b3..aa90459 100644 --- a/src/views/settings/settingsTreeDataProvider.ts +++ b/src/views/settings/settingsTreeDataProvider.ts @@ -1,7 +1,7 @@ import { CancellationToken, commands, EventEmitter, ExtensionContext, QuickPickItem, QuickPickItemKind, ThemeIcon, TreeCheckboxChangeEvent, TreeDataProvider, TreeItem, TreeItemCheckboxState, Uri, window, workspace } from "vscode"; import { Option } from "../../act"; import { act } from "../../extension"; -import { SettingFileName, Visibility } from "../../settingsManager"; +import { Mode, SettingFileName, Visibility } from "../../settingsManager"; import { StorageKey } from "../../storageManager"; import { GithubLocalActionsTreeItem } from "../githubLocalActionsTreeItem"; import InputsTreeItem from "./inputs"; @@ -50,6 +50,23 @@ export default class SettingsTreeDataProvider implements TreeDataProvider { + const token = await act.settingsManager.githubManager.getGithubCLIToken(); + if (token) { + const newSetting = settingTreeItem.setting; + newSetting.mode = Mode.generate; + newSetting.value = ''; + newSetting.visible = Visibility.hide; + await act.settingsManager.editSetting(settingTreeItem.workspaceFolder, newSetting, settingTreeItem.storageKey); + this.refresh(); + } + }), + commands.registerCommand('githubLocalActions.disableGithubCLIToken', async (settingTreeItem: SettingTreeItem) => { + const newSetting = settingTreeItem.setting; + newSetting.mode = Mode.manual; + await act.settingsManager.editSetting(settingTreeItem.workspaceFolder, newSetting, settingTreeItem.storageKey); + this.refresh(); + }), commands.registerCommand('githubLocalActions.createVariableFile', async (variablesTreeItem: VariablesTreeItem) => { const variableFileName = await window.showInputBox({ prompt: `Enter the name for the variable file`,