Skip to content

Commit 7dc9b8a

Browse files
authored
fix(action): handle 422 already_exists race condition (#665)
- Add retry logic for 422 'already_exists' errors in race conditions - Allow action to find and update existing releases instead of failing - Add test to verify race condition handling works correctly - Fixes regression that broke matrix workflows in v2.2.2+ closes #616
1 parent 0f0e0b9 commit 7dc9b8a

File tree

3 files changed

+92
-4
lines changed

3 files changed

+92
-4
lines changed

__tests__/github.test.ts

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { asset, findTagFromReleases, mimeOrDefault, Release, Releaser } from '../src/github';
1+
import {
2+
asset,
3+
findTagFromReleases,
4+
mimeOrDefault,
5+
release,
6+
Release,
7+
Releaser,
8+
} from '../src/github';
29

310
import { assert, describe, it } from 'vitest';
411

@@ -227,4 +234,75 @@ describe('github', () => {
227234
});
228235
});
229236
});
237+
238+
describe('error handling', () => {
239+
it('handles 422 already_exists error gracefully', async () => {
240+
const mockReleaser: Releaser = {
241+
getReleaseByTag: () => Promise.reject('Not implemented'),
242+
createRelease: () =>
243+
Promise.reject({
244+
status: 422,
245+
response: { data: { errors: [{ code: 'already_exists' }] } },
246+
}),
247+
updateRelease: () =>
248+
Promise.resolve({
249+
data: {
250+
id: 1,
251+
upload_url: 'test',
252+
html_url: 'test',
253+
tag_name: 'v1.0.0',
254+
name: 'test',
255+
body: 'test',
256+
target_commitish: 'main',
257+
draft: false,
258+
prerelease: false,
259+
assets: [],
260+
},
261+
}),
262+
allReleases: async function* () {
263+
yield {
264+
data: [
265+
{
266+
id: 1,
267+
upload_url: 'test',
268+
html_url: 'test',
269+
tag_name: 'v1.0.0',
270+
name: 'test',
271+
body: 'test',
272+
target_commitish: 'main',
273+
draft: false,
274+
prerelease: false,
275+
assets: [],
276+
},
277+
],
278+
};
279+
},
280+
} as const;
281+
282+
const config = {
283+
github_token: 'test-token',
284+
github_ref: 'refs/tags/v1.0.0',
285+
github_repository: 'owner/repo',
286+
input_tag_name: undefined,
287+
input_name: undefined,
288+
input_body: undefined,
289+
input_body_path: undefined,
290+
input_files: [],
291+
input_draft: undefined,
292+
input_prerelease: undefined,
293+
input_preserve_order: undefined,
294+
input_overwrite_files: undefined,
295+
input_fail_on_unmatched_files: false,
296+
input_target_commitish: undefined,
297+
input_discussion_category_name: undefined,
298+
input_generate_release_notes: false,
299+
input_append_body: false,
300+
input_make_latest: undefined,
301+
};
302+
303+
const result = await release(config, mockReleaser, 1);
304+
assert.ok(result);
305+
assert.equal(result.id, 1);
306+
});
307+
});
230308
});

dist/index.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/github.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -388,8 +388,18 @@ async function createRelease(
388388
throw error;
389389

390390
case 422:
391-
console.log('Skip retry - validation failed');
392-
throw error;
391+
// Check if this is a race condition with "already_exists" error
392+
const errorData = error.response?.data;
393+
if (errorData?.errors?.[0]?.code === 'already_exists') {
394+
console.log(
395+
'⚠️ Release already exists (race condition detected), retrying to find and update existing release...',
396+
);
397+
// Don't throw - allow retry to find existing release
398+
} else {
399+
console.log('Skip retry - validation failed');
400+
throw error;
401+
}
402+
break;
393403
}
394404

395405
console.log(`retrying... (${maxRetries - 1} retries remaining)`);

0 commit comments

Comments
 (0)