gitpadi 2.0.6 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitlab/duo/chat-rules.md +40 -0
- package/.gitlab/duo/mr-review-instructions.md +44 -0
- package/.gitlab-ci.yml +136 -0
- package/README.md +585 -57
- package/action.yml +21 -2
- package/dist/applicant-scorer.js +27 -105
- package/dist/cli.js +1082 -36
- package/dist/commands/apply-for-issue.js +396 -0
- package/dist/commands/bounty-hunter.js +441 -0
- package/dist/commands/contribute.js +245 -51
- package/dist/commands/gitlab-issues.js +87 -0
- package/dist/commands/gitlab-mrs.js +163 -0
- package/dist/commands/gitlab-pipelines.js +95 -0
- package/dist/commands/prs.js +3 -3
- package/dist/core/github.js +28 -0
- package/dist/core/gitlab.js +233 -0
- package/dist/gitlab-agents/ci-recovery-agent.js +173 -0
- package/dist/gitlab-agents/contributor-scoring-agent.js +159 -0
- package/dist/gitlab-agents/grade-assignment-agent.js +252 -0
- package/dist/gitlab-agents/mr-review-agent.js +200 -0
- package/dist/gitlab-agents/reminder-agent.js +164 -0
- package/dist/grade-assignment.js +262 -0
- package/dist/remind-contributors.js +127 -0
- package/dist/review-and-merge.js +125 -0
- package/examples/gitpadi.yml +152 -0
- package/package.json +20 -4
- package/src/applicant-scorer.ts +33 -141
- package/src/cli.ts +1119 -35
- package/src/commands/apply-for-issue.ts +452 -0
- package/src/commands/bounty-hunter.ts +529 -0
- package/src/commands/contribute.ts +264 -50
- package/src/commands/gitlab-issues.ts +87 -0
- package/src/commands/gitlab-mrs.ts +185 -0
- package/src/commands/gitlab-pipelines.ts +104 -0
- package/src/commands/prs.ts +3 -3
- package/src/core/github.ts +29 -0
- package/src/core/gitlab.ts +397 -0
- package/src/gitlab-agents/ci-recovery-agent.ts +201 -0
- package/src/gitlab-agents/contributor-scoring-agent.ts +196 -0
- package/src/gitlab-agents/grade-assignment-agent.ts +275 -0
- package/src/gitlab-agents/mr-review-agent.ts +231 -0
- package/src/gitlab-agents/reminder-agent.ts +203 -0
- package/src/grade-assignment.ts +283 -0
- package/src/remind-contributors.ts +159 -0
- package/src/review-and-merge.ts +143 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// commands/gitlab-mrs.ts — GitLab Merge Request management for GitPadi
|
|
2
|
+
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import Table from 'cli-table3';
|
|
6
|
+
import {
|
|
7
|
+
getNamespace, getProject, getFullProject, requireGitLabProject,
|
|
8
|
+
listGitLabMRs, getGitLabMR, createGitLabMR, mergeGitLabMR, updateGitLabMR,
|
|
9
|
+
listGitLabMRChanges, listGitLabMRNotes, createGitLabMRNote, updateGitLabMRNote,
|
|
10
|
+
getGitLabMRPipelineStatus, withGitLabRetry,
|
|
11
|
+
} from '../core/gitlab.js';
|
|
12
|
+
|
|
13
|
+
export async function listMRs(opts: { state?: string; limit?: number } = {}) {
|
|
14
|
+
requireGitLabProject();
|
|
15
|
+
const spinner = ora(`Fetching MRs from ${chalk.cyan(getFullProject())}...`).start();
|
|
16
|
+
try {
|
|
17
|
+
const state = (['opened', 'closed', 'merged', 'all'].includes(opts.state || '')
|
|
18
|
+
? opts.state : 'opened') as 'opened' | 'closed' | 'merged' | 'all';
|
|
19
|
+
|
|
20
|
+
const mrs = await withGitLabRetry(() =>
|
|
21
|
+
listGitLabMRs(getNamespace(), getProject(), { state, per_page: opts.limit || 50 })
|
|
22
|
+
);
|
|
23
|
+
spinner.stop();
|
|
24
|
+
|
|
25
|
+
if (!mrs.length) { console.log(chalk.yellow('\n No merge requests found.\n')); return; }
|
|
26
|
+
|
|
27
|
+
const table = new Table({
|
|
28
|
+
head: ['#', 'Title', 'Author', 'Branch', 'State'].map(h => chalk.cyan(h)),
|
|
29
|
+
style: { head: [], border: [] },
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
mrs.forEach(mr => {
|
|
33
|
+
const stateLabel = mr.state === 'opened' ? chalk.green('open')
|
|
34
|
+
: mr.state === 'merged' ? chalk.magenta('merged') : chalk.red(mr.state);
|
|
35
|
+
table.push([
|
|
36
|
+
`!${mr.iid}`,
|
|
37
|
+
mr.title.substring(0, 50),
|
|
38
|
+
`@${mr.author.username}`,
|
|
39
|
+
mr.source_branch.substring(0, 25),
|
|
40
|
+
stateLabel,
|
|
41
|
+
]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
console.log(`\n${chalk.bold(`🔀 Merge Requests — ${getFullProject()}`)} (${mrs.length})\n`);
|
|
45
|
+
console.log(table.toString());
|
|
46
|
+
console.log('');
|
|
47
|
+
} catch (e: any) { spinner.fail(e.message); }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function mergeMR(iid: number, opts: { squash?: boolean; message?: string; force?: boolean } = {}) {
|
|
51
|
+
requireGitLabProject();
|
|
52
|
+
|
|
53
|
+
if (opts.force) {
|
|
54
|
+
const spinner = ora(`Force merging MR !${iid}...`).start();
|
|
55
|
+
try {
|
|
56
|
+
const result = await withGitLabRetry(() =>
|
|
57
|
+
mergeGitLabMR(getNamespace(), getProject(), iid, { squash: opts.squash ?? true, message: opts.message })
|
|
58
|
+
);
|
|
59
|
+
spinner.succeed(`Force merged MR ${chalk.green(`!${iid}`)} ${chalk.yellow('(CI skipped)')}`);
|
|
60
|
+
console.log(chalk.dim(` SHA: ${result.sha}\n`));
|
|
61
|
+
} catch (e: any) { spinner.fail(e.message); }
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const spinner = ora(`Checking CI status for MR !${iid}...`).start();
|
|
66
|
+
try {
|
|
67
|
+
// Poll up to 15 min (90 × 10s)
|
|
68
|
+
let attempts = 0;
|
|
69
|
+
const maxAttempts = 90;
|
|
70
|
+
let ciStatus: string | null = null;
|
|
71
|
+
|
|
72
|
+
while (attempts < maxAttempts) {
|
|
73
|
+
const pipeline = await withGitLabRetry(() => getGitLabMRPipelineStatus(getNamespace(), getProject(), iid));
|
|
74
|
+
|
|
75
|
+
if (!pipeline) {
|
|
76
|
+
spinner.info(chalk.dim('No CI pipeline found — proceeding without check.'));
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
ciStatus = pipeline.status;
|
|
81
|
+
|
|
82
|
+
if (['success', 'failed', 'canceled', 'skipped'].includes(ciStatus)) break;
|
|
83
|
+
|
|
84
|
+
spinner.text = chalk.dim(`Pipeline ${ciStatus}... (${attempts * 10}s elapsed)`);
|
|
85
|
+
await new Promise(r => setTimeout(r, 10000));
|
|
86
|
+
attempts++;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
spinner.stop();
|
|
90
|
+
|
|
91
|
+
if (ciStatus === 'failed' || ciStatus === 'canceled') {
|
|
92
|
+
console.log(chalk.red(` ❌ Pipeline ${ciStatus} — merge blocked. Fix the pipeline and retry.\n`));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (attempts >= maxAttempts) {
|
|
96
|
+
console.log(chalk.yellow(` ⚠️ Pipeline still running after 15 min. Try again later.\n`));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (ciStatus === 'success') {
|
|
100
|
+
console.log(chalk.green(` ✅ Pipeline passed!\n`));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const mergeSpinner = ora(`Merging MR !${iid}...`).start();
|
|
104
|
+
const result = await withGitLabRetry(() =>
|
|
105
|
+
mergeGitLabMR(getNamespace(), getProject(), iid, { squash: opts.squash ?? true, message: opts.message })
|
|
106
|
+
);
|
|
107
|
+
mergeSpinner.succeed(`Merged MR ${chalk.green(`!${iid}`)}`);
|
|
108
|
+
console.log(chalk.dim(` SHA: ${result.sha}\n`));
|
|
109
|
+
} catch (e: any) { spinner.fail(e.message); }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function closeMR(iid: number) {
|
|
113
|
+
requireGitLabProject();
|
|
114
|
+
const spinner = ora(`Closing MR !${iid}...`).start();
|
|
115
|
+
try {
|
|
116
|
+
await withGitLabRetry(() => updateGitLabMR(getNamespace(), getProject(), iid, { state_event: 'close' }));
|
|
117
|
+
spinner.succeed(`Closed MR ${chalk.red(`!${iid}`)}`);
|
|
118
|
+
} catch (e: any) { spinner.fail(e.message); }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function reviewMR(iid: number) {
|
|
122
|
+
requireGitLabProject();
|
|
123
|
+
const spinner = ora(`Reviewing MR !${iid}...`).start();
|
|
124
|
+
try {
|
|
125
|
+
const mr = await withGitLabRetry(() => getGitLabMR(getNamespace(), getProject(), iid));
|
|
126
|
+
const changes = await withGitLabRetry(() => listGitLabMRChanges(getNamespace(), getProject(), iid));
|
|
127
|
+
spinner.stop();
|
|
128
|
+
|
|
129
|
+
console.log(`\n${chalk.bold(`🔍 MR Review — !${iid}: ${mr.title}`)}`);
|
|
130
|
+
console.log(chalk.dim(` Author: @${mr.author.username} Branch: ${mr.source_branch} → ${mr.target_branch}\n`));
|
|
131
|
+
|
|
132
|
+
const checks: Array<{ name: string; icon: string; detail: string }> = [];
|
|
133
|
+
|
|
134
|
+
// Linked issues
|
|
135
|
+
const linked = mr.description?.match(/(fix(es|ed)?|clos(e[sd]?)|resolv(e[sd]?))\s+#\d+/gi) || [];
|
|
136
|
+
checks.push({ name: 'Linked Issues', icon: linked.length ? '✅' : '⚠️', detail: linked.length ? linked.join(', ') : 'None found — use "Fixes #N"' });
|
|
137
|
+
|
|
138
|
+
// Size
|
|
139
|
+
const totalLines = changes.reduce((sum, f) => {
|
|
140
|
+
const adds = (f.diff.match(/^\+/gm) || []).length;
|
|
141
|
+
const dels = (f.diff.match(/^-/gm) || []).length;
|
|
142
|
+
return sum + adds + dels;
|
|
143
|
+
}, 0);
|
|
144
|
+
checks.push({ name: 'MR Size', icon: totalLines > 1000 ? '❌' : totalLines > 500 ? '⚠️' : '✅', detail: `~${totalLines} lines changed (${changes.length} files)` });
|
|
145
|
+
|
|
146
|
+
// Tests
|
|
147
|
+
const srcFiles = changes.filter(f => !f.new_path.includes('test') && !f.new_path.includes('spec') && /\.(ts|rs|js|py)$/.test(f.new_path));
|
|
148
|
+
const testFiles = changes.filter(f => f.new_path.includes('test') || f.new_path.includes('spec'));
|
|
149
|
+
checks.push({ name: 'Tests', icon: srcFiles.length > 0 && testFiles.length === 0 ? '⚠️' : '✅', detail: `${testFiles.length} test file(s), ${srcFiles.length} source file(s)` });
|
|
150
|
+
|
|
151
|
+
// Sensitive files
|
|
152
|
+
const sensitive = changes.filter(f => /(\.env|secret|credential|password|\.key|\.pem)/i.test(f.new_path));
|
|
153
|
+
checks.push({ name: 'Security', icon: sensitive.length ? '❌' : '✅', detail: sensitive.length ? `Flagged: ${sensitive.map(f => f.new_path).join(', ')}` : 'Clean' });
|
|
154
|
+
|
|
155
|
+
// Draft
|
|
156
|
+
checks.push({ name: 'Draft Status', icon: mr.draft ? '⚠️' : '✅', detail: mr.draft ? 'MR is a draft — not ready to merge' : 'Ready for review' });
|
|
157
|
+
|
|
158
|
+
checks.forEach(c => console.log(` ${c.icon} ${chalk.bold(c.name)}: ${c.detail}`));
|
|
159
|
+
console.log('');
|
|
160
|
+
} catch (e: any) { spinner.fail(e.message); }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function diffMR(iid: number) {
|
|
164
|
+
requireGitLabProject();
|
|
165
|
+
const spinner = ora(`Fetching diff for MR !${iid}...`).start();
|
|
166
|
+
try {
|
|
167
|
+
const changes = await withGitLabRetry(() => listGitLabMRChanges(getNamespace(), getProject(), iid));
|
|
168
|
+
spinner.stop();
|
|
169
|
+
|
|
170
|
+
console.log(`\n${chalk.bold(`📄 MR !${iid} — Changed Files`)} (${changes.length})\n`);
|
|
171
|
+
|
|
172
|
+
const table = new Table({
|
|
173
|
+
head: ['File', 'Status'].map(h => chalk.cyan(h)),
|
|
174
|
+
style: { head: [], border: [] },
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
changes.forEach(f => {
|
|
178
|
+
const status = f.new_file ? chalk.green('added') : f.deleted_file ? chalk.red('removed') : chalk.yellow('modified');
|
|
179
|
+
table.push([f.new_path.substring(0, 70), status]);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
console.log(table.toString());
|
|
183
|
+
console.log('');
|
|
184
|
+
} catch (e: any) { spinner.fail(e.message); }
|
|
185
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// commands/gitlab-pipelines.ts — GitLab CI/CD pipeline management for GitPadi
|
|
2
|
+
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import Table from 'cli-table3';
|
|
6
|
+
import {
|
|
7
|
+
getNamespace, getProject, getFullProject, requireGitLabProject,
|
|
8
|
+
listGitLabPipelines, getGitLabPipeline, listGitLabPipelineJobs, getGitLabJobLog,
|
|
9
|
+
withGitLabRetry,
|
|
10
|
+
} from '../core/gitlab.js';
|
|
11
|
+
|
|
12
|
+
function pipelineStatusColor(status: string): string {
|
|
13
|
+
switch (status) {
|
|
14
|
+
case 'success': return chalk.green(status);
|
|
15
|
+
case 'failed': return chalk.red(status);
|
|
16
|
+
case 'running': return chalk.yellow(status);
|
|
17
|
+
case 'canceled': return chalk.dim(status);
|
|
18
|
+
case 'pending': return chalk.blue(status);
|
|
19
|
+
default: return chalk.dim(status);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function listPipelines(opts: { ref?: string; limit?: number } = {}) {
|
|
24
|
+
requireGitLabProject();
|
|
25
|
+
const spinner = ora(`Fetching pipelines from ${chalk.cyan(getFullProject())}...`).start();
|
|
26
|
+
try {
|
|
27
|
+
const pipelines = await withGitLabRetry(() =>
|
|
28
|
+
listGitLabPipelines(getNamespace(), getProject(), { per_page: opts.limit || 20, ref: opts.ref })
|
|
29
|
+
);
|
|
30
|
+
spinner.stop();
|
|
31
|
+
|
|
32
|
+
if (!pipelines.length) { console.log(chalk.yellow('\n No pipelines found.\n')); return; }
|
|
33
|
+
|
|
34
|
+
const table = new Table({
|
|
35
|
+
head: ['ID', 'Status', 'Ref', 'SHA', 'Created'].map(h => chalk.cyan(h)),
|
|
36
|
+
style: { head: [], border: [] },
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
pipelines.forEach(p => {
|
|
40
|
+
table.push([
|
|
41
|
+
`#${p.id}`,
|
|
42
|
+
pipelineStatusColor(p.status),
|
|
43
|
+
p.ref.substring(0, 25),
|
|
44
|
+
p.sha.substring(0, 8),
|
|
45
|
+
new Date(p.created_at).toLocaleDateString(),
|
|
46
|
+
]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
console.log(`\n${chalk.bold(`🔧 Pipelines — ${getFullProject()}`)} (${pipelines.length})\n`);
|
|
50
|
+
console.log(table.toString());
|
|
51
|
+
console.log('');
|
|
52
|
+
} catch (e: any) { spinner.fail(e.message); }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function viewPipelineJobs(pipelineId: number) {
|
|
56
|
+
requireGitLabProject();
|
|
57
|
+
const spinner = ora(`Fetching jobs for pipeline #${pipelineId}...`).start();
|
|
58
|
+
try {
|
|
59
|
+
const [pipeline, jobs] = await Promise.all([
|
|
60
|
+
withGitLabRetry(() => getGitLabPipeline(getNamespace(), getProject(), pipelineId)),
|
|
61
|
+
withGitLabRetry(() => listGitLabPipelineJobs(getNamespace(), getProject(), pipelineId)),
|
|
62
|
+
]);
|
|
63
|
+
spinner.stop();
|
|
64
|
+
|
|
65
|
+
console.log(`\n${chalk.bold(`🔧 Pipeline #${pipelineId}`)} — ${pipelineStatusColor(pipeline.status)} — ${pipeline.ref}\n`);
|
|
66
|
+
|
|
67
|
+
const table = new Table({
|
|
68
|
+
head: ['Job', 'Stage', 'Status', 'Duration'].map(h => chalk.cyan(h)),
|
|
69
|
+
style: { head: [], border: [] },
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
jobs.forEach(job => {
|
|
73
|
+
const duration = job.duration ? `${Math.round(job.duration)}s` : '-';
|
|
74
|
+
table.push([job.name, job.stage, pipelineStatusColor(job.status), duration]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
console.log(table.toString());
|
|
78
|
+
console.log('');
|
|
79
|
+
|
|
80
|
+
const failed = jobs.filter(j => j.status === 'failed');
|
|
81
|
+
if (failed.length) {
|
|
82
|
+
console.log(chalk.red(` ❌ ${failed.length} failed job(s): ${failed.map(j => j.name).join(', ')}\n`));
|
|
83
|
+
}
|
|
84
|
+
} catch (e: any) { spinner.fail(e.message); }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function viewJobLog(jobId: number) {
|
|
88
|
+
requireGitLabProject();
|
|
89
|
+
const spinner = ora(`Fetching log for job #${jobId}...`).start();
|
|
90
|
+
try {
|
|
91
|
+
const log = await withGitLabRetry(() => getGitLabJobLog(getNamespace(), getProject(), jobId));
|
|
92
|
+
spinner.stop();
|
|
93
|
+
|
|
94
|
+
console.log(`\n${chalk.bold(`📋 Job #${jobId} Log`)}\n`);
|
|
95
|
+
// Show last 100 lines to avoid overwhelming the terminal
|
|
96
|
+
const lines = log.split('\n');
|
|
97
|
+
const tail = lines.slice(-100).join('\n');
|
|
98
|
+
if (lines.length > 100) {
|
|
99
|
+
console.log(chalk.dim(` ... (showing last 100 of ${lines.length} lines)\n`));
|
|
100
|
+
}
|
|
101
|
+
console.log(chalk.dim(tail));
|
|
102
|
+
console.log('');
|
|
103
|
+
} catch (e: any) { spinner.fail(e.message); }
|
|
104
|
+
}
|
package/src/commands/prs.ts
CHANGED
|
@@ -60,9 +60,9 @@ export async function mergePR(number: number, opts: { method?: string; message?:
|
|
|
60
60
|
const { data: pr } = await octokit.pulls.get({ owner: getOwner(), repo: getRepo(), pull_number: number });
|
|
61
61
|
const sha = pr.head.sha;
|
|
62
62
|
|
|
63
|
-
// Step 2: Poll until all checks complete (max
|
|
63
|
+
// Step 2: Poll until all checks complete (max 15 min)
|
|
64
64
|
let attempts = 0;
|
|
65
|
-
const maxAttempts =
|
|
65
|
+
const maxAttempts = 90;
|
|
66
66
|
let allComplete = false;
|
|
67
67
|
let ciChecks: Array<{ name: string; status: string; conclusion: string | null }> = [];
|
|
68
68
|
|
|
@@ -122,7 +122,7 @@ export async function mergePR(number: number, opts: { method?: string; message?:
|
|
|
122
122
|
console.log('');
|
|
123
123
|
|
|
124
124
|
if (!allComplete) {
|
|
125
|
-
console.log(chalk.yellow(` ⚠️ Checks still running after
|
|
125
|
+
console.log(chalk.yellow(` ⚠️ Checks still running after 15 min. Try again later.\n`));
|
|
126
126
|
return;
|
|
127
127
|
}
|
|
128
128
|
if (!allPassed) {
|
package/src/core/github.ts
CHANGED
|
@@ -114,6 +114,35 @@ export async function getRepoDetails(owner: string, repo: string) {
|
|
|
114
114
|
return data;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
export async function getRepoPermissions(owner: string, repo: string) {
|
|
118
|
+
const data = await getRepoDetails(owner, repo);
|
|
119
|
+
return data.permissions || { admin: false, push: false, pull: true };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Retries an async operation on GitHub rate-limit (429) or secondary rate-limit (403)
|
|
124
|
+
* with exponential backoff. maxRetries defaults to 4 (covers ~30s total wait).
|
|
125
|
+
*/
|
|
126
|
+
export async function withRetry<T>(fn: () => Promise<T>, maxRetries = 4): Promise<T> {
|
|
127
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
128
|
+
try {
|
|
129
|
+
return await fn();
|
|
130
|
+
} catch (e: any) {
|
|
131
|
+
const isRateLimit = e.status === 429 || (e.status === 403 && /rate limit/i.test(e.message || ''));
|
|
132
|
+
if (isRateLimit && attempt < maxRetries) {
|
|
133
|
+
// Honour Retry-After header when present, else exponential backoff
|
|
134
|
+
const retryAfter = parseInt(e.response?.headers?.['retry-after'] || '0', 10);
|
|
135
|
+
const delay = retryAfter > 0 ? retryAfter * 1000 : Math.pow(2, attempt) * 1000 + Math.random() * 500;
|
|
136
|
+
await new Promise(r => setTimeout(r, delay));
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
throw e;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Unreachable — satisfies TS return type
|
|
143
|
+
throw new Error('withRetry: max retries exceeded');
|
|
144
|
+
}
|
|
145
|
+
|
|
117
146
|
export async function getLatestCheckRuns(owner: string, repo: string, ref: string) {
|
|
118
147
|
const octokit = getOctokit();
|
|
119
148
|
const { data: checks } = await octokit.checks.listForRef({ owner, repo, ref });
|