Compare commits

...

10 Commits

Author SHA1 Message Date
adf88e431c feat: Add Gitea workflow support (.gitea/workflows) - Support for both .github/workflows and .gitea/workflows directories - Fixed workflow execution path resolution - Cleaned up debugging code - Updated to version 1.2.5
Some checks failed
Test Gitea Workflow / test (push) Has been cancelled
2025-08-03 23:00:22 +07:00
atoko
bc25f97d70 Write to filesystem for non-secret StorageKey values (#191) 2025-04-05 17:49:59 -04:00
Andrew Glago
eebee47f40 Add support for running specific events on workflows and jobs (#190)
* feat: add support for running specific workflows

* feat: extend registered commands

* docs: add changelog entry, update readme

* chore: remove 'access commands via' note, moved to documentation

* docs: add @a11rew to contributors

* fix: remove debug change

* Update change log to link to release notes

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

* Reorder actions for consistency

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

* Improve type safety with optional options param and mandatory workflow param

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

---------

Signed-off-by: Sanjula Ganepola <sanjulagane@gmail.com>
Co-authored-by: Sanjula Ganepola <sanjulagane@gmail.com>
2025-04-03 12:29:45 -04:00
Sanjula Ganepola
c505e8af9b Link to issues with good first issue label (#194)
* Link to issues with good first issue label

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

* Remove space

---------

Signed-off-by: Sanjula Ganepola <sanjulagane@gmail.com>
2025-04-03 12:29:34 -04:00
Sanjula Ganepola
b7bd8d9600 Remove default keyboard shortcut (#189)
* Remove keyboard shortcut

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

* Bump to 1.2.5

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

---------

Signed-off-by: Sanjula Ganepola <sanjulagane@gmail.com>
2025-03-24 20:03:36 -04:00
Sanjula Ganepola
9452872b96 Add inputs to release workflow
Signed-off-by: Sanjula Ganepola <sanjulagane@gmail.com>
2025-03-23 18:38:21 -04:00
Sanjula Ganepola
6e3b0e7b21 Bump to 1.2.4
Signed-off-by: Sanjula Ganepola <sanjulagane@gmail.com>
2025-03-23 18:28:38 -04:00
Sanjula Ganepola
e8f3f6c673 Add setting to change workflow directory (#188)
Signed-off-by: Sanjula Ganepola <sanjulagane@gmail.com>
2025-03-23 18:27:30 -04:00
Sanjula Ganepola
412914d32a Bump to v1.2.3
Signed-off-by: Sanjula Ganepola <sanjulagane@gmail.com>
2025-02-19 23:36:54 -05:00
Sanjula Ganepola
431ac5e6a8 Update report an issue action to open github issue with autogenerated issue template (#166)
Signed-off-by: Sanjula Ganepola <sanjulagane@gmail.com>
2025-02-11 22:07:11 -05:00
23 changed files with 5111 additions and 1265 deletions

View File

@@ -0,0 +1,21 @@
name: Test Gitea Workflow
run-name: Test Gitea Workflow
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run tests
run: echo "Testing Gitea workflow"
- name: Build
run: echo "Building project"

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:
@@ -103,4 +99,12 @@ body:
[...]
render: yml
validations:
required: false
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

@@ -2,6 +2,17 @@ name: Publish to the Marketplace and Open VSX
on:
workflow_dispatch:
inputs:
publish_openvsx:
description: 'Publish to Open VSX'
type: boolean
required: true
default: true
publish_marketplace:
description: 'Publish to Marketplace'
type: boolean
required: true
default: true
release:
types: [created]
@@ -30,7 +41,9 @@ jobs:
npm install -g vsce ovsx
- name: Publish to Open VSX
if: github.event_name == 'release' || inputs.publish_openvsx == true
run: npx ovsx publish -p ${{ secrets.OPEN_VSX_TOKEN }}
- name: Publish to Marketplace
if: github.event_name == 'release' || inputs.publish_marketplace == true
run: vsce publish -p ${{ secrets.VS_MARKETPLACE_TOKEN }}

View File

@@ -1,9 +1,3 @@
# Change Log
All notable changes to the "github-local-actions" extension will be documented in this file.
Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
## [1.0.0]
- Initial release
All notable changes to the "github-local-actions" extension will be documented in the [GitHub release notes](https://github.com/SanjulaGanepola/github-local-actions/releases).

View File

@@ -2,7 +2,7 @@
Thank you for your interest in contributing to **GitHub Local Actions**! Whether you're fixing a bug, adding a new feature, or improving the documentation, your contributions are highly valued and help make this project better for everyone.
No contribution is too small—every bit helps! If you're unsure where to start, check out our [open issues](https://github.com/SanjulaGanepola/github-local-actions/issues) or reach out on our [discussion board](https://github.com/SanjulaGanepola/github-local-actions/discussions) to discuss your ideas. By contributing, you are agreeing to follow our [Code of Conduct](https://github.com/SanjulaGanepola/github-local-actions/blob/main/CODE_OF_CONDUCT.md) guidelines. Lets keep this a welcoming and fun space for everyone to collaborate!
No contribution is too small—every bit helps! If you're unsure where to start, check out our [open issues](https://github.com/SanjulaGanepola/github-local-actions/issues) (look out for the [good first issue](https://github.com/SanjulaGanepola/github-local-actions/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22) label) or reach out on our [discussion board](https://github.com/SanjulaGanepola/github-local-actions/discussions) to discuss your ideas. By contributing, you are agreeing to follow our [Code of Conduct](https://github.com/SanjulaGanepola/github-local-actions/blob/main/CODE_OF_CONDUCT.md) guidelines. Lets keep this a welcoming and fun space for everyone to collaborate!
## Getting Started
@@ -25,5 +25,7 @@ Thanks so much to everyone [who has contributed](https://github.com/SanjulaGanep
* [@SanjulaGanepola](https://github.com/SanjulaGanepola)
* [@ChristopherHX](https://github.com/ChristopherHX)
* [@a11rew](https://github.com/a11rew)
* [@atoko](https://github.com/atoko)
Want to see your name on this list? Join us and contribute!

View File

@@ -36,6 +36,8 @@ The `Workflows` view is where you can manage and run workflows locally. You have
2. **Run Single Workflow**: Run a single workflow in the workspace.
3. **Run Job**: Run a specific job in a workflow.
4. **Run Event**: Run multiple workflows using a [GitHub event](https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows).
5. **Run Workflow Event**: Run a specific event on a workflow.
6. **Run Job Event**: Run a specific event on a job.
![Workflows View](https://raw.githubusercontent.com/SanjulaGanepola/github-local-actions/main/images/workflows-view.png)

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "github-local-actions",
"version": "1.2.2",
"version": "1.2.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "github-local-actions",
"version": "1.2.2",
"version": "1.2.5",
"license": "Apache-2.0",
"dependencies": {
"child_process": "^1.0.2",

View File

@@ -9,11 +9,11 @@
},
"publisher": "SanjulaGanepola",
"license": "Apache-2.0",
"version": "1.2.2",
"version": "1.2.5",
"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"
},
@@ -131,13 +131,6 @@
"when": "githubLocalActions:noSettings && workspaceFolderCount > 0"
}
],
"keybindings": [
{
"command": "githubLocalActions.runWorkflow",
"key": "ctrl+g",
"mac": "cmd+g"
}
],
"commands": [
{
"category": "GitHub Local Actions",
@@ -189,24 +182,36 @@
"title": "Refresh",
"icon": "$(refresh)"
},
{
"category": "GitHub Local Actions",
"command": "githubLocalActions.openWorkflow",
"title": "Open Workflow",
"icon": "$(go-to-file)"
},
{
"category": "GitHub Local Actions",
"command": "githubLocalActions.runWorkflow",
"title": "Run Workflow",
"icon": "$(debug-start)"
},
{
"category": "GitHub Local Actions",
"command": "githubLocalActions.runWorkflowEvent",
"title": "Run Workflow with Event",
"icon": "$(symbol-event)"
},
{
"category": "GitHub Local Actions",
"command": "githubLocalActions.openWorkflow",
"title": "Open Workflow",
"icon": "$(go-to-file)"
},
{
"category": "GitHub Local Actions",
"command": "githubLocalActions.runJob",
"title": "Run Job",
"icon": "$(debug-start)"
},
{
"category": "GitHub Local Actions",
"command": "githubLocalActions.runJobEvent",
"title": "Run Job with Event",
"icon": "$(symbol-event)"
},
{
"category": "GitHub Local Actions",
"command": "githubLocalActions.clearAll",
@@ -424,18 +429,26 @@
"command": "githubLocalActions.refreshWorkflows",
"when": "never"
},
{
"command": "githubLocalActions.openWorkflow",
"when": "never"
},
{
"command": "githubLocalActions.runWorkflow",
"when": "never"
},
{
"command": "githubLocalActions.runWorkflowEvent",
"when": "never"
},
{
"command": "githubLocalActions.openWorkflow",
"when": "never"
},
{
"command": "githubLocalActions.runJob",
"when": "never"
},
{
"command": "githubLocalActions.runJobEvent",
"when": "never"
},
{
"command": "githubLocalActions.clearAll",
"when": "never"
@@ -637,20 +650,55 @@
"group": "inline@1"
},
{
"command": "githubLocalActions.openWorkflow",
"command": "githubLocalActions.runWorkflow",
"when": "view == workflows && viewItem =~ /^githubLocalActions.workflow.*/",
"group": "inline@0"
},
{
"command": "githubLocalActions.runWorkflow",
"when": "view == workflows && viewItem =~ /^githubLocalActions.workflow.*/",
"group": "workflows@0"
},
{
"command": "githubLocalActions.runWorkflowEvent",
"when": "view == workflows && viewItem =~ /^githubLocalActions.workflow.*/",
"group": "inline@1"
},
{
"command": "githubLocalActions.runWorkflowEvent",
"when": "view == workflows && viewItem =~ /^githubLocalActions.workflow.*/",
"group": "workflows@1"
},
{
"command": "githubLocalActions.openWorkflow",
"when": "view == workflows && viewItem =~ /^githubLocalActions.workflow.*/",
"group": "inline@2"
},
{
"command": "githubLocalActions.openWorkflow",
"when": "view == workflows && viewItem =~ /^githubLocalActions.workflow.*/",
"group": "workflows@2"
},
{
"command": "githubLocalActions.runJob",
"when": "view == workflows && viewItem =~ /^githubLocalActions.job.*/",
"group": "inline@0"
},
{
"command": "githubLocalActions.runJob",
"when": "view == workflows && viewItem =~ /^githubLocalActions.job.*/",
"group": "jobs@0"
},
{
"command": "githubLocalActions.runJobEvent",
"when": "view == workflows && viewItem =~ /^githubLocalActions.job.*/",
"group": "inline@1"
},
{
"command": "githubLocalActions.runJobEvent",
"when": "view == workflows && viewItem =~ /^githubLocalActions.job.*/",
"group": "jobs@1"
},
{
"command": "githubLocalActions.clearAll",
"when": "view == history && viewItem =~ /^githubLocalActions.workspaceFolderHistory.*/ && workspaceFolderCount > 1",
@@ -786,6 +834,11 @@
"type": "string",
"default": "act"
},
"githubLocalActions.workflowsDirectory": {
"markdownDescription": "The relative path to the directory containing your workflows. By default, this will be `.github/workflows`.",
"type": "string",
"default": ".github/workflows"
},
"githubLocalActions.dockerDesktopPath": {
"markdownDescription": "The path to your Docker Desktop executable (used for Windows and MacOS). To start Docker Engine from the `Components` view, this application will be launched. Refer to the default path based on OS:\n\n* **Windows**: `C:/Program Files/Docker/Docker/Docker Desktop.exe`\n\n* **MacOS**: `/Applications/Docker.app`",
"type": "string",
@@ -864,4 +917,4 @@
"webpack": "^5.96.1",
"webpack-cli": "^5.1.4"
}
}
}

2935
src/Extension Host.log Normal file

File diff suppressed because it is too large Load Diff

2169
src/act.ts

File diff suppressed because it is too large Load Diff

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

@@ -1,53 +1,70 @@
import { ConfigurationTarget, workspace } from 'vscode';
import { Act } from './act';
import { ConfigurationTarget, workspace } from "vscode";
import { Act } from "./act";
export enum Platform {
windows = 'win32',
mac = 'darwin',
linux = 'linux'
windows = "win32",
mac = "darwin",
linux = "linux",
}
export enum Section {
actCommand = 'actCommand',
dockerDesktopPath = 'dockerDesktopPath'
actCommand = "actCommand",
dockerDesktopPath = "dockerDesktopPath",
}
export namespace ConfigurationManager {
export const group: string = 'githubLocalActions';
export const searchPrefix: string = '@ext:sanjulaganepola.github-local-actions';
export const group: string = "githubLocalActions";
export const searchPrefix: string =
"@ext:sanjulaganepola.github-local-actions";
export async function initialize(): Promise<void> {
let dockerDesktopPath = ConfigurationManager.get<string>(Section.dockerDesktopPath);
if (!dockerDesktopPath) {
switch (process.platform) {
case Platform.windows:
dockerDesktopPath = 'C:/Program Files/Docker/Docker/Docker Desktop.exe';
break;
case Platform.mac:
dockerDesktopPath = '/Applications/Docker.app';
break;
default:
return;
}
await ConfigurationManager.set(Section.dockerDesktopPath, dockerDesktopPath);
}
let actCommand = ConfigurationManager.get<string>(Section.actCommand);
if (!actCommand) {
await ConfigurationManager.set(Section.actCommand, Act.defaultActCommand);
}
export async function initialize(): Promise<void> {
let actCommand = ConfigurationManager.get<string>(Section.actCommand);
if (!actCommand) {
await ConfigurationManager.set(Section.actCommand, Act.defaultActCommand);
}
export function getSearchTerm(section: Section): string {
return `${ConfigurationManager.searchPrefix} ${ConfigurationManager.group}.${section}`;
}
// Don't set a default workflows directory to allow multi-directory support
// let workflowsDirectory = ConfigurationManager.get<string>(Section.workflowsDirectory);
// if (!workflowsDirectory) {
// await ConfigurationManager.set(Section.workflowsDirectory, WorkflowsManager.defaultWorkflowsDirectory);
// }
export function get<T>(section: Section): T | undefined {
return workspace.getConfiguration(ConfigurationManager.group).get(section) as T;
}
let dockerDesktopPath = ConfigurationManager.get<string>(
Section.dockerDesktopPath
);
if (!dockerDesktopPath) {
switch (process.platform) {
case Platform.windows:
dockerDesktopPath =
"C:/Program Files/Docker/Docker/Docker Desktop.exe";
break;
case Platform.mac:
dockerDesktopPath = "/Applications/Docker.app";
break;
default:
return;
}
export async function set(section: Section, value: any): Promise<void> {
return await workspace.getConfiguration(ConfigurationManager.group).update(section, value, ConfigurationTarget.Global);
await ConfigurationManager.set(
Section.dockerDesktopPath,
dockerDesktopPath
);
}
}
}
export function getSearchTerm(section: Section): string {
return `${ConfigurationManager.searchPrefix} ${ConfigurationManager.group}.${section}`;
}
export function get<T>(section: Section): T | undefined {
return workspace
.getConfiguration(ConfigurationManager.group)
.get(section) as T;
}
export async function set(section: Section, value: any): Promise<void> {
return await workspace
.getConfiguration(ConfigurationManager.group)
.update(section, value, ConfigurationTarget.Global);
}
}

View File

@@ -1,15 +1,23 @@
import * as vscode from 'vscode';
import { commands, env, TreeCheckboxChangeEvent, Uri, window, workspace } from 'vscode';
import { Act } from './act';
import { ConfigurationManager } from './configurationManager';
import ComponentsTreeDataProvider from './views/components/componentsTreeDataProvider';
import { DecorationProvider } from './views/decorationProvider';
import { GithubLocalActionsTreeItem } from './views/githubLocalActionsTreeItem';
import HistoryTreeDataProvider from './views/history/historyTreeDataProvider';
import SettingTreeItem from './views/settings/setting';
import SettingsTreeDataProvider from './views/settings/settingsTreeDataProvider';
import WorkflowsTreeDataProvider from './views/workflows/workflowsTreeDataProvider';
import { WorkflowsManager } from './workflowsManager';
import {
commands,
env,
ExtensionContext,
TreeCheckboxChangeEvent,
Uri,
window,
workspace,
} from "vscode";
import { Act } from "./act";
import { ConfigurationManager, Section } from "./configurationManager";
import { IssueHandler } from "./issueHandler";
import ComponentsTreeDataProvider from "./views/components/componentsTreeDataProvider";
import { DecorationProvider } from "./views/decorationProvider";
import { GithubLocalActionsTreeItem } from "./views/githubLocalActionsTreeItem";
import HistoryTreeDataProvider from "./views/history/historyTreeDataProvider";
import SettingTreeItem from "./views/settings/setting";
import SettingsTreeDataProvider from "./views/settings/settingsTreeDataProvider";
import WorkflowsTreeDataProvider from "./views/workflows/workflowsTreeDataProvider";
import { WorkflowsManager } from "./workflowsManager";
export let act: Act;
export let componentsTreeDataProvider: ComponentsTreeDataProvider;
@@ -17,63 +25,121 @@ export let workflowsTreeDataProvider: WorkflowsTreeDataProvider;
export let historyTreeDataProvider: HistoryTreeDataProvider;
export let settingsTreeDataProvider: SettingsTreeDataProvider;
export function activate(context: vscode.ExtensionContext) {
console.log('Congratulations, your extension "github-local-actions" is now active!');
export function activate(context: ExtensionContext) {
console.log(
'Congratulations, your extension "github-local-actions" is now active!'
);
act = new Act(context);
act = new Act(context);
// Create tree views
const decorationProvider = new DecorationProvider();
componentsTreeDataProvider = new ComponentsTreeDataProvider(context);
const componentsTreeView = window.createTreeView(ComponentsTreeDataProvider.VIEW_ID, { treeDataProvider: componentsTreeDataProvider, showCollapseAll: true });
workflowsTreeDataProvider = new WorkflowsTreeDataProvider(context);
const workflowsTreeView = window.createTreeView(WorkflowsTreeDataProvider.VIEW_ID, { treeDataProvider: workflowsTreeDataProvider, showCollapseAll: true });
historyTreeDataProvider = new HistoryTreeDataProvider(context);
const historyTreeView = window.createTreeView(HistoryTreeDataProvider.VIEW_ID, { treeDataProvider: historyTreeDataProvider, showCollapseAll: true });
settingsTreeDataProvider = new SettingsTreeDataProvider(context);
const settingsTreeView = window.createTreeView(SettingsTreeDataProvider.VIEW_ID, { treeDataProvider: settingsTreeDataProvider, showCollapseAll: true });
settingsTreeView.onDidChangeCheckboxState(async (event: TreeCheckboxChangeEvent<GithubLocalActionsTreeItem>) => {
await settingsTreeDataProvider.onDidChangeCheckboxState(event as TreeCheckboxChangeEvent<SettingTreeItem>);
});
// Create tree views
const decorationProvider = new DecorationProvider();
componentsTreeDataProvider = new ComponentsTreeDataProvider(context);
const componentsTreeView = window.createTreeView(
ComponentsTreeDataProvider.VIEW_ID,
{ treeDataProvider: componentsTreeDataProvider, showCollapseAll: true }
);
// Create file watcher
const workflowsFileWatcher = workspace.createFileSystemWatcher(`**/${WorkflowsManager.WORKFLOWS_DIRECTORY}/*.{${WorkflowsManager.YML_EXTENSION},${WorkflowsManager.YAML_EXTENSION}}`);
workflowsFileWatcher.onDidCreate(() => {
workflowsTreeDataProvider.refresh();
settingsTreeDataProvider.refresh();
});
workflowsFileWatcher.onDidChange(() => {
workflowsTreeDataProvider.refresh();
settingsTreeDataProvider.refresh();
});
workflowsFileWatcher.onDidDelete(() => {
workflowsTreeDataProvider.refresh();
settingsTreeDataProvider.refresh();
});
workflowsTreeDataProvider = new WorkflowsTreeDataProvider(context);
const workflowsTreeView = window.createTreeView(
WorkflowsTreeDataProvider.VIEW_ID,
{ treeDataProvider: workflowsTreeDataProvider, showCollapseAll: true }
);
// Initialize configurations
ConfigurationManager.initialize();
workspace.onDidChangeConfiguration(async event => {
if (event.affectsConfiguration(ConfigurationManager.group)) {
await ConfigurationManager.initialize();
componentsTreeDataProvider.refresh();
}
});
historyTreeDataProvider = new HistoryTreeDataProvider(context);
const historyTreeView = window.createTreeView(
HistoryTreeDataProvider.VIEW_ID,
{ treeDataProvider: historyTreeDataProvider, showCollapseAll: true }
);
settingsTreeDataProvider = new SettingsTreeDataProvider(context);
const settingsTreeView = window.createTreeView(
SettingsTreeDataProvider.VIEW_ID,
{ treeDataProvider: settingsTreeDataProvider, showCollapseAll: true }
);
settingsTreeView.onDidChangeCheckboxState(
async (event: TreeCheckboxChangeEvent<GithubLocalActionsTreeItem>) => {
await settingsTreeDataProvider.onDidChangeCheckboxState(
event as TreeCheckboxChangeEvent<SettingTreeItem>
);
}
);
context.subscriptions.push(
componentsTreeView,
workflowsTreeView,
historyTreeView,
settingsTreeView,
window.registerFileDecorationProvider(decorationProvider),
workflowsFileWatcher,
commands.registerCommand('githubLocalActions.viewDocumentation', async () => {
await env.openExternal(Uri.parse('https://nektosact.com'));
}),
commands.registerCommand('githubLocalActions.reportAnIssue', async () => {
await env.openExternal(Uri.parse('https://github.com/SanjulaGanepola/github-local-actions/issues'));
}),
);
// Create file watcher
let workflowsFileWatcher = setupFileWatcher(context);
// Initialize configurations
ConfigurationManager.initialize();
workspace.onDidChangeConfiguration(async (event) => {
if (event.affectsConfiguration(ConfigurationManager.group)) {
await ConfigurationManager.initialize();
if (
event.affectsConfiguration(
`${ConfigurationManager.group}.${Section.actCommand}`
) ||
event.affectsConfiguration(
`${ConfigurationManager.group}.${Section.dockerDesktopPath}`
)
) {
componentsTreeDataProvider.refresh();
}
}
});
context.subscriptions.push(
componentsTreeView,
workflowsTreeView,
historyTreeView,
settingsTreeView,
window.registerFileDecorationProvider(decorationProvider),
workflowsFileWatcher,
commands.registerCommand(
"githubLocalActions.viewDocumentation",
async () => {
await env.openExternal(
Uri.parse(
"https://sanjulaganepola.github.io/github-local-actions-docs"
)
);
}
),
commands.registerCommand("githubLocalActions.reportAnIssue", async () => {
await IssueHandler.openBugReport(context);
})
);
}
export function deactivate() { }
function setupFileWatcher(context: ExtensionContext) {
const workflowsDirectories = WorkflowsManager.getWorkflowsDirectories();
const fileWatchers: any[] = [];
for (const workflowsDirectory of workflowsDirectories) {
const workflowsFileWatcher = workspace.createFileSystemWatcher(
`**/${workflowsDirectory}/*.{${WorkflowsManager.ymlExtension},${WorkflowsManager.yamlExtension}}`
);
workflowsFileWatcher.onDidCreate(() => {
workflowsTreeDataProvider.refresh();
settingsTreeDataProvider.refresh();
});
workflowsFileWatcher.onDidChange(() => {
workflowsTreeDataProvider.refresh();
settingsTreeDataProvider.refresh();
});
workflowsFileWatcher.onDidDelete(() => {
workflowsTreeDataProvider.refresh();
settingsTreeDataProvider.refresh();
});
fileWatchers.push(workflowsFileWatcher);
}
// Return a disposable that disposes all watchers
return {
dispose: () => {
fileWatchers.forEach((watcher) => watcher.dispose());
},
};
}
export function deactivate() {}

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);
}
});
}

View File

@@ -49,11 +49,26 @@ export enum HistoryStatus {
export class HistoryManager {
storageManager: StorageManager;
workspaceHistory: { [path: string]: History[] };
private workspaceHistory: { [path: string]: History[] };
constructor(storageManager: StorageManager) {
this.storageManager = storageManager;
const workspaceHistory = this.storageManager.get<{ [path: string]: History[] }>(StorageKey.WorkspaceHistory) || {};
this.workspaceHistory = {};
this.syncHistory();
}
async getWorkspaceHistory() {
if (!this.workspaceHistory) {
await this.syncHistory();
}
return this.workspaceHistory;
}
async syncHistory() {
const workspaceHistory = await this.storageManager.get<{ [path: string]: History[] }>(StorageKey.WorkspaceHistory) || {};
for (const [path, historyLogs] of Object.entries(workspaceHistory)) {
workspaceHistory[path] = historyLogs.map(history => {
history.jobs?.forEach((job, jobIndex) => {
@@ -79,22 +94,27 @@ export class HistoryManager {
});
}
this.workspaceHistory = workspaceHistory;
}
};
async clearAll(workspaceFolder: WorkspaceFolder) {
const existingHistory = this.workspaceHistory[workspaceFolder.uri.fsPath];
await this.syncHistory();
const existingHistory = this.workspaceHistory?.[workspaceFolder.uri.fsPath] ?? [];
for (const history of existingHistory) {
try {
await workspace.fs.delete(Uri.file(history.logPath));
} catch (error: any) { }
}
this.workspaceHistory[workspaceFolder.uri.fsPath] = [];
if (this.workspaceHistory) {
this.workspaceHistory[workspaceFolder.uri.fsPath] = [];
}
historyTreeDataProvider.refresh();
await this.storageManager.update(StorageKey.WorkspaceHistory, this.workspaceHistory);
}
async viewOutput(history: History) {
await this.syncHistory();
try {
const document = await workspace.openTextDocument(history.logPath);
await window.showTextDocument(document);
@@ -112,9 +132,10 @@ export class HistoryManager {
}
async remove(history: History) {
const historyIndex = this.workspaceHistory[history.commandArgs.path].findIndex(workspaceHistory => workspaceHistory.index === history.index);
await this.syncHistory();
const historyIndex = (this.workspaceHistory?.[history.commandArgs.path] ?? []).findIndex(workspaceHistory => workspaceHistory.index === history.index);
if (historyIndex > -1) {
this.workspaceHistory[history.commandArgs.path].splice(historyIndex, 1);
(this.workspaceHistory?.[history.commandArgs.path] ?? []).splice(historyIndex, 1);
await this.storageManager.update(StorageKey.WorkspaceHistory, this.workspaceHistory);
try {

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 = ((await act.historyManager.getWorkspaceHistory())[workspaceFolder.uri.fsPath] ?? []).filter(history => history.commandArgs.workflow?.uri.fsPath === workflow.uri.fsPath);
if (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

@@ -124,7 +124,7 @@ export class SettingsManager {
}
}
const existingSettings = this.storageManager.get<{ [path: string]: Setting[] }>(storageKey) || {};
const existingSettings = await 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);
@@ -154,7 +154,7 @@ export class SettingsManager {
}
async getCustomSettings(workspaceFolder: WorkspaceFolder, storageKey: StorageKey): Promise<CustomSetting[]> {
const existingCustomSettings = this.storageManager.get<{ [path: string]: CustomSetting[] }>(storageKey) || {};
const existingCustomSettings = await this.storageManager.get<{ [path: string]: CustomSetting[] }>(storageKey) || {};
return existingCustomSettings[workspaceFolder.uri.fsPath] || [];
}
@@ -233,7 +233,7 @@ export class SettingsManager {
}
async editCustomSetting(workspaceFolder: WorkspaceFolder, newCustomSetting: CustomSetting, storageKey: StorageKey, forceAppend: boolean = false) {
const existingCustomSettings = this.storageManager.get<{ [path: string]: CustomSetting[] }>(storageKey) || {};
const existingCustomSettings = await this.storageManager.get<{ [path: string]: CustomSetting[] }>(storageKey) || {};
if (existingCustomSettings[workspaceFolder.uri.fsPath]) {
const index = existingCustomSettings[workspaceFolder.uri.fsPath]
.findIndex(customSetting =>
@@ -254,7 +254,7 @@ export class SettingsManager {
}
async removeCustomSetting(workspaceFolder: WorkspaceFolder, existingCustomSetting: CustomSetting, storageKey: StorageKey) {
const existingCustomSettings = this.storageManager.get<{ [path: string]: CustomSetting[] }>(storageKey) || {};
const existingCustomSettings = await this.storageManager.get<{ [path: string]: CustomSetting[] }>(storageKey) || {};
if (existingCustomSettings[workspaceFolder.uri.fsPath]) {
const index = existingCustomSettings[workspaceFolder.uri.fsPath].findIndex(customSetting =>
storageKey === StorageKey.Options ?
@@ -289,7 +289,7 @@ export class SettingsManager {
newSetting.value = '';
}
const existingSettings = this.storageManager.get<{ [path: string]: Setting[] }>(storageKey) || {};
const existingSettings = await 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) {

View File

@@ -1,4 +1,4 @@
import { ExtensionContext } from "vscode";
import { ExtensionContext, Uri, workspace } from "vscode";
export enum StorageKey {
WorkspaceHistory = 'workspaceHistory',
@@ -9,7 +9,7 @@ export enum StorageKey {
Inputs = 'inputs',
InputFiles = 'inputFiles',
Runners = 'runners',
PayloadFiles = 'PayloadFiles',
PayloadFiles = 'payloadFiles',
Options = 'options'
}
@@ -21,15 +21,43 @@ export class StorageManager {
this.context = context;
}
keys(): readonly string[] {
return this.context.globalState.keys();
private async getStorageDirectory(): Promise<Uri> {
const storageDirectory = Uri.joinPath(this.context.storageUri ?? this.context.globalStorageUri, "storageManager");
await workspace.fs.createDirectory(storageDirectory).then(undefined, () => void 0);
return storageDirectory;
}
get<T>(storageKey: StorageKey): T | undefined {
return this.context.globalState.get<T>(`${this.extensionKey}.${storageKey}`);
private async getStorageFile(storageKey: StorageKey): Promise<Uri> {
const storageDirectory = await this.getStorageDirectory();
return Uri.joinPath(storageDirectory, `${storageKey}.json`);
}
async get<T>(storageKey: StorageKey): Promise<T | undefined> {
if ([StorageKey.Secrets, StorageKey.SecretFiles].includes(storageKey)) {
return this.context.globalState.get<T>(`${this.extensionKey}.${storageKey}`);
}
const storageFile = await this.getStorageFile(storageKey);
return workspace.fs.readFile(storageFile).then(data => {
if (data) {
return JSON.parse(data.toString()) as T;
}
return undefined;
}, (error) => {
if (error.code === 'FileNotFound') {
return undefined;
}
});
}
async update(storageKey: StorageKey, value: any): Promise<void> {
await this.context.globalState.update(`${this.extensionKey}.${storageKey}`, value);
if ([StorageKey.Secrets, StorageKey.SecretFiles].includes(storageKey)) {
await this.context.globalState.update(`${this.extensionKey}.${storageKey}`, value);
return;
}
const data = JSON.stringify(value, null, 2);
const storageFile = await this.getStorageFile(storageKey);
await workspace.fs.writeFile(storageFile, Buffer.from(data));
}
}

View File

@@ -87,18 +87,18 @@ export default class HistoryTreeDataProvider implements TreeDataProvider<GithubL
if (workspaceFolders.length === 1) {
items.push(...await new WorkspaceFolderHistoryTreeItem(workspaceFolders[0]).getChildren());
const workspaceHistory = act.historyManager.workspaceHistory[workspaceFolders[0].uri.fsPath];
if (workspaceHistory && workspaceHistory.length > 0) {
isRunning = act.historyManager.workspaceHistory[workspaceFolders[0].uri.fsPath].find(workspaceHistory => workspaceHistory.status === HistoryStatus.Running) !== undefined;
const workspaceHistory = (await act.historyManager.getWorkspaceHistory())[workspaceFolders[0].uri.fsPath] ?? [];
if (workspaceHistory.length > 0) {
isRunning = workspaceHistory.find(workspaceHistory => workspaceHistory.status === HistoryStatus.Running) !== undefined;
noHistory = false;
}
} else if (workspaceFolders.length > 1) {
for (const workspaceFolder of workspaceFolders) {
items.push(new WorkspaceFolderHistoryTreeItem(workspaceFolder));
const workspaceHistory = act.historyManager.workspaceHistory[workspaceFolder.uri.fsPath];
if (workspaceHistory && workspaceHistory.length > 0) {
isRunning = act.historyManager.workspaceHistory[workspaceFolder.uri.fsPath].find(workspaceHistory => workspaceHistory.status === HistoryStatus.Running) !== undefined;
const workspaceHistory = (await act.historyManager.getWorkspaceHistory())[workspaceFolder.uri.fsPath] ?? [];
if (workspaceHistory.length > 0) {
isRunning = workspaceHistory.find(workspaceHistory => workspaceHistory.status === HistoryStatus.Running) !== undefined;
noHistory = false;
}
}

View File

@@ -15,7 +15,7 @@ export default class WorkspaceFolderHistoryTreeItem extends TreeItem implements
async getChildren(): Promise<GithubLocalActionsTreeItem[]> {
const items: GithubLocalActionsTreeItem[] = [];
const workspaceHistory = act.historyManager.workspaceHistory[this.workspaceFolder.uri.fsPath];
const workspaceHistory = (await act.historyManager.getWorkspaceHistory())[this.workspaceFolder.uri.fsPath];
if (workspaceHistory) {
for (const history of workspaceHistory.slice().reverse()) {
items.push(new HistoryTreeItem(this.workspaceFolder, history));

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[] = [];

View File

@@ -1,5 +1,14 @@
import * as path from "path";
import { CancellationToken, commands, EventEmitter, ExtensionContext, TreeDataProvider, TreeItem, window, workspace } from "vscode";
import {
CancellationToken,
commands,
EventEmitter,
ExtensionContext,
TreeDataProvider,
TreeItem,
window,
workspace,
} from "vscode";
import { Event } from "../../act";
import { act } from "../../extension";
import { Utils } from "../../utils";
@@ -9,139 +18,276 @@ import JobTreeItem from "./job";
import WorkflowTreeItem from "./workflow";
import WorkspaceFolderWorkflowsTreeItem from "./workspaceFolderWorkflows";
export default class WorkflowsTreeDataProvider implements TreeDataProvider<GithubLocalActionsTreeItem> {
private _onDidChangeTreeData = new EventEmitter<GithubLocalActionsTreeItem | undefined | null | void>();
readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
static VIEW_ID = 'workflows';
export default class WorkflowsTreeDataProvider
implements TreeDataProvider<GithubLocalActionsTreeItem>
{
private _onDidChangeTreeData = new EventEmitter<
GithubLocalActionsTreeItem | undefined | null | void
>();
readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
static VIEW_ID = "workflows";
constructor(context: ExtensionContext) {
context.subscriptions.push(
commands.registerCommand('githubLocalActions.runAllWorkflows', async (workspaceFolderWorkflowsTreeItem?: WorkspaceFolderWorkflowsTreeItem) => {
const workspaceFolder = await Utils.getWorkspaceFolder(workspaceFolderWorkflowsTreeItem?.workspaceFolder);
if (workspaceFolder) {
await act.runAllWorkflows(workspaceFolder);
}
}),
commands.registerCommand('githubLocalActions.runEvent', async (workspaceFolderWorkflowsTreeItem?: WorkspaceFolderWorkflowsTreeItem) => {
const workspaceFolder = await Utils.getWorkspaceFolder(workspaceFolderWorkflowsTreeItem?.workspaceFolder);
if (workspaceFolder) {
const event = await window.showQuickPick(Object.values(Event), {
title: 'Select the event to run',
placeHolder: 'Event'
});
if (event) {
await act.runEvent(workspaceFolder, event as Event);
}
}
}),
commands.registerCommand('githubLocalActions.refreshWorkflows', async () => {
this.refresh();
}),
commands.registerCommand('githubLocalActions.openWorkflow', async (workflowTreeItem: WorkflowTreeItem) => {
try {
const document = await workspace.openTextDocument(workflowTreeItem.workflow.uri);
await window.showTextDocument(document);
} catch (error: any) {
try {
await workspace.fs.stat(workflowTreeItem.workflow.uri);
window.showErrorMessage(`Failed to open workflow. Error: ${error}`);
} catch (error: any) {
window.showErrorMessage(`Workflow ${path.parse(workflowTreeItem.workflow.uri.fsPath).base} not found.`);
}
}
}),
commands.registerCommand('githubLocalActions.runWorkflow', async (workflowTreeItem: WorkflowTreeItem) => {
if (workflowTreeItem) {
await act.runWorkflow(workflowTreeItem.workspaceFolder, workflowTreeItem.workflow);
} else {
let errorMessage: string | undefined;
const activeTextEditor = window.activeTextEditor;
if (activeTextEditor) {
const uri = activeTextEditor.document.uri;
const fileName = path.parse(uri.fsPath).base;
if (uri.path.match(`.*/${WorkflowsManager.WORKFLOWS_DIRECTORY}/.*\\.(${WorkflowsManager.YAML_EXTENSION}|${WorkflowsManager.YML_EXTENSION})`)) {
const workspaceFolder = workspace.getWorkspaceFolder(uri);
if (workspaceFolder) {
const workflows = await act.workflowsManager.getWorkflows(workspaceFolder);
const workflow = workflows.find(workflow => workflow.uri.fsPath === uri.fsPath);
if (workflow) {
await act.runWorkflow(workspaceFolder, workflow);
} else {
errorMessage = `Workflow not found in workflow directory (${WorkflowsManager.WORKFLOWS_DIRECTORY}).`;
}
} else {
errorMessage = `${fileName} must be opened in a workspace folder to be executed locally.`;
}
} else {
errorMessage = `${fileName} is not a workflow that can be executed locally.`;
}
} else {
errorMessage = 'No workflow opened to execute locally.';
}
if (errorMessage) {
window.showErrorMessage(errorMessage, 'View Workflows').then(async value => {
if (value === 'View Workflows') {
await commands.executeCommand('workflows.focus');
}
});
}
}
}),
commands.registerCommand('githubLocalActions.runJob', async (jobTreeItem: JobTreeItem) => {
await act.runJob(jobTreeItem.workspaceFolder, jobTreeItem.workflow, jobTreeItem.job);
})
);
}
refresh(element?: GithubLocalActionsTreeItem) {
this._onDidChangeTreeData.fire(element);
}
getTreeItem(element: GithubLocalActionsTreeItem): GithubLocalActionsTreeItem | Thenable<GithubLocalActionsTreeItem> {
return element;
}
async resolveTreeItem(item: TreeItem, element: GithubLocalActionsTreeItem, token: CancellationToken): Promise<GithubLocalActionsTreeItem> {
if (element.getToolTip) {
element.tooltip = await element.getToolTip();
constructor(context: ExtensionContext) {
context.subscriptions.push(
commands.registerCommand(
"githubLocalActions.runAllWorkflows",
async (
workspaceFolderWorkflowsTreeItem?: WorkspaceFolderWorkflowsTreeItem
) => {
const workspaceFolder = await Utils.getWorkspaceFolder(
workspaceFolderWorkflowsTreeItem?.workspaceFolder
);
if (workspaceFolder) {
await act.runAllWorkflows(workspaceFolder);
}
}
),
commands.registerCommand(
"githubLocalActions.runEvent",
async (
workspaceFolderWorkflowsTreeItem?: WorkspaceFolderWorkflowsTreeItem
) => {
const workspaceFolder = await Utils.getWorkspaceFolder(
workspaceFolderWorkflowsTreeItem?.workspaceFolder
);
if (workspaceFolder) {
const event = await window.showQuickPick(Object.values(Event), {
title: "Select the event to run",
placeHolder: "Event",
});
return element;
}
if (event) {
await act.runEvent(workspaceFolder, event as Event);
}
}
}
),
commands.registerCommand(
"githubLocalActions.refreshWorkflows",
async () => {
this.refresh();
}
),
commands.registerCommand(
"githubLocalActions.openWorkflow",
async (workflowTreeItem: WorkflowTreeItem) => {
try {
const document = await workspace.openTextDocument(
workflowTreeItem.workflow.uri
);
await window.showTextDocument(document);
} catch (error: any) {
try {
await workspace.fs.stat(workflowTreeItem.workflow.uri);
window.showErrorMessage(
`Failed to open workflow. Error: ${error}`
);
} catch (error: any) {
window.showErrorMessage(
`Workflow ${
path.parse(workflowTreeItem.workflow.uri.fsPath).base
} not found.`
);
}
}
}
),
commands.registerCommand(
"githubLocalActions.runWorkflow",
async (workflowTreeItem: WorkflowTreeItem) => {
if (workflowTreeItem) {
await act.runWorkflow(
workflowTreeItem.workspaceFolder,
workflowTreeItem.workflow
);
} else {
let errorMessage: string | undefined;
async getChildren(element?: GithubLocalActionsTreeItem): Promise<GithubLocalActionsTreeItem[]> {
if (element) {
return element.getChildren();
} else {
const items: GithubLocalActionsTreeItem[] = [];
let noWorkflows: boolean = true;
const workspaceFolders = workspace.workspaceFolders;
if (workspaceFolders) {
if (workspaceFolders.length === 1) {
items.push(...await new WorkspaceFolderWorkflowsTreeItem(workspaceFolders[0]).getChildren());
const workflows = await act.workflowsManager.getWorkflows(workspaceFolders[0]);
if (workflows && workflows.length > 0) {
noWorkflows = false;
}
} else if (workspaceFolders.length > 1) {
for (const workspaceFolder of workspaceFolders) {
items.push(new WorkspaceFolderWorkflowsTreeItem(workspaceFolder));
const workflows = await act.workflowsManager.getWorkflows(workspaceFolder);
if (workflows && workflows.length > 0) {
noWorkflows = false;
}
}
const activeTextEditor = window.activeTextEditor;
if (activeTextEditor) {
const uri = activeTextEditor.document.uri;
const fileName = path.parse(uri.fsPath).base;
const workflowsDirectory =
WorkflowsManager.getWorkflowsDirectory();
if (
uri.path.match(
`.*/${workflowsDirectory}/.*\\.(${WorkflowsManager.yamlExtension}|${WorkflowsManager.ymlExtension})`
)
) {
const workspaceFolder = workspace.getWorkspaceFolder(uri);
if (workspaceFolder) {
const workflows = await act.workflowsManager.getWorkflows(
workspaceFolder
);
const workflow = workflows.find(
(workflow) => workflow.uri.fsPath === uri.fsPath
);
if (workflow) {
await act.runWorkflow(workspaceFolder, workflow);
} else {
errorMessage = `Workflow not found in workflow directory (${workflowsDirectory}).`;
}
} else {
errorMessage = `${fileName} must be opened in a workspace folder to be executed locally.`;
}
} else {
errorMessage = `${fileName} is not a workflow that can be executed locally.`;
}
} else {
errorMessage = "No workflow opened to execute locally.";
}
await commands.executeCommand('setContext', 'githubLocalActions:noWorkflows', noWorkflows);
return items;
if (errorMessage) {
window
.showErrorMessage(errorMessage, "View Workflows")
.then(async (value) => {
if (value === "View Workflows") {
await commands.executeCommand("workflows.focus");
}
});
}
}
}
),
commands.registerCommand(
"githubLocalActions.runJob",
async (jobTreeItem: JobTreeItem) => {
await act.runJob(
jobTreeItem.workspaceFolder,
jobTreeItem.workflow,
jobTreeItem.job
);
}
),
commands.registerCommand(
"githubLocalActions.runWorkflowEvent",
async (workflowTreeItem: WorkflowTreeItem) => {
// Filter to only events that are registered on the workflow
const registeredEventsOnWorkflow = Object.keys(
workflowTreeItem.workflow.yaml.on
);
if (registeredEventsOnWorkflow.length === 0) {
window.showErrorMessage(
`No events registered on the workflow (${workflowTreeItem.workflow.name}). Add an event to the \`on\` section of the workflow to trigger it.`
);
return;
}
const event = await window.showQuickPick(registeredEventsOnWorkflow, {
title: "Select the event to run",
placeHolder: "Event",
});
if (event) {
await act.runEvent(
workflowTreeItem.workspaceFolder,
event as Event,
{ workflow: workflowTreeItem.workflow }
);
}
}
),
commands.registerCommand(
"githubLocalActions.runJobEvent",
async (jobTreeItem: JobTreeItem) => {
// Filter to only events that are registered on the job's parent workflow
const registeredEventsOnJobParentWorkflow = Object.keys(
jobTreeItem.workflow.yaml.on
);
if (registeredEventsOnJobParentWorkflow.length === 0) {
window.showErrorMessage(
`No events registered on the workflow (${jobTreeItem.workflow.name}). Add an event to the \`on\` section of the workflow to trigger it.`
);
return;
}
const event = await window.showQuickPick(
registeredEventsOnJobParentWorkflow,
{
title: "Select the event to run",
placeHolder: "Event",
}
);
if (event) {
await act.runEvent(jobTreeItem.workspaceFolder, event as Event, {
workflow: jobTreeItem.workflow,
job: jobTreeItem.job,
});
}
}
)
);
}
refresh(element?: GithubLocalActionsTreeItem) {
this._onDidChangeTreeData.fire(element);
}
getTreeItem(
element: GithubLocalActionsTreeItem
): GithubLocalActionsTreeItem | Thenable<GithubLocalActionsTreeItem> {
return element;
}
async resolveTreeItem(
item: TreeItem,
element: GithubLocalActionsTreeItem,
token: CancellationToken
): Promise<GithubLocalActionsTreeItem> {
if (element.getToolTip) {
element.tooltip = await element.getToolTip();
}
}
return element;
}
async getChildren(
element?: GithubLocalActionsTreeItem
): Promise<GithubLocalActionsTreeItem[]> {
if (element) {
return element.getChildren();
} else {
const items: GithubLocalActionsTreeItem[] = [];
let noWorkflows: boolean = true;
const workspaceFolders = workspace.workspaceFolders;
if (workspaceFolders) {
if (workspaceFolders.length === 1) {
items.push(
...(await new WorkspaceFolderWorkflowsTreeItem(
workspaceFolders[0]
).getChildren())
);
const workflows = await act.workflowsManager.getWorkflows(
workspaceFolders[0]
);
if (workflows && workflows.length > 0) {
noWorkflows = false;
}
} else if (workspaceFolders.length > 1) {
for (const workspaceFolder of workspaceFolders) {
items.push(new WorkspaceFolderWorkflowsTreeItem(workspaceFolder));
const workflows = await act.workflowsManager.getWorkflows(
workspaceFolder
);
if (workflows && workflows.length > 0) {
noWorkflows = false;
}
}
}
}
await commands.executeCommand(
"setContext",
"githubLocalActions:noWorkflows",
noWorkflows
);
return items;
}
}
}

View File

@@ -4,49 +4,82 @@ import { RelativePattern, Uri, workspace, WorkspaceFolder } from "vscode";
import * as yaml from "yaml";
export interface Workflow {
name: string,
uri: Uri,
fileContent?: string,
yaml?: any,
error?: string
name: string;
uri: Uri;
fileContent?: string;
yaml?: any;
error?: string;
}
export interface Job {
name: string
id: string
name: string;
id: string;
}
export class WorkflowsManager {
static WORKFLOWS_DIRECTORY: string = '.github/workflows';
static YAML_EXTENSION: string = 'yaml';
static YML_EXTENSION: string = 'yml';
static defaultWorkflowsDirectory: string = ".github/workflows";
static giteaWorkflowsDirectory: string = ".gitea/workflows";
static yamlExtension: string = "yaml";
static ymlExtension: string = "yml";
static getWorkflowsDirectories(): string[] {
const directories = [
WorkflowsManager.defaultWorkflowsDirectory,
WorkflowsManager.giteaWorkflowsDirectory,
];
return directories;
}
static getWorkflowsDirectory(): string {
return WorkflowsManager.defaultWorkflowsDirectory;
}
async getWorkflows(workspaceFolder: WorkspaceFolder): Promise<Workflow[]> {
const workflows: Workflow[] = [];
const workflowFileUris = await workspace.findFiles(new RelativePattern(workspaceFolder, `${WorkflowsManager.WORKFLOWS_DIRECTORY}/*.{${WorkflowsManager.YAML_EXTENSION},${WorkflowsManager.YML_EXTENSION}}`));
for await (const workflowFileUri of workflowFileUris) {
let yamlContent: any | undefined;
const workflowsDirectories = WorkflowsManager.getWorkflowsDirectories();
for (const workflowsDirectory of workflowsDirectories) {
try {
const fileContent = await fs.readFile(workflowFileUri.fsPath, 'utf8');
yamlContent = yaml.parse(fileContent);
const workflowFileUris = await workspace.findFiles(
new RelativePattern(
workspaceFolder,
`${workflowsDirectory}/*.{${WorkflowsManager.yamlExtension},${WorkflowsManager.ymlExtension}}`
)
);
workflows.push({
name: yamlContent.name || path.parse(workflowFileUri.fsPath).name,
uri: workflowFileUri,
fileContent: fileContent,
yaml: yaml.parse(fileContent)
});
} catch (error: any) {
workflows.push({
name: (yamlContent ? yamlContent.name : undefined) || path.parse(workflowFileUri.fsPath).name,
uri: workflowFileUri,
error: 'Failed to parse workflow'
});
for await (const workflowFileUri of workflowFileUris) {
let yamlContent: any | undefined;
try {
const fileContent = await fs.readFile(
workflowFileUri.fsPath,
"utf8"
);
yamlContent = yaml.parse(fileContent);
workflows.push({
name: yamlContent.name || path.parse(workflowFileUri.fsPath).name,
uri: workflowFileUri,
fileContent: fileContent,
yaml: yaml.parse(fileContent),
});
} catch (error: any) {
workflows.push({
name:
(yamlContent ? yamlContent.name : undefined) ||
path.parse(workflowFileUri.fsPath).name,
uri: workflowFileUri,
error: "Failed to parse workflow",
});
}
}
} catch (error) {
// Directory doesn't exist, skip it
continue;
}
}
return workflows;
}
}
}