Add github manager with authentication and import variable support
Signed-off-by: Sanjula Ganepola <sanjulagane@gmail.com>
This commit is contained in:
155
src/githubManager.ts
Normal file
155
src/githubManager.ts
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import { Octokit } from "octokit";
|
||||||
|
import * as path from "path";
|
||||||
|
import { authentication, AuthenticationSession, commands, extensions, window, WorkspaceFolder } from "vscode";
|
||||||
|
import { GitExtension } from "./import/git";
|
||||||
|
|
||||||
|
export interface Response<T> {
|
||||||
|
data: T,
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GithubRepository {
|
||||||
|
owner: string,
|
||||||
|
repo: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GithubEnvironment {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GithubVariable {
|
||||||
|
name: string,
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GitHubManager {
|
||||||
|
async getRepository(workspaceFolder: WorkspaceFolder, command: string, args: any[]): Promise<GithubRepository | undefined> {
|
||||||
|
const gitApi = extensions.getExtension<GitExtension>('vscode.git')?.exports.getAPI(1);
|
||||||
|
if (gitApi) {
|
||||||
|
if (gitApi.state === 'initialized') {
|
||||||
|
const repository = gitApi.getRepository(workspaceFolder.uri);
|
||||||
|
|
||||||
|
if (repository) {
|
||||||
|
const remoteOriginUrl = await repository.getConfig('remote.origin.url');
|
||||||
|
|
||||||
|
if (remoteOriginUrl) {
|
||||||
|
const parsedPath = path.parse(remoteOriginUrl);
|
||||||
|
const parsedParentPath = path.parse(parsedPath.dir);
|
||||||
|
|
||||||
|
return {
|
||||||
|
owner: parsedParentPath.name,
|
||||||
|
repo: parsedPath.name
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
window.showErrorMessage('Remote GitHub URL not found.');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
window.showErrorMessage(`${workspaceFolder.name} does not have a Git repository`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
window.showErrorMessage('Git extension is still being initialized. Please try again later.', 'Try Again').then(async value => {
|
||||||
|
if (value && value === 'Try Again') {
|
||||||
|
await commands.executeCommand(command, ...args);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
window.showErrorMessage('Failed to load VS Code Git API.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEnvironments(owner: string, repo: string): Promise<Response<GithubEnvironment[]>> {
|
||||||
|
const environments: Response<GithubEnvironment[]> = {
|
||||||
|
data: []
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.get(
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
'/repos/{owner}/{repo}/environments'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response) {
|
||||||
|
for (const environment of response.environments) {
|
||||||
|
environments.data.push({
|
||||||
|
name: environment.name
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
environments.error = error.message ? error.message : error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return environments;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getVariables(owner: string, repo: string, environment?: string): Promise<Response<GithubVariable[]>> {
|
||||||
|
const variables: Response<GithubVariable[]> = {
|
||||||
|
data: []
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = environment ?
|
||||||
|
await this.get(
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
'/repos/{owner}/{repo}/environments/{environment_name}/variables',
|
||||||
|
{
|
||||||
|
environment_name: environment
|
||||||
|
}
|
||||||
|
) :
|
||||||
|
await this.get(
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
'/repos/{owner}/{repo}/actions/variables'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response) {
|
||||||
|
for (const variable of response.variables) {
|
||||||
|
variables.data.push({
|
||||||
|
name: variable.name,
|
||||||
|
value: variable.value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
variables.error = error.message ? error.message : error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return variables;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async get(owner: string, repo: string, endpoint: string, additionalParams?: Record<string, any>) {
|
||||||
|
const session = await this.getSession();
|
||||||
|
if (!session) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const octokit = new Octokit({
|
||||||
|
auth: session.accessToken
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await octokit.request(`GET ${endpoint}`, {
|
||||||
|
owner: owner,
|
||||||
|
repo: repo,
|
||||||
|
...additionalParams,
|
||||||
|
headers: {
|
||||||
|
'X-GitHub-Api-Version': '2022-11-28'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getSession(): Promise<AuthenticationSession | undefined> {
|
||||||
|
try {
|
||||||
|
return await authentication.getSession('github', ['repo'], { createIfNone: true });
|
||||||
|
} catch (error) {
|
||||||
|
window.showErrorMessage(`Failed to authenticate to GitHub. Error ${error}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -58,7 +58,7 @@ export class HistoryManager {
|
|||||||
|
|
||||||
async viewOutput(history: History) {
|
async viewOutput(history: History) {
|
||||||
try {
|
try {
|
||||||
const document = await workspace.openTextDocument(history.logPath)
|
const document = await workspace.openTextDocument(history.logPath);
|
||||||
await window.showTextDocument(document);
|
await window.showTextDocument(document);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.showErrorMessage(`${history.name} #${history.count} log file not found`);
|
window.showErrorMessage(`${history.name} #${history.count} log file not found`);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { WorkspaceFolder } from "vscode";
|
import { WorkspaceFolder } from "vscode";
|
||||||
import { act } from "./extension";
|
import { act } from "./extension";
|
||||||
|
import { GitHubManager } from "./githubManager";
|
||||||
import { SecretManager } from "./secretManager";
|
import { SecretManager } from "./secretManager";
|
||||||
import { StorageKey, StorageManager } from "./storageManager";
|
import { StorageKey, StorageManager } from "./storageManager";
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ export enum Visibility {
|
|||||||
export class SettingsManager {
|
export class SettingsManager {
|
||||||
storageManager: StorageManager;
|
storageManager: StorageManager;
|
||||||
secretManager: SecretManager;
|
secretManager: SecretManager;
|
||||||
|
githubManager: GitHubManager;
|
||||||
static secretsRegExp: RegExp = /\${{\s*secrets\.(.*?)\s*}}/g;
|
static secretsRegExp: RegExp = /\${{\s*secrets\.(.*?)\s*}}/g;
|
||||||
static variablesRegExp: RegExp = /\${{\s*vars\.(.*?)(?:\s*==\s*(.*?))?\s*}}/g;
|
static variablesRegExp: RegExp = /\${{\s*vars\.(.*?)(?:\s*==\s*(.*?))?\s*}}/g;
|
||||||
static inputsRegExp: RegExp = /\${{\s*(?:inputs|github\.event\.inputs)\.(.*?)(?:\s*==\s*(.*?))?\s*}}/g;
|
static inputsRegExp: RegExp = /\${{\s*(?:inputs|github\.event\.inputs)\.(.*?)(?:\s*==\s*(.*?))?\s*}}/g;
|
||||||
@@ -27,6 +29,7 @@ export class SettingsManager {
|
|||||||
constructor(storageManager: StorageManager, secretManager: SecretManager) {
|
constructor(storageManager: StorageManager, secretManager: SecretManager) {
|
||||||
this.storageManager = storageManager;
|
this.storageManager = storageManager;
|
||||||
this.secretManager = secretManager;
|
this.secretManager = secretManager;
|
||||||
|
this.githubManager = new GitHubManager();
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSettings(workspaceFolder: WorkspaceFolder, isUserSelected: boolean) {
|
async getSettings(workspaceFolder: WorkspaceFolder, isUserSelected: boolean) {
|
||||||
@@ -34,12 +37,14 @@ export class SettingsManager {
|
|||||||
const variables = (await this.getSetting(workspaceFolder, SettingsManager.variablesRegExp, StorageKey.Variables, false, Visibility.show)).filter(variable => !isUserSelected || variable.selected);
|
const variables = (await this.getSetting(workspaceFolder, SettingsManager.variablesRegExp, StorageKey.Variables, false, Visibility.show)).filter(variable => !isUserSelected || variable.selected);
|
||||||
const inputs = (await this.getSetting(workspaceFolder, SettingsManager.inputsRegExp, StorageKey.Inputs, false, Visibility.show)).filter(input => !isUserSelected || (input.selected && input.value));
|
const inputs = (await this.getSetting(workspaceFolder, SettingsManager.inputsRegExp, StorageKey.Inputs, false, Visibility.show)).filter(input => !isUserSelected || (input.selected && input.value));
|
||||||
const runners = (await this.getSetting(workspaceFolder, SettingsManager.runnersRegExp, StorageKey.Runners, false, Visibility.show)).filter(runner => !isUserSelected || (runner.selected && runner.value));
|
const runners = (await this.getSetting(workspaceFolder, SettingsManager.runnersRegExp, StorageKey.Runners, false, Visibility.show)).filter(runner => !isUserSelected || (runner.selected && runner.value));
|
||||||
|
const environments = await this.getEnvironments(workspaceFolder);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
secrets: secrets,
|
secrets: secrets,
|
||||||
variables: variables,
|
variables: variables,
|
||||||
inputs: inputs,
|
inputs: inputs,
|
||||||
runners: runners
|
runners: runners,
|
||||||
|
environments: environments
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,6 +94,37 @@ export class SettingsManager {
|
|||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getEnvironments(workspaceFolder: WorkspaceFolder): Promise<Setting[]> {
|
||||||
|
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<any>(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 editSetting(workspaceFolder: WorkspaceFolder, newSetting: Setting, storageKey: StorageKey) {
|
async editSetting(workspaceFolder: WorkspaceFolder, newSetting: Setting, storageKey: StorageKey) {
|
||||||
const value = newSetting.value;
|
const value = newSetting.value;
|
||||||
if (storageKey === StorageKey.Secrets) {
|
if (storageKey === StorageKey.Secrets) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { CancellationToken, commands, EventEmitter, ExtensionContext, TreeCheckboxChangeEvent, TreeDataProvider, TreeItem, TreeItemCheckboxState, window, workspace } from "vscode";
|
import { CancellationToken, commands, EventEmitter, ExtensionContext, QuickPickItem, QuickPickItemKind, ThemeIcon, TreeCheckboxChangeEvent, TreeDataProvider, TreeItem, TreeItemCheckboxState, window, workspace } from "vscode";
|
||||||
import { act } from "../../extension";
|
import { act } from "../../extension";
|
||||||
import { Visibility } from "../../settingsManager";
|
import { Visibility } from "../../settingsManager";
|
||||||
|
import { StorageKey } from "../../storageManager";
|
||||||
import { GithubLocalActionsTreeItem } from "../githubLocalActionsTreeItem";
|
import { GithubLocalActionsTreeItem } from "../githubLocalActionsTreeItem";
|
||||||
import SettingTreeItem from "./setting";
|
import SettingTreeItem from "./setting";
|
||||||
import WorkspaceFolderSettingsTreeItem from "./workspaceFolderSettings";
|
import WorkspaceFolderSettingsTreeItem from "./workspaceFolderSettings";
|
||||||
@@ -27,6 +28,112 @@ export default class SettingsTreeDataProvider implements TreeDataProvider<Github
|
|||||||
await act.settingsManager.editSetting(settingTreeItem.workspaceFolder, newSetting, settingTreeItem.storageKey);
|
await act.settingsManager.editSetting(settingTreeItem.workspaceFolder, newSetting, settingTreeItem.storageKey);
|
||||||
this.refresh();
|
this.refresh();
|
||||||
}),
|
}),
|
||||||
|
commands.registerCommand('githubLocalActions.importFromGithub', async (settingTreeItem: SettingTreeItem) => {
|
||||||
|
const settings = await act.settingsManager.getSettings(settingTreeItem.workspaceFolder, false);
|
||||||
|
const variableNames = settings.variables.map(variable => variable.key);
|
||||||
|
if (variableNames.length > 0) {
|
||||||
|
const repository = await act.settingsManager.githubManager.getRepository(settingTreeItem.workspaceFolder, 'githubLocalActions.importFromGithub', [settingTreeItem]);
|
||||||
|
if (repository) {
|
||||||
|
const variableOptions: QuickPickItem[] = [];
|
||||||
|
const errors: string[] = [];
|
||||||
|
|
||||||
|
await window.withProgress({ location: { viewId: SettingsTreeDataProvider.VIEW_ID } }, async () => {
|
||||||
|
// Get repository variables
|
||||||
|
const repositoryVariables = await act.settingsManager.githubManager.getVariables(repository.owner, repository.repo);
|
||||||
|
if (repositoryVariables.error) {
|
||||||
|
errors.push(repositoryVariables.error);
|
||||||
|
} else {
|
||||||
|
const matchingVariables = repositoryVariables.data.filter(variable => variableNames.includes(variable.name));
|
||||||
|
if (matchingVariables.length > 0) {
|
||||||
|
variableOptions.push({
|
||||||
|
label: 'Repository Variables',
|
||||||
|
kind: QuickPickItemKind.Separator
|
||||||
|
});
|
||||||
|
|
||||||
|
variableOptions.push(
|
||||||
|
...matchingVariables.map(variable => {
|
||||||
|
return {
|
||||||
|
label: variable.name,
|
||||||
|
description: variable.value,
|
||||||
|
iconPath: new ThemeIcon('symbol-variable')
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get environment variables
|
||||||
|
const environments = await act.settingsManager.githubManager.getEnvironments(repository.owner, repository.repo);
|
||||||
|
if (environments.error) {
|
||||||
|
errors.push(environments.error);
|
||||||
|
} else {
|
||||||
|
for (const environment of environments.data) {
|
||||||
|
const environmentVariables = await act.settingsManager.githubManager.getVariables(repository.owner, repository.repo, environment.name);
|
||||||
|
if (environmentVariables.error) {
|
||||||
|
errors.push(environmentVariables.error);
|
||||||
|
} else {
|
||||||
|
const matchingVariables = environmentVariables.data.filter(variable => variableNames.includes(variable.name));
|
||||||
|
if (matchingVariables.length > 0) {
|
||||||
|
variableOptions.push({
|
||||||
|
label: environment.name,
|
||||||
|
kind: QuickPickItemKind.Separator
|
||||||
|
});
|
||||||
|
|
||||||
|
variableOptions.push(
|
||||||
|
...matchingVariables.map(variable => {
|
||||||
|
return {
|
||||||
|
label: variable.name,
|
||||||
|
description: variable.value,
|
||||||
|
iconPath: new ThemeIcon('symbol-variable')
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
window.showErrorMessage(`Error(s) encountered retrieving variables from GitHub. Errors: ${[...new Set(errors)].join(' ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (variableOptions.length > 0) {
|
||||||
|
const selectedVariables = await window.showQuickPick(variableOptions, {
|
||||||
|
title: 'Select the variables to import from GitHub',
|
||||||
|
placeHolder: 'Variables',
|
||||||
|
matchOnDescription: true,
|
||||||
|
canPickMany: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (selectedVariables) {
|
||||||
|
const seen = new Set();
|
||||||
|
const hasDuplicates = selectedVariables.some(variable => {
|
||||||
|
return seen.size === seen.add(variable.label).size;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasDuplicates) {
|
||||||
|
window.showErrorMessage('Duplicate variables selected');
|
||||||
|
} else {
|
||||||
|
for await (const variable of selectedVariables) {
|
||||||
|
const newSetting = settings.variables.find(existingVariable => existingVariable.key === variable.label);
|
||||||
|
if (newSetting && variable.description) {
|
||||||
|
newSetting.value = variable.description;
|
||||||
|
await act.settingsManager.editSetting(settingTreeItem.workspaceFolder, newSetting, StorageKey.Variables);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (errors.length === 0) {
|
||||||
|
window.showErrorMessage('No matching variables defined in Github');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
window.showErrorMessage('No variables found in workflow(s)');
|
||||||
|
}
|
||||||
|
}),
|
||||||
commands.registerCommand('githubLocalActions.editSetting', async (settingTreeItem: SettingTreeItem) => {
|
commands.registerCommand('githubLocalActions.editSetting', async (settingTreeItem: SettingTreeItem) => {
|
||||||
const newValue = await window.showInputBox({
|
const newValue = await window.showInputBox({
|
||||||
prompt: `Enter the value for ${settingTreeItem.setting.value}`,
|
prompt: `Enter the value for ${settingTreeItem.setting.value}`,
|
||||||
|
|||||||
Reference in New Issue
Block a user