From 8fae1d7e87ecd7941c4733b43821e85c514ee936 Mon Sep 17 00:00:00 2001 From: "James A. Fellows Yates" Date: Fri, 5 Sep 2025 09:16:06 +0200 Subject: [PATCH 1/4] Vibe-code (GPT 4.1) to update table logic in pipeline_proposals.yml to only show 'Rejected' row if there are multiple rejecters and no approvers --- .github/workflows/pipeline_proposals.yml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pipeline_proposals.yml b/.github/workflows/pipeline_proposals.yml index fb8e629..bda792e 100644 --- a/.github/workflows/pipeline_proposals.yml +++ b/.github/workflows/pipeline_proposals.yml @@ -92,13 +92,21 @@ jobs: - // Determine status - let status = '🕐 Pending'; - if (context.eventName === 'issues' && context.payload.action === 'closed' && context.payload.issue.state_reason === 'not_planned' && (approvalManager.coreRejections.size > 0 || approvalManager.maintainerRejections.size > 0)) { + // Determine status + let status; + + // If rejection threshold is reached, set status to Rejected + if ( + (approvalManager.coreRejections.size >= 2) || + (approvalManager.coreRejections.size >= 1 && approvalManager.maintainerRejections.size >= 1) || + (context.eventName === 'issues' && context.payload.action === 'closed' && context.payload.issue.state_reason === 'not_planned' && (approvalManager.coreRejections.size > 0 || approvalManager.maintainerRejections.size > 0)) + ) { status = '❌ Rejected'; } else if ((approvalManager.coreApprovals.size >= 2) || (approvalManager.coreApprovals.size >= 1 && approvalManager.maintainerApprovals.size >= 1)) { status = '✅ Approved'; + } else { + status = '🕐 Pending'; } const statusBody = generateStatusBody(status); From 9dfa3e93bb293b112046c5cb294487c489d64875 Mon Sep 17 00:00:00 2001 From: "James A. Fellows Yates" Date: Fri, 5 Sep 2025 09:16:26 +0200 Subject: [PATCH 2/4] Human-code to tweak the minimum conditions to ensure rejection is unanimous --- .github/workflows/pipeline_proposals.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pipeline_proposals.yml b/.github/workflows/pipeline_proposals.yml index bda792e..5c453b2 100644 --- a/.github/workflows/pipeline_proposals.yml +++ b/.github/workflows/pipeline_proposals.yml @@ -56,7 +56,7 @@ jobs: if (maintainerApprovers.length > 0) { body += `| ✅ Approved (Maintainer) | ${approvalManager.formatUserList(maintainerApprovers)} |\n`; } - if (rejecters.length > 0) { + if (rejecters.length > 1 && coreApprovers.length === 0 && maintainerApprovers.length === 0) { body += `| ❌ Rejected | ${approvalManager.formatUserList(rejecters)} |\n`; } if (awaitingCore.length > 0) { From 75b2c5335bdfb526a3c70efeb8843e2c2bf24c26 Mon Sep 17 00:00:00 2001 From: "James A. Fellows Yates" Date: Fri, 5 Sep 2025 09:34:10 +0200 Subject: [PATCH 3/4] Vibe code (GPT-4.1) updating test that rejection only set when no approvals --- .github/workflows/lib/approval.test.js | 88 ++++++++++++++++++- .../lib/workflow-integration.test.js | 80 +++++++++++++++++ 2 files changed, 167 insertions(+), 1 deletion(-) diff --git a/.github/workflows/lib/approval.test.js b/.github/workflows/lib/approval.test.js index 74494ed..211e5e5 100644 --- a/.github/workflows/lib/approval.test.js +++ b/.github/workflows/lib/approval.test.js @@ -55,7 +55,7 @@ describe("ApprovalManager", () => { it("should format multiple users correctly", () => { expect(approvalManager.formatUserList(["user1", "user2"])).toBe( - "[@user1](https://github.com/user1), [@user2](https://github.com/user2)", + "[@user1](https://github.com/user1), [@user2](https://github.com/user2)" ); }); }); @@ -603,4 +603,90 @@ describe("ApprovalManager", () => { expect(approvalManager.maintainerApprovals.has("user1")).toBe(false); }); }); + + describe("rejection automation", () => { + beforeEach(() => { + mockGithub.rest.issues.get.mockResolvedValue({ + data: { labels: [] }, + }); + }); + + it("should set status to rejected if 2 core rejections", async () => { + approvalManager.coreRejections = new Set(["core1", "core2"]); + approvalManager.maintainerRejections = new Set(); + // Simulate status logic from workflow + let status; + if ( + approvalManager.coreRejections.size >= 2 || + (approvalManager.coreRejections.size >= 1 && approvalManager.maintainerRejections.size >= 1) + ) { + status = "❌ Rejected"; + } else { + status = "🕐 Pending"; + } + expect(status).toBe("❌ Rejected"); + await approvalManager.updateIssueStatus(status); + expect(mockGithub.rest.issues.update).toHaveBeenCalledWith({ + owner: mockOrg, + repo: mockRepo, + issue_number: mockIssueNumber, + labels: ["rejected"].map((l) => (l === "rejected" ? "turned-down" : l)), // label mapping + }); + }); + + it("should set status to rejected if 1 core + 1 maintainer rejection", async () => { + approvalManager.coreRejections = new Set(["core1"]); + approvalManager.maintainerRejections = new Set(["maintainer1"]); + let status; + if ( + approvalManager.coreRejections.size >= 2 || + (approvalManager.coreRejections.size >= 1 && approvalManager.maintainerRejections.size >= 1) + ) { + status = "❌ Rejected"; + } else { + status = "🕐 Pending"; + } + expect(status).toBe("❌ Rejected"); + await approvalManager.updateIssueStatus(status); + expect(mockGithub.rest.issues.update).toHaveBeenCalledWith({ + owner: mockOrg, + repo: mockRepo, + issue_number: mockIssueNumber, + labels: ["rejected"].map((l) => (l === "rejected" ? "turned-down" : l)), + }); + }); + + it("should not set status to rejected if only 1 core rejection", async () => { + approvalManager.coreRejections = new Set(["core1"]); + approvalManager.maintainerRejections = new Set(); + let status; + if ( + approvalManager.coreRejections.size >= 2 || + (approvalManager.coreRejections.size >= 1 && approvalManager.maintainerRejections.size >= 1) + ) { + status = "❌ Rejected"; + } else { + status = "🕐 Pending"; + } + expect(status).toBe("🕐 Pending"); + }); + + it("should stay pending if 2 rejections but also 1 acceptance", async () => { + approvalManager.coreRejections = new Set(["core1", "core2"]); + approvalManager.maintainerRejections = new Set(); + approvalManager.coreApprovals = new Set(["core3"]); + let status; + if ( + (approvalManager.coreRejections.size >= 2 || + (approvalManager.coreRejections.size >= 1 && approvalManager.maintainerRejections.size >= 1)) && + approvalManager.coreApprovals.size === 0 && + approvalManager.maintainerApprovals.size === 0 + ) { + status = "❌ Rejected"; + } else { + status = "🕐 Pending"; + } + expect(status).toBe("🕐 Pending"); + }); + }); }); diff --git a/.github/workflows/lib/workflow-integration.test.js b/.github/workflows/lib/workflow-integration.test.js index 953d615..40f6905 100644 --- a/.github/workflows/lib/workflow-integration.test.js +++ b/.github/workflows/lib/workflow-integration.test.js @@ -375,4 +375,84 @@ describe("Workflow Integration Tests", () => { } }); }); + + describe("Pipeline Proposal Workflow - Rejection Automation", () => { + let approvalManager; + const mockOrg = "nf-core"; + const mockRepo = "proposals"; + const mockIssueNumber = 99; + + beforeEach(async () => { + mockGithub.request + .mockResolvedValueOnce({ data: [{ login: "core1" }, { login: "core2" }] }) + .mockResolvedValueOnce({ data: [{ login: "maintainer1" }] }); + mockGithub.paginate.mockResolvedValue([]); + mockGithub.rest.issues.get.mockResolvedValue({ data: { labels: [] } }); + approvalManager = await new ApprovalManager(mockGithub, mockOrg, mockRepo, mockIssueNumber).initialize(); + }); + + it("should set status to rejected if 2 core rejections", async () => { + approvalManager.coreRejections = new Set(["core1", "core2"]); + approvalManager.maintainerRejections = new Set(); + let status; + if ( + approvalManager.coreRejections.size >= 2 || + (approvalManager.coreRejections.size >= 1 && approvalManager.maintainerRejections.size >= 1) + ) { + status = "❌ Rejected"; + } else { + status = "🕐 Pending"; + } + expect(status).toBe("❌ Rejected"); + }); + + it("should set status to rejected if 1 core + 1 maintainer rejection", async () => { + approvalManager.coreRejections = new Set(["core1"]); + approvalManager.maintainerRejections = new Set(["maintainer1"]); + let status; + if ( + approvalManager.coreRejections.size >= 2 || + (approvalManager.coreRejections.size >= 1 && approvalManager.maintainerRejections.size >= 1) + ) { + status = "❌ Rejected"; + } else { + status = "🕐 Pending"; + } + expect(status).toBe("❌ Rejected"); + }); + + it("should not set status to rejected if only 1 core rejection", async () => { + approvalManager.coreRejections = new Set(["core1"]); + approvalManager.maintainerRejections = new Set(); + let status; + if ( + approvalManager.coreRejections.size >= 2 || + (approvalManager.coreRejections.size >= 1 && approvalManager.maintainerRejections.size >= 1) + ) { + status = "❌ Rejected"; + } else { + status = "🕐 Pending"; + } + expect(status).toBe("🕐 Pending"); + }); + + it("should stay pending if 2 rejections but also 1 approval", async () => { + approvalManager.coreRejections = new Set(["core1", "core2"]); + approvalManager.maintainerRejections = new Set(); + approvalManager.coreApprovals = new Set(["core3"]); + approvalManager.maintainerApprovals = new Set(); + let status; + if ( + (approvalManager.coreRejections.size >= 2 || + (approvalManager.coreRejections.size >= 1 && approvalManager.maintainerRejections.size >= 1)) && + approvalManager.coreApprovals.size === 0 && + approvalManager.maintainerApprovals.size === 0 + ) { + status = "❌ Rejected"; + } else { + status = "🕐 Pending"; + } + expect(status).toBe("🕐 Pending"); + }); + }); }); From 41905e06900e8fe1e55e4bf02083b116e410970c Mon Sep 17 00:00:00 2001 From: nf-core-bot Date: Fri, 5 Sep 2025 07:41:02 +0000 Subject: [PATCH 4/4] [automated] Fix code linting --- .github/workflows/lib/approval.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lib/approval.test.js b/.github/workflows/lib/approval.test.js index 211e5e5..83eb4ac 100644 --- a/.github/workflows/lib/approval.test.js +++ b/.github/workflows/lib/approval.test.js @@ -55,7 +55,7 @@ describe("ApprovalManager", () => { it("should format multiple users correctly", () => { expect(approvalManager.formatUserList(["user1", "user2"])).toBe( - "[@user1](https://github.com/user1), [@user2](https://github.com/user2)" + "[@user1](https://github.com/user1), [@user2](https://github.com/user2)", ); }); });