From 08f7c2145065810dbc773c063271a90403709d7f Mon Sep 17 00:00:00 2001 From: Alexander Zielonka Date: Wed, 9 Sep 2026 17:43:43 +0200 Subject: [PATCH] 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) Claude-Session: https://claude.ai/code/session_01AhVSfMFdWnXKCv5JfLnArZ --- .gitea/workflows/release.yml | 76 ++++++++++++++++++++++++++++++++++ apps/cli/update.test.ts | 11 ++++- apps/cli/update.ts | 10 ++++- config/config.example.yaml | 2 +- docs/handoff.md | 14 +++---- docs/offene-punkte.md | 24 ++--------- packages/core/src/yaml.test.ts | 55 ++++++++++++++++++++++++ packages/core/src/yaml.ts | 6 ++- 8 files changed, 165 insertions(+), 33 deletions(-) create mode 100644 .gitea/workflows/release.yml create mode 100644 packages/core/src/yaml.test.ts diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..56255c5 --- /dev/null +++ b/.gitea/workflows/release.yml @@ -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 diff --git a/apps/cli/update.test.ts b/apps/cli/update.test.ts index 050331f..2da5c01 100644 --- a/apps/cli/update.test.ts +++ b/apps/cli/update.test.ts @@ -5,7 +5,7 @@ import { test } from 'node:test'; 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', () => { // 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); }); + +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 }); +}); diff --git a/apps/cli/update.ts b/apps/cli/update.ts index 7a07664..387e479 100644 --- a/apps/cli/update.ts +++ b/apps/cli/update.ts @@ -56,10 +56,15 @@ export function looksLikeLogin(contentType: string | null, body: Buffer): boolea 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 { const raw = await fs.readFile(path.join(installRoot, 'version.json'), 'utf8').catch(() => null); 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. */ @@ -114,7 +119,8 @@ export async function checkForUpdate(opts: { force?: boolean } = {}): Promise { + 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)); +}); diff --git a/packages/core/src/yaml.ts b/packages/core/src/yaml.ts index 1dc4cb5..699faec 100644 --- a/packages/core/src/yaml.ts +++ b/packages/core/src/yaml.ts @@ -51,7 +51,11 @@ function stripComment(line: string): string { type Frame = { indent: number; target: YamlMap; listKey?: string }; 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 stack: Frame[] = [{ indent: -1, target: root }]; let i = 0;