Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 71 additions & 3 deletions src/commands/triggerWorkflowRun.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,34 @@
import {basename} from "path";
import * as vscode from "vscode";

import {getGitHead, getGitHubContextForWorkspaceUri, GitHubRepoContext} from "../git/repository";
import {getWorkflowUri, parseWorkflowFile} from "../workflow/workflow";

import {Workflow} from "../model";
import {RunStore} from "../store/store";

import {log} from "../log";

interface TriggerRunCommandOptions {
wf?: Workflow;
gitHubRepoContext: GitHubRepoContext;
}

export function registerTriggerWorkflowRun(context: vscode.ExtensionContext) {
export function registerTriggerWorkflowRun(context: vscode.ExtensionContext, store: RunStore) {
context.subscriptions.push(
vscode.commands.registerCommand(
"github-actions.explorer.triggerRun",
async (args: TriggerRunCommandOptions | vscode.Uri) => {
let workflowUri: vscode.Uri | null = null;
let workflowIdForApi: number | string | undefined;

if (args instanceof vscode.Uri) {
workflowUri = args;
workflowIdForApi = basename(workflowUri.fsPath);
} else if (args.wf) {
const wf: Workflow = args.wf;
workflowUri = getWorkflowUri(args.gitHubRepoContext, wf.path);
workflowIdForApi = wf.id;
}

if (!workflowUri) {
Expand All @@ -43,6 +51,28 @@ export function registerTriggerWorkflowRun(context: vscode.ExtensionContext) {
return;
}

const relativeWorkflowPath = vscode.workspace.asRelativePath(workflowUri, false);
if (!workflowIdForApi) {
workflowIdForApi = basename(workflowUri.fsPath);
}

let latestRunId: number | undefined;
try {
log(`Fetching latest run for workflow: ${workflowIdForApi}`);
const result = await gitHubRepoContext.client.actions.listWorkflowRuns({
owner: gitHubRepoContext.owner,
repo: gitHubRepoContext.name,
workflow_id: workflowIdForApi,
per_page: 1
});
latestRunId = result.data.workflow_runs[0]?.id;
log(`Latest run ID before trigger: ${latestRunId}`);
} catch (e) {
log(`Error fetching latest run: ${(e as Error).message}`);
}

let dispatched = false;

let selectedEvent: string | undefined;
if (workflow.events.workflow_dispatch !== undefined && workflow.events.repository_dispatch !== undefined) {
selectedEvent = await vscode.window.showQuickPick(["repository_dispatch", "workflow_dispatch"], {
Expand Down Expand Up @@ -85,8 +115,6 @@ export function registerTriggerWorkflowRun(context: vscode.ExtensionContext) {
}

try {
const relativeWorkflowPath = vscode.workspace.asRelativePath(workflowUri, false);

await gitHubRepoContext.client.actions.createWorkflowDispatch({
owner: gitHubRepoContext.owner,
repo: gitHubRepoContext.name,
Expand All @@ -95,6 +123,7 @@ export function registerTriggerWorkflowRun(context: vscode.ExtensionContext) {
inputs
});

dispatched = true;
vscode.window.setStatusBarMessage(`GitHub Actions: Workflow event dispatched`, 2000);
} catch (error) {
return vscode.window.showErrorMessage(`Could not create workflow dispatch: ${(error as Error)?.message}`);
Expand Down Expand Up @@ -134,10 +163,49 @@ export function registerTriggerWorkflowRun(context: vscode.ExtensionContext) {
client_payload: {}
});

dispatched = true;
vscode.window.setStatusBarMessage(`GitHub Actions: Repository event '${event_type}' dispatched`, 2000);
}
}

if (dispatched) {
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Window,
title: "Waiting for workflow run to start..."
},
async () => {
log("Starting loop to check for new workflow run...");
for (let i = 0; i < 20; i++) {
if (i > 0) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
try {
log(`Checking for new run (attempt ${i + 1}/20)...`);
const result = await gitHubRepoContext.client.actions.listWorkflowRuns({
owner: gitHubRepoContext.owner,
repo: gitHubRepoContext.name,
workflow_id: workflowIdForApi as string | number,
per_page: 1
});
const newLatestRunId = result.data.workflow_runs[0]?.id;
log(`Latest run ID found: ${newLatestRunId} (Previous: ${latestRunId ?? "none"})`);

if (newLatestRunId && newLatestRunId !== latestRunId) {
log(`Found new workflow run: ${newLatestRunId}. Triggering refresh and polling.`);
await vscode.commands.executeCommand("github-actions.explorer.refresh");
// Poll for 15 minutes (225 * 4s)
store.pollRun(newLatestRunId, gitHubRepoContext, 4000, 225);
break;
}
} catch (e) {
log(`Error checking for new run: ${(e as Error).message}`);
}
}
}
);
}

return vscode.commands.executeCommand("github-actions.explorer.refresh");
}
)
Expand Down
9 changes: 8 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ export async function activate(context: vscode.ExtensionContext) {

const store = new RunStore();

// Handle focus changes to pause/resume polling
context.subscriptions.push(
vscode.window.onDidChangeWindowState(e => {
store.setFocused(e.focused);
})
);

// Pinned workflows
await initPinnedWorkflows(store);

Expand All @@ -73,7 +80,7 @@ export async function activate(context: vscode.ExtensionContext) {
registerOpenWorkflowFile(context);
registerOpenWorkflowJobLogs(context);
registerOpenWorkflowStepLogs(context);
registerTriggerWorkflowRun(context);
registerTriggerWorkflowRun(context, store);
registerReRunWorkflowRun(context);
registerCancelWorkflowRun(context);

Expand Down
29 changes: 27 additions & 2 deletions src/store/store.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {setInterval} from "timers";
import {EventEmitter} from "vscode";
import {GitHubRepoContext} from "../git/repository";
import {logDebug} from "../log";
import {log, logDebug} from "../log";
import * as model from "../model";
import {WorkflowRun} from "./workflowRun";

Expand All @@ -20,6 +20,18 @@ type Updater = {
export class RunStore extends EventEmitter<RunStoreEvent> {
private runs = new Map<number, WorkflowRun>();
private updaters = new Map<number, Updater>();
private _isFocused = true;
private _isViewVisible = true;

setFocused(focused: boolean) {
this._isFocused = focused;
logDebug(`[Store]: Focus state changed to ${String(focused)}`);
}

setViewVisible(visible: boolean) {
this._isViewVisible = visible;
logDebug(`[Store]: View visibility changed to ${String(visible)}`);
}

getRun(runId: number): WorkflowRun | undefined {
return this.runs.get(runId);
Expand All @@ -46,6 +58,7 @@ export class RunStore extends EventEmitter<RunStoreEvent> {
* Start polling for updates for the given run
*/
pollRun(runId: number, repoContext: GitHubRepoContext, intervalMs: number, attempts = 10) {
log(`Starting polling for run ${runId} every ${intervalMs}ms for ${attempts} attempts`);
const existingUpdater: Updater | undefined = this.updaters.get(runId);
if (existingUpdater && existingUpdater.handle) {
clearInterval(existingUpdater.handle);
Expand All @@ -65,7 +78,11 @@ export class RunStore extends EventEmitter<RunStoreEvent> {
}

private async fetchRun(updater: Updater) {
logDebug("Updating run: ", updater.runId);
if (!this._isFocused || !this._isViewVisible) {
return;
}

log(`Fetching run update: ${updater.runId}. Remaining attempts: ${updater.remainingAttempts}`);

updater.remainingAttempts--;
if (updater.remainingAttempts === 0) {
Expand All @@ -83,6 +100,14 @@ export class RunStore extends EventEmitter<RunStoreEvent> {
});

const run = result.data;
log(`Polled run: ${run.id} Status: ${run.status || "null"} Conclusion: ${run.conclusion || "null"}`);
this.addRun(updater.repoContext, run);

if (run.status === "completed") {
if (updater.handle) {
clearInterval(updater.handle);
}
this.updaters.delete(updater.runId);
}
}
}
8 changes: 3 additions & 5 deletions src/store/workflowRun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,9 @@ abstract class WorkflowRunBase {
}

updateRun(run: model.WorkflowRun) {
if (this._run.status !== "completed" || this._run.updated_at !== run.updated_at) {
// Refresh jobs if the run is not completed or it was updated (i.e. re-run)
// For in-progress runs, we can't rely on updated at to change when jobs change
this._jobs = undefined;
}
// Always clear jobs cache when updating run to ensure we get latest job status
// This is critical for polling to work correctly for in-progress runs
this._jobs = undefined;

this._run = run;
}
Expand Down
12 changes: 11 additions & 1 deletion src/treeViews/treeViews.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,17 @@ import {WorkflowsTreeProvider} from "./workflows";

export async function initTreeViews(context: vscode.ExtensionContext, store: RunStore): Promise<void> {
const workflowTreeProvider = new WorkflowsTreeProvider(store);
context.subscriptions.push(vscode.window.registerTreeDataProvider("github-actions.workflows", workflowTreeProvider));
const workflowTreeView = vscode.window.createTreeView("github-actions.workflows", {
treeDataProvider: workflowTreeProvider
});
context.subscriptions.push(workflowTreeView);

store.setViewVisible(workflowTreeView.visible);
context.subscriptions.push(
workflowTreeView.onDidChangeVisibility(e => {
store.setViewVisible(e.visible);
})
);

const settingsTreeProvider = new SettingsTreeProvider();
context.subscriptions.push(vscode.window.registerTreeDataProvider("github-actions.settings", settingsTreeProvider));
Expand Down
12 changes: 12 additions & 0 deletions src/treeViews/workflowRunTreeDataProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@ export abstract class WorkflowRunTreeDataProvider {
): WorkflowRunNode[] {
return runData.map(runData => {
const workflowRun = this.store.addRun(gitHubRepoContext, runData);

// Auto-poll active runs
if (
workflowRun.run.status === "in_progress" ||
workflowRun.run.status === "queued" ||
workflowRun.run.status === "waiting" ||
workflowRun.run.status === "requested"
) {
// Poll every 4 seconds for up to 15 minutes (225 attempts)
this.store.pollRun(workflowRun.run.id, gitHubRepoContext, 4000, 225);
}

const node = new WorkflowRunNode(
this.store,
gitHubRepoContext,
Expand Down
3 changes: 2 additions & 1 deletion src/treeViews/workflows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ import {WorkflowNode} from "./workflows/workflowNode";
import {getWorkflowNodes, WorkflowsRepoNode} from "./workflows/workflowsRepoNode";
import {WorkflowStepNode} from "./workflows/workflowStepNode";

type WorkflowsTreeNode =
export type WorkflowsTreeNode =
| AuthenticationNode
| NoGitHubRepositoryNode
| WorkflowsRepoNode
| WorkflowNode
| WorkflowRunNode
| PreviousAttemptsNode
Expand Down