From 658307f4bef8dc09aa36f918aadcbcaa4b0815d0 Mon Sep 17 00:00:00 2001 From: Othmane Ataallah <”othmane.ataallah.triztech@gmail.com”> Date: Mon, 4 May 2026 10:29:44 +0100 Subject: [PATCH] feat(pushes) --- README.md | 12 ++ src/adapters/sheetsAdapter.js | 223 +++++++++++++++++++++++++++++++++- src/workers/issueProcessor.js | 57 ++++++--- 3 files changed, 269 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 15f30de..dc8bfa8 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,18 @@ The adapter creates and manages the following sheets (tabs) with canonical heade - `ProcessedAt`, `IssueKey`, `IssueNumber`, `Assignee`, `UserID`, `TotalMinutes` - The adapter converts the time values returned from Gitea (seconds) into minutes, rounded. It upserts rows by `IssueNumber`+`UserID` to avoid duplicates for the same issue/user. +- `Pushes` — organization-wide push events. Header columns: + - `ProcessedAt`, `EventHash`, `RepoFullName`, `Ref`, `Branch`, `Pusher`, `PusherEmail`, `CommitCount`, `HeadCommitId`, `HeadCommitMessage`, `CompareURL`, `RepoURL`, `CommitsSummary` + - Push rows are idempotent by `eventHash` (if the same push webhook is received twice the existing row is updated instead of duplicated). + - Push rows are idempotent by `eventHash` (if the same push webhook is received twice the existing row is updated instead of duplicated). + - `CommitsSummary` is truncated to a configurable per-commit snippet length and a configurable number of commits; a `CommitsTruncated` column indicates truncation. + - Per-commit rows can be enabled using `PUSH_COMMITS_ENABLED=true`. When enabled a `PushCommits` sheet is created with one row per commit (upserts on EventHash+CommitId). + +Configuration for push commits: +- `PUSH_COMMITS_ENABLED` — set to `true` to write per-commit rows to `PushCommits`. +- `PUSH_COMMIT_SNIPPET_LENGTH` — maximum characters per commit message snippet (default 80). +- `PUSH_COMMITS_SUMMARY_LIMIT` — maximum number of commits included in the `CommitsSummary` (default 10). + - `DeadLetters` — failed events for manual inspection. Header columns: - `ProcessedAt`, `EventHash`, `IssueKey`, `Error`, `PayloadBase64`, `Notes` diff --git a/src/adapters/sheetsAdapter.js b/src/adapters/sheetsAdapter.js index 6bc0266..f45456a 100644 --- a/src/adapters/sheetsAdapter.js +++ b/src/adapters/sheetsAdapter.js @@ -40,6 +40,14 @@ const DEADLETTERS_HEADER = ["ProcessedAt", "EventHash", "IssueKey", "Error", "Pa const COMMENTS_HEADER = process.env.GOOGLE_COMMENTS_HEADER ? JSON.parse(process.env.GOOGLE_COMMENTS_HEADER) : ["ProcessedAt", "IssueNumber", "Actor", "CommentURL"]; const TIME_ENTRIES_SHEET = process.env.GOOGLE_TIME_ENTRIES_SHEET || "TimeEntries"; const TIME_ENTRIES_HEADER = ["ProcessedAt", "IssueKey", "IssueNumber", "Assignee", "UserID", "TotalMinutes"]; +const PUSHES_SHEET = process.env.GOOGLE_PUSHES_SHEET || "Pushes"; +// Add a boolean indicator if the commits summary was truncated +const PUSHES_HEADER = ["ProcessedAt", "EventHash", "RepoFullName", "Ref", "Branch", "Pusher", "PusherEmail", "CommitCount", "HeadCommitId", "HeadCommitMessage", "CompareURL", "RepoURL", "CommitsSummary", "CommitsTruncated"]; +const PUSH_COMMITS_SHEET = process.env.GOOGLE_PUSH_COMMITS_SHEET || "PushCommits"; +const PUSH_COMMITS_ENABLED = String(process.env.PUSH_COMMITS_ENABLED || "").toLowerCase() === "true"; +const PUSH_COMMITS_HEADER = ["ProcessedAt", "EventHash", "RepoFullName", "Ref", "Branch", "CommitId", "CommitMessage", "CommitAuthor", "CommitURL", "CommitTimestamp"]; +const PUSH_COMMIT_SNIPPET_LENGTH = Number(process.env.PUSH_COMMIT_SNIPPET_LENGTH || 80); +const PUSH_COMMITS_SUMMARY_LIMIT = Number(process.env.PUSH_COMMITS_SUMMARY_LIMIT || 10); // Default number of rows to keep on a freshly-created sheet. Can be overridden with env var const DEFAULT_SHEET_ROWS = Number(process.env.GOOGLE_SHEET_DEFAULT_ROWS || 100); @@ -468,10 +476,45 @@ export async function appendEvent(evt = {}) { const processedAt = formatDate(new Date().toISOString()); const eventHash = evt.eventHash || (evt.meta && evt.meta.eventHash) || ""; - const eventType = evt.type || (evt.event && evt.event.action) || ""; - const issueKey = evt.issueKey || (evt.meta && evt.meta.issueKey) || (evt.event && evt.event.repository && evt.event.issue ? `${evt.event.repository.full_name}#${evt.event.issue.number}` : ""); - const actor = (evt.actor || (evt.event && evt.event.sender && evt.event.sender.login) || (evt.meta && evt.meta.actor)) || ""; - const summary = evt.summary || (evt.event && evt.event.action) || ""; + const eventObj = evt.event || evt; + let eventType = evt.type || (eventObj && eventObj.action) || ""; + const issueKey = evt.issueKey || (evt.meta && evt.meta.issueKey) || (eventObj && eventObj.repository && eventObj.issue ? `${eventObj.repository.full_name}#${eventObj.issue.number}` : ""); + const actor = (evt.actor || (eventObj && eventObj.sender && eventObj.sender.login) || (evt.meta && evt.meta.actor)) || ""; + let summary = evt.summary || (eventObj && eventObj.action) || ""; + // If push event, provide a helpful summary + try { + if (eventObj && !eventType) { + // detect push-like events and special cases + const ref = eventObj.ref || ""; + const branch = String(ref).replace(/^refs\/heads\//, ""); + const commitCount = Array.isArray(eventObj.commits) ? eventObj.commits.length : 0; + const isDeleted = eventObj.deleted === true || (typeof eventObj.after === 'string' && /^0+$/.test(eventObj.after)); + if ((eventObj.ref || eventObj.commits)) { + if (isDeleted) { + eventType = 'push-delete'; + summary = summary || `deleted ${branch}`; + } else if (commitCount === 0 && !eventObj.head_commit) { + eventType = 'push-empty'; + summary = summary || 'no head commit'; + } else { + eventType = 'push'; + if (!summary) { + if (eventObj.ref) summary = `push to ${branch}`; + else if (commitCount > 0) summary = `${commitCount} commit(s)`; + } + } + } + } else if (!summary && eventObj) { + if (eventObj.ref) { + const branch = String(eventObj.ref).replace(/^refs\/heads\//, ""); + summary = `push to ${branch}`; + } else if (Array.isArray(eventObj.commits) && eventObj.commits.length > 0) { + summary = `${eventObj.commits.length} commit(s)`; + } + } + } catch (e) { + // ignore + } const status = evt.status || "pending"; // If eventHash exists in Events, update only the Status (and ProcessedAt) @@ -483,11 +526,34 @@ export async function appendEvent(evt = {}) { if (String(values[i][0]) === String(eventHash)) { const rowIndex = i + 1; const statusCol = EVENTS_HEADER.indexOf('Status') + 1; - const processedAtCol = 1; // ProcessedAt - const statusRange = `${EVENTS_SHEET}!${colNumberToLetter(statusCol)}${rowIndex}`; const processedAtRange = `${EVENTS_SHEET}!A${rowIndex}`; + const statusRange = `${EVENTS_SHEET}!${colNumberToLetter(statusCol)}${rowIndex}`; + + // update status and processedAt await requestWithRetry(() => sheets.spreadsheets.values.update({ spreadsheetId, range: statusRange, valueInputOption: 'RAW', requestBody: { values: [[status]] } })); await requestWithRetry(() => sheets.spreadsheets.values.update({ spreadsheetId, range: processedAtRange, valueInputOption: 'RAW', requestBody: { values: [[processedAt]] } })); + + // update EventType, Summary, and IssueKey if present + try { + const eventTypeCol = EVENTS_HEADER.indexOf('EventType') + 1; + const summaryCol = EVENTS_HEADER.indexOf('Summary') + 1; + const issueKeyCol = EVENTS_HEADER.indexOf('IssueKey') + 1; + if (eventType) { + const range = `${EVENTS_SHEET}!${colNumberToLetter(eventTypeCol)}${rowIndex}`; + await requestWithRetry(() => sheets.spreadsheets.values.update({ spreadsheetId, range, valueInputOption: 'RAW', requestBody: { values: [[eventType]] } })); + } + if (summary) { + const range = `${EVENTS_SHEET}!${colNumberToLetter(summaryCol)}${rowIndex}`; + await requestWithRetry(() => sheets.spreadsheets.values.update({ spreadsheetId, range, valueInputOption: 'RAW', requestBody: { values: [[summary]] } })); + } + if (issueKey) { + const range = `${EVENTS_SHEET}!${colNumberToLetter(issueKeyCol)}${rowIndex}`; + await requestWithRetry(() => sheets.spreadsheets.values.update({ spreadsheetId, range, valueInputOption: 'RAW', requestBody: { values: [[issueKey]] } })); + } + } catch (e2) { + logger.warn('Failed updating event extra fields', { err: e2 && e2.message ? e2.message : e2 }); + } + logger.info('sheetsAdapter updated event status', { issueKey, eventHash, status }); return; } @@ -614,6 +680,151 @@ export async function appendTimeEntries(issueKey, issueNumber, times = []) { return perUserMinutes; } +export async function appendPush(evt = {}) { + const sheets = await sheetsClient(); + const spreadsheetId = process.env.GOOGLE_SHEET_ID || config.googleSheetId; + await ensureSheet(sheets, spreadsheetId, PUSHES_SHEET, PUSHES_HEADER); + + const processedAt = formatDate(new Date().toISOString()); + const event = evt.event || evt; + const eventHash = evt.eventHash || (evt.meta && evt.meta.eventHash) || ""; + const repoFullName = (event.repository && (event.repository.full_name || (event.repository.owner && event.repository.owner.login && event.repository.name && `${event.repository.owner.login}/${event.repository.name}`))) || ""; + const ref = event.ref || ""; + const branch = String(ref).replace(/^refs\/heads\//, ""); + const pusher = (event.pusher && (event.pusher.name || event.pusher.username)) || (event.sender && event.sender.login) || (evt.actor) || ""; + const pusherEmail = (event.pusher && event.pusher.email) || ""; + const commitCount = Array.isArray(event.commits) ? event.commits.length : 0; + const headCommitId = event.after || (event.head_commit && (event.head_commit.id || event.head_commit.sha)) || (commitCount > 0 && event.commits[commitCount - 1] && (event.commits[commitCount - 1].id || event.commits[commitCount - 1].sha)) || ""; + const headCommitMessage = (event.head_commit && (event.head_commit.message || event.head_commit.msg)) || (commitCount > 0 && event.commits[commitCount - 1] && (event.commits[commitCount - 1].message || event.commits[commitCount - 1].msg)) || ""; + const compareUrl = event.compare_url || event.compareUrl || ""; + const repoUrl = (event.repository && (event.repository.html_url || event.repository.url)) || ""; + + + // Build commits summary with truncation awareness + let commitsTruncated = false; + let commitsSummary = ''; + if (Array.isArray(event.commits) && event.commits.length > 0) { + const parts = []; + const limit = Math.max(1, PUSH_COMMITS_SUMMARY_LIMIT || 10); + for (let i = 0; i < Math.min(limit, event.commits.length); i++) { + const c = event.commits[i] || {}; + let msg = (c.message || c.msg || '').replace(/\s+/g, ' ').trim(); + if (msg.length > PUSH_COMMIT_SNIPPET_LENGTH) { + msg = msg.substring(0, PUSH_COMMIT_SNIPPET_LENGTH); + commitsTruncated = true; + } + const id = (c.id || c.sha || '').substring(0, 7); + parts.push(`${id}:${msg}`); + } + if (event.commits.length > limit) commitsTruncated = true; + commitsSummary = parts.join(' | '); + } + + // If eventHash exists in Pushes, update some fields instead of appending duplicate + if (eventHash) { + try { + const res = await requestWithRetry(() => sheets.spreadsheets.values.get({ spreadsheetId, range: `${PUSHES_SHEET}!B:B` })); + const values = (res && res.data && res.data.values) || []; + for (let i = 0; i < values.length; i++) { + if (String(values[i][0]) === String(eventHash)) { + const rowIndex = i + 1; + const processedAtRange = `${PUSHES_SHEET}!A${rowIndex}`; + const commitCountCol = PUSHES_HEADER.indexOf('CommitCount') + 1; + const headIdCol = PUSHES_HEADER.indexOf('HeadCommitId') + 1; + const headMsgCol = PUSHES_HEADER.indexOf('HeadCommitMessage') + 1; + const commitsSummaryCol = PUSHES_HEADER.indexOf('CommitsSummary') + 1; + const commitsTruncatedCol = PUSHES_HEADER.indexOf('CommitsTruncated') + 1; + await requestWithRetry(() => sheets.spreadsheets.values.update({ spreadsheetId, range: processedAtRange, valueInputOption: 'RAW', requestBody: { values: [[processedAt]] } })); + await requestWithRetry(() => sheets.spreadsheets.values.update({ spreadsheetId, range: `${PUSHES_SHEET}!${colNumberToLetter(commitCountCol)}${rowIndex}`, valueInputOption: 'RAW', requestBody: { values: [[commitCount]] } })); + await requestWithRetry(() => sheets.spreadsheets.values.update({ spreadsheetId, range: `${PUSHES_SHEET}!${colNumberToLetter(headIdCol)}${rowIndex}`, valueInputOption: 'RAW', requestBody: { values: [[headCommitId]] } })); + await requestWithRetry(() => sheets.spreadsheets.values.update({ spreadsheetId, range: `${PUSHES_SHEET}!${colNumberToLetter(headMsgCol)}${rowIndex}`, valueInputOption: 'RAW', requestBody: { values: [[headCommitMessage]] } })); + await requestWithRetry(() => sheets.spreadsheets.values.update({ spreadsheetId, range: `${PUSHES_SHEET}!${colNumberToLetter(commitsSummaryCol)}${rowIndex}`, valueInputOption: 'RAW', requestBody: { values: [[commitsSummary]] } })); + await requestWithRetry(() => sheets.spreadsheets.values.update({ spreadsheetId, range: `${PUSHES_SHEET}!${colNumberToLetter(commitsTruncatedCol)}${rowIndex}`, valueInputOption: 'RAW', requestBody: { values: [[commitsTruncated ? 'true' : 'false']] } })); + logger.info('sheetsAdapter updated push row', { repoFullName, eventHash }); + if (PUSH_COMMITS_ENABLED) await appendPushCommits(evt); + return; + } + } + } catch (e) { + logger.warn('Error checking for existing push eventHash, will append', { err: e && e.message ? e.message : e }); + } + } + const row = [processedAt, eventHash, repoFullName, ref, branch, pusher, pusherEmail, commitCount, headCommitId, headCommitMessage, compareUrl, repoUrl, commitsSummary, commitsTruncated ? 'true' : 'false']; + const range = `${PUSHES_SHEET}!A:${colNumberToLetter(PUSHES_HEADER.length)}`; + await requestWithRetry(() => sheets.spreadsheets.values.append({ spreadsheetId, range, valueInputOption: 'RAW', requestBody: { values: [row] } })); + logger.info('sheetsAdapter appended push', { repoFullName, branch, commitCount, eventHash }); + if (PUSH_COMMITS_ENABLED) await appendPushCommits(evt); +} + +export async function appendPushCommits(evt = {}) { + const sheets = await sheetsClient(); + const spreadsheetId = process.env.GOOGLE_SHEET_ID || config.googleSheetId; + await ensureSheet(sheets, spreadsheetId, PUSH_COMMITS_SHEET, PUSH_COMMITS_HEADER); + + const processedAt = formatDate(new Date().toISOString()); + const event = evt.event || evt; + const eventHash = evt.eventHash || (evt.meta && evt.meta.eventHash) || ""; + const repoFullName = (event.repository && (event.repository.full_name || (event.repository.owner && event.repository.owner.login && event.repository.name && `${event.repository.owner.login}/${event.repository.name}`))) || ""; + const ref = event.ref || ""; + const branch = String(ref).replace(/^refs\/heads\//, ""); + const commits = Array.isArray(event.commits) ? event.commits : []; + + if (commits.length === 0) return; + + const existingRes = await requestWithRetry(() => sheets.spreadsheets.values.get({ spreadsheetId, range: `${PUSH_COMMITS_SHEET}!A2:${colNumberToLetter(PUSH_COMMITS_HEADER.length)}` })).catch(() => ({ data: { values: [] } })); + const existing = (existingRes && existingRes.data && existingRes.data.values) || []; + + const toAppend = []; + for (let i = 0; i < commits.length; i++) { + const c = commits[i] || {}; + const commitId = (c.id || c.sha || '') + ''; + let msg = (c.message || c.msg || '').replace(/\s+/g, ' ').trim(); + let truncated = false; + if (msg.length > PUSH_COMMIT_SNIPPET_LENGTH) { + msg = msg.substring(0, PUSH_COMMIT_SNIPPET_LENGTH); + truncated = true; + } + const author = (c.author && (c.author.name || c.author.username)) || (c.commit && c.commit.author && c.commit.author.name) || ''; + const url = c.url || c.html_url || ''; + const ts = (c.timestamp || c.date || (c.commit && c.commit.author && c.commit.author.date)) || ''; + + // find existing row by EventHash + CommitId + let found = false; + for (let j = 0; j < existing.length; j++) { + const row = existing[j] || []; + const rowEventHash = row[1] !== undefined ? String(row[1]) : ''; + const rowCommitId = row[5] !== undefined ? String(row[5]) : ''; + if (String(rowEventHash) === String(eventHash) && String(rowCommitId) === String(commitId)) { + const rowIndex = 2 + j; // because we started at A2 + // update CommitMessage, CommitAuthor, CommitURL, CommitTimestamp, ProcessedAt + const commitMsgCol = PUSH_COMMITS_HEADER.indexOf('CommitMessage') + 1; + const commitAuthorCol = PUSH_COMMITS_HEADER.indexOf('CommitAuthor') + 1; + const commitUrlCol = PUSH_COMMITS_HEADER.indexOf('CommitURL') + 1; + const commitTsCol = PUSH_COMMITS_HEADER.indexOf('CommitTimestamp') + 1; + await requestWithRetry(() => sheets.spreadsheets.values.update({ spreadsheetId, range: `${PUSH_COMMITS_SHEET}!${colNumberToLetter(commitMsgCol)}${rowIndex}`, valueInputOption: 'RAW', requestBody: { values: [[msg]] } })); + await requestWithRetry(() => sheets.spreadsheets.values.update({ spreadsheetId, range: `${PUSH_COMMITS_SHEET}!${colNumberToLetter(commitAuthorCol)}${rowIndex}`, valueInputOption: 'RAW', requestBody: { values: [[author]] } })); + await requestWithRetry(() => sheets.spreadsheets.values.update({ spreadsheetId, range: `${PUSH_COMMITS_SHEET}!${colNumberToLetter(commitUrlCol)}${rowIndex}`, valueInputOption: 'RAW', requestBody: { values: [[url]] } })); + await requestWithRetry(() => sheets.spreadsheets.values.update({ spreadsheetId, range: `${PUSH_COMMITS_SHEET}!${colNumberToLetter(commitTsCol)}${rowIndex}`, valueInputOption: 'RAW', requestBody: { values: [[ts]] } })); + // update processedAt + await requestWithRetry(() => sheets.spreadsheets.values.update({ spreadsheetId, range: `${PUSH_COMMITS_SHEET}!A${rowIndex}`, valueInputOption: 'RAW', requestBody: { values: [[processedAt]] } })); + found = true; + break; + } + } + + if (!found) { + toAppend.push([processedAt, eventHash, repoFullName, ref, branch, commitId, msg, author, url, ts]); + } + } + + if (toAppend.length > 0) { + const range = `${PUSH_COMMITS_SHEET}!A:${colNumberToLetter(PUSH_COMMITS_HEADER.length)}`; + await requestWithRetry(() => sheets.spreadsheets.values.append({ spreadsheetId, range, valueInputOption: 'RAW', requestBody: { values: toAppend } })); + } + + logger.info('sheetsAdapter appended/updated push commits', { eventHash, appended: toAppend.length }); +} + export async function updateIssueFields(issueKey, updates = {}) { const sheets = await sheetsClient(); const spreadsheetId = process.env.GOOGLE_SHEET_ID || config.googleSheetId; diff --git a/src/workers/issueProcessor.js b/src/workers/issueProcessor.js index db44b98..b8d4cb8 100644 --- a/src/workers/issueProcessor.js +++ b/src/workers/issueProcessor.js @@ -14,35 +14,35 @@ async function handler(payload) { const headers = payload.headers || {}; const issue = event && event.issue; - if (!issue) { - logger.info("Event has no issue field; skipping processing"); - return; - } + const giteaEvent = (headers && (headers['x-gitea-event'] || headers['x-gitea-event'.toLowerCase()])) || null; + const isPush = giteaEvent === 'push' || Boolean(event && (event.ref || event.commits)); - const issueKey = meta.issueKey || (event.repository && event.repository.full_name ? `${event.repository.full_name}#${issue.number}` : null); + const issueKey = meta.issueKey || (issue && event.repository && event.repository.full_name ? `${event.repository.full_name}#${issue.number}` : null); - // dedupe: if eventHash already exists in Events or matches issue.LastEventHash, skip + // dedupe: if eventHash already exists in Events, skip. For issues also compare Issue.LastEventHash try { const seen = await sheetsAdapter.isEventKnown(meta.eventHash); if (seen) { - logger.info("Duplicate event detected in Events sheet; skipping", { issueKey, eventHash: meta.eventHash }); + logger.info("Duplicate event detected in Events sheet; skipping", { issueKey, eventHash: meta.eventHash, isPush }); try { - await sheetsAdapter.appendEvent({ event, eventHash: meta.eventHash, issueKey, actor: meta.actor, summary: event.action || "", status: "duplicate", meta }); + await sheetsAdapter.appendEvent({ event, eventHash: meta.eventHash, issueKey, actor: meta.actor, summary: event.action || (isPush ? 'push' : ''), status: "duplicate", meta }); } catch (e) { logger.warn("Failed to append duplicate event record", { err: e && e.message ? e.message : e }); } return; } - const existing = await sheetsAdapter.getIssueByIssueKey(issueKey); - if (existing && existing.LastEventHash && String(existing.LastEventHash) === String(meta.eventHash)) { - logger.info("Duplicate event detected by Issue.LastEventHash; skipping", { issueKey, eventHash: meta.eventHash }); - try { - await sheetsAdapter.appendEvent({ event, eventHash: meta.eventHash, issueKey, actor: meta.actor, summary: event.action || "", status: "duplicate", meta }); - } catch (e) { - logger.warn("Failed to append duplicate event record", { err: e && e.message ? e.message : e }); + if (issue) { + const existing = await sheetsAdapter.getIssueByIssueKey(issueKey); + if (existing && existing.LastEventHash && String(existing.LastEventHash) === String(meta.eventHash)) { + logger.info("Duplicate event detected by Issue.LastEventHash; skipping", { issueKey, eventHash: meta.eventHash }); + try { + await sheetsAdapter.appendEvent({ event, eventHash: meta.eventHash, issueKey, actor: meta.actor, summary: event.action || "", status: "duplicate", meta }); + } catch (e) { + logger.warn("Failed to append duplicate event record", { err: e && e.message ? e.message : e }); + } + return; } - return; } } catch (e) { logger.warn("Error during duplicate check; continuing processing", { err: e && e.message ? e.message : e }); @@ -55,8 +55,31 @@ async function handler(payload) { logger.warn("Failed to append processing event", { err: e && e.message ? e.message : e }); } + // If this is a push event (no issue attached), handle and persist push data + if (isPush && !issue) { + try { + await sheetsAdapter.appendPush({ event, eventHash: meta.eventHash, actor: meta.actor }); + await sheetsAdapter.appendEvent({ event, eventHash: meta.eventHash, issueKey: null, actor: meta.actor, summary: 'push', status: 'success', meta }); + logger.info('Worker processed push event', { repo: (event.repository && event.repository.full_name) || (event.repository && event.repository.name) || '', ref: event.ref, commits: (event.commits && event.commits.length) || 0 }); + return; + } catch (err) { + logger.error('Worker error processing push', { err: err && err.message ? err.message : err }); + try { + await sheetsAdapter.appendEvent({ event, eventHash: meta.eventHash, issueKey: null, actor: meta.actor, summary: 'push', status: 'failed', meta }); + } catch (e2) { + logger.warn('Failed to append failed push event', { err: e2 && e2.message ? e2.message : e2 }); + } + + try { + await sheetsAdapter.appendDeadLetter({ eventHash: meta.eventHash, issueKey: null, error: err, payload: event, notes: 'push-processing-failed' }); + } catch (e3) { + logger.warn('Failed to append dead letter for push', { err: e3 && e3.message ? e3.message : e3 }); + } + return; + } + } + // If this is a comment event, append comment row and update issue - const giteaEvent = (headers && (headers['x-gitea-event'] || headers['x-gitea-event'.toLowerCase()])) || null; const isComment = Boolean(event && event.comment) || giteaEvent === 'issue_comment'; try {