|
1 | 1 | /** |
2 | | - * Requeue endpoint handlers. |
| 2 | + * Force-release endpoint handler. |
3 | 3 | * |
4 | | - * Handles manual re-evaluation requests and force-release of locked |
5 | | - * bounties. Enforces business rules: 24h max age, once per issue, |
6 | | - * and force-release performs NO GitHub mutations. |
| 4 | + * Force-release performs NO GitHub mutations — only Redis + DB cleanup. |
7 | 5 | */ |
8 | 6 |
|
9 | 7 | import { logger } from "../logger.js"; |
10 | | -import { |
11 | | - getBounty, |
12 | | - getRequeueRecord, |
13 | | - insertRequeueRecord, |
14 | | - updateBountyStatus, |
15 | | - unlockBounty, |
16 | | - insertAuditLog, |
17 | | -} from "../db/index.js"; |
18 | | -import { reopenIssue, postComment } from "../github/client.js"; |
19 | | -import { clearVerdictLabels } from "../github/mutations.js"; |
20 | | -import { enqueue } from "../queue/processor.js"; |
| 8 | +import { unlockBounty, insertAuditLog } from "../db/index.js"; |
21 | 9 | import { getRedis } from "../redis.js"; |
22 | | -import { TARGET_REPO } from "../config.js"; |
23 | | - |
24 | | -/* ------------------------------------------------------------------ */ |
25 | | -/* Config */ |
26 | | -/* ------------------------------------------------------------------ */ |
27 | | - |
28 | | -/** Maximum age (ms) of an issue eligible for requeue (default 24h). */ |
29 | | -const REQUEUE_MAX_AGE_MS = parseInt( |
30 | | - process.env.REQUEUE_MAX_AGE_MS || "86400000", |
31 | | - 10, |
32 | | -); |
33 | | - |
34 | | -/* ------------------------------------------------------------------ */ |
35 | | -/* Repo helpers */ |
36 | | -/* ------------------------------------------------------------------ */ |
37 | | - |
38 | | -function parseRepo(): { owner: string; repo: string } { |
39 | | - const parts = TARGET_REPO.split("/"); |
40 | | - if (parts.length !== 2 || !parts[0] || !parts[1]) { |
41 | | - throw new Error(`Invalid TARGET_REPO format: "${TARGET_REPO}"`); |
42 | | - } |
43 | | - return { owner: parts[0], repo: parts[1] }; |
44 | | -} |
45 | | - |
46 | | -/* ------------------------------------------------------------------ */ |
47 | | -/* Requeue handler */ |
48 | | -/* ------------------------------------------------------------------ */ |
49 | | - |
50 | | -/** |
51 | | - * Handle a requeue request for an issue. |
52 | | - * |
53 | | - * Business rules: |
54 | | - * - Issue must exist in DB |
55 | | - * - Issue created_at must be within 24 hours |
56 | | - * - Issue must not already have been requeued |
57 | | - * |
58 | | - * If valid: reopen issue, clear labels, post re-eval comment, create |
59 | | - * requeue record, enqueue for processing. |
60 | | - * |
61 | | - * If invalid: return rejection with reason — NO GitHub mutations. |
62 | | - * |
63 | | - * @param issueNumber - GitHub issue number |
64 | | - * @param requesterId - ID of the requester |
65 | | - * @param requesterContext - Optional context from the requester |
66 | | - * @returns Success/failure with optional error message |
67 | | - */ |
68 | | -export async function handleRequeue( |
69 | | - issueNumber: number, |
70 | | - requesterId: string, |
71 | | - requesterContext?: object, |
72 | | -): Promise<{ success: boolean; error?: string }> { |
73 | | - // Validate: issue exists in DB |
74 | | - const bounty = getBounty(issueNumber); |
75 | | - if (!bounty) { |
76 | | - logger.info({ issueNumber }, "Requeue: issue not found in DB"); |
77 | | - return { success: false, error: `Issue #${issueNumber} not found` }; |
78 | | - } |
79 | | - |
80 | | - // Validate: issue created_at within 24 hours |
81 | | - if (bounty.created_at) { |
82 | | - const createdAt = new Date(bounty.created_at).getTime(); |
83 | | - const age = Date.now() - createdAt; |
84 | | - if (age > REQUEUE_MAX_AGE_MS) { |
85 | | - logger.info( |
86 | | - { issueNumber, ageMs: age, maxAgeMs: REQUEUE_MAX_AGE_MS }, |
87 | | - "Requeue: issue too old", |
88 | | - ); |
89 | | - return { |
90 | | - success: false, |
91 | | - error: `Issue #${issueNumber} is older than ${REQUEUE_MAX_AGE_MS / 3600000}h — not eligible for requeue`, |
92 | | - }; |
93 | | - } |
94 | | - } |
95 | | - |
96 | | - // Validate: not already requeued |
97 | | - const existingRequeue = getRequeueRecord(issueNumber); |
98 | | - if (existingRequeue) { |
99 | | - logger.info({ issueNumber }, "Requeue: already requeued"); |
100 | | - return { |
101 | | - success: false, |
102 | | - error: `Issue #${issueNumber} has already been requeued`, |
103 | | - }; |
104 | | - } |
105 | | - |
106 | | - // All validations passed — proceed with requeue |
107 | | - |
108 | | - const { owner, repo } = parseRepo(); |
109 | | - |
110 | | - try { |
111 | | - // Reopen issue |
112 | | - await reopenIssue(owner, repo, issueNumber); |
113 | | - |
114 | | - // Clear verdict labels |
115 | | - await clearVerdictLabels(issueNumber); |
116 | | - |
117 | | - // Post re-evaluation comment |
118 | | - await postComment( |
119 | | - owner, |
120 | | - repo, |
121 | | - issueNumber, |
122 | | - `## 🔄 Re-evaluation Requested\n\nThis issue has been requeued for re-validation by \`${requesterId}\`.\n\nPrevious verdict labels have been cleared.`, |
123 | | - ); |
124 | | - |
125 | | - // Create requeue record |
126 | | - insertRequeueRecord({ |
127 | | - issue_number: issueNumber, |
128 | | - requester_id: requesterId, |
129 | | - requester_context: requesterContext |
130 | | - ? JSON.stringify(requesterContext) |
131 | | - : undefined, |
132 | | - }); |
133 | | - |
134 | | - // Reset bounty status |
135 | | - updateBountyStatus(issueNumber, "pending"); |
136 | | - |
137 | | - // Enqueue for processing |
138 | | - enqueue({ |
139 | | - issueNumber, |
140 | | - workspaceId: bounty.workspace_id ?? "", |
141 | | - retryCount: 0, |
142 | | - addedAt: new Date().toISOString(), |
143 | | - }); |
144 | | - |
145 | | - // Audit log |
146 | | - insertAuditLog({ |
147 | | - workspace_id: bounty.workspace_id ?? undefined, |
148 | | - action: "bounty.requeued", |
149 | | - actor: requesterId, |
150 | | - details: JSON.stringify({ |
151 | | - issue_number: issueNumber, |
152 | | - requester_context: requesterContext, |
153 | | - }), |
154 | | - github_ref: `#${issueNumber}`, |
155 | | - }); |
156 | | - |
157 | | - logger.info({ issueNumber, requesterId }, "Requeue: success"); |
158 | | - return { success: true }; |
159 | | - } catch (err: unknown) { |
160 | | - const msg = err instanceof Error ? err.message : String(err); |
161 | | - logger.error({ issueNumber, err: msg }, "Requeue: failed"); |
162 | | - return { success: false, error: msg }; |
163 | | - } |
164 | | -} |
165 | 10 |
|
166 | 11 | /* ------------------------------------------------------------------ */ |
167 | 12 | /* Force-release handler */ |
|
0 commit comments