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
This commit is contained in:
1 parent
5f6301268a
commit
08f7c21450
8 files changed
+165
-33
No files matched your search
@@ -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
@@ -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
@@ -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)) {
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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));
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
|
|||||||
Reference in new issue
Block a user