From 01d3e7949308f518977a666b3d3078d99ec376aa Mon Sep 17 00:00:00 2001 From: Heidi Date: Thu, 14 May 2026 21:11:32 +0100 Subject: [PATCH] fix --- README.md | 3 ++ Tree/FallingLeaves.tsx | 1 - webhook.cjs | 111 ++++++++++++++++++++++++++++++++++++----- 3 files changed, 102 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 223e440..9cd1d37 100644 --- a/README.md +++ b/README.md @@ -107,3 +107,6 @@ listener restarts and GitHub push deploy start/success/failure events, set: ```sh DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/... ``` + +Deploy completion and failure notifications include a changed-file summary and a +truncated patch preview for the pushed diff. diff --git a/Tree/FallingLeaves.tsx b/Tree/FallingLeaves.tsx index 488b107..06922c5 100644 --- a/Tree/FallingLeaves.tsx +++ b/Tree/FallingLeaves.tsx @@ -1,7 +1,6 @@ import React, { useEffect, useRef } from "react"; import { TRUNK_TOP_CSS } from "./Tree"; -// aaa interface Leaf { x: number; y: number; diff --git a/webhook.cjs b/webhook.cjs index 0b73bc8..c33af2f 100644 --- a/webhook.cjs +++ b/webhook.cjs @@ -74,6 +74,17 @@ function truncate(value, maxLength = 900) { return `${text.slice(0, maxLength - 3)}...` } +function codeBlock(value, language = '', maxLength = 1000) { + const fence = language ? `\`\`\`${language}\n` : '```\n' + const suffix = '\n```' + const body = truncate(value || 'No diff returned', maxLength - fence.length - suffix.length) + return `${fence}${body}${suffix}` +} + +function validGitSha(value) { + return /^[0-9a-f]{7,40}$/i.test(String(value || '')) +} + function verifySignature(rawBody, signature) { if (!SECRET) return true if (!signature || !signature.startsWith('sha256=')) return false @@ -119,6 +130,35 @@ function run(command, args, options = {}) { }) } +function runCapture(command, args, options = {}) { + return new Promise((resolve, reject) => { + const label = [command, ...args].join(' ') + const child = spawn(commandFor(command), args, { + cwd: __dirname, + env: { ...process.env, ...options.env }, + shell: process.platform === 'win32', + stdio: ['ignore', 'pipe', 'pipe'], + }) + let stdout = '' + let stderr = '' + + child.stdout.on('data', (chunk) => { + stdout += chunk + }) + child.stderr.on('data', (chunk) => { + stderr += chunk + }) + child.on('error', reject) + child.on('exit', (code) => { + if (code === 0) { + resolve(stdout.trim()) + } else { + reject(new Error(`${label} exited with ${code}: ${truncate(stderr || stdout, 500)}`)) + } + }) + }) +} + async function ensureBuildDependencies() { await run('npm', ['install', '--production=false', '--include=dev', '--include=optional'], { env: { @@ -230,6 +270,44 @@ function deployFields(push) { return fields } +async function deployDiff(push) { + const before = push?.before + const after = push?.after + + if (!validGitSha(before) || !validGitSha(after) || /^0+$/.test(before)) { + return { + summary: 'Diff unavailable for this push range', + patch: push?.compare ? `Compare: ${push.compare}` : '', + } + } + + const range = `${before}..${after}` + const [nameStatus, shortStat, patch] = await Promise.all([ + runCapture('git', ['diff', '--name-status', '--find-renames', range]), + runCapture('git', ['diff', '--shortstat', range]), + runCapture('git', ['diff', '--find-renames', '--unified=1', range]), + ]) + + return { + summary: [shortStat, nameStatus].filter(Boolean).join('\n'), + patch, + } +} + +function diffFields(diff) { + if (!diff) return [] + + const fields = [] + if (diff.summary) { + fields.push({ name: 'Diff summary', value: codeBlock(diff.summary, 'diff'), inline: false }) + } + if (diff.patch) { + fields.push({ name: 'Patch preview', value: codeBlock(diff.patch, 'diff'), inline: false }) + } + + return fields +} + async function notifyDeployStarted(push) { await sendDiscordEmbed({ title: 'GitHub push deploy started', @@ -239,21 +317,22 @@ async function notifyDeployStarted(push) { }) } -async function notifyDeployCompleted(push) { +async function notifyDeployCompleted(push, diff) { await sendDiscordEmbed({ title: 'GitHub push deploy completed', color: 0x00f2ff, - fields: deployFields(push), + fields: [...deployFields(push), ...diffFields(diff)], timestamp: new Date().toISOString(), }) } -async function notifyDeployFailed(push, error) { +async function notifyDeployFailed(push, error, diff) { await sendDiscordEmbed({ title: 'GitHub push deploy failed', color: 0xe82517, fields: [ ...deployFields(push), + ...diffFields(diff), { name: 'Error', value: truncate(error?.message || error), inline: false }, ], timestamp: new Date().toISOString(), @@ -261,16 +340,24 @@ async function notifyDeployFailed(push, error) { } async function deploy(push) { - await notifyDeployStarted(push) - await run('git', ['pull', '--ff-only']) - await ensureBuildDependencies() - await run('npm', ['run', 'build']) + let diff = null - for (const target of RESTART_TARGETS) { - await run('pm2', ['reload', target, '--update-env']) + try { + await notifyDeployStarted(push) + await run('git', ['pull', '--ff-only']) + diff = await deployDiff(push) + await ensureBuildDependencies() + await run('npm', ['run', 'build']) + + for (const target of RESTART_TARGETS) { + await run('pm2', ['reload', target, '--update-env']) + } + + await notifyDeployCompleted(push, diff) + } catch (error) { + error.deployDiff = diff + throw error } - - await notifyDeployCompleted(push) } http @@ -313,7 +400,7 @@ http .then(() => console.log('GitHub push deploy completed')) .catch((error) => { console.error('GitHub push deploy failed:', error) - notifyDeployFailed(push, error) + notifyDeployFailed(push, error, error.deployDiff) }) .finally(() => { deploying = false