From 3692e70b7a9cb27c3380215378dcaa8825154b5e Mon Sep 17 00:00:00 2001 From: Shayne Boyer Date: Thu, 18 Dec 2025 14:43:06 -0500 Subject: [PATCH 01/18] Fix #229: Auto-refresh UI when a new workflow run is triggered --- src/commands/triggerWorkflowRun.ts | 46 ++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/src/commands/triggerWorkflowRun.ts b/src/commands/triggerWorkflowRun.ts index 3deba5b0..c85adf28 100644 --- a/src/commands/triggerWorkflowRun.ts +++ b/src/commands/triggerWorkflowRun.ts @@ -43,6 +43,22 @@ export function registerTriggerWorkflowRun(context: vscode.ExtensionContext) { return; } + const relativeWorkflowPath = vscode.workspace.asRelativePath(workflowUri, false); + let latestRunId: number | undefined; + try { + const result = await gitHubRepoContext.client.actions.listWorkflowRuns({ + owner: gitHubRepoContext.owner, + repo: gitHubRepoContext.name, + workflow_id: relativeWorkflowPath, + per_page: 1 + }); + latestRunId = result.data.workflow_runs[0]?.id; + } catch (e) { + // Ignore error + } + + 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 +101,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 +109,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 +149,37 @@ export function registerTriggerWorkflowRun(context: vscode.ExtensionContext) { client_payload: {} }); + dispatched = true; vscode.window.setStatusBarMessage(`GitHub Actions: Repository event '${event_type}' dispatched`, 2000); } } + if (dispatched) { + vscode.window.withProgress({ + location: vscode.ProgressLocation.Window, + title: "Waiting for workflow run to start..." + }, async () => { + for (let i = 0; i < 20; i++) { + await new Promise(resolve => setTimeout(resolve, 1000)); + try { + const result = await gitHubRepoContext.client.actions.listWorkflowRuns({ + owner: gitHubRepoContext.owner, + repo: gitHubRepoContext.name, + workflow_id: relativeWorkflowPath, + per_page: 1 + }); + const newLatestRunId = result.data.workflow_runs[0]?.id; + if (newLatestRunId && newLatestRunId !== latestRunId) { + await vscode.commands.executeCommand("github-actions.explorer.refresh"); + break; + } + } catch (e) { + // Ignore + } + } + }); + } + return vscode.commands.executeCommand("github-actions.explorer.refresh"); } ) From f7cfef85a43e2f0a5d9b62552ea8367c1e4b8044 Mon Sep 17 00:00:00 2001 From: Shayne Boyer Date: Thu, 18 Dec 2025 14:46:04 -0500 Subject: [PATCH 02/18] Add test plan for auto-refresh feature in GitHub Actions extension --- TEST_PLAN.md | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 TEST_PLAN.md diff --git a/TEST_PLAN.md b/TEST_PLAN.md new file mode 100644 index 00000000..bf6489d7 --- /dev/null +++ b/TEST_PLAN.md @@ -0,0 +1,52 @@ +# Test Plan for Issue #229 Fix + +## Objective +Verify that the GitHub Actions extension automatically refreshes the workflow run list after a user manually triggers a workflow run. + +## Prerequisites +1. A GitHub repository with GitHub Actions enabled. +2. A workflow file in that repository that supports `workflow_dispatch` (manual trigger). + * Example `workflow_dispatch` configuration in `.github/workflows/test.yml`: + ```yaml + name: Manual Test + on: workflow_dispatch + jobs: + build: + runs-on: ubuntu-latest + steps: + - run: echo "Hello World" + ``` +3. The repository is cloned locally and opened in VS Code. + +## Manual Test Steps + +1. **Launch Extension in Debug Mode**: + * Open the `vscode-github-actions` project in VS Code. + * Press `F5` or go to "Run and Debug" and select "Watch & Launch Extension". + * This will open a new "Extension Development Host" window. + +2. **Open Test Repository**: + * In the Extension Development Host window, open the folder of your test repository (from Prerequisites). + +3. **Navigate to GitHub Actions View**: + * Click on the GitHub Actions icon in the Activity Bar (sidebar). + * Expand the "Workflows" section. + * Find your manual workflow (e.g., "Manual Test"). + +4. **Trigger Workflow Run**: + * Right-click on the workflow name. + * Select **"Run Workflow"**. + * (Optional) If prompted for branch or inputs, provide them and confirm. + +5. **Verify Behavior**: + * **Immediate Feedback**: Look for a status bar message or notification saying "Waiting for workflow run to start...". + * **Auto-Refresh**: Watch the "Workflows" list. Within 1-20 seconds, the list should automatically refresh. + * **New Run**: A new workflow run (likely in "Queued" or "In Progress" state) should appear at the top of the list under the workflow. + +## Expected Result +* The user does **not** need to manually click the refresh button. +* The UI updates automatically once the new run is created on GitHub. + +## Troubleshooting +* If the run doesn't appear after 20 seconds, check if the workflow actually started on GitHub.com. +* Check the "Output" panel (select "GitHub Actions" in the dropdown) for any error logs. From 0e7ed6183974b130f6de4f9b6f44e247752fcf77 Mon Sep 17 00:00:00 2001 From: Shayne Boyer Date: Thu, 18 Dec 2025 14:52:39 -0500 Subject: [PATCH 03/18] Fix #229: Poll new workflow run for status updates --- src/commands/triggerWorkflowRun.ts | 4 +++- src/extension.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/commands/triggerWorkflowRun.ts b/src/commands/triggerWorkflowRun.ts index c85adf28..c7c74881 100644 --- a/src/commands/triggerWorkflowRun.ts +++ b/src/commands/triggerWorkflowRun.ts @@ -4,13 +4,14 @@ import {getGitHead, getGitHubContextForWorkspaceUri, GitHubRepoContext} from ".. import {getWorkflowUri, parseWorkflowFile} from "../workflow/workflow"; import {Workflow} from "../model"; +import {RunStore} from "../store/store"; 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", @@ -171,6 +172,7 @@ export function registerTriggerWorkflowRun(context: vscode.ExtensionContext) { const newLatestRunId = result.data.workflow_runs[0]?.id; if (newLatestRunId && newLatestRunId !== latestRunId) { await vscode.commands.executeCommand("github-actions.explorer.refresh"); + store.pollRun(newLatestRunId, gitHubRepoContext, 1000, 20); break; } } catch (e) { diff --git a/src/extension.ts b/src/extension.ts index 210c9548..d1fdd50d 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -73,7 +73,7 @@ export async function activate(context: vscode.ExtensionContext) { registerOpenWorkflowFile(context); registerOpenWorkflowJobLogs(context); registerOpenWorkflowStepLogs(context); - registerTriggerWorkflowRun(context); + registerTriggerWorkflowRun(context, store); registerReRunWorkflowRun(context); registerCancelWorkflowRun(context); From decdefa55c2a743ca749bd67101e0acde25ca380 Mon Sep 17 00:00:00 2001 From: Shayne Boyer Date: Thu, 18 Dec 2025 14:58:54 -0500 Subject: [PATCH 04/18] Fix #229: Improve polling duration and stop condition --- src/commands/triggerWorkflowRun.ts | 3 ++- src/store/store.ts | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/commands/triggerWorkflowRun.ts b/src/commands/triggerWorkflowRun.ts index c7c74881..ff3f8790 100644 --- a/src/commands/triggerWorkflowRun.ts +++ b/src/commands/triggerWorkflowRun.ts @@ -172,7 +172,8 @@ export function registerTriggerWorkflowRun(context: vscode.ExtensionContext, sto const newLatestRunId = result.data.workflow_runs[0]?.id; if (newLatestRunId && newLatestRunId !== latestRunId) { await vscode.commands.executeCommand("github-actions.explorer.refresh"); - store.pollRun(newLatestRunId, gitHubRepoContext, 1000, 20); + // Poll for 15 minutes (225 * 4s) + store.pollRun(newLatestRunId, gitHubRepoContext, 4000, 225); break; } } catch (e) { diff --git a/src/store/store.ts b/src/store/store.ts index 44918f33..4fa769e3 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -84,5 +84,12 @@ export class RunStore extends EventEmitter { const run = result.data; this.addRun(updater.repoContext, run); + + if (run.status === "completed" || run.status === "cancelled" || run.status === "failure" || run.status === "success" || run.status === "skipped" || run.status === "timed_out") { + if (updater.handle) { + clearInterval(updater.handle); + } + this.updaters.delete(updater.runId); + } } } From ba66672ec2fde72630328778af33a70012d09c08 Mon Sep 17 00:00:00 2001 From: Shayne Boyer Date: Thu, 18 Dec 2025 15:04:14 -0500 Subject: [PATCH 05/18] Fix #229: Force job refresh on run update and add logging --- src/store/store.ts | 1 + src/store/workflowRun.ts | 8 +++----- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/store/store.ts b/src/store/store.ts index 4fa769e3..46cb3123 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -83,6 +83,7 @@ export class RunStore extends EventEmitter { }); const run = result.data; + logDebug("Polled run:", run.id, "Status:", run.status, "Conclusion:", run.conclusion); this.addRun(updater.repoContext, run); if (run.status === "completed" || run.status === "cancelled" || run.status === "failure" || run.status === "success" || run.status === "skipped" || run.status === "timed_out") { diff --git a/src/store/workflowRun.ts b/src/store/workflowRun.ts index 1001bc2e..25eb5fe0 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; } From 89fbe5a12b64a6d94fb4640ecf50b1f86679a211 Mon Sep 17 00:00:00 2001 From: Shayne Boyer Date: Thu, 18 Dec 2025 15:13:27 -0500 Subject: [PATCH 06/18] Fix #229: Add verbose logging for debugging polling issues --- src/commands/triggerWorkflowRun.ts | 5 ++++- src/store/store.ts | 7 ++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/commands/triggerWorkflowRun.ts b/src/commands/triggerWorkflowRun.ts index ff3f8790..253bdd0b 100644 --- a/src/commands/triggerWorkflowRun.ts +++ b/src/commands/triggerWorkflowRun.ts @@ -6,6 +6,8 @@ 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; @@ -171,13 +173,14 @@ export function registerTriggerWorkflowRun(context: vscode.ExtensionContext, sto }); const newLatestRunId = result.data.workflow_runs[0]?.id; 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) { - // Ignore + log(`Error checking for new run: ${(e as Error).message}`); } } }); diff --git a/src/store/store.ts b/src/store/store.ts index 46cb3123..d09f1fc6 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"; @@ -46,6 +46,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 +66,7 @@ export class RunStore extends EventEmitter { } private async fetchRun(updater: Updater) { - logDebug("Updating run: ", updater.runId); + log(`Fetching run update: ${updater.runId}. Remaining attempts: ${updater.remainingAttempts}`); updater.remainingAttempts--; if (updater.remainingAttempts === 0) { @@ -83,7 +84,7 @@ export class RunStore extends EventEmitter { }); const run = result.data; - logDebug("Polled run:", run.id, "Status:", run.status, "Conclusion:", run.conclusion); + log(`Polled run: ${run.id} Status: ${run.status} Conclusion: ${run.conclusion}`); this.addRun(updater.repoContext, run); if (run.status === "completed" || run.status === "cancelled" || run.status === "failure" || run.status === "success" || run.status === "skipped" || run.status === "timed_out") { From 0b5c87db1390fcfcc6691db1ffae667f99f6cf62 Mon Sep 17 00:00:00 2001 From: Shayne Boyer Date: Thu, 18 Dec 2025 15:17:39 -0500 Subject: [PATCH 07/18] Fix #229: Use workflow ID or filename for polling, fix path issue --- src/commands/triggerWorkflowRun.ts | 64 ++++++++++++++++++------------ 1 file changed, 39 insertions(+), 25 deletions(-) diff --git a/src/commands/triggerWorkflowRun.ts b/src/commands/triggerWorkflowRun.ts index 253bdd0b..084390bd 100644 --- a/src/commands/triggerWorkflowRun.ts +++ b/src/commands/triggerWorkflowRun.ts @@ -1,3 +1,4 @@ +import {basename} from "path"; import * as vscode from "vscode"; import {getGitHead, getGitHubContextForWorkspaceUri, GitHubRepoContext} from "../git/repository"; @@ -19,11 +20,15 @@ export function registerTriggerWorkflowRun(context: vscode.ExtensionContext, sto "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) { @@ -47,17 +52,23 @@ export function registerTriggerWorkflowRun(context: vscode.ExtensionContext, sto } 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: relativeWorkflowPath, + workflow_id: workflowIdForApi, per_page: 1 }); latestRunId = result.data.workflow_runs[0]?.id; + log(`Latest run ID before trigger: ${latestRunId}`); } catch (e) { - // Ignore error + log(`Error fetching latest run: ${(e as Error).message}`); } let dispatched = false; @@ -158,32 +169,35 @@ export function registerTriggerWorkflowRun(context: vscode.ExtensionContext, sto } if (dispatched) { - vscode.window.withProgress({ - location: vscode.ProgressLocation.Window, - title: "Waiting for workflow run to start..." - }, async () => { - for (let i = 0; i < 20; i++) { - await new Promise(resolve => setTimeout(resolve, 1000)); - try { - const result = await gitHubRepoContext.client.actions.listWorkflowRuns({ - owner: gitHubRepoContext.owner, - repo: gitHubRepoContext.name, - workflow_id: relativeWorkflowPath, - per_page: 1 - }); - const newLatestRunId = result.data.workflow_runs[0]?.id; - 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; + vscode.window.withProgress( + { + location: vscode.ProgressLocation.Window, + title: "Waiting for workflow run to start..." + }, + async () => { + for (let i = 0; i < 20; i++) { + await new Promise(resolve => setTimeout(resolve, 1000)); + try { + const result = await gitHubRepoContext.client.actions.listWorkflowRuns({ + owner: gitHubRepoContext.owner, + repo: gitHubRepoContext.name, + workflow_id: workflowIdForApi!, + per_page: 1 + }); + const newLatestRunId = result.data.workflow_runs[0]?.id; + 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}`); } - } catch (e) { - log(`Error checking for new run: ${(e as Error).message}`); } } - }); + ); } return vscode.commands.executeCommand("github-actions.explorer.refresh"); From a62d2e03df1cd334948ee7e57988a6df020b6d02 Mon Sep 17 00:00:00 2001 From: Shayne Boyer Date: Thu, 18 Dec 2025 15:20:51 -0500 Subject: [PATCH 08/18] Fix #229: Add even more verbose logging for polling loop --- src/commands/triggerWorkflowRun.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/commands/triggerWorkflowRun.ts b/src/commands/triggerWorkflowRun.ts index 084390bd..923b0102 100644 --- a/src/commands/triggerWorkflowRun.ts +++ b/src/commands/triggerWorkflowRun.ts @@ -175,16 +175,20 @@ export function registerTriggerWorkflowRun(context: vscode.ExtensionContext, sto title: "Waiting for workflow run to start..." }, async () => { + log("Starting loop to check for new workflow run..."); for (let i = 0; i < 20; i++) { 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!, + workflow_id: workflowIdForApi!, per_page: 1 }); const newLatestRunId = result.data.workflow_runs[0]?.id; + log(`Latest run ID found: ${newLatestRunId} (Previous: ${latestRunId})`); + if (newLatestRunId && newLatestRunId !== latestRunId) { log(`Found new workflow run: ${newLatestRunId}. Triggering refresh and polling.`); await vscode.commands.executeCommand("github-actions.explorer.refresh"); From 964065cb5993097f95f00d10e1d3712056c3456e Mon Sep 17 00:00:00 2001 From: Shayne Boyer Date: Thu, 18 Dec 2025 15:24:14 -0500 Subject: [PATCH 09/18] Fix #229: Auto-poll active runs and respect window focus --- src/extension.ts | 7 +++++++ src/store/store.ts | 19 ++++++++++++++++++- src/treeViews/workflowRunTreeDataProvider.ts | 12 ++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/extension.ts b/src/extension.ts index d1fdd50d..dbee6d20 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); diff --git a/src/store/store.ts b/src/store/store.ts index d09f1fc6..414101a8 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -20,6 +20,12 @@ type Updater = { export class RunStore extends EventEmitter { private runs = new Map(); private updaters = new Map(); + private _isFocused = true; + + setFocused(focused: boolean) { + this._isFocused = focused; + logDebug(`[Store]: Focus state changed to ${focused}`); + } getRun(runId: number): WorkflowRun | undefined { return this.runs.get(runId); @@ -66,6 +72,10 @@ export class RunStore extends EventEmitter { } private async fetchRun(updater: Updater) { + if (!this._isFocused) { + return; + } + log(`Fetching run update: ${updater.runId}. Remaining attempts: ${updater.remainingAttempts}`); updater.remainingAttempts--; @@ -87,7 +97,14 @@ export class RunStore extends EventEmitter { log(`Polled run: ${run.id} Status: ${run.status} Conclusion: ${run.conclusion}`); this.addRun(updater.repoContext, run); - if (run.status === "completed" || run.status === "cancelled" || run.status === "failure" || run.status === "success" || run.status === "skipped" || run.status === "timed_out") { + if ( + run.status === "completed" || + run.status === "cancelled" || + run.status === "failure" || + run.status === "success" || + run.status === "skipped" || + run.status === "timed_out" + ) { if (updater.handle) { clearInterval(updater.handle); } diff --git a/src/treeViews/workflowRunTreeDataProvider.ts b/src/treeViews/workflowRunTreeDataProvider.ts index 4ce67dd4..9627c9f9 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, From 6fdb2edfcd383aa4f2a0874616fce962794632d7 Mon Sep 17 00:00:00 2001 From: Shayne Boyer Date: Thu, 18 Dec 2025 15:29:35 -0500 Subject: [PATCH 10/18] Fix #229: Pause polling when Workflows view is not visible --- src/store/store.ts | 8 +++++++- src/treeViews/treeViews.ts | 10 +++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/store/store.ts b/src/store/store.ts index 414101a8..036639ab 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -21,12 +21,18 @@ 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 ${focused}`); } + setViewVisible(visible: boolean) { + this._isViewVisible = visible; + logDebug(`[Store]: View visibility changed to ${visible}`); + } + getRun(runId: number): WorkflowRun | undefined { return this.runs.get(runId); } @@ -72,7 +78,7 @@ export class RunStore extends EventEmitter { } private async fetchRun(updater: Updater) { - if (!this._isFocused) { + if (!this._isFocused || !this._isViewVisible) { return; } diff --git a/src/treeViews/treeViews.ts b/src/treeViews/treeViews.ts index 6261b1b2..2596d96d 100644 --- a/src/treeViews/treeViews.ts +++ b/src/treeViews/treeViews.ts @@ -11,7 +11,15 @@ 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); + workflowTreeView.onDidChangeVisibility(e => { + store.setViewVisible(e.visible); + }); const settingsTreeProvider = new SettingsTreeProvider(); context.subscriptions.push(vscode.window.registerTreeDataProvider("github-actions.settings", settingsTreeProvider)); From 4c04b43f523f2c998225a2fa5563ef86143e58fb Mon Sep 17 00:00:00 2001 From: Shayne Boyer Date: Thu, 18 Dec 2025 15:39:30 -0500 Subject: [PATCH 11/18] Fix #229: Improve logging for focus and visibility state changes; refine run polling log output --- src/store/store.ts | 15 ++++----------- src/treeViews/workflows.ts | 3 ++- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/store/store.ts b/src/store/store.ts index 036639ab..ea025703 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -25,12 +25,12 @@ export class RunStore extends EventEmitter { setFocused(focused: boolean) { this._isFocused = focused; - logDebug(`[Store]: Focus state changed to ${focused}`); + logDebug(`[Store]: Focus state changed to ${String(focused)}`); } setViewVisible(visible: boolean) { this._isViewVisible = visible; - logDebug(`[Store]: View visibility changed to ${visible}`); + logDebug(`[Store]: View visibility changed to ${String(visible)}`); } getRun(runId: number): WorkflowRun | undefined { @@ -100,17 +100,10 @@ export class RunStore extends EventEmitter { }); const run = result.data; - log(`Polled run: ${run.id} Status: ${run.status} Conclusion: ${run.conclusion}`); + log(`Polled run: ${run.id} Status: ${run.status || "null"} Conclusion: ${run.conclusion || "null"}`); this.addRun(updater.repoContext, run); - if ( - run.status === "completed" || - run.status === "cancelled" || - run.status === "failure" || - run.status === "success" || - run.status === "skipped" || - run.status === "timed_out" - ) { + if (run.status === "completed") { if (updater.handle) { clearInterval(updater.handle); } diff --git a/src/treeViews/workflows.ts b/src/treeViews/workflows.ts index b91a55b6..51f4bcdc 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 From fc52ae598ef0a254379204bb9c46004a3054ed20 Mon Sep 17 00:00:00 2001 From: Shayne Boyer Date: Thu, 18 Dec 2025 15:48:01 -0500 Subject: [PATCH 12/18] Remove TEST_PLAN.md --- TEST_PLAN.md | 52 ---------------------------------------------------- 1 file changed, 52 deletions(-) delete mode 100644 TEST_PLAN.md diff --git a/TEST_PLAN.md b/TEST_PLAN.md deleted file mode 100644 index bf6489d7..00000000 --- a/TEST_PLAN.md +++ /dev/null @@ -1,52 +0,0 @@ -# Test Plan for Issue #229 Fix - -## Objective -Verify that the GitHub Actions extension automatically refreshes the workflow run list after a user manually triggers a workflow run. - -## Prerequisites -1. A GitHub repository with GitHub Actions enabled. -2. A workflow file in that repository that supports `workflow_dispatch` (manual trigger). - * Example `workflow_dispatch` configuration in `.github/workflows/test.yml`: - ```yaml - name: Manual Test - on: workflow_dispatch - jobs: - build: - runs-on: ubuntu-latest - steps: - - run: echo "Hello World" - ``` -3. The repository is cloned locally and opened in VS Code. - -## Manual Test Steps - -1. **Launch Extension in Debug Mode**: - * Open the `vscode-github-actions` project in VS Code. - * Press `F5` or go to "Run and Debug" and select "Watch & Launch Extension". - * This will open a new "Extension Development Host" window. - -2. **Open Test Repository**: - * In the Extension Development Host window, open the folder of your test repository (from Prerequisites). - -3. **Navigate to GitHub Actions View**: - * Click on the GitHub Actions icon in the Activity Bar (sidebar). - * Expand the "Workflows" section. - * Find your manual workflow (e.g., "Manual Test"). - -4. **Trigger Workflow Run**: - * Right-click on the workflow name. - * Select **"Run Workflow"**. - * (Optional) If prompted for branch or inputs, provide them and confirm. - -5. **Verify Behavior**: - * **Immediate Feedback**: Look for a status bar message or notification saying "Waiting for workflow run to start...". - * **Auto-Refresh**: Watch the "Workflows" list. Within 1-20 seconds, the list should automatically refresh. - * **New Run**: A new workflow run (likely in "Queued" or "In Progress" state) should appear at the top of the list under the workflow. - -## Expected Result -* The user does **not** need to manually click the refresh button. -* The UI updates automatically once the new run is created on GitHub. - -## Troubleshooting -* If the run doesn't appear after 20 seconds, check if the workflow actually started on GitHub.com. -* Check the "Output" panel (select "GitHub Actions" in the dropdown) for any error logs. From a65e12fbae8124c0ca363b9c6135dff65bd2f378 Mon Sep 17 00:00:00 2001 From: Shayne Boyer Date: Fri, 19 Dec 2025 10:45:31 -0500 Subject: [PATCH 13/18] Fix lint errors: await promise, remove non-null assertion, fix template expression --- src/commands/triggerWorkflowRun.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commands/triggerWorkflowRun.ts b/src/commands/triggerWorkflowRun.ts index 923b0102..d3339bd2 100644 --- a/src/commands/triggerWorkflowRun.ts +++ b/src/commands/triggerWorkflowRun.ts @@ -169,7 +169,7 @@ export function registerTriggerWorkflowRun(context: vscode.ExtensionContext, sto } if (dispatched) { - vscode.window.withProgress( + await vscode.window.withProgress( { location: vscode.ProgressLocation.Window, title: "Waiting for workflow run to start..." @@ -183,11 +183,11 @@ export function registerTriggerWorkflowRun(context: vscode.ExtensionContext, sto const result = await gitHubRepoContext.client.actions.listWorkflowRuns({ owner: gitHubRepoContext.owner, repo: gitHubRepoContext.name, - workflow_id: workflowIdForApi!, + 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})`); + log(`Latest run ID found: ${newLatestRunId} (Previous: ${latestRunId ?? 'none'})`); if (newLatestRunId && newLatestRunId !== latestRunId) { log(`Found new workflow run: ${newLatestRunId}. Triggering refresh and polling.`); From a08153ab159f3e33bbb42774f080bf83d24af744 Mon Sep 17 00:00:00 2001 From: Shayne Boyer Date: Fri, 19 Dec 2025 10:47:00 -0500 Subject: [PATCH 14/18] Fix prettier formatting --- src/commands/triggerWorkflowRun.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/triggerWorkflowRun.ts b/src/commands/triggerWorkflowRun.ts index d3339bd2..ee2f33c5 100644 --- a/src/commands/triggerWorkflowRun.ts +++ b/src/commands/triggerWorkflowRun.ts @@ -187,7 +187,7 @@ export function registerTriggerWorkflowRun(context: vscode.ExtensionContext, sto per_page: 1 }); const newLatestRunId = result.data.workflow_runs[0]?.id; - log(`Latest run ID found: ${newLatestRunId} (Previous: ${latestRunId ?? 'none'})`); + log(`Latest run ID found: ${newLatestRunId} (Previous: ${latestRunId ?? "none"})`); if (newLatestRunId && newLatestRunId !== latestRunId) { log(`Found new workflow run: ${newLatestRunId}. Triggering refresh and polling.`); From c358f6ee5444698bf73d5711bdb7d798749da2da Mon Sep 17 00:00:00 2001 From: Shayne Boyer Date: Fri, 19 Dec 2025 10:51:12 -0500 Subject: [PATCH 15/18] Update src/commands/triggerWorkflowRun.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/commands/triggerWorkflowRun.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/commands/triggerWorkflowRun.ts b/src/commands/triggerWorkflowRun.ts index ee2f33c5..9035d921 100644 --- a/src/commands/triggerWorkflowRun.ts +++ b/src/commands/triggerWorkflowRun.ts @@ -177,7 +177,9 @@ export function registerTriggerWorkflowRun(context: vscode.ExtensionContext, sto async () => { log("Starting loop to check for new workflow run..."); for (let i = 0; i < 20; i++) { - await new Promise(resolve => setTimeout(resolve, 1000)); + 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({ From a909e5938e667f5d1fabc826e935185de764b852 Mon Sep 17 00:00:00 2001 From: Shayne Boyer Date: Fri, 19 Dec 2025 10:54:03 -0500 Subject: [PATCH 16/18] Update src/treeViews/treeViews.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/treeViews/treeViews.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/treeViews/treeViews.ts b/src/treeViews/treeViews.ts index 2596d96d..f75c569b 100644 --- a/src/treeViews/treeViews.ts +++ b/src/treeViews/treeViews.ts @@ -17,9 +17,11 @@ export async function initTreeViews(context: vscode.ExtensionContext, store: Run context.subscriptions.push(workflowTreeView); store.setViewVisible(workflowTreeView.visible); - workflowTreeView.onDidChangeVisibility(e => { - store.setViewVisible(e.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)); From b504394f69947b7fbae432be3619b6048f31454d Mon Sep 17 00:00:00 2001 From: Shayne Boyer <7681382+spboyer@users.noreply.github.com> Date: Mon, 2 Nov 2026 23:16:44 -0500 Subject: [PATCH 17/18] fix: preserve active polling sessions across tree refreshes Avoid resetting automatic polling deadlines and budgets while keeping explicit rerun/cancel restarts. Skip the duplicate refresh after detecting a dispatched run and add polling regressions. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/commands/triggerWorkflowRun.ts | 8 +- src/store/polling.test.ts | 263 +++++++++++++++++++ src/store/store.ts | 11 +- src/treeViews/workflowRunTreeDataProvider.ts | 2 +- 4 files changed, 278 insertions(+), 6 deletions(-) create mode 100644 src/store/polling.test.ts diff --git a/src/commands/triggerWorkflowRun.ts b/src/commands/triggerWorkflowRun.ts index 9035d921..b5ed2938 100644 --- a/src/commands/triggerWorkflowRun.ts +++ b/src/commands/triggerWorkflowRun.ts @@ -169,7 +169,7 @@ export function registerTriggerWorkflowRun(context: vscode.ExtensionContext, sto } if (dispatched) { - await vscode.window.withProgress( + const refreshed = await vscode.window.withProgress( { location: vscode.ProgressLocation.Window, title: "Waiting for workflow run to start..." @@ -196,14 +196,18 @@ export function registerTriggerWorkflowRun(context: vscode.ExtensionContext, sto await vscode.commands.executeCommand("github-actions.explorer.refresh"); // Poll for 15 minutes (225 * 4s) store.pollRun(newLatestRunId, gitHubRepoContext, 4000, 225); - break; + return true; } } catch (e) { log(`Error checking for new run: ${(e as Error).message}`); } } + return false; } ); + if (refreshed) { + return; + } } return vscode.commands.executeCommand("github-actions.explorer.refresh"); diff --git a/src/store/polling.test.ts b/src/store/polling.test.ts new file mode 100644 index 00000000..f4c0962b --- /dev/null +++ b/src/store/polling.test.ts @@ -0,0 +1,263 @@ +import {jest} from "@jest/globals"; +import {Octokit} from "@octokit/rest"; +import {setImmediate as nextTurn} from "node:timers"; +import type * as vscode from "vscode"; +import type {GitHubRepoContext} from "../git/repository"; +import type {WorkflowRun as RunData} from "../model"; +import type {WorkflowRunNode} from "../treeViews/shared/workflowRunNode"; + +class TestUri { + constructor(public fsPath: string) {} + + static file(path: string) { + return new TestUri(path); + } +} + +const executeCommand = jest.fn<(command: string) => Promise>(); +const registerCommand = + jest.fn<(command: string, callback: (uri: vscode.Uri) => Promise) => vscode.Disposable>(); +const showInputBox = jest.fn<() => Promise>(); +const log = jest.fn(); +let repoContext: GitHubRepoContext; + +// Jest 29.0's ESM resolver also needs the virtual module registered in its CommonJS map. +jest.mock("vscode", () => ({}), {virtual: true}); +jest.unstable_mockModule( + "vscode", + () => ({ + Uri: TestUri, + EventEmitter: class { + event = jest.fn<(listener: unknown) => void>(); + fire = jest.fn<(event: unknown) => void>(); + }, + commands: {executeCommand, registerCommand}, + workspace: { + getWorkspaceFolder: () => ({uri: TestUri.file("/repo")}), + asRelativePath: () => ".github/workflows/build.yml" + }, + window: { + showInputBox, + setStatusBarMessage: jest.fn(), + withProgress: async (_options: unknown, task: () => Promise) => task() + }, + ProgressLocation: {Window: 10} + }), + {virtual: true} +); +jest.unstable_mockModule("../log", () => ({log, logDebug: jest.fn()})); +jest.unstable_mockModule("timers", () => ({ + setInterval: (callback: () => void, intervalMs: number) => setInterval(callback, intervalMs) +})); +jest.unstable_mockModule("../git/repository", () => ({ + getGitHead: () => Promise.resolve("refs/heads/main"), + getGitHubContextForWorkspaceUri: () => Promise.resolve(repoContext) +})); +jest.unstable_mockModule("../workflow/workflow", () => ({ + parseWorkflowFile: () => Promise.resolve({events: {workflow_dispatch: {}}}), + getWorkflowUri: () => TestUri.file("/repo/.github/workflows/build.yml") +})); +jest.unstable_mockModule("./workflowRun", () => ({ + WorkflowRun: class { + constructor(_context: GitHubRepoContext, public run: RunData) {} + + updateRun(run: RunData) { + this.run = run; + } + } +})); +jest.unstable_mockModule("../treeViews/shared/workflowRunNode", () => ({ + WorkflowRunNode: class { + updateRun = jest.fn(); + } +})); + +const {RunStore} = await import("./store"); +const {WorkflowRunTreeDataProvider} = await import("../treeViews/workflowRunTreeDataProvider"); +const {registerTriggerWorkflowRun} = await import("../commands/triggerWorkflowRun"); +const {Uri} = await import("vscode"); + +class TestProvider extends WorkflowRunTreeDataProvider { + refresh(run: RunData) { + return this.runNodes(repoContext, [run]); + } + + protected _updateNode = jest.fn<(node: WorkflowRunNode) => void>(); +} + +const runData = {id: 42, status: "in_progress", updated_at: "2025-01-01T00:00:00Z"} as RunData; + +beforeEach(() => { + jest.useFakeTimers(); + jest.clearAllMocks(); + repoContext = { + client: new Octokit(), + repositoryState: undefined, + workspaceUri: Uri.file("/repo"), + id: 1, + owner: "owner", + name: "repo", + organizationOwned: false, + defaultBranch: "main", + permissionLevel: "write" + }; + executeCommand.mockResolvedValue(undefined); + showInputBox.mockResolvedValue("main"); +}); + +afterEach(() => { + jest.clearAllTimers(); + jest.useRealTimers(); +}); + +function mockRunFetch(run: RunData = runData) { + return jest.spyOn(repoContext.client.actions, "getWorkflowRun").mockResolvedValue({ + data: run, + status: 200, + headers: {}, + url: "https://api.github.com/repos/owner/repo/actions/runs/42" + }); +} + +async function advanceTime(ms: number) { + await new Promise(resolve => nextTurn(resolve)); + for (let elapsed = 0; elapsed < ms; elapsed += 1000) { + jest.advanceTimersByTime(Math.min(1000, ms - elapsed)); + await new Promise(resolve => nextTurn(resolve)); + } +} + +describe("active run polling", () => { + it("preserves the timer deadline when tree nodes refresh", async () => { + const fetch = mockRunFetch(); + const provider = new TestProvider(new RunStore()); + provider.refresh(runData); + await advanceTime(3000); + provider.refresh(runData); + await advanceTime(1000); + expect(fetch).toHaveBeenCalledTimes(1); + expect(jest.getTimerCount()).toBe(1); + }); + + it("does not replenish the 225-attempt budget on tree refresh", async () => { + const fetch = mockRunFetch(); + const provider = new TestProvider(new RunStore()); + provider.refresh(runData); + await advanceTime(4000); + provider.refresh(runData); + await advanceTime(4000 * 224); + expect(fetch).toHaveBeenCalledTimes(225); + expect(jest.getTimerCount()).toBe(0); + await advanceTime(4000); + expect(fetch).toHaveBeenCalledTimes(225); + }); + + it("preserves explicit restart behavior for rerun and cancel commands", async () => { + const fetch = mockRunFetch(); + const store = new RunStore(); + const provider = new TestProvider(store); + provider.refresh(runData); + await advanceTime(3000); + store.pollRun(runData.id, repoContext, 1000, 2); + provider.refresh(runData); + await advanceTime(2000); + expect(fetch).toHaveBeenCalledTimes(2); + expect(jest.getTimerCount()).toBe(0); + }); + + it.each(["focus", "visibility"])("pauses without consuming attempts when %s is lost", async state => { + const fetch = mockRunFetch(); + const store = new RunStore(); + const setActive = (active: boolean) => { + if (state === "focus") store.setFocused(active); + else store.setViewVisible(active); + }; + store.pollRun(runData.id, repoContext, 1000, 2); + setActive(false); + await advanceTime(10000); + expect(fetch).not.toHaveBeenCalled(); + expect(jest.getTimerCount()).toBe(1); + setActive(true); + await advanceTime(2000); + expect(fetch).toHaveBeenCalledTimes(2); + expect(jest.getTimerCount()).toBe(0); + }); + + it("stops polling when the run completes", async () => { + const fetch = mockRunFetch({...runData, status: "completed"}); + new TestProvider(new RunStore()).refresh(runData); + await advanceTime(8000); + expect(fetch).toHaveBeenCalledTimes(1); + expect(jest.getTimerCount()).toBe(0); + }); +}); + +describe("triggered workflow refresh", () => { + function setupTrigger() { + const listRuns = jest.spyOn(repoContext.client.actions, "listWorkflowRuns").mockResolvedValue({ + data: {total_count: 1, workflow_runs: [runData]}, + status: 200, + headers: {}, + url: "https://api.github.com/repos/owner/repo/actions/workflows/build.yml/runs" + }); + const dispatch = jest.spyOn(repoContext.client.actions, "createWorkflowDispatch").mockResolvedValue({ + data: undefined as never, + status: 204, + headers: {}, + url: "https://api.github.com/repos/owner/repo/actions/workflows/build.yml/dispatches" + }); + const store = new RunStore(); + const poll = jest.spyOn(store, "pollRun").mockImplementation(() => undefined); + registerCommand.mockReturnValue({dispose: () => undefined}); + const context: Partial = {subscriptions: []}; + registerTriggerWorkflowRun(context as vscode.ExtensionContext, store); + const callback = registerCommand.mock.calls[0][1]; + return {listRuns, dispatch, poll, invoke: () => callback(Uri.file("/repo/.github/workflows/build.yml"))}; + } + + it("refreshes once and polls immediately when a new run is detected", async () => { + const {listRuns, poll, invoke} = setupTrigger(); + listRuns.mockResolvedValueOnce({ + data: {total_count: 0, workflow_runs: []}, + status: 200, + headers: {}, + url: "https://api.github.com/repos/owner/repo/actions/workflows/build.yml/runs" + }); + await invoke(); + expect(listRuns).toHaveBeenCalledTimes(2); + expect(executeCommand).toHaveBeenCalledTimes(1); + expect(executeCommand).toHaveBeenCalledWith("github-actions.explorer.refresh"); + expect(poll).toHaveBeenCalledWith(runData.id, repoContext, 4000, 225); + expect(jest.getTimerCount()).toBe(0); + }); + + it("keeps the fallback refresh when discovery times out", async () => { + const {listRuns, poll, invoke} = setupTrigger(); + const pending = invoke(); + await advanceTime(19000); + await pending; + expect(listRuns).toHaveBeenCalledTimes(21); + expect(executeCommand).toHaveBeenCalledTimes(1); + expect(poll).not.toHaveBeenCalled(); + }); + + it("keeps the fallback refresh when discovery requests fail", async () => { + const {listRuns, poll, invoke} = setupTrigger(); + listRuns.mockRejectedValue(new Error("API unavailable")); + const pending = invoke(); + await advanceTime(19000); + await pending; + expect(log).toHaveBeenCalledWith("Error checking for new run: API unavailable"); + expect(executeCommand).toHaveBeenCalledTimes(1); + expect(poll).not.toHaveBeenCalled(); + }); + + it("does not poll when dispatch is cancelled", async () => { + const {dispatch, poll, invoke} = setupTrigger(); + showInputBox.mockResolvedValue(undefined); + await invoke(); + expect(dispatch).not.toHaveBeenCalled(); + expect(poll).not.toHaveBeenCalled(); + expect(executeCommand).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/store/store.ts b/src/store/store.ts index ea025703..244b1877 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -55,11 +55,16 @@ export class RunStore extends EventEmitter { } /** - * Start polling for updates for the given run + * Start polling for updates for the given run. + * Set restartExisting to false during tree refreshes to preserve the current polling budget. */ - pollRun(runId: number, repoContext: GitHubRepoContext, intervalMs: number, attempts = 10) { - log(`Starting polling for run ${runId} every ${intervalMs}ms for ${attempts} attempts`); + pollRun(runId: number, repoContext: GitHubRepoContext, intervalMs: number, attempts = 10, restartExisting = true) { const existingUpdater: Updater | undefined = this.updaters.get(runId); + if (existingUpdater && !restartExisting) { + return; + } + + log(`Starting polling for run ${runId} every ${intervalMs}ms for ${attempts} attempts`); if (existingUpdater && existingUpdater.handle) { clearInterval(existingUpdater.handle); } diff --git a/src/treeViews/workflowRunTreeDataProvider.ts b/src/treeViews/workflowRunTreeDataProvider.ts index 9627c9f9..6606f76e 100644 --- a/src/treeViews/workflowRunTreeDataProvider.ts +++ b/src/treeViews/workflowRunTreeDataProvider.ts @@ -33,7 +33,7 @@ export abstract class WorkflowRunTreeDataProvider { workflowRun.run.status === "requested" ) { // Poll every 4 seconds for up to 15 minutes (225 attempts) - this.store.pollRun(workflowRun.run.id, gitHubRepoContext, 4000, 225); + this.store.pollRun(workflowRun.run.id, gitHubRepoContext, 4000, 225, false); } const node = new WorkflowRunNode( From 180e1835fcbe82cc414ea7d61189317f339dd8c5 Mon Sep 17 00:00:00 2001 From: Shayne Boyer <7681382+spboyer@users.noreply.github.com> Date: Mon, 2 Nov 2026 23:24:02 -0500 Subject: [PATCH 18/18] ci: use a supported runner and Node version for build Pin the nondeployment build job to Ubuntu 24.04 for the locked Playwright installer and use maintained Node 24. Preserve browser installation and all existing checks, with an explicit job timeout. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/build.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 93cc2e03..4ee012f4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,12 +12,14 @@ permissions: jobs: build: - runs-on: ubuntu-latest + # Keep the runner compatible with the locked Playwright browser installer. + runs-on: ubuntu-24.04 + timeout-minutes: 30 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 16.x + node-version: 24.x cache: "npm" registry-url: "https://npm.pkg.github.com" - run: npm ci