3939 const path = require('path');
4040
4141 const workflowDir = process.env.WORKFLOW_DIR;
42+ const tagShaCache = new Map();
43+ const nonRetryableStatuses = new Set([400, 401, 403, 404, 422]);
44+
45+ const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
46+
47+ async function withRetries(operation, label, maxAttempts = 3) {
48+ let lastErr;
49+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
50+ try {
51+ return await operation();
52+ } catch (err) {
53+ if (nonRetryableStatuses.has(err.status)) {
54+ throw err;
55+ }
56+ lastErr = err;
57+ if (attempt < maxAttempts) {
58+ core.warning(`${label} failed on attempt ${attempt}/${maxAttempts}: ${err.message}; retrying`);
59+ await sleep(250 * attempt);
60+ }
61+ }
62+ }
63+ throw lastErr;
64+ }
4265
4366 // Find all workflow files
4467 const files = fs.readdirSync(workflowDir)
@@ -91,28 +114,44 @@ runs:
91114
92115 // Resolve a tag to its commit SHA, handling annotated tags
93116 async function resolveTagSha(owner, repo, tag) {
117+ const cacheKey = `${owner}/${repo}@${tag}`;
118+ if (tagShaCache.has(cacheKey)) {
119+ return tagShaCache.get(cacheKey);
120+ }
121+
94122 try {
95- const ref = await github.rest.git.getRef({
96- owner,
97- repo,
98- ref: `tags/${tag}`,
99- });
123+ const ref = await withRetries(
124+ () => github.rest.git.getRef({
125+ owner,
126+ repo,
127+ ref: `tags/${tag}`,
128+ }),
129+ `Resolving ${cacheKey}`
130+ );
100131
101132 const obj = ref.data.object;
133+ let sha;
102134
103135 // Lightweight tag — points directly to a commit
104136 if (obj.type === 'commit') {
105- return obj.sha;
137+ sha = obj.sha;
138+ tagShaCache.set(cacheKey, sha);
139+ return sha;
106140 }
107141
108142 // Annotated tag — dereference to get the commit
109143 if (obj.type === 'tag') {
110- const tagObj = await github.rest.git.getTag({
111- owner,
112- repo,
113- tag_sha: obj.sha,
114- });
115- return tagObj.data.object.sha;
144+ const tagObj = await withRetries(
145+ () => github.rest.git.getTag({
146+ owner,
147+ repo,
148+ tag_sha: obj.sha,
149+ }),
150+ `Dereferencing ${cacheKey}`
151+ );
152+ sha = tagObj.data.object.sha;
153+ tagShaCache.set(cacheKey, sha);
154+ return sha;
116155 }
117156
118157 throw new Error(`Unexpected ref object type: ${obj.type}`);
@@ -130,7 +169,10 @@ runs:
130169
131170 // Check if the pinned SHA exists in this repo at all
132171 try {
133- await github.rest.repos.getCommit({ owner, repo, ref: pinnedSha });
172+ await withRetries(
173+ () => github.rest.repos.getCommit({ owner, repo, ref: pinnedSha }),
174+ `Checking ${owner}/${repo}@${pinnedSha}`
175+ );
134176 details.existsInRepo = true;
135177 } catch (err) {
136178 details.existsInRepo = (err.status !== 404 && err.status !== 422);
@@ -139,9 +181,12 @@ runs:
139181 // Find which tags (if any) point to this SHA
140182 if (details.existsInRepo) {
141183 try {
142- const tags = await github.paginate(github.rest.repos.listTags, {
143- owner, repo, per_page: 100,
144- });
184+ const tags = await withRetries(
185+ () => github.paginate(github.rest.repos.listTags, {
186+ owner, repo, per_page: 100,
187+ }),
188+ `Listing tags for ${owner}/${repo}`
189+ );
145190 details.matchingTags = tags
146191 .filter(t => t.commit.sha === pinnedSha)
147192 .map(t => t.name);
@@ -238,6 +283,28 @@ runs:
238283 github-token : ${{ inputs.github-token }}
239284 script : |
240285 const marker = '<!-- verify-sha-pinning -->';
286+ const nonRetryableStatuses = new Set([400, 401, 403, 404, 422]);
287+ const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
288+
289+ async function withRetries(operation, label, maxAttempts = 3) {
290+ let lastErr;
291+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
292+ try {
293+ return await operation();
294+ } catch (err) {
295+ if (nonRetryableStatuses.has(err.status)) {
296+ throw err;
297+ }
298+ lastErr = err;
299+ if (attempt < maxAttempts) {
300+ core.warning(`${label} failed on attempt ${attempt}/${maxAttempts}: ${err.message}; retrying`);
301+ await sleep(250 * attempt);
302+ }
303+ }
304+ }
305+ throw lastErr;
306+ }
307+
241308 const prNumber = context.issue.number;
242309 if (!prNumber) {
243310 core.info('Not a PR context — skipping comment');
@@ -251,11 +318,14 @@ runs:
251318 const status = process.env.STATUS || 'pass';
252319
253320 // Find existing comment
254- const { data: comments } = await github.rest.issues.listComments({
255- owner: context.repo.owner,
256- repo: context.repo.repo,
257- issue_number: prNumber,
258- });
321+ const { data: comments } = await withRetries(
322+ () => github.rest.issues.listComments({
323+ owner: context.repo.owner,
324+ repo: context.repo.repo,
325+ issue_number: prNumber,
326+ }),
327+ `Listing comments for PR #${prNumber}`
328+ );
259329 const existing = comments.find(c => c.body?.includes(marker));
260330
261331 let body;
@@ -308,19 +378,25 @@ runs:
308378 }
309379
310380 if (existing) {
311- await github.rest.issues.updateComment({
312- owner: context.repo.owner,
313- repo: context.repo.repo,
314- comment_id: existing.id,
315- body,
316- });
381+ await withRetries(
382+ () => github.rest.issues.updateComment({
383+ owner: context.repo.owner,
384+ repo: context.repo.repo,
385+ comment_id: existing.id,
386+ body,
387+ }),
388+ `Updating comment #${existing.id}`
389+ );
317390 core.info(`Updated existing comment #${existing.id}`);
318391 } else {
319- await github.rest.issues.createComment({
320- owner: context.repo.owner,
321- repo: context.repo.repo,
322- issue_number: prNumber,
323- body,
324- });
392+ await withRetries(
393+ () => github.rest.issues.createComment({
394+ owner: context.repo.owner,
395+ repo: context.repo.repo,
396+ issue_number: prNumber,
397+ body,
398+ }),
399+ `Creating comment for PR #${prNumber}`
400+ );
325401 core.info('Created new PR comment');
326402 }
0 commit comments