diff --git a/src/commands/triggerWorkflowRun.ts b/src/commands/triggerWorkflowRun.ts index 3deba5b..9035d92 100644 --- a/src/commands/triggerWorkflowRun.ts +++ b/src/commands/triggerWorkflowRun.ts @@ -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) { @@ -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"], { @@ -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, @@ -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}`); @@ -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"); } ) diff --git a/src/extension.ts b/src/extension.ts index 210c954..dbee6d2 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -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); @@ -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); diff --git a/src/store/store.ts b/src/store/store.ts index 44918f3..ea02570 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -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"; @@ -20,6 +20,18 @@ type Updater = { export class RunStore extends EventEmitter { private runs = new Map(); private updaters = new Map(); + 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); @@ -46,6 +58,7 @@ export class RunStore extends EventEmitter { * 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); @@ -65,7 +78,11 @@ export class RunStore extends EventEmitter { } 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) { @@ -83,6 +100,14 @@ export class RunStore extends EventEmitter { }); 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); + } } } diff --git a/src/store/workflowRun.ts b/src/store/workflowRun.ts index 1001bc2..25eb5fe 100644 --- a/src/store/workflowRun.ts +++ b/src/store/workflowRun.ts @@ -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; } diff --git a/src/treeViews/treeViews.ts b/src/treeViews/treeViews.ts index 6261b1b..f75c569 100644 --- a/src/treeViews/treeViews.ts +++ b/src/treeViews/treeViews.ts @@ -11,7 +11,17 @@ import {WorkflowsTreeProvider} from "./workflows"; export async function initTreeViews(context: vscode.ExtensionContext, store: RunStore): Promise { 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)); diff --git a/src/treeViews/workflowRunTreeDataProvider.ts b/src/treeViews/workflowRunTreeDataProvider.ts index 4ce67dd..9627c9f 100644 --- a/src/treeViews/workflowRunTreeDataProvider.ts +++ b/src/treeViews/workflowRunTreeDataProvider.ts @@ -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, diff --git a/src/treeViews/workflows.ts b/src/treeViews/workflows.ts index b91a55b..51f4bcd 100644 --- a/src/treeViews/workflows.ts +++ b/src/treeViews/workflows.ts @@ -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