Releases in Gitea anlegen
npm run release legt zu jedem Etikett v* ohne Release eines an. Der Text ist der Abschnitt der Fassung aus CHANGELOG.md, das Aenderungsprotokoll bleibt also die eine Quelle. Zugang ueber GITEA_TOKEN oder die Zugangsdaten, mit denen Git pusht; sie werden nie ausgegeben. Vorhandene Releases bleiben unangetastet, --trocken zeigt nur an. Die Releases fuer 1.0.0 bis 1.1.1 sind damit nachgetragen. Der Ablauf im Aenderungsprotokoll nennt den Schritt jetzt nach dem Push. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
f980eb5b7f
commit
ff598119ec
+12
-1
@@ -17,15 +17,26 @@ Eine Fassung wird so festgeschrieben:
|
||||
```bash
|
||||
npm version minor -m "Fassung %s" # hebt package.json und setzt ein Git-Etikett
|
||||
git push --follow-tags
|
||||
npm run release # legt in Gitea das Release zum Etikett an
|
||||
```
|
||||
|
||||
Vor `npm version` wandert der Abschnitt „Unveröffentlicht" unter eine neue
|
||||
Überschrift mit Nummer und Datum.
|
||||
Überschrift mit Nummer und Datum. Genau dieser Abschnitt wird der Text des
|
||||
Releases – das Änderungsprotokoll bleibt die eine Quelle. `npm run release`
|
||||
legt nur an, was fehlt, und lässt sich gefahrlos wiederholen; mit
|
||||
`-- --trocken` zeigt es nur an, was es täte.
|
||||
|
||||
---
|
||||
|
||||
## [Unveröffentlicht]
|
||||
|
||||
### Hinzugefügt
|
||||
|
||||
- **Releases in Gitea**, nicht nur Etiketten: `npm run release` legt zu jedem
|
||||
Etikett `v*` ohne Release eines an, mit dem Abschnitt der Fassung aus diesem
|
||||
Änderungsprotokoll als Text. Zugang über `GITEA_TOKEN` oder die Zugangsdaten,
|
||||
mit denen Git pusht. Die Releases für 1.0.0 bis 1.1.1 sind nachgetragen
|
||||
|
||||
## [1.1.1] – 2026-10-08
|
||||
|
||||
### Hinzugefügt
|
||||
|
||||
+2
-1
@@ -23,7 +23,8 @@
|
||||
"screenshots": "tsx scripts/screenshots.ts",
|
||||
"symbole": "tsx scripts/symbole.ts",
|
||||
"abnahme": "node scripts/pruefungen/abnahme.mjs",
|
||||
"abnahme:container": "node scripts/pruefungen/container.mjs"
|
||||
"abnahme:container": "node scripts/pruefungen/container.mjs",
|
||||
"release": "node scripts/release.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@node-rs/argon2": "^2.0.2",
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
|
||||
/**
|
||||
* Legt zu Git-Etiketten die passenden Releases in Gitea an.
|
||||
*
|
||||
* node scripts/release.mjs alle Etiketten v*, denen noch ein Release fehlt
|
||||
* node scripts/release.mjs v1.1.1 nur dieses
|
||||
* node scripts/release.mjs --trocken nur anzeigen, was angelegt würde
|
||||
*
|
||||
* Der Text des Releases ist der Abschnitt der Fassung aus CHANGELOG.md – das
|
||||
* Änderungsprotokoll bleibt die eine Quelle, das Release zeigt es nur an.
|
||||
*
|
||||
* Zugang: GITEA_TOKEN, falls gesetzt, sonst dieselben Zugangsdaten, mit denen
|
||||
* Git pusht (`git credential fill`). Sie werden nie ausgegeben.
|
||||
*
|
||||
* Bereits vorhandene Releases bleiben unangetastet – der Lauf lässt sich also
|
||||
* beliebig oft wiederholen.
|
||||
*/
|
||||
|
||||
const argumente = process.argv.slice(2);
|
||||
const trocken = argumente.includes('--trocken');
|
||||
const gewuenscht = argumente.filter((a) => !a.startsWith('--'));
|
||||
|
||||
function git(...befehl) {
|
||||
const ergebnis = spawnSync('git', befehl, { encoding: 'utf8' });
|
||||
if (ergebnis.status !== 0) throw new Error(`git ${befehl.join(' ')}: ${ergebnis.stderr.trim()}`);
|
||||
return ergebnis.stdout.trim();
|
||||
}
|
||||
|
||||
// --- Wohin? ------------------------------------------------------------------
|
||||
|
||||
const remote = git('remote', 'get-url', 'origin');
|
||||
const treffer = remote.match(/^(?:https?:\/\/(?:[^@/]+@)?([^/]+)|[^@]+@([^:]+):)\/?(.+?)\/(.+?)(?:\.git)?$/);
|
||||
if (!treffer) {
|
||||
console.error(`Die Adresse von origin verstehe ich nicht: ${remote}`);
|
||||
process.exit(2);
|
||||
}
|
||||
const host = treffer[1] ?? treffer[2];
|
||||
const [besitzer, repo] = [treffer[3], treffer[4]];
|
||||
const API = `https://${host}/api/v1/repos/${besitzer}/${repo}`;
|
||||
|
||||
// --- Womit? ------------------------------------------------------------------
|
||||
|
||||
function anmeldung() {
|
||||
if (process.env.GITEA_TOKEN) return `token ${process.env.GITEA_TOKEN}`;
|
||||
const ergebnis = spawnSync('git', ['credential', 'fill'], {
|
||||
input: `protocol=https\nhost=${host}\n\n`,
|
||||
encoding: 'utf8',
|
||||
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
||||
});
|
||||
const felder = Object.fromEntries(
|
||||
ergebnis.stdout
|
||||
.split('\n')
|
||||
.filter((z) => z.includes('='))
|
||||
.map((z) => [z.slice(0, z.indexOf('=')), z.slice(z.indexOf('=') + 1)]),
|
||||
);
|
||||
if (!felder.username || !felder.password) {
|
||||
console.error('Keine Zugangsdaten gefunden. GITEA_TOKEN setzen oder einmal von Hand pushen.');
|
||||
process.exit(2);
|
||||
}
|
||||
return `Basic ${Buffer.from(`${felder.username}:${felder.password}`).toString('base64')}`;
|
||||
}
|
||||
|
||||
// --- Was? --------------------------------------------------------------------
|
||||
|
||||
/** Die Abschnitte des Änderungsprotokolls, je Fassung. */
|
||||
function abschnitte() {
|
||||
const text = fs.readFileSync('CHANGELOG.md', 'utf8');
|
||||
const teile = new Map();
|
||||
const kopf = /^## \[(\d+\.\d+\.\d+)\] – (\d{4}-\d{2}-\d{2})\s*$/gm;
|
||||
const funde = [...text.matchAll(kopf)];
|
||||
funde.forEach((fund, i) => {
|
||||
const ende = i + 1 < funde.length ? funde[i + 1].index : text.length;
|
||||
const rumpf = text
|
||||
.slice(fund.index + fund[0].length, ende)
|
||||
.replace(/\n---\s*$/, '')
|
||||
.trim();
|
||||
teile.set(fund[1], { datum: fund[2], rumpf });
|
||||
});
|
||||
return teile;
|
||||
}
|
||||
|
||||
const etiketten = git('tag', '--list', 'v*', '--sort=v:refname').split('\n').filter(Boolean);
|
||||
const ziele = gewuenscht.length > 0 ? gewuenscht : etiketten;
|
||||
const texte = abschnitte();
|
||||
|
||||
async function anfrage(pfad, art = 'GET', koerper) {
|
||||
const antwort = await fetch(`${API}${pfad}`, {
|
||||
method: art,
|
||||
headers: { Authorization: anmeldung(), 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||
body: koerper ? JSON.stringify(koerper) : undefined,
|
||||
});
|
||||
return antwort;
|
||||
}
|
||||
|
||||
let fehler = 0;
|
||||
for (const etikett of ziele) {
|
||||
if (!etiketten.includes(etikett)) {
|
||||
console.error(`✗ ${etikett}: Dieses Etikett gibt es nicht.`);
|
||||
fehler += 1;
|
||||
continue;
|
||||
}
|
||||
const fassung = etikett.replace(/^v/, '');
|
||||
const abschnitt = texte.get(fassung);
|
||||
if (!abschnitt) {
|
||||
console.error(`✗ ${etikett}: Im Änderungsprotokoll fehlt der Abschnitt [${fassung}].`);
|
||||
fehler += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const vorhanden = await anfrage(`/releases/tags/${etikett}`);
|
||||
if (vorhanden.status === 200) {
|
||||
console.log(`· ${etikett}: Release gibt es schon.`);
|
||||
continue;
|
||||
}
|
||||
if (vorhanden.status !== 404) {
|
||||
console.error(`✗ ${etikett}: Gitea antwortet ${vorhanden.status} auf die Abfrage.`);
|
||||
fehler += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const release = {
|
||||
tag_name: etikett,
|
||||
name: `Fassung ${fassung} – ${abschnitt.datum}`,
|
||||
body: abschnitt.rumpf,
|
||||
draft: false,
|
||||
prerelease: false,
|
||||
};
|
||||
|
||||
if (trocken) {
|
||||
console.log(`○ ${etikett}: würde angelegt als „${release.name}" (${release.body.length} Zeichen Text).`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const angelegt = await anfrage('/releases', 'POST', release);
|
||||
if (angelegt.status === 201) {
|
||||
console.log(`✓ ${etikett}: Release angelegt.`);
|
||||
} else {
|
||||
console.error(`✗ ${etikett}: Gitea antwortet ${angelegt.status} – ${(await angelegt.text()).slice(0, 200)}`);
|
||||
fehler += 1;
|
||||
}
|
||||
}
|
||||
|
||||
process.exit(fehler === 0 ? 0 : 1);
|
||||
Reference in New Issue
Block a user