Compare commits

...

11 Commits

Author SHA1 Message Date
513aad88bb refactor: Remove all popup notifications and status bar items - Replace all window.showInformationMessage, window.showErrorMessage, and window.showWarningMessage with silent handling - Remove status bar items and related code - Improve user experience by eliminating intrusive notifications - Maintain functionality while providing cleaner, less disruptive interface
Some checks failed
Test Gitea Workflow / test (push) Has been cancelled
2025-08-03 23:12:13 +07:00
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
24 changed files with 5109 additions and 1352 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: labels:
- 'bug' - 'bug'
body: 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 - type: input
id: github-local-actions-version id: github-local-actions-version
attributes: attributes:
@@ -76,14 +80,6 @@ body:
render: sh render: sh
validations: 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
- type: textarea - type: textarea
id: act-bug-report id: act-bug-report
attributes: attributes:
@@ -104,3 +100,11 @@ body:
render: yml render: yml
validations: 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: on:
workflow_dispatch: 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: release:
types: [created] types: [created]
@@ -30,7 +41,9 @@ jobs:
npm install -g vsce ovsx npm install -g vsce ovsx
- name: Publish to Open VSX - name: Publish to Open VSX
if: github.event_name == 'release' || inputs.publish_openvsx == true
run: npx ovsx publish -p ${{ secrets.OPEN_VSX_TOKEN }} run: npx ovsx publish -p ${{ secrets.OPEN_VSX_TOKEN }}
- name: Publish to Marketplace - name: Publish to Marketplace
if: github.event_name == 'release' || inputs.publish_marketplace == true
run: vsce publish -p ${{ secrets.VS_MARKETPLACE_TOKEN }} run: vsce publish -p ${{ secrets.VS_MARKETPLACE_TOKEN }}

View File

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

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. 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 ## Getting Started
@@ -25,5 +25,7 @@ Thanks so much to everyone [who has contributed](https://github.com/SanjulaGanep
* [@SanjulaGanepola](https://github.com/SanjulaGanepola) * [@SanjulaGanepola](https://github.com/SanjulaGanepola)
* [@ChristopherHX](https://github.com/ChristopherHX) * [@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! 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. 2. **Run Single Workflow**: Run a single workflow in the workspace.
3. **Run Job**: Run a specific job in a workflow. 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). 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) ![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", "name": "github-local-actions",
"version": "1.2.2", "version": "1.2.5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "github-local-actions", "name": "github-local-actions",
"version": "1.2.2", "version": "1.2.5",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"child_process": "^1.0.2", "child_process": "^1.0.2",

View File

@@ -9,11 +9,11 @@
}, },
"publisher": "SanjulaGanepola", "publisher": "SanjulaGanepola",
"license": "Apache-2.0", "license": "Apache-2.0",
"version": "1.2.2", "version": "1.2.5",
"repository": { "repository": {
"url": "https://github.com/SanjulaGanepola/github-local-actions" "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": { "bugs": {
"url": "https://github.com/SanjulaGanepola/github-local-actions/issues" "url": "https://github.com/SanjulaGanepola/github-local-actions/issues"
}, },
@@ -131,13 +131,6 @@
"when": "githubLocalActions:noSettings && workspaceFolderCount > 0" "when": "githubLocalActions:noSettings && workspaceFolderCount > 0"
} }
], ],
"keybindings": [
{
"command": "githubLocalActions.runWorkflow",
"key": "ctrl+g",
"mac": "cmd+g"
}
],
"commands": [ "commands": [
{ {
"category": "GitHub Local Actions", "category": "GitHub Local Actions",
@@ -189,24 +182,36 @@
"title": "Refresh", "title": "Refresh",
"icon": "$(refresh)" "icon": "$(refresh)"
}, },
{
"category": "GitHub Local Actions",
"command": "githubLocalActions.openWorkflow",
"title": "Open Workflow",
"icon": "$(go-to-file)"
},
{ {
"category": "GitHub Local Actions", "category": "GitHub Local Actions",
"command": "githubLocalActions.runWorkflow", "command": "githubLocalActions.runWorkflow",
"title": "Run Workflow", "title": "Run Workflow",
"icon": "$(debug-start)" "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", "category": "GitHub Local Actions",
"command": "githubLocalActions.runJob", "command": "githubLocalActions.runJob",
"title": "Run Job", "title": "Run Job",
"icon": "$(debug-start)" "icon": "$(debug-start)"
}, },
{
"category": "GitHub Local Actions",
"command": "githubLocalActions.runJobEvent",
"title": "Run Job with Event",
"icon": "$(symbol-event)"
},
{ {
"category": "GitHub Local Actions", "category": "GitHub Local Actions",
"command": "githubLocalActions.clearAll", "command": "githubLocalActions.clearAll",
@@ -424,18 +429,26 @@
"command": "githubLocalActions.refreshWorkflows", "command": "githubLocalActions.refreshWorkflows",
"when": "never" "when": "never"
}, },
{
"command": "githubLocalActions.openWorkflow",
"when": "never"
},
{ {
"command": "githubLocalActions.runWorkflow", "command": "githubLocalActions.runWorkflow",
"when": "never" "when": "never"
}, },
{
"command": "githubLocalActions.runWorkflowEvent",
"when": "never"
},
{
"command": "githubLocalActions.openWorkflow",
"when": "never"
},
{ {
"command": "githubLocalActions.runJob", "command": "githubLocalActions.runJob",
"when": "never" "when": "never"
}, },
{
"command": "githubLocalActions.runJobEvent",
"when": "never"
},
{ {
"command": "githubLocalActions.clearAll", "command": "githubLocalActions.clearAll",
"when": "never" "when": "never"
@@ -637,20 +650,55 @@
"group": "inline@1" "group": "inline@1"
}, },
{ {
"command": "githubLocalActions.openWorkflow", "command": "githubLocalActions.runWorkflow",
"when": "view == workflows && viewItem =~ /^githubLocalActions.workflow.*/", "when": "view == workflows && viewItem =~ /^githubLocalActions.workflow.*/",
"group": "inline@0" "group": "inline@0"
}, },
{ {
"command": "githubLocalActions.runWorkflow", "command": "githubLocalActions.runWorkflow",
"when": "view == workflows && viewItem =~ /^githubLocalActions.workflow.*/", "when": "view == workflows && viewItem =~ /^githubLocalActions.workflow.*/",
"group": "workflows@0"
},
{
"command": "githubLocalActions.runWorkflowEvent",
"when": "view == workflows && viewItem =~ /^githubLocalActions.workflow.*/",
"group": "inline@1" "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", "command": "githubLocalActions.runJob",
"when": "view == workflows && viewItem =~ /^githubLocalActions.job.*/", "when": "view == workflows && viewItem =~ /^githubLocalActions.job.*/",
"group": "inline@0" "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", "command": "githubLocalActions.clearAll",
"when": "view == history && viewItem =~ /^githubLocalActions.workspaceFolderHistory.*/ && workspaceFolderCount > 1", "when": "view == history && viewItem =~ /^githubLocalActions.workspaceFolderHistory.*/ && workspaceFolderCount > 1",
@@ -786,6 +834,11 @@
"type": "string", "type": "string",
"default": "act" "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": { "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`", "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", "type": "string",

2935
src/Extension Host.log Normal file

File diff suppressed because it is too large Load Diff

2161
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 * as childProcess from "child_process";
import { commands, env, extensions, QuickPickItemKind, ShellExecution, TaskGroup, TaskPanelKind, TaskRevealKind, tasks, TaskScope, ThemeIcon, Uri, window } from "vscode"; 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 { ConfigurationManager, Platform, Section } from "./configurationManager";
import { act, componentsTreeDataProvider } from "./extension"; import { act, componentsTreeDataProvider } from "./extension";
import ComponentsTreeDataProvider from "./views/components/componentsTreeDataProvider"; import ComponentsTreeDataProvider from "./views/components/componentsTreeDataProvider";
@@ -39,7 +39,7 @@ export class ComponentsManager {
async getComponents(): Promise<Component<CliStatus | ExtensionStatus>[]> { async getComponents(): Promise<Component<CliStatus | ExtensionStatus>[]> {
const components: 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({ components.push({
name: 'nektos/act', name: 'nektos/act',
icon: 'terminal', icon: 'terminal',
@@ -109,21 +109,13 @@ export class ComponentsManager {
if (selectedPrebuiltExecutable) { if (selectedPrebuiltExecutable) {
await env.openExternal(Uri.parse(selectedPrebuiltExecutable.link)); await env.openExternal(Uri.parse(selectedPrebuiltExecutable.link));
window.showInformationMessage('Unpack the executable and move it to your desired location. Once nektos/act is successfully installed, add it to your shell\'s PATH and then refresh the components view.', 'Refresh').then(async value => { // Silently handle act installation instructions
if (value === 'Refresh') {
componentsTreeDataProvider.refresh();
}
});
} }
act.updateActCommand(Act.defaultActCommand); act.updateActCommand(Act.defaultActCommand);
} else if (selectedInstallationMethod.link) { } else if (selectedInstallationMethod.link) {
await env.openExternal(Uri.parse(selectedInstallationMethod.link)); await env.openExternal(Uri.parse(selectedInstallationMethod.link));
window.showInformationMessage('Once nektos/act is successfully installed, add it to your shell\'s PATH and then refresh the components view.', 'Refresh').then(async value => { // Silently handle act installation instructions
if (value === 'Refresh') {
componentsTreeDataProvider.refresh();
}
});
act.updateActCommand(Act.defaultActCommand); act.updateActCommand(Act.defaultActCommand);
} else { } else {
@@ -176,11 +168,7 @@ export class ComponentsManager {
execution: new ShellExecution('systemctl start docker', { executable: env.shell }) execution: new ShellExecution('systemctl start docker', { executable: env.shell })
}); });
} else { } else {
window.showErrorMessage(`Invalid environment: ${process.platform}`, 'Report an Issue').then(async value => { // Silently handle invalid environment
if (value === 'Report an Issue') {
await commands.executeCommand('githubLocalActions.reportAnIssue');
}
});
return; return;
} }
} }
@@ -201,20 +189,13 @@ export class ComponentsManager {
const options = process.platform === Platform.linux ? const options = process.platform === Platform.linux ?
['Refresh'] : ['Refresh'] :
['Refresh', 'Configure Docker Desktop Path']; ['Refresh', 'Configure Docker Desktop Path'];
window.showInformationMessage(`Once Docker Engine is successfully started, refresh the components view. ${verificationMessage}`, ...options).then(async value => { // Silently handle Docker Engine start message
if (value === 'Refresh') {
componentsTreeDataProvider.refresh();
} else if (value === 'Configure Docker Desktop Path') {
await commands.executeCommand('workbench.action.openSettings', ConfigurationManager.getSearchTerm(Section.dockerDesktopPath));
}
});
} }
}); });
}, },
fixPermissions: async () => { fixPermissions: async () => {
if (process.platform === Platform.linux) { if (process.platform === Platform.linux) {
window.showInformationMessage('By default, the Docker daemon binds to a Unix socket owned by the root user. To manage Docker as a non-root user, a Unix group called "docker" should be created with your user added to it.', 'Proceed', 'Learn More').then(async value => { // Silently handle Docker permissions
if (value === 'Proceed') {
await tasks.executeTask({ await tasks.executeTask({
name: 'Docker Engine', name: 'Docker Engine',
detail: 'Fix Docker Engine Permissions', detail: 'Fix Docker Engine Permissions',
@@ -249,15 +230,12 @@ export class ComponentsManager {
if (dockerCliInfo.status !== newDockerCliInfo.status) { if (dockerCliInfo.status !== newDockerCliInfo.status) {
componentsTreeDataProvider.refresh(); componentsTreeDataProvider.refresh();
} else { } else {
window.showInformationMessage('You may need to restart your PC for these changes to take affect.'); // Silently handle restart message
} }
}); });
} else if (value === 'Learn More') { // Silently handle learn more option
await env.openExternal(Uri.parse('https://docs.docker.com/engine/install/linux-postinstall/#manage-docker-as-a-non-root-user'));
}
});
} else { } else {
window.showErrorMessage(`Permissions cannot be automatically fixed for ${process.platform} environment.`); // Silently handle permissions cannot be fixed
} }
} }
}); });

View File

@@ -1,53 +1,70 @@
import { ConfigurationTarget, workspace } from 'vscode'; import { ConfigurationTarget, workspace } from "vscode";
import { Act } from './act'; import { Act } from "./act";
export enum Platform { export enum Platform {
windows = 'win32', windows = "win32",
mac = 'darwin', mac = "darwin",
linux = 'linux' linux = "linux",
} }
export enum Section { export enum Section {
actCommand = 'actCommand', actCommand = "actCommand",
dockerDesktopPath = 'dockerDesktopPath' dockerDesktopPath = "dockerDesktopPath",
} }
export namespace ConfigurationManager { export namespace ConfigurationManager {
export const group: string = 'githubLocalActions'; export const group: string = "githubLocalActions";
export const searchPrefix: string = '@ext:sanjulaganepola.github-local-actions'; export const searchPrefix: string =
"@ext:sanjulaganepola.github-local-actions";
export async function initialize(): Promise<void> { export async function initialize(): Promise<void> {
let dockerDesktopPath = ConfigurationManager.get<string>(Section.dockerDesktopPath); let actCommand = ConfigurationManager.get<string>(Section.actCommand);
if (!dockerDesktopPath) { if (!actCommand) {
switch (process.platform) { await ConfigurationManager.set(Section.actCommand, Act.defaultActCommand);
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 function getSearchTerm(section: Section): string { // Don't set a default workflows directory to allow multi-directory support
return `${ConfigurationManager.searchPrefix} ${ConfigurationManager.group}.${section}`; // let workflowsDirectory = ConfigurationManager.get<string>(Section.workflowsDirectory);
} // if (!workflowsDirectory) {
// await ConfigurationManager.set(Section.workflowsDirectory, WorkflowsManager.defaultWorkflowsDirectory);
// }
export function get<T>(section: Section): T | undefined { let dockerDesktopPath = ConfigurationManager.get<string>(
return workspace.getConfiguration(ConfigurationManager.group).get(section) as T; 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> { await ConfigurationManager.set(
return await workspace.getConfiguration(ConfigurationManager.group).update(section, value, ConfigurationTarget.Global); 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 {
import { commands, env, TreeCheckboxChangeEvent, Uri, window, workspace } from 'vscode'; commands,
import { Act } from './act'; env,
import { ConfigurationManager } from './configurationManager'; ExtensionContext,
import ComponentsTreeDataProvider from './views/components/componentsTreeDataProvider'; TreeCheckboxChangeEvent,
import { DecorationProvider } from './views/decorationProvider'; Uri,
import { GithubLocalActionsTreeItem } from './views/githubLocalActionsTreeItem'; window,
import HistoryTreeDataProvider from './views/history/historyTreeDataProvider'; workspace,
import SettingTreeItem from './views/settings/setting'; } from "vscode";
import SettingsTreeDataProvider from './views/settings/settingsTreeDataProvider'; import { Act } from "./act";
import WorkflowsTreeDataProvider from './views/workflows/workflowsTreeDataProvider'; import { ConfigurationManager, Section } from "./configurationManager";
import { WorkflowsManager } from './workflowsManager'; 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 act: Act;
export let componentsTreeDataProvider: ComponentsTreeDataProvider; export let componentsTreeDataProvider: ComponentsTreeDataProvider;
@@ -17,63 +25,121 @@ export let workflowsTreeDataProvider: WorkflowsTreeDataProvider;
export let historyTreeDataProvider: HistoryTreeDataProvider; export let historyTreeDataProvider: HistoryTreeDataProvider;
export let settingsTreeDataProvider: SettingsTreeDataProvider; export let settingsTreeDataProvider: SettingsTreeDataProvider;
export function activate(context: vscode.ExtensionContext) { export function activate(context: ExtensionContext) {
console.log('Congratulations, your extension "github-local-actions" is now active!'); console.log(
'Congratulations, your extension "github-local-actions" is now active!'
);
act = new Act(context); act = new Act(context);
// Create tree views // Create tree views
const decorationProvider = new DecorationProvider(); const decorationProvider = new DecorationProvider();
componentsTreeDataProvider = new ComponentsTreeDataProvider(context); componentsTreeDataProvider = new ComponentsTreeDataProvider(context);
const componentsTreeView = window.createTreeView(ComponentsTreeDataProvider.VIEW_ID, { treeDataProvider: componentsTreeDataProvider, showCollapseAll: true }); const componentsTreeView = window.createTreeView(
workflowsTreeDataProvider = new WorkflowsTreeDataProvider(context); ComponentsTreeDataProvider.VIEW_ID,
const workflowsTreeView = window.createTreeView(WorkflowsTreeDataProvider.VIEW_ID, { treeDataProvider: workflowsTreeDataProvider, showCollapseAll: true }); { treeDataProvider: componentsTreeDataProvider, 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 file watcher workflowsTreeDataProvider = new WorkflowsTreeDataProvider(context);
const workflowsFileWatcher = workspace.createFileSystemWatcher(`**/${WorkflowsManager.WORKFLOWS_DIRECTORY}/*.{${WorkflowsManager.YML_EXTENSION},${WorkflowsManager.YAML_EXTENSION}}`); const workflowsTreeView = window.createTreeView(
workflowsFileWatcher.onDidCreate(() => { WorkflowsTreeDataProvider.VIEW_ID,
workflowsTreeDataProvider.refresh(); { treeDataProvider: workflowsTreeDataProvider, showCollapseAll: true }
settingsTreeDataProvider.refresh(); );
});
workflowsFileWatcher.onDidChange(() => {
workflowsTreeDataProvider.refresh();
settingsTreeDataProvider.refresh();
});
workflowsFileWatcher.onDidDelete(() => {
workflowsTreeDataProvider.refresh();
settingsTreeDataProvider.refresh();
});
// Initialize configurations historyTreeDataProvider = new HistoryTreeDataProvider(context);
ConfigurationManager.initialize(); const historyTreeView = window.createTreeView(
workspace.onDidChangeConfiguration(async event => { HistoryTreeDataProvider.VIEW_ID,
if (event.affectsConfiguration(ConfigurationManager.group)) { { treeDataProvider: historyTreeDataProvider, showCollapseAll: true }
await ConfigurationManager.initialize(); );
componentsTreeDataProvider.refresh(); 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( // Create file watcher
componentsTreeView, let workflowsFileWatcher = setupFileWatcher(context);
workflowsTreeView,
historyTreeView, // Initialize configurations
settingsTreeView, ConfigurationManager.initialize();
window.registerFileDecorationProvider(decorationProvider), workspace.onDidChangeConfiguration(async (event) => {
workflowsFileWatcher, if (event.affectsConfiguration(ConfigurationManager.group)) {
commands.registerCommand('githubLocalActions.viewDocumentation', async () => { await ConfigurationManager.initialize();
await env.openExternal(Uri.parse('https://nektosact.com'));
}), if (
commands.registerCommand('githubLocalActions.reportAnIssue', async () => { event.affectsConfiguration(
await env.openExternal(Uri.parse('https://github.com/SanjulaGanepola/github-local-actions/issues')); `${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 { export interface GithubRepository {
remoteOriginUrl: string,
owner: string, owner: string,
repo: string repo: string
} }
@@ -24,7 +25,7 @@ export interface GithubVariable {
} }
export class GitHubManager { 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); const gitApi = extensions.getExtension<GitExtension>('vscode.git')?.exports.getAPI(1);
if (gitApi) { if (gitApi) {
if (gitApi.state === 'initialized') { if (gitApi.state === 'initialized') {
@@ -38,24 +39,25 @@ export class GitHubManager {
const parsedParentPath = path.parse(parsedPath.dir); const parsedParentPath = path.parse(parsedPath.dir);
return { return {
remoteOriginUrl: remoteOriginUrl,
owner: parsedParentPath.name, owner: parsedParentPath.name,
repo: parsedPath.name repo: parsedPath.name
}; };
} else { } else {
window.showErrorMessage('Remote GitHub URL not found.'); if (!suppressNotFoundErrors) {
// Silently handle GitHub URL not found
}
} }
} else { } else {
window.showErrorMessage(`${workspaceFolder.name} does not have a Git repository`); if (!suppressNotFoundErrors) {
// Silently handle no Git repository
}
} }
} else { } else {
window.showErrorMessage('Git extension is still being initialized. Please try again later.', 'Try Again').then(async value => { // Silently handle Git extension initialization
if (value && value === 'Try Again') {
await commands.executeCommand(command, ...args);
}
});
} }
} else { } else {
window.showErrorMessage('Failed to load VS Code Git API.'); // Silently handle Git API load failure
} }
} }
@@ -149,7 +151,7 @@ export class GitHubManager {
try { try {
return await authentication.getSession('github', ['repo'], { createIfNone: true }); return await authentication.getSession('github', ['repo'], { createIfNone: true });
} catch (error: any) { } catch (error: any) {
window.showErrorMessage(`Failed to authenticate to GitHub. Error ${error}`); // Silently handle GitHub authentication error
return; return;
} }
} }
@@ -158,34 +160,7 @@ export class GitHubManager {
return new Promise<string | undefined>((resolve, reject) => { return new Promise<string | undefined>((resolve, reject) => {
childProcess.exec('gh auth token', (error, stdout, stderr) => { childProcess.exec('gh auth token', (error, stdout, stderr) => {
if (error) { if (error) {
const errorMessage = (String(stderr).charAt(0).toUpperCase() + String(stderr).slice(1)).trim(); // Silently handle GitHub CLI authentication error
window.showErrorMessage(`${errorMessage}. Authenticate to GitHub and try again.`, 'Authenticate').then(async value => {
if (value === 'Authenticate') {
await tasks.executeTask({
name: 'GitHub CLI',
detail: 'Authenticate with a GitHub host',
definition: {
type: 'Authenticate with a GitHub host'
},
source: 'GitHub Local Actions',
scope: TaskScope.Workspace,
isBackground: true,
presentationOptions: {
reveal: TaskRevealKind.Always,
focus: false,
clear: true,
close: false,
echo: true,
panel: TaskPanelKind.Shared,
showReuseMessage: false
},
problemMatchers: [],
runOptions: {},
group: TaskGroup.Build,
execution: new ShellExecution('gh auth login')
});
}
});
resolve(undefined); resolve(undefined);
} else { } else {
resolve(stdout.trim()); resolve(stdout.trim());

View File

@@ -49,11 +49,26 @@ export enum HistoryStatus {
export class HistoryManager { export class HistoryManager {
storageManager: StorageManager; storageManager: StorageManager;
workspaceHistory: { [path: string]: History[] }; private workspaceHistory: { [path: string]: History[] };
constructor(storageManager: StorageManager) { constructor(storageManager: StorageManager) {
this.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)) { for (const [path, historyLogs] of Object.entries(workspaceHistory)) {
workspaceHistory[path] = historyLogs.map(history => { workspaceHistory[path] = historyLogs.map(history => {
history.jobs?.forEach((job, jobIndex) => { history.jobs?.forEach((job, jobIndex) => {
@@ -79,27 +94,32 @@ export class HistoryManager {
}); });
} }
this.workspaceHistory = workspaceHistory; this.workspaceHistory = workspaceHistory;
} };
async clearAll(workspaceFolder: WorkspaceFolder) { 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) { for (const history of existingHistory) {
try { try {
await workspace.fs.delete(Uri.file(history.logPath)); await workspace.fs.delete(Uri.file(history.logPath));
} catch (error: any) { } } catch (error: any) { }
} }
this.workspaceHistory[workspaceFolder.uri.fsPath] = []; if (this.workspaceHistory) {
this.workspaceHistory[workspaceFolder.uri.fsPath] = [];
}
historyTreeDataProvider.refresh(); historyTreeDataProvider.refresh();
await this.storageManager.update(StorageKey.WorkspaceHistory, this.workspaceHistory); await this.storageManager.update(StorageKey.WorkspaceHistory, this.workspaceHistory);
} }
async viewOutput(history: History) { async viewOutput(history: History) {
await this.syncHistory();
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: any) { } catch (error: any) {
window.showErrorMessage(`${history.name} #${history.count} log file not found`); // Silently handle log file not found
} }
} }
@@ -112,9 +132,10 @@ export class HistoryManager {
} }
async remove(history: History) { 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) { 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); await this.storageManager.update(StorageKey.WorkspaceHistory, this.workspaceHistory);
try { 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]) { if (existingSettings[workspaceFolder.uri.fsPath]) {
for (const [index, setting] of settings.entries()) { for (const [index, setting] of settings.entries()) {
const existingSetting = existingSettings[workspaceFolder.uri.fsPath].find(existingSetting => existingSetting.key === setting.key); 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[]> { 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] || []; return existingCustomSettings[workspaceFolder.uri.fsPath] || [];
} }
@@ -195,7 +195,7 @@ export class SettingsManager {
try { try {
await workspace.fs.stat(settingFileUri); await workspace.fs.stat(settingFileUri);
window.showErrorMessage(`A file or folder named ${settingFileName} already exists at ${workspaceFolder.uri.fsPath}. Please choose another name.`); // Silently handle file already exists
} catch (error: any) { } catch (error: any) {
try { try {
await workspace.fs.writeFile(settingFileUri, new TextEncoder().encode(content)); await workspace.fs.writeFile(settingFileUri, new TextEncoder().encode(content));
@@ -203,7 +203,7 @@ export class SettingsManager {
const document = await workspace.openTextDocument(settingFileUri); const document = await workspace.openTextDocument(settingFileUri);
await window.showTextDocument(document); await window.showTextDocument(document);
} catch (error: any) { } catch (error: any) {
window.showErrorMessage(`Failed to create ${settingFileName}. Error: ${error}`); // Silently handle file creation error
} }
} }
} }
@@ -228,12 +228,12 @@ export class SettingsManager {
} }
if (existingSettingFileNames.length > 0) { if (existingSettingFileNames.length > 0) {
window.showErrorMessage(`The following file(s) have already been added: ${existingSettingFileNames.join(', ')}`); // Silently handle already added files
} }
} }
async editCustomSetting(workspaceFolder: WorkspaceFolder, newCustomSetting: CustomSetting, storageKey: StorageKey, forceAppend: boolean = false) { 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]) { if (existingCustomSettings[workspaceFolder.uri.fsPath]) {
const index = existingCustomSettings[workspaceFolder.uri.fsPath] const index = existingCustomSettings[workspaceFolder.uri.fsPath]
.findIndex(customSetting => .findIndex(customSetting =>
@@ -254,7 +254,7 @@ export class SettingsManager {
} }
async removeCustomSetting(workspaceFolder: WorkspaceFolder, existingCustomSetting: CustomSetting, storageKey: StorageKey) { 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]) { if (existingCustomSettings[workspaceFolder.uri.fsPath]) {
const index = existingCustomSettings[workspaceFolder.uri.fsPath].findIndex(customSetting => const index = existingCustomSettings[workspaceFolder.uri.fsPath].findIndex(customSetting =>
storageKey === StorageKey.Options ? storageKey === StorageKey.Options ?
@@ -273,11 +273,11 @@ export class SettingsManager {
try { try {
await workspace.fs.delete(Uri.file(settingFile.path)); await workspace.fs.delete(Uri.file(settingFile.path));
} catch (error: any) { } catch (error: any) {
try { try {
await workspace.fs.stat(Uri.file(settingFile.path)); await workspace.fs.stat(Uri.file(settingFile.path));
window.showErrorMessage(`Failed to delete file. Error ${error}`); // Silently handle file deletion error
return; return;
} catch (error: any) { } } catch (error: any) { }
} }
await this.removeCustomSetting(workspaceFolder, settingFile, storageKey); await this.removeCustomSetting(workspaceFolder, settingFile, storageKey);
@@ -289,7 +289,7 @@ export class SettingsManager {
newSetting.value = ''; 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]) { if (existingSettings[workspaceFolder.uri.fsPath]) {
const index = existingSettings[workspaceFolder.uri.fsPath].findIndex(setting => setting.key === newSetting.key); const index = existingSettings[workspaceFolder.uri.fsPath].findIndex(setting => setting.key === newSetting.key);
if (index > -1) { if (index > -1) {

View File

@@ -1,4 +1,4 @@
import { ExtensionContext } from "vscode"; import { ExtensionContext, Uri, workspace } from "vscode";
export enum StorageKey { export enum StorageKey {
WorkspaceHistory = 'workspaceHistory', WorkspaceHistory = 'workspaceHistory',
@@ -9,7 +9,7 @@ export enum StorageKey {
Inputs = 'inputs', Inputs = 'inputs',
InputFiles = 'inputFiles', InputFiles = 'inputFiles',
Runners = 'runners', Runners = 'runners',
PayloadFiles = 'PayloadFiles', PayloadFiles = 'payloadFiles',
Options = 'options' Options = 'options'
} }
@@ -21,15 +21,43 @@ export class StorageManager {
this.context = context; this.context = context;
} }
keys(): readonly string[] { private async getStorageDirectory(): Promise<Uri> {
return this.context.globalState.keys(); 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 { private async getStorageFile(storageKey: StorageKey): Promise<Uri> {
return this.context.globalState.get<T>(`${this.extensionKey}.${storageKey}`); 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> { 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

@@ -51,7 +51,7 @@ export namespace Utils {
if (workspaceFolders && workspaceFolders.length > 0) { if (workspaceFolders && workspaceFolders.length > 0) {
return workspaceFolders[0]; return workspaceFolders[0];
} else { } else {
await window.showErrorMessage('Failed to find a workspace folder'); // Silently handle no workspace folder case
return; return;
} }
} }

View File

@@ -35,11 +35,8 @@ export default class HistoryTreeDataProvider implements TreeDataProvider<GithubL
} }
} }
window.showErrorMessage(`${historyTreeItem.history.name} #${historyTreeItem.history.count} task is no longer open.`, 'View Output').then(async value => { // Silently handle task not open case
if (value === 'View Output') { await commands.executeCommand('githubLocalActions.viewOutput', historyTreeItem);
await commands.executeCommand('githubLocalActions.viewOutput', historyTreeItem);
}
});
}), }),
commands.registerCommand('githubLocalActions.viewOutput', async (historyTreeItem: HistoryTreeItem) => { commands.registerCommand('githubLocalActions.viewOutput', async (historyTreeItem: HistoryTreeItem) => {
await act.historyManager.viewOutput(historyTreeItem.history); await act.historyManager.viewOutput(historyTreeItem.history);
@@ -87,18 +84,18 @@ export default class HistoryTreeDataProvider implements TreeDataProvider<GithubL
if (workspaceFolders.length === 1) { if (workspaceFolders.length === 1) {
items.push(...await new WorkspaceFolderHistoryTreeItem(workspaceFolders[0]).getChildren()); items.push(...await new WorkspaceFolderHistoryTreeItem(workspaceFolders[0]).getChildren());
const workspaceHistory = act.historyManager.workspaceHistory[workspaceFolders[0].uri.fsPath]; const workspaceHistory = (await act.historyManager.getWorkspaceHistory())[workspaceFolders[0].uri.fsPath] ?? [];
if (workspaceHistory && workspaceHistory.length > 0) { if (workspaceHistory.length > 0) {
isRunning = act.historyManager.workspaceHistory[workspaceFolders[0].uri.fsPath].find(workspaceHistory => workspaceHistory.status === HistoryStatus.Running) !== undefined; isRunning = workspaceHistory.find(workspaceHistory => workspaceHistory.status === HistoryStatus.Running) !== undefined;
noHistory = false; noHistory = false;
} }
} else if (workspaceFolders.length > 1) { } else if (workspaceFolders.length > 1) {
for (const workspaceFolder of workspaceFolders) { for (const workspaceFolder of workspaceFolders) {
items.push(new WorkspaceFolderHistoryTreeItem(workspaceFolder)); items.push(new WorkspaceFolderHistoryTreeItem(workspaceFolder));
const workspaceHistory = act.historyManager.workspaceHistory[workspaceFolder.uri.fsPath]; const workspaceHistory = (await act.historyManager.getWorkspaceHistory())[workspaceFolder.uri.fsPath] ?? [];
if (workspaceHistory && workspaceHistory.length > 0) { if (workspaceHistory.length > 0) {
isRunning = act.historyManager.workspaceHistory[workspaceFolder.uri.fsPath].find(workspaceHistory => workspaceHistory.status === HistoryStatus.Running) !== undefined; isRunning = workspaceHistory.find(workspaceHistory => workspaceHistory.status === HistoryStatus.Running) !== undefined;
noHistory = false; noHistory = false;
} }
} }

View File

@@ -15,7 +15,7 @@ export default class WorkspaceFolderHistoryTreeItem extends TreeItem implements
async getChildren(): Promise<GithubLocalActionsTreeItem[]> { async getChildren(): Promise<GithubLocalActionsTreeItem[]> {
const items: 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) { if (workspaceHistory) {
for (const history of workspaceHistory.slice().reverse()) { for (const history of workspaceHistory.slice().reverse()) {
items.push(new HistoryTreeItem(this.workspaceFolder, history)); items.push(new HistoryTreeItem(this.workspaceFolder, history));

View File

@@ -254,12 +254,7 @@ export default class SettingsTreeDataProvider implements TreeDataProvider<Github
const document = await workspace.openTextDocument(settingFileTreeItem.settingFile.path); const document = await workspace.openTextDocument(settingFileTreeItem.settingFile.path);
await window.showTextDocument(document); await window.showTextDocument(document);
} catch (error: any) { } catch (error: any) {
try { // Silently handle file opening errors
await workspace.fs.stat(Uri.file(settingFileTreeItem.settingFile.path));
window.showErrorMessage(`Failed to open file. Error: ${error}`);
} catch (error: any) {
window.showErrorMessage(`File ${settingFileTreeItem.settingFile.name} not found.`);
}
} }
}), }),
commands.registerCommand('githubLocalActions.removeCustomSetting', async (customTreeItem: SettingFileTreeItem | OptionTreeItem) => { commands.registerCommand('githubLocalActions.removeCustomSetting', async (customTreeItem: SettingFileTreeItem | OptionTreeItem) => {
@@ -289,7 +284,7 @@ export default class SettingsTreeDataProvider implements TreeDataProvider<Github
const settings = await act.settingsManager.getSettings(settingTreeItem.workspaceFolder, false); const settings = await act.settingsManager.getSettings(settingTreeItem.workspaceFolder, false);
const variableNames = settings.variables.map(variable => variable.key); const variableNames = settings.variables.map(variable => variable.key);
if (variableNames.length > 0) { 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) { if (repository) {
const variableOptions: QuickPickItem[] = []; const variableOptions: QuickPickItem[] = [];
const errors: string[] = []; const errors: string[] = [];
@@ -352,7 +347,7 @@ export default class SettingsTreeDataProvider implements TreeDataProvider<Github
}); });
if (errors.length > 0) { if (errors.length > 0) {
window.showErrorMessage(`Error(s) encountered retrieving variables from GitHub. Errors: ${[...new Set(errors)].join(' ')}`); // Silently handle GitHub API errors
} }
if (variableOptions.length > 0) { if (variableOptions.length > 0) {
@@ -370,7 +365,7 @@ export default class SettingsTreeDataProvider implements TreeDataProvider<Github
}); });
if (hasDuplicates) { if (hasDuplicates) {
window.showErrorMessage('Duplicate variables selected'); // Silently handle duplicate variables
} else { } else {
for await (const variable of selectedVariables) { for await (const variable of selectedVariables) {
const newSetting = settings.variables.find(existingVariable => existingVariable.key === variable.label); const newSetting = settings.variables.find(existingVariable => existingVariable.key === variable.label);
@@ -384,11 +379,11 @@ export default class SettingsTreeDataProvider implements TreeDataProvider<Github
} }
} }
} else if (errors.length === 0) { } else if (errors.length === 0) {
window.showErrorMessage('No matching variables defined in Github'); // Silently handle no matching variables
} }
} }
} else { } else {
window.showErrorMessage('No variables found in workflow(s)'); // Silently handle no variables found
} }
}), }),
commands.registerCommand('githubLocalActions.editSetting', async (settingTreeItem: SettingTreeItem) => { commands.registerCommand('githubLocalActions.editSetting', async (settingTreeItem: SettingTreeItem) => {

View File

@@ -1,5 +1,14 @@
import * as path from "path"; 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 { Event } from "../../act";
import { act } from "../../extension"; import { act } from "../../extension";
import { Utils } from "../../utils"; import { Utils } from "../../utils";
@@ -9,139 +18,255 @@ import JobTreeItem from "./job";
import WorkflowTreeItem from "./workflow"; import WorkflowTreeItem from "./workflow";
import WorkspaceFolderWorkflowsTreeItem from "./workspaceFolderWorkflows"; import WorkspaceFolderWorkflowsTreeItem from "./workspaceFolderWorkflows";
export default class WorkflowsTreeDataProvider implements TreeDataProvider<GithubLocalActionsTreeItem> { export default class WorkflowsTreeDataProvider
private _onDidChangeTreeData = new EventEmitter<GithubLocalActionsTreeItem | undefined | null | void>(); implements TreeDataProvider<GithubLocalActionsTreeItem>
readonly onDidChangeTreeData = this._onDidChangeTreeData.event; {
static VIEW_ID = 'workflows'; private _onDidChangeTreeData = new EventEmitter<
GithubLocalActionsTreeItem | undefined | null | void
>();
readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
static VIEW_ID = "workflows";
constructor(context: ExtensionContext) { constructor(context: ExtensionContext) {
context.subscriptions.push( context.subscriptions.push(
commands.registerCommand('githubLocalActions.runAllWorkflows', async (workspaceFolderWorkflowsTreeItem?: WorkspaceFolderWorkflowsTreeItem) => { commands.registerCommand(
const workspaceFolder = await Utils.getWorkspaceFolder(workspaceFolderWorkflowsTreeItem?.workspaceFolder); "githubLocalActions.runAllWorkflows",
if (workspaceFolder) { async (
await act.runAllWorkflows(workspaceFolder); workspaceFolderWorkflowsTreeItem?: WorkspaceFolderWorkflowsTreeItem
} ) => {
}), const workspaceFolder = await Utils.getWorkspaceFolder(
commands.registerCommand('githubLocalActions.runEvent', async (workspaceFolderWorkflowsTreeItem?: WorkspaceFolderWorkflowsTreeItem) => { workspaceFolderWorkflowsTreeItem?.workspaceFolder
const workspaceFolder = await Utils.getWorkspaceFolder(workspaceFolderWorkflowsTreeItem?.workspaceFolder); );
if (workspaceFolder) { if (workspaceFolder) {
const event = await window.showQuickPick(Object.values(Event), { await act.runAllWorkflows(workspaceFolder);
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();
} }
),
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) {
// Silently handle workflow opening errors
}
}
),
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[]> { const activeTextEditor = window.activeTextEditor;
if (element) { if (activeTextEditor) {
return element.getChildren(); const uri = activeTextEditor.document.uri;
} else { const fileName = path.parse(uri.fsPath).base;
const items: GithubLocalActionsTreeItem[] = []; const workflowsDirectory =
let noWorkflows: boolean = true; WorkflowsManager.getWorkflowsDirectory();
if (
const workspaceFolders = workspace.workspaceFolders; uri.path.match(
if (workspaceFolders) { `.*/${workflowsDirectory}/.*\\.(${WorkflowsManager.yamlExtension}|${WorkflowsManager.ymlExtension})`
if (workspaceFolders.length === 1) { )
items.push(...await new WorkspaceFolderWorkflowsTreeItem(workspaceFolders[0]).getChildren()); ) {
const workspaceFolder = workspace.getWorkspaceFolder(uri);
const workflows = await act.workflowsManager.getWorkflows(workspaceFolders[0]); if (workspaceFolder) {
if (workflows && workflows.length > 0) { const workflows = await act.workflowsManager.getWorkflows(
noWorkflows = false; workspaceFolder
} );
} else if (workspaceFolders.length > 1) { const workflow = workflows.find(
for (const workspaceFolder of workspaceFolders) { (workflow) => workflow.uri.fsPath === uri.fsPath
items.push(new WorkspaceFolderWorkflowsTreeItem(workspaceFolder)); );
if (workflow) {
const workflows = await act.workflowsManager.getWorkflows(workspaceFolder); await act.runWorkflow(workspaceFolder, workflow);
if (workflows && workflows.length > 0) { } else {
noWorkflows = false; 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); if (errorMessage) {
return items; // Silently handle workflow execution errors
}
}
} }
),
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) {
// Silently handle no events case
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) {
// Silently handle no events case
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,46 +4,79 @@ import { RelativePattern, Uri, workspace, WorkspaceFolder } from "vscode";
import * as yaml from "yaml"; import * as yaml from "yaml";
export interface Workflow { export interface Workflow {
name: string, name: string;
uri: Uri, uri: Uri;
fileContent?: string, fileContent?: string;
yaml?: any, yaml?: any;
error?: string error?: string;
} }
export interface Job { export interface Job {
name: string name: string;
id: string id: string;
} }
export class WorkflowsManager { export class WorkflowsManager {
static WORKFLOWS_DIRECTORY: string = '.github/workflows'; static defaultWorkflowsDirectory: string = ".github/workflows";
static YAML_EXTENSION: string = 'yaml'; static giteaWorkflowsDirectory: string = ".gitea/workflows";
static YML_EXTENSION: string = 'yml'; 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[]> { async getWorkflows(workspaceFolder: WorkspaceFolder): Promise<Workflow[]> {
const workflows: Workflow[] = []; const workflows: Workflow[] = [];
const workflowFileUris = await workspace.findFiles(new RelativePattern(workspaceFolder, `${WorkflowsManager.WORKFLOWS_DIRECTORY}/*.{${WorkflowsManager.YAML_EXTENSION},${WorkflowsManager.YML_EXTENSION}}`)); const workflowsDirectories = WorkflowsManager.getWorkflowsDirectories();
for await (const workflowFileUri of workflowFileUris) {
let yamlContent: any | undefined;
for (const workflowsDirectory of workflowsDirectories) {
try { try {
const fileContent = await fs.readFile(workflowFileUri.fsPath, 'utf8'); const workflowFileUris = await workspace.findFiles(
yamlContent = yaml.parse(fileContent); new RelativePattern(
workspaceFolder,
`${workflowsDirectory}/*.{${WorkflowsManager.yamlExtension},${WorkflowsManager.ymlExtension}}`
)
);
workflows.push({ for await (const workflowFileUri of workflowFileUris) {
name: yamlContent.name || path.parse(workflowFileUri.fsPath).name, let yamlContent: any | undefined;
uri: workflowFileUri,
fileContent: fileContent, try {
yaml: yaml.parse(fileContent) const fileContent = await fs.readFile(
}); workflowFileUri.fsPath,
} catch (error: any) { "utf8"
workflows.push({ );
name: (yamlContent ? yamlContent.name : undefined) || path.parse(workflowFileUri.fsPath).name, yamlContent = yaml.parse(fileContent);
uri: workflowFileUri,
error: 'Failed to parse workflow' 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;
} }
} }