1 Commits
Author SHA1 Message Date
ZeasyAlexandClaude Opus 5 08f7c21450 Veroeffentlichung ueber Gitea, BOM-Toleranz, Tests fuer den YAML-Leser
Das Repo ist jetzt oeffentlich, die Pakete haengen an der Veroeffentlichung
`latest`. Damit funktioniert `abdulive update` ohne Token.

- config.example.yaml traegt die gepruefte Update-Adresse
- .gitea/workflows/release.yml baut beim Tag und ersetzt `latest`
- Byte Order Mark: Notepad und `Set-Content -Encoding utf8` schreiben eines,
  und ohne Behandlung hiess der erste Schluessel in der config.yaml
  "\ufeffchannel" - die Einstellung waere stillschweigend ignoriert worden
- Der YAML-Leser hatte gar keine Tests; er liest die Konfiguration, an der
  alles haengt. Jetzt acht.

Der Updater ist einmal komplett durchgelaufen: 0.4.0 erkannt, 119 MB geladen,
Pruefsumme geprueft, bereitgelegt, von launcher.ps1 getauscht.

93 Tests, Typpruefung sauber.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AhVSfMFdWnXKCv5JfLnArZ
2026-09-09 17:43:43 +02:00
8 changed files with 165 additions and 33 deletions

No files matched your search

+76
View File
@@ -0,0 +1,76 @@
# Baut das Windows-Paket und haengt es an die Veroeffentlichung `latest`.
#
# Ausgeloest von einem Tag `v0.5.1` oder von Hand ueber die Actions-Seite.
# Der Updater auf Abdus Rechner fragt immer dieselbe Adresse ab:
# https://git.zeasy.dev/ZeasyAlex/abdulive/releases/download/latest/abdulive.json
# Deshalb wird `latest` bei jedem Lauf geloescht und neu angelegt.
name: Paket bauen und veroeffentlichen
on:
push:
tags: ['v*']
workflow_dispatch:
inputs:
version:
description: 'Versionsnummer, z. B. 0.5.1'
required: true
jobs:
paket:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '24'
- name: Tests und Typpruefung
run: |
node --test "packages/**/*.test.ts" "apps/**/*.test.ts"
npm i --no-save typescript@5.9 @types/node
npx tsc --noEmit
- name: Version bestimmen
id: v
run: |
if [ -n "${{ inputs.version }}" ]; then
echo "wert=${{ inputs.version }}" >> "$GITHUB_OUTPUT"
else
echo "wert=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
fi
# Laedt Node, ffmpeg und whisper.cpp bei ihren Herausgebern und packt
# alles zusammen. Siehe scripts/build-package.ts und ADR-0007.
- name: Paket bauen
run: node scripts/build-package.ts --version "${{ steps.v.outputs.wert }}"
- name: Veroeffentlichung ersetzen
env:
TOKEN: ${{ secrets.GITEA_TOKEN }}
API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
VERSION: ${{ steps.v.outputs.wert }}
run: |
set -eu
auth="Authorization: token $TOKEN"
# Vorhandene `latest` weg - sie soll immer den aktuellen Stand tragen
alt=$(curl -sf -H "$auth" "$API/releases/tags/latest" \
| node -e 'let d="";process.stdin.on("data",c=>d+=c).on("end",()=>{try{process.stdout.write(String(JSON.parse(d).id))}catch{}})' || true)
if [ -n "$alt" ]; then
curl -sf -X DELETE -H "$auth" "$API/releases/$alt" || true
fi
id=$(curl -sf -H "$auth" -H 'Content-Type: application/json' -X POST "$API/releases" \
-d "{\"tag_name\":\"latest\",\"target_commitish\":\"$GITHUB_SHA\",\"name\":\"abdulive $VERSION\",\"body\":\"Paket entpacken und darin install.ps1 ausfuehren.\"}" \
| node -e 'let d="";process.stdin.on("data",c=>d+=c).on("end",()=>process.stdout.write(String(JSON.parse(d).id)))')
for f in dist/abdulive-"$VERSION"-win-x64.zip dist/abdulive.json; do
curl -sf -H "$auth" -X POST "$API/releases/$id/assets?name=$(basename "$f")" \
-F "attachment=@$f" -o /dev/null
echo " hochgeladen: $(basename "$f")"
done
- name: Was drin ist
run: cat dist/abdulive.json
+10 -1
View File
@@ -5,7 +5,7 @@
import { test } from 'node:test'; import { test } from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { isNewer, looksLikeLogin } from './update.ts'; import { isNewer, looksLikeLogin, parseJsonLoose } from './update.ts';
test('vergleicht Versionen als Zahlen, nicht als Text', () => { test('vergleicht Versionen als Zahlen, nicht als Text', () => {
// Der Fall, der bei Zeichenkettenvergleich falsch herauskaeme // Der Fall, der bei Zeichenkettenvergleich falsch herauskaeme
@@ -42,3 +42,12 @@ test('haelt ein echtes Manifest nicht faelschlich fuer HTML', () => {
})); }));
assert.equal(looksLikeLogin(null, manifest), false); assert.equal(looksLikeLogin(null, manifest), false);
}); });
test('ein Byte Order Mark macht das Manifest nicht unlesbar', () => {
// Windows PowerShell 5.1 schreibt bei `Set-Content -Encoding utf8` eines,
// und Notepad ebenso. JSON.parse verschluckt sich daran.
const mitBom = '{"version":"0.5.0"}';
assert.throws(() => JSON.parse(mitBom));
assert.deepEqual(parseJsonLoose(mitBom), { version: '0.5.0' });
assert.deepEqual(parseJsonLoose('{"a":1}'), { a: 1 });
});
+8 -2
View File
@@ -56,10 +56,15 @@ export function looksLikeLogin(contentType: string | null, body: Buffer): boolea
head.includes('/user/login'); head.includes('/user/login');
} }
/** JSON.parse verschluckt sich an einem Byte Order Mark - Windows schreibt eines. */
export function parseJsonLoose(text: string): unknown {
return JSON.parse(text.replace(/^/, ''));
}
async function localVersion(): Promise<string> { async function localVersion(): Promise<string> {
const raw = await fs.readFile(path.join(installRoot, 'version.json'), 'utf8').catch(() => null); const raw = await fs.readFile(path.join(installRoot, 'version.json'), 'utf8').catch(() => null);
if (!raw) return '0'; if (!raw) return '0';
return (JSON.parse(raw) as { version?: string }).version ?? '0'; return (parseJsonLoose(raw) as { version?: string }).version ?? '0';
} }
/** Adresse und - falls hinterlegt - der Zugriffstoken. */ /** Adresse und - falls hinterlegt - der Zugriffstoken. */
@@ -114,7 +119,8 @@ export async function checkForUpdate(opts: { force?: boolean } = {}): Promise<vo
console.log(`Quelle: ${url}`); console.log(`Quelle: ${url}`);
console.log(`Installiert: ${current}`); console.log(`Installiert: ${current}`);
const manifest = JSON.parse((await hole(`${url}/abdulive.json`, headers)).toString('utf8')) as Manifest; const manifest = parseJsonLoose(
(await hole(`${url}/abdulive.json`, headers)).toString('utf8')) as Manifest;
console.log(`Angeboten: ${manifest.version}`); console.log(`Angeboten: ${manifest.version}`);
if (!opts.force && !isNewer(manifest.version, current)) { if (!opts.force && !isNewer(manifest.version, current)) {
+1 -1
View File
@@ -29,7 +29,7 @@ ingest:
# Anmeldung (privates Gitea), gehoert der Zugriffstoken NICHT hierher, # Anmeldung (privates Gitea), gehoert der Zugriffstoken NICHT hierher,
# sondern auf die Einrichtungsseite - siehe ADR-0005. # sondern auf die Einrichtungsseite - siehe ADR-0005.
update: update:
url: "" url: "https://git.zeasy.dev/ZeasyAlex/abdulive/releases/download/latest"
obs: obs:
url: ws://127.0.0.1:4455 url: ws://127.0.0.1:4455
+7 -7
View File
@@ -60,9 +60,10 @@ abdulive fetch-model
abdulive check-env abdulive check-env
``` ```
Einen Einzeiler zum Nachladen gibt es nicht — `git.zeasy.dev` ist komplett Das Repo ist seit 2026-09-09 oeffentlich, die Pakete haengen an der
angemeldet-only. **Wo die Pakete liegen sollen, ist die naechste offene Veroeffentlichung `latest`. Einen `irm … | iex`-Einzeiler gibt es trotzdem
Entscheidung**, und ohne sie funktioniert auch `abdulive update` nicht. nicht — Rohdateien liefert Gitea als `text/plain`, und ein Fehlschlag saehe
aus wie ein PowerShell-Syntaxfehler statt wie ein Fehlschlag.
Ausführlich in [`05-setup.md`](05-setup.md). Ausführlich in [`05-setup.md`](05-setup.md).
@@ -120,7 +121,6 @@ Verbesserungen stehen in [`offene-punkte.md`](offene-punkte.md).
Und: Das Aufräumen im Arbeitsordner fehlt komplett. Rohdateien sammeln sich Und: Das Aufräumen im Arbeitsordner fehlt komplett. Rohdateien sammeln sich
unbegrenzt an. Das ist die Lücke, die als Erstes weh tun wird. unbegrenzt an. Das ist die Lücke, die als Erstes weh tun wird.
Der Updater ist gebaut und in Teilen geprüft — Manifest, Download, Prüfsumme Der Updater ist einmal komplett durchgelaufen: 0.4.0 erkannt, 119 MB von
und Bereitlegen laufen. Der eigentliche Tausch durch `launcher.ps1` ist noch `git.zeasy.dev` geladen, Prüfsumme geprüft, bereitgelegt, von `launcher.ps1`
nie im Feld passiert. Vor dem ersten echten Update einmal künstlich getauscht, `pending` aufgeräumt. Was noch nie lief, ist die CI-Pipeline.
durchspielen.
+3 -21
View File
@@ -68,33 +68,15 @@ Der Durchlauf am echten Clip war brauchbar, aber nicht gut. Was auffiel:
## Auslieferung ## Auslieferung
- [ ] **Wo liegen die Pakete?** Das ist die Entscheidung, die als Naechstes
ansteht — ohne sie funktioniert `abdulive update` gar nicht.
`git.zeasy.dev` verlangt **instanzweit** eine Anmeldung: `/explore`,
die API und jede Rohdatei antworten mit 303 auf `/user/login`. Ein
`irm … | iex`-Einzeiler bekommt dadurch die Anmeldeseite und versucht,
sie als PowerShell auszufuehren. Drei Wege:
*(a)* eine Ablage ohne Anmeldung, etwa `dl.zeasy.software` — am
einfachsten, und das Paket enthaelt ohnehin den vollstaendigen Quelltext
als lesbare `.ts`-Dateien;
*(b)* Gitea-Release plus Lese-Token auf Abdus Rechner — der Token darf
dann aber das ganze private Repo lesen;
*(c)* kein Updater, Alexander spielt neue Fassungen per Fernwartung ein.
Code und Installer sind auf *(a)* und *(b)* vorbereitet: `update.url` in
der `config.yaml`, Token im verschluesselten Speicher.
- [ ] **Code-Signing.** Das Paket ist nicht signiert, SmartScreen warnt bei - [ ] **Code-Signing.** Das Paket ist nicht signiert, SmartScreen warnt bei
jeder Installation vor einem unbekannten Herausgeber. Ein jeder Installation vor einem unbekannten Herausgeber. Ein
OV-Zertifikat kostet rund 200 EUR im Jahr, ein EV-Zertifikat mehr und OV-Zertifikat kostet rund 200 EUR im Jahr, ein EV-Zertifikat mehr und
braucht Hardware. Solange nur Abdu installiert, reicht ein Klick auf braucht Hardware. Solange nur Abdu installiert, reicht ein Klick auf
"Trotzdem ausfuehren" - sobald jemand Drittes das Paket bekommt, nicht "Trotzdem ausfuehren" - sobald jemand Drittes das Paket bekommt, nicht
mehr. mehr.
- [ ] **CI auf git.zeasy.dev.** `scripts/build-package.ts` laeuft bisher von - [ ] **CI einmal wirklich laufen lassen.** `.gitea/workflows/release.yml`
Hand. Als Forgejo-Action beim Tag gebaut und als Veroeffentlichung ist geschrieben, der Runner (`ubuntu-latest`) steht — ausgefuehrt wurde
hochgeladen, findet `abdulive update` sie von allein. sie aber noch nie. Beim ersten Tag zusehen, ob sie durchlaeuft.
- [ ] **Update noch nie im Feld gelaufen.** Bereitlegen und Pruefsumme sind
getestet, der Tausch durch `launcher.ps1` nur gelesen. Vor dem ersten
echten Update einmal kuenstlich durchspielen.
- [ ] **Hardware-Encoding.** Der mitgelieferte ffmpeg kann QSV, AMF und NVENC - [ ] **Hardware-Encoding.** Der mitgelieferte ffmpeg kann QSV, AMF und NVENC
(`--enable-libvpl`, `--enable-amf`, `--enable-ffnvcodec`) - aus WSL2 (`--enable-libvpl`, `--enable-amf`, `--enable-ffnvcodec`) - aus WSL2
heraus ging das nicht. `clipper.ffmpeg.encoder` steht weiter auf heraus ging das nicht. `clipper.ffmpeg.encoder` steht weiter auf
+55
View File
@@ -0,0 +1,55 @@
// Der YAML-Leser ist selbst geschrieben (ADR-0001: keine Abhaengigkeiten) und
// liest die Konfiguration, an der alles haengt - hatte aber bis hierher keinen
// einzigen Test. Geprueft wird genau der Sprachumfang, den config/*.yaml
// benutzt.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { parseYaml } from './yaml.ts';
test('verschachtelte Maps ueber Einrueckung', () => {
assert.deepEqual(parseYaml('channel:\n login: abdulive_\n language: de\n'),
{ channel: { login: 'abdulive_', language: 'de' } });
});
test('erkennt Zahlen, Wahrheitswerte und Leeres', () => {
const y = parseYaml([
'a: 42', 'b: -7', 'c: 3.5', 'd: true', 'e: false', 'f:', 'g: ~',
].join('\n'));
assert.deepEqual(y, { a: 42, b: -7, c: 3.5, d: true, e: false, f: null, g: null });
});
test('Anfuehrungszeichen bleiben Text', () => {
// Wichtig fuer update.url - sonst wuerde aus "0.5" eine Zahl
const y = parseYaml('update:\n url: "https://git.zeasy.dev/x"\n v: "0.5"\n');
assert.deepEqual(y, { update: { url: 'https://git.zeasy.dev/x', v: '0.5' } });
});
test('Inline-Maps, wie sie im Layout stehen', () => {
const y = parseYaml('crop: { x: 0, y: 0, w: 480, h: 270 }\n');
assert.deepEqual(y, { crop: { x: 0, y: 0, w: 480, h: 270 } });
});
test('Listen aus Skalaren', () => {
const y = parseYaml('chapterScenes: [Gameplay, "Just Chatting"]\n');
assert.deepEqual(y, { chapterScenes: ['Gameplay', 'Just Chatting'] });
});
test('Kommentare und Leerzeilen stoeren nicht', () => {
const y = parseYaml('# Kopfzeile\n\nobs:\n # dazwischen\n url: ws://127.0.0.1:4455\n');
assert.deepEqual(y, { obs: { url: 'ws://127.0.0.1:4455' } });
});
test('Zeilenenden nach Windows-Art', () => {
// config.yaml wird auf dem Stream-PC bearbeitet
assert.deepEqual(parseYaml('a:\r\n b: 1\r\n'), { a: { b: 1 } });
});
test('ein Byte Order Mark verstellt nicht den ersten Schluessel', () => {
// Notepad und `Set-Content -Encoding utf8` unter Windows PowerShell 5.1
// schreiben eines. Ohne Behandlung hiesse der erste Schluessel "channel",
// und die Einstellung wuerde stillschweigend ignoriert - ohne Fehlermeldung.
const y = parseYaml('\uFEFFchannel:\n login: abdulive_\n');
assert.deepEqual(y, { channel: { login: 'abdulive_' } });
assert.ok(!Object.keys(y).some((k) => k.charCodeAt(0) === 0xfeff));
});
+5 -1
View File
@@ -51,7 +51,11 @@ function stripComment(line: string): string {
type Frame = { indent: number; target: YamlMap; listKey?: string }; type Frame = { indent: number; target: YamlMap; listKey?: string };
export function parseYaml(text: string): YamlMap { export function parseYaml(text: string): YamlMap {
const lines = text.split(/\r?\n/); // Byte Order Mark abschneiden. Notepad und `Set-Content -Encoding utf8`
// unter Windows PowerShell 5.1 schreiben eines an den Dateianfang; ohne
// diese Zeile heisst der erste Schluessel dann "channel" und die
// Einstellung wird stillschweigend ignoriert.
const lines = text.replace(/^/, '').split(/\r?\n/);
const root: YamlMap = {}; const root: YamlMap = {};
const stack: Frame[] = [{ indent: -1, target: root }]; const stack: Frame[] = [{ indent: -1, target: root }];
let i = 0; let i = 0;