Files
d4rkbot/tools/post-devlog.mjs
D4rkst3randClaude Fable 5 07afa094a2
Deploy / check (push) Has been cancelled
Deploy / deploy (push) Has been cancelled
tools/post-devlog.mjs: Devlogs aus diesem Repo posten
devlog.py im EcoGame-Repo taugt für Bot-Devlogs nicht: es zählt
EcoGame-Commits, bricht ohne solche ganz ab, betitelt den Post als
„Devlog — EcoGame" und löscht die Bilder nach dem Senden.

Das Skript hier zählt die Commits von d4rkbot, nimmt einen Bilder-Ordner
(max. 4, wie der Endpoint), lässt die Bilder liegen und kann mit --dry
vorher zeigen, was rausginge. Die Webhook-Adresse kommt aus der Umgebung
oder einer Datei und wird nie ausgegeben — auch nicht im Fehlerfall.

Beim Testen gegen einen lokalen Bot ohne Discord gefunden: payload_json
als Blob anzuhängen kommt beim Endpoint als Datei-Teil an und wird
zusammen mit den Nicht-Bildern verworfen — der Post wäre mit „empty
devlog" abgelehnt worden. Muss ein Textfeld sein.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 17:09:10 +02:00

134 lines
4.8 KiB
JavaScript

#!/usr/bin/env node
// Devlog aus DIESEM Repo posten.
//
// tools/devlog.py im EcoGame-Repo geht dafür nicht: es zählt EcoGame-Commits,
// bricht ohne solche ab, betitelt den Post als „Devlog — EcoGame" und löscht
// die Bilder nach dem Senden. Dieses Skript zählt die Commits von d4rkbot,
// lässt die Bilder liegen und kann vorher trocken laufen.
//
// Aufruf:
// node tools/post-devlog.mjs --text docs/devlog-hub.md --images A:/eco/devlog-hub-bilder
// node tools/post-devlog.mjs --text docs/devlog-hub.md --dry
//
// Die Webhook-Adresse (enthält das Secret) kommt aus der Umgebungsvariable
// DEVLOG_WEBHOOK oder aus einer Datei — Standard ist die vorhandene
// A:/eco/EcoGame/tools/.devlog_webhook. Sie wird nie ausgegeben.
import { execFileSync } from 'node:child_process';
import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs';
import { basename, dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const REPO = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const MAX_IMAGES = 4;
const IMAGE_PATTERN = /\.(png|jpe?g|gif|webp)$/i;
function arg(name, fallback = null) {
const i = process.argv.indexOf(`--${name}`);
return i !== -1 && process.argv[i + 1] && !process.argv[i + 1].startsWith('--')
? process.argv[i + 1]
: fallback;
}
const has = (name) => process.argv.includes(`--${name}`);
function webhookUrl() {
const fromEnv = String(process.env.DEVLOG_WEBHOOK ?? '').trim();
if (fromEnv) return fromEnv;
const file = arg('webhook-file', 'A:/eco/EcoGame/tools/.devlog_webhook');
if (!existsSync(file)) {
console.error(
`Keine Webhook-Adresse gefunden.\n` +
` DEVLOG_WEBHOOK setzen oder --webhook-file <datei> angeben.\n` +
` Gesucht wurde: ${file}`
);
process.exit(1);
}
return readFileSync(file, 'utf8').trim();
}
/** Commits dieses Repos seit <since> — nur für die Fußzeile */
function commitCount(since) {
try {
const out = execFileSync('git', ['-C', REPO, 'log', `--since=${since}`, '--oneline', '--no-merges'], {
encoding: 'utf8',
});
return out.split('\n').filter((l) => l.trim()).length;
} catch {
return 0;
}
}
function collectImages(dir) {
if (!dir) return [];
if (!existsSync(dir) || !statSync(dir).isDirectory()) {
console.error(`Bilder-Ordner nicht gefunden: ${dir}`);
process.exit(1);
}
const files = readdirSync(dir).filter((f) => IMAGE_PATTERN.test(f)).sort();
if (files.length > MAX_IMAGES) {
console.warn(`Hinweis: ${files.length} Bilder gefunden, es werden die ersten ${MAX_IMAGES} gepostet.`);
}
return files.slice(0, MAX_IMAGES).map((f) => join(dir, f));
}
const textFile = arg('text');
if (!textFile) {
console.error('Bitte --text <datei> angeben (Markdown mit der Prosa).');
process.exit(1);
}
if (!existsSync(textFile)) {
console.error(`Textdatei nicht gefunden: ${textFile}`);
process.exit(1);
}
const prose = readFileSync(textFile, 'utf8').trim();
if (!prose) {
console.error('Die Textdatei ist leer — nicht gepostet.');
process.exit(1);
}
const project = arg('project', 'd4rkbot');
const since = arg('since', 'yesterday');
const images = collectImages(arg('images'));
const n = commitCount(since);
const description = `${prose}\n\n*${n} Commit${n === 1 ? '' : 's'} heute — Details im [Repo](https://git.d4rkst3r.de/D4rkst3r/${project})*`;
const payload = {
embeds: [{
title: `Devlog — ${project}`,
description,
url: `https://git.d4rkst3r.de/D4rkst3r/${project}`,
}],
};
if (description.length > 3900) {
console.warn(`Achtung: Text ist ${description.length} Zeichen — Discord kürzt ab 4096.`);
}
console.log(`Projekt: ${project}`);
console.log(`Text: ${textFile} (${prose.length} Zeichen)`);
console.log(`Commits: ${n} seit ${since}`);
console.log(`Bilder: ${images.length ? images.map((i) => basename(i)).join(', ') : '— keine —'}`);
if (has('dry')) {
console.log('\n--- Vorschau (nichts gesendet) ---\n');
console.log(description);
process.exit(0);
}
const form = new FormData();
// Als Textfeld anhängen, nicht als Blob: ein Blob käme als Datei-Teil an und
// würde vom Endpoint zusammen mit den Nicht-Bildern verworfen.
form.append('payload_json', JSON.stringify(payload));
for (const [i, file] of images.entries()) {
form.append(`files[${i}]`, new Blob([readFileSync(file)]), basename(file));
}
const res = await fetch(webhookUrl(), { method: 'POST', body: form });
if (!res.ok) {
// Die Adresse enthält das Secret — deshalb nur Status und Antworttext zeigen
console.error(`\nFehlgeschlagen: HTTP ${res.status}`);
console.error(await res.text().catch(() => ''));
process.exit(1);
}
console.log(`\nGepostet (HTTP ${res.status}). Die Bilder bleiben liegen.`);