import * as path from "path"; import { Uri, window, workspace, WorkspaceFolder } from "vscode"; import { act } from "./extension"; import { GitHubManager } from "./githubManager"; import { SecretManager } from "./secretManager"; import { StorageKey, StorageManager } from "./storageManager"; export interface Settings { secrets: Setting[]; secretFiles: SettingFile[]; variables: Setting[]; variableFiles: SettingFile[]; inputs: Setting[]; inputFiles: SettingFile[]; runners: Setting[]; payloadFiles: SettingFile[]; environments: Setting[]; } export interface Setting { key: string, value: string, password: boolean, selected: boolean, visible: Visibility } export interface SettingFile { name: string, path: string, selected: boolean } export enum Visibility { show = 'show', hide = 'hide' } export enum SettingFileName { secretFile = '.secrets', variableFile = '.env', inputFile = '.input', payloadFile = 'payload.json' } export class SettingsManager { storageManager: StorageManager; secretManager: SecretManager; githubManager: GitHubManager; static secretsRegExp: RegExp = /\${{\s*secrets\.(.*?)\s*}}/g; static variablesRegExp: RegExp = /\${{\s*vars\.(.*?)(?:\s*==\s*(.*?))?\s*}}/g; static inputsRegExp: RegExp = /\${{\s*(?:inputs|github\.event\.inputs)\.(.*?)(?:\s*==\s*(.*?))?\s*}}/g; static runnersRegExp: RegExp = /runs-on:\s*(.+)/g; constructor(storageManager: StorageManager, secretManager: SecretManager) { this.storageManager = storageManager; this.secretManager = secretManager; this.githubManager = new GitHubManager(); } 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); const secretFiles = (await this.getSettingFiles(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); const variableFiles = (await this.getSettingFiles(workspaceFolder, StorageKey.VariableFiles)).filter(variableFile => !isUserSelected || variableFile.selected); const inputs = (await this.getSetting(workspaceFolder, SettingsManager.inputsRegExp, StorageKey.Inputs, false, Visibility.show)).filter(input => !isUserSelected || (input.selected && input.value)); const inputFiles = (await this.getSettingFiles(workspaceFolder, StorageKey.InputFiles)).filter(inputFile => !isUserSelected || inputFile.selected); const runners = (await this.getSetting(workspaceFolder, SettingsManager.runnersRegExp, StorageKey.Runners, false, Visibility.show)).filter(runner => !isUserSelected || (runner.selected && runner.value)); const payloadFiles = (await this.getSettingFiles(workspaceFolder, StorageKey.PayloadFiles)).filter(payloadFile => !isUserSelected || payloadFile.selected); const environments = await this.getEnvironments(workspaceFolder); return { secrets: secrets, secretFiles: secretFiles, variables: variables, variableFiles: variableFiles, inputs: inputs, inputFiles: inputFiles, runners: runners, payloadFiles: payloadFiles, environments: environments }; } async getSetting(workspaceFolder: WorkspaceFolder, regExp: RegExp, storageKey: StorageKey, password: boolean, visible: Visibility): Promise { const settings: Setting[] = []; const workflows = await act.workflowsManager.getWorkflows(workspaceFolder); for (const workflow of workflows) { if (!workflow.fileContent) { continue; } const workflowSettings = this.findInWorkflow(workflow.fileContent, regExp, password, visible); for (const workflowSetting of workflowSettings) { const existingSetting = settings.find(setting => setting.key === workflowSetting.key); if (!existingSetting) { settings.push(workflowSetting); } } } const existingSettings = this.storageManager.get<{ [path: string]: Setting[] }>(storageKey) || {}; if (existingSettings[workspaceFolder.uri.fsPath]) { for (const [index, setting] of settings.entries()) { const existingSetting = existingSettings[workspaceFolder.uri.fsPath].find(existingSetting => existingSetting.key === setting.key); if (existingSetting) { let value: string; if (storageKey === StorageKey.Secrets) { value = await this.secretManager.get(workspaceFolder, storageKey, setting.key) || ""; } else { value = existingSetting.value; } settings[index] = { key: setting.key, value: value, password: existingSetting.password, selected: existingSetting.selected, visible: existingSetting.visible }; } } } existingSettings[workspaceFolder.uri.fsPath] = settings; this.storageManager.update(storageKey, existingSettings); return settings; } async getSettingFiles(workspaceFolder: WorkspaceFolder, storageKey: StorageKey): Promise { const existingSettingFiles = this.storageManager.get<{ [path: string]: SettingFile[] }>(storageKey) || {}; return existingSettingFiles[workspaceFolder.uri.fsPath] || []; } async getEnvironments(workspaceFolder: WorkspaceFolder): Promise { const environments: Setting[] = []; const workflows = await act.workflowsManager.getWorkflows(workspaceFolder); for (const workflow of workflows) { if (!workflow.yaml) { continue; } const jobs = workflow.yaml?.jobs; if (jobs) { for (const details of Object.values(jobs)) { if (details.environment) { const existingEnvironment = environments.find(environment => environment.key === details.environment); if (!existingEnvironment) { environments.push({ key: details.environment, value: '', password: false, selected: false, visible: Visibility.show }); } } } } } return environments; } async createSettingFile(workspaceFolder: WorkspaceFolder, storageKey: StorageKey, settingFileName: string) { const settingFileUri = Uri.file(path.join(workspaceFolder.uri.fsPath, settingFileName)); try { await workspace.fs.stat(settingFileUri); window.showErrorMessage(`A file or folder named ${settingFileName} already exists at ${workspaceFolder.uri.fsPath}. Please choose another name.`); } catch (error: any) { try { await workspace.fs.writeFile(settingFileUri, new TextEncoder().encode('')); await this.locateSettingFile(workspaceFolder, storageKey, [settingFileUri]); const document = await workspace.openTextDocument(settingFileUri); await window.showTextDocument(document); } catch (error: any) { window.showErrorMessage(`Failed to create ${settingFileName}. Error: ${error}`); } } } async locateSettingFile(workspaceFolder: WorkspaceFolder, storageKey: StorageKey, settingFilesUris: Uri[]) { const settingFilesPaths = (await act.settingsManager.getSettingFiles(workspaceFolder, storageKey)).map(settingFile => settingFile.path); const existingSettingFileNames: string[] = []; for await (const uri of settingFilesUris) { const settingFileName = path.parse(uri.fsPath).name; if (settingFilesPaths.includes(uri.fsPath)) { existingSettingFileNames.push(settingFileName); } else { const newSettingFile: SettingFile = { name: path.parse(uri.fsPath).base, path: uri.fsPath, selected: false }; await act.settingsManager.editSettingFile(workspaceFolder, newSettingFile, storageKey); } } if (existingSettingFileNames.length > 0) { window.showErrorMessage(`The following file(s) have already been added: ${existingSettingFileNames.join(', ')}`); } } async editSettingFile(workspaceFolder: WorkspaceFolder, newSettingFile: SettingFile, storageKey: StorageKey) { const existingSettingFiles = this.storageManager.get<{ [path: string]: SettingFile[] }>(storageKey) || {}; if (existingSettingFiles[workspaceFolder.uri.fsPath]) { const index = existingSettingFiles[workspaceFolder.uri.fsPath].findIndex(settingFile => settingFile.path === newSettingFile.path); if (index > -1) { existingSettingFiles[workspaceFolder.uri.fsPath][index] = newSettingFile; } else { existingSettingFiles[workspaceFolder.uri.fsPath].push(newSettingFile); } } else { existingSettingFiles[workspaceFolder.uri.fsPath] = [newSettingFile]; } await this.storageManager.update(storageKey, existingSettingFiles); } async removeSettingFile(workspaceFolder: WorkspaceFolder, settingFile: SettingFile, storageKey: StorageKey) { const existingSettingFiles = this.storageManager.get<{ [path: string]: SettingFile[] }>(storageKey) || {}; if (existingSettingFiles[workspaceFolder.uri.fsPath]) { const settingFileIndex = existingSettingFiles[workspaceFolder.uri.fsPath].findIndex(settingFile => settingFile.path === settingFile.path); if (settingFileIndex > -1) { existingSettingFiles[workspaceFolder.uri.fsPath].splice(settingFileIndex, 1); } } await this.storageManager.update(storageKey, existingSettingFiles); } async deleteSettingFile(workspaceFolder: WorkspaceFolder, settingFile: SettingFile, storageKey: StorageKey) { try { await workspace.fs.delete(Uri.file(settingFile.path)); } catch (error: any) { try { await workspace.fs.stat(Uri.file(settingFile.path)); window.showErrorMessage(`Failed to delete file. Error ${error}`); return; } catch (error: any) { } } await this.removeSettingFile(workspaceFolder, settingFile, storageKey); } async editSetting(workspaceFolder: WorkspaceFolder, newSetting: Setting, storageKey: StorageKey) { const value = newSetting.value; if (storageKey === StorageKey.Secrets) { newSetting.value = ''; } const existingSettings = this.storageManager.get<{ [path: string]: Setting[] }>(storageKey) || {}; if (existingSettings[workspaceFolder.uri.fsPath]) { const index = existingSettings[workspaceFolder.uri.fsPath].findIndex(setting => setting.key === newSetting.key); if (index > -1) { existingSettings[workspaceFolder.uri.fsPath][index] = newSetting; } else { existingSettings[workspaceFolder.uri.fsPath].push(newSetting); } } else { existingSettings[workspaceFolder.uri.fsPath] = [newSetting]; } await this.storageManager.update(storageKey, existingSettings); if (storageKey === StorageKey.Secrets) { if (value === '') { await this.secretManager.delete(workspaceFolder, storageKey, newSetting.key); } else { await this.secretManager.store(workspaceFolder, storageKey, newSetting.key, value); } } } private findInWorkflow(content: string, regExp: RegExp, password: boolean, visible: Visibility) { const results: Setting[] = []; const matches = content.matchAll(regExp); for (const match of matches) { results.push({ key: match[1], value: '', password: password, selected: false, visible: visible }); } return results; } }