From 0fb37b430edef9cf5b838b8d60d893351abdb5aa Mon Sep 17 00:00:00 2001 From: Francisco Solla Date: Tue, 25 Aug 2026 01:38:30 -0300 Subject: [PATCH] feat(OPS97): disableAutoMerge no github-api + auto_merge normalizado (tests unit) --- scripts/lib/github-api.mjs | 20 +++++++++++++++ tests/unit/githubApi.unit.spec.ts | 41 +++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/scripts/lib/github-api.mjs b/scripts/lib/github-api.mjs index 3955f753..c54e6c12 100644 --- a/scripts/lib/github-api.mjs +++ b/scripts/lib/github-api.mjs @@ -175,6 +175,9 @@ export const createApi = ({ merged: Boolean(pr.merged_at || pr.merged), draft: Boolean(pr.draft), mergeable: pr.mergeable ?? null, + autoMerge: pr.auto_merge + ? { mergeMethod: pr.auto_merge.merge_method ?? '', enabledBy: pr.auto_merge.enabled_by?.login ?? '' } + : null, nodeId: pr.node_id ?? '', head: { ref: pr.head?.ref ?? '', sha: pr.head?.sha ?? '' }, base: { ref: pr.base?.ref ?? '' }, @@ -386,6 +389,23 @@ export const createApi = ({ ) }, + /** + * DELETE /pulls/{number}/auto-merge — disarms GitHub's native auto-merge. + * Tolerates 404 (nothing armed) — idempotent by contract; any other error + * rethrows. The react-audit skill calls this right after opening its PR + * and after every subsequent push (the safety net workflow re-arms on + * each `synchronize`). + */ + disableAutoMerge: async (number) => { + try { + await request(`/repos/${owner}/${name}/pulls/${number}/auto-merge`, { + method: 'DELETE', + }) + } catch (error) { + if (!/\b404\b/.test(String(error?.message ?? ''))) throw error + } + }, + /** * GET /repos/{owner}/{repo}/branches/{branch}/protection — normalized for * drift comparison; `null` when the branch has no protection rule (404). diff --git a/tests/unit/githubApi.unit.spec.ts b/tests/unit/githubApi.unit.spec.ts index 2a8c35ad..601330c9 100644 --- a/tests/unit/githubApi.unit.spec.ts +++ b/tests/unit/githubApi.unit.spec.ts @@ -132,4 +132,45 @@ describe('github-api (issue tracker layer)', () => { expect(file?.content).toBe('olá mundo') expect(file?.sha).toBe('s') }) + + it('getPullRequest normalizes auto_merge into autoMerge', async () => { + const api = createApi({ + token: 'tok', + fetchImpl: async () => + ok({ number: 5, state: 'open', draft: false, auto_merge: { merge_method: 'rebase' } }), + }) + const pr = await api.getPullRequest(5) + expect(pr?.autoMerge?.mergeMethod).toBe('rebase') + expect(pr?.head).toEqual({ ref: '', sha: '' }) + }) + + it('disableAutoMerge DELETEs the auto-merge endpoint', async () => { + const calls: FetchCall[] = [] + const api = createApi({ + token: 'tok', + fetchImpl: async (url, init) => { + calls.push({ url: String(url), init }) + return ok({}) + }, + }) + await api.disableAutoMerge(7) + expect(calls[0].url).toContain('/repos/fsolla/teqo/pulls/7/auto-merge') + expect(calls[0].init?.method).toBe('DELETE') + }) + + it('disableAutoMerge tolerates 404 when nothing is armed', async () => { + const api = createApi({ + token: 'tok', + fetchImpl: async () => ok({ message: 'Auto-merge request is disabled for this pull request' }, 404), + }) + await expect(api.disableAutoMerge(9)).resolves.toBeUndefined() + }) + + it('disableAutoMerge rethrows non-404 errors', async () => { + const api = createApi({ + token: 'tok', + fetchImpl: async () => ok({ message: 'nope' }, 422), + }) + await expect(api.disableAutoMerge(11)).rejects.toThrow(/422/) + }) })