Make webhook tests independent with isolated PRs and retry on merge timing issues
Some checks failed
CI / test (pull_request) Has been cancelled

Each test now creates its own PR and cleans up after itself. Tests no longer depend on shared state or ordering, making them resilient to failures.

Added retry logic to mergePR for Gitea's 404/405 responses while computing mergeability. Used crypto.randomUUID() for unique branch names instead of Date.now() to eliminate collision risk.
This commit is contained in:
Corey Johnson 2026-03-09 15:56:09 -07:00
parent 238df92888
commit a71bf2d492
2 changed files with 46 additions and 17 deletions

View File

@ -228,10 +228,30 @@ export async function commentOnPR(repo: string, prNumber: number, body: string,
}
export async function mergePR(repo: string, prNumber: number, user: User) {
return giteaFetch(`/repos/${repo}/pulls/${prNumber}/merge`, user, {
method: "POST",
body: JSON.stringify({ Do: "merge", merge_message_field: "Test merge" }),
})
// Gitea returns 404/405 while computing mergeability after PR creation
const path = `/repos/${repo}/pulls/${prNumber}/merge`
for (let attempt = 0; attempt < 10; attempt++) {
const response = await fetch(`${giteaUrl}/api/v1${path}`, {
method: "POST",
headers: {
Authorization: `token ${user.token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ Do: "merge" }),
})
if (response.ok) return
if (response.status === 404 || response.status === 405) {
await Bun.sleep(1000)
continue
}
const body = await response.text().catch(() => "")
throw new Error(`Gitea ${response.status}: POST ${path}\n${body}`)
}
throw new Error(`Gitea merge failed after 10 retries for ${path}`)
}
export async function createReview(repo: string, prNumber: number, user: User, filePath: string) {

View File

@ -4,7 +4,6 @@ setDefaultTimeout(30_000)
import { ensure } from "@workshop/shared/utils"
import {
type User,
type TestPR,
setupWebhooks,
expectPullRequestWebhook,
expectIssueCommentWebhook,
@ -15,6 +14,10 @@ import {
mergePR,
} from "./helpers"
function testBranch(label: string) {
return `test-${crypto.randomUUID().slice(0, 8)}-${label}`
}
function getEnv(name: string): string {
const value = process.env[name]
ensure(value, `Missing env var: ${name}. See .env.example`)
@ -37,20 +40,19 @@ afterAll(() => teardown())
// --- Tests for Corey's repo (Spike opens PR, Corey comments/reviews/merges) ---
describe(`${coreyRepo}`, () => {
let pr: TestPR
const branch = `test-${Date.now()}-corey`
test("opening a PR sends pull_request webhook with correct author and repo", async () => {
pr = await expectPullRequestWebhook({ action: "opened", branch }, async (webhook) => {
const result = await openTestPR(coreyRepo, spike, branch)
const branch = testBranch("open")
await expectPullRequestWebhook({ action: "opened", branch }, async (webhook) => {
await openTestPR(coreyRepo, spike, branch)
const payload = await webhook
expect(payload.pull_request.user.login).toBe(spike.username)
expect(payload.repository.full_name).toBe(coreyRepo)
return result
})
})
test("commenting sends issue_comment webhook with correct user", async () => {
const branch = testBranch("comment")
const pr = await openTestPR(coreyRepo, spike, branch)
await expectIssueCommentWebhook({ action: "created", username: corey.username, prNumber: pr.number }, async (webhook) => {
await commentOnPR(coreyRepo, pr.number, "Looks good!", corey)
const payload = await webhook
@ -59,6 +61,8 @@ describe(`${coreyRepo}`, () => {
})
test("review with line comment sends pull_request_comment webhook", async () => {
const branch = testBranch("review")
const pr = await openTestPR(coreyRepo, spike, branch)
await expectPullRequestCommentWebhook({ action: "reviewed", branch }, async (webhook) => {
await createReview(coreyRepo, pr.number, corey, pr.filename)
const payload = await webhook
@ -68,6 +72,8 @@ describe(`${coreyRepo}`, () => {
})
test("merging sends pull_request closed webhook", async () => {
const branch = testBranch("merge")
const pr = await openTestPR(coreyRepo, spike, branch)
await expectPullRequestWebhook({ action: "closed", branch }, async (webhook) => {
await mergePR(coreyRepo, pr.number, corey)
const payload = await webhook
@ -79,20 +85,19 @@ describe(`${coreyRepo}`, () => {
// --- Tests for Spike's repo (Corey opens PR, Spike comments/reviews/merges) ---
describe(`${spikeRepo}`, () => {
let pr: TestPR
const branch = `test-${Date.now()}-spike`
test("opening a PR sends pull_request webhook with correct author and repo", async () => {
pr = await expectPullRequestWebhook({ action: "opened", branch }, async (webhook) => {
const result = await openTestPR(spikeRepo, corey, branch)
const branch = testBranch("open")
await expectPullRequestWebhook({ action: "opened", branch }, async (webhook) => {
await openTestPR(spikeRepo, corey, branch)
const payload = await webhook
expect(payload.pull_request.user.login).toBe(corey.username)
expect(payload.repository.full_name).toBe(spikeRepo)
return result
})
})
test("commenting sends issue_comment webhook with correct user", async () => {
const branch = testBranch("comment")
const pr = await openTestPR(spikeRepo, corey, branch)
await expectIssueCommentWebhook({ action: "created", username: spike.username, prNumber: pr.number }, async (webhook) => {
await commentOnPR(spikeRepo, pr.number, "On it!", spike)
const payload = await webhook
@ -101,6 +106,8 @@ describe(`${spikeRepo}`, () => {
})
test("review with line comment sends pull_request_comment webhook", async () => {
const branch = testBranch("review")
const pr = await openTestPR(spikeRepo, corey, branch)
await expectPullRequestCommentWebhook({ action: "reviewed", branch }, async (webhook) => {
await createReview(spikeRepo, pr.number, spike, pr.filename)
const payload = await webhook
@ -110,6 +117,8 @@ describe(`${spikeRepo}`, () => {
})
test("merging sends pull_request closed webhook", async () => {
const branch = testBranch("merge")
const pr = await openTestPR(spikeRepo, corey, branch)
await expectPullRequestWebhook({ action: "closed", branch }, async (webhook) => {
await mergePR(spikeRepo, pr.number, spike)
const payload = await webhook