From af50a47c13e23e0b3c46719dbd92fe00144362a5 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 30 Mar 2026 16:26:58 +0200 Subject: [PATCH] sessions: add rerun action for failed CI checks in PR checks view (#306347) Add a per-check rerun button for failed CI checks in the changes view. Uses the GitHub Actions rerun-failed-jobs API, extracting the workflow run ID from the check's detailsUrl. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../contrib/changes/browser/ciStatusWidget.ts | 15 ++++++++++- .../browser/fetchers/githubPRCIFetcher.ts | 11 ++++++++ .../models/githubPullRequestCIModel.ts | 27 +++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/vs/sessions/contrib/changes/browser/ciStatusWidget.ts b/src/vs/sessions/contrib/changes/browser/ciStatusWidget.ts index f9a2721e6d7..8a839722971 100644 --- a/src/vs/sessions/contrib/changes/browser/ciStatusWidget.ts +++ b/src/vs/sessions/contrib/changes/browser/ciStatusWidget.ts @@ -66,6 +66,7 @@ class CICheckListRenderer implements IListRenderer GitHubPullRequestCIModel | undefined, ) { } renderTemplate(container: HTMLElement): ICICheckTemplateData { @@ -104,6 +105,18 @@ class CICheckListRenderer implements IListRenderer { + await this._getModel()?.rerunFailedCheck(element.check); + }, + ))); + } + if (element.check.detailsUrl) { actions.push(templateData.elementDisposables.add(new Action( 'ci.openOnGitHub', @@ -210,7 +223,7 @@ export class CIStatusWidget extends Disposable { 'CIStatusWidget', listContainer, new CICheckListDelegate(), - [new CICheckListRenderer(this._labels, this._openerService)], + [new CICheckListRenderer(this._labels, this._openerService, () => this._model)], { multipleSelectionSupport: false, openOnSingleClick: false, diff --git a/src/vs/sessions/contrib/github/browser/fetchers/githubPRCIFetcher.ts b/src/vs/sessions/contrib/github/browser/fetchers/githubPRCIFetcher.ts index c212fa79443..3b46c17564a 100644 --- a/src/vs/sessions/contrib/github/browser/fetchers/githubPRCIFetcher.ts +++ b/src/vs/sessions/contrib/github/browser/fetchers/githubPRCIFetcher.ts @@ -68,6 +68,17 @@ export class GitHubPRCIFetcher { return data.check_runs.map(mapCheckRun); } + /** + * Rerun failed jobs in a GitHub Actions workflow run. + */ + async rerunFailedJobs(owner: string, repo: string, runId: number): Promise { + await this._apiClient.request( + 'POST', + `/repos/${e(owner)}/${e(repo)}/actions/runs/${runId}/rerun-failed-jobs`, + 'githubApi.rerunFailedJobs' + ); + } + /** * Get logs/output for a specific check run. * diff --git a/src/vs/sessions/contrib/github/browser/models/githubPullRequestCIModel.ts b/src/vs/sessions/contrib/github/browser/models/githubPullRequestCIModel.ts index 6a1dd490aaf..bd985e085c6 100644 --- a/src/vs/sessions/contrib/github/browser/models/githubPullRequestCIModel.ts +++ b/src/vs/sessions/contrib/github/browser/models/githubPullRequestCIModel.ts @@ -60,6 +60,20 @@ export class GitHubPullRequestCIModel extends Disposable { return this._fetcher.getCheckRunAnnotations(this.owner, this.repo, checkRunId); } + /** + * Rerun a failed check by extracting the workflow run ID from its details URL + * and calling the GitHub Actions rerun-failed-jobs API, then refresh status. + */ + async rerunFailedCheck(check: IGitHubCICheck): Promise { + const runId = parseWorkflowRunId(check.detailsUrl); + if (!runId) { + this._logService.warn(`${LOG_PREFIX} Cannot rerun check "${check.name}": no workflow run ID found in detailsUrl`); + return; + } + await this._fetcher.rerunFailedJobs(this.owner, this.repo, runId); + await this.refresh(); + } + /** * Start periodic polling. Each cycle refreshes CI check data. */ @@ -88,3 +102,16 @@ export class GitHubPullRequestCIModel extends Disposable { super.dispose(); } } + +/** + * Extract the GitHub Actions workflow run ID from a check run's details URL. + * URLs follow the pattern: `https://github.com/{owner}/{repo}/actions/runs/{run_id}/job/{job_id}` + */ +function parseWorkflowRunId(detailsUrl: string | undefined): number | undefined { + if (!detailsUrl) { + return undefined; + } + const match = /\/actions\/runs\/(?\d+)/.exec(detailsUrl); + const runId = match?.groups?.runId; + return runId ? parseInt(runId, 10) : undefined; +}