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
|
```bash
|
||||||
npm version minor -m "Fassung %s" # hebt package.json und setzt ein Git-Etikett
|
npm version minor -m "Fassung %s" # hebt package.json und setzt ein Git-Etikett
|
||||||
git push --follow-tags
|
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
|
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]
|
## [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
|
## [1.1.1] – 2026-10-08
|
||||||
|
|
||||||
### Hinzugefügt
|
### Hinzugefügt
|
||||||
|
|||||||
+2
-1
@@ -23,7 +23,8 @@
|
|||||||
"screenshots": "tsx scripts/screenshots.ts",
|
"screenshots": "tsx scripts/screenshots.ts",
|
||||||
"symbole": "tsx scripts/symbole.ts",
|
"symbole": "tsx scripts/symbole.ts",
|
||||||
"abnahme": "node scripts/pruefungen/abnahme.mjs",
|
"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": {
|
"dependencies": {
|
||||||
"@node-rs/argon2": "^2.0.2",
|
"@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