Compare commits

..
4 Commits
Author SHA1 Message Date
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
94d898e445 chore(deps): bump the npm-production group across 1 directory with 6 updates (#30)
Bumps the npm-production group with 6 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@anthropic-ai/sdk](https://github.com/anthropics/anthropic-sdk-typescript) | `0.39.0` | `0.111.0` |
| [dotenv](https://github.com/motdotla/dotenv) | `16.6.1` | `17.4.2` |
| [express](https://github.com/expressjs/express) | `4.22.2` | `5.2.1` |
| [helmet](https://github.com/helmetjs/helmet) | `8.2.0` | `8.3.0` |
| [openai](https://github.com/openai/openai-node) | `4.104.0` | `6.46.0` |
| [sharp](https://github.com/lovell/sharp) | `0.33.5` | `0.35.3` |



Updates `@anthropic-ai/sdk` from 0.39.0 to 0.111.0
- [Release notes](https://github.com/anthropics/anthropic-sdk-typescript/releases)
- [Changelog](https://github.com/anthropics/anthropic-sdk-typescript/blob/main/CHANGELOG.md)
- [Commits](https://github.com/anthropics/anthropic-sdk-typescript/compare/sdk-v0.39.0...sdk-v0.111.0)

Updates `dotenv` from 16.6.1 to 17.4.2
- [Changelog](https://github.com/motdotla/dotenv/blob/master/CHANGELOG.md)
- [Commits](https://github.com/motdotla/dotenv/compare/v16.6.1...v17.4.2)

Updates `express` from 4.22.2 to 5.2.1
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/v4.22.2...v5.2.1)

Updates `helmet` from 8.2.0 to 8.3.0
- [Changelog](https://github.com/helmetjs/helmet/blob/main/CHANGELOG.md)
- [Commits](https://github.com/helmetjs/helmet/compare/v8.2.0...v8.3.0)

Updates `openai` from 4.104.0 to 6.46.0
- [Release notes](https://github.com/openai/openai-node/releases)
- [Changelog](https://github.com/openai/openai-node/blob/main/CHANGELOG.md)
- [Commits](https://github.com/openai/openai-node/compare/v4.104.0...v6.46.0)

Updates `sharp` from 0.33.5 to 0.35.3
- [Release notes](https://github.com/lovell/sharp/releases)
- [Commits](https://github.com/lovell/sharp/compare/v0.33.5...v0.35.3)

---
updated-dependencies:
- dependency-name: "@anthropic-ai/sdk"
  dependency-version: 0.111.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-production
- dependency-name: dotenv
  dependency-version: 17.4.2
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: npm-production
- dependency-name: express
  dependency-version: 5.2.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: npm-production
- dependency-name: helmet
  dependency-version: 8.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-production
- dependency-name: openai
  dependency-version: 6.46.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: npm-production
- dependency-name: sharp
  dependency-version: 0.35.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-14 17:40:41 +02:00
Masterluke77 0a3d0b86e5 revert: remove unintended UX placeholder 2026-07-11 02:55:04 +02:00
Masterluke77 20f2e9a299 placeholder 2026-07-11 02:54:56 +02:00
Masterluke77andGitHub 637275effc Updater 3.4 – Lifecycle Hardening (#28)
Production-ready updater lifecycle hardening with final-head checks, Windows PowerShell 5.1 compatibility, reproducible package generation, provenance validation, payload rollback protection and documented acceptance.
2026-07-10 19:54:36 +02:00
24 changed files with 2118 additions and 1023 deletions
+92 -2
View File
@@ -5,6 +5,7 @@ on:
branches: [main, develop, 'feature/**', 'release/**', 'hotfix/**']
pull_request:
branches: [main, develop]
workflow_dispatch:
permissions:
contents: read
@@ -42,16 +43,21 @@ jobs:
- name: Git-Staging-Filter Regressionstest
run: npm run verify:git-staging
- name: Release-Manifest und Paketvertrag prüfen
run: npm run verify:release-manifest
- name: Git-Ignore-Grenzen für Quell- und Runtimeordner prüfen
run: npm run verify:ignore-boundaries
- name: Syntaxprüfung
run: npm run check
- name: PowerShell-Regressionen statisch prüfen
run: npm run verify:powershell-static
- name: Updater-3.4-Lifecycle und Startkette prüfen
run: npm run verify:updater-start-chain && npm run verify:updater-lifecycle
- name: Produktiven Source-Root und Archivgrenzen prüfen
run: npm run verify:source-boundaries
@@ -140,9 +146,93 @@ jobs:
- name: Alle PowerShell-Dateien mit Windows PowerShell 5.1 parsen
shell: powershell
run: .\tools\verify-powershell-parser.ps1
run: .\tools\verify-powershell-parser.ps1 -ReportPath "$env:RUNNER_TEMP\powershell-parser-diagnostics.txt"
- name: Parserdiagnose bei Fehler sichern
if: failure()
uses: actions/upload-artifact@v4
with:
name: powershell-parser-diagnostics-${{ github.event.pull_request.head.sha || github.sha }}
path: ${{ runner.temp }}/powershell-parser-diagnostics.txt
if-no-files-found: error
retention-days: 14
- name: Vollständigen Vendoo-Releasevertrag unter Windows ausführen
shell: pwsh
run: npm run verify:all
package-after-acceptance:
name: Updater-3.4-Paket nach Abnahme
needs: [quality, windows-release-contract]
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Exakten geprüften Head auschecken
uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0
- name: Node.js 22 aktivieren
uses: actions/setup-node@v4
with:
node-version: '22.x'
cache: npm
- name: Abhängigkeiten reproduzierbar installieren
run: npm ci
- name: main für den Provenienzvergleich aktualisieren
run: git fetch origin main:refs/remotes/origin/main
- name: Reproduzierbares Root-Manifest bestätigen
shell: bash
run: |
node tools/generate-release-manifest.mjs .
git diff --exit-code -- release-manifest.json
- name: Paket mit unveränderlicher Provenienz erzeugen
env:
VENDOO_SOURCE_COMMIT: ${{ github.event.pull_request.head.sha || github.sha }}
run: node tools/build-release.mjs
- name: Paketmanifest und SHA-256 prüfen
shell: bash
run: |
set -euo pipefail
release_name="$(tr -d '\r\n' < release-output/RELEASE_NAME.txt)"
package_dir="release-output/${release_name}"
node --input-type=module - "$package_dir" <<'NODE'
import { readAndValidateManifest } from './tools/release-manifest-lib.mjs';
const packageDir = process.argv[2];
const manifest = readAndValidateManifest(packageDir);
if (manifest.schema !== 'vendoo-release-package-v2') throw new Error(`Unerwartetes Paketschema: ${manifest.schema}`);
if (manifest.updaterVersion !== '3.4.0') throw new Error(`Unerwartete Updater-Version: ${manifest.updaterVersion}`);
if (manifest.targetVersion !== '1.43.0') throw new Error(`Unerwartete Zielversion: ${manifest.targetVersion}`);
console.log(`Paket validiert: ${manifest.targetVersion}, Updater ${manifest.updaterVersion}, Payload ${manifest.payloadHash}`);
NODE
archive="release-output/${release_name}.zip"
python tools/build-deterministic-zip.py "$package_dir" "$archive"
first_hash="$(sha256sum "$archive" | awk '{print $1}')"
python tools/build-deterministic-zip.py "$package_dir" "$archive"
second_hash="$(sha256sum "$archive" | awk '{print $1}')"
if [[ "$first_hash" != "$second_hash" ]]; then
echo "Der ZIP-Build ist nicht byte-reproduzierbar." >&2
exit 1
fi
python -m zipfile -t "$archive"
(
cd release-output
sha256sum "${release_name}.zip" > "${release_name}.zip.sha256"
sha256sum --check "${release_name}.zip.sha256"
)
- name: Abgenommenes Paket als CI-Artefakt bereitstellen
uses: actions/upload-artifact@v4
with:
name: vendoo-1.43.0-updater-3.4-${{ github.event.pull_request.head.sha || github.sha }}
path: |
release-output/*.zip
release-output/*.zip.sha256
if-no-files-found: error
retention-days: 14
+27 -3
View File
@@ -3,19 +3,20 @@ name: Vendoo Release Manifest Sync
on:
push:
branches:
- release/1.43.0-production-update-v3
- 'release/*-production-update-v*'
workflow_dispatch:
permissions:
contents: write
actions: write
concurrency:
group: vendoo-release-manifest-sync-${{ github.ref }}
cancel-in-progress: true
cancel-in-progress: false
jobs:
sync:
name: Release-Manifest reproduzierbar synchronisieren
if: ${{ !contains(github.event.head_commit.message, '[manifest-sync]') }}
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
@@ -34,10 +35,14 @@ jobs:
run: node tools/generate-release-manifest.mjs .
- name: Geändertes Manifest committen
id: manifest
shell: bash
run: |
set -euo pipefail
if git diff --quiet -- release-manifest.json; then
echo "Release-Manifest ist bereits aktuell."
echo "changed=false" >> "$GITHUB_OUTPUT"
echo "head_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
exit 0
fi
git config user.name "vendoo-release-bot"
@@ -45,3 +50,22 @@ jobs:
git add release-manifest.json
git commit -m "chore: Release-Manifest synchronisieren [manifest-sync]"
git push origin HEAD:${GITHUB_REF_NAME}
echo "changed=true" >> "$GITHUB_OUTPUT"
echo "head_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Finalen Manifest-Head explizit validieren
if: ${{ steps.manifest.outputs.changed == 'true' }}
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
set -euo pipefail
final_head="${{ steps.manifest.outputs.head_sha }}"
remote_head="$(git ls-remote origin "refs/heads/${GITHUB_REF_NAME}" | awk '{print $1}')"
if [[ -z "$remote_head" || "$remote_head" != "$final_head" ]]; then
echo "Der gepushte Manifest-Head ist nicht stabil: lokal=$final_head remote=$remote_head" >&2
exit 1
fi
gh workflow run ci.yml --repo "$GITHUB_REPOSITORY" --ref "$GITHUB_REF_NAME"
gh workflow run sync-release-manifest.yml --repo "$GITHUB_REPOSITORY" --ref "$GITHUB_REF_NAME"
echo "Vendoo CI und Manifest-Sync wurden für finalen Head $final_head explizit registriert."
+1 -1
View File
@@ -7,7 +7,7 @@ if errorlevel 1 goto :path_error
set "VENDOO_PS=%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe"
if not exist "%VENDOO_PS%" goto :powershell_missing
echo Vendoo Production Updater 3.3 preflight...
echo Vendoo Production Updater 3.4 preflight...
"%VENDOO_PS%" -NoLogo -NoProfile -ExecutionPolicy Bypass -File ".\scripts\updater\Vendoo.Updater.Preflight.ps1"
set "VENDOO_RC=%ERRORLEVEL%"
if not "%VENDOO_RC%"=="0" goto :preflight_failed
@@ -0,0 +1,82 @@
# Abnahmebericht Vendoo Production Updater 3.4
**Status:** in technischer Abnahme
**Branch:** `feature/updater-3.4-lifecycle`
**Pull Request:** `#28`
**Zielversion:** Vendoo `1.43.0`
**Updater-Version:** `3.4.0`
## Verbindliche Ausgangslage
- `main` basiert auf dem gültigen Release aus PR #26.
- PR #27 ist geschlossen, nicht gemergt und kein zulässiger Quellstand.
- Der alte Worker 3.3 ist aus dem aktiven Release-Payload entfernt.
- Persistente Produktionsdaten unter `/app/data` sind nicht Bestandteil des Payloads.
- PR #28 bleibt bis zum vollständigen Linux-/Windows-Nachweis ein Draft.
## Ist-/Lückenanalyse Updater 3.3
| Bereich | Reale 3.3-Lücke | Umsetzung 3.4 | Nachweis |
|---|---|---|---|
| Check-Registrierung | `gh pr checks --watch` konnte bei noch nicht registrierten Checks sofort mit `no checks reported` abbrechen | eigener Check-Classifier mit 90 Sekunden Grace Period | Regressionstest und GitHub-CI ausstehend |
| Manifest-Sync | Head-SHA konnte sich nach Start der Checkphase ändern | aktuelle `headRefOid` wird in jeder Runde gelesen; Wechsel startet Grace Period und Auswertung neu | Regressionstest und Integrationstest ausstehend |
| Wiederholung nach Merge | 3.3 suchte nur offene PRs und konnte nach einem Merge erneut Sourcearbeit beginnen | gemergter passender PR führt direkt in Deployment/Verifikation | Regressionstest vorhanden, CI ausstehend |
| `main` bereits Zielversion | 3.3 erzeugte weiterhin Arbeitskopie und Releasebranch | Sourcephase wird vollständig übersprungen | Regressionstest vorhanden, CI ausstehend |
| Produktion bereits Zielversion | kein sofortiger erfolgreicher Zielzustand | sofortiger Erfolg ohne Branch, PR oder Deployment | Regressionstest vorhanden, reale Produktionsprobe ausstehend |
| veralteter Payload | Overlay plus Force-Push konnte neuere `main`-Änderungen zurücksetzen | Provenienz v2, Base-Ancestor-Prüfung und Pfadkonflikt-Guard vor dem Overlay | realer temporärer Git-Regressionstest vorhanden, CI ausstehend |
| Branch/PR-Idempotenz | Branch wurde stets neu erzeugt und unbedingt mit `--force` gepusht | deterministischer Branch, PR-Reconciliation und `--force-with-lease` | statischer Vertrag vorhanden, Windows-Lauf ausstehend |
| Check-Lifecycle | keine konkrete Workflow-Run-Auswertung am finalen Head | Rollup plus Actions-Runs nach `head_sha`, neuester Run entscheidet | Regressionstests vorhanden, GitHub-Lauf ausstehend |
| Merge-Sicherheit | kein atomarer Nachweis, dass der gemergte Head geprüft wurde | `--match-head-commit` mit unmittelbar zuvor bestätigtem finalen SHA | statischer Vertrag vorhanden, Integration ausstehend |
## Zustandsautomat
Der verbindliche Automat ist in `docs/PRODUCTION_UPDATER_3_4.md` dokumentiert. Die terminalen Zustände sind:
- `COMPLETED`: GitHub- und Produktionsziel nachgewiesen,
- `FAILED`: terminaler Pflichtcheck, Payloadkonflikt, Timeout oder technischer Fehler,
- `CANCELLED`: expliziter Benutzerabbruch mit erhaltenem Sitzungsstand.
Ein fehlender oder noch laufender Check ist kein terminaler Fehler.
## Regressionstest-Matrix
| Szenario | Erwartung | Automatisierter Test | Linux | Windows |
|---|---|---:|---:|---:|
| verspätet registrierte Checks | innerhalb Grace Period weiter warten | vorhanden | ausstehend | ausstehend |
| `queued` / `pending` / `in_progress` | nicht fehlschlagen | vorhanden | ausstehend | ausstehend |
| Manifest-Sync erzeugt neuen Head | alten Head verwerfen, neuen Head vollständig prüfen | Worker-/Lifecycle-Vertrag vorhanden | ausstehend | ausstehend |
| bereits gemergter PR | keinen zweiten PR erzeugen | vorhanden | ausstehend | ausstehend |
| bereits deployte Zielversion | sofort erfolgreich ohne Änderungen | vorhanden | ausstehend | ausstehend |
| doppelter Start | vorhandenen Zustand übernehmen | Entscheidungs- und Sessionvertrag vorhanden | ausstehend | ausstehend |
| parallele Workflow-Runs | zeitlich neuesten Run bewerten | vorhanden | ausstehend | ausstehend |
| veralteter Payload gegen neueren `main` | vor Overlay blockieren | realer temporärer Git-Test vorhanden | ausstehend | ausstehend |
| blockierter Rückschritt | Konfliktpfade ausgeben, keine Sourceänderung | vorhanden | ausstehend | ausstehend |
| älterer Erfolg, neuerer Fehler | neuer Fehler ist terminal | vorhanden | ausstehend | ausstehend |
| älterer Fehler, neuer Lauf | neuer Lauf bleibt pending | vorhanden | ausstehend | ausstehend |
| älterer Abbruch, neuer Erfolg | neuer Erfolg gilt | vorhanden | ausstehend | ausstehend |
## Releasevertrag
Vor einer Freigabe müssen nachweislich erfolgreich sein:
1. Syntax- und Sicherheitsgates unter Linux,
2. vollständiges `npm run verify:all` unter Windows,
3. PowerShell-Parserprüfung von UI, Host und Worker34,
4. Manifest-Reproduzierbarkeit,
5. Release-Staging mit `vendoo-release-package-v2`,
6. SHA-256-Prüfung aller Paketdateien,
7. Nachweis, dass der alte Worker 3.3 nicht ausgeliefert wird,
8. keine Änderung an Datenbank- oder `/app/data`-Inhalten,
9. Review des finalen PR-Head-SHA.
## Noch offene Abnahmepunkte
- GitHub-CI für den finalen PR-Head muss vollständig grün sein.
- Der Windows-Vertrag muss den echten PowerShell-Parser und alle bestehenden Tests erfolgreich durchlaufen.
- Das auslieferbare Paket darf erst danach erzeugt und als Workflow-Artefakt übernommen werden.
- Die öffentliche Produktionsantwort konnte in dieser Arbeitsumgebung noch nicht unabhängig verifiziert werden; sie ist daher kein bereits erbrachter Nachweis.
## Freigabeentscheidung
**Noch nicht freigegeben.**
Es existiert noch kein als produktionsreif deklarierter Download. Der Bericht wird erst nach finaler CI-, Paket- und Produktionsabnahme auf „bestanden“ gesetzt.
+189
View File
@@ -0,0 +1,189 @@
# Vendoo Production Updater 3.4
## Zweck
Updater 3.4 ersetzt den linearen, branchorientierten Ablauf von Updater 3.3 durch einen idempotenten Release-Lifecycle. Der Updater arbeitet auf einen nachprüfbaren Zielzustand hin und erzeugt keine weiteren Quelländerungen, sobald `main` oder die Produktion die Zielversion bereits erreicht haben.
Der Vertrag schützt insbesondere:
- den aktuellen Stand von `main` vor veralteten Payloads,
- den Pull Request vor Merge eines nicht mehr geprüften Head-SHA,
- vorhandene Releases vor doppelten Branches und Pull Requests,
- persistente Produktionsdaten unter `/app/data`,
- Datenbanken, Uploads, Benutzer, Artikel, Einstellungen, API-Schlüssel, FLUX-Modelle und Backups.
## Quellwahrheit
Der Updater wertet den Zustand in dieser Reihenfolge aus:
1. aktueller GitHub-Branch `main`,
2. aktueller Pull-Request-Head und konkrete GitHub-Workflow-Runs,
3. öffentliche Produktionsantwort `/readyz`,
4. lokaler Sitzungsstand nur als Wiederaufnahmekontext.
Ein lokaler Sitzungsstand darf niemals einen neueren GitHub- oder Produktionszustand überschreiben.
## Zustandsautomat
```text
PACKAGE_VALIDATED
-> TOOLS_READY
-> GITHUB_READY
-> TARGET_OBSERVED
TARGET_OBSERVED
-> COMPLETED
wenn main == target und production == target
-> DEPLOY_REQUIRED
wenn main == target und production != target
-> PR_RESUME
wenn ein passender offener PR existiert
-> DEPLOY_REQUIRED
wenn ein passender PR bereits gemergt ist
-> SOURCE_REQUIRED
nur wenn main noch nicht target ist und kein passender PR existiert
SOURCE_REQUIRED
-> PAYLOAD_GUARD
-> LOCAL_CONTRACT
-> TARGET_REOBSERVED
-> PR_RECONCILED
PR_RESUME | PR_RECONCILED
-> CHECKS_TRACKING
CHECKS_TRACKING
-> CHECKS_TRACKING
wenn sich der PR-Head-SHA ändert; Grace Period beginnt erneut
-> CHECKS_TRACKING
wenn Checks noch nicht registriert, queued, pending oder in_progress sind
-> MERGE_READY
wenn alle Pflichtchecks und Pflichtworkflows am stabilen finalen Head erfolgreich sind
-> FAILED
bei terminalem Pflichtfehler, Abbruch oder Gesamtzeitüberschreitung
MERGE_READY
-> MERGED
nur per --match-head-commit für den geprüften Head-SHA
-> DEPLOY_REQUIRED
-> PRODUCTION_VERIFIED
-> COMPLETED
```
## Idempotenzregeln
### Bereits aktuelle Produktion
Wenn `main` und `/readyz` bereits die Zielversion melden, endet der Updater sofort erfolgreich. Es werden weder Arbeitskopie noch Branch, Pull Request, Commit oder Deployment erzeugt.
### `main` bereits auf Zielversion
Wenn `main` bereits die Zielversion enthält, wird die Sourcephase vollständig übersprungen. Der Updater prüft nur noch, ob die Produktion nachziehen muss.
### Bereits gemergter Pull Request
Ein bereits gemergter passender Pull Request wird als abgeschlossene Sourcephase behandelt. Der Updater erzeugt keinen zweiten Pull Request und setzt beim Deployment beziehungsweise bei der Produktionsprüfung fort.
### Bereits offener Pull Request
Ein vorhandener offener Pull Request des deterministischen Releasebranches wird übernommen. Sein aktueller `headRefOid` ist die alleinige Prüfreferenz.
### Doppelter oder paralleler Start
Die Desktopoberfläche verhindert parallele lokale Prozesse per Mutex. Zusätzlich gleicht jeder Lauf GitHub und Produktion erneut ab. Ein konkurrierender Lauf, der zwischen lokaler Prüfung und Push einen Pull Request erzeugt oder `main` aktualisiert, wird bei der zweiten Zielbeobachtung erkannt und übernommen.
## Paket-Provenienz
Auslieferbare Pakete verwenden das Schema `vendoo-release-package-v2` und enthalten:
- `targetVersion`,
- `updaterVersion`,
- `sourceCommit`,
- `baseCommit`,
- `createdAt`,
- `payloadHash`,
- die vollständige SHA-256-Dateiliste,
- explizite Löschungen.
`payloadHash` wird aus den stabilen Manifestfeldern berechnet. Jede Änderung an Herkunft, Zieldateien oder Löschungen verändert die Paketidentität.
Vor dem Overlay wird geprüft:
1. `baseCommit` und aktueller `main` müssen lokal auflösbar sein.
2. `baseCommit` muss Vorfahr des aktuellen `main` sein.
3. Alle Pfade, die Payload und `main` seit `baseCommit` beide verändert haben, werden verglichen.
4. Weicht der gewünschte Payloadstand vom aktuellen `main` ab, wird der Lauf vor jeder Quelländerung blockiert.
5. Nicht kollidierende Fortschritte in `main` dürfen bestehen bleiben.
Damit kann ein altes Paket neuere Änderungen aus `main` weder still noch per Force-Push zurücksetzen.
## GitHub-Check-Lifecycle
Updater 3.4 verwendet nicht mehr `gh pr checks --watch` als Blackbox.
Für jeden aktuellen PR-Head werden ausgewertet:
- `statusCheckRollup` des Pull Requests,
- Workflow-Runs aus `actions/runs?head_sha=<finaler-head>&per_page=100`,
- die Pflichtchecks `Syntax, Sicherheit und Release-Gates`, `Windows vollständiger Releasevertrag` und `Updater-3.4-Paket nach Abnahme`,
- die Pflichtworkflows `Vendoo CI` und `Vendoo Release Manifest Sync`.
Regeln:
- `no checks reported` ist während der 90-sekündigen Registrierungs-Grace-Period kein Fehler.
- `queued`, `pending`, `in_progress`, `waiting` und `requested` sind laufende Zustände.
- Bei mehreren Runs desselben Workflows entscheidet der zeitlich neueste Run; ein alter Erfolg darf einen neueren Fehler nicht verdecken.
- Ändert der Manifest-Sync den Head-SHA, werden Grace Period und Checkauswertung für den neuen Head vollständig neu gestartet.
- Vor dem Merge wird der Head-SHA erneut gelesen.
- Der Merge verwendet `--match-head-commit`; ein weiterer Headwechsel verhindert den Merge atomar.
### Final-Head-Registrierung nach Manifest-Sync
Ein Push mit dem GitHub-Workflow-Token startet für den dadurch erzeugten Commit nicht automatisch einen weiteren Push-Workflow. Deshalb bestätigt der Manifest-Sync nach seinem Commit zuerst den tatsächlichen Remote-Head und dispatcht anschließend explizit:
1. `Vendoo CI` für den neuen Head,
2. `Vendoo Release Manifest Sync` für denselben Head.
Der zweite Manifest-Sync-Lauf findet ein unverändertes Manifest, erzeugt keinen weiteren Commit und dient als konkreter erfolgreicher Workflow-Nachweis am finalen SHA. Die Concurrency des Sync-Workflows darf den auslösenden Lauf nicht abbrechen.
### Paket erst nach Plattformabnahme
Das auslieferbare ZIP entsteht im Job `Updater-3.4-Paket nach Abnahme`. Dieser Job besitzt eine harte `needs`-Abhängigkeit auf Linux- und Windows-Abnahme. Er läuft daher erst, wenn beide Verträge erfolgreich abgeschlossen wurden. Danach werden Root-Manifest, Paketmanifest v2, Zielversion, Updater-Version und ZIP-SHA-256 erneut geprüft. Nur dieses Artefakt ist ein auslieferbarer Kandidat.
## Sitzungsvertrag
Sitzungen verwenden `vendoo-updater-session-v2`. Die Identität besteht aus:
- Repository,
- Zielversion,
- Payload-Hash,
- deterministischem Releasebranch.
Gespeichert werden unter anderem Pull-Request-Nummer und URL, finaler geprüfter Head-SHA, Merge-Commit, Produktionsversion, Releasezustand und Abschlusszeitpunkt. Der Sitzungsstand dient ausschließlich der Wiederaufnahme; jeder Lauf validiert die externen Zustände erneut.
## Branch- und Push-Sicherheit
- Releasebranch: `release/<zielversion>-production-update-v4`
- kein unbedingtes `git push --force`
- vorhandene Branches werden nur mit `--force-with-lease=<erwarteter-remote-sha>` aktualisiert
- Merge ausschließlich für den nachgewiesenen finalen Head-SHA
- kein Quell-PR, wenn `main` bereits auf Zielversion ist
## Produktionsvertrag
Das Deployment verändert keine persistenten Nutzdaten. `/app/data` bleibt außerhalb des Release-Payloads. Der Updater akzeptiert Erfolg erst, wenn `/readyz` sowohl `ok: true` als auch exakt die Zielversion meldet.
Die Option `-SkipProductionDeploy` ist ausschließlich für Source-/CI-Abnahmen vorgesehen. Sie darf nicht als Nachweis eines vollständigen Produktionsupdates ausgegeben werden.
## Fehlerdiagnose
Bei Fehler oder Timeout werden protokolliert:
- aktuelle Phase,
- aktueller PR und Head-SHA,
- Pflichtcheck- und Workflow-Matrix,
- Prozessausgabe und Exit-Code,
- Payload-Konfliktpfade,
- konkrete Wiederanlaufempfehlung.
Ein erneuter Start beginnt nicht blind von vorn, sondern beobachtet Zielzustand, Pull Requests, Merge und Produktion erneut und setzt beim tatsächlich offenen Zustand fort.
+754 -604
View File
File diff suppressed because it is too large Load Diff
+13 -10
View File
@@ -22,7 +22,7 @@
"verify:git-staging": "node tools/verify-git-staging.mjs",
"generate:tokens": "node tools/generate-design-tokens.mjs",
"verify:architecture": "node tools/verify-platform-architecture-1.41.2.mjs",
"verify:all": "npm run verify:git && npm run verify:git-regression && npm run verify:git-staging && npm run verify:release-manifest && npm run verify:ignore-boundaries && npm run check && npm run verify:powershell-static && npm run verify:source-boundaries && npm run verify:esm-exports && npm run verify:flux-loop && npm run verify:capabilities && npm run verify:coolify && npm run verify:initial-modules && npm run verify:architecture && npm run verify:themes && npm run verify:security && npm run verify:identity && npm run verify:invitations && npm run verify:sqlite-cleanup && npm run verify:catalog && npm run verify:modules && npm run verify:production-operations && npm run verify:one-click-update && npm run verify:module-navigation && npm run verify:updater-ui && npm run verify:updater-hotfix && npm run verify:deployment && npm run verify:config && npm run verify:installer-shell && npm run verify:db",
"verify:all": "npm run verify:git && npm run verify:git-regression && npm run verify:git-staging && npm run verify:release-manifest && npm run verify:ignore-boundaries && npm run check && npm run verify:powershell-static && npm run verify:source-boundaries && npm run verify:esm-exports && npm run verify:flux-loop && npm run verify:capabilities && npm run verify:coolify && npm run verify:initial-modules && npm run verify:architecture && npm run verify:themes && npm run verify:security && npm run verify:identity && npm run verify:invitations && npm run verify:sqlite-cleanup && npm run verify:catalog && npm run verify:modules && npm run verify:production-operations && npm run verify:updater-start-chain && npm run verify:updater-ci-lifecycle && npm run verify:one-click-update && npm run verify:module-navigation && npm run verify:updater-ui && npm run verify:updater-hotfix && npm run verify:deployment && npm run verify:config && npm run verify:installer-shell && npm run verify:db",
"verify:themes": "node tools/verify-theme-manager-1.41.2.mjs",
"verify:security": "node tools/verify-security-foundation-1.41.2.mjs",
"verify:identity": "node tools/verify-identity-settings-1.41.2.mjs && node --no-warnings tools/verify-identity-db-1.40.0.mjs",
@@ -37,24 +37,27 @@
"verify:ignore-boundaries": "node tools/verify-git-ignore-boundaries-1.41.2.mjs",
"verify:production-operations": "node tools/verify-production-operations-1.42.0.mjs && node --no-warnings tools/verify-production-operations-db-1.42.0.mjs",
"verify:invitations": "node tools/verify-controlled-invitations-1.42.0.mjs",
"verify:one-click-update": "node tools/verify-production-updater-3.3.mjs",
"verify:one-click-update": "node tools/verify-production-updater-3.4.mjs",
"verify:module-navigation": "node tools/verify-module-navigation-generator-1.43.0.mjs",
"verify:updater-ui": "node tools/verify-production-updater-3.3.mjs",
"verify:updater-hotfix": "node tools/verify-production-updater-3.3.mjs",
"verify:updater-ui": "node tools/verify-production-updater-3.4.mjs",
"verify:updater-hotfix": "node tools/verify-production-updater-3.4.mjs",
"verify:updater-lifecycle": "node tools/verify-production-updater-3.4.mjs",
"verify:updater-start-chain": "node tools/verify-updater-start-chain-3.4.mjs",
"verify:updater-ci-lifecycle": "node tools/verify-updater-ci-lifecycle-3.4.mjs",
"verify:powershell-static": "node tools/verify-powershell-static.mjs",
"verify:sqlite-cleanup": "node tools/verify-sqlite-cleanup-contract-1.43.0.mjs && node tools/verify-windows-sqlite-cleanup-regression-1.43.0.mjs",
"verify:release-manifest": "node tools/verify-release-manifest-staging-3.0.mjs"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.39.0",
"@anthropic-ai/sdk": "^0.111.0",
"better-sqlite3": "^12.11.1",
"cookie-parser": "^1.4.7",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"helmet": "^8.2.0",
"dotenv": "^17.4.2",
"express": "^5.2.1",
"helmet": "^8.3.0",
"multer": "^2.2.0",
"nodemailer": "^9.0.3",
"openai": "^4.77.0",
"sharp": "^0.33.5"
"openai": "^6.46.0",
"sharp": "^0.35.3"
}
}
+74 -30
View File
@@ -1,7 +1,7 @@
{
"schema": "vendoo-release-manifest-v1",
"version": "1.43.0",
"updaterVersion": "3.3.0",
"updaterVersion": "3.4.0",
"mode": "overlay-with-explicit-removals",
"requiredFiles": [
"package.json",
@@ -15,10 +15,14 @@
"scripts/updater/Vendoo.Updater.Preflight.ps1",
"scripts/updater/Vendoo.Updater.UI.ps1",
"scripts/updater/Vendoo.Updater.Host.ps1",
"scripts/updater/Vendoo.Updater.Worker.ps1",
"tools/apply-release-manifest.mjs"
"scripts/updater/Vendoo.Updater.Worker34.ps1",
"tools/apply-release-manifest.mjs",
"tools/updater-lifecycle-3.4.mjs",
"tools/check-release-payload-3.4.mjs"
],
"removedFiles": [
"scripts/updater/Vendoo.Updater.Worker.ps1"
],
"removedFiles": [],
"files": [
{
"path": ".dockerignore",
@@ -57,8 +61,8 @@
},
{
"path": ".github/workflows/ci.yml",
"sha256": "384990816ba3295cc6458d7d808422cc25cfc2e9cf9598b9052f7aadc7f70b6b",
"size": 4233
"sha256": "c0e80dbafb1d1491b851eb002dc323e52ea89e7e0456dde034f3303a8f4751c2",
"size": 8173
},
{
"path": ".github/workflows/release.yml",
@@ -67,8 +71,8 @@
},
{
"path": ".github/workflows/sync-release-manifest.yml",
"sha256": "f25e9fe77a8d3c419b37b22f226f9d2e2ed69820894288be5b4bb09ac10bab1a",
"size": 1342
"sha256": "9067f929284d7a524013e41658a91d6da6993a931efce84530ecc05363fb0ca2",
"size": 2495
},
{
"path": ".github/workflows/vendoo-production-deploy.yml",
@@ -690,6 +694,11 @@
"sha256": "581e0d192e08a00d8f19b325320fecc3634c487fd3a75135852546fd6fc2cccc",
"size": 584
},
{
"path": "docs/ABNAHMEBERICHT_PRODUCTION_UPDATER_3_4.md",
"sha256": "dc6ae417a4ec45ac4d65ac4f9d5f1eae45d1af2e4988fd124547113b265ff493",
"size": 5724
},
{
"path": "docs/architecture/CATALOG_MEDIA_MODULES_1_39_0.md",
"sha256": "b39d2d892068e1313fc00c9345d54240b6d50caf8a940261a088e67d8778ac63",
@@ -900,6 +909,11 @@
"sha256": "9c8957ea1593b55e9fe6786fa87fbb156874c7be3e3a54e0987bd94d18fb6bf0",
"size": 970
},
{
"path": "docs/PRODUCTION_UPDATER_3_4.md",
"sha256": "e6264636042b53ac1728614d0341f14c26264f3b73f52ffcba3fbd4f6f8fe179",
"size": 8326
},
{
"path": "docs/PUBLISH_CENTER_2026.md",
"sha256": "4f74fef6877126d2579356b9731955e29f05ea8b55b59abb91569e47730a1c3f",
@@ -2027,8 +2041,8 @@
},
{
"path": "package.json",
"sha256": "cbc637abe9c70f94b51daeeca2eba33be31a55327ee1b5da9e5cf12552aed666",
"size": 4434
"sha256": "cd21451f91312c33bc9bd5b808103c3428854587cc8e9b5ba3b2908085043344",
"size": 4759
},
{
"path": "platforms/ebay-de.mjs",
@@ -2407,23 +2421,23 @@
},
{
"path": "scripts/updater/Vendoo.Updater.Host.ps1",
"sha256": "01f330e1bcd05ddf48685b9382057be7aa8e9329538d3c6e5044bb17129c53c4",
"size": 5807
"sha256": "f02c56bd0dc13eca7830f8cbd7de94a6cba8331504176694878834f43e6b3495",
"size": 5828
},
{
"path": "scripts/updater/Vendoo.Updater.Preflight.ps1",
"sha256": "180adf209ea2c4f7d7a64594e231ecd5508dd44993cce15c0d871aa0a1b6e83a",
"size": 3605
"sha256": "ef91ccf925e7a8a1c8f27ddf2d1d7b997249798a63334f8fe1b5d27626eb525e",
"size": 3607
},
{
"path": "scripts/updater/Vendoo.Updater.UI.ps1",
"sha256": "ddf685413aa052bbc7b150bd396ff796ba62b30d9db30fa25f2c47fa50c85ab1",
"size": 28122
"sha256": "b499570054f409310cbb8c4f89af7ce7e26b7f226b630a1f644aadbd2cee0fe4",
"size": 28166
},
{
"path": "scripts/updater/Vendoo.Updater.Worker.ps1",
"sha256": "5b7ea5829d0f7499e418d546d0a05ef63557dd5a9a2b4ba60ac9f0e2c9617a51",
"size": 30578
"path": "scripts/updater/Vendoo.Updater.Worker34.ps1",
"sha256": "0cae461ee0c1499314a9ad1eab2650f9c0a2d3e22d03b712d5a6164e93acc03c",
"size": 35852
},
{
"path": "SECURITY.md",
@@ -2465,10 +2479,20 @@
"sha256": "3ddece908ffdfb15c94852c0fcc554ce7de39725650b4d139d0afc2f59297a55",
"size": 889
},
{
"path": "tools/build-deterministic-zip.py",
"sha256": "844ceacf2d3d90c5d948233b2ba1a51ae9f913c1b13a07eacc77888ebc2b7877",
"size": 2101
},
{
"path": "tools/build-release.mjs",
"sha256": "fd35ee48f4c0ae1d14f7ffe9e9b27a5802305b6dfa7726f3df79e03a0e57335a",
"size": 2368
"sha256": "d00a8998e093d24ea3ff1cea88d9d00ed14a17d61ceb80517dc9b849b10a1190",
"size": 3585
},
{
"path": "tools/check-release-payload-3.4.mjs",
"sha256": "f4fad67726a0926867652c5e5e0071d9191c51a4555c178ccec1a817b7edcd26",
"size": 4038
},
{
"path": "tools/check-syntax.mjs",
@@ -2492,14 +2516,19 @@
},
{
"path": "tools/release-manifest-lib.mjs",
"sha256": "45dc394a9ef255c3acde855213e23ed727c280c23c1bdf41156048268f603368",
"size": 10320
"sha256": "9310d33f35823692e307dcfdc87f43218fd9b3c3a1c696d9839935d1641acc57",
"size": 11400
},
{
"path": "tools/test-runtime-cleanup.mjs",
"sha256": "5bb59f8283122f6db66e94883f04246ceb108399f97f6e961831bfe5b6b10025",
"size": 1002
},
{
"path": "tools/updater-lifecycle-3.4.mjs",
"sha256": "d5320924be6e18dfd656fdba717789e15e6aedbd8c5afbeaffc21e141272edae",
"size": 9982
},
{
"path": "tools/verify-capability-graph-1.40.2.mjs",
"sha256": "e3d8c6e8dfd566acbe037ad744edbf60816152d4994698c2a9c46d2b890e246a",
@@ -2917,8 +2946,8 @@
},
{
"path": "tools/verify-powershell-parser.ps1",
"sha256": "140cb2adc774612c43c8e8962b7ecdbe70ca0e7def42ac0dd71b86695697e570",
"size": 1295
"sha256": "205b0327719ffc39a97092d5e08336262d257a8273389458fde68343bd22b6b7",
"size": 1748
},
{
"path": "tools/verify-powershell-static.mjs",
@@ -2990,6 +3019,11 @@
"sha256": "2c276876f6fde314a9a7cbc7f93d14cc6a378dce4eebaec7dc1af5b0e395a779",
"size": 8753
},
{
"path": "tools/verify-production-updater-3.4.mjs",
"sha256": "46f1b5a658f6b406f427686ca0961ee18bb18b18d0ca3e219bfbedcc9b1522f0",
"size": 11063
},
{
"path": "tools/verify-release-1.24.0.mjs",
"sha256": "551ec082e47dbb54ff8c942985face7dead33b684c9e50218ba31fe20ef7d8d4",
@@ -3047,8 +3081,8 @@
},
{
"path": "tools/verify-release-manifest-staging-3.0.mjs",
"sha256": "36111e35cb4b4a75376a028b2407678bbce0c40585c606965f1680d4aa46afca",
"size": 5356
"sha256": "f5fa6b4332cde9e461f3e986a61dab80dfb1aafeb76599fa1f10add93ff5331d",
"size": 6550
},
{
"path": "tools/verify-security-1.31.0.py",
@@ -3170,11 +3204,21 @@
"sha256": "0f3f684019763a14f5fbaec42e42c680cf748d930d46cd607fc61eb413ca4553",
"size": 2360
},
{
"path": "tools/verify-updater-ci-lifecycle-3.4.mjs",
"sha256": "02a0e0d28bfcc3424fe6929ebbe79b0e8000e35d8d4d60aa8b83622baa6800d1",
"size": 3709
},
{
"path": "tools/verify-updater-hang-hotfix-1.43.0.mjs",
"sha256": "ae015616e289a4bfb371a2823c35e8e5a013cbdefd4005c47ac972c2e5df7516",
"size": 46
},
{
"path": "tools/verify-updater-start-chain-3.4.mjs",
"sha256": "de7d34580349ef64b8236590d491dfcfab9eebb3c934821867a52312f923f662",
"size": 3186
},
{
"path": "tools/verify-updater-status-ui-1.43.0.mjs",
"sha256": "ae015616e289a4bfb371a2823c35e8e5a013cbdefd4005c47ac972c2e5df7516",
@@ -3187,13 +3231,13 @@
},
{
"path": "UPDATE_VENDOO.bat",
"sha256": "aab80ea7612891502fe3e6e275c864fd3c0239b9ca91738252363706a5ee4a8a",
"sha256": "d028f3c20dc26cdabd0db1cfcff5e0ecccd1fd457894ae1c41b7d896acf8f5f4",
"size": 1294
},
{
"path": "update-manifest.json",
"sha256": "a080d7733b65715573b9936502ce5228a8bfccd59a1766115be8cda908fa3285",
"size": 446
"sha256": "eb5aaabc083a6c4f837dd396712c1a6708fbb25833d9cd79543d29152a481939",
"size": 531
}
]
}
+5 -5
View File
@@ -15,18 +15,18 @@ param(
Set-StrictMode -Version 2.0
$ErrorActionPreference = 'Stop'
$TargetVersion = '1.43.0'
$UpdaterVersion = '3.3.0'
$UpdaterVersion = '3.4.0'
function Get-DefaultSteps {
return @(
[ordered]@{id='package';title='Paket prüfen';state='pending'},
[ordered]@{id='tools';title='System prüfen';state='pending'},
[ordered]@{id='github';title='GitHub verbinden';state='pending'},
[ordered]@{id='production';title='Produktion vorbereiten';state='pending'},
[ordered]@{id='workspace';title='Arbeitskopie erstellen';state='pending'},
[ordered]@{id='production';title='Zielzustand erkennen';state='pending'},
[ordered]@{id='workspace';title='Payload sicher vorbereiten';state='pending'},
[ordered]@{id='tests';title='Vollständig testen';state='pending'},
[ordered]@{id='pullrequest';title='Pull Request erstellen';state='pending'},
[ordered]@{id='merge';title='Sicher mergen';state='pending'},
[ordered]@{id='pullrequest';title='Pull Request abgleichen';state='pending'},
[ordered]@{id='merge';title='Finalen Head prüfen und mergen';state='pending'},
[ordered]@{id='deploy';title='Coolify deployen';state='pending'},
[ordered]@{id='verify';title='Produktion bestätigen';state='pending'}
)
+2 -2
View File
@@ -3,7 +3,7 @@ param()
Set-StrictMode -Version 2.0
$ErrorActionPreference = 'Stop'
$UpdaterVersion = '3.3.0'
$UpdaterVersion = '3.4.0'
function Get-CanonicalPackageRoot {
$candidate = Join-Path -Path $PSScriptRoot -ChildPath '..\..'
@@ -51,7 +51,7 @@ try {
(Join-Path $PackageRoot 'scripts\update-vendoo-gui.ps1'),
(Join-Path $PackageRoot 'scripts\updater\Vendoo.Updater.UI.ps1'),
(Join-Path $PackageRoot 'scripts\updater\Vendoo.Updater.Host.ps1'),
(Join-Path $PackageRoot 'scripts\updater\Vendoo.Updater.Worker.ps1')
(Join-Path $PackageRoot 'scripts\updater\Vendoo.Updater.Worker34.ps1')
)
Write-PreflightLog 'INFO' 'Echte Windows-PowerShell-Parserprüfung gestartet.'
+6 -6
View File
@@ -10,8 +10,8 @@ Add-Type -AssemblyName System.Xaml
$PackageRoot = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..\..') -ErrorAction Stop).ProviderPath.TrimEnd([System.IO.Path]::DirectorySeparatorChar,[System.IO.Path]::AltDirectorySeparatorChar)
$TargetVersion = '1.43.0'
$UpdaterVersion = '3.3.0'
$WorkerScript = Join-Path $PSScriptRoot 'Vendoo.Updater.Worker.ps1'
$UpdaterVersion = '3.4.0'
$WorkerScript = Join-Path $PSScriptRoot 'Vendoo.Updater.Worker34.ps1'
$WorkerHostScript = Join-Path $PSScriptRoot 'Vendoo.Updater.Host.ps1'
$LocalRoot = Join-Path $env:LOCALAPPDATA 'Vendoo\Updater'
$RuntimeRoot = Join-Path $LocalRoot 'runtime'
@@ -158,8 +158,8 @@ foreach ($name in $names) { Set-Variable -Name $name -Value $window.FindName($na
$steps = @(
@{id='package'; title='Paket prüfen'}, @{id='tools'; title='System prüfen'}, @{id='github'; title='GitHub verbinden'},
@{id='production'; title='Produktion vorbereiten'}, @{id='workspace'; title='Arbeitskopie erstellen'}, @{id='tests'; title='Vollständig testen'},
@{id='pullrequest'; title='Pull Request erstellen'}, @{id='merge'; title='Sicher mergen'}, @{id='deploy'; title='Coolify deployen'}, @{id='verify'; title='Produktion bestätigen'}
@{id='production'; title='Zielzustand erkennen'}, @{id='workspace'; title='Payload sicher vorbereiten'}, @{id='tests'; title='Vollständig testen'},
@{id='pullrequest'; title='Pull Request abgleichen'}, @{id='merge'; title='Finalen Head prüfen und mergen'}, @{id='deploy'; title='Coolify deployen'}, @{id='verify'; title='Produktion bestätigen'}
)
$script:StepControls = @{}
foreach ($step in $steps) {
@@ -206,8 +206,8 @@ function Update-Steps($status) {
function Write-InitialStatus {
$steps=@(
@{id='package';title='Paket prüfen';state='pending'},@{id='tools';title='System prüfen';state='pending'},@{id='github';title='GitHub verbinden';state='pending'},
@{id='production';title='Produktion vorbereiten';state='pending'},@{id='workspace';title='Arbeitskopie erstellen';state='pending'},@{id='tests';title='Vollständig testen';state='pending'},
@{id='pullrequest';title='Pull Request erstellen';state='pending'},@{id='merge';title='Sicher mergen';state='pending'},@{id='deploy';title='Coolify deployen';state='pending'},@{id='verify';title='Produktion bestätigen';state='pending'}
@{id='production';title='Zielzustand erkennen';state='pending'},@{id='workspace';title='Payload sicher vorbereiten';state='pending'},@{id='tests';title='Vollständig testen';state='pending'},
@{id='pullrequest';title='Pull Request abgleichen';state='pending'},@{id='merge';title='Finalen Head prüfen und mergen';state='pending'},@{id='deploy';title='Coolify deployen';state='pending'},@{id='verify';title='Produktion bestätigen';state='pending'}
)
$payload=[ordered]@{version=$TargetVersion;updaterVersion=$UpdaterVersion;percent=0;phase='Updater wird vorbereitet';detail='Die überwachte Update-Umgebung wird initialisiert.';state='running';error='';remediation='';updatedAt=(Get-Date).ToString('o');logFile=$script:LogFile;sessionFile=$script:SessionFile;steps=$steps;request=$null}|ConvertTo-Json -Depth 8
[IO.File]::WriteAllText($script:StatusFile,$payload,(New-Object Text.UTF8Encoding($false)))
-334
View File
@@ -1,334 +0,0 @@
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)][string]$StatusFile,
[Parameter(Mandatory=$true)][string]$EventFile,
[Parameter(Mandatory=$true)][string]$InputFile,
[Parameter(Mandatory=$true)][string]$CancelFile,
[Parameter(Mandatory=$true)][string]$SessionFile,
[Parameter(Mandatory=$true)][string]$LogFile,
[switch]$SkipProductionDeploy,
[switch]$SelfTest,
[string]$SelfTestFailAt = ''
)
Set-StrictMode -Version 2.0
$ErrorActionPreference='Stop';$ProgressPreference='SilentlyContinue'
Add-Type -AssemblyName System.Net.Http
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$PackageRoot=(Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..\..') -ErrorAction Stop).ProviderPath.TrimEnd([System.IO.Path]::DirectorySeparatorChar,[System.IO.Path]::AltDirectorySeparatorChar)
$Repository='Masterluke77/vendoo';$BaseBranch='main';$TargetVersion='1.43.0';$UpdaterVersion='3.3.0';$PublicBaseUrl='https://vendoo.flatlined.de'
$LocalRoot=Join-Path $env:LOCALAPPDATA 'Vendoo\Updater';$ToolsRoot=Join-Path $LocalRoot 'tools';$LogsRoot=Join-Path $LocalRoot 'logs';$WorkRoot=Join-Path $LocalRoot "work\$TargetVersion";New-Item -ItemType Directory -Path $ToolsRoot,$LogsRoot,(Split-Path -Parent $SessionFile) -Force|Out-Null
$Stamp=Get-Date -Format 'yyyyMMdd-HHmmss';$Branch="release/$TargetVersion-production-update-v3";$CloneDir=Join-Path $WorkRoot 'repository'
$script:Tools=@{};$script:CurrentStep='package';$script:LastHeartbeat=Get-Date
$script:Steps=@(
[ordered]@{id='package';title='Paket prüfen';state='pending'},[ordered]@{id='tools';title='System prüfen';state='pending'},[ordered]@{id='github';title='GitHub verbinden';state='pending'},
[ordered]@{id='production';title='Produktion vorbereiten';state='pending'},[ordered]@{id='workspace';title='Arbeitskopie erstellen';state='pending'},[ordered]@{id='tests';title='Vollständig testen';state='pending'},
[ordered]@{id='pullrequest';title='Pull Request erstellen';state='pending'},[ordered]@{id='merge';title='Sicher mergen';state='pending'},[ordered]@{id='deploy';title='Coolify deployen';state='pending'},[ordered]@{id='verify';title='Produktion bestätigen';state='pending'}
)
function Write-Event([string]$Level,[string]$Message){
$safe=$Message -replace '(?i)(https://[^\s]*(?:token|webhook)[^\s]*)','[VERDECKT]'
$entry=[ordered]@{at=(Get-Date).ToString('o');level=$Level.ToUpperInvariant();message=$safe}|ConvertTo-Json -Compress
[IO.File]::AppendAllText($EventFile,$entry+[Environment]::NewLine,(New-Object Text.UTF8Encoding($false)))
[IO.File]::AppendAllText($LogFile,"$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [$($Level.ToUpperInvariant())] $safe$([Environment]::NewLine)",(New-Object Text.UTF8Encoding($false)))
}
function Write-State([int]$Percent,[string]$Phase,[string]$Detail,[string]$State='running',[string]$Error='',[string]$Remediation='', $Request=$null){
$payload=[ordered]@{version=$TargetVersion;updaterVersion=$UpdaterVersion;percent=[Math]::Max(0,[Math]::Min(100,$Percent));phase=$Phase;detail=$Detail;state=$State;error=$Error;remediation=$Remediation;updatedAt=(Get-Date).ToString('o');logFile=$LogFile;sessionFile=$SessionFile;steps=$script:Steps;request=$Request}|ConvertTo-Json -Depth 8
$tmp="$StatusFile.$PID.tmp";[IO.File]::WriteAllText($tmp,$payload,(New-Object Text.UTF8Encoding($false)));Move-Item -LiteralPath $tmp -Destination $StatusFile -Force
}
function Set-Step([string]$Id,[string]$State){foreach($s in $script:Steps){if($s.id -eq $Id){$s.state=$State}elseif($State -eq 'active' -and $s.state -eq 'active'){$s.state='pending'}};$script:CurrentStep=$Id}
function Complete-Step([string]$Id){foreach($s in $script:Steps){if($s.id -eq $Id){$s.state='done'}}}
function Fail-Step{foreach($s in $script:Steps){if($s.id -eq $script:CurrentStep){$s.state='failed'}}}
function Assert-NotCancelled{if(Test-Path -LiteralPath $CancelFile){throw [System.OperationCanceledException]::new('Das Update wurde durch den Benutzer abgebrochen.')}}
function Save-Session($Data){if(-not($Data.PSObject.Properties.Name -contains 'updaterVersion')){$Data|Add-Member -NotePropertyName updaterVersion -NotePropertyValue $UpdaterVersion};$Data.updaterVersion=$UpdaterVersion;$Data.updatedAt=(Get-Date).ToString('o');$tmp="$SessionFile.tmp";$Data|ConvertTo-Json -Depth 8|Set-Content -LiteralPath $tmp -Encoding UTF8;Move-Item $tmp $SessionFile -Force}
function Load-Session{if(Test-Path -LiteralPath $SessionFile){try{$saved=Get-Content $SessionFile -Raw -Encoding UTF8|ConvertFrom-Json;if([string]$saved.version-eq$TargetVersion -and [string]$saved.updaterVersion-eq$UpdaterVersion){return $saved};Write-Event 'INFO' 'Veralteter oder anderer Updater-Sitzungsstand wird nicht übernommen.'}catch{Write-Event 'WARN' 'Gespeicherter Sitzungsstand ist ungültig und wird verworfen.'}};return [pscustomobject]@{version=$TargetVersion;updaterVersion=$UpdaterVersion;branch=$Branch;prUrl='';merged=$false;runId='';stage='new';updatedAt=''}}
function Quote-Arg([string]$Value){if($null -eq $Value){return '""'};if($Value -notmatch '[\s"]'){return $Value};return '"'+($Value -replace '(\\*)"','$1$1\"' -replace '(\\+)$','$1$1')+'"'}
function Stop-Tree([int]$ProcessId){
try { & taskkill.exe /PID $ProcessId /T /F 2>$null | Out-Null }
catch { try { Stop-Process -Id $ProcessId -Force -ErrorAction SilentlyContinue } catch {} }
}
function Get-TailText([string]$Text,[int]$MaxLength){
if([string]::IsNullOrWhiteSpace($Text)){return ''}
$value=$Text.Trim()
if($value.Length -gt $MaxLength){return $value.Substring($value.Length-$MaxLength)}
return $value
}
function Get-ProcessFailureSummary([string]$StdErr,[string]$StdOut){
$parts=New-Object Collections.Generic.List[string]
$errorTail=Get-TailText $StdErr 1800
$outputTail=Get-TailText $StdOut 900
if($errorTail){$parts.Add("Fehlerausgabe:`n$errorTail")}
if($outputTail){$parts.Add("Letzte Standardausgabe:`n$outputTail")}
if($parts.Count -eq 0){return 'Der Prozess lieferte keine Diagnoseausgabe.'}
return ($parts -join "`n`n")
}
function Invoke-External{
param([string]$File,[string[]]$Arguments,[string]$WorkingDirectory=$PackageRoot,[int]$TimeoutSeconds=120,[switch]$AllowFailure,[string]$InputText='',[string]$Label='Prozess',[switch]$Quiet)
Assert-NotCancelled
$psi=New-Object Diagnostics.ProcessStartInfo;$psi.FileName=$File;$psi.Arguments=(@($Arguments|ForEach-Object{Quote-Arg ([string]$_)}) -join ' ');$psi.WorkingDirectory=$WorkingDirectory;$psi.UseShellExecute=$false;$psi.CreateNoWindow=$true;$psi.RedirectStandardOutput=$true;$psi.RedirectStandardError=$true;$psi.RedirectStandardInput=$true
# Node.js, npm and Git emit UTF-8 when redirected. Windows PowerShell otherwise decodes
# redirected streams with the active OEM code page, which corrupts German umlauts.
$utf8NoBom=New-Object Text.UTF8Encoding($false)
if($psi.PSObject.Properties.Name -contains 'StandardOutputEncoding'){$psi.StandardOutputEncoding=$utf8NoBom}
if($psi.PSObject.Properties.Name -contains 'StandardErrorEncoding'){$psi.StandardErrorEncoding=$utf8NoBom}
$psi.EnvironmentVariables['NO_COLOR']='1'
$psi.EnvironmentVariables['FORCE_COLOR']='0'
$psi.EnvironmentVariables['NPM_CONFIG_COLOR']='false'
$psi.EnvironmentVariables['NPM_CONFIG_UNICODE']='true'
$p=New-Object Diagnostics.Process;$p.StartInfo=$psi
if(-not $Quiet){Write-Event 'INFO' "$Label gestartet."}
if(-not $p.Start()){throw "$Label konnte nicht gestartet werden."}
$outTask = $p.StandardOutput.ReadToEndAsync()
$errTask = $p.StandardError.ReadToEndAsync()
if ($InputText) { $p.StandardInput.Write($InputText) }
$p.StandardInput.Close()
$sw=[Diagnostics.Stopwatch]::StartNew()
while(-not $p.WaitForExit(500)){
if(Test-Path -LiteralPath $CancelFile){Stop-Tree $p.Id;throw [System.OperationCanceledException]::new('Das Update wurde durch den Benutzer abgebrochen.')}
if($sw.Elapsed.TotalSeconds -gt $TimeoutSeconds){Stop-Tree $p.Id;throw "$Label hat das Zeitlimit von $TimeoutSeconds Sekunden überschritten."}
if(-not $Quiet -and ((Get-Date)-$script:LastHeartbeat).TotalSeconds -ge 8){Write-Event 'INFO' ("$Label läuft seit {0:mm\:ss}." -f $sw.Elapsed);$script:LastHeartbeat=Get-Date}
}
$p.WaitForExit();$stdout=[string]$outTask.Result;$stderr=[string]$errTask.Result;$code=$p.ExitCode;$p.Dispose()
if(-not $Quiet){
foreach($line in @($stdout -split "`r?`n")){if($line.Trim()){Write-Event 'OUTPUT' $line.Trim()}}
foreach($line in @($stderr -split "`r?`n")){
$clean=$line.Trim()
if(-not $clean){continue}
if($code -ne 0){Write-Event 'ERROR' $clean}
elseif($clean -match '(?i)\b(warn|warning|deprecated)\b'){Write-Event 'WARN' $clean}
else{Write-Event 'OUTPUT' $clean}
}
}
if(-not $AllowFailure -and $code -ne 0){$summary=Get-ProcessFailureSummary $stderr $stdout;throw "$Label ist mit Exit-Code $code fehlgeschlagen.`n$summary"}
return [pscustomobject]@{Code=$code;Output=$stdout;Error=$stderr}
}
function Resolve-Tool([string]$Name,[string[]]$Candidates=@()){
if($script:Tools.ContainsKey($Name) -and (Test-Path -LiteralPath $script:Tools[$Name])){return $script:Tools[$Name]}
foreach($c in @($Candidates)){if($c -and (Test-Path -LiteralPath $c -PathType Leaf)){$script:Tools[$Name]=(Resolve-Path $c).Path;return $script:Tools[$Name]}}
$cmd=Get-Command $Name -CommandType Application -ErrorAction SilentlyContinue|Select-Object -First 1;if($cmd -and $cmd.Source -and (Test-Path $cmd.Source)){$script:Tools[$Name]=$cmd.Source;return $cmd.Source};return $null
}
function Get-GitPath{return Resolve-Tool 'git.exe' @("$env:ProgramFiles\Git\cmd\git.exe","$env:LOCALAPPDATA\Programs\Git\cmd\git.exe")}
function Get-NodePath{return Resolve-Tool 'node.exe' @("$env:ProgramFiles\nodejs\node.exe","$env:LOCALAPPDATA\Programs\nodejs\node.exe")}
function Get-NpmPath{return Resolve-Tool 'npm.cmd' @("$env:ProgramFiles\nodejs\npm.cmd","$env:LOCALAPPDATA\Programs\nodejs\npm.cmd")}
function Invoke-WebJson([string]$Uri){for($i=1;$i-le3;$i++){try{return Invoke-RestMethod -Uri $Uri -Headers @{'User-Agent'="Vendoo-Updater/$UpdaterVersion";'Accept'='application/vnd.github+json'} -TimeoutSec 30}catch{if($i-eq3){throw};Start-Sleep -Seconds (2*$i)}}}
function Download-File([string]$Uri,[string]$Destination,[string]$Label){
for($attempt=1;$attempt-le3;$attempt++){try{
$handler = New-Object Net.Http.HttpClientHandler
$client = [Net.Http.HttpClient]::new($handler)
$client.Timeout=[TimeSpan]::FromMinutes(5);$client.DefaultRequestHeaders.UserAgent.ParseAdd("Vendoo-Updater/$UpdaterVersion")
$resp = $client.GetAsync($Uri,[Net.Http.HttpCompletionOption]::ResponseHeadersRead).Result
$null = $resp.EnsureSuccessStatusCode()
$total=$resp.Content.Headers.ContentLength;$input=$resp.Content.ReadAsStreamAsync().Result;$output=[IO.File]::Open($Destination,[IO.FileMode]::Create,[IO.FileAccess]::Write,[IO.FileShare]::None)
try{
$buf=New-Object byte[] 65536;$readTotal=0L;$lastReported=-10
while(($read=$input.Read($buf,0,$buf.Length))-gt0){
Assert-NotCancelled;$output.Write($buf,0,$read);$readTotal+=$read
if($total -and $total -gt 0){
$percent=[Math]::Min(100,[int][Math]::Floor(($readTotal*100.0)/$total))
if($percent -ge ($lastReported+10) -or $readTotal -ge $total){
$bucket=[Math]::Min(100,[int]([Math]::Floor($percent/10)*10))
if($bucket -gt $lastReported){
$lastReported=$bucket
Write-Event 'INFO' ("${Label}: {0}% ({1:N1} von {2:N1} MB)" -f $bucket,($readTotal/1048576),($total/1048576))
Write-State 18 'GitHub CLI' ("${Label} wird geladen: {0}%" -f $bucket)
}
}
}
}
}finally{$output.Dispose();$input.Dispose();$resp.Dispose();$client.Dispose();$handler.Dispose()}
if(-not $total){Write-Event 'INFO' "${Label}: Download abgeschlossen."}
return
}catch{if($attempt-eq3){throw "$Label konnte nach drei Versuchen nicht geladen werden: $($_.Exception.Message)"};Write-Event 'WARN' "${Label}: Downloadversuch $attempt fehlgeschlagen; erneuter Versuch.";Start-Sleep -Seconds (3*$attempt)}}
}
function Ensure-GitHubCli{
$managed=Join-Path $ToolsRoot 'github-cli\current\bin\gh.exe';if(Test-Path $managed){$test=Invoke-External $managed @('--version') -TimeoutSeconds 20 -AllowFailure -Label 'GitHub CLI prüfen';if($test.Code-eq0){$script:Tools['gh.exe']=$managed;return $managed}}
Write-State 18 'GitHub CLI' 'Die offizielle portable GitHub CLI wird sicher geladen und per SHA-256 geprüft.'
$release=Invoke-WebJson 'https://api.github.com/repos/cli/cli/releases/latest';$arch=if([Environment]::Is64BitOperatingSystem){'amd64'}else{'386'};$asset=@($release.assets|Where-Object{$_.name -like "gh_*_windows_$arch.zip"}|Select-Object -First 1);$checks=@($release.assets|Where-Object{$_.name -like '*_checksums.txt'}|Select-Object -First 1)
if(-not $asset -or -not $checks){throw 'Die offiziellen GitHub-CLI-Release-Dateien konnten nicht ermittelt werden.'}
$tmp=Join-Path $env:TEMP "vendoo-gh-$PID";New-Item -ItemType Directory -Path $tmp -Force|Out-Null;$zip=Join-Path $tmp $asset.name;$checkFile=Join-Path $tmp $checks.name;$extract=Join-Path $tmp 'extract'
try{Download-File $asset.browser_download_url $zip 'GitHub CLI';Download-File $checks.browser_download_url $checkFile 'Prüfsummen';$line=Get-Content $checkFile|Where-Object{$_ -match [regex]::Escape($asset.name)}|Select-Object -First 1;if(-not $line){throw 'Für das GitHub-CLI-Archiv wurde keine offizielle Prüfsumme gefunden.'};$expected=($line -split '\s+')[0].ToLowerInvariant();$actual=(Get-FileHash $zip -Algorithm SHA256).Hash.ToLowerInvariant();if($actual-ne$expected){throw 'SHA-256-Prüfung der GitHub CLI fehlgeschlagen.'};New-Item -ItemType Directory -Path $extract -Force|Out-Null;Expand-Archive $zip $extract -Force;$source=Get-ChildItem $extract -Filter gh.exe -File -Recurse|Select-Object -First 1;if(-not $source){throw 'Das geprüfte GitHub-CLI-Archiv enthält keine gh.exe.'};$targetRoot=Split-Path -Parent (Split-Path -Parent $managed);if(Test-Path $targetRoot){Remove-Item $targetRoot -Recurse -Force};New-Item -ItemType Directory -Path (Split-Path -Parent $managed) -Force|Out-Null;Copy-Item $source.FullName $managed -Force;$test=Invoke-External $managed @('--version') -TimeoutSeconds 20 -Label 'GitHub CLI validieren';$script:Tools['gh.exe']=$managed;Write-Event 'OK' "GitHub CLI $($release.tag_name) wurde geprüft und lokal bereitgestellt.";return $managed}finally{Remove-Item $tmp -Recurse -Force -ErrorAction SilentlyContinue}
}
function Test-GitHubAuthentication([string]$GhPath){
$status=Invoke-External $GhPath @('auth','status','--hostname','github.com') -TimeoutSeconds 20 -AllowFailure -Label 'GitHub-Anmeldung prüfen' -Quiet
return ($status.Code -eq 0)
}
function Invoke-VisibleGitHubLogin([string]$GhPath){
$runtimeDirectory=Split-Path -Parent $StatusFile
$loginScript=Join-Path $runtimeDirectory 'github-login.cmd'
$escapedGh=$GhPath.Replace('"','""')
$lines=@(
'@echo off',
'chcp 65001 >nul',
'title Vendoo - GitHub Anmeldung',
'echo ============================================================',
'echo Vendoo - Sichere GitHub Anmeldung',
'echo ============================================================',
'echo.',
'echo Der einmalige GitHub-Code wird angezeigt und in die',
'echo Zwischenablage kopiert. Folge danach dem Browserdialog.',
'echo Dieses Fenster schliesst sich nach erfolgreicher Anmeldung.',
'echo.',
('"{0}" auth login --hostname github.com --git-protocol https --web --clipboard' -f $escapedGh),
'set "VENDOO_GH_LOGIN_EXIT=%ERRORLEVEL%"',
'echo.',
'if "%VENDOO_GH_LOGIN_EXIT%"=="0" (',
' echo GitHub-Anmeldung erfolgreich. Vendoo faehrt automatisch fort.',
') else (',
' echo GitHub-Anmeldung wurde nicht abgeschlossen. Fehlercode: %VENDOO_GH_LOGIN_EXIT%',
' echo Das Fenster kann geschlossen werden; der Vendoo Updater bleibt geoeffnet.',
' pause',
')',
'exit /b %VENDOO_GH_LOGIN_EXIT%'
)
[IO.File]::WriteAllLines($loginScript,$lines,(New-Object Text.UTF8Encoding($false)))
Write-State 26 'GitHub-Anmeldung' 'Im geöffneten Anmeldefenster den Gerätecode bestätigen und den Browserdialog abschließen. Danach geht es automatisch weiter.' 'waiting'
Write-Event 'WAIT' 'Sichtbares GitHub-Anmeldefenster wird geöffnet.'
try{
$loginProcess=Start-Process -FilePath 'cmd.exe' -ArgumentList @('/d','/c',('"{0}"' -f $loginScript)) -WorkingDirectory $runtimeDirectory -WindowStyle Normal -PassThru
}catch{
throw "Das sichtbare GitHub-Anmeldefenster konnte nicht gestartet werden: $($_.Exception.Message)"
}
$deadline=(Get-Date).AddMinutes(10);$lastStatus=Get-Date
while((Get-Date)-lt$deadline){
Assert-NotCancelled
if(Test-GitHubAuthentication $GhPath){
Write-Event 'OK' 'GitHub-Anmeldung wurde bestätigt.'
try{if(-not $loginProcess.HasExited){$loginProcess.CloseMainWindow()|Out-Null}}catch{}
return
}
if($loginProcess.HasExited){
Start-Sleep -Seconds 1
if(Test-GitHubAuthentication $GhPath){Write-Event 'OK' 'GitHub-Anmeldung wurde bestätigt.';return}
throw "Die GitHub-Anmeldung wurde nicht abgeschlossen. Das Anmeldefenster endete mit Exit-Code $($loginProcess.ExitCode)."
}
if(((Get-Date)-$lastStatus).TotalSeconds -ge 15){
Write-Event 'INFO' 'GitHub-Anmeldung wartet auf Abschluss im Browser.'
$lastStatus=Get-Date
}
Start-Sleep -Seconds 2
}
try{if(-not $loginProcess.HasExited){Stop-Tree $loginProcess.Id}}catch{}
throw 'Die GitHub-Anmeldung wurde nicht innerhalb von zehn Minuten abgeschlossen.'
}
function Check-ProductionVersion{try{$r=Invoke-RestMethod -Uri "$PublicBaseUrl/readyz?updater=$Stamp" -TimeoutSec 20 -Headers @{'Cache-Control'='no-cache';'User-Agent'="Vendoo-Updater/$UpdaterVersion"};return [pscustomobject]@{Ready=[bool]$r.ok;Version=[string]$r.version}}catch{return [pscustomobject]@{Ready=$false;Version='unbekannt'}}}
function Assert-Package{foreach($f in @('package.json','package-lock.json','update-manifest.json','release-manifest.json','db\schema.sql','tools\apply-release-manifest.mjs')){if(-not(Test-Path(Join-Path $PackageRoot $f))){throw "Paketdatei fehlt: $f"}};$p=Get-Content (Join-Path $PackageRoot 'package.json') -Raw -Encoding UTF8|ConvertFrom-Json;$m=Get-Content (Join-Path $PackageRoot 'update-manifest.json') -Raw -Encoding UTF8|ConvertFrom-Json;$r=Get-Content (Join-Path $PackageRoot 'release-manifest.json') -Raw -Encoding UTF8|ConvertFrom-Json;if([string]$p.version-ne$TargetVersion -or [string]$m.version-ne$TargetVersion -or [string]$r.version-ne$TargetVersion){throw "Paket-, Update- und Release-Manifest-Version müssen exakt $TargetVersion sein."};if([string]$r.updaterVersion-ne$UpdaterVersion){throw "Release-Manifest gehört zu Updater $($r.updaterVersion), erwartet wird $UpdaterVersion."};if([string]$r.mode-ne'overlay-with-explicit-removals'){throw "Unzulässiger Release-Modus: $($r.mode)"}}
try{
Write-Event 'INFO' "Vendoo Production Updater $UpdaterVersion gestartet. Zielversion $TargetVersion."
$session=Load-Session
Set-Step 'package' 'active';Write-State 3 'Paketprüfung' 'Version, Manifest und sicherheitsrelevante Paketdateien werden geprüft.';Assert-Package;if($SelfTestFailAt-eq'package'){throw 'Simulierter Paketfehler.'};Complete-Step 'package';Write-Event 'OK' 'Paketprüfung erfolgreich.'
Set-Step 'tools' 'active';Write-State 9 'Systemprüfung' 'Git, Node.js und npm werden mit festen Zeitlimits geprüft.'
$git=Get-GitPath;if(-not$git){throw 'Git für Windows wurde nicht gefunden. Bitte Git für Windows installieren; danach kann derselbe Updater erneut gestartet werden.'};$node=Get-NodePath;$npm=Get-NpmPath;if(-not$node -or -not$npm){throw 'Node.js 22 LTS oder npm wurde nicht gefunden.'};$gv=Invoke-External $git @('--version') -TimeoutSeconds 20 -Label 'Git prüfen';$nv=Invoke-External $node @('--version') -TimeoutSeconds 20 -Label 'Node.js prüfen';if($nv.Output -notmatch 'v?(\d+)\.' -or [int]$Matches[1]-lt22){throw "Node.js 22 oder neuer ist erforderlich. Gefunden: $($nv.Output.Trim())"};$gh=Ensure-GitHubCli;Complete-Step 'tools';Write-Event 'OK' 'Systemprüfung erfolgreich.'
Set-Step 'github' 'active';Write-State 24 'GitHub-Verbindung' 'Anmeldung und Repository-Berechtigungen werden geprüft.'
if(-not (Test-GitHubAuthentication $gh)){Invoke-VisibleGitHubLogin $gh}
Invoke-External $gh @('auth','setup-git','--hostname','github.com') -TimeoutSeconds 60 -Label 'Git-Anmeldung konfigurieren'|Out-Null
$repo=Invoke-External $gh @('repo','view',$Repository,'--json','viewerPermission,defaultBranchRef,isPrivate') -TimeoutSeconds 60 -Label 'Repository-Zugriff prüfen';$repoObj=$repo.Output|ConvertFrom-Json;if([string]$repoObj.viewerPermission -notin @('ADMIN','MAINTAIN','WRITE')){throw "Für $Repository fehlt Schreibzugriff. GitHub meldet $($repoObj.viewerPermission)."};Complete-Step 'github';Write-Event 'OK' "GitHub-Zugriff bestätigt: $($repoObj.viewerPermission)."
Set-Step 'production' 'active';Write-State 32 'Produktionsvorbereitung' 'Aktueller Produktionsstand und Coolify Auto Deploy werden geprüft.'
$prod=Check-ProductionVersion;Write-Event 'INFO' "Produktion meldet ready=$($prod.Ready), version=$($prod.Version)."
if(-not$SkipProductionDeploy){Write-Event 'OK' 'Kein Coolify-Webhook erforderlich: Nach dem Merge wird das bestehende Coolify Auto Deploy der GitHub-Verknüpfung verwendet.'}
Complete-Step 'production'
if($SelfTest){foreach($id in @('workspace','tests','pullrequest','merge','deploy','verify')){Set-Step $id 'active';Write-State 50 "Selbsttest: $id" 'Darstellung, Statuswechsel und Protokoll werden simuliert.';Start-Sleep -Milliseconds 350;Complete-Step $id};Write-State 100 'Selbsttest erfolgreich' 'Die Updater-Oberfläche und der Worker-Vertrag funktionieren.' 'success';return}
if([bool]$session.merged){Write-Event 'INFO' 'Gespeicherter Wiederaufnahmepunkt: main wurde bereits aktualisiert.';foreach($id in @('workspace','tests','pullrequest','merge')){Complete-Step $id}}
else {
Set-Step 'workspace' 'active'
Write-State 40 'Arbeitskopie' 'main wird isoliert geklont; das vollständige Release-Manifest wird ohne Leeren der Baseline angewendet.'
if (Test-Path -LiteralPath $WorkRoot) { Remove-Item -LiteralPath $WorkRoot -Recurse -Force }
New-Item -ItemType Directory -Path $WorkRoot -Force | Out-Null
Invoke-External $gh @('repo','clone',$Repository,$CloneDir,'--','--branch',$BaseBranch,'--single-branch') -WorkingDirectory $WorkRoot -TimeoutSeconds 300 -Label 'Repository klonen' | Out-Null
Invoke-External $git @('switch','-c',$Branch) -WorkingDirectory $CloneDir -TimeoutSeconds 60 -Label 'Release-Branch erstellen' | Out-Null
Invoke-External $node @((Join-Path $PackageRoot 'tools\apply-release-manifest.mjs'),$PackageRoot,$CloneDir) -TimeoutSeconds 300 -Label 'SHA-256-geprüftes Release-Manifest anwenden' | Out-Null
Complete-Step 'workspace'
Set-Step 'tests' 'active'
Write-State 52 'Abnahmetests' 'Abhängigkeiten werden reproduzierbar installiert; danach läuft der vollständige verify:all-Vertrag.'
Invoke-External $npm @('ci','--no-audit','--no-fund') -WorkingDirectory $CloneDir -TimeoutSeconds 1200 -Label 'npm ci' | Out-Null
Write-State 61 'Abnahmetests' 'Alle Vendoo-Sicherheits-, Datenbank-, Modul- und Release-Gates laufen.'
Invoke-External $npm @('run','verify:all') -WorkingDirectory $CloneDir -TimeoutSeconds 3600 -Label 'npm run verify:all' | Out-Null
Complete-Step 'tests'
Write-Event 'OK' 'Alle lokalen Abnahmetests sind grün.'
Set-Step 'pullrequest' 'active'
Write-State 70 'Pull Request' 'Der geprüfte Stand wird committed und in einen kontrollierten Pull Request übertragen.'
Invoke-External $git @('config','user.name','Vendoo Production Updater') -WorkingDirectory $CloneDir -Label 'Git-Name setzen' | Out-Null
$uid = (Invoke-External $gh @('api','user','--jq','.id') -Label 'GitHub-Benutzer-ID').Output.Trim()
$ulogin = (Invoke-External $gh @('api','user','--jq','.login') -Label 'GitHub-Benutzername').Output.Trim()
Invoke-External $git @('config','user.email',"$uid+$ulogin@users.noreply.github.com") -WorkingDirectory $CloneDir -Label 'Git-E-Mail setzen' | Out-Null
Invoke-External $git @('add','--all') -WorkingDirectory $CloneDir -Label 'Änderungen vormerken' | Out-Null
$diff = Invoke-External $git @('diff','--cached','--quiet') -WorkingDirectory $CloneDir -AllowFailure -Label 'Änderungen prüfen'
if ($diff.Code -eq 0) {
Write-Event 'INFO' 'main enthält bereits denselben Source-Stand.'
$session.merged = $true
$session.stage = 'merged'
Save-Session $session
Complete-Step 'pullrequest'
Complete-Step 'merge'
}
elseif ($diff.Code -eq 1) {
Invoke-External $git @('commit','-m',"release: Vendoo $TargetVersion production update") -WorkingDirectory $CloneDir -TimeoutSeconds 120 -Label 'Release committen' | Out-Null
Invoke-External $git @('push','--force','--set-upstream','origin',$Branch) -WorkingDirectory $CloneDir -TimeoutSeconds 300 -Label 'Release-Branch pushen' | Out-Null
$existing = Invoke-External $gh @('pr','list','--repo',$Repository,'--head',$Branch,'--state','open','--json','url','--jq','.[0].url') -AllowFailure -Label 'Vorhandenen Pull Request suchen'
$prUrl = $existing.Output.Trim()
if (-not $prUrl) {
$body = Join-Path $WorkRoot 'pr-body.md'
$bodyText = @"
## Vendoo $TargetVersion Production Update
Erstellt durch Vendoo Production Updater $UpdaterVersion.
- vollständiges `npm run verify:all` erfolgreich
- keine Datenbanken, Uploads, Secrets oder `/app/data`-Inhalte
- Coolify Auto Deploy mit exakter Versionsprüfung
"@
$bodyText | Set-Content -LiteralPath $body -Encoding UTF8
$prUrl = (Invoke-External $gh @('pr','create','--repo',$Repository,'--base',$BaseBranch,'--head',$Branch,'--title',"Vendoo $TargetVersion Production Update",'--body-file',$body) -WorkingDirectory $CloneDir -TimeoutSeconds 180 -Label 'Pull Request erstellen').Output.Trim()
}
$session.prUrl = $prUrl
$session.stage = 'pr'
Save-Session $session
Complete-Step 'pullrequest'
Write-Event 'OK' "Pull Request: $prUrl"
Set-Step 'merge' 'active'
Write-State 78 'GitHub-Prüfungen' 'Alle gemeldeten GitHub-Checks werden abgewartet.'
$rollup = Invoke-External $gh @('pr','view',$prUrl,'--repo',$Repository,'--json','statusCheckRollup') -Label 'GitHub-Checks ermitteln'
$rollupObj = $rollup.Output | ConvertFrom-Json
if (@($rollupObj.statusCheckRollup).Count -gt 0) {
Invoke-External $gh @('pr','checks',$prUrl,'--repo',$Repository,'--watch','--fail-fast') -WorkingDirectory $CloneDir -TimeoutSeconds 2700 -Label 'GitHub-Checks abwarten' | Out-Null
}
Write-State 82 'Merge' 'Der vollständig geprüfte Pull Request wird per Squash nach main übernommen.'
Invoke-External $gh @('pr','merge',$prUrl,'--repo',$Repository,'--squash','--delete-branch','--subject',"Vendoo $TargetVersion Production Update") -WorkingDirectory $CloneDir -TimeoutSeconds 300 -Label 'Pull Request mergen' | Out-Null
$session.merged = $true
$session.stage = 'merged'
Save-Session $session
Complete-Step 'merge'
Write-Event 'OK' 'main wurde kontrolliert aktualisiert.'
}
else {
throw "Git-Diff-Prüfung lieferte unerwarteten Exit-Code $($diff.Code)."
}
}
if($SkipProductionDeploy){foreach($id in @('deploy','verify')){foreach($s in $script:Steps){if($s.id-eq$id){$s.state='skipped'}}};Write-State 100 'Source aktualisiert' 'GitHub wurde aktualisiert; das Produktionsdeployment wurde auf Wunsch übersprungen.' 'success';return}
Set-Step 'deploy' 'active';Write-State 87 'Coolify Auto Deploy' 'main wurde aktualisiert. Coolify übernimmt das Deployment über die bestehende GitHub-Verknüpfung.'
$session.stage='deploy';Save-Session $session;Write-Event 'INFO' 'Warte auf Coolify Auto Deploy. Es wird kein Webhook und kein GitHub Secret benötigt.'
$deadline=(Get-Date).AddMinutes(15);$last='';$nextLog=Get-Date
do{
Assert-NotCancelled
$prod=Check-ProductionVersion
$last="ready=$($prod.Ready), version=$($prod.Version)"
if((Get-Date)-ge$nextLog){Write-Event 'INFO' "Coolify Auto Deploy: $last";$nextLog=(Get-Date).AddSeconds(30)}
if($prod.Ready -and $prod.Version-eq$TargetVersion){break}
Start-Sleep -Seconds 10
}while((Get-Date)-lt$deadline)
if(-not($prod.Ready -and $prod.Version-eq$TargetVersion)){throw "Coolify hat die Zielversion innerhalb von 15 Minuten nicht bereitgestellt. Letzter Stand: $last. In Coolify bei der Vendoo-Anwendung unter Advanced prüfen, ob Auto Deploy aktiviert ist."}
Complete-Step 'deploy'
Set-Step 'verify' 'active';Write-State 97 'Produktionsprüfung' 'Die öffentlich erreichbare Readiness und die exakte Zielversion werden abschließend bestätigt.'
$prod=Check-ProductionVersion
if(-not($prod.Ready -and $prod.Version-eq$TargetVersion)){throw "Die abschließende Produktionsprüfung ist fehlgeschlagen. Stand: ready=$($prod.Ready), version=$($prod.Version)"}
Complete-Step 'verify';$session.stage='complete';Save-Session $session;Write-State 100 'Update erfolgreich' "Vendoo $TargetVersion läuft geprüft in Produktion." 'success';Write-Event 'OK' "Vendoo $TargetVersion wurde vollständig aktualisiert.";return
}catch [System.OperationCanceledException]{Fail-Step;Write-State 100 'Update abgebrochen' $_.Exception.Message 'cancelled' $_.Exception.Message 'Der gespeicherte Wiederaufnahmepunkt bleibt erhalten. Mit „Erneut versuchen“ kann der sichere Ablauf fortgesetzt werden.';Write-Event 'WARN' $_.Exception.Message;return
}catch{Fail-Step;$msg=$_.Exception.Message;$remediation=if ($script:CurrentStep -eq 'tools'){'Systemvoraussetzung korrigieren und „Erneut versuchen“ wählen. Es wurden noch keine GitHub- oder Produktionsänderungen vorgenommen.'}elseif ($script:CurrentStep -in @('deploy','verify')){'main kann bereits aktualisiert sein. „Erneut versuchen“ setzt beim Deployment beziehungsweise der Produktionsprüfung fort.'}else{'„Erneut versuchen“ verwendet den gespeicherten Stand. Vor einem erfolgreichen Merge bleibt main unverändert.'};Write-State 100 'Update sicher gestoppt' $msg 'failed' $msg $remediation;Write-Event 'ERROR' $msg;return
}finally{if(Test-Path $InputFile){Remove-Item $InputFile -Force -ErrorAction SilentlyContinue}}
+155
View File
@@ -0,0 +1,155 @@
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)][string]$StatusFile,
[Parameter(Mandatory=$true)][string]$EventFile,
[Parameter(Mandatory=$true)][string]$InputFile,
[Parameter(Mandatory=$true)][string]$CancelFile,
[Parameter(Mandatory=$true)][string]$SessionFile,
[Parameter(Mandatory=$true)][string]$LogFile,
[switch]$SkipProductionDeploy,
[switch]$SelfTest,
[string]$SelfTestFailAt = ''
)
Set-StrictMode -Version 2.0
$ErrorActionPreference='Stop';$ProgressPreference='SilentlyContinue'
Add-Type -AssemblyName System.Net.Http
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$PackageRoot=(Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..\..') -ErrorAction Stop).ProviderPath.TrimEnd([System.IO.Path]::DirectorySeparatorChar,[System.IO.Path]::AltDirectorySeparatorChar)
$Repository='Masterluke77/vendoo';$BaseBranch='main';$TargetVersion='1.43.0';$UpdaterVersion='3.4.0';$PublicBaseUrl='https://vendoo.flatlined.de'
$Branch="release/$TargetVersion-production-update-v4"
$LocalRoot=Join-Path $env:LOCALAPPDATA 'Vendoo\Updater';$ToolsRoot=Join-Path $LocalRoot 'tools';$LogsRoot=Join-Path $LocalRoot 'logs';$WorkRoot=Join-Path $LocalRoot "work\$TargetVersion-v4";$CloneDir=Join-Path $WorkRoot 'repository'
New-Item -ItemType Directory -Path $ToolsRoot,$LogsRoot,(Split-Path -Parent $SessionFile) -Force|Out-Null
$Stamp=Get-Date -Format 'yyyyMMdd-HHmmss';$script:Tools=@{};$script:CurrentStep='package';$script:LastHeartbeat=Get-Date;$script:LastCheckLog=[DateTime]::MinValue
$LifecycleScript=Join-Path $PackageRoot 'tools\updater-lifecycle-3.4.mjs'
$PayloadGuardScript=Join-Path $PackageRoot 'tools\check-release-payload-3.4.mjs'
$script:Steps=@(
[ordered]@{id='package';title='Paket prüfen';state='pending'},[ordered]@{id='tools';title='System prüfen';state='pending'},[ordered]@{id='github';title='GitHub verbinden';state='pending'},
[ordered]@{id='production';title='Zielzustand erkennen';state='pending'},[ordered]@{id='workspace';title='Payload sicher vorbereiten';state='pending'},[ordered]@{id='tests';title='Vollständig testen';state='pending'},
[ordered]@{id='pullrequest';title='Pull Request abgleichen';state='pending'},[ordered]@{id='merge';title='Finalen Head prüfen und mergen';state='pending'},[ordered]@{id='deploy';title='Coolify deployen';state='pending'},[ordered]@{id='verify';title='Produktion bestätigen';state='pending'}
)
function Write-Event([string]$Level,[string]$Message){
$safe=$Message -replace '(?i)(https://[^\s]*(?:token|webhook)[^\s]*)','[VERDECKT]'
$entry=[ordered]@{at=(Get-Date).ToString('o');level=$Level.ToUpperInvariant();message=$safe}|ConvertTo-Json -Compress
[IO.File]::AppendAllText($EventFile,$entry+[Environment]::NewLine,(New-Object Text.UTF8Encoding($false)))
[IO.File]::AppendAllText($LogFile,"$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [$($Level.ToUpperInvariant())] $safe$([Environment]::NewLine)",(New-Object Text.UTF8Encoding($false)))
}
function Write-State([int]$Percent,[string]$Phase,[string]$Detail,[string]$State='running',[string]$Error='',[string]$Remediation='', $Request=$null){
$payload=[ordered]@{version=$TargetVersion;updaterVersion=$UpdaterVersion;percent=[Math]::Max(0,[Math]::Min(100,$Percent));phase=$Phase;detail=$Detail;state=$State;error=$Error;remediation=$Remediation;updatedAt=(Get-Date).ToString('o');logFile=$LogFile;sessionFile=$SessionFile;steps=$script:Steps;request=$Request}|ConvertTo-Json -Depth 10
$tmp="$StatusFile.$PID.tmp";[IO.File]::WriteAllText($tmp,$payload,(New-Object Text.UTF8Encoding($false)));Move-Item -LiteralPath $tmp -Destination $StatusFile -Force
}
function Set-Step([string]$Id,[string]$State){foreach($s in $script:Steps){if($s.id -eq $Id){$s.state=$State}elseif($State -eq 'active' -and $s.state -eq 'active'){$s.state='pending'}};$script:CurrentStep=$Id}
function Complete-Step([string]$Id){foreach($s in $script:Steps){if($s.id -eq $Id){$s.state='done'}}}
function Skip-Step([string]$Id){foreach($s in $script:Steps){if($s.id -eq $Id){$s.state='skipped'}}}
function Fail-Step{foreach($s in $script:Steps){if($s.id -eq $script:CurrentStep){$s.state='failed'}}}
function Assert-NotCancelled{if(Test-Path -LiteralPath $CancelFile){throw [System.OperationCanceledException]::new('Das Update wurde durch den Benutzer abgebrochen.')}}
function Add-OrSetProperty($Object,[string]$Name,$Value){if($Object.PSObject.Properties.Name -contains $Name){$Object.$Name=$Value}else{$Object|Add-Member -NotePropertyName $Name -NotePropertyValue $Value}}
function Save-Session($Data){
Add-OrSetProperty $Data 'schema' 'vendoo-updater-session-v2';Add-OrSetProperty $Data 'repository' $Repository;Add-OrSetProperty $Data 'targetVersion' $TargetVersion;Add-OrSetProperty $Data 'updaterVersion' $UpdaterVersion;Add-OrSetProperty $Data 'payloadHash' $script:Manifest.payloadHash;Add-OrSetProperty $Data 'branch' $Branch;Add-OrSetProperty $Data 'updatedAt' (Get-Date).ToString('o')
$tmp="$SessionFile.tmp";$json=$Data|ConvertTo-Json -Depth 10;[IO.File]::WriteAllText($tmp,$json,(New-Object Text.UTF8Encoding($false)));Move-Item $tmp $SessionFile -Force
}
function New-Session{return [pscustomobject]@{schema='vendoo-updater-session-v2';repository=$Repository;targetVersion=$TargetVersion;updaterVersion=$UpdaterVersion;payloadHash=[string]$script:Manifest.payloadHash;branch=$Branch;releaseState='new';prNumber=$null;prUrl='';finalPrHeadSha='';mergeCommit='';productionVersion='';completedAt='';updatedAt=''}}
function Load-Session{
if(Test-Path -LiteralPath $SessionFile){try{$saved=Get-Content $SessionFile -Raw -Encoding UTF8|ConvertFrom-Json;if([string]$saved.schema-eq'vendoo-updater-session-v2' -and [string]$saved.repository-eq$Repository -and [string]$saved.targetVersion-eq$TargetVersion -and [string]$saved.payloadHash-eq[string]$script:Manifest.payloadHash){return $saved};Write-Event 'INFO' 'Veralteter oder anderer Updater-Sitzungsstand wird nicht übernommen.'}catch{Write-Event 'WARN' 'Gespeicherter Sitzungsstand ist ungültig und wird verworfen.'}}
return New-Session
}
function Quote-Arg([string]$Value){if($null -eq $Value){return '""'};if($Value -notmatch '[\s"]'){return $Value};return '"'+($Value -replace '(\\*)"','$1$1\"' -replace '(\\+)$','$1$1')+'"'}
function Stop-Tree([int]$ProcessId){try{& taskkill.exe /PID $ProcessId /T /F 2>$null|Out-Null}catch{try{Stop-Process -Id $ProcessId -Force -ErrorAction SilentlyContinue}catch{}}}
function Get-TailText([string]$Text,[int]$MaxLength){if([string]::IsNullOrWhiteSpace($Text)){return ''};$value=$Text.Trim();if($value.Length -gt $MaxLength){return $value.Substring($value.Length-$MaxLength)};return $value}
function Get-ProcessFailureSummary([string]$StdErr,[string]$StdOut){$parts=New-Object Collections.Generic.List[string];$e=Get-TailText $StdErr 2200;$o=Get-TailText $StdOut 1200;if($e){$parts.Add("Fehlerausgabe:`n$e")};if($o){$parts.Add("Letzte Standardausgabe:`n$o")};if($parts.Count-eq0){return 'Der Prozess lieferte keine Diagnoseausgabe.'};return($parts-join"`n`n")}
function Invoke-External{
param([string]$File,[string[]]$Arguments,[string]$WorkingDirectory=$PackageRoot,[int]$TimeoutSeconds=120,[switch]$AllowFailure,[string]$InputText='',[string]$Label='Prozess',[switch]$Quiet)
Assert-NotCancelled
$psi=New-Object Diagnostics.ProcessStartInfo;$psi.FileName=$File;$psi.Arguments=(@($Arguments|ForEach-Object{Quote-Arg([string]$_)})-join' ');$psi.WorkingDirectory=$WorkingDirectory;$psi.UseShellExecute=$false;$psi.CreateNoWindow=$true;$psi.RedirectStandardOutput=$true;$psi.RedirectStandardError=$true;$psi.RedirectStandardInput=$true
$utf8=New-Object Text.UTF8Encoding($false);if($psi.PSObject.Properties.Name-contains'StandardOutputEncoding'){$psi.StandardOutputEncoding=$utf8};if($psi.PSObject.Properties.Name-contains'StandardErrorEncoding'){$psi.StandardErrorEncoding=$utf8}
$psi.EnvironmentVariables['NO_COLOR']='1';$psi.EnvironmentVariables['FORCE_COLOR']='0';$psi.EnvironmentVariables['NPM_CONFIG_COLOR']='false';$psi.EnvironmentVariables['NPM_CONFIG_UNICODE']='true'
$p=New-Object Diagnostics.Process;$p.StartInfo=$psi;if(-not$Quiet){Write-Event 'INFO' "$Label gestartet."};if(-not$p.Start()){throw "$Label konnte nicht gestartet werden."};$outTask=$p.StandardOutput.ReadToEndAsync();$errTask=$p.StandardError.ReadToEndAsync();if($InputText){$p.StandardInput.Write($InputText)};$p.StandardInput.Close();$sw=[Diagnostics.Stopwatch]::StartNew()
while(-not$p.WaitForExit(500)){if(Test-Path -LiteralPath $CancelFile){Stop-Tree $p.Id;throw [System.OperationCanceledException]::new('Das Update wurde durch den Benutzer abgebrochen.')};if($sw.Elapsed.TotalSeconds-gt$TimeoutSeconds){Stop-Tree $p.Id;throw "$Label hat das Zeitlimit von $TimeoutSeconds Sekunden überschritten."};if(-not$Quiet-and((Get-Date)-$script:LastHeartbeat).TotalSeconds-ge8){Write-Event 'INFO' ("$Label läuft seit {0:mm\:ss}."-f$sw.Elapsed);$script:LastHeartbeat=Get-Date}}
$p.WaitForExit();$stdout=[string]$outTask.Result;$stderr=[string]$errTask.Result;$code=$p.ExitCode;$p.Dispose()
if(-not$Quiet){foreach($line in @($stdout-split"`r?`n")){if($line.Trim()){Write-Event 'OUTPUT' $line.Trim()}};foreach($line in @($stderr-split"`r?`n")){$clean=$line.Trim();if(-not$clean){continue};if($code-ne0){Write-Event 'ERROR' $clean}elseif($clean-match'(?i)\b(warn|warning|deprecated)\b'){Write-Event 'WARN' $clean}else{Write-Event 'OUTPUT' $clean}}}
if(-not$AllowFailure-and$code-ne0){throw "$Label ist mit Exit-Code $code fehlgeschlagen.`n$(Get-ProcessFailureSummary $stderr $stdout)"};return[pscustomobject]@{Code=$code;Output=$stdout;Error=$stderr}
}
function Resolve-Tool([string]$Name,[string[]]$Candidates=@()){if($script:Tools.ContainsKey($Name)-and(Test-Path -LiteralPath $script:Tools[$Name])){return$script:Tools[$Name]};foreach($c in @($Candidates)){if($c-and(Test-Path -LiteralPath $c -PathType Leaf)){$script:Tools[$Name]=(Resolve-Path $c).Path;return$script:Tools[$Name]}};$cmd=Get-Command $Name -CommandType Application -ErrorAction SilentlyContinue|Select-Object -First 1;if($cmd-and$cmd.Source-and(Test-Path $cmd.Source)){$script:Tools[$Name]=$cmd.Source;return$cmd.Source};return$null}
function Get-GitPath{return Resolve-Tool 'git.exe' @("$env:ProgramFiles\Git\cmd\git.exe","$env:LOCALAPPDATA\Programs\Git\cmd\git.exe")}
function Get-NodePath{return Resolve-Tool 'node.exe' @("$env:ProgramFiles\nodejs\node.exe","$env:LOCALAPPDATA\Programs\nodejs\node.exe")}
function Get-NpmPath{return Resolve-Tool 'npm.cmd' @("$env:ProgramFiles\nodejs\npm.cmd","$env:LOCALAPPDATA\Programs\nodejs\npm.cmd")}
function Invoke-WebJson([string]$Uri){for($i=1;$i-le3;$i++){try{return Invoke-RestMethod -Uri $Uri -Headers @{'User-Agent'="Vendoo-Updater/$UpdaterVersion";'Accept'='application/vnd.github+json'} -TimeoutSec 30}catch{if($i-eq3){throw};Start-Sleep -Seconds(2*$i)}}}
function Download-File([string]$Uri,[string]$Destination,[string]$Label){for($attempt=1;$attempt-le3;$attempt++){try{$handler=New-Object Net.Http.HttpClientHandler;$client=[Net.Http.HttpClient]::new($handler);$client.Timeout=[TimeSpan]::FromMinutes(5);$client.DefaultRequestHeaders.UserAgent.ParseAdd("Vendoo-Updater/$UpdaterVersion");$resp=$client.GetAsync($Uri,[Net.Http.HttpCompletionOption]::ResponseHeadersRead).Result;$null=$resp.EnsureSuccessStatusCode();$input=$resp.Content.ReadAsStreamAsync().Result;$output=[IO.File]::Open($Destination,[IO.FileMode]::Create,[IO.FileAccess]::Write,[IO.FileShare]::None);try{$buf=New-Object byte[] 65536;while(($read=$input.Read($buf,0,$buf.Length))-gt0){Assert-NotCancelled;$output.Write($buf,0,$read)}}finally{$output.Dispose();$input.Dispose();$resp.Dispose();$client.Dispose();$handler.Dispose()};return}catch{if($attempt-eq3){throw "$Label konnte nach drei Versuchen nicht geladen werden: $($_.Exception.Message)"};Start-Sleep -Seconds(3*$attempt)}}}
function Ensure-GitHubCli{
$managed=Join-Path $ToolsRoot 'github-cli\current\bin\gh.exe';if(Test-Path $managed){$test=Invoke-External $managed @('--version') -TimeoutSeconds 20 -AllowFailure -Label 'GitHub CLI prüfen' -Quiet;if($test.Code-eq0){$script:Tools['gh.exe']=$managed;return$managed}}
Write-State 18 'GitHub CLI' 'Die offizielle portable GitHub CLI wird sicher geladen und per SHA-256 geprüft.';$release=Invoke-WebJson 'https://api.github.com/repos/cli/cli/releases/latest';$arch=if([Environment]::Is64BitOperatingSystem){'amd64'}else{'386'};$asset=@($release.assets|Where-Object{$_.name-like"gh_*_windows_$arch.zip"}|Select-Object -First 1);$checks=@($release.assets|Where-Object{$_.name-like'*_checksums.txt'}|Select-Object -First 1);if(-not$asset-or-not$checks){throw'Die offiziellen GitHub-CLI-Release-Dateien konnten nicht ermittelt werden.'}
$tmp=Join-Path $env:TEMP "vendoo-gh-$PID";New-Item -ItemType Directory -Path $tmp -Force|Out-Null;$zip=Join-Path $tmp $asset.name;$checkFile=Join-Path $tmp $checks.name;$extract=Join-Path $tmp 'extract'
try{Download-File $asset.browser_download_url $zip 'GitHub CLI';Download-File $checks.browser_download_url $checkFile 'Prüfsummen';$line=Get-Content $checkFile|Where-Object{$_-match[regex]::Escape($asset.name)}|Select-Object -First 1;if(-not$line){throw'Für das GitHub-CLI-Archiv wurde keine offizielle Prüfsumme gefunden.'};$expected=($line-split'\s+')[0].ToLowerInvariant();$actual=(Get-FileHash $zip -Algorithm SHA256).Hash.ToLowerInvariant();if($actual-ne$expected){throw'SHA-256-Prüfung der GitHub CLI fehlgeschlagen.'};New-Item -ItemType Directory -Path $extract -Force|Out-Null;Expand-Archive $zip $extract -Force;$source=Get-ChildItem $extract -Filter gh.exe -File -Recurse|Select-Object -First 1;if(-not$source){throw'Das geprüfte GitHub-CLI-Archiv enthält keine gh.exe.'};$targetRoot=Split-Path -Parent(Split-Path -Parent $managed);if(Test-Path $targetRoot){Remove-Item $targetRoot -Recurse -Force};New-Item -ItemType Directory -Path(Split-Path -Parent $managed)-Force|Out-Null;Copy-Item $source.FullName $managed -Force;$null=Invoke-External $managed @('--version') -TimeoutSeconds 20 -Label 'GitHub CLI validieren';$script:Tools['gh.exe']=$managed;Write-Event 'OK' "GitHub CLI $($release.tag_name) wurde geprüft und lokal bereitgestellt.";return$managed}finally{Remove-Item $tmp -Recurse -Force -ErrorAction SilentlyContinue}
}
function Test-GitHubAuthentication([string]$GhPath){$status=Invoke-External $GhPath @('auth','status','--hostname','github.com') -TimeoutSeconds 20 -AllowFailure -Label 'GitHub-Anmeldung prüfen' -Quiet;return($status.Code-eq0)}
function Invoke-VisibleGitHubLogin([string]$GhPath){
$runtimeDirectory=Split-Path -Parent $StatusFile;$loginScript=Join-Path $runtimeDirectory 'github-login.cmd';$escapedGh=$GhPath.Replace('"','""');$lines=@('@echo off','chcp 65001 >nul','title Vendoo - GitHub Anmeldung','echo Vendoo - Sichere GitHub Anmeldung','echo.','echo Folge dem Browserdialog zur GitHub-Anmeldung.','echo.',('"{0}" auth login --hostname github.com --git-protocol https --web --clipboard'-f$escapedGh),'exit /b %ERRORLEVEL%');[IO.File]::WriteAllLines($loginScript,$lines,(New-Object Text.UTF8Encoding($false)));Write-State 26 'GitHub-Anmeldung' 'Im geöffneten Fenster den Gerätecode bestätigen.' 'waiting';$loginProcess=Start-Process -FilePath 'cmd.exe' -ArgumentList @('/d','/c',('"{0}"'-f$loginScript))-WorkingDirectory $runtimeDirectory -WindowStyle Normal -PassThru;$deadline=(Get-Date).AddMinutes(10);while((Get-Date)-lt$deadline){Assert-NotCancelled;if(Test-GitHubAuthentication $GhPath){return};if($loginProcess.HasExited){throw'Die GitHub-Anmeldung wurde nicht abgeschlossen.'};Start-Sleep -Seconds 2};throw'Die GitHub-Anmeldung wurde nicht innerhalb von zehn Minuten abgeschlossen.'
}
function Check-ProductionVersion{try{$r=Invoke-RestMethod -Uri "$PublicBaseUrl/readyz?updater=$Stamp" -TimeoutSec 20 -Headers @{'Cache-Control'='no-cache';'User-Agent'="Vendoo-Updater/$UpdaterVersion"};return[pscustomobject]@{Ready=[bool]$r.ok;Version=[string]$r.version}}catch{return[pscustomobject]@{Ready=$false;Version='unbekannt'}}}
function Invoke-Lifecycle([string]$Command,$InputObject){$json=$InputObject|ConvertTo-Json -Depth 20 -Compress;$r=Invoke-External $script:Node @($LifecycleScript,$Command)-InputText $json -TimeoutSeconds 60 -Label "Lifecycle $Command" -Quiet;return($r.Output|ConvertFrom-Json)}
function Assert-Package{
foreach($f in @('package.json','package-lock.json','update-manifest.json','release-manifest.json','db\schema.sql','tools\apply-release-manifest.mjs','tools\updater-lifecycle-3.4.mjs','tools\check-release-payload-3.4.mjs')){if(-not(Test-Path(Join-Path $PackageRoot $f))){throw"Paketdatei fehlt: $f"}}
$p=Get-Content(Join-Path $PackageRoot 'package.json')-Raw-Encoding UTF8|ConvertFrom-Json;$m=Get-Content(Join-Path $PackageRoot 'update-manifest.json')-Raw-Encoding UTF8|ConvertFrom-Json;$r=Get-Content(Join-Path $PackageRoot 'release-manifest.json')-Raw-Encoding UTF8|ConvertFrom-Json
if([string]$p.version-ne$TargetVersion-or[string]$m.version-ne$TargetVersion-or[string]$r.targetVersion-ne$TargetVersion){throw"Paket-, Update- und Release-Manifest-Version müssen exakt $TargetVersion sein."};if([string]$r.schema-ne'vendoo-release-package-v2'){if(-not$SelfTest){throw"Auslieferbares Paket benötigt Manifest-Schema vendoo-release-package-v2, gefunden: $($r.schema)"}};if([string]$r.updaterVersion-ne$UpdaterVersion){throw"Release-Manifest gehört zu Updater $($r.updaterVersion), erwartet wird $UpdaterVersion."};foreach($name in @('sourceCommit','baseCommit','createdAt','payloadHash')){if([string]::IsNullOrWhiteSpace([string]$r.$name)){if(-not$SelfTest){throw"Release-Manifest enthält keine gültige Herkunft: $name fehlt."}}};$script:Manifest=$r
}
function Get-MainVersion([string]$GhPath){$encoded=(Invoke-External $GhPath @('api',"repos/$Repository/contents/package.json?ref=$BaseBranch",'--jq','.content')-TimeoutSeconds 60 -Label 'main-Version lesen' -Quiet).Output -replace'\s','';$text=[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($encoded));return[string](($text|ConvertFrom-Json).version)}
function Get-ReleaseObservation([string]$GhPath){
$sha=(Invoke-External $GhPath @('api',"repos/$Repository/commits/$BaseBranch",'--jq','.sha')-TimeoutSeconds 60 -Label 'main-SHA lesen' -Quiet).Output.Trim();$version=Get-MainVersion $GhPath;$prod=Check-ProductionVersion
$prResult=Invoke-External $GhPath @('pr','list','--repo',$Repository,'--head',$Branch,'--state','all','--limit','100','--json','number,url,state,mergedAt,closedAt,updatedAt,headRefOid,title')-TimeoutSeconds 60 -AllowFailure -Label 'Release-PRs ermitteln' -Quiet;$prs=@();if($prResult.Code-eq0-and$prResult.Output.Trim()){$raw=@($prResult.Output|ConvertFrom-Json);foreach($pr in $raw){$prs+=[pscustomobject]@{targetVersion=$TargetVersion;number=$pr.number;url=$pr.url;state=$pr.state;mergedAt=$pr.mergedAt;closedAt=$pr.closedAt;updatedAt=$pr.updatedAt;headRefOid=$pr.headRefOid;title=$pr.title}}}
return[pscustomobject]@{MainSha=$sha;MainVersion=$version;Production=$prod;PullRequests=$prs}
}
function Get-Decision($Observation,$Session){return Invoke-Lifecycle 'decide' ([ordered]@{targetVersion=$TargetVersion;mainVersion=$Observation.MainVersion;productionReady=$Observation.Production.Ready;productionVersion=$Observation.Production.Version;pullRequests=$Observation.PullRequests;session=$Session})}
function Complete-WithoutSource([string]$Reason,$Observation,$Session){foreach($id in @('workspace','tests','pullrequest','merge','deploy','verify')){Skip-Step $id};$Session.releaseState='completed';$Session.productionVersion=$Observation.Production.Version;$Session.completedAt=(Get-Date).ToString('o');Save-Session $Session;Write-State 100 'Bereits vollständig aktuell' "Vendoo $TargetVersion ist in main und Produktion bereits bestätigt. Grund: $Reason" 'success';Write-Event 'OK' 'Zielzustand bereits erreicht; kein Branch, kein PR und kein Redeploy erforderlich.'}
function Get-OpenPrFromDecision($Decision){if($Decision.PSObject.Properties.Name-contains'pullRequest'){return$Decision.pullRequest};return$null}
function Wait-RequiredGitHubChecks([string]$GhPath,[string]$PrUrl,$Session){
$deadline=(Get-Date).AddMinutes(45);$head='';$headSince=Get-Date;$lastMatrix=''
while($true){Assert-NotCancelled;$pr=(Invoke-External $GhPath @('pr','view',$PrUrl,'--repo',$Repository,'--json','number,url,state,mergedAt,headRefOid,statusCheckRollup')-TimeoutSeconds 90 -Label 'PR-Head und Checks lesen' -Quiet).Output|ConvertFrom-Json
if($pr.mergedAt){return[pscustomobject]@{Merged=$true;HeadSha=[string]$pr.headRefOid;Pr=$pr}}
$current=[string]$pr.headRefOid;if(-not$current){throw'Der Pull Request meldet keinen Head-SHA.'};if($current-ne$head){$head=$current;$headSince=Get-Date;$Session.finalPrHeadSha=$head;Save-Session $Session;Write-Event 'INFO' "Aktueller PR-Head-SHA: $head. Check-Grace-Period wurde neu gestartet."}
$runsResult=Invoke-External $GhPath @('api',"repos/$Repository/actions/runs?head_sha=$head&per_page=100")-TimeoutSeconds 90 -AllowFailure -Label 'Workflow-Runs des finalen Heads lesen' -Quiet;$runs=@();if($runsResult.Code-eq0-and$runsResult.Output.Trim()){try{$runs=@(($runsResult.Output|ConvertFrom-Json).workflow_runs)}catch{$runs=@()}}
$elapsed=[int]((Get-Date)-$headSince).TotalSeconds;$timedOut=(Get-Date)-ge$deadline;$classification=Invoke-Lifecycle 'classify-checks' ([ordered]@{checks=@($pr.statusCheckRollup);workflowRuns=$runs;elapsedSeconds=$elapsed;gracePeriodSeconds=90;timedOut=$timedOut})
$matrix=@($classification.rows|ForEach-Object{"$($_.name): $($_.state) ($($_.conclusion)$($_.status))"})-join'; '
if($matrix-ne$lastMatrix-or((Get-Date)-$script:LastCheckLog).TotalSeconds-ge30){Write-Event 'INFO' "GitHub-Status für ${head}: $matrix";$lastMatrix=$matrix;$script:LastCheckLog=Get-Date}
if([string]$classification.state-eq'failed'){throw"Ein erforderlicher GitHub-Check ist fehlgeschlagen.`n$matrix"}
if([string]$classification.state-eq'timeout'){throw"Zeitlimit beim Warten auf GitHub-Checks für finalen Head $head.`n$matrix"}
if([string]$classification.state-eq'success'){$verify=(Invoke-External $GhPath @('pr','view',$PrUrl,'--repo',$Repository,'--json','headRefOid')-TimeoutSeconds 60 -Label 'Finalen PR-Head bestätigen' -Quiet).Output|ConvertFrom-Json;if([string]$verify.headRefOid-eq$head){return[pscustomobject]@{Merged=$false;HeadSha=$head;Pr=$pr}};Write-Event 'INFO' 'PR-Head änderte sich nach erfolgreicher Check-Auswertung; Auswertung wird für den neuen Head wiederholt.'}
Write-State 80 'GitHub-Checks' "Finaler Head ${head}: $($classification.reason).";Start-Sleep -Seconds 10
}
}
function Ensure-Deployment($Session){
$prod=Check-ProductionVersion;if($prod.Ready-and$prod.Version-eq$TargetVersion){Write-Event 'OK' 'Produktion meldet die Zielversion bereits; Redeploy-Wartephase entfällt.';Complete-Step 'deploy';return$prod}
if($SkipProductionDeploy){Skip-Step 'deploy';Skip-Step 'verify';Write-State 100 'Source aktualisiert' 'GitHub wurde aktualisiert; das Produktionsdeployment wurde auf Wunsch übersprungen.' 'success';return$null}
Set-Step 'deploy' 'active';Write-State 88 'Coolify Auto Deploy' 'main ist auf Zielstand. Coolify übernimmt das Deployment über die bestehende GitHub-Verknüpfung.';$Session.releaseState='deploying';Save-Session $Session;$deadline=(Get-Date).AddMinutes(15);$nextLog=Get-Date;$last=''
do{Assert-NotCancelled;$prod=Check-ProductionVersion;$last="ready=$($prod.Ready), version=$($prod.Version)";if((Get-Date)-ge$nextLog){Write-Event 'INFO' "Coolify Auto Deploy: $last";$nextLog=(Get-Date).AddSeconds(30)};if($prod.Ready-and$prod.Version-eq$TargetVersion){break};Start-Sleep -Seconds 10}while((Get-Date)-lt$deadline)
if(-not($prod.Ready-and$prod.Version-eq$TargetVersion)){throw"Coolify hat die Zielversion innerhalb von 15 Minuten nicht bereitgestellt. Letzter Stand: $last."};Complete-Step 'deploy';return$prod
}
try{
Write-Event 'INFO' "Vendoo Production Updater $UpdaterVersion gestartet. Zielversion $TargetVersion."
Set-Step 'package' 'active';Write-State 3 'Paketprüfung' 'Version, Herkunft, Payload-Hash und Manifest werden geprüft.';Assert-Package;$session=Load-Session;if($SelfTestFailAt-eq'package'){throw'Simulierter Paketfehler.'};Complete-Step 'package';Write-Event 'OK' "Paketprüfung erfolgreich. payloadHash=$($script:Manifest.payloadHash), sourceCommit=$($script:Manifest.sourceCommit), baseCommit=$($script:Manifest.baseCommit)."
Set-Step 'tools' 'active';Write-State 9 'Systemprüfung' 'Git, Node.js, npm und GitHub CLI werden geprüft.';$script:Git=Get-GitPath;if(-not$script:Git){throw'Git für Windows wurde nicht gefunden.'};$script:Node=Get-NodePath;$script:Npm=Get-NpmPath;if(-not$script:Node-or-not$script:Npm){throw'Node.js 22 LTS oder npm wurde nicht gefunden.'};$nv=Invoke-External $script:Node @('--version')-TimeoutSeconds 20 -Label 'Node.js prüfen';if($nv.Output-notmatch'v?(\d+)\.'-or[int]$Matches[1]-lt22){throw'Node.js 22 oder neuer ist erforderlich.'};$gh=Ensure-GitHubCli;Complete-Step 'tools'
Set-Step 'github' 'active';Write-State 22 'GitHub-Verbindung' 'Anmeldung und Repository-Berechtigungen werden geprüft.';if(-not(Test-GitHubAuthentication $gh)){Invoke-VisibleGitHubLogin $gh};Invoke-External $gh @('auth','setup-git','--hostname','github.com')-TimeoutSeconds 60 -Label 'Git-Anmeldung konfigurieren'|Out-Null;$repo=(Invoke-External $gh @('repo','view',$Repository,'--json','viewerPermission,defaultBranchRef,isPrivate')-TimeoutSeconds 60 -Label 'Repository-Zugriff prüfen').Output|ConvertFrom-Json;if([string]$repo.viewerPermission-notin@('ADMIN','MAINTAIN','WRITE')){throw"Für $Repository fehlt Schreibzugriff."};Complete-Step 'github'
Set-Step 'production' 'active';Write-State 31 'Zielzustand' 'main, Produktion, bestehende PRs und Sitzung werden vor jeder Änderung abgeglichen.';$observation=Get-ReleaseObservation $gh;$decision=Get-Decision $observation $session;Write-Event 'INFO' "Lifecycle-Entscheidung: $($decision.action) / $($decision.reason); main=$($observation.MainVersion)@$($observation.MainSha); production=$($observation.Production.Version).";Complete-Step 'production'
if($SelfTest){foreach($id in @('workspace','tests','pullrequest','merge','deploy','verify')){Set-Step $id 'active';Start-Sleep -Milliseconds 120;Complete-Step $id};Write-State 100 'Selbsttest erfolgreich' 'Updater-3.4-Oberfläche und Host-Vertrag funktionieren.' 'success';return}
if([string]$decision.action-eq'complete'){Complete-WithoutSource $decision.reason $observation $session;return}
$prUrl='';$prNumber=$null;$sourceRequired=([string]$decision.action-eq'source')
if([string]$decision.action-eq'resume-pr'){$pr=Get-OpenPrFromDecision $decision;$prUrl=[string]$pr.url;$prNumber=$pr.number;$session.prUrl=$prUrl;$session.prNumber=$prNumber;$session.releaseState='checks';Save-Session $session;foreach($id in @('workspace','tests')){Skip-Step $id};Complete-Step 'pullrequest';Write-Event 'INFO' "Vorhandener offener Pull Request wird übernommen: $prUrl"}
elseif([string]$decision.action-eq'deploy'){foreach($id in @('workspace','tests','pullrequest','merge')){Skip-Step $id};$sourceRequired=$false}
if($sourceRequired){
Set-Step 'workspace' 'active';Write-State 40 'Payload-Sicherheitsprüfung' 'main wird isoliert geklont; Herkunft und mögliche Rückschritte werden vor dem Overlay geprüft.';if(Test-Path -LiteralPath $WorkRoot){Remove-Item $WorkRoot -Recurse -Force};New-Item -ItemType Directory -Path $WorkRoot -Force|Out-Null;Invoke-External $gh @('repo','clone',$Repository,$CloneDir,'--','--branch',$BaseBranch,'--single-branch')-WorkingDirectory $WorkRoot -TimeoutSeconds 300 -Label 'Repository klonen'|Out-Null
$guard=Invoke-External $script:Node @($PayloadGuardScript,$PackageRoot,$CloneDir,[string]$script:Manifest.baseCommit,[string]$observation.MainSha)-TimeoutSeconds 300 -AllowFailure -Label 'Veralteten Payload ausschließen';if($guard.Code-ne0){$detail='';try{$g=$guard.Output|ConvertFrom-Json;$detail=@($g.conflicts)-join', '}catch{$detail=$guard.Output};throw"Payload-Rückschritt blockiert. baseCommit=$($script:Manifest.baseCommit), main=$($observation.MainSha). Konflikte: $detail"}
Invoke-External $script:Git @('switch','-c',$Branch,$observation.MainSha)-WorkingDirectory $CloneDir -TimeoutSeconds 60 -Label 'Deterministischen Release-Branch erstellen'|Out-Null;Invoke-External $script:Node @((Join-Path $PackageRoot 'tools\apply-release-manifest.mjs'),$PackageRoot,$CloneDir)-TimeoutSeconds 300 -Label 'SHA-256-geprüftes Release-Manifest anwenden'|Out-Null;Complete-Step 'workspace'
Set-Step 'tests' 'active';Write-State 55 'Abnahmetests' 'npm ci und der vollständige Releasevertrag werden auf dem sicheren Overlay ausgeführt.';Invoke-External $script:Npm @('ci','--no-audit','--no-fund')-WorkingDirectory $CloneDir -TimeoutSeconds 1200 -Label 'npm ci'|Out-Null;Invoke-External $script:Npm @('run','verify:all')-WorkingDirectory $CloneDir -TimeoutSeconds 3600 -Label 'npm run verify:all'|Out-Null;Complete-Step 'tests'
$latest=Get-ReleaseObservation $gh;$lateDecision=Get-Decision $latest $session;if([string]$lateDecision.action-eq'complete'){Complete-WithoutSource 'target-reached-during-local-tests' $latest $session;return}elseif([string]$lateDecision.action-eq'resume-pr'){$pr=Get-OpenPrFromDecision $lateDecision;$prUrl=[string]$pr.url;$prNumber=$pr.number;Write-Event 'INFO' "Paralleler Lauf hat bereits einen PR erzeugt; dieser wird übernommen: $prUrl"}elseif([string]$lateDecision.action-eq'deploy'){foreach($id in @('pullrequest','merge')){Skip-Step $id};$sourceRequired=$false}else{
Set-Step 'pullrequest' 'active';Write-State 70 'Pull Request' 'Branch und Pull Request werden idempotent abgeglichen.';Invoke-External $script:Git @('config','user.name','Vendoo Production Updater')-WorkingDirectory $CloneDir -Label 'Git-Name setzen'|Out-Null;$uid=(Invoke-External $gh @('api','user','--jq','.id')-Label 'GitHub-Benutzer-ID').Output.Trim();$ulogin=(Invoke-External $gh @('api','user','--jq','.login')-Label 'GitHub-Benutzername').Output.Trim();Invoke-External $script:Git @('config','user.email',"$uid+$ulogin@users.noreply.github.com")-WorkingDirectory $CloneDir -Label 'Git-E-Mail setzen'|Out-Null;Invoke-External $script:Git @('add','--all')-WorkingDirectory $CloneDir -Label 'Änderungen vormerken'|Out-Null;$diff=Invoke-External $script:Git @('diff','--cached','--quiet')-WorkingDirectory $CloneDir -AllowFailure -Label 'Änderungen prüfen';if($diff.Code-ne1){throw'Payload erzeugt gegenüber main keinen gültigen neuen Source-Stand.'};Invoke-External $script:Git @('commit','-m',"release: Vendoo $TargetVersion production update via updater 3.4")-WorkingDirectory $CloneDir -TimeoutSeconds 120 -Label 'Release committen'|Out-Null
$remoteLine=(Invoke-External $script:Git @('ls-remote','--heads','origin',"refs/heads/$Branch")-WorkingDirectory $CloneDir -TimeoutSeconds 60 -AllowFailure -Label 'Remote-Branch prüfen' -Quiet).Output.Trim();if($remoteLine){$remoteSha=($remoteLine-split'\s+')[0];Invoke-External $script:Git @('push',"--force-with-lease=refs/heads/$Branch`:$remoteSha",'--set-upstream','origin',$Branch)-WorkingDirectory $CloneDir -TimeoutSeconds 300 -Label 'Bestehenden Release-Branch sicher aktualisieren'|Out-Null}else{Invoke-External $script:Git @('push','--set-upstream','origin',$Branch)-WorkingDirectory $CloneDir -TimeoutSeconds 300 -Label 'Release-Branch pushen'|Out-Null}
$existing=Invoke-External $gh @('pr','list','--repo',$Repository,'--head',$Branch,'--state','open','--json','number,url','--jq','.[0]')-AllowFailure -Label 'Vorhandenen Pull Request erneut suchen' -Quiet;if($existing.Output.Trim()){$existingObj=$existing.Output|ConvertFrom-Json;$prUrl=[string]$existingObj.url;$prNumber=$existingObj.number}else{$body=Join-Path $WorkRoot 'pr-body.md';$bodyText="## Vendoo $TargetVersion Production Update`r`n`r`nErstellt durch Vendoo Production Updater $UpdaterVersion.`r`n`r`n- vollständiges ``npm run verify:all`` erfolgreich`r`n- Payload-Herkunft und Rückschrittschutz geprüft`r`n- keine Datenbanken, Uploads, Secrets oder ``/app/data``-Inhalte`r`n- GitHub-Checks werden am finalen Head-SHA ausgewertet`r`n";[IO.File]::WriteAllText($body,$bodyText,(New-Object Text.UTF8Encoding($false)));$created=(Invoke-External $gh @('pr','create','--repo',$Repository,'--base',$BaseBranch,'--head',$Branch,'--title',"Vendoo $TargetVersion Production Update",'--body-file',$body)-WorkingDirectory $CloneDir -TimeoutSeconds 180 -Label 'Pull Request erstellen').Output.Trim();$prUrl=$created;$prInfo=(Invoke-External $gh @('pr','view',$prUrl,'--repo',$Repository,'--json','number')-TimeoutSeconds 60 -Label 'PR-Nummer lesen' -Quiet).Output|ConvertFrom-Json;$prNumber=$prInfo.number}
$session.prUrl=$prUrl;$session.prNumber=$prNumber;$session.releaseState='checks';Save-Session $session;Complete-Step 'pullrequest';Write-Event 'OK' "Pull Request: $prUrl"
}
}
if($prUrl){Set-Step 'merge' 'active';Write-State 78 'GitHub-Checks' 'Workflow-Runs und Pflichtchecks werden für den jeweils aktuellen PR-Head verfolgt.';$checkResult=Wait-RequiredGitHubChecks $gh $prUrl $session;if(-not$checkResult.Merged){Write-State 84 'Merge' 'Der geprüfte finale Head wird atomar per Squash nach main übernommen.';Invoke-External $gh @('pr','merge',$prUrl,'--repo',$Repository,'--squash','--delete-branch','--subject',"Vendoo $TargetVersion Production Update",'--match-head-commit',[string]$checkResult.HeadSha)-WorkingDirectory $CloneDir -TimeoutSeconds 300 -Label 'Pull Request mergen'|Out-Null};$mergedInfo=(Invoke-External $gh @('pr','view',$prUrl,'--repo',$Repository,'--json','mergedAt,mergeCommit,headRefOid,state')-TimeoutSeconds 60 -Label 'Merge-Ergebnis bestätigen' -Quiet).Output|ConvertFrom-Json;if(-not$mergedInfo.mergedAt){throw'Der Pull Request wurde nicht als gemergt bestätigt.'};$session.finalPrHeadSha=[string]$mergedInfo.headRefOid;$session.mergeCommit=[string]$mergedInfo.mergeCommit.oid;$session.releaseState='merged';Save-Session $session;Complete-Step 'merge';Write-Event 'OK' "Merge bestätigt: $($session.mergeCommit)"}
$prod=Ensure-Deployment $session;if($SkipProductionDeploy){return};Set-Step 'verify' 'active';Write-State 97 'Produktionsprüfung' 'Die öffentliche Readiness und die exakte Zielversion werden abschließend bestätigt.';if($null-eq$prod){$prod=Check-ProductionVersion};if(-not($prod.Ready-and$prod.Version-eq$TargetVersion)){throw"Die abschließende Produktionsprüfung ist fehlgeschlagen: ready=$($prod.Ready), version=$($prod.Version)"};Complete-Step 'verify';$session.productionVersion=$prod.Version;$session.releaseState='completed';$session.completedAt=(Get-Date).ToString('o');Save-Session $session;Write-State 100 'Update erfolgreich' "Vendoo $TargetVersion läuft geprüft in Produktion." 'success';Write-Event 'OK' "Vendoo $TargetVersion wurde vollständig und idempotent aktualisiert."
} catch [System.OperationCanceledException] {Fail-Step;Write-State 100 'Update abgebrochen' $_.Exception.Message 'cancelled' $_.Exception.Message 'Der gespeicherte Wiederaufnahmepunkt bleibt erhalten.';Write-Event 'WARN' $_.Exception.Message
}catch{Fail-Step;$msg=$_.Exception.Message;$remediation=if($script:CurrentStep -in @('deploy','verify')){'main kann bereits aktualisiert sein. Ein erneuter Start setzt bei Deployment und Produktionsprüfung fort.'}elseif($msg-match'Payload-Rückschritt'){'Das Paket ist gegenüber main veraltet. Kein Branch und kein PR wurden aus diesem Payload erstellt. Ein neues Paket aus aktuellem main erzeugen.'}else{'Ein erneuter Start gleicht main, PR, finalen Head-SHA und Sitzung erneut ab; bereits abgeschlossene Phasen werden nicht dupliziert.'};Write-State 100 'Update sicher gestoppt' $msg 'failed' $msg $remediation;Write-Event 'ERROR' $msg
}finally{if(Test-Path $InputFile){Remove-Item $InputFile -Force -ErrorAction SilentlyContinue}}
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env python3
"""Build a byte-reproducible ZIP archive from a release directory."""
from __future__ import annotations
import argparse
import os
from pathlib import Path
from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo
FIXED_TIMESTAMP = (1980, 1, 1, 0, 0, 0)
def iter_files(source: Path):
for path in sorted(source.rglob("*"), key=lambda item: item.as_posix()):
if path.is_file() and not path.is_symlink():
yield path
def build_archive(source: Path, destination: Path) -> None:
source = source.resolve(strict=True)
if not source.is_dir():
raise ValueError(f"Source is not a directory: {source}")
destination = destination.resolve()
if source == destination or source in destination.parents:
raise ValueError("Destination must be outside the source directory.")
destination.parent.mkdir(parents=True, exist_ok=True)
temporary = destination.with_suffix(destination.suffix + ".tmp")
temporary.unlink(missing_ok=True)
try:
with ZipFile(temporary, "w", compression=ZIP_DEFLATED, compresslevel=9) as archive:
for file_path in iter_files(source):
relative = file_path.relative_to(source.parent).as_posix()
info = ZipInfo(relative, FIXED_TIMESTAMP)
info.compress_type = ZIP_DEFLATED
info.create_system = 3
mode = file_path.stat().st_mode & 0o777
info.external_attr = (mode & 0xFFFF) << 16
info.flag_bits |= 0x800
archive.writestr(info, file_path.read_bytes(), compress_type=ZIP_DEFLATED, compresslevel=9)
os.replace(temporary, destination)
finally:
temporary.unlink(missing_ok=True)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("source", type=Path)
parser.add_argument("destination", type=Path)
args = parser.parse_args()
build_archive(args.source, args.destination)
print(f"Deterministic ZIP created: {args.destination}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+23 -5
View File
@@ -8,22 +8,38 @@ import {
writeFileSync,
} from 'node:fs';
import { createHash } from 'node:crypto';
import { spawnSync } from 'node:child_process';
import { dirname, join, relative, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
import { generateReleaseManifest } from './generate-release-manifest.mjs';
import { MANIFEST_FILE, readAndValidateManifest } from './release-manifest-lib.mjs';
import { createPackageManifest } from './updater-lifecycle-3.4.mjs';
const root = dirname(dirname(fileURLToPath(import.meta.url)));
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
const outputRoot = join(root, 'release-output');
const packageName = `Vendoo_${pkg.version}`;
const stage = join(outputRoot, packageName);
const normalize = value => value.split(sep).join('/');
// The committed manifest is the only release payload contract. Regenerate it first so
// stale hashes or accidentally omitted source files make the release fail immediately.
function git(args, { allowFailure = false } = {}) {
const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' });
if (!allowFailure && result.status !== 0) {
throw new Error(`git ${args.join(' ')} fehlgeschlagen: ${(result.stderr || result.stdout || '').trim()}`);
}
return result.status === 0 ? String(result.stdout || '').trim() : '';
}
// Das committed Manifest bleibt der reproduzierbare Quellvertrag. Das ausgelieferte
// Paket erhält zusätzlich eine unveränderliche Git-Herkunft und einen Payload-Hash.
generateReleaseManifest(root);
const manifest = readAndValidateManifest(root);
const sourceCommit = process.env.VENDOO_SOURCE_COMMIT || git(['rev-parse', 'HEAD']);
const configuredBase = process.env.VENDOO_BASE_COMMIT || '';
const discoveredBase = configuredBase || git(['merge-base', sourceCommit, 'origin/main'], { allowFailure: true });
const baseCommit = discoveredBase || sourceCommit;
const createdAt = process.env.VENDOO_SOURCE_CREATED_AT || git(['show', '-s', '--format=%cI', sourceCommit]);
const packageManifest = createPackageManifest(manifest, { sourceCommit, baseCommit, createdAt });
const packageName = `Vendoo_${pkg.version}_Updater_${manifest.updaterVersion}`;
const stage = join(outputRoot, packageName);
rmSync(outputRoot, { recursive: true, force: true });
mkdirSync(stage, { recursive: true });
@@ -34,7 +50,8 @@ for (const entry of manifest.files) {
copyFileSync(source, target);
if (statSync(target).size !== entry.size) throw new Error(`Release-Kopie hat falsche Größe: ${entry.path}`);
}
copyFileSync(join(root, MANIFEST_FILE), join(stage, MANIFEST_FILE));
writeFileSync(join(stage, MANIFEST_FILE), `${JSON.stringify(packageManifest, null, 2)}\n`, 'utf8');
readAndValidateManifest(stage);
const checksums = [];
function hashTree(dir) {
@@ -55,3 +72,4 @@ writeFileSync(join(stage, 'FILE_CHECKSUMS_SHA256.txt'), `${checksums.join('\n')}
writeFileSync(join(outputRoot, 'RELEASE_NAME.txt'), `${packageName}\n`, 'utf8');
console.log(`Manifestbasiertes Release-Staging erstellt: ${stage}`);
console.log(`${manifest.files.length} Payload-Dateien und ${checksums.length} Paketdateien mit SHA-256 erfasst.`);
console.log(`Paket-Provenienz: source=${sourceCommit}, base=${baseCommit}, payload=${packageManifest.payloadHash}`);
+86
View File
@@ -0,0 +1,86 @@
import { createHash } from 'node:crypto';
import { readFileSync } from 'node:fs';
import { resolve, join } from 'node:path';
import { spawnSync } from 'node:child_process';
import { evaluatePayloadGuard } from './updater-lifecycle-3.4.mjs';
const sha256 = value => createHash('sha256').update(value).digest('hex');
function git(repo, args, { allowFailure = false, encoding = 'utf8' } = {}) {
const result = spawnSync('git', ['-C', repo, ...args], { encoding, maxBuffer: 64 * 1024 * 1024 });
if (!allowFailure && result.status !== 0) {
throw new Error(`git ${args.join(' ')} fehlgeschlagen: ${(result.stderr || result.stdout || '').trim()}`);
}
return result;
}
function readGitFile(repo, commit, path) {
const result = git(repo, ['show', `${commit}:${path}`], { allowFailure: true, encoding: null });
if (result.status !== 0) return null;
return Buffer.from(result.stdout);
}
function normalize(path) {
return String(path || '').replaceAll('\\', '/').replace(/^\.\//, '');
}
export function inspectPayload({ packageRoot, repositoryRoot, baseCommit, currentMainCommit }) {
const source = resolve(packageRoot);
const repo = resolve(repositoryRoot);
const manifest = JSON.parse(readFileSync(join(source, 'release-manifest.json'), 'utf8'));
const baseExists = git(repo, ['cat-file', '-e', `${baseCommit}^{commit}`], { allowFailure: true }).status === 0;
const mainExists = git(repo, ['cat-file', '-e', `${currentMainCommit}^{commit}`], { allowFailure: true }).status === 0;
if (!baseExists || !mainExists) {
return {
...evaluatePayloadGuard({ baseCommit: baseExists ? baseCommit : '', currentMainCommit: mainExists ? currentMainCommit : '', baseIsAncestor: false }),
payloadChangedPaths: [],
mainChangedPaths: [],
};
}
const baseIsAncestor = git(repo, ['merge-base', '--is-ancestor', baseCommit, currentMainCommit], { allowFailure: true }).status === 0;
const mainChangedPaths = new Set(
String(git(repo, ['diff', '--name-only', `${baseCommit}..${currentMainCommit}`, '--']).stdout || '')
.split(/\r?\n/).map(normalize).filter(Boolean),
);
const payloadChangedPaths = new Set();
const desired = new Map();
for (const entry of manifest.files || []) {
const path = normalize(entry.path);
const baseBytes = readGitFile(repo, baseCommit, path);
const baseHash = baseBytes === null ? null : sha256(baseBytes);
if (baseHash !== String(entry.sha256)) payloadChangedPaths.add(path);
desired.set(path, String(entry.sha256));
}
for (const value of manifest.removedFiles || []) {
const path = normalize(value);
if (readGitFile(repo, baseCommit, path) !== null) payloadChangedPaths.add(path);
desired.set(path, null);
}
const conflictingPaths = [];
for (const path of payloadChangedPaths) {
if (!mainChangedPaths.has(path)) continue;
const currentBytes = readGitFile(repo, currentMainCommit, path);
const currentHash = currentBytes === null ? null : sha256(currentBytes);
if (currentHash !== desired.get(path)) conflictingPaths.push(path);
}
return {
...evaluatePayloadGuard({ baseCommit, currentMainCommit, baseIsAncestor, conflictingPaths }),
payloadChangedPaths: [...payloadChangedPaths].sort(),
mainChangedPaths: [...mainChangedPaths].sort(),
};
}
if (process.argv[1] && import.meta.url === new URL(`file://${process.argv[1].replaceAll('\\', '/')}`).href) {
const [packageRoot, repositoryRoot, baseCommit, currentMainCommit] = process.argv.slice(2);
try {
if (!packageRoot || !repositoryRoot || !baseCommit || !currentMainCommit) {
throw new Error('Aufruf: node check-release-payload-3.4.mjs <packageRoot> <repositoryRoot> <baseCommit> <currentMainCommit>');
}
const result = inspectPayload({ packageRoot, repositoryRoot, baseCommit, currentMainCommit });
process.stdout.write(`${JSON.stringify(result)}\n`);
if (!result.allowed) process.exitCode = 2;
} catch (error) {
process.stderr.write(`[FEHLER] ${error.message}\n`);
process.exit(1);
}
}
+16 -2
View File
@@ -10,6 +10,7 @@ import {
statSync,
} from 'node:fs';
import { createHash } from 'node:crypto';
import { computePackagePayloadHash, PACKAGE_MANIFEST_SCHEMA } from './updater-lifecycle-3.4.mjs';
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path';
export const MANIFEST_SCHEMA = 'vendoo-release-manifest-v1';
@@ -27,8 +28,10 @@ export const REQUIRED_RELEASE_FILES = Object.freeze([
'scripts/updater/Vendoo.Updater.Preflight.ps1',
'scripts/updater/Vendoo.Updater.UI.ps1',
'scripts/updater/Vendoo.Updater.Host.ps1',
'scripts/updater/Vendoo.Updater.Worker.ps1',
'scripts/updater/Vendoo.Updater.Worker34.ps1',
'tools/apply-release-manifest.mjs',
'tools/updater-lifecycle-3.4.mjs',
'tools/check-release-payload-3.4.mjs',
]);
const allowedEnvTemplates = new Set(['.env.example', '.env.docker.example']);
@@ -142,8 +145,19 @@ export function readAndValidateManifest(sourceRoot, { allowExtraFiles = false }
const manifestPath = join(base, MANIFEST_FILE);
if (!existsSync(manifestPath)) throw new Error(`${MANIFEST_FILE} fehlt.`);
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
if (manifest.schema !== MANIFEST_SCHEMA) throw new Error(`Unbekanntes Manifest-Schema: ${manifest.schema}`);
if (![MANIFEST_SCHEMA, PACKAGE_MANIFEST_SCHEMA].includes(manifest.schema)) throw new Error(`Unbekanntes Manifest-Schema: ${manifest.schema}`);
if (manifest.mode !== 'overlay-with-explicit-removals') throw new Error(`Unzulässiger Release-Modus: ${manifest.mode}`);
if (manifest.schema === PACKAGE_MANIFEST_SCHEMA) {
if (manifest.targetVersion !== manifest.version) throw new Error('targetVersion und version des Paketmanifests stimmen nicht überein.');
for (const field of ['sourceCommit', 'baseCommit']) {
if (!/^[a-f0-9]{40}$/.test(String(manifest[field] || ''))) throw new Error(`Ungültige Paket-Provenienz: ${field}`);
}
const createdAt = Date.parse(String(manifest.createdAt || ''));
if (!Number.isFinite(createdAt)) throw new Error('Ungültige Paket-Provenienz: createdAt');
if (!/^[a-f0-9]{64}$/.test(String(manifest.payloadHash || ''))) throw new Error('Ungültige Paket-Provenienz: payloadHash');
const expectedPayloadHash = computePackagePayloadHash(manifest);
if (manifest.payloadHash !== expectedPayloadHash) throw new Error('Payload-Hash des Paketmanifests stimmt nicht.');
}
if (!Array.isArray(manifest.files) || manifest.files.length === 0) throw new Error('Release-Manifest enthält keine Dateien.');
if (!Array.isArray(manifest.removedFiles)) throw new Error('removedFiles fehlt im Release-Manifest.');
const seen = new Set();
+200
View File
@@ -0,0 +1,200 @@
import { createHash } from 'node:crypto';
const ACTIVE = new Set(['queued', 'pending', 'in_progress', 'waiting', 'requested']);
const SUCCESS = new Set(['success', 'neutral', 'skipped']);
const FAILURE = new Set(['failure', 'timed_out', 'action_required', 'startup_failure', 'stale']);
const CANCELLED = new Set(['cancelled']);
export const UPDATER_VERSION = '3.4.0';
export const SESSION_SCHEMA = 'vendoo-updater-session-v2';
export const PACKAGE_MANIFEST_SCHEMA = 'vendoo-release-package-v2';
export const REQUIRED_CHECKS = Object.freeze([
'Syntax, Sicherheit und Release-Gates',
'Windows vollständiger Releasevertrag',
'Updater-3.4-Paket nach Abnahme',
]);
export const REQUIRED_WORKFLOWS = Object.freeze([
'Vendoo CI',
'Vendoo Release Manifest Sync',
]);
const lower = value => String(value || '').trim().toLowerCase();
const time = value => {
const parsed = Date.parse(value || '');
return Number.isFinite(parsed) ? parsed : 0;
};
const stablePayload = manifest => ({
schema: PACKAGE_MANIFEST_SCHEMA,
targetVersion: String(manifest.targetVersion || manifest.version || ''),
updaterVersion: String(manifest.updaterVersion || ''),
sourceCommit: String(manifest.sourceCommit || ''),
baseCommit: String(manifest.baseCommit || ''),
createdAt: String(manifest.createdAt || ''),
mode: String(manifest.mode || ''),
requiredFiles: [...(manifest.requiredFiles || [])],
removedFiles: [...(manifest.removedFiles || [])],
files: [...(manifest.files || [])],
});
export function computePackagePayloadHash(manifest) {
return createHash('sha256').update(JSON.stringify(stablePayload(manifest))).digest('hex');
}
export function createPackageManifest(sourceManifest, { sourceCommit, baseCommit, createdAt }) {
const manifest = {
...sourceManifest,
schema: PACKAGE_MANIFEST_SCHEMA,
targetVersion: String(sourceManifest.version || sourceManifest.targetVersion || ''),
sourceCommit: String(sourceCommit || ''),
baseCommit: String(baseCommit || ''),
createdAt: String(createdAt || ''),
};
manifest.payloadHash = computePackagePayloadHash(manifest);
return manifest;
}
export function payloadIdentity(manifest) {
const targetVersion = String(manifest?.targetVersion || manifest?.version || '');
const payloadHash = String(manifest?.payloadHash || '');
return `${targetVersion}:${payloadHash}`;
}
export function createSession({ repository, targetVersion, updaterVersion = UPDATER_VERSION, payloadHash, branch }) {
return {
schema: SESSION_SCHEMA,
repository,
targetVersion,
updaterVersion,
payloadHash,
branch,
releaseState: 'new',
prNumber: null,
prUrl: '',
finalPrHeadSha: '',
mergeCommit: '',
productionVersion: '',
completedAt: '',
updatedAt: new Date().toISOString(),
};
}
export function sessionMatches(session, { repository, targetVersion, payloadHash }) {
return Boolean(session && session.schema === SESSION_SCHEMA && session.repository === repository && session.targetVersion === targetVersion && session.payloadHash === payloadHash);
}
export function decideReleasePath({ targetVersion, mainVersion, productionReady, productionVersion, pullRequests = [], session = null }) {
const productionAtTarget = Boolean(productionReady && productionVersion === targetVersion);
if (session?.releaseState === 'completed' && productionAtTarget) return { action: 'complete', reason: 'completed-session' };
if (mainVersion === targetVersion) {
return productionAtTarget ? { action: 'complete', reason: 'main-and-production-at-target' } : { action: 'deploy', reason: 'main-at-target-production-behind' };
}
const relevant = pullRequests.filter(pr => pr && pr.targetVersion === targetVersion);
const open = relevant.filter(pr => lower(pr.state) === 'open').sort((a, b) => time(b.updatedAt) - time(a.updatedAt))[0];
if (open) return { action: 'resume-pr', reason: 'matching-open-pr', pullRequest: open };
const merged = relevant.filter(pr => Boolean(pr.mergedAt || pr.merged)).sort((a, b) => time(b.mergedAt || b.updatedAt) - time(a.mergedAt || a.updatedAt))[0];
if (merged) return { action: 'deploy', reason: 'matching-pr-already-merged', pullRequest: merged };
return { action: 'source', reason: 'source-update-required' };
}
function normalizeCheck(check) {
const status = lower(check?.status);
const conclusion = lower(check?.conclusion);
const updatedAt = check?.completedAt || check?.startedAt || check?.updatedAt || '';
let state = 'missing';
if (ACTIVE.has(status) || (!conclusion && status && status !== 'completed')) state = 'pending';
else if (SUCCESS.has(conclusion)) state = 'success';
else if (FAILURE.has(conclusion)) state = 'failure';
else if (CANCELLED.has(conclusion)) state = 'cancelled';
else if (status === 'completed' && !conclusion) state = 'pending';
return { name: String(check?.name || ''), workflowName: String(check?.workflowName || ''), state, status, conclusion, updatedAt, detailsUrl: String(check?.detailsUrl || '') };
}
function bestCheck(checks) {
return [...checks].sort((a, b) => {
const dateDelta = time(b.updatedAt) - time(a.updatedAt);
if (dateDelta) return dateDelta;
const rank = { success: 4, pending: 3, failure: 2, cancelled: 1, missing: 0 };
return (rank[b.state] || 0) - (rank[a.state] || 0);
})[0];
}
function normalizeRun(run) {
const status = lower(run?.status);
const conclusion = lower(run?.conclusion);
let state = 'missing';
if (ACTIVE.has(status)) state = 'pending';
else if (SUCCESS.has(conclusion)) state = 'success';
else if (FAILURE.has(conclusion)) state = 'failure';
else if (CANCELLED.has(conclusion)) state = 'cancelled';
return { name: String(run?.name || run?.workflowName || ''), state, status, conclusion, updatedAt: run?.updated_at || run?.updatedAt || run?.run_started_at || '', url: String(run?.html_url || run?.url || '') };
}
function bestRun(runs) {
return [...runs].sort((a, b) => {
const dateDelta = time(b.updatedAt) - time(a.updatedAt);
if (dateDelta) return dateDelta;
const rank = { success: 4, pending: 3, failure: 2, cancelled: 1, missing: 0 };
return (rank[b.state] || 0) - (rank[a.state] || 0);
})[0];
}
export function classifyChecks({ checks = [], workflowRuns = [], requiredChecks = REQUIRED_CHECKS, requiredWorkflows = REQUIRED_WORKFLOWS, elapsedSeconds = 0, gracePeriodSeconds = 90, timedOut = false }) {
const normalizedChecks = checks.map(normalizeCheck);
const normalizedRuns = workflowRuns.map(normalizeRun);
const checkRows = requiredChecks.map(name => {
const candidates = normalizedChecks.filter(check => check.name === name);
return candidates.length ? bestCheck(candidates) : { name, state: 'missing', status: '', conclusion: '', updatedAt: '', detailsUrl: '' };
});
const workflowRows = requiredWorkflows.map(name => {
const candidates = normalizedRuns.filter(run => run.name === name);
return candidates.length ? bestRun(candidates) : { name, state: 'missing', status: '', conclusion: '', updatedAt: '', url: '' };
});
const rows = [...checkRows, ...workflowRows];
const terminalFailure = rows.find(row => row.state === 'failure');
if (terminalFailure) return { state: 'failed', reason: `${terminalFailure.name}: ${terminalFailure.conclusion || terminalFailure.status}`, rows };
const terminalCancelled = rows.find(row => row.state === 'cancelled');
if (terminalCancelled) return { state: 'failed', reason: `${terminalCancelled.name}: cancelled`, rows };
if (rows.every(row => row.state === 'success')) return { state: 'success', reason: 'all-required-checks-successful', rows };
const missing = rows.filter(row => row.state === 'missing');
if (timedOut) return { state: 'timeout', reason: 'required-checks-not-terminal-before-timeout', rows };
if (missing.length && elapsedSeconds < gracePeriodSeconds) return { state: 'waiting-registration', reason: 'checks-within-registration-grace-period', rows };
if (missing.length) return { state: 'waiting-missing', reason: 'required-checks-not-yet-registered', rows };
return { state: 'pending', reason: 'required-checks-still-running', rows };
}
export function formatStatusMatrix(rows = []) {
return rows.map(row => `${row.name}: ${row.state} (${row.conclusion || row.status || 'not-reported'})`).join('\n');
}
export function evaluatePayloadGuard({ baseCommit, currentMainCommit, baseIsAncestor, conflictingPaths = [] }) {
if (!baseCommit || !currentMainCommit) return { allowed: false, reason: 'missing-commit-provenance', conflicts: [] };
if (!baseIsAncestor) return { allowed: false, reason: 'base-is-not-ancestor-of-main', conflicts: [] };
if (conflictingPaths.length) return { allowed: false, reason: 'payload-would-overwrite-newer-main', conflicts: [...conflictingPaths].sort() };
return { allowed: true, reason: baseCommit === currentMainCommit ? 'exact-base' : 'non-conflicting-main-advance', conflicts: [] };
}
function parseStdin() {
return new Promise((resolve, reject) => {
let input = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', chunk => { input += chunk; });
process.stdin.on('end', () => { try { resolve(input.trim() ? JSON.parse(input) : {}); } catch (error) { reject(error); } });
process.stdin.on('error', reject);
});
}
if (process.argv[1] && import.meta.url === new URL(`file://${process.argv[1].replaceAll('\\', '/')}`).href) {
const command = process.argv[2];
try {
const input = await parseStdin();
let output;
if (command === 'decide') output = decideReleasePath(input);
else if (command === 'classify-checks') output = classifyChecks(input);
else if (command === 'payload-guard') output = evaluatePayloadGuard(input);
else throw new Error(`Unbekannter Lifecycle-Befehl: ${command}`);
process.stdout.write(`${JSON.stringify(output)}\n`);
} catch (error) {
process.stderr.write(`[FEHLER] ${error.message}\n`);
process.exit(1);
}
}
+20 -5
View File
@@ -1,5 +1,7 @@
[CmdletBinding()]
param()
param(
[string]$ReportPath = ''
)
Set-StrictMode -Version 2.0
$ErrorActionPreference = 'Stop'
@@ -10,7 +12,7 @@ $files = Get-ChildItem -LiteralPath $root -Recurse -File | Where-Object {
} | Where-Object {
$full = $_.FullName
-not (@($excludedParts | Where-Object { $full.IndexOf($_,[StringComparison]::OrdinalIgnoreCase) -ge 0 }).Count)
}
} | Sort-Object FullName
$failures = New-Object System.Collections.Generic.List[string]
foreach ($file in $files) {
@@ -18,14 +20,27 @@ foreach ($file in $files) {
$errors = $null
[void][System.Management.Automation.Language.Parser]::ParseFile($file.FullName,[ref]$tokens,[ref]$errors)
foreach ($parseError in @($errors)) {
$failures.Add(("{0}:{1}:{2} {3}" -f $parseError.Extent.File,$parseError.Extent.StartLineNumber,$parseError.Extent.StartColumnNumber,$parseError.Message))
$relative = $file.FullName.Substring($root.Length).TrimStart('\')
$failures.Add(("{0}:{1}:{2} {3}" -f $relative,$parseError.Extent.StartLineNumber,$parseError.Extent.StartColumnNumber,$parseError.Message))
}
}
if ($failures.Count -gt 0) {
Write-Error ("Windows-PowerShell-Parser-Gate fehlgeschlagen ({0} Fehler):{1}{2}" -f $failures.Count,[Environment]::NewLine,(@($failures) -join [Environment]::NewLine))
$report = "Windows-PowerShell-Parser-Gate fehlgeschlagen ($($failures.Count) Fehler):$([Environment]::NewLine)$(@($failures) -join [Environment]::NewLine)"
} else {
$report = "OK: Windows PowerShell $($PSVersionTable.PSVersion) hat $($files.Count) PowerShell-Dateien ohne Parserfehler akzeptiert."
}
if (-not [string]::IsNullOrWhiteSpace($ReportPath)) {
$parent = Split-Path -Parent $ReportPath
if ($parent) { New-Item -ItemType Directory -Path $parent -Force | Out-Null }
[IO.File]::WriteAllText($ReportPath,$report,(New-Object Text.UTF8Encoding($false)))
}
if ($failures.Count -gt 0) {
[Console]::Error.WriteLine($report)
exit 1
}
Write-Host ("OK: Windows PowerShell {0} hat {1} PowerShell-Dateien ohne Parserfehler akzeptiert." -f $PSVersionTable.PSVersion,$files.Count)
Write-Host $report
exit 0
+170
View File
@@ -0,0 +1,170 @@
import { existsSync, readFileSync, mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { createHash } from 'node:crypto';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { spawnSync } from 'node:child_process';
import {
classifyChecks,
createPackageManifest,
decideReleasePath,
evaluatePayloadGuard,
formatStatusMatrix,
PACKAGE_MANIFEST_SCHEMA,
REQUIRED_CHECKS,
REQUIRED_WORKFLOWS,
SESSION_SCHEMA,
UPDATER_VERSION,
} from './updater-lifecycle-3.4.mjs';
import { inspectPayload } from './check-release-payload-3.4.mjs';
const root = resolve(import.meta.dirname, '..');
const failures = [];
const assert = (condition, message) => { if (!condition) failures.push(message); };
const read = rel => readFileSync(join(root, rel), 'utf8');
for (const file of [
'scripts/updater/Vendoo.Updater.Worker34.ps1',
'tools/updater-lifecycle-3.4.mjs',
'tools/check-release-payload-3.4.mjs',
'tools/verify-production-updater-3.4.mjs',
'docs/PRODUCTION_UPDATER_3_4.md',
]) assert(existsSync(join(root, file)), `Pflichtdatei fehlt: ${file}`);
const lifecycle = read('tools/updater-lifecycle-3.4.mjs');
for (const marker of ['main-and-production-at-target', 'main-at-target-production-behind', 'matching-open-pr', 'matching-pr-already-merged']) {
assert(lifecycle.includes(marker), `Lifecycle-Entscheidung fehlt: ${marker}`);
}
if (existsSync(join(root, 'scripts/updater/Vendoo.Updater.Worker34.ps1'))) {
const worker = read('scripts/updater/Vendoo.Updater.Worker34.ps1');
for (const marker of [
"'3.4.0'", 'production-update-v4', 'payloadHash', 'baseCommit', 'sourceCommit',
'Get-ReleaseObservation', 'Wait-RequiredGitHubChecks', 'headRefOid', 'workflow_runs',
'force-with-lease', 'completedAt', 'finalPrHeadSha',
'check-release-payload-3.4.mjs', '--match-head-commit',
]) assert(worker.includes(marker), `Worker-3.4-Vertrag fehlt: ${marker}`);
assert(!worker.includes("@('pr','checks'"), 'Updater 3.4 darf gh pr checks --watch nicht mehr verwenden.');
assert(!worker.includes("@('push','--force'"), 'Updater 3.4 darf keinen unbedingten Force-Push verwenden.');
}
if (existsSync(join(root, 'scripts/updater/Vendoo.Updater.UI.ps1'))) {
const ui = read('scripts/updater/Vendoo.Updater.UI.ps1');
assert(ui.includes("$UpdaterVersion = '3.4.0'"), 'Updater-UI meldet nicht Version 3.4.0.');
assert(ui.includes("Vendoo.Updater.Worker34.ps1"), 'Updater-UI startet nicht den Worker 3.4.');
}
if (existsSync(join(root, 'scripts/updater/Vendoo.Updater.Host.ps1'))) {
assert(read('scripts/updater/Vendoo.Updater.Host.ps1').includes("$UpdaterVersion = '3.4.0'"), 'Updater-Host meldet nicht Version 3.4.0.');
}
const manifestSync = read('.github/workflows/sync-release-manifest.yml');
assert(manifestSync.includes("'release/*-production-update-v*'"), 'Manifest-Sync ist nicht auf versionsneutrale Releasebranches generalisiert.');
assert(!manifestSync.includes('release/1.43.0-production-update-v3'), 'Manifest-Sync enthält weiterhin den alten festen 3.3-Branch.');
const packageJson = JSON.parse(read('package.json'));
for (const name of ['verify:one-click-update', 'verify:updater-ui', 'verify:updater-hotfix']) {
assert(packageJson.scripts?.[name] === 'node tools/verify-production-updater-3.4.mjs', `${name} verweist nicht auf Updater 3.4.`);
}
assert(packageJson.scripts?.['verify:updater-lifecycle'] === 'node tools/verify-production-updater-3.4.mjs', 'Eigenständiger Lifecycle-Test fehlt.');
const updateManifest = JSON.parse(read('update-manifest.json'));
assert(updateManifest.updaterVersion === UPDATER_VERSION && updateManifest.updater_version === UPDATER_VERSION, 'Update-Manifest meldet nicht 3.4.0.');
const sourceManifest = {
schema: 'vendoo-release-manifest-v1', version: '1.44.0', updaterVersion: UPDATER_VERSION,
mode: 'overlay-with-explicit-removals', requiredFiles: ['package.json'], removedFiles: [],
files: [{ path: 'package.json', sha256: 'a'.repeat(64), size: 1 }],
};
const packageManifest = createPackageManifest(sourceManifest, { sourceCommit: '1'.repeat(40), baseCommit: '2'.repeat(40), createdAt: '2026-07-10T12:00:00Z' });
assert(packageManifest.schema === PACKAGE_MANIFEST_SCHEMA, 'Auslieferungsmanifest verwendet nicht Schema v2.');
assert(/^[a-f0-9]{64}$/.test(packageManifest.payloadHash), 'Auslieferungsmanifest besitzt keinen stabilen Payload-Hash.');
const successChecks = REQUIRED_CHECKS.map(name => ({ name, status: 'COMPLETED', conclusion: 'SUCCESS', completedAt: '2026-07-10T12:00:00Z' }));
const successRuns = REQUIRED_WORKFLOWS.map(name => ({ name, status: 'completed', conclusion: 'success', updated_at: '2026-07-10T12:00:00Z' }));
let result = classifyChecks({ checks: [], workflowRuns: [], elapsedSeconds: 10, gracePeriodSeconds: 90 });
assert(result.state === 'waiting-registration', 'Leere Checks innerhalb der Grace Period müssen warten.');
result = classifyChecks({ checks: successChecks, workflowRuns: successRuns, elapsedSeconds: 120 });
assert(result.state === 'success', 'Vollständig erfolgreiche Checks wurden nicht erkannt.');
for (const status of ['QUEUED', 'PENDING', 'IN_PROGRESS']) {
result = classifyChecks({ checks: [{ name: REQUIRED_CHECKS[0], status }], workflowRuns: [], elapsedSeconds: 120 });
assert(['pending', 'waiting-missing'].includes(result.state), `${status} wurde nicht als laufend behandelt.`);
}
result = classifyChecks({ checks: [...successChecks, { ...successChecks[0], completedAt: '2026-07-10T11:00:00Z', conclusion: 'CANCELLED' }], workflowRuns: successRuns, elapsedSeconds: 120 });
assert(result.state === 'success', 'Paralleler älterer abgebrochener Check darf neueren Erfolg nicht überstimmen.');
result = classifyChecks({ checks: successChecks.map((x, i) => i ? x : ({ ...x, conclusion: 'FAILURE' })), workflowRuns: successRuns, elapsedSeconds: 120 });
assert(result.state === 'failed', 'Fehlgeschlagener Pflichtcheck wurde nicht terminal erkannt.');
assert(formatStatusMatrix(result.rows).includes(REQUIRED_CHECKS[0]), 'Timeout-/Fehlermatrix enthält Checknamen nicht.');
const runBase = successRuns.filter(run => run.name !== REQUIRED_WORKFLOWS[0]);
result = classifyChecks({
checks: successChecks,
workflowRuns: [
...runBase,
{ name: REQUIRED_WORKFLOWS[0], status: 'completed', conclusion: 'success', updated_at: '2026-07-10T11:00:00Z' },
{ name: REQUIRED_WORKFLOWS[0], status: 'completed', conclusion: 'failure', updated_at: '2026-07-10T12:00:00Z' },
],
elapsedSeconds: 120,
});
assert(result.state === 'failed', 'Ein älterer erfolgreicher Workflow-Run darf einen neueren Fehler nicht verdecken.');
result = classifyChecks({
checks: successChecks,
workflowRuns: [
...runBase,
{ name: REQUIRED_WORKFLOWS[0], status: 'completed', conclusion: 'failure', updated_at: '2026-07-10T11:00:00Z' },
{ name: REQUIRED_WORKFLOWS[0], status: 'in_progress', conclusion: null, updated_at: '2026-07-10T12:00:00Z' },
],
elapsedSeconds: 120,
});
assert(result.state === 'pending', 'Ein neuer laufender Workflow-Run muss einen älteren Fehler ablösen.');
result = classifyChecks({
checks: successChecks,
workflowRuns: [
...runBase,
{ name: REQUIRED_WORKFLOWS[0], status: 'completed', conclusion: 'cancelled', updated_at: '2026-07-10T11:00:00Z' },
{ name: REQUIRED_WORKFLOWS[0], status: 'completed', conclusion: 'success', updated_at: '2026-07-10T12:00:00Z' },
],
elapsedSeconds: 120,
});
assert(result.state === 'success', 'Ein neuer erfolgreicher Workflow-Run muss einen älteren Abbruch ablösen.');
let decision = decideReleasePath({ targetVersion: '1.43.0', mainVersion: '1.43.0', productionReady: true, productionVersion: '1.43.0' });
assert(decision.action === 'complete', 'Bereits deployte Zielversion muss sofort erfolgreich enden.');
decision = decideReleasePath({ targetVersion: '1.43.0', mainVersion: '1.43.0', productionReady: true, productionVersion: '1.42.0' });
assert(decision.action === 'deploy', 'main auf Zielversion darf keinen Quell-PR erzeugen.');
decision = decideReleasePath({ targetVersion: '1.44.0', mainVersion: '1.43.0', productionReady: true, productionVersion: '1.43.0', pullRequests: [{ targetVersion: '1.44.0', state: 'OPEN', number: 30, updatedAt: '2026-07-10T12:00:00Z' }] });
assert(decision.action === 'resume-pr', 'Vorhandener offener PR muss übernommen werden.');
decision = decideReleasePath({ targetVersion: '1.44.0', mainVersion: '1.43.0', productionReady: true, productionVersion: '1.43.0', pullRequests: [{ targetVersion: '1.44.0', state: 'CLOSED', mergedAt: '2026-07-10T12:00:00Z' }] });
assert(decision.action === 'deploy', 'Bereits gemergter PR muss die Sourcephase überspringen.');
assert(evaluatePayloadGuard({ baseCommit: 'a', currentMainCommit: 'b', baseIsAncestor: true, conflictingPaths: ['x'] }).allowed === false, 'Veralteter Payload mit Konflikt wurde nicht blockiert.');
assert(evaluatePayloadGuard({ baseCommit: 'a', currentMainCommit: 'b', baseIsAncestor: true, conflictingPaths: [] }).allowed === true, 'Nicht kollidierender Main-Fortschritt muss zulässig sein.');
const temp = mkdtempSync(join(tmpdir(), 'vendoo-updater34-'));
try {
const repo = join(temp, 'repo');
const pkg = join(temp, 'pkg');
mkdirSync(repo); mkdirSync(pkg);
const run = (args, cwd = repo) => {
const r = spawnSync('git', args, { cwd, encoding: 'utf8' });
if (r.status !== 0) throw new Error(r.stderr || r.stdout);
return r.stdout.trim();
};
run(['init']); run(['config', 'user.email', 'ci@example.invalid']); run(['config', 'user.name', 'CI']);
writeFileSync(join(repo, 'a.txt'), 'base\n'); writeFileSync(join(repo, 'b.txt'), 'base\n');
run(['add', '.']); run(['commit', '-m', 'base']); const base = run(['rev-parse', 'HEAD']);
writeFileSync(join(repo, 'a.txt'), 'main-new\n'); run(['add', '.']); run(['commit', '-m', 'main']); const main = run(['rev-parse', 'HEAD']);
const content = Buffer.from('payload-old\n');
writeFileSync(join(pkg, 'a.txt'), content);
writeFileSync(join(pkg, 'release-manifest.json'), JSON.stringify({ schema: PACKAGE_MANIFEST_SCHEMA, files: [{ path: 'a.txt', sha256: createHash('sha256').update(content).digest('hex'), size: content.length }], removedFiles: [] }));
const guard = inspectPayload({ packageRoot: pkg, repositoryRoot: repo, baseCommit: base, currentMainCommit: main });
assert(!guard.allowed && guard.conflicts.includes('a.txt'), 'Realer Git-Payload-Rückschritt wurde nicht blockiert.');
} catch (error) {
failures.push(`Payload-Regressionstest fehlgeschlagen: ${error.message}`);
} finally {
rmSync(temp, { recursive: true, force: true });
}
assert(SESSION_SCHEMA === 'vendoo-updater-session-v2', 'Session-Schema ist nicht v2.');
if (failures.length) {
console.error(`Production-Updater-3.4-Gate fehlgeschlagen (${failures.length})`);
failures.forEach(failure => console.error(`- ${failure}`));
process.exit(1);
}
console.log('Production-Updater-3.4-Gate erfolgreich: Zustandsautomat, Grace Period, finaler Head-SHA, Idempotenz, parallele Runs und Payload-Rückschrittschutz geprüft.');
+26 -9
View File
@@ -10,6 +10,7 @@ import {
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { applyReleaseManifest, buildReleaseManifest, MANIFEST_FILE, readAndValidateManifest } from './release-manifest-lib.mjs';
import { createPackageManifest, PACKAGE_MANIFEST_SCHEMA } from './updater-lifecycle-3.4.mjs';
const temp = mkdtempSync(join(tmpdir(), 'vendoo-release-manifest-'));
const source = join(temp, 'source');
@@ -21,13 +22,15 @@ const requiredBodies = new Map([
['server.mjs', 'export const server = true;\n'],
['lib/db.mjs', 'export const db = true;\n'],
['db/schema.sql', 'CREATE TABLE test(id INTEGER PRIMARY KEY);\n'],
['update-manifest.json', '{"version":"1.43.0","updaterVersion":"3.3.0"}\n'],
['update-manifest.json', '{"version":"1.43.0","updaterVersion":"3.4.0"}\n'],
['UPDATE_VENDOO.bat', '@echo off\r\n'],
['scripts/updater/Vendoo.Updater.Preflight.ps1', 'Write-Output preflight\n'],
['scripts/updater/Vendoo.Updater.UI.ps1', 'Write-Output ui\n'],
['scripts/updater/Vendoo.Updater.Host.ps1', 'Write-Output host\n'],
['scripts/updater/Vendoo.Updater.Worker.ps1', 'Write-Output worker\n'],
['scripts/updater/Vendoo.Updater.Worker34.ps1', 'Write-Output worker34\n'],
['tools/apply-release-manifest.mjs', 'export {};\n'],
['tools/updater-lifecycle-3.4.mjs', 'export const lifecycle = true;\n'],
['tools/check-release-payload-3.4.mjs', 'export const payloadGuard = true;\n'],
]);
function write(root, rel, content) {
@@ -36,10 +39,15 @@ function write(root, rel, content) {
writeFileSync(target, content, 'utf8');
}
function writeManifest(root, removedFiles = []) {
const manifest = buildReleaseManifest(root, { version: '1.43.0', updaterVersion: '3.3.0', removedFiles });
writeFileSync(join(root, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
return manifest;
function writePackageManifest(root, removedFiles = []) {
const sourceManifest = buildReleaseManifest(root, { version: '1.43.0', updaterVersion: '3.4.0', removedFiles });
const packageManifest = createPackageManifest(sourceManifest, {
sourceCommit: '1'.repeat(40),
baseCommit: '2'.repeat(40),
createdAt: '2026-07-10T12:00:00Z',
});
writeFileSync(join(root, MANIFEST_FILE), `${JSON.stringify(packageManifest, null, 2)}\n`, 'utf8');
return packageManifest;
}
try {
@@ -50,7 +58,8 @@ try {
write(destination, 'db/schema.sql', 'OLD SCHEMA');
write(destination, 'baseline-only.txt', 'must survive overlay');
write(destination, 'obsolete.txt', 'must be removed explicitly');
const manifest = writeManifest(source, ['obsolete.txt']);
const manifest = writePackageManifest(source, ['obsolete.txt']);
if (manifest.schema !== PACKAGE_MANIFEST_SCHEMA) throw new Error('Paketmanifest verwendet nicht Schema v2.');
if (manifest.files.some(entry => entry.path === 'logs/secret.log')) throw new Error('Runtime-Log wurde in das Release-Manifest aufgenommen.');
const result = applyReleaseManifest(source, destination);
if (result.copied !== manifest.files.length || result.controlCopied !== 1 || result.removed !== 1) throw new Error('Release-Zähler sind falsch.');
@@ -61,10 +70,18 @@ try {
readAndValidateManifest(destination, { allowExtraFiles: true });
let controlDeletionBlocked = false;
try { buildReleaseManifest(source, { version: '1.43.0', updaterVersion: '3.3.0', removedFiles: [MANIFEST_FILE] }); }
try { buildReleaseManifest(source, { version: '1.43.0', updaterVersion: '3.4.0', removedFiles: [MANIFEST_FILE] }); }
catch (error) { controlDeletionBlocked = /Steuerdatei/.test(error.message); }
if (!controlDeletionBlocked) throw new Error('Löschung von release-manifest.json wurde nicht blockiert.');
const tamperedManifest = JSON.parse(readFileSync(join(source, MANIFEST_FILE), 'utf8'));
tamperedManifest.baseCommit = '3'.repeat(40);
writeFileSync(join(source, MANIFEST_FILE), `${JSON.stringify(tamperedManifest, null, 2)}\n`, 'utf8');
let provenanceBlocked = false;
try { readAndValidateManifest(source); } catch (error) { provenanceBlocked = /Payload-Hash/.test(error.message); }
if (!provenanceBlocked) throw new Error('Manipulierte Paket-Provenienz wurde nicht blockiert.');
writeFileSync(join(source, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
const corrupt = join(temp, 'corrupt');
mkdirSync(corrupt, { recursive: true });
for (const entry of manifest.files) {
@@ -91,7 +108,7 @@ try {
try { applyReleaseManifest(missing, destination); } catch (error) { missingBlocked = /fehlende Paketdatei|Pflichtdatei|verweist auf fehlende/.test(error.message); }
if (!missingBlocked) throw new Error('Fehlende Pflichtdatei db/schema.sql wurde nicht blockiert.');
console.log(`Release-Manifest-Gate 3.3 erfolgreich: ${manifest.files.length} Dateien hashgeprüft, Overlay bewahrt Baseline, Löschungen sind explizit, Manipulation und fehlendes db/schema.sql werden blockiert.`);
console.log(`Release-Manifest-Gate 3.4 erfolgreich: ${manifest.files.length} Dateien hashgeprüft, Paket-Provenienz validiert, Overlay bewahrt Baseline, Löschungen sind explizit, Manipulation und fehlendes db/schema.sql werden blockiert.`);
} finally {
rmSync(temp, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
}
+51
View File
@@ -0,0 +1,51 @@
import { readFileSync } from 'node:fs';
import { resolve, join } from 'node:path';
import { REQUIRED_CHECKS, REQUIRED_WORKFLOWS } from './updater-lifecycle-3.4.mjs';
const root = resolve(import.meta.dirname, '..');
const read = path => readFileSync(join(root, path), 'utf8');
const failures = [];
const assert = (condition, message) => { if (!condition) failures.push(message); };
const ci = read('.github/workflows/ci.yml');
const sync = read('.github/workflows/sync-release-manifest.yml');
assert(ci.includes('workflow_dispatch:'), 'Vendoo CI kann nicht explizit auf dem finalen Head gestartet werden.');
assert(ci.includes('name: Updater-3.4-Paket nach Abnahme'), 'Nachgelagerter Paket-Pflichtcheck fehlt.');
assert(ci.includes('needs: [quality, windows-release-contract]'), 'Das Paket ist nicht strikt von Linux- und Windows-Abnahme abhängig.');
assert(ci.includes('git diff --exit-code -- release-manifest.json'), 'Paketjob bestätigt kein reproduzierbares Root-Manifest.');
assert(ci.includes('vendoo-release-package-v2'), 'Paketjob validiert das Provenienzschema v2 nicht.');
assert(ci.includes('actions/upload-artifact@v4'), 'Das geprüfte Paket wird nicht als CI-Artefakt bereitgestellt.');
assert(ci.includes('powershell-parser-diagnostics'), 'Windows-Parserfehler werden nicht als Diagnoseartefakt gesichert.');
assert(ci.includes('tools/build-deterministic-zip.py'), 'Paketjob verwendet keinen deterministischen ZIP-Build.');
assert(ci.includes('first_hash') && ci.includes('second_hash'), 'Paketjob weist die Byte-Reproduzierbarkeit des ZIPs nicht nach.');
assert(!ci.includes('zip -qr'), 'Paketjob verwendet weiterhin den zeitstempelabhängigen Standard-ZIP-Build.');
const deterministicZip = read('tools/build-deterministic-zip.py');
assert(deterministicZip.includes('FIXED_TIMESTAMP = (1980, 1, 1, 0, 0, 0)'), 'Deterministischer ZIP-Build besitzt keinen fixierten Zeitstempel.');
assert(deterministicZip.includes('sorted(source.rglob'), 'Deterministischer ZIP-Build sortiert die Payload nicht.');
assert(sync.includes("'release/*-production-update-v*'"), 'Manifest-Sync ist nicht versionsneutral auf Releasebranches begrenzt.');
assert(sync.includes('workflow_dispatch:'), 'Manifest-Sync kann nicht auf dem finalen Head registriert werden.');
assert(sync.includes('actions: write'), 'Manifest-Sync darf keine Final-Head-Workflows dispatchen.');
assert(sync.includes('gh workflow run ci.yml'), 'Manifest-Sync dispatcht Vendoo CI nicht auf den finalen Head.');
assert(sync.includes('gh workflow run sync-release-manifest.yml'), 'Manifest-Sync registriert keinen eigenen Lauf auf dem finalen Head.');
assert(sync.includes('git ls-remote origin'), 'Manifest-Sync bestätigt den gepushten Remote-Head nicht.');
assert(sync.includes('cancel-in-progress: false'), 'Manifest-Sync könnte seinen eigenen Final-Head-Dispatch abbrechen.');
assert(!sync.includes('release/1.43.0-production-update-v3'), 'Manifest-Sync enthält weiterhin den alten festen 3.3-Branch.');
for (const name of [
'Syntax, Sicherheit und Release-Gates',
'Windows vollständiger Releasevertrag',
'Updater-3.4-Paket nach Abnahme',
]) assert(REQUIRED_CHECKS.includes(name), `Lifecycle-Pflichtcheck fehlt: ${name}`);
for (const name of ['Vendoo CI', 'Vendoo Release Manifest Sync']) {
assert(REQUIRED_WORKFLOWS.includes(name), `Lifecycle-Pflichtworkflow fehlt: ${name}`);
}
if (failures.length) {
console.error(`Updater-3.4-CI-Lifecycle-Gate fehlgeschlagen (${failures.length})`);
failures.forEach(failure => console.error(`- ${failure}`));
process.exit(1);
}
console.log('Updater-3.4-CI-Lifecycle-Gate erfolgreich: finaler Head, Windows-Diagnose und reproduzierbares Paket sind verbindlich abgesichert.');
+60
View File
@@ -0,0 +1,60 @@
import { existsSync, readFileSync } from 'node:fs';
import { join, resolve } from 'node:path';
const root = resolve(import.meta.dirname, '..');
const failures = [];
const assert = (condition, message) => { if (!condition) failures.push(message); };
const read = path => readFileSync(join(root, path), 'utf8');
const files = [
'UPDATE_VENDOO.bat',
'scripts/updater/Vendoo.Updater.Preflight.ps1',
'scripts/update-vendoo-gui.ps1',
'scripts/updater/Vendoo.Updater.UI.ps1',
'scripts/updater/Vendoo.Updater.Host.ps1',
'scripts/updater/Vendoo.Updater.Worker34.ps1',
];
for (const file of files) assert(existsSync(join(root, file)), `Updater-Startdatei fehlt: ${file}`);
if (existsSync(join(root, 'UPDATE_VENDOO.bat'))) {
const starter = read('UPDATE_VENDOO.bat');
assert(starter.includes('Production Updater 3.4 preflight'), 'Root-Starter meldet nicht Updater 3.4.');
assert(starter.includes('Vendoo.Updater.Preflight.ps1'), 'Root-Starter startet den Preflight nicht.');
assert(!starter.includes('Production Updater 3.3'), 'Root-Starter enthält noch die alte 3.3-Bezeichnung.');
}
if (existsSync(join(root, 'scripts/updater/Vendoo.Updater.Preflight.ps1'))) {
const preflight = read('scripts/updater/Vendoo.Updater.Preflight.ps1');
assert(preflight.includes("$UpdaterVersion = '3.4.0'"), 'Preflight meldet nicht Version 3.4.0.');
assert(preflight.includes('Vendoo.Updater.Worker34.ps1'), 'Preflight prüft Worker34 nicht.');
assert(!preflight.includes("Vendoo.Updater.Worker.ps1'"), 'Preflight verlangt weiterhin den entfernten Worker 3.3.');
}
if (existsSync(join(root, 'scripts/update-vendoo-gui.ps1'))) {
assert(read('scripts/update-vendoo-gui.ps1').includes('Vendoo.Updater.UI.ps1'), 'GUI-Bootstrap startet die Updater-UI nicht.');
}
if (existsSync(join(root, 'scripts/updater/Vendoo.Updater.UI.ps1'))) {
const ui = read('scripts/updater/Vendoo.Updater.UI.ps1');
assert(ui.includes("$UpdaterVersion = '3.4.0'"), 'UI meldet nicht Version 3.4.0.');
assert(ui.includes('Vendoo.Updater.Host.ps1'), 'UI startet den Host nicht.');
assert(ui.includes('Vendoo.Updater.Worker34.ps1'), 'UI übergibt Worker34 nicht an den Host.');
assert(!ui.includes("Vendoo.Updater.Worker.ps1'"), 'UI verweist weiterhin auf den entfernten Worker 3.3.');
}
if (existsSync(join(root, 'scripts/updater/Vendoo.Updater.Host.ps1'))) {
const host = read('scripts/updater/Vendoo.Updater.Host.ps1');
assert(host.includes("$UpdaterVersion = '3.4.0'"), 'Host meldet nicht Version 3.4.0.');
assert(host.includes('[System.Management.Automation.Language.Parser]::ParseFile'), 'Host parst den Worker nicht vor der Ausführung.');
}
if (existsSync(join(root, 'scripts/updater/Vendoo.Updater.Worker34.ps1'))) {
assert(read('scripts/updater/Vendoo.Updater.Worker34.ps1').includes("$UpdaterVersion='3.4.0'"), 'Worker34 meldet nicht Version 3.4.0.');
}
if (failures.length) {
console.error(`Updater-3.4-Startketten-Gate fehlgeschlagen (${failures.length})`);
failures.forEach(failure => console.error(`- ${failure}`));
process.exit(1);
}
console.log('Updater-3.4-Startketten-Gate erfolgreich: Root-Starter, Preflight, UI, Host und Worker34 sind konsistent verdrahtet.');
+7 -5
View File
@@ -1,13 +1,15 @@
{
"schema": "vendoo-update-manifest-v1",
"version": "1.43.0",
"name": "Vendoo Manifest-Based Production Update 3.3",
"name": "Vendoo Manifest-Based Production Update 3.4",
"channel": "stable",
"minimum_node": "22",
"notes": "Production Updater 3.3 trennt Windows-Git-Bash-Portabilität von der strikten POSIX-Rechteprüfung und liefert vollständige Shell-Diagnosen.",
"notes": "Production Updater 3.4 verwendet einen idempotenten Release-Lifecycle, verfolgt GitHub-Checks am finalen PR-Head-SHA und blockiert veraltete Payloads vor jeder Quelländerung.",
"download_url": "",
"sha256": "",
"updater_version": "3.3.0",
"updaterVersion": "3.3.0",
"removedFiles": []
"updater_version": "3.4.0",
"updaterVersion": "3.4.0",
"removedFiles": [
"scripts/updater/Vendoo.Updater.Worker.ps1"
]
}