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: 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

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',

View File

@@ -1,41 +1,54 @@
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 (!actCommand) {
await ConfigurationManager.set(Section.actCommand, Act.defaultActCommand);
}
// 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);
// }
let dockerDesktopPath = ConfigurationManager.get<string>(
Section.dockerDesktopPath
);
if (!dockerDesktopPath) { if (!dockerDesktopPath) {
switch (process.platform) { switch (process.platform) {
case Platform.windows: case Platform.windows:
dockerDesktopPath = 'C:/Program Files/Docker/Docker/Docker Desktop.exe'; dockerDesktopPath =
"C:/Program Files/Docker/Docker/Docker Desktop.exe";
break; break;
case Platform.mac: case Platform.mac:
dockerDesktopPath = '/Applications/Docker.app'; dockerDesktopPath = "/Applications/Docker.app";
break; break;
default: default:
return; return;
} }
await ConfigurationManager.set(Section.dockerDesktopPath, dockerDesktopPath); await ConfigurationManager.set(
} Section.dockerDesktopPath,
dockerDesktopPath
let actCommand = ConfigurationManager.get<string>(Section.actCommand); );
if (!actCommand) {
await ConfigurationManager.set(Section.actCommand, Act.defaultActCommand);
} }
} }
@@ -44,10 +57,14 @@ export namespace ConfigurationManager {
} }
export function get<T>(section: Section): T | undefined { export function get<T>(section: Section): T | undefined {
return workspace.getConfiguration(ConfigurationManager.group).get(section) as T; return workspace
.getConfiguration(ConfigurationManager.group)
.get(section) as T;
} }
export async function set(section: Section, value: any): Promise<void> { export async function set(section: Section, value: any): Promise<void> {
return await workspace.getConfiguration(ConfigurationManager.group).update(section, value, ConfigurationTarget.Global); 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,27 +25,99 @@ 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(
ComponentsTreeDataProvider.VIEW_ID,
{ treeDataProvider: componentsTreeDataProvider, showCollapseAll: true }
);
workflowsTreeDataProvider = new WorkflowsTreeDataProvider(context); workflowsTreeDataProvider = new WorkflowsTreeDataProvider(context);
const workflowsTreeView = window.createTreeView(WorkflowsTreeDataProvider.VIEW_ID, { treeDataProvider: workflowsTreeDataProvider, showCollapseAll: true }); const workflowsTreeView = window.createTreeView(
WorkflowsTreeDataProvider.VIEW_ID,
{ treeDataProvider: workflowsTreeDataProvider, showCollapseAll: true }
);
historyTreeDataProvider = new HistoryTreeDataProvider(context); historyTreeDataProvider = new HistoryTreeDataProvider(context);
const historyTreeView = window.createTreeView(HistoryTreeDataProvider.VIEW_ID, { treeDataProvider: historyTreeDataProvider, showCollapseAll: true }); const historyTreeView = window.createTreeView(
HistoryTreeDataProvider.VIEW_ID,
{ treeDataProvider: historyTreeDataProvider, showCollapseAll: true }
);
settingsTreeDataProvider = new SettingsTreeDataProvider(context); settingsTreeDataProvider = new SettingsTreeDataProvider(context);
const settingsTreeView = window.createTreeView(SettingsTreeDataProvider.VIEW_ID, { treeDataProvider: settingsTreeDataProvider, showCollapseAll: true }); const settingsTreeView = window.createTreeView(
settingsTreeView.onDidChangeCheckboxState(async (event: TreeCheckboxChangeEvent<GithubLocalActionsTreeItem>) => { SettingsTreeDataProvider.VIEW_ID,
await settingsTreeDataProvider.onDidChangeCheckboxState(event as TreeCheckboxChangeEvent<SettingTreeItem>); { treeDataProvider: settingsTreeDataProvider, showCollapseAll: true }
}); );
settingsTreeView.onDidChangeCheckboxState(
async (event: TreeCheckboxChangeEvent<GithubLocalActionsTreeItem>) => {
await settingsTreeDataProvider.onDidChangeCheckboxState(
event as TreeCheckboxChangeEvent<SettingTreeItem>
);
}
);
// Create file watcher // Create file watcher
const workflowsFileWatcher = workspace.createFileSystemWatcher(`**/${WorkflowsManager.WORKFLOWS_DIRECTORY}/*.{${WorkflowsManager.YML_EXTENSION},${WorkflowsManager.YAML_EXTENSION}}`); 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);
})
);
}
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(() => { workflowsFileWatcher.onDidCreate(() => {
workflowsTreeDataProvider.refresh(); workflowsTreeDataProvider.refresh();
settingsTreeDataProvider.refresh(); settingsTreeDataProvider.refresh();
@@ -51,29 +131,15 @@ export function activate(context: vscode.ExtensionContext) {
settingsTreeDataProvider.refresh(); settingsTreeDataProvider.refresh();
}); });
// Initialize configurations fileWatchers.push(workflowsFileWatcher);
ConfigurationManager.initialize();
workspace.onDidChangeConfiguration(async event => {
if (event.affectsConfiguration(ConfigurationManager.group)) {
await ConfigurationManager.initialize();
componentsTreeDataProvider.refresh();
} }
});
context.subscriptions.push( // Return a disposable that disposes all watchers
componentsTreeView, return {
workflowsTreeView, dispose: () => {
historyTreeView, fileWatchers.forEach((watcher) => watcher.dispose());
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'));
}),
);
} }
export function deactivate() { } 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,19 +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 {
if (!suppressNotFoundErrors) {
window.showErrorMessage('Remote GitHub URL not found.'); window.showErrorMessage('Remote GitHub URL not found.');
} }
} else {
window.showErrorMessage(`${workspaceFolder.name} does not have a Git repository`);
} }
} else { } else {
window.showErrorMessage('Git extension is still being initialized. Please try again later.', 'Try Again').then(async value => { if (!suppressNotFoundErrors) {
if (value && value === 'Try Again') { window.showErrorMessage(`${workspaceFolder.name} does not have a Git repository`);
await commands.executeCommand(command, ...args); }
}
} else {
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 { 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,22 +94,27 @@ 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) { }
} }
if (this.workspaceHistory) {
this.workspaceHistory[workspaceFolder.uri.fsPath] = []; 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);
@@ -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] || [];
} }
@@ -233,7 +233,7 @@ export class SettingsManager {
} }
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 ?
@@ -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> {
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}`); 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> {
if ([StorageKey.Secrets, StorageKey.SecretFiles].includes(storageKey)) {
await this.context.globalState.update(`${this.extensionKey}.${storageKey}`, value); 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) { 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

@@ -289,7 +289,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[] = [];

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,51 +18,88 @@ 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>
{
private _onDidChangeTreeData = new EventEmitter<
GithubLocalActionsTreeItem | undefined | null | void
>();
readonly onDidChangeTreeData = this._onDidChangeTreeData.event; readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
static VIEW_ID = 'workflows'; 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",
async (
workspaceFolderWorkflowsTreeItem?: WorkspaceFolderWorkflowsTreeItem
) => {
const workspaceFolder = await Utils.getWorkspaceFolder(
workspaceFolderWorkflowsTreeItem?.workspaceFolder
);
if (workspaceFolder) { if (workspaceFolder) {
await act.runAllWorkflows(workspaceFolder); await act.runAllWorkflows(workspaceFolder);
} }
}), }
commands.registerCommand('githubLocalActions.runEvent', async (workspaceFolderWorkflowsTreeItem?: WorkspaceFolderWorkflowsTreeItem) => { ),
const workspaceFolder = await Utils.getWorkspaceFolder(workspaceFolderWorkflowsTreeItem?.workspaceFolder); commands.registerCommand(
"githubLocalActions.runEvent",
async (
workspaceFolderWorkflowsTreeItem?: WorkspaceFolderWorkflowsTreeItem
) => {
const workspaceFolder = await Utils.getWorkspaceFolder(
workspaceFolderWorkflowsTreeItem?.workspaceFolder
);
if (workspaceFolder) { if (workspaceFolder) {
const event = await window.showQuickPick(Object.values(Event), { const event = await window.showQuickPick(Object.values(Event), {
title: 'Select the event to run', title: "Select the event to run",
placeHolder: 'Event' placeHolder: "Event",
}); });
if (event) { if (event) {
await act.runEvent(workspaceFolder, event as Event); await act.runEvent(workspaceFolder, event as Event);
} }
} }
}), }
commands.registerCommand('githubLocalActions.refreshWorkflows', async () => { ),
commands.registerCommand(
"githubLocalActions.refreshWorkflows",
async () => {
this.refresh(); this.refresh();
}), }
commands.registerCommand('githubLocalActions.openWorkflow', async (workflowTreeItem: WorkflowTreeItem) => { ),
commands.registerCommand(
"githubLocalActions.openWorkflow",
async (workflowTreeItem: WorkflowTreeItem) => {
try { try {
const document = await workspace.openTextDocument(workflowTreeItem.workflow.uri); const document = await workspace.openTextDocument(
workflowTreeItem.workflow.uri
);
await window.showTextDocument(document); await window.showTextDocument(document);
} catch (error: any) { } catch (error: any) {
try { try {
await workspace.fs.stat(workflowTreeItem.workflow.uri); await workspace.fs.stat(workflowTreeItem.workflow.uri);
window.showErrorMessage(`Failed to open workflow. Error: ${error}`); window.showErrorMessage(
`Failed to open workflow. Error: ${error}`
);
} catch (error: any) { } catch (error: any) {
window.showErrorMessage(`Workflow ${path.parse(workflowTreeItem.workflow.uri.fsPath).base} not found.`); window.showErrorMessage(
`Workflow ${
path.parse(workflowTreeItem.workflow.uri.fsPath).base
} not found.`
);
} }
} }
}), }
commands.registerCommand('githubLocalActions.runWorkflow', async (workflowTreeItem: WorkflowTreeItem) => { ),
commands.registerCommand(
"githubLocalActions.runWorkflow",
async (workflowTreeItem: WorkflowTreeItem) => {
if (workflowTreeItem) { if (workflowTreeItem) {
await act.runWorkflow(workflowTreeItem.workspaceFolder, workflowTreeItem.workflow); await act.runWorkflow(
workflowTreeItem.workspaceFolder,
workflowTreeItem.workflow
);
} else { } else {
let errorMessage: string | undefined; let errorMessage: string | undefined;
@@ -61,15 +107,25 @@ export default class WorkflowsTreeDataProvider implements TreeDataProvider<Githu
if (activeTextEditor) { if (activeTextEditor) {
const uri = activeTextEditor.document.uri; const uri = activeTextEditor.document.uri;
const fileName = path.parse(uri.fsPath).base; const fileName = path.parse(uri.fsPath).base;
if (uri.path.match(`.*/${WorkflowsManager.WORKFLOWS_DIRECTORY}/.*\\.(${WorkflowsManager.YAML_EXTENSION}|${WorkflowsManager.YML_EXTENSION})`)) { const workflowsDirectory =
WorkflowsManager.getWorkflowsDirectory();
if (
uri.path.match(
`.*/${workflowsDirectory}/.*\\.(${WorkflowsManager.yamlExtension}|${WorkflowsManager.ymlExtension})`
)
) {
const workspaceFolder = workspace.getWorkspaceFolder(uri); const workspaceFolder = workspace.getWorkspaceFolder(uri);
if (workspaceFolder) { if (workspaceFolder) {
const workflows = await act.workflowsManager.getWorkflows(workspaceFolder); const workflows = await act.workflowsManager.getWorkflows(
const workflow = workflows.find(workflow => workflow.uri.fsPath === uri.fsPath); workspaceFolder
);
const workflow = workflows.find(
(workflow) => workflow.uri.fsPath === uri.fsPath
);
if (workflow) { if (workflow) {
await act.runWorkflow(workspaceFolder, workflow); await act.runWorkflow(workspaceFolder, workflow);
} else { } else {
errorMessage = `Workflow not found in workflow directory (${WorkflowsManager.WORKFLOWS_DIRECTORY}).`; errorMessage = `Workflow not found in workflow directory (${workflowsDirectory}).`;
} }
} else { } else {
errorMessage = `${fileName} must be opened in a workspace folder to be executed locally.`; errorMessage = `${fileName} must be opened in a workspace folder to be executed locally.`;
@@ -78,21 +134,91 @@ export default class WorkflowsTreeDataProvider implements TreeDataProvider<Githu
errorMessage = `${fileName} is not a workflow that can be executed locally.`; errorMessage = `${fileName} is not a workflow that can be executed locally.`;
} }
} else { } else {
errorMessage = 'No workflow opened to execute locally.'; errorMessage = "No workflow opened to execute locally.";
} }
if (errorMessage) { if (errorMessage) {
window.showErrorMessage(errorMessage, 'View Workflows').then(async value => { window
if (value === 'View Workflows') { .showErrorMessage(errorMessage, "View Workflows")
await commands.executeCommand('workflows.focus'); .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.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,
});
}
}
)
); );
} }
@@ -100,11 +226,17 @@ export default class WorkflowsTreeDataProvider implements TreeDataProvider<Githu
this._onDidChangeTreeData.fire(element); this._onDidChangeTreeData.fire(element);
} }
getTreeItem(element: GithubLocalActionsTreeItem): GithubLocalActionsTreeItem | Thenable<GithubLocalActionsTreeItem> { getTreeItem(
element: GithubLocalActionsTreeItem
): GithubLocalActionsTreeItem | Thenable<GithubLocalActionsTreeItem> {
return element; return element;
} }
async resolveTreeItem(item: TreeItem, element: GithubLocalActionsTreeItem, token: CancellationToken): Promise<GithubLocalActionsTreeItem> { async resolveTreeItem(
item: TreeItem,
element: GithubLocalActionsTreeItem,
token: CancellationToken
): Promise<GithubLocalActionsTreeItem> {
if (element.getToolTip) { if (element.getToolTip) {
element.tooltip = await element.getToolTip(); element.tooltip = await element.getToolTip();
} }
@@ -112,7 +244,9 @@ export default class WorkflowsTreeDataProvider implements TreeDataProvider<Githu
return element; return element;
} }
async getChildren(element?: GithubLocalActionsTreeItem): Promise<GithubLocalActionsTreeItem[]> { async getChildren(
element?: GithubLocalActionsTreeItem
): Promise<GithubLocalActionsTreeItem[]> {
if (element) { if (element) {
return element.getChildren(); return element.getChildren();
} else { } else {
@@ -122,9 +256,15 @@ export default class WorkflowsTreeDataProvider implements TreeDataProvider<Githu
const workspaceFolders = workspace.workspaceFolders; const workspaceFolders = workspace.workspaceFolders;
if (workspaceFolders) { if (workspaceFolders) {
if (workspaceFolders.length === 1) { if (workspaceFolders.length === 1) {
items.push(...await new WorkspaceFolderWorkflowsTreeItem(workspaceFolders[0]).getChildren()); items.push(
...(await new WorkspaceFolderWorkflowsTreeItem(
workspaceFolders[0]
).getChildren())
);
const workflows = await act.workflowsManager.getWorkflows(workspaceFolders[0]); const workflows = await act.workflowsManager.getWorkflows(
workspaceFolders[0]
);
if (workflows && workflows.length > 0) { if (workflows && workflows.length > 0) {
noWorkflows = false; noWorkflows = false;
} }
@@ -132,7 +272,9 @@ export default class WorkflowsTreeDataProvider implements TreeDataProvider<Githu
for (const workspaceFolder of workspaceFolders) { for (const workspaceFolder of workspaceFolders) {
items.push(new WorkspaceFolderWorkflowsTreeItem(workspaceFolder)); items.push(new WorkspaceFolderWorkflowsTreeItem(workspaceFolder));
const workflows = await act.workflowsManager.getWorkflows(workspaceFolder); const workflows = await act.workflowsManager.getWorkflows(
workspaceFolder
);
if (workflows && workflows.length > 0) { if (workflows && workflows.length > 0) {
noWorkflows = false; noWorkflows = false;
} }
@@ -140,7 +282,11 @@ export default class WorkflowsTreeDataProvider implements TreeDataProvider<Githu
} }
} }
await commands.executeCommand('setContext', 'githubLocalActions:noWorkflows', noWorkflows); await commands.executeCommand(
"setContext",
"githubLocalActions:noWorkflows",
noWorkflows
);
return items; return items;
} }
} }

View File

@@ -4,48 +4,81 @@ 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 (const workflowsDirectory of workflowsDirectories) {
try {
const workflowFileUris = await workspace.findFiles(
new RelativePattern(
workspaceFolder,
`${workflowsDirectory}/*.{${WorkflowsManager.yamlExtension},${WorkflowsManager.ymlExtension}}`
)
);
for await (const workflowFileUri of workflowFileUris) { for await (const workflowFileUri of workflowFileUris) {
let yamlContent: any | undefined; let yamlContent: any | undefined;
try { try {
const fileContent = await fs.readFile(workflowFileUri.fsPath, 'utf8'); const fileContent = await fs.readFile(
workflowFileUri.fsPath,
"utf8"
);
yamlContent = yaml.parse(fileContent); yamlContent = yaml.parse(fileContent);
workflows.push({ workflows.push({
name: yamlContent.name || path.parse(workflowFileUri.fsPath).name, name: yamlContent.name || path.parse(workflowFileUri.fsPath).name,
uri: workflowFileUri, uri: workflowFileUri,
fileContent: fileContent, fileContent: fileContent,
yaml: yaml.parse(fileContent) yaml: yaml.parse(fileContent),
}); });
} catch (error: any) { } catch (error: any) {
workflows.push({ workflows.push({
name: (yamlContent ? yamlContent.name : undefined) || path.parse(workflowFileUri.fsPath).name, name:
(yamlContent ? yamlContent.name : undefined) ||
path.parse(workflowFileUri.fsPath).name,
uri: workflowFileUri, uri: workflowFileUri,
error: 'Failed to parse workflow' error: "Failed to parse workflow",
}); });
} }
} }
} catch (error) {
// Directory doesn't exist, skip it
continue;
}
}
return workflows; return workflows;
} }