Skip to content

Commit 868927d

Browse files
committed
Add previous attempts for workflow runs
1 parent ac3a5fd commit 868927d

File tree

7 files changed

+152
-10
lines changed

7 files changed

+152
-10
lines changed

src/model.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {components} from "@octokit/openapi-types";
12
import {RestEndpointMethods} from "@octokit/plugin-rest-endpoint-methods/dist-types/generated/method-types";
23

34
// Type helpers
@@ -20,11 +21,12 @@ type OctokitRepoData<
2021
> = GetElementType<Await<ReturnType<RestEndpointMethods["repos"][Operation]>>["data"][ResultProperty]>;
2122

2223
//
23-
// Domain types
24+
// Domain contracts
2425
//
2526

2627
export type Workflow = OctokitData<"listRepoWorkflows", "workflows">;
27-
export type WorkflowRun = OctokitData<"listWorkflowRuns", "workflow_runs">;
28+
export type WorkflowRun = components["schemas"]["workflow-run"];
29+
export type WorkflowRunAttempt = WorkflowRun;
2830

2931
export type WorkflowJob = OctokitData<"listJobsForWorkflowRun", "jobs">;
3032

src/store/workflowRun.ts

Lines changed: 85 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import {GitHubRepoContext} from "../git/repository";
2-
import {logDebug} from "../log";
2+
import {log, logDebug} from "../log";
33
import * as model from "../model";
44
import {WorkflowJob} from "./WorkflowJob";
55

6-
export class WorkflowRun {
7-
private _gitHubRepoContext: GitHubRepoContext;
8-
private _run: model.WorkflowRun;
6+
abstract class WorkflowRunBase {
7+
protected _gitHubRepoContext: GitHubRepoContext;
8+
protected _run: model.WorkflowRun;
9+
910
private _jobs: Promise<WorkflowJob[]> | undefined;
1011

1112
constructor(gitHubRepoContext: GitHubRepoContext, run: model.WorkflowRun) {
@@ -17,6 +18,10 @@ export class WorkflowRun {
1718
return this._run;
1819
}
1920

21+
get hasPreviousAttempts(): boolean {
22+
return (this.run.run_attempt || 1) > 1;
23+
}
24+
2025
updateRun(run: model.WorkflowRun) {
2126
if (this._run.updated_at !== run.updated_at) {
2227
// Run has changed, reset jobs. Note: this doesn't work in all cases, there might be race conditions
@@ -37,7 +42,17 @@ export class WorkflowRun {
3742
return this._jobs;
3843
}
3944

40-
private async fetchJobs(): Promise<WorkflowJob[]> {
45+
protected abstract fetchJobs(): Promise<WorkflowJob[]>;
46+
}
47+
48+
export class WorkflowRun extends WorkflowRunBase {
49+
private _attempts: Promise<WorkflowRunAttempt[]> | undefined;
50+
51+
constructor(gitHubRepoContext: GitHubRepoContext, run: model.WorkflowRun) {
52+
super(gitHubRepoContext, run);
53+
}
54+
55+
override async fetchJobs(): Promise<WorkflowJob[]> {
4156
logDebug("Getting workflow jobs");
4257

4358
const result = await this._gitHubRepoContext.client.actions.listJobsForWorkflowRun({
@@ -50,4 +65,69 @@ export class WorkflowRun {
5065
const jobs: model.WorkflowJob[] = resp.jobs;
5166
return jobs.map(j => new WorkflowJob(this._gitHubRepoContext, j));
5267
}
68+
69+
attempts(): Promise<WorkflowRunAttempt[]> {
70+
if (!this._attempts) {
71+
this._attempts = this._updateAttempts();
72+
}
73+
74+
return this._attempts;
75+
}
76+
77+
private async _updateAttempts(): Promise<WorkflowRunAttempt[]> {
78+
const attempts: WorkflowRunAttempt[] = [];
79+
80+
const attempt = this.run.run_attempt || 1;
81+
if (attempt > 1) {
82+
for (let i = 1; i < attempt; i++) {
83+
const runAttemptResp = await this._gitHubRepoContext.client.actions.getWorkflowRunAttempt({
84+
owner: this._gitHubRepoContext.owner,
85+
repo: this._gitHubRepoContext.name,
86+
run_id: this._run.id,
87+
attempt_number: i
88+
});
89+
if (runAttemptResp.status !== 200) {
90+
log(
91+
"Failed to get workflow run attempt",
92+
this._run.id,
93+
"for attempt",
94+
i,
95+
runAttemptResp.status,
96+
runAttemptResp.data
97+
);
98+
continue;
99+
}
100+
101+
const runAttempt = runAttemptResp.data;
102+
attempts.push(new WorkflowRunAttempt(this._gitHubRepoContext, runAttempt, i));
103+
}
104+
}
105+
106+
return attempts;
107+
}
108+
}
109+
110+
export class WorkflowRunAttempt extends WorkflowRunBase {
111+
public readonly attempt: number;
112+
113+
constructor(gitHubRepoContext: GitHubRepoContext, run: model.WorkflowRunAttempt, attempt: number) {
114+
super(gitHubRepoContext, run);
115+
116+
this.attempt = attempt;
117+
}
118+
119+
override async fetchJobs(): Promise<WorkflowJob[]> {
120+
logDebug("Getting workflow run attempt jobs", this._run.id, "for attempt", this.attempt);
121+
122+
const result = await this._gitHubRepoContext.client.actions.listJobsForWorkflowRunAttempt({
123+
owner: this._gitHubRepoContext.owner,
124+
repo: this._gitHubRepoContext.name,
125+
run_id: this._run.id,
126+
attempt_number: this.attempt
127+
});
128+
129+
const resp = result.data;
130+
const jobs: model.WorkflowJob[] = resp.jobs;
131+
return jobs.map(j => new WorkflowJob(this._gitHubRepoContext, j));
132+
}
53133
}

src/treeViews/currentBranch.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import {CurrentBranchRepoNode} from "./current-branch/currentBranchRepoNode";
66
import {log, logDebug} from "../log";
77
import {RunStore} from "../store/store";
88
import {NoRunForBranchNode} from "./current-branch/noRunForBranchNode";
9+
import {AttemptNode} from "./shared/attemptNode";
910
import {NoWorkflowJobsNode} from "./shared/noWorkflowJobsNode";
11+
import {PreviousAttemptsNode} from "./shared/previousAttemptsNode";
1012
import {WorkflowJobNode} from "./shared/workflowJobNode";
1113
import {WorkflowRunNode} from "./shared/workflowRunNode";
1214
import {WorkflowRunTreeDataProvider} from "./workflowRunTreeDataProvider";
@@ -15,6 +17,8 @@ import {WorkflowStepNode} from "./workflows/workflowStepNode";
1517
type CurrentBranchTreeNode =
1618
| CurrentBranchRepoNode
1719
| WorkflowRunNode
20+
| PreviousAttemptsNode
21+
| AttemptNode
1822
| WorkflowJobNode
1923
| NoWorkflowJobsNode
2024
| WorkflowStepNode
@@ -78,6 +82,10 @@ export class CurrentBranchTreeProvider
7882
return this.getRuns(element.gitHubRepoContext, element.currentBranchName);
7983
} else if (element instanceof WorkflowRunNode) {
8084
return element.getJobs();
85+
} else if (element instanceof PreviousAttemptsNode) {
86+
return element.getAttempts();
87+
} else if (element instanceof AttemptNode) {
88+
return element.getJobs();
8189
} else if (element instanceof WorkflowJobNode) {
8290
return element.getSteps();
8391
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import * as vscode from "vscode";
2+
import {GitHubRepoContext} from "../../git/repository";
3+
import {WorkflowRunAttempt} from "../../store/workflowRun";
4+
import {getIconForWorkflowRun} from "../icons";
5+
import {WorkflowJobNode} from "./workflowJobNode";
6+
7+
export class AttemptNode extends vscode.TreeItem {
8+
constructor(private gitHubRepoContext: GitHubRepoContext, private attempt: WorkflowRunAttempt) {
9+
super(`Attempt #${attempt.attempt}`, vscode.TreeItemCollapsibleState.Collapsed);
10+
11+
this.iconPath = getIconForWorkflowRun(this.attempt.run);
12+
this.tooltip = `#${this.attempt.attempt}: ${this.attempt.run.status || ""} ${this.attempt.run.conclusion || ""}`;
13+
}
14+
15+
async getJobs(): Promise<WorkflowJobNode[]> {
16+
const jobs = await this.attempt.jobs();
17+
18+
return jobs.map(job => new WorkflowJobNode(this.gitHubRepoContext, job));
19+
}
20+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import * as vscode from "vscode";
2+
import {GitHubRepoContext} from "../../git/repository";
3+
import {WorkflowRun} from "../../store/workflowRun";
4+
import {AttemptNode} from "./attemptNode";
5+
6+
export class PreviousAttemptsNode extends vscode.TreeItem {
7+
constructor(private gitHubRepoContext: GitHubRepoContext, private run: WorkflowRun) {
8+
super("Previous attempts", vscode.TreeItemCollapsibleState.Collapsed);
9+
}
10+
11+
async getAttempts(): Promise<AttemptNode[]> {
12+
const attempts = await this.run.attempts();
13+
return attempts.map(attempt => new AttemptNode(this.gitHubRepoContext, attempt));
14+
}
15+
}

src/treeViews/shared/workflowRunNode.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {RunStore} from "../../store/store";
55
import {WorkflowRun} from "../../store/workflowRun";
66
import {getIconForWorkflowRun} from "../icons";
77
import {NoWorkflowJobsNode} from "./noWorkflowJobsNode";
8+
import {PreviousAttemptsNode} from "./previousAttemptsNode";
89
import {WorkflowJobNode} from "./workflowJobNode";
910

1011
export type WorkflowRunCommandArgs = Pick<WorkflowRunNode, "gitHubRepoContext" | "run" | "store">;
@@ -42,10 +43,18 @@ export class WorkflowRunNode extends vscode.TreeItem {
4243
this.tooltip = `${this.run.run.status || ""} ${this.run.run.conclusion || ""}`;
4344
}
4445

45-
async getJobs(): Promise<(WorkflowJobNode | NoWorkflowJobsNode)[]> {
46+
async getJobs(): Promise<(WorkflowJobNode | NoWorkflowJobsNode | PreviousAttemptsNode)[]> {
4647
const jobs = await this.run.jobs();
4748

48-
return jobs.map(job => new WorkflowJobNode(this.gitHubRepoContext, job));
49+
const children: (WorkflowJobNode | NoWorkflowJobsNode | PreviousAttemptsNode)[] = jobs.map(
50+
job => new WorkflowJobNode(this.gitHubRepoContext, job)
51+
);
52+
53+
if (this.run.hasPreviousAttempts) {
54+
children.push(new PreviousAttemptsNode(this.gitHubRepoContext, this.run));
55+
}
56+
57+
return children;
4958
}
5059

5160
private static _getLabel(run: WorkflowRun, workflowName?: string): string {

src/treeViews/workflows.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ import {getWorkflowNodes, WorkflowsRepoNode} from "./workflows/workflowsRepoNode
55

66
import {getGitHubContext} from "../git/repository";
77
import {RunStore} from "../store/store";
8+
import {AttemptNode} from "./shared/attemptNode";
89
import {AuthenticationNode} from "./shared/authenticationNode";
910
import {ErrorNode} from "./shared/errorNode";
1011
import {NoGitHubRepositoryNode} from "./shared/noGitHubRepositoryNode";
1112
import {NoWorkflowJobsNode} from "./shared/noWorkflowJobsNode";
13+
import {PreviousAttemptsNode} from "./shared/previousAttemptsNode";
1214
import {WorkflowJobNode} from "./shared/workflowJobNode";
1315
import {WorkflowRunNode} from "./shared/workflowRunNode";
1416
import {WorkflowRunTreeDataProvider} from "./workflowRunTreeDataProvider";
@@ -19,9 +21,11 @@ type WorkflowsTreeNode =
1921
| AuthenticationNode
2022
| NoGitHubRepositoryNode
2123
| WorkflowNode
24+
| WorkflowRunNode
25+
| PreviousAttemptsNode
26+
| AttemptNode
2227
| WorkflowJobNode
2328
| NoWorkflowJobsNode
24-
| WorkflowRunNode
2529
| WorkflowStepNode;
2630

2731
export class WorkflowsTreeProvider
@@ -87,6 +91,10 @@ export class WorkflowsTreeProvider
8791
return this.getRuns(element);
8892
} else if (element instanceof WorkflowRunNode) {
8993
return element.getJobs();
94+
} else if (element instanceof PreviousAttemptsNode) {
95+
return element.getAttempts();
96+
} else if (element instanceof AttemptNode) {
97+
return element.getJobs();
9098
} else if (element instanceof WorkflowJobNode) {
9199
return element.getSteps();
92100
}

0 commit comments

Comments
 (0)