Update report an issue action to open github issue with autogenerated issue template (#166)

Signed-off-by: Sanjula Ganepola <sanjulagane@gmail.com>
This commit is contained in:
Sanjula Ganepola
2025-02-11 22:07:11 -05:00
committed by GitHub
parent 5103c8065f
commit 431ac5e6a8
8 changed files with 255 additions and 39 deletions

View File

@@ -3,6 +3,10 @@ description: Report a bug or issue.
labels:
- 'bug'
body:
- type: markdown
attributes:
value: |
⭐ Most of the content for this bug report can be automatically generated by selecting `Help and Support` -> `Report an Issue` from any GitHub Local Actions view. ⭐
- type: input
id: github-local-actions-version
attributes:
@@ -76,14 +80,6 @@ body:
render: sh
validations:
required: false
- type: textarea
id: bug-description
attributes:
label: Bug Description
description: |
Describe the bug you encountered and share all steps to reproduce it.
placeholder: |
Bug description
- type: textarea
id: act-bug-report
attributes:
@@ -104,3 +100,11 @@ body:
render: yml
validations:
required: false
- type: textarea
id: bug-description
attributes:
label: Bug Description
description: |
Describe the bug you encountered and share all steps to reproduce it.
placeholder: |
Bug description

View File

@@ -13,7 +13,7 @@
"repository": {
"url": "https://github.com/SanjulaGanepola/github-local-actions"
},
"homepage": "https://github.com/SanjulaGanepola/github-local-actions/blob/main/README.md",
"homepage": "https://sanjulaganepola.github.io/github-local-actions-docs",
"bugs": {
"url": "https://github.com/SanjulaGanepola/github-local-actions/issues"
},

View File

@@ -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 { Mode, SettingsManager } from './settingsManager';
import { Mode, Settings, SettingsManager } from './settingsManager';
import { StorageKey, StorageManager } from './storageManager';
import { Utils } from "./utils";
import { Job, Workflow, WorkflowsManager } from "./workflowsManager";
@@ -576,6 +576,30 @@ export class Act {
return path.join(cacheHomeDir, ...paths);
}
async buildActCommand(settings: Settings, options: string[]) {
const userOptions: string[] = [
...settings.secrets.map(secret => `${Option.Secret} ${secret.key}`),
(settings.secretFiles.length > 0 ? `${Option.SecretFile} "${settings.secretFiles[0].path}"` : `${Option.SecretFile} ""`),
...settings.variables.map(variable => `${Option.Var} ${variable.key}="${Utils.escapeSpecialCharacters(variable.value)}"`),
(settings.variableFiles.length > 0 ? `${Option.VarFile} "${settings.variableFiles[0].path}"` : `${Option.VarFile} ""`),
...settings.inputs.map(input => `${Option.Input} ${input.key}="${Utils.escapeSpecialCharacters(input.value)}"`),
(settings.inputFiles.length > 0 ? `${Option.InputFile} "${settings.inputFiles[0].path}"` : `${Option.InputFile} ""`),
...settings.runners.map(runner => `${Option.Platform} ${runner.key}="${Utils.escapeSpecialCharacters(runner.value)}"`),
(settings.payloadFiles.length > 0 ? `${Option.EventPath} "${settings.payloadFiles[0].path}"` : `${Option.EventPath} ""`),
...settings.options.map(option => option.path ? `--${option.name}${option.default && ['true', 'false'].includes(option.default) ? "=" : " "}"${Utils.escapeSpecialCharacters(option.path)}"` : `--${option.name}`)
];
const actCommand = Act.getActCommand();
const executionCommand = `${actCommand} ${Option.Json} ${Option.Verbose} ${options.join(' ')} ${userOptions.join(' ')}`;
const displayCommand = `${actCommand} ${options.join(' ')} ${userOptions.join(' ')}`;
return {
userOptions,
executionCommand,
displayCommand
};
}
async runCommand(commandArgs: CommandArgs) {
// Check if required components are ready
// const unreadyComponents = await this.componentsManager.getUnreadyComponents();
@@ -624,23 +648,8 @@ export class Act {
} catch (error: any) { }
// Build command with settings
const actCommand = Act.getActCommand();
const settings = await this.settingsManager.getSettings(workspaceFolder, true);
const userOptions: string[] = [
...settings.secrets.map(secret => `${Option.Secret} ${secret.key}`),
(settings.secretFiles.length > 0 ? `${Option.SecretFile} "${settings.secretFiles[0].path}"` : `${Option.SecretFile} ""`),
...settings.variables.map(variable => `${Option.Var} ${variable.key}="${Utils.escapeSpecialCharacters(variable.value)}"`),
(settings.variableFiles.length > 0 ? `${Option.VarFile} "${settings.variableFiles[0].path}"` : `${Option.VarFile} ""`),
...settings.inputs.map(input => `${Option.Input} ${input.key}="${Utils.escapeSpecialCharacters(input.value)}"`),
(settings.inputFiles.length > 0 ? `${Option.InputFile} "${settings.inputFiles[0].path}"` : `${Option.InputFile} ""`),
...settings.runners.map(runner => `${Option.Platform} ${runner.key}="${Utils.escapeSpecialCharacters(runner.value)}"`),
(settings.payloadFiles.length > 0 ? `${Option.EventPath} "${settings.payloadFiles[0].path}"` : `${Option.EventPath} ""`),
...settings.options.map(option => option.path ? `--${option.name}${option.default && ['true', 'false'].includes(option.default) ? "=" : " "}"${Utils.escapeSpecialCharacters(option.path)}"` : `--${option.name}`)
];
const command = `${actCommand} ${Option.Json} ${Option.Verbose} ${commandArgs.options.join(' ')} ${userOptions.join(' ')}`;
const displayCommand = `${actCommand} ${commandArgs.options.join(' ')} ${userOptions.join(' ')}`;
const { userOptions, executionCommand, displayCommand } = await this.buildActCommand(settings, commandArgs.options);
// Execute task
const taskExecution = await tasks.executeTask({
@@ -854,7 +863,7 @@ export class Act {
};
const exec = childProcess.spawn(
command,
executionCommand,
{
cwd: commandArgs.path,
shell: shell,

View File

@@ -1,6 +1,6 @@
import * as childProcess from "child_process";
import { commands, env, extensions, QuickPickItemKind, ShellExecution, TaskGroup, TaskPanelKind, TaskRevealKind, tasks, TaskScope, ThemeIcon, Uri, window } from "vscode";
import { Act } from "./act";
import { Act, Option } from "./act";
import { ConfigurationManager, Platform, Section } from "./configurationManager";
import { act, componentsTreeDataProvider } from "./extension";
import ComponentsTreeDataProvider from "./views/components/componentsTreeDataProvider";
@@ -39,7 +39,7 @@ export class ComponentsManager {
async getComponents(): Promise<Component<CliStatus | ExtensionStatus>[]> {
const components: Component<CliStatus | ExtensionStatus>[] = [];
const actCliInfo = await this.getCliInfo(`${Act.getActCommand()} --version`, ComponentsManager.actVersionRegExp, false, false);
const actCliInfo = await this.getCliInfo(`${Act.getActCommand()} ${Option.Version}`, ComponentsManager.actVersionRegExp, false, false);
components.push({
name: 'nektos/act',
icon: 'terminal',

View File

@@ -2,6 +2,7 @@ import * as vscode from 'vscode';
import { commands, env, TreeCheckboxChangeEvent, Uri, window, workspace } from 'vscode';
import { Act } from './act';
import { ConfigurationManager } from './configurationManager';
import { IssueHandler } from './issueHandler';
import ComponentsTreeDataProvider from './views/components/componentsTreeDataProvider';
import { DecorationProvider } from './views/decorationProvider';
import { GithubLocalActionsTreeItem } from './views/githubLocalActionsTreeItem';
@@ -68,10 +69,10 @@ export function activate(context: vscode.ExtensionContext) {
window.registerFileDecorationProvider(decorationProvider),
workflowsFileWatcher,
commands.registerCommand('githubLocalActions.viewDocumentation', async () => {
await env.openExternal(Uri.parse('https://nektosact.com'));
await env.openExternal(Uri.parse('https://sanjulaganepola.github.io/github-local-actions-docs'));
}),
commands.registerCommand('githubLocalActions.reportAnIssue', async () => {
await env.openExternal(Uri.parse('https://github.com/SanjulaGanepola/github-local-actions/issues'));
await IssueHandler.openBugReport(context);
}),
);
}

View File

@@ -10,6 +10,7 @@ export interface Response<T> {
}
export interface GithubRepository {
remoteOriginUrl: string,
owner: string,
repo: string
}
@@ -24,7 +25,7 @@ export interface GithubVariable {
}
export class GitHubManager {
async getRepository(workspaceFolder: WorkspaceFolder, command: string, args: any[]): Promise<GithubRepository | undefined> {
async getRepository(workspaceFolder: WorkspaceFolder, suppressNotFoundErrors: boolean, tryAgainOptions?: { command: string, args: any[] }): Promise<GithubRepository | undefined> {
const gitApi = extensions.getExtension<GitExtension>('vscode.git')?.exports.getAPI(1);
if (gitApi) {
if (gitApi.state === 'initialized') {
@@ -38,19 +39,25 @@ export class GitHubManager {
const parsedParentPath = path.parse(parsedPath.dir);
return {
remoteOriginUrl: remoteOriginUrl,
owner: parsedParentPath.name,
repo: parsedPath.name
};
} else {
window.showErrorMessage('Remote GitHub URL not found.');
if (!suppressNotFoundErrors) {
window.showErrorMessage('Remote GitHub URL not found.');
}
}
} else {
window.showErrorMessage(`${workspaceFolder.name} does not have a Git repository`);
if (!suppressNotFoundErrors) {
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);
const items = tryAgainOptions ? ['Try Again'] : [];
window.showErrorMessage('Git extension is still being initialized. Please try again later.', ...items).then(async value => {
if (value && value === 'Try Again' && tryAgainOptions) {
await commands.executeCommand(tryAgainOptions.command, ...tryAgainOptions.args);
}
});
}

195
src/issueHandler.ts Normal file
View File

@@ -0,0 +1,195 @@
import * as childProcess from "child_process";
import * as fs from "fs/promises";
import * as path from "path";
import { env, ExtensionContext, ProgressLocation, QuickPickItem, ThemeIcon, Uri, window, workspace } from "vscode";
import { Act, Option } from "./act";
import { act } from "./extension";
interface BugReport {
githubLocalActionsVersion?: string
actVersion?: string,
githubRepositoryLink?: string,
workflowContent?: string,
actCommandUsed?: string,
actCommandOutput?: string,
actBugReport?: string
bugDescription?: string,
}
// Used to map bug report keys to ids and titles in the issue template
const bugReportToTemplateMap: Record<keyof BugReport, { id: string, title: string }> = {
githubLocalActionsVersion: { id: 'github-local-actions-version', title: 'Github Local Actions Version' },
actVersion: { id: 'act-version', title: 'Act Version' },
githubRepositoryLink: { id: 'github-repository-link', title: 'GitHub Repository Link' },
workflowContent: { id: 'workflow-content', title: 'Workflow Content' },
actCommandUsed: { id: 'act-command-used', title: 'Act Command Used' },
actCommandOutput: { id: 'act-command-output', title: 'Act Command Output' },
actBugReport: { id: 'act-bug-report', title: 'Act Bug Report' },
bugDescription: { id: 'bug-description', title: 'Bug Description' }
};
export namespace IssueHandler {
export async function openBugReport(context: ExtensionContext) {
try {
const bugReport = await generateBugReport(context);
if (bugReport) {
const params = Object.entries(bugReport)
.filter(([key, value]) => value !== undefined && value !== '')
.map(([key, value]) => `${encodeURIComponent(bugReportToTemplateMap[key as keyof BugReport].id)}=${encodeURIComponent(value)}`)
.join('&');
const bugReportUrl: string = 'https://github.com/SanjulaGanepola/github-local-actions/issues/new?assignees=&labels=bug&projects=&template=1-bug_report.yml';
const urlWithParams = params ? `${bugReportUrl}&${params}` : bugReportUrl;
await env.openExternal(Uri.parse(urlWithParams));
}
} catch (error) {
await env.openExternal(Uri.parse('https://github.com/SanjulaGanepola/github-local-actions/issues'));
}
}
async function generateBugReport(context: ExtensionContext): Promise<BugReport | undefined> {
return await window.withProgress({ location: ProgressLocation.Notification, title: 'Generating bug report...' }, async () => {
const fullBugReport: BugReport = {};
const infoItems: (QuickPickItem & { key: keyof BugReport })[] = [];
// Get extension version
const githubLocalActionsVersion = context.extension.packageJSON.version;
if (githubLocalActionsVersion) {
fullBugReport.githubLocalActionsVersion = `v${githubLocalActionsVersion}`;
infoItems.push({
label: bugReportToTemplateMap['githubLocalActionsVersion'].title,
description: fullBugReport.githubLocalActionsVersion,
iconPath: new ThemeIcon('robot'),
picked: true,
key: 'githubLocalActionsVersion'
});
}
// Get act version
const actVersion = (await act.componentsManager.getComponents()).find(component => component.name === 'nektos/act')?.version;
if (actVersion) {
fullBugReport.actVersion = `v${actVersion}`;
infoItems.push({
label: bugReportToTemplateMap['actVersion'].title,
description: fullBugReport.actVersion,
iconPath: new ThemeIcon('terminal'),
picked: true,
key: 'actVersion'
});
}
let isWorkflowFound: boolean = false;
const activeEditor = window.activeTextEditor;
const workspaceFolder = activeEditor ?
workspace.getWorkspaceFolder(activeEditor.document.uri) :
(workspace.workspaceFolders && workspace.workspaceFolders.length > 0 ? workspace.workspaceFolders[0] : undefined);
if (workspaceFolder) {
// Get repository link
const repository = await act.settingsManager.githubManager.getRepository(workspaceFolder, true);
const githubRepositoryLink = repository?.remoteOriginUrl;
if (githubRepositoryLink) {
fullBugReport.githubRepositoryLink = githubRepositoryLink;
infoItems.push({
label: bugReportToTemplateMap['githubRepositoryLink'].title,
description: fullBugReport.githubRepositoryLink,
iconPath: new ThemeIcon('link'),
picked: true,
key: 'githubRepositoryLink'
});
}
if (activeEditor) {
const workflows = await act.workflowsManager.getWorkflows(workspaceFolder);
const workflow = workflows.find(workflow => workflow.uri.fsPath === activeEditor.document.uri.fsPath);
if (workflow) {
isWorkflowFound = true;
// Get workflow content
const workflowContent = workflow?.fileContent;
if (workflowContent) {
fullBugReport.workflowContent = workflowContent;
infoItems.push({
label: bugReportToTemplateMap['workflowContent'].title,
description: path.parse(workflow.uri.fsPath).base,
iconPath: new ThemeIcon('file'),
picked: true,
key: 'workflowContent'
});
}
const workflowHistory = act.historyManager.workspaceHistory[workspaceFolder.uri.fsPath]?.filter(history => history.commandArgs.workflow?.uri.fsPath === workflow.uri.fsPath);
if (workflowHistory && workflowHistory.length > 0) {
// Get last act command
const settings = await act.settingsManager.getSettings(workspaceFolder, true);
const history = workflowHistory[workflowHistory.length - 1];
const actCommandUsed = (await act.buildActCommand(settings, history.commandArgs.options)).displayCommand;
if (actCommandUsed) {
fullBugReport.actCommandUsed = actCommandUsed;
infoItems.push({
label: bugReportToTemplateMap['actCommandUsed'].title,
description: `${history.name} #${history.count}`,
iconPath: new ThemeIcon('code'),
picked: true,
key: 'actCommandUsed'
});
}
try {
// Get last act command output
const actCommandOutput = await fs.readFile(history.logPath, 'utf8');
if (actCommandOutput) {
fullBugReport.actCommandOutput = actCommandOutput;
infoItems.push({
label: bugReportToTemplateMap['actCommandOutput'].title,
description: path.parse(history.logPath).base,
iconPath: new ThemeIcon('note'),
picked: true,
key: 'actCommandOutput'
});
}
} catch (error: any) { }
}
}
}
}
// Get act bug report
const actBugReport = await new Promise<string | undefined>((resolve, reject) => {
childProcess.exec(`${Act.getActCommand()} ${Option.BugReport}`, (error, stdout, stderr) => {
if (!error) {
resolve(stdout);
}
});
});
if (actBugReport) {
fullBugReport.actBugReport = actBugReport;
infoItems.push({
label: bugReportToTemplateMap['actBugReport'].title,
iconPath: new ThemeIcon('report'),
picked: true,
key: 'actBugReport'
});
}
const defaultTitle = 'Select the information to include in the bug report';
const extendedTitle = 'More information can be included in the bug report by having the relevant workflow opened in the editor before invoking this command.';
const selectedInfo = await window.showQuickPick(infoItems, {
title: isWorkflowFound ? defaultTitle : `${defaultTitle}. ${extendedTitle}`,
placeHolder: 'Bug Report Information',
canPickMany: true
});
if (selectedInfo) {
const bugReport: BugReport = {};
const selectedInfoKeys = selectedInfo.map(info => info.key);
for (const key of selectedInfoKeys) {
bugReport[key] = fullBugReport[key];
}
return bugReport;
}
});
}
}

View File

@@ -289,7 +289,7 @@ export default class SettingsTreeDataProvider implements TreeDataProvider<Github
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]);
const repository = await act.settingsManager.githubManager.getRepository(settingTreeItem.workspaceFolder, false, { command: 'githubLocalActions.importFromGithub', args: [settingTreeItem] });
if (repository) {
const variableOptions: QuickPickItem[] = [];
const errors: string[] = [];