Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94d898e445 | ||
|
|
0a3d0b86e5 | ||
|
|
20f2e9a299 | ||
|
|
637275effc |
@@ -5,6 +5,7 @@ on:
|
|||||||
branches: [main, develop, 'feature/**', 'release/**', 'hotfix/**']
|
branches: [main, develop, 'feature/**', 'release/**', 'hotfix/**']
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main, develop]
|
branches: [main, develop]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -42,16 +43,21 @@ jobs:
|
|||||||
- name: Git-Staging-Filter Regressionstest
|
- name: Git-Staging-Filter Regressionstest
|
||||||
run: npm run verify:git-staging
|
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
|
- name: Git-Ignore-Grenzen für Quell- und Runtimeordner prüfen
|
||||||
run: npm run verify:ignore-boundaries
|
run: npm run verify:ignore-boundaries
|
||||||
|
|
||||||
- name: Syntaxprüfung
|
- name: Syntaxprüfung
|
||||||
run: npm run check
|
run: npm run check
|
||||||
|
|
||||||
|
|
||||||
- name: PowerShell-Regressionen statisch prüfen
|
- name: PowerShell-Regressionen statisch prüfen
|
||||||
run: npm run verify:powershell-static
|
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
|
- name: Produktiven Source-Root und Archivgrenzen prüfen
|
||||||
run: npm run verify:source-boundaries
|
run: npm run verify:source-boundaries
|
||||||
|
|
||||||
@@ -140,9 +146,93 @@ jobs:
|
|||||||
|
|
||||||
- name: Alle PowerShell-Dateien mit Windows PowerShell 5.1 parsen
|
- name: Alle PowerShell-Dateien mit Windows PowerShell 5.1 parsen
|
||||||
shell: powershell
|
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
|
- name: Vollständigen Vendoo-Releasevertrag unter Windows ausführen
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: npm run verify:all
|
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
|
||||||
|
|||||||
@@ -3,19 +3,20 @@ name: Vendoo Release Manifest Sync
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- release/1.43.0-production-update-v3
|
- 'release/*-production-update-v*'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
actions: write
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: vendoo-release-manifest-sync-${{ github.ref }}
|
group: vendoo-release-manifest-sync-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
sync:
|
sync:
|
||||||
name: Release-Manifest reproduzierbar synchronisieren
|
name: Release-Manifest reproduzierbar synchronisieren
|
||||||
if: ${{ !contains(github.event.head_commit.message, '[manifest-sync]') }}
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
@@ -34,10 +35,14 @@ jobs:
|
|||||||
run: node tools/generate-release-manifest.mjs .
|
run: node tools/generate-release-manifest.mjs .
|
||||||
|
|
||||||
- name: Geändertes Manifest committen
|
- name: Geändertes Manifest committen
|
||||||
|
id: manifest
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
if git diff --quiet -- release-manifest.json; then
|
if git diff --quiet -- release-manifest.json; then
|
||||||
echo "Release-Manifest ist bereits aktuell."
|
echo "Release-Manifest ist bereits aktuell."
|
||||||
|
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "head_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
git config user.name "vendoo-release-bot"
|
git config user.name "vendoo-release-bot"
|
||||||
@@ -45,3 +50,22 @@ jobs:
|
|||||||
git add release-manifest.json
|
git add release-manifest.json
|
||||||
git commit -m "chore: Release-Manifest synchronisieren [manifest-sync]"
|
git commit -m "chore: Release-Manifest synchronisieren [manifest-sync]"
|
||||||
git push origin HEAD:${GITHUB_REF_NAME}
|
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
@@ -7,7 +7,7 @@ if errorlevel 1 goto :path_error
|
|||||||
set "VENDOO_PS=%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe"
|
set "VENDOO_PS=%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe"
|
||||||
if not exist "%VENDOO_PS%" goto :powershell_missing
|
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"
|
"%VENDOO_PS%" -NoLogo -NoProfile -ExecutionPolicy Bypass -File ".\scripts\updater\Vendoo.Updater.Preflight.ps1"
|
||||||
set "VENDOO_RC=%ERRORLEVEL%"
|
set "VENDOO_RC=%ERRORLEVEL%"
|
||||||
if not "%VENDOO_RC%"=="0" goto :preflight_failed
|
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.
|
||||||
@@ -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.
|
||||||
Generated
+754
-604
File diff suppressed because it is too large
Load Diff
+13
-10
@@ -22,7 +22,7 @@
|
|||||||
"verify:git-staging": "node tools/verify-git-staging.mjs",
|
"verify:git-staging": "node tools/verify-git-staging.mjs",
|
||||||
"generate:tokens": "node tools/generate-design-tokens.mjs",
|
"generate:tokens": "node tools/generate-design-tokens.mjs",
|
||||||
"verify:architecture": "node tools/verify-platform-architecture-1.41.2.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:themes": "node tools/verify-theme-manager-1.41.2.mjs",
|
||||||
"verify:security": "node tools/verify-security-foundation-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",
|
"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: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: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: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: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-ui": "node tools/verify-production-updater-3.4.mjs",
|
||||||
"verify:updater-hotfix": "node tools/verify-production-updater-3.3.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: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: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"
|
"verify:release-manifest": "node tools/verify-release-manifest-staging-3.0.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.39.0",
|
"@anthropic-ai/sdk": "^0.111.0",
|
||||||
"better-sqlite3": "^12.11.1",
|
"better-sqlite3": "^12.11.1",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^17.4.2",
|
||||||
"express": "^4.21.2",
|
"express": "^5.2.1",
|
||||||
"helmet": "^8.2.0",
|
"helmet": "^8.3.0",
|
||||||
"multer": "^2.2.0",
|
"multer": "^2.2.0",
|
||||||
"nodemailer": "^9.0.3",
|
"nodemailer": "^9.0.3",
|
||||||
"openai": "^4.77.0",
|
"openai": "^6.46.0",
|
||||||
"sharp": "^0.33.5"
|
"sharp": "^0.35.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+74
-30
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"schema": "vendoo-release-manifest-v1",
|
"schema": "vendoo-release-manifest-v1",
|
||||||
"version": "1.43.0",
|
"version": "1.43.0",
|
||||||
"updaterVersion": "3.3.0",
|
"updaterVersion": "3.4.0",
|
||||||
"mode": "overlay-with-explicit-removals",
|
"mode": "overlay-with-explicit-removals",
|
||||||
"requiredFiles": [
|
"requiredFiles": [
|
||||||
"package.json",
|
"package.json",
|
||||||
@@ -15,10 +15,14 @@
|
|||||||
"scripts/updater/Vendoo.Updater.Preflight.ps1",
|
"scripts/updater/Vendoo.Updater.Preflight.ps1",
|
||||||
"scripts/updater/Vendoo.Updater.UI.ps1",
|
"scripts/updater/Vendoo.Updater.UI.ps1",
|
||||||
"scripts/updater/Vendoo.Updater.Host.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/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": [
|
"files": [
|
||||||
{
|
{
|
||||||
"path": ".dockerignore",
|
"path": ".dockerignore",
|
||||||
@@ -57,8 +61,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": ".github/workflows/ci.yml",
|
"path": ".github/workflows/ci.yml",
|
||||||
"sha256": "384990816ba3295cc6458d7d808422cc25cfc2e9cf9598b9052f7aadc7f70b6b",
|
"sha256": "c0e80dbafb1d1491b851eb002dc323e52ea89e7e0456dde034f3303a8f4751c2",
|
||||||
"size": 4233
|
"size": 8173
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": ".github/workflows/release.yml",
|
"path": ".github/workflows/release.yml",
|
||||||
@@ -67,8 +71,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": ".github/workflows/sync-release-manifest.yml",
|
"path": ".github/workflows/sync-release-manifest.yml",
|
||||||
"sha256": "f25e9fe77a8d3c419b37b22f226f9d2e2ed69820894288be5b4bb09ac10bab1a",
|
"sha256": "9067f929284d7a524013e41658a91d6da6993a931efce84530ecc05363fb0ca2",
|
||||||
"size": 1342
|
"size": 2495
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": ".github/workflows/vendoo-production-deploy.yml",
|
"path": ".github/workflows/vendoo-production-deploy.yml",
|
||||||
@@ -690,6 +694,11 @@
|
|||||||
"sha256": "581e0d192e08a00d8f19b325320fecc3634c487fd3a75135852546fd6fc2cccc",
|
"sha256": "581e0d192e08a00d8f19b325320fecc3634c487fd3a75135852546fd6fc2cccc",
|
||||||
"size": 584
|
"size": 584
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "docs/ABNAHMEBERICHT_PRODUCTION_UPDATER_3_4.md",
|
||||||
|
"sha256": "dc6ae417a4ec45ac4d65ac4f9d5f1eae45d1af2e4988fd124547113b265ff493",
|
||||||
|
"size": 5724
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "docs/architecture/CATALOG_MEDIA_MODULES_1_39_0.md",
|
"path": "docs/architecture/CATALOG_MEDIA_MODULES_1_39_0.md",
|
||||||
"sha256": "b39d2d892068e1313fc00c9345d54240b6d50caf8a940261a088e67d8778ac63",
|
"sha256": "b39d2d892068e1313fc00c9345d54240b6d50caf8a940261a088e67d8778ac63",
|
||||||
@@ -900,6 +909,11 @@
|
|||||||
"sha256": "9c8957ea1593b55e9fe6786fa87fbb156874c7be3e3a54e0987bd94d18fb6bf0",
|
"sha256": "9c8957ea1593b55e9fe6786fa87fbb156874c7be3e3a54e0987bd94d18fb6bf0",
|
||||||
"size": 970
|
"size": 970
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "docs/PRODUCTION_UPDATER_3_4.md",
|
||||||
|
"sha256": "e6264636042b53ac1728614d0341f14c26264f3b73f52ffcba3fbd4f6f8fe179",
|
||||||
|
"size": 8326
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "docs/PUBLISH_CENTER_2026.md",
|
"path": "docs/PUBLISH_CENTER_2026.md",
|
||||||
"sha256": "4f74fef6877126d2579356b9731955e29f05ea8b55b59abb91569e47730a1c3f",
|
"sha256": "4f74fef6877126d2579356b9731955e29f05ea8b55b59abb91569e47730a1c3f",
|
||||||
@@ -2027,8 +2041,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "package.json",
|
"path": "package.json",
|
||||||
"sha256": "cbc637abe9c70f94b51daeeca2eba33be31a55327ee1b5da9e5cf12552aed666",
|
"sha256": "cd21451f91312c33bc9bd5b808103c3428854587cc8e9b5ba3b2908085043344",
|
||||||
"size": 4434
|
"size": 4759
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "platforms/ebay-de.mjs",
|
"path": "platforms/ebay-de.mjs",
|
||||||
@@ -2407,23 +2421,23 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "scripts/updater/Vendoo.Updater.Host.ps1",
|
"path": "scripts/updater/Vendoo.Updater.Host.ps1",
|
||||||
"sha256": "01f330e1bcd05ddf48685b9382057be7aa8e9329538d3c6e5044bb17129c53c4",
|
"sha256": "f02c56bd0dc13eca7830f8cbd7de94a6cba8331504176694878834f43e6b3495",
|
||||||
"size": 5807
|
"size": 5828
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "scripts/updater/Vendoo.Updater.Preflight.ps1",
|
"path": "scripts/updater/Vendoo.Updater.Preflight.ps1",
|
||||||
"sha256": "180adf209ea2c4f7d7a64594e231ecd5508dd44993cce15c0d871aa0a1b6e83a",
|
"sha256": "ef91ccf925e7a8a1c8f27ddf2d1d7b997249798a63334f8fe1b5d27626eb525e",
|
||||||
"size": 3605
|
"size": 3607
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "scripts/updater/Vendoo.Updater.UI.ps1",
|
"path": "scripts/updater/Vendoo.Updater.UI.ps1",
|
||||||
"sha256": "ddf685413aa052bbc7b150bd396ff796ba62b30d9db30fa25f2c47fa50c85ab1",
|
"sha256": "b499570054f409310cbb8c4f89af7ce7e26b7f226b630a1f644aadbd2cee0fe4",
|
||||||
"size": 28122
|
"size": 28166
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "scripts/updater/Vendoo.Updater.Worker.ps1",
|
"path": "scripts/updater/Vendoo.Updater.Worker34.ps1",
|
||||||
"sha256": "5b7ea5829d0f7499e418d546d0a05ef63557dd5a9a2b4ba60ac9f0e2c9617a51",
|
"sha256": "0cae461ee0c1499314a9ad1eab2650f9c0a2d3e22d03b712d5a6164e93acc03c",
|
||||||
"size": 30578
|
"size": 35852
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "SECURITY.md",
|
"path": "SECURITY.md",
|
||||||
@@ -2465,10 +2479,20 @@
|
|||||||
"sha256": "3ddece908ffdfb15c94852c0fcc554ce7de39725650b4d139d0afc2f59297a55",
|
"sha256": "3ddece908ffdfb15c94852c0fcc554ce7de39725650b4d139d0afc2f59297a55",
|
||||||
"size": 889
|
"size": 889
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "tools/build-deterministic-zip.py",
|
||||||
|
"sha256": "844ceacf2d3d90c5d948233b2ba1a51ae9f913c1b13a07eacc77888ebc2b7877",
|
||||||
|
"size": 2101
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "tools/build-release.mjs",
|
"path": "tools/build-release.mjs",
|
||||||
"sha256": "fd35ee48f4c0ae1d14f7ffe9e9b27a5802305b6dfa7726f3df79e03a0e57335a",
|
"sha256": "d00a8998e093d24ea3ff1cea88d9d00ed14a17d61ceb80517dc9b849b10a1190",
|
||||||
"size": 2368
|
"size": 3585
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "tools/check-release-payload-3.4.mjs",
|
||||||
|
"sha256": "f4fad67726a0926867652c5e5e0071d9191c51a4555c178ccec1a817b7edcd26",
|
||||||
|
"size": 4038
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "tools/check-syntax.mjs",
|
"path": "tools/check-syntax.mjs",
|
||||||
@@ -2492,14 +2516,19 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "tools/release-manifest-lib.mjs",
|
"path": "tools/release-manifest-lib.mjs",
|
||||||
"sha256": "45dc394a9ef255c3acde855213e23ed727c280c23c1bdf41156048268f603368",
|
"sha256": "9310d33f35823692e307dcfdc87f43218fd9b3c3a1c696d9839935d1641acc57",
|
||||||
"size": 10320
|
"size": 11400
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "tools/test-runtime-cleanup.mjs",
|
"path": "tools/test-runtime-cleanup.mjs",
|
||||||
"sha256": "5bb59f8283122f6db66e94883f04246ceb108399f97f6e961831bfe5b6b10025",
|
"sha256": "5bb59f8283122f6db66e94883f04246ceb108399f97f6e961831bfe5b6b10025",
|
||||||
"size": 1002
|
"size": 1002
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "tools/updater-lifecycle-3.4.mjs",
|
||||||
|
"sha256": "d5320924be6e18dfd656fdba717789e15e6aedbd8c5afbeaffc21e141272edae",
|
||||||
|
"size": 9982
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "tools/verify-capability-graph-1.40.2.mjs",
|
"path": "tools/verify-capability-graph-1.40.2.mjs",
|
||||||
"sha256": "e3d8c6e8dfd566acbe037ad744edbf60816152d4994698c2a9c46d2b890e246a",
|
"sha256": "e3d8c6e8dfd566acbe037ad744edbf60816152d4994698c2a9c46d2b890e246a",
|
||||||
@@ -2917,8 +2946,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "tools/verify-powershell-parser.ps1",
|
"path": "tools/verify-powershell-parser.ps1",
|
||||||
"sha256": "140cb2adc774612c43c8e8962b7ecdbe70ca0e7def42ac0dd71b86695697e570",
|
"sha256": "205b0327719ffc39a97092d5e08336262d257a8273389458fde68343bd22b6b7",
|
||||||
"size": 1295
|
"size": 1748
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "tools/verify-powershell-static.mjs",
|
"path": "tools/verify-powershell-static.mjs",
|
||||||
@@ -2990,6 +3019,11 @@
|
|||||||
"sha256": "2c276876f6fde314a9a7cbc7f93d14cc6a378dce4eebaec7dc1af5b0e395a779",
|
"sha256": "2c276876f6fde314a9a7cbc7f93d14cc6a378dce4eebaec7dc1af5b0e395a779",
|
||||||
"size": 8753
|
"size": 8753
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "tools/verify-production-updater-3.4.mjs",
|
||||||
|
"sha256": "46f1b5a658f6b406f427686ca0961ee18bb18b18d0ca3e219bfbedcc9b1522f0",
|
||||||
|
"size": 11063
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "tools/verify-release-1.24.0.mjs",
|
"path": "tools/verify-release-1.24.0.mjs",
|
||||||
"sha256": "551ec082e47dbb54ff8c942985face7dead33b684c9e50218ba31fe20ef7d8d4",
|
"sha256": "551ec082e47dbb54ff8c942985face7dead33b684c9e50218ba31fe20ef7d8d4",
|
||||||
@@ -3047,8 +3081,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "tools/verify-release-manifest-staging-3.0.mjs",
|
"path": "tools/verify-release-manifest-staging-3.0.mjs",
|
||||||
"sha256": "36111e35cb4b4a75376a028b2407678bbce0c40585c606965f1680d4aa46afca",
|
"sha256": "f5fa6b4332cde9e461f3e986a61dab80dfb1aafeb76599fa1f10add93ff5331d",
|
||||||
"size": 5356
|
"size": 6550
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "tools/verify-security-1.31.0.py",
|
"path": "tools/verify-security-1.31.0.py",
|
||||||
@@ -3170,11 +3204,21 @@
|
|||||||
"sha256": "0f3f684019763a14f5fbaec42e42c680cf748d930d46cd607fc61eb413ca4553",
|
"sha256": "0f3f684019763a14f5fbaec42e42c680cf748d930d46cd607fc61eb413ca4553",
|
||||||
"size": 2360
|
"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",
|
"path": "tools/verify-updater-hang-hotfix-1.43.0.mjs",
|
||||||
"sha256": "ae015616e289a4bfb371a2823c35e8e5a013cbdefd4005c47ac972c2e5df7516",
|
"sha256": "ae015616e289a4bfb371a2823c35e8e5a013cbdefd4005c47ac972c2e5df7516",
|
||||||
"size": 46
|
"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",
|
"path": "tools/verify-updater-status-ui-1.43.0.mjs",
|
||||||
"sha256": "ae015616e289a4bfb371a2823c35e8e5a013cbdefd4005c47ac972c2e5df7516",
|
"sha256": "ae015616e289a4bfb371a2823c35e8e5a013cbdefd4005c47ac972c2e5df7516",
|
||||||
@@ -3187,13 +3231,13 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "UPDATE_VENDOO.bat",
|
"path": "UPDATE_VENDOO.bat",
|
||||||
"sha256": "aab80ea7612891502fe3e6e275c864fd3c0239b9ca91738252363706a5ee4a8a",
|
"sha256": "d028f3c20dc26cdabd0db1cfcff5e0ecccd1fd457894ae1c41b7d896acf8f5f4",
|
||||||
"size": 1294
|
"size": 1294
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "update-manifest.json",
|
"path": "update-manifest.json",
|
||||||
"sha256": "a080d7733b65715573b9936502ce5228a8bfccd59a1766115be8cda908fa3285",
|
"sha256": "eb5aaabc083a6c4f837dd396712c1a6708fbb25833d9cd79543d29152a481939",
|
||||||
"size": 446
|
"size": 531
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,18 +15,18 @@ param(
|
|||||||
Set-StrictMode -Version 2.0
|
Set-StrictMode -Version 2.0
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
$TargetVersion = '1.43.0'
|
$TargetVersion = '1.43.0'
|
||||||
$UpdaterVersion = '3.3.0'
|
$UpdaterVersion = '3.4.0'
|
||||||
|
|
||||||
function Get-DefaultSteps {
|
function Get-DefaultSteps {
|
||||||
return @(
|
return @(
|
||||||
[ordered]@{id='package';title='Paket prüfen';state='pending'},
|
[ordered]@{id='package';title='Paket prüfen';state='pending'},
|
||||||
[ordered]@{id='tools';title='System prüfen';state='pending'},
|
[ordered]@{id='tools';title='System prüfen';state='pending'},
|
||||||
[ordered]@{id='github';title='GitHub verbinden';state='pending'},
|
[ordered]@{id='github';title='GitHub verbinden';state='pending'},
|
||||||
[ordered]@{id='production';title='Produktion vorbereiten';state='pending'},
|
[ordered]@{id='production';title='Zielzustand erkennen';state='pending'},
|
||||||
[ordered]@{id='workspace';title='Arbeitskopie erstellen';state='pending'},
|
[ordered]@{id='workspace';title='Payload sicher vorbereiten';state='pending'},
|
||||||
[ordered]@{id='tests';title='Vollständig testen';state='pending'},
|
[ordered]@{id='tests';title='Vollständig testen';state='pending'},
|
||||||
[ordered]@{id='pullrequest';title='Pull Request erstellen';state='pending'},
|
[ordered]@{id='pullrequest';title='Pull Request abgleichen';state='pending'},
|
||||||
[ordered]@{id='merge';title='Sicher mergen';state='pending'},
|
[ordered]@{id='merge';title='Finalen Head prüfen und mergen';state='pending'},
|
||||||
[ordered]@{id='deploy';title='Coolify deployen';state='pending'},
|
[ordered]@{id='deploy';title='Coolify deployen';state='pending'},
|
||||||
[ordered]@{id='verify';title='Produktion bestätigen';state='pending'}
|
[ordered]@{id='verify';title='Produktion bestätigen';state='pending'}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ param()
|
|||||||
|
|
||||||
Set-StrictMode -Version 2.0
|
Set-StrictMode -Version 2.0
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
$UpdaterVersion = '3.3.0'
|
$UpdaterVersion = '3.4.0'
|
||||||
|
|
||||||
function Get-CanonicalPackageRoot {
|
function Get-CanonicalPackageRoot {
|
||||||
$candidate = Join-Path -Path $PSScriptRoot -ChildPath '..\..'
|
$candidate = Join-Path -Path $PSScriptRoot -ChildPath '..\..'
|
||||||
@@ -51,7 +51,7 @@ try {
|
|||||||
(Join-Path $PackageRoot 'scripts\update-vendoo-gui.ps1'),
|
(Join-Path $PackageRoot 'scripts\update-vendoo-gui.ps1'),
|
||||||
(Join-Path $PackageRoot 'scripts\updater\Vendoo.Updater.UI.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.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.'
|
Write-PreflightLog 'INFO' 'Echte Windows-PowerShell-Parserprüfung gestartet.'
|
||||||
|
|||||||
@@ -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)
|
$PackageRoot = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..\..') -ErrorAction Stop).ProviderPath.TrimEnd([System.IO.Path]::DirectorySeparatorChar,[System.IO.Path]::AltDirectorySeparatorChar)
|
||||||
$TargetVersion = '1.43.0'
|
$TargetVersion = '1.43.0'
|
||||||
$UpdaterVersion = '3.3.0'
|
$UpdaterVersion = '3.4.0'
|
||||||
$WorkerScript = Join-Path $PSScriptRoot 'Vendoo.Updater.Worker.ps1'
|
$WorkerScript = Join-Path $PSScriptRoot 'Vendoo.Updater.Worker34.ps1'
|
||||||
$WorkerHostScript = Join-Path $PSScriptRoot 'Vendoo.Updater.Host.ps1'
|
$WorkerHostScript = Join-Path $PSScriptRoot 'Vendoo.Updater.Host.ps1'
|
||||||
$LocalRoot = Join-Path $env:LOCALAPPDATA 'Vendoo\Updater'
|
$LocalRoot = Join-Path $env:LOCALAPPDATA 'Vendoo\Updater'
|
||||||
$RuntimeRoot = Join-Path $LocalRoot 'runtime'
|
$RuntimeRoot = Join-Path $LocalRoot 'runtime'
|
||||||
@@ -158,8 +158,8 @@ foreach ($name in $names) { Set-Variable -Name $name -Value $window.FindName($na
|
|||||||
|
|
||||||
$steps = @(
|
$steps = @(
|
||||||
@{id='package'; title='Paket prüfen'}, @{id='tools'; title='System prüfen'}, @{id='github'; title='GitHub verbinden'},
|
@{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='production'; title='Zielzustand erkennen'}, @{id='workspace'; title='Payload sicher vorbereiten'}, @{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='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 = @{}
|
$script:StepControls = @{}
|
||||||
foreach ($step in $steps) {
|
foreach ($step in $steps) {
|
||||||
@@ -206,8 +206,8 @@ function Update-Steps($status) {
|
|||||||
function Write-InitialStatus {
|
function Write-InitialStatus {
|
||||||
$steps=@(
|
$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='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='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 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='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
|
$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)))
|
[IO.File]::WriteAllText($script:StatusFile,$payload,(New-Object Text.UTF8Encoding($false)))
|
||||||
|
|||||||
@@ -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}}
|
|
||||||
@@ -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}}
|
||||||
@@ -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
@@ -8,22 +8,38 @@ import {
|
|||||||
writeFileSync,
|
writeFileSync,
|
||||||
} from 'node:fs';
|
} from 'node:fs';
|
||||||
import { createHash } from 'node:crypto';
|
import { createHash } from 'node:crypto';
|
||||||
|
import { spawnSync } from 'node:child_process';
|
||||||
import { dirname, join, relative, sep } from 'node:path';
|
import { dirname, join, relative, sep } from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { generateReleaseManifest } from './generate-release-manifest.mjs';
|
import { generateReleaseManifest } from './generate-release-manifest.mjs';
|
||||||
import { MANIFEST_FILE, readAndValidateManifest } from './release-manifest-lib.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 root = dirname(dirname(fileURLToPath(import.meta.url)));
|
||||||
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
|
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
|
||||||
const outputRoot = join(root, 'release-output');
|
const outputRoot = join(root, 'release-output');
|
||||||
const packageName = `Vendoo_${pkg.version}`;
|
|
||||||
const stage = join(outputRoot, packageName);
|
|
||||||
const normalize = value => value.split(sep).join('/');
|
const normalize = value => value.split(sep).join('/');
|
||||||
|
|
||||||
// The committed manifest is the only release payload contract. Regenerate it first so
|
function git(args, { allowFailure = false } = {}) {
|
||||||
// stale hashes or accidentally omitted source files make the release fail immediately.
|
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);
|
generateReleaseManifest(root);
|
||||||
const manifest = readAndValidateManifest(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 });
|
rmSync(outputRoot, { recursive: true, force: true });
|
||||||
mkdirSync(stage, { recursive: true });
|
mkdirSync(stage, { recursive: true });
|
||||||
@@ -34,7 +50,8 @@ for (const entry of manifest.files) {
|
|||||||
copyFileSync(source, target);
|
copyFileSync(source, target);
|
||||||
if (statSync(target).size !== entry.size) throw new Error(`Release-Kopie hat falsche Größe: ${entry.path}`);
|
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 = [];
|
const checksums = [];
|
||||||
function hashTree(dir) {
|
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');
|
writeFileSync(join(outputRoot, 'RELEASE_NAME.txt'), `${packageName}\n`, 'utf8');
|
||||||
console.log(`Manifestbasiertes Release-Staging erstellt: ${stage}`);
|
console.log(`Manifestbasiertes Release-Staging erstellt: ${stage}`);
|
||||||
console.log(`${manifest.files.length} Payload-Dateien und ${checksums.length} Paketdateien mit SHA-256 erfasst.`);
|
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}`);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
statSync,
|
statSync,
|
||||||
} from 'node:fs';
|
} from 'node:fs';
|
||||||
import { createHash } from 'node:crypto';
|
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';
|
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
||||||
|
|
||||||
export const MANIFEST_SCHEMA = 'vendoo-release-manifest-v1';
|
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.Preflight.ps1',
|
||||||
'scripts/updater/Vendoo.Updater.UI.ps1',
|
'scripts/updater/Vendoo.Updater.UI.ps1',
|
||||||
'scripts/updater/Vendoo.Updater.Host.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/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']);
|
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);
|
const manifestPath = join(base, MANIFEST_FILE);
|
||||||
if (!existsSync(manifestPath)) throw new Error(`${MANIFEST_FILE} fehlt.`);
|
if (!existsSync(manifestPath)) throw new Error(`${MANIFEST_FILE} fehlt.`);
|
||||||
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
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.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.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.');
|
if (!Array.isArray(manifest.removedFiles)) throw new Error('removedFiles fehlt im Release-Manifest.');
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param()
|
param(
|
||||||
|
[string]$ReportPath = ''
|
||||||
|
)
|
||||||
|
|
||||||
Set-StrictMode -Version 2.0
|
Set-StrictMode -Version 2.0
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
@@ -10,7 +12,7 @@ $files = Get-ChildItem -LiteralPath $root -Recurse -File | Where-Object {
|
|||||||
} | Where-Object {
|
} | Where-Object {
|
||||||
$full = $_.FullName
|
$full = $_.FullName
|
||||||
-not (@($excludedParts | Where-Object { $full.IndexOf($_,[StringComparison]::OrdinalIgnoreCase) -ge 0 }).Count)
|
-not (@($excludedParts | Where-Object { $full.IndexOf($_,[StringComparison]::OrdinalIgnoreCase) -ge 0 }).Count)
|
||||||
}
|
} | Sort-Object FullName
|
||||||
|
|
||||||
$failures = New-Object System.Collections.Generic.List[string]
|
$failures = New-Object System.Collections.Generic.List[string]
|
||||||
foreach ($file in $files) {
|
foreach ($file in $files) {
|
||||||
@@ -18,14 +20,27 @@ foreach ($file in $files) {
|
|||||||
$errors = $null
|
$errors = $null
|
||||||
[void][System.Management.Automation.Language.Parser]::ParseFile($file.FullName,[ref]$tokens,[ref]$errors)
|
[void][System.Management.Automation.Language.Parser]::ParseFile($file.FullName,[ref]$tokens,[ref]$errors)
|
||||||
foreach ($parseError in @($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) {
|
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
|
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
|
exit 0
|
||||||
|
|||||||
@@ -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.');
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
import { tmpdir } from 'node:os';
|
import { tmpdir } from 'node:os';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { applyReleaseManifest, buildReleaseManifest, MANIFEST_FILE, readAndValidateManifest } from './release-manifest-lib.mjs';
|
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 temp = mkdtempSync(join(tmpdir(), 'vendoo-release-manifest-'));
|
||||||
const source = join(temp, 'source');
|
const source = join(temp, 'source');
|
||||||
@@ -21,13 +22,15 @@ const requiredBodies = new Map([
|
|||||||
['server.mjs', 'export const server = true;\n'],
|
['server.mjs', 'export const server = true;\n'],
|
||||||
['lib/db.mjs', 'export const db = true;\n'],
|
['lib/db.mjs', 'export const db = true;\n'],
|
||||||
['db/schema.sql', 'CREATE TABLE test(id INTEGER PRIMARY KEY);\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'],
|
['UPDATE_VENDOO.bat', '@echo off\r\n'],
|
||||||
['scripts/updater/Vendoo.Updater.Preflight.ps1', 'Write-Output preflight\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.UI.ps1', 'Write-Output ui\n'],
|
||||||
['scripts/updater/Vendoo.Updater.Host.ps1', 'Write-Output host\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/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) {
|
function write(root, rel, content) {
|
||||||
@@ -36,10 +39,15 @@ function write(root, rel, content) {
|
|||||||
writeFileSync(target, content, 'utf8');
|
writeFileSync(target, content, 'utf8');
|
||||||
}
|
}
|
||||||
|
|
||||||
function writeManifest(root, removedFiles = []) {
|
function writePackageManifest(root, removedFiles = []) {
|
||||||
const manifest = buildReleaseManifest(root, { version: '1.43.0', updaterVersion: '3.3.0', removedFiles });
|
const sourceManifest = buildReleaseManifest(root, { version: '1.43.0', updaterVersion: '3.4.0', removedFiles });
|
||||||
writeFileSync(join(root, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
const packageManifest = createPackageManifest(sourceManifest, {
|
||||||
return manifest;
|
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 {
|
try {
|
||||||
@@ -50,7 +58,8 @@ try {
|
|||||||
write(destination, 'db/schema.sql', 'OLD SCHEMA');
|
write(destination, 'db/schema.sql', 'OLD SCHEMA');
|
||||||
write(destination, 'baseline-only.txt', 'must survive overlay');
|
write(destination, 'baseline-only.txt', 'must survive overlay');
|
||||||
write(destination, 'obsolete.txt', 'must be removed explicitly');
|
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.');
|
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);
|
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.');
|
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 });
|
readAndValidateManifest(destination, { allowExtraFiles: true });
|
||||||
|
|
||||||
let controlDeletionBlocked = false;
|
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); }
|
catch (error) { controlDeletionBlocked = /Steuerdatei/.test(error.message); }
|
||||||
if (!controlDeletionBlocked) throw new Error('Löschung von release-manifest.json wurde nicht blockiert.');
|
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');
|
const corrupt = join(temp, 'corrupt');
|
||||||
mkdirSync(corrupt, { recursive: true });
|
mkdirSync(corrupt, { recursive: true });
|
||||||
for (const entry of manifest.files) {
|
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); }
|
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.');
|
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 {
|
} finally {
|
||||||
rmSync(temp, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
rmSync(temp, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.');
|
||||||
@@ -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.');
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
{
|
{
|
||||||
"schema": "vendoo-update-manifest-v1",
|
"schema": "vendoo-update-manifest-v1",
|
||||||
"version": "1.43.0",
|
"version": "1.43.0",
|
||||||
"name": "Vendoo Manifest-Based Production Update 3.3",
|
"name": "Vendoo Manifest-Based Production Update 3.4",
|
||||||
"channel": "stable",
|
"channel": "stable",
|
||||||
"minimum_node": "22",
|
"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": "",
|
"download_url": "",
|
||||||
"sha256": "",
|
"sha256": "",
|
||||||
"updater_version": "3.3.0",
|
"updater_version": "3.4.0",
|
||||||
"updaterVersion": "3.3.0",
|
"updaterVersion": "3.4.0",
|
||||||
"removedFiles": []
|
"removedFiles": [
|
||||||
|
"scripts/updater/Vendoo.Updater.Worker.ps1"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user