Compare commits

...
Author SHA1 Message Date
Masterluke77 e70bb9b3e5 ci: remove final typed-catch repair job 2026-07-10 16:11:10 +02:00
vendoo-updater-build a694d3f50e fix: make typed Worker34 catch PowerShell 5.1 compatible 2026-07-10 14:10:30 +00:00
Masterluke77 1c9209d2c9 ci: stage final typed-catch parser repair 2026-07-10 16:10:12 +02:00
Masterluke77 8cf2600783 ci: remove temporary Worker34 repair import workflow 2026-07-10 16:07:19 +02:00
Masterluke77 c44b29c878 ci: remove temporary Worker34 repair export job 2026-07-10 16:07:07 +02:00
Masterluke77 ea0c72936e ci: run Worker34 repair import on PR head 2026-07-10 16:06:28 +02:00
vendoo-updater-build 7b04856217 fix: make Worker34 compatible with Windows PowerShell 5.1 2026-07-10 14:06:14 +00:00
Masterluke77 32bd4e9cf1 ci: import verified Worker34 parser repair artifact 2026-07-10 16:05:59 +02:00
Masterluke77 a3241fd595 ci: export Worker34 parser repair as controlled artifact 2026-07-10 16:02:50 +02:00
Masterluke77 f495c534a1 ci: run guarded Worker34 repair on PR synchronize events 2026-07-10 16:01:14 +02:00
Masterluke77 5c2411d509 ci: allow guarded Worker34 repair for app commits 2026-07-10 16:00:12 +02:00
Masterluke77 bd91c6b45a ci: stage deterministic Worker34 parser compatibility repair 2026-07-10 15:59:30 +02:00
Masterluke77 6b821cb09d ci: run updater 3.4 diagnostic acceptance on clean tree 2026-07-10 15:20:00 +02:00
Masterluke77 1533f56a13 ci: trigger updater 3.4 diagnostic acceptance 2026-07-10 15:19:48 +02:00
vendoo-release-bot b4d0c51327 chore: regenerate updater 3.4 release manifest 2026-07-10 13:19:33 +00:00
Masterluke77 b0a80c0521 ci: regenerate updater 3.4 manifest after CI hardening 2026-07-10 15:19:19 +02:00
Masterluke77 34936e840f chore: remove temporary updater CI patch workflow 2026-07-10 15:19:09 +02:00
Masterluke77 cff8c53894 test: enforce updater parser diagnostics and deterministic package 2026-07-10 15:18:59 +02:00
Masterluke77 0b9f305e89 ci: add Windows parser diagnostics and deterministic package build 2026-07-10 15:18:42 +02:00
Masterluke77 9688258707 ci: patch updater 3.4 Windows diagnostics and package contract 2026-07-10 15:17:39 +02:00
Masterluke77 bbecc829e7 ci: run updater 3.4 acceptance on clean tree 2026-07-10 15:16:28 +02:00
Masterluke77 2ecc6379d3 ci: trigger clean updater 3.4 acceptance 2026-07-10 15:16:10 +02:00
vendoo-release-bot f00dbb66dd chore: regenerate updater 3.4 release manifest 2026-07-10 13:15:52 +00:00
Masterluke77 76123e3760 ci: regenerate updater 3.4 manifest from clean tree 2026-07-10 15:15:37 +02:00
Masterluke77 d4943fe108 chore: remove temporary updater acceptance repair workflow 2026-07-10 15:15:20 +02:00
Masterluke77 2aa304e3d9 chore: remove completed updater package patch workflow 2026-07-10 15:15:08 +02:00
Masterluke77 fd478f3c34 chore: remove final updater acceptance trigger 2026-07-10 15:15:02 +02:00
Masterluke77 e7fe38be7b chore: remove temporary updater acceptance trigger 2026-07-10 15:14:56 +02:00
Masterluke77 737d792860 fix: make updater acceptance repair line-safe 2026-07-10 15:13:54 +02:00
Masterluke77 d1f16a4c5c ci: persist Windows PowerShell parser diagnostics 2026-07-10 15:13:23 +02:00
Masterluke77 ffecf045db fix: make updater 3.4 acceptance repair deterministic 2026-07-10 15:12:23 +02:00
Masterluke77 867cc6c2c0 ci: repair updater 3.4 acceptance contract 2026-07-10 15:11:39 +02:00
Masterluke77 2362c15477 ci: stage final updater 3.4 acceptance run 2026-07-10 15:04:36 +02:00
vendoo-release-bot f52e6f85eb chore: regenerate final updater 3.4 release manifest 2026-07-10 13:04:14 +00:00
Masterluke77 8a6e13c86e build: stage final updater 3.4 release manifest 2026-07-10 15:04:02 +02:00
Masterluke77 33aaaaea1e build: stage deterministic updater package contract 2026-07-10 15:03:21 +02:00
Masterluke77 1fb62916bc build: add deterministic updater zip builder 2026-07-10 15:02:53 +02:00
Masterluke77 5a925498a5 ci: stage updater 3.4 acceptance verification 2026-07-10 14:58:23 +02:00
vendoo-release-bot 65f94d041a chore: regenerate updater 3.4 acceptance manifest 2026-07-10 12:58:19 +00:00
Masterluke77 ae0cfb7c59 build: stage updater 3.4 acceptance manifest regeneration 2026-07-10 14:58:04 +02:00
Masterluke77 ae5821a505 docs: document final-head dispatch and package acceptance 2026-07-10 14:57:24 +02:00
Masterluke77 bcebb0e6e4 test: include updater final-head CI contract in release gates 2026-07-10 14:56:48 +02:00
Masterluke77 958a5877ed test: add updater final-head CI lifecycle gate 2026-07-10 14:56:27 +02:00
Masterluke77 75a997843d feat: require package acceptance on final head 2026-07-10 14:56:12 +02:00
Masterluke77 e6f44fbd84 ci: dispatch validation workflows for manifest-synced head 2026-07-10 14:55:26 +02:00
Masterluke77 e40a218779 ci: validate and package updater 3.4 after dual-platform acceptance 2026-07-10 14:55:06 +02:00
Masterluke77 da4b4dd567 ci: verify final clean updater 3.4 tree 2026-07-10 14:53:24 +02:00
Masterluke77 b6ee6f9aab ci: stage clean updater 3.4 verification 2026-07-10 14:53:16 +02:00
vendoo-release-bot 4157afa2f3 chore: regenerate clean updater 3.4 release manifest 2026-07-10 12:52:58 +00:00
Masterluke77 18b334a20c build: stage clean updater 3.4 manifest regeneration 2026-07-10 14:52:45 +02:00
Masterluke77 1308d6cefd test: add updater 3.4 start-chain gate 2026-07-10 14:52:31 +02:00
Masterluke77 75d811ef19 test: lock updater 3.4 start chain contract 2026-07-10 14:52:11 +02:00
Masterluke77 80b024bc93 chore: label root updater starter as version 3.4 2026-07-10 14:51:53 +02:00
Masterluke77 05a90aea21 fix: route updater preflight to worker 3.4 2026-07-10 14:51:36 +02:00
Masterluke77 d0a2a560a1 ci: verify clean updater 3.4 tree 2026-07-10 14:50:47 +02:00
Masterluke77 0b98ecd4a4 ci: trigger final updater 3.4 verification 2026-07-10 14:50:00 +02:00
vendoo-release-bot c8bc69968f chore: refresh updater 3.4 release manifest 2026-07-10 12:48:52 +00:00
Masterluke77 537af2509c build: stage refreshed updater 3.4 manifest 2026-07-10 14:48:38 +02:00
vendoo-updater-build 196919c5e5 fix: make updater head interpolation PowerShell-safe 2026-07-10 12:48:12 +00:00
Masterluke77 bce6536a1c fix: stage updater worker head interpolation repair 2026-07-10 14:48:01 +02:00
vendoo-updater-build f6f612f429 chore: remove temporary updater diagnostics 2026-07-10 12:47:52 +00:00
Masterluke77 d36c0c704b ci: stage updater 3.4 PowerShell static diagnostics 2026-07-10 14:47:38 +02:00
Masterluke77 7393aa2cbc test: validate updater 3.4 package manifest provenance 2026-07-10 14:46:05 +02:00
vendoo-release-bot 3fb4db14f6 chore: synchronize updater 3.4 release manifest 2026-07-10 12:45:56 +00:00
Masterluke77 df6fc77781 build: stage final updater 3.4 manifest synchronization 2026-07-10 14:45:39 +02:00
Masterluke77 d8bdefc7fd docs: add updater 3.4 acceptance evidence matrix 2026-07-10 14:45:26 +02:00
Masterluke77 e661bc902f docs: define production updater 3.4 lifecycle contract 2026-07-10 14:45:05 +02:00
vendoo-updater-build f24b1ae217 feat: validate updater 3.4 package provenance 2026-07-10 12:43:56 +00:00
Masterluke77 fb9d4aa923 feat: build updater packages with immutable provenance 2026-07-10 14:43:41 +02:00
Masterluke77 87ed676c2f build: stage updater 3.4 release provenance patch 2026-07-10 14:43:27 +02:00
Masterluke77 dd9c62e989 chore: remove retired updater 3.3 worker from releases 2026-07-10 14:43:10 +02:00
Masterluke77 14f1a0fa20 refactor: retire production updater 3.3 worker 2026-07-10 14:43:04 +02:00
Masterluke77 3818cee11f test: harden updater 3.4 parallel workflow regressions 2026-07-10 14:42:04 +02:00
vendoo-updater-build a2af2b3f3e chore: route updater UI to lifecycle 3.4 2026-07-10 12:41:51 +00:00
Masterluke77 cb05237247 build: stage updater 3.4 UI routing patch 2026-07-10 14:41:38 +02:00
Masterluke77 975451bb93 ci: generalize release manifest sync lifecycle 2026-07-10 14:41:16 +02:00
Masterluke77 2f75ed44cc chore: declare production updater 3.4 2026-07-10 14:41:08 +02:00
Masterluke77 baef1a51af chore: route updater host to version 3.4 2026-07-10 14:41:00 +02:00
Masterluke77 ce2304cc52 fix: evaluate latest workflow run deterministically 2026-07-10 14:40:21 +02:00
Masterluke77 d9d6f9e91a chore: route updater gates to lifecycle 3.4 2026-07-10 14:38:24 +02:00
vendoo-updater-build 01c635bca4 feat: add production updater 3.4 worker 2026-07-10 12:38:04 +00:00
Masterluke77 ac06adfca2 build: assemble updater from exact gzip blob 2026-07-10 14:37:51 +02:00
Masterluke77 6d997a95cc build: stage exact updater 3.4 worker archive 2026-07-10 14:37:36 +02:00
Masterluke77 d6b65ee659 build: repair updater worker transport part 3 2026-07-10 14:34:56 +02:00
Masterluke77 07d47ce952 build: repair updater worker transport part 2 2026-07-10 14:33:52 +02:00
Masterluke77 a04127d832 build: normalize staged base64 before assembly 2026-07-10 14:31:14 +02:00
Masterluke77 120039a163 build: allow PR-triggered updater assembly 2026-07-10 14:29:45 +02:00
Masterluke77 aba86b1b2a build: trigger updater 3.4 assembly 2026-07-10 14:28:58 +02:00
Masterluke77 27a8eba16e build: assemble updater 3.4 worker 2026-07-10 14:28:39 +02:00
Masterluke77 1d16ec0673 build: stage updater 3.4 worker part 4 2026-07-10 14:28:23 +02:00
Masterluke77 344b18c07c build: stage updater 3.4 worker part 3 2026-07-10 14:27:26 +02:00
Masterluke77 8d3ca950dd build: stage updater 3.4 worker part 2 2026-07-10 14:26:24 +02:00
Masterluke77 e7560dcf69 build: stage updater 3.4 worker part 1 2026-07-10 14:25:19 +02:00
Masterluke77 03fe917305 test: add updater 3.4 lifecycle regressions 2026-07-10 14:23:09 +02:00
Masterluke77 3604f83c93 feat: add updater payload regression guard 2026-07-10 14:22:37 +02:00
Masterluke77 25254e37d6 feat: add updater 3.4 lifecycle model 2026-07-10 14:22:24 +02:00
23 changed files with 1358 additions and 413 deletions
+92 -2
View File
@@ -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
+27 -3
View File
@@ -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
View File
@@ -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.
+189
View File
@@ -0,0 +1,189 @@
# Vendoo Production Updater 3.4
## Zweck
Updater 3.4 ersetzt den linearen, branchorientierten Ablauf von Updater 3.3 durch einen idempotenten Release-Lifecycle. Der Updater arbeitet auf einen nachprüfbaren Zielzustand hin und erzeugt keine weiteren Quelländerungen, sobald `main` oder die Produktion die Zielversion bereits erreicht haben.
Der Vertrag schützt insbesondere:
- den aktuellen Stand von `main` vor veralteten Payloads,
- den Pull Request vor Merge eines nicht mehr geprüften Head-SHA,
- vorhandene Releases vor doppelten Branches und Pull Requests,
- persistente Produktionsdaten unter `/app/data`,
- Datenbanken, Uploads, Benutzer, Artikel, Einstellungen, API-Schlüssel, FLUX-Modelle und Backups.
## Quellwahrheit
Der Updater wertet den Zustand in dieser Reihenfolge aus:
1. aktueller GitHub-Branch `main`,
2. aktueller Pull-Request-Head und konkrete GitHub-Workflow-Runs,
3. öffentliche Produktionsantwort `/readyz`,
4. lokaler Sitzungsstand nur als Wiederaufnahmekontext.
Ein lokaler Sitzungsstand darf niemals einen neueren GitHub- oder Produktionszustand überschreiben.
## Zustandsautomat
```text
PACKAGE_VALIDATED
-> TOOLS_READY
-> GITHUB_READY
-> TARGET_OBSERVED
TARGET_OBSERVED
-> COMPLETED
wenn main == target und production == target
-> DEPLOY_REQUIRED
wenn main == target und production != target
-> PR_RESUME
wenn ein passender offener PR existiert
-> DEPLOY_REQUIRED
wenn ein passender PR bereits gemergt ist
-> SOURCE_REQUIRED
nur wenn main noch nicht target ist und kein passender PR existiert
SOURCE_REQUIRED
-> PAYLOAD_GUARD
-> LOCAL_CONTRACT
-> TARGET_REOBSERVED
-> PR_RECONCILED
PR_RESUME | PR_RECONCILED
-> CHECKS_TRACKING
CHECKS_TRACKING
-> CHECKS_TRACKING
wenn sich der PR-Head-SHA ändert; Grace Period beginnt erneut
-> CHECKS_TRACKING
wenn Checks noch nicht registriert, queued, pending oder in_progress sind
-> MERGE_READY
wenn alle Pflichtchecks und Pflichtworkflows am stabilen finalen Head erfolgreich sind
-> FAILED
bei terminalem Pflichtfehler, Abbruch oder Gesamtzeitüberschreitung
MERGE_READY
-> MERGED
nur per --match-head-commit für den geprüften Head-SHA
-> DEPLOY_REQUIRED
-> PRODUCTION_VERIFIED
-> COMPLETED
```
## Idempotenzregeln
### Bereits aktuelle Produktion
Wenn `main` und `/readyz` bereits die Zielversion melden, endet der Updater sofort erfolgreich. Es werden weder Arbeitskopie noch Branch, Pull Request, Commit oder Deployment erzeugt.
### `main` bereits auf Zielversion
Wenn `main` bereits die Zielversion enthält, wird die Sourcephase vollständig übersprungen. Der Updater prüft nur noch, ob die Produktion nachziehen muss.
### Bereits gemergter Pull Request
Ein bereits gemergter passender Pull Request wird als abgeschlossene Sourcephase behandelt. Der Updater erzeugt keinen zweiten Pull Request und setzt beim Deployment beziehungsweise bei der Produktionsprüfung fort.
### Bereits offener Pull Request
Ein vorhandener offener Pull Request des deterministischen Releasebranches wird übernommen. Sein aktueller `headRefOid` ist die alleinige Prüfreferenz.
### Doppelter oder paralleler Start
Die Desktopoberfläche verhindert parallele lokale Prozesse per Mutex. Zusätzlich gleicht jeder Lauf GitHub und Produktion erneut ab. Ein konkurrierender Lauf, der zwischen lokaler Prüfung und Push einen Pull Request erzeugt oder `main` aktualisiert, wird bei der zweiten Zielbeobachtung erkannt und übernommen.
## Paket-Provenienz
Auslieferbare Pakete verwenden das Schema `vendoo-release-package-v2` und enthalten:
- `targetVersion`,
- `updaterVersion`,
- `sourceCommit`,
- `baseCommit`,
- `createdAt`,
- `payloadHash`,
- die vollständige SHA-256-Dateiliste,
- explizite Löschungen.
`payloadHash` wird aus den stabilen Manifestfeldern berechnet. Jede Änderung an Herkunft, Zieldateien oder Löschungen verändert die Paketidentität.
Vor dem Overlay wird geprüft:
1. `baseCommit` und aktueller `main` müssen lokal auflösbar sein.
2. `baseCommit` muss Vorfahr des aktuellen `main` sein.
3. Alle Pfade, die Payload und `main` seit `baseCommit` beide verändert haben, werden verglichen.
4. Weicht der gewünschte Payloadstand vom aktuellen `main` ab, wird der Lauf vor jeder Quelländerung blockiert.
5. Nicht kollidierende Fortschritte in `main` dürfen bestehen bleiben.
Damit kann ein altes Paket neuere Änderungen aus `main` weder still noch per Force-Push zurücksetzen.
## GitHub-Check-Lifecycle
Updater 3.4 verwendet nicht mehr `gh pr checks --watch` als Blackbox.
Für jeden aktuellen PR-Head werden ausgewertet:
- `statusCheckRollup` des Pull Requests,
- Workflow-Runs aus `actions/runs?head_sha=<finaler-head>&per_page=100`,
- die Pflichtchecks `Syntax, Sicherheit und Release-Gates`, `Windows vollständiger Releasevertrag` und `Updater-3.4-Paket nach Abnahme`,
- die Pflichtworkflows `Vendoo CI` und `Vendoo Release Manifest Sync`.
Regeln:
- `no checks reported` ist während der 90-sekündigen Registrierungs-Grace-Period kein Fehler.
- `queued`, `pending`, `in_progress`, `waiting` und `requested` sind laufende Zustände.
- Bei mehreren Runs desselben Workflows entscheidet der zeitlich neueste Run; ein alter Erfolg darf einen neueren Fehler nicht verdecken.
- Ändert der Manifest-Sync den Head-SHA, werden Grace Period und Checkauswertung für den neuen Head vollständig neu gestartet.
- Vor dem Merge wird der Head-SHA erneut gelesen.
- Der Merge verwendet `--match-head-commit`; ein weiterer Headwechsel verhindert den Merge atomar.
### Final-Head-Registrierung nach Manifest-Sync
Ein Push mit dem GitHub-Workflow-Token startet für den dadurch erzeugten Commit nicht automatisch einen weiteren Push-Workflow. Deshalb bestätigt der Manifest-Sync nach seinem Commit zuerst den tatsächlichen Remote-Head und dispatcht anschließend explizit:
1. `Vendoo CI` für den neuen Head,
2. `Vendoo Release Manifest Sync` für denselben Head.
Der zweite Manifest-Sync-Lauf findet ein unverändertes Manifest, erzeugt keinen weiteren Commit und dient als konkreter erfolgreicher Workflow-Nachweis am finalen SHA. Die Concurrency des Sync-Workflows darf den auslösenden Lauf nicht abbrechen.
### Paket erst nach Plattformabnahme
Das auslieferbare ZIP entsteht im Job `Updater-3.4-Paket nach Abnahme`. Dieser Job besitzt eine harte `needs`-Abhängigkeit auf Linux- und Windows-Abnahme. Er läuft daher erst, wenn beide Verträge erfolgreich abgeschlossen wurden. Danach werden Root-Manifest, Paketmanifest v2, Zielversion, Updater-Version und ZIP-SHA-256 erneut geprüft. Nur dieses Artefakt ist ein auslieferbarer Kandidat.
## Sitzungsvertrag
Sitzungen verwenden `vendoo-updater-session-v2`. Die Identität besteht aus:
- Repository,
- Zielversion,
- Payload-Hash,
- deterministischem Releasebranch.
Gespeichert werden unter anderem Pull-Request-Nummer und URL, finaler geprüfter Head-SHA, Merge-Commit, Produktionsversion, Releasezustand und Abschlusszeitpunkt. Der Sitzungsstand dient ausschließlich der Wiederaufnahme; jeder Lauf validiert die externen Zustände erneut.
## Branch- und Push-Sicherheit
- Releasebranch: `release/<zielversion>-production-update-v4`
- kein unbedingtes `git push --force`
- vorhandene Branches werden nur mit `--force-with-lease=<erwarteter-remote-sha>` aktualisiert
- Merge ausschließlich für den nachgewiesenen finalen Head-SHA
- kein Quell-PR, wenn `main` bereits auf Zielversion ist
## Produktionsvertrag
Das Deployment verändert keine persistenten Nutzdaten. `/app/data` bleibt außerhalb des Release-Payloads. Der Updater akzeptiert Erfolg erst, wenn `/readyz` sowohl `ok: true` als auch exakt die Zielversion meldet.
Die Option `-SkipProductionDeploy` ist ausschließlich für Source-/CI-Abnahmen vorgesehen. Sie darf nicht als Nachweis eines vollständigen Produktionsupdates ausgegeben werden.
## Fehlerdiagnose
Bei Fehler oder Timeout werden protokolliert:
- aktuelle Phase,
- aktueller PR und Head-SHA,
- Pflichtcheck- und Workflow-Matrix,
- Prozessausgabe und Exit-Code,
- Payload-Konfliktpfade,
- konkrete Wiederanlaufempfehlung.
Ein erneuter Start beginnt nicht blind von vorn, sondern beobachtet Zielzustand, Pull Requests, Merge und Produktion erneut und setzt beim tatsächlich offenen Zustand fort.
+7 -4
View File
@@ -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,10 +37,13 @@
"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"
+74 -30
View File
@@ -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
} }
] ]
} }
+5 -5
View File
@@ -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'}
) )
+2 -2
View File
@@ -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.'
+6 -6
View File
@@ -10,8 +10,8 @@ Add-Type -AssemblyName System.Xaml
$PackageRoot = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..\..') -ErrorAction Stop).ProviderPath.TrimEnd([System.IO.Path]::DirectorySeparatorChar,[System.IO.Path]::AltDirectorySeparatorChar) $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)))
-334
View File
@@ -1,334 +0,0 @@
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)][string]$StatusFile,
[Parameter(Mandatory=$true)][string]$EventFile,
[Parameter(Mandatory=$true)][string]$InputFile,
[Parameter(Mandatory=$true)][string]$CancelFile,
[Parameter(Mandatory=$true)][string]$SessionFile,
[Parameter(Mandatory=$true)][string]$LogFile,
[switch]$SkipProductionDeploy,
[switch]$SelfTest,
[string]$SelfTestFailAt = ''
)
Set-StrictMode -Version 2.0
$ErrorActionPreference='Stop';$ProgressPreference='SilentlyContinue'
Add-Type -AssemblyName System.Net.Http
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$PackageRoot=(Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..\..') -ErrorAction Stop).ProviderPath.TrimEnd([System.IO.Path]::DirectorySeparatorChar,[System.IO.Path]::AltDirectorySeparatorChar)
$Repository='Masterluke77/vendoo';$BaseBranch='main';$TargetVersion='1.43.0';$UpdaterVersion='3.3.0';$PublicBaseUrl='https://vendoo.flatlined.de'
$LocalRoot=Join-Path $env:LOCALAPPDATA 'Vendoo\Updater';$ToolsRoot=Join-Path $LocalRoot 'tools';$LogsRoot=Join-Path $LocalRoot 'logs';$WorkRoot=Join-Path $LocalRoot "work\$TargetVersion";New-Item -ItemType Directory -Path $ToolsRoot,$LogsRoot,(Split-Path -Parent $SessionFile) -Force|Out-Null
$Stamp=Get-Date -Format 'yyyyMMdd-HHmmss';$Branch="release/$TargetVersion-production-update-v3";$CloneDir=Join-Path $WorkRoot 'repository'
$script:Tools=@{};$script:CurrentStep='package';$script:LastHeartbeat=Get-Date
$script:Steps=@(
[ordered]@{id='package';title='Paket prüfen';state='pending'},[ordered]@{id='tools';title='System prüfen';state='pending'},[ordered]@{id='github';title='GitHub verbinden';state='pending'},
[ordered]@{id='production';title='Produktion vorbereiten';state='pending'},[ordered]@{id='workspace';title='Arbeitskopie erstellen';state='pending'},[ordered]@{id='tests';title='Vollständig testen';state='pending'},
[ordered]@{id='pullrequest';title='Pull Request erstellen';state='pending'},[ordered]@{id='merge';title='Sicher mergen';state='pending'},[ordered]@{id='deploy';title='Coolify deployen';state='pending'},[ordered]@{id='verify';title='Produktion bestätigen';state='pending'}
)
function Write-Event([string]$Level,[string]$Message){
$safe=$Message -replace '(?i)(https://[^\s]*(?:token|webhook)[^\s]*)','[VERDECKT]'
$entry=[ordered]@{at=(Get-Date).ToString('o');level=$Level.ToUpperInvariant();message=$safe}|ConvertTo-Json -Compress
[IO.File]::AppendAllText($EventFile,$entry+[Environment]::NewLine,(New-Object Text.UTF8Encoding($false)))
[IO.File]::AppendAllText($LogFile,"$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [$($Level.ToUpperInvariant())] $safe$([Environment]::NewLine)",(New-Object Text.UTF8Encoding($false)))
}
function Write-State([int]$Percent,[string]$Phase,[string]$Detail,[string]$State='running',[string]$Error='',[string]$Remediation='', $Request=$null){
$payload=[ordered]@{version=$TargetVersion;updaterVersion=$UpdaterVersion;percent=[Math]::Max(0,[Math]::Min(100,$Percent));phase=$Phase;detail=$Detail;state=$State;error=$Error;remediation=$Remediation;updatedAt=(Get-Date).ToString('o');logFile=$LogFile;sessionFile=$SessionFile;steps=$script:Steps;request=$Request}|ConvertTo-Json -Depth 8
$tmp="$StatusFile.$PID.tmp";[IO.File]::WriteAllText($tmp,$payload,(New-Object Text.UTF8Encoding($false)));Move-Item -LiteralPath $tmp -Destination $StatusFile -Force
}
function Set-Step([string]$Id,[string]$State){foreach($s in $script:Steps){if($s.id -eq $Id){$s.state=$State}elseif($State -eq 'active' -and $s.state -eq 'active'){$s.state='pending'}};$script:CurrentStep=$Id}
function Complete-Step([string]$Id){foreach($s in $script:Steps){if($s.id -eq $Id){$s.state='done'}}}
function Fail-Step{foreach($s in $script:Steps){if($s.id -eq $script:CurrentStep){$s.state='failed'}}}
function Assert-NotCancelled{if(Test-Path -LiteralPath $CancelFile){throw [System.OperationCanceledException]::new('Das Update wurde durch den Benutzer abgebrochen.')}}
function Save-Session($Data){if(-not($Data.PSObject.Properties.Name -contains 'updaterVersion')){$Data|Add-Member -NotePropertyName updaterVersion -NotePropertyValue $UpdaterVersion};$Data.updaterVersion=$UpdaterVersion;$Data.updatedAt=(Get-Date).ToString('o');$tmp="$SessionFile.tmp";$Data|ConvertTo-Json -Depth 8|Set-Content -LiteralPath $tmp -Encoding UTF8;Move-Item $tmp $SessionFile -Force}
function Load-Session{if(Test-Path -LiteralPath $SessionFile){try{$saved=Get-Content $SessionFile -Raw -Encoding UTF8|ConvertFrom-Json;if([string]$saved.version-eq$TargetVersion -and [string]$saved.updaterVersion-eq$UpdaterVersion){return $saved};Write-Event 'INFO' 'Veralteter oder anderer Updater-Sitzungsstand wird nicht übernommen.'}catch{Write-Event 'WARN' 'Gespeicherter Sitzungsstand ist ungültig und wird verworfen.'}};return [pscustomobject]@{version=$TargetVersion;updaterVersion=$UpdaterVersion;branch=$Branch;prUrl='';merged=$false;runId='';stage='new';updatedAt=''}}
function Quote-Arg([string]$Value){if($null -eq $Value){return '""'};if($Value -notmatch '[\s"]'){return $Value};return '"'+($Value -replace '(\\*)"','$1$1\"' -replace '(\\+)$','$1$1')+'"'}
function Stop-Tree([int]$ProcessId){
try { & taskkill.exe /PID $ProcessId /T /F 2>$null | Out-Null }
catch { try { Stop-Process -Id $ProcessId -Force -ErrorAction SilentlyContinue } catch {} }
}
function Get-TailText([string]$Text,[int]$MaxLength){
if([string]::IsNullOrWhiteSpace($Text)){return ''}
$value=$Text.Trim()
if($value.Length -gt $MaxLength){return $value.Substring($value.Length-$MaxLength)}
return $value
}
function Get-ProcessFailureSummary([string]$StdErr,[string]$StdOut){
$parts=New-Object Collections.Generic.List[string]
$errorTail=Get-TailText $StdErr 1800
$outputTail=Get-TailText $StdOut 900
if($errorTail){$parts.Add("Fehlerausgabe:`n$errorTail")}
if($outputTail){$parts.Add("Letzte Standardausgabe:`n$outputTail")}
if($parts.Count -eq 0){return 'Der Prozess lieferte keine Diagnoseausgabe.'}
return ($parts -join "`n`n")
}
function Invoke-External{
param([string]$File,[string[]]$Arguments,[string]$WorkingDirectory=$PackageRoot,[int]$TimeoutSeconds=120,[switch]$AllowFailure,[string]$InputText='',[string]$Label='Prozess',[switch]$Quiet)
Assert-NotCancelled
$psi=New-Object Diagnostics.ProcessStartInfo;$psi.FileName=$File;$psi.Arguments=(@($Arguments|ForEach-Object{Quote-Arg ([string]$_)}) -join ' ');$psi.WorkingDirectory=$WorkingDirectory;$psi.UseShellExecute=$false;$psi.CreateNoWindow=$true;$psi.RedirectStandardOutput=$true;$psi.RedirectStandardError=$true;$psi.RedirectStandardInput=$true
# Node.js, npm and Git emit UTF-8 when redirected. Windows PowerShell otherwise decodes
# redirected streams with the active OEM code page, which corrupts German umlauts.
$utf8NoBom=New-Object Text.UTF8Encoding($false)
if($psi.PSObject.Properties.Name -contains 'StandardOutputEncoding'){$psi.StandardOutputEncoding=$utf8NoBom}
if($psi.PSObject.Properties.Name -contains 'StandardErrorEncoding'){$psi.StandardErrorEncoding=$utf8NoBom}
$psi.EnvironmentVariables['NO_COLOR']='1'
$psi.EnvironmentVariables['FORCE_COLOR']='0'
$psi.EnvironmentVariables['NPM_CONFIG_COLOR']='false'
$psi.EnvironmentVariables['NPM_CONFIG_UNICODE']='true'
$p=New-Object Diagnostics.Process;$p.StartInfo=$psi
if(-not $Quiet){Write-Event 'INFO' "$Label gestartet."}
if(-not $p.Start()){throw "$Label konnte nicht gestartet werden."}
$outTask = $p.StandardOutput.ReadToEndAsync()
$errTask = $p.StandardError.ReadToEndAsync()
if ($InputText) { $p.StandardInput.Write($InputText) }
$p.StandardInput.Close()
$sw=[Diagnostics.Stopwatch]::StartNew()
while(-not $p.WaitForExit(500)){
if(Test-Path -LiteralPath $CancelFile){Stop-Tree $p.Id;throw [System.OperationCanceledException]::new('Das Update wurde durch den Benutzer abgebrochen.')}
if($sw.Elapsed.TotalSeconds -gt $TimeoutSeconds){Stop-Tree $p.Id;throw "$Label hat das Zeitlimit von $TimeoutSeconds Sekunden überschritten."}
if(-not $Quiet -and ((Get-Date)-$script:LastHeartbeat).TotalSeconds -ge 8){Write-Event 'INFO' ("$Label läuft seit {0:mm\:ss}." -f $sw.Elapsed);$script:LastHeartbeat=Get-Date}
}
$p.WaitForExit();$stdout=[string]$outTask.Result;$stderr=[string]$errTask.Result;$code=$p.ExitCode;$p.Dispose()
if(-not $Quiet){
foreach($line in @($stdout -split "`r?`n")){if($line.Trim()){Write-Event 'OUTPUT' $line.Trim()}}
foreach($line in @($stderr -split "`r?`n")){
$clean=$line.Trim()
if(-not $clean){continue}
if($code -ne 0){Write-Event 'ERROR' $clean}
elseif($clean -match '(?i)\b(warn|warning|deprecated)\b'){Write-Event 'WARN' $clean}
else{Write-Event 'OUTPUT' $clean}
}
}
if(-not $AllowFailure -and $code -ne 0){$summary=Get-ProcessFailureSummary $stderr $stdout;throw "$Label ist mit Exit-Code $code fehlgeschlagen.`n$summary"}
return [pscustomobject]@{Code=$code;Output=$stdout;Error=$stderr}
}
function Resolve-Tool([string]$Name,[string[]]$Candidates=@()){
if($script:Tools.ContainsKey($Name) -and (Test-Path -LiteralPath $script:Tools[$Name])){return $script:Tools[$Name]}
foreach($c in @($Candidates)){if($c -and (Test-Path -LiteralPath $c -PathType Leaf)){$script:Tools[$Name]=(Resolve-Path $c).Path;return $script:Tools[$Name]}}
$cmd=Get-Command $Name -CommandType Application -ErrorAction SilentlyContinue|Select-Object -First 1;if($cmd -and $cmd.Source -and (Test-Path $cmd.Source)){$script:Tools[$Name]=$cmd.Source;return $cmd.Source};return $null
}
function Get-GitPath{return Resolve-Tool 'git.exe' @("$env:ProgramFiles\Git\cmd\git.exe","$env:LOCALAPPDATA\Programs\Git\cmd\git.exe")}
function Get-NodePath{return Resolve-Tool 'node.exe' @("$env:ProgramFiles\nodejs\node.exe","$env:LOCALAPPDATA\Programs\nodejs\node.exe")}
function Get-NpmPath{return Resolve-Tool 'npm.cmd' @("$env:ProgramFiles\nodejs\npm.cmd","$env:LOCALAPPDATA\Programs\nodejs\npm.cmd")}
function Invoke-WebJson([string]$Uri){for($i=1;$i-le3;$i++){try{return Invoke-RestMethod -Uri $Uri -Headers @{'User-Agent'="Vendoo-Updater/$UpdaterVersion";'Accept'='application/vnd.github+json'} -TimeoutSec 30}catch{if($i-eq3){throw};Start-Sleep -Seconds (2*$i)}}}
function Download-File([string]$Uri,[string]$Destination,[string]$Label){
for($attempt=1;$attempt-le3;$attempt++){try{
$handler = New-Object Net.Http.HttpClientHandler
$client = [Net.Http.HttpClient]::new($handler)
$client.Timeout=[TimeSpan]::FromMinutes(5);$client.DefaultRequestHeaders.UserAgent.ParseAdd("Vendoo-Updater/$UpdaterVersion")
$resp = $client.GetAsync($Uri,[Net.Http.HttpCompletionOption]::ResponseHeadersRead).Result
$null = $resp.EnsureSuccessStatusCode()
$total=$resp.Content.Headers.ContentLength;$input=$resp.Content.ReadAsStreamAsync().Result;$output=[IO.File]::Open($Destination,[IO.FileMode]::Create,[IO.FileAccess]::Write,[IO.FileShare]::None)
try{
$buf=New-Object byte[] 65536;$readTotal=0L;$lastReported=-10
while(($read=$input.Read($buf,0,$buf.Length))-gt0){
Assert-NotCancelled;$output.Write($buf,0,$read);$readTotal+=$read
if($total -and $total -gt 0){
$percent=[Math]::Min(100,[int][Math]::Floor(($readTotal*100.0)/$total))
if($percent -ge ($lastReported+10) -or $readTotal -ge $total){
$bucket=[Math]::Min(100,[int]([Math]::Floor($percent/10)*10))
if($bucket -gt $lastReported){
$lastReported=$bucket
Write-Event 'INFO' ("${Label}: {0}% ({1:N1} von {2:N1} MB)" -f $bucket,($readTotal/1048576),($total/1048576))
Write-State 18 'GitHub CLI' ("${Label} wird geladen: {0}%" -f $bucket)
}
}
}
}
}finally{$output.Dispose();$input.Dispose();$resp.Dispose();$client.Dispose();$handler.Dispose()}
if(-not $total){Write-Event 'INFO' "${Label}: Download abgeschlossen."}
return
}catch{if($attempt-eq3){throw "$Label konnte nach drei Versuchen nicht geladen werden: $($_.Exception.Message)"};Write-Event 'WARN' "${Label}: Downloadversuch $attempt fehlgeschlagen; erneuter Versuch.";Start-Sleep -Seconds (3*$attempt)}}
}
function Ensure-GitHubCli{
$managed=Join-Path $ToolsRoot 'github-cli\current\bin\gh.exe';if(Test-Path $managed){$test=Invoke-External $managed @('--version') -TimeoutSeconds 20 -AllowFailure -Label 'GitHub CLI prüfen';if($test.Code-eq0){$script:Tools['gh.exe']=$managed;return $managed}}
Write-State 18 'GitHub CLI' 'Die offizielle portable GitHub CLI wird sicher geladen und per SHA-256 geprüft.'
$release=Invoke-WebJson 'https://api.github.com/repos/cli/cli/releases/latest';$arch=if([Environment]::Is64BitOperatingSystem){'amd64'}else{'386'};$asset=@($release.assets|Where-Object{$_.name -like "gh_*_windows_$arch.zip"}|Select-Object -First 1);$checks=@($release.assets|Where-Object{$_.name -like '*_checksums.txt'}|Select-Object -First 1)
if(-not $asset -or -not $checks){throw 'Die offiziellen GitHub-CLI-Release-Dateien konnten nicht ermittelt werden.'}
$tmp=Join-Path $env:TEMP "vendoo-gh-$PID";New-Item -ItemType Directory -Path $tmp -Force|Out-Null;$zip=Join-Path $tmp $asset.name;$checkFile=Join-Path $tmp $checks.name;$extract=Join-Path $tmp 'extract'
try{Download-File $asset.browser_download_url $zip 'GitHub CLI';Download-File $checks.browser_download_url $checkFile 'Prüfsummen';$line=Get-Content $checkFile|Where-Object{$_ -match [regex]::Escape($asset.name)}|Select-Object -First 1;if(-not $line){throw 'Für das GitHub-CLI-Archiv wurde keine offizielle Prüfsumme gefunden.'};$expected=($line -split '\s+')[0].ToLowerInvariant();$actual=(Get-FileHash $zip -Algorithm SHA256).Hash.ToLowerInvariant();if($actual-ne$expected){throw 'SHA-256-Prüfung der GitHub CLI fehlgeschlagen.'};New-Item -ItemType Directory -Path $extract -Force|Out-Null;Expand-Archive $zip $extract -Force;$source=Get-ChildItem $extract -Filter gh.exe -File -Recurse|Select-Object -First 1;if(-not $source){throw 'Das geprüfte GitHub-CLI-Archiv enthält keine gh.exe.'};$targetRoot=Split-Path -Parent (Split-Path -Parent $managed);if(Test-Path $targetRoot){Remove-Item $targetRoot -Recurse -Force};New-Item -ItemType Directory -Path (Split-Path -Parent $managed) -Force|Out-Null;Copy-Item $source.FullName $managed -Force;$test=Invoke-External $managed @('--version') -TimeoutSeconds 20 -Label 'GitHub CLI validieren';$script:Tools['gh.exe']=$managed;Write-Event 'OK' "GitHub CLI $($release.tag_name) wurde geprüft und lokal bereitgestellt.";return $managed}finally{Remove-Item $tmp -Recurse -Force -ErrorAction SilentlyContinue}
}
function Test-GitHubAuthentication([string]$GhPath){
$status=Invoke-External $GhPath @('auth','status','--hostname','github.com') -TimeoutSeconds 20 -AllowFailure -Label 'GitHub-Anmeldung prüfen' -Quiet
return ($status.Code -eq 0)
}
function Invoke-VisibleGitHubLogin([string]$GhPath){
$runtimeDirectory=Split-Path -Parent $StatusFile
$loginScript=Join-Path $runtimeDirectory 'github-login.cmd'
$escapedGh=$GhPath.Replace('"','""')
$lines=@(
'@echo off',
'chcp 65001 >nul',
'title Vendoo - GitHub Anmeldung',
'echo ============================================================',
'echo Vendoo - Sichere GitHub Anmeldung',
'echo ============================================================',
'echo.',
'echo Der einmalige GitHub-Code wird angezeigt und in die',
'echo Zwischenablage kopiert. Folge danach dem Browserdialog.',
'echo Dieses Fenster schliesst sich nach erfolgreicher Anmeldung.',
'echo.',
('"{0}" auth login --hostname github.com --git-protocol https --web --clipboard' -f $escapedGh),
'set "VENDOO_GH_LOGIN_EXIT=%ERRORLEVEL%"',
'echo.',
'if "%VENDOO_GH_LOGIN_EXIT%"=="0" (',
' echo GitHub-Anmeldung erfolgreich. Vendoo faehrt automatisch fort.',
') else (',
' echo GitHub-Anmeldung wurde nicht abgeschlossen. Fehlercode: %VENDOO_GH_LOGIN_EXIT%',
' echo Das Fenster kann geschlossen werden; der Vendoo Updater bleibt geoeffnet.',
' pause',
')',
'exit /b %VENDOO_GH_LOGIN_EXIT%'
)
[IO.File]::WriteAllLines($loginScript,$lines,(New-Object Text.UTF8Encoding($false)))
Write-State 26 'GitHub-Anmeldung' 'Im geöffneten Anmeldefenster den Gerätecode bestätigen und den Browserdialog abschließen. Danach geht es automatisch weiter.' 'waiting'
Write-Event 'WAIT' 'Sichtbares GitHub-Anmeldefenster wird geöffnet.'
try{
$loginProcess=Start-Process -FilePath 'cmd.exe' -ArgumentList @('/d','/c',('"{0}"' -f $loginScript)) -WorkingDirectory $runtimeDirectory -WindowStyle Normal -PassThru
}catch{
throw "Das sichtbare GitHub-Anmeldefenster konnte nicht gestartet werden: $($_.Exception.Message)"
}
$deadline=(Get-Date).AddMinutes(10);$lastStatus=Get-Date
while((Get-Date)-lt$deadline){
Assert-NotCancelled
if(Test-GitHubAuthentication $GhPath){
Write-Event 'OK' 'GitHub-Anmeldung wurde bestätigt.'
try{if(-not $loginProcess.HasExited){$loginProcess.CloseMainWindow()|Out-Null}}catch{}
return
}
if($loginProcess.HasExited){
Start-Sleep -Seconds 1
if(Test-GitHubAuthentication $GhPath){Write-Event 'OK' 'GitHub-Anmeldung wurde bestätigt.';return}
throw "Die GitHub-Anmeldung wurde nicht abgeschlossen. Das Anmeldefenster endete mit Exit-Code $($loginProcess.ExitCode)."
}
if(((Get-Date)-$lastStatus).TotalSeconds -ge 15){
Write-Event 'INFO' 'GitHub-Anmeldung wartet auf Abschluss im Browser.'
$lastStatus=Get-Date
}
Start-Sleep -Seconds 2
}
try{if(-not $loginProcess.HasExited){Stop-Tree $loginProcess.Id}}catch{}
throw 'Die GitHub-Anmeldung wurde nicht innerhalb von zehn Minuten abgeschlossen.'
}
function Check-ProductionVersion{try{$r=Invoke-RestMethod -Uri "$PublicBaseUrl/readyz?updater=$Stamp" -TimeoutSec 20 -Headers @{'Cache-Control'='no-cache';'User-Agent'="Vendoo-Updater/$UpdaterVersion"};return [pscustomobject]@{Ready=[bool]$r.ok;Version=[string]$r.version}}catch{return [pscustomobject]@{Ready=$false;Version='unbekannt'}}}
function Assert-Package{foreach($f in @('package.json','package-lock.json','update-manifest.json','release-manifest.json','db\schema.sql','tools\apply-release-manifest.mjs')){if(-not(Test-Path(Join-Path $PackageRoot $f))){throw "Paketdatei fehlt: $f"}};$p=Get-Content (Join-Path $PackageRoot 'package.json') -Raw -Encoding UTF8|ConvertFrom-Json;$m=Get-Content (Join-Path $PackageRoot 'update-manifest.json') -Raw -Encoding UTF8|ConvertFrom-Json;$r=Get-Content (Join-Path $PackageRoot 'release-manifest.json') -Raw -Encoding UTF8|ConvertFrom-Json;if([string]$p.version-ne$TargetVersion -or [string]$m.version-ne$TargetVersion -or [string]$r.version-ne$TargetVersion){throw "Paket-, Update- und Release-Manifest-Version müssen exakt $TargetVersion sein."};if([string]$r.updaterVersion-ne$UpdaterVersion){throw "Release-Manifest gehört zu Updater $($r.updaterVersion), erwartet wird $UpdaterVersion."};if([string]$r.mode-ne'overlay-with-explicit-removals'){throw "Unzulässiger Release-Modus: $($r.mode)"}}
try{
Write-Event 'INFO' "Vendoo Production Updater $UpdaterVersion gestartet. Zielversion $TargetVersion."
$session=Load-Session
Set-Step 'package' 'active';Write-State 3 'Paketprüfung' 'Version, Manifest und sicherheitsrelevante Paketdateien werden geprüft.';Assert-Package;if($SelfTestFailAt-eq'package'){throw 'Simulierter Paketfehler.'};Complete-Step 'package';Write-Event 'OK' 'Paketprüfung erfolgreich.'
Set-Step 'tools' 'active';Write-State 9 'Systemprüfung' 'Git, Node.js und npm werden mit festen Zeitlimits geprüft.'
$git=Get-GitPath;if(-not$git){throw 'Git für Windows wurde nicht gefunden. Bitte Git für Windows installieren; danach kann derselbe Updater erneut gestartet werden.'};$node=Get-NodePath;$npm=Get-NpmPath;if(-not$node -or -not$npm){throw 'Node.js 22 LTS oder npm wurde nicht gefunden.'};$gv=Invoke-External $git @('--version') -TimeoutSeconds 20 -Label 'Git prüfen';$nv=Invoke-External $node @('--version') -TimeoutSeconds 20 -Label 'Node.js prüfen';if($nv.Output -notmatch 'v?(\d+)\.' -or [int]$Matches[1]-lt22){throw "Node.js 22 oder neuer ist erforderlich. Gefunden: $($nv.Output.Trim())"};$gh=Ensure-GitHubCli;Complete-Step 'tools';Write-Event 'OK' 'Systemprüfung erfolgreich.'
Set-Step 'github' 'active';Write-State 24 'GitHub-Verbindung' 'Anmeldung und Repository-Berechtigungen werden geprüft.'
if(-not (Test-GitHubAuthentication $gh)){Invoke-VisibleGitHubLogin $gh}
Invoke-External $gh @('auth','setup-git','--hostname','github.com') -TimeoutSeconds 60 -Label 'Git-Anmeldung konfigurieren'|Out-Null
$repo=Invoke-External $gh @('repo','view',$Repository,'--json','viewerPermission,defaultBranchRef,isPrivate') -TimeoutSeconds 60 -Label 'Repository-Zugriff prüfen';$repoObj=$repo.Output|ConvertFrom-Json;if([string]$repoObj.viewerPermission -notin @('ADMIN','MAINTAIN','WRITE')){throw "Für $Repository fehlt Schreibzugriff. GitHub meldet $($repoObj.viewerPermission)."};Complete-Step 'github';Write-Event 'OK' "GitHub-Zugriff bestätigt: $($repoObj.viewerPermission)."
Set-Step 'production' 'active';Write-State 32 'Produktionsvorbereitung' 'Aktueller Produktionsstand und Coolify Auto Deploy werden geprüft.'
$prod=Check-ProductionVersion;Write-Event 'INFO' "Produktion meldet ready=$($prod.Ready), version=$($prod.Version)."
if(-not$SkipProductionDeploy){Write-Event 'OK' 'Kein Coolify-Webhook erforderlich: Nach dem Merge wird das bestehende Coolify Auto Deploy der GitHub-Verknüpfung verwendet.'}
Complete-Step 'production'
if($SelfTest){foreach($id in @('workspace','tests','pullrequest','merge','deploy','verify')){Set-Step $id 'active';Write-State 50 "Selbsttest: $id" 'Darstellung, Statuswechsel und Protokoll werden simuliert.';Start-Sleep -Milliseconds 350;Complete-Step $id};Write-State 100 'Selbsttest erfolgreich' 'Die Updater-Oberfläche und der Worker-Vertrag funktionieren.' 'success';return}
if([bool]$session.merged){Write-Event 'INFO' 'Gespeicherter Wiederaufnahmepunkt: main wurde bereits aktualisiert.';foreach($id in @('workspace','tests','pullrequest','merge')){Complete-Step $id}}
else {
Set-Step 'workspace' 'active'
Write-State 40 'Arbeitskopie' 'main wird isoliert geklont; das vollständige Release-Manifest wird ohne Leeren der Baseline angewendet.'
if (Test-Path -LiteralPath $WorkRoot) { Remove-Item -LiteralPath $WorkRoot -Recurse -Force }
New-Item -ItemType Directory -Path $WorkRoot -Force | Out-Null
Invoke-External $gh @('repo','clone',$Repository,$CloneDir,'--','--branch',$BaseBranch,'--single-branch') -WorkingDirectory $WorkRoot -TimeoutSeconds 300 -Label 'Repository klonen' | Out-Null
Invoke-External $git @('switch','-c',$Branch) -WorkingDirectory $CloneDir -TimeoutSeconds 60 -Label 'Release-Branch erstellen' | Out-Null
Invoke-External $node @((Join-Path $PackageRoot 'tools\apply-release-manifest.mjs'),$PackageRoot,$CloneDir) -TimeoutSeconds 300 -Label 'SHA-256-geprüftes Release-Manifest anwenden' | Out-Null
Complete-Step 'workspace'
Set-Step 'tests' 'active'
Write-State 52 'Abnahmetests' 'Abhängigkeiten werden reproduzierbar installiert; danach läuft der vollständige verify:all-Vertrag.'
Invoke-External $npm @('ci','--no-audit','--no-fund') -WorkingDirectory $CloneDir -TimeoutSeconds 1200 -Label 'npm ci' | Out-Null
Write-State 61 'Abnahmetests' 'Alle Vendoo-Sicherheits-, Datenbank-, Modul- und Release-Gates laufen.'
Invoke-External $npm @('run','verify:all') -WorkingDirectory $CloneDir -TimeoutSeconds 3600 -Label 'npm run verify:all' | Out-Null
Complete-Step 'tests'
Write-Event 'OK' 'Alle lokalen Abnahmetests sind grün.'
Set-Step 'pullrequest' 'active'
Write-State 70 'Pull Request' 'Der geprüfte Stand wird committed und in einen kontrollierten Pull Request übertragen.'
Invoke-External $git @('config','user.name','Vendoo Production Updater') -WorkingDirectory $CloneDir -Label 'Git-Name setzen' | Out-Null
$uid = (Invoke-External $gh @('api','user','--jq','.id') -Label 'GitHub-Benutzer-ID').Output.Trim()
$ulogin = (Invoke-External $gh @('api','user','--jq','.login') -Label 'GitHub-Benutzername').Output.Trim()
Invoke-External $git @('config','user.email',"$uid+$ulogin@users.noreply.github.com") -WorkingDirectory $CloneDir -Label 'Git-E-Mail setzen' | Out-Null
Invoke-External $git @('add','--all') -WorkingDirectory $CloneDir -Label 'Änderungen vormerken' | Out-Null
$diff = Invoke-External $git @('diff','--cached','--quiet') -WorkingDirectory $CloneDir -AllowFailure -Label 'Änderungen prüfen'
if ($diff.Code -eq 0) {
Write-Event 'INFO' 'main enthält bereits denselben Source-Stand.'
$session.merged = $true
$session.stage = 'merged'
Save-Session $session
Complete-Step 'pullrequest'
Complete-Step 'merge'
}
elseif ($diff.Code -eq 1) {
Invoke-External $git @('commit','-m',"release: Vendoo $TargetVersion production update") -WorkingDirectory $CloneDir -TimeoutSeconds 120 -Label 'Release committen' | Out-Null
Invoke-External $git @('push','--force','--set-upstream','origin',$Branch) -WorkingDirectory $CloneDir -TimeoutSeconds 300 -Label 'Release-Branch pushen' | Out-Null
$existing = Invoke-External $gh @('pr','list','--repo',$Repository,'--head',$Branch,'--state','open','--json','url','--jq','.[0].url') -AllowFailure -Label 'Vorhandenen Pull Request suchen'
$prUrl = $existing.Output.Trim()
if (-not $prUrl) {
$body = Join-Path $WorkRoot 'pr-body.md'
$bodyText = @"
## Vendoo $TargetVersion Production Update
Erstellt durch Vendoo Production Updater $UpdaterVersion.
- vollständiges `npm run verify:all` erfolgreich
- keine Datenbanken, Uploads, Secrets oder `/app/data`-Inhalte
- Coolify Auto Deploy mit exakter Versionsprüfung
"@
$bodyText | Set-Content -LiteralPath $body -Encoding UTF8
$prUrl = (Invoke-External $gh @('pr','create','--repo',$Repository,'--base',$BaseBranch,'--head',$Branch,'--title',"Vendoo $TargetVersion Production Update",'--body-file',$body) -WorkingDirectory $CloneDir -TimeoutSeconds 180 -Label 'Pull Request erstellen').Output.Trim()
}
$session.prUrl = $prUrl
$session.stage = 'pr'
Save-Session $session
Complete-Step 'pullrequest'
Write-Event 'OK' "Pull Request: $prUrl"
Set-Step 'merge' 'active'
Write-State 78 'GitHub-Prüfungen' 'Alle gemeldeten GitHub-Checks werden abgewartet.'
$rollup = Invoke-External $gh @('pr','view',$prUrl,'--repo',$Repository,'--json','statusCheckRollup') -Label 'GitHub-Checks ermitteln'
$rollupObj = $rollup.Output | ConvertFrom-Json
if (@($rollupObj.statusCheckRollup).Count -gt 0) {
Invoke-External $gh @('pr','checks',$prUrl,'--repo',$Repository,'--watch','--fail-fast') -WorkingDirectory $CloneDir -TimeoutSeconds 2700 -Label 'GitHub-Checks abwarten' | Out-Null
}
Write-State 82 'Merge' 'Der vollständig geprüfte Pull Request wird per Squash nach main übernommen.'
Invoke-External $gh @('pr','merge',$prUrl,'--repo',$Repository,'--squash','--delete-branch','--subject',"Vendoo $TargetVersion Production Update") -WorkingDirectory $CloneDir -TimeoutSeconds 300 -Label 'Pull Request mergen' | Out-Null
$session.merged = $true
$session.stage = 'merged'
Save-Session $session
Complete-Step 'merge'
Write-Event 'OK' 'main wurde kontrolliert aktualisiert.'
}
else {
throw "Git-Diff-Prüfung lieferte unerwarteten Exit-Code $($diff.Code)."
}
}
if($SkipProductionDeploy){foreach($id in @('deploy','verify')){foreach($s in $script:Steps){if($s.id-eq$id){$s.state='skipped'}}};Write-State 100 'Source aktualisiert' 'GitHub wurde aktualisiert; das Produktionsdeployment wurde auf Wunsch übersprungen.' 'success';return}
Set-Step 'deploy' 'active';Write-State 87 'Coolify Auto Deploy' 'main wurde aktualisiert. Coolify übernimmt das Deployment über die bestehende GitHub-Verknüpfung.'
$session.stage='deploy';Save-Session $session;Write-Event 'INFO' 'Warte auf Coolify Auto Deploy. Es wird kein Webhook und kein GitHub Secret benötigt.'
$deadline=(Get-Date).AddMinutes(15);$last='';$nextLog=Get-Date
do{
Assert-NotCancelled
$prod=Check-ProductionVersion
$last="ready=$($prod.Ready), version=$($prod.Version)"
if((Get-Date)-ge$nextLog){Write-Event 'INFO' "Coolify Auto Deploy: $last";$nextLog=(Get-Date).AddSeconds(30)}
if($prod.Ready -and $prod.Version-eq$TargetVersion){break}
Start-Sleep -Seconds 10
}while((Get-Date)-lt$deadline)
if(-not($prod.Ready -and $prod.Version-eq$TargetVersion)){throw "Coolify hat die Zielversion innerhalb von 15 Minuten nicht bereitgestellt. Letzter Stand: $last. In Coolify bei der Vendoo-Anwendung unter Advanced prüfen, ob Auto Deploy aktiviert ist."}
Complete-Step 'deploy'
Set-Step 'verify' 'active';Write-State 97 'Produktionsprüfung' 'Die öffentlich erreichbare Readiness und die exakte Zielversion werden abschließend bestätigt.'
$prod=Check-ProductionVersion
if(-not($prod.Ready -and $prod.Version-eq$TargetVersion)){throw "Die abschließende Produktionsprüfung ist fehlgeschlagen. Stand: ready=$($prod.Ready), version=$($prod.Version)"}
Complete-Step 'verify';$session.stage='complete';Save-Session $session;Write-State 100 'Update erfolgreich' "Vendoo $TargetVersion läuft geprüft in Produktion." 'success';Write-Event 'OK' "Vendoo $TargetVersion wurde vollständig aktualisiert.";return
}catch [System.OperationCanceledException]{Fail-Step;Write-State 100 'Update abgebrochen' $_.Exception.Message 'cancelled' $_.Exception.Message 'Der gespeicherte Wiederaufnahmepunkt bleibt erhalten. Mit „Erneut versuchen“ kann der sichere Ablauf fortgesetzt werden.';Write-Event 'WARN' $_.Exception.Message;return
}catch{Fail-Step;$msg=$_.Exception.Message;$remediation=if ($script:CurrentStep -eq 'tools'){'Systemvoraussetzung korrigieren und „Erneut versuchen“ wählen. Es wurden noch keine GitHub- oder Produktionsänderungen vorgenommen.'}elseif ($script:CurrentStep -in @('deploy','verify')){'main kann bereits aktualisiert sein. „Erneut versuchen“ setzt beim Deployment beziehungsweise der Produktionsprüfung fort.'}else{'„Erneut versuchen“ verwendet den gespeicherten Stand. Vor einem erfolgreichen Merge bleibt main unverändert.'};Write-State 100 'Update sicher gestoppt' $msg 'failed' $msg $remediation;Write-Event 'ERROR' $msg;return
}finally{if(Test-Path $InputFile){Remove-Item $InputFile -Force -ErrorAction SilentlyContinue}}
+155
View File
@@ -0,0 +1,155 @@
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)][string]$StatusFile,
[Parameter(Mandatory=$true)][string]$EventFile,
[Parameter(Mandatory=$true)][string]$InputFile,
[Parameter(Mandatory=$true)][string]$CancelFile,
[Parameter(Mandatory=$true)][string]$SessionFile,
[Parameter(Mandatory=$true)][string]$LogFile,
[switch]$SkipProductionDeploy,
[switch]$SelfTest,
[string]$SelfTestFailAt = ''
)
Set-StrictMode -Version 2.0
$ErrorActionPreference='Stop';$ProgressPreference='SilentlyContinue'
Add-Type -AssemblyName System.Net.Http
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$PackageRoot=(Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..\..') -ErrorAction Stop).ProviderPath.TrimEnd([System.IO.Path]::DirectorySeparatorChar,[System.IO.Path]::AltDirectorySeparatorChar)
$Repository='Masterluke77/vendoo';$BaseBranch='main';$TargetVersion='1.43.0';$UpdaterVersion='3.4.0';$PublicBaseUrl='https://vendoo.flatlined.de'
$Branch="release/$TargetVersion-production-update-v4"
$LocalRoot=Join-Path $env:LOCALAPPDATA 'Vendoo\Updater';$ToolsRoot=Join-Path $LocalRoot 'tools';$LogsRoot=Join-Path $LocalRoot 'logs';$WorkRoot=Join-Path $LocalRoot "work\$TargetVersion-v4";$CloneDir=Join-Path $WorkRoot 'repository'
New-Item -ItemType Directory -Path $ToolsRoot,$LogsRoot,(Split-Path -Parent $SessionFile) -Force|Out-Null
$Stamp=Get-Date -Format 'yyyyMMdd-HHmmss';$script:Tools=@{};$script:CurrentStep='package';$script:LastHeartbeat=Get-Date;$script:LastCheckLog=[DateTime]::MinValue
$LifecycleScript=Join-Path $PackageRoot 'tools\updater-lifecycle-3.4.mjs'
$PayloadGuardScript=Join-Path $PackageRoot 'tools\check-release-payload-3.4.mjs'
$script:Steps=@(
[ordered]@{id='package';title='Paket prüfen';state='pending'},[ordered]@{id='tools';title='System prüfen';state='pending'},[ordered]@{id='github';title='GitHub verbinden';state='pending'},
[ordered]@{id='production';title='Zielzustand erkennen';state='pending'},[ordered]@{id='workspace';title='Payload sicher vorbereiten';state='pending'},[ordered]@{id='tests';title='Vollständig testen';state='pending'},
[ordered]@{id='pullrequest';title='Pull Request abgleichen';state='pending'},[ordered]@{id='merge';title='Finalen Head prüfen und mergen';state='pending'},[ordered]@{id='deploy';title='Coolify deployen';state='pending'},[ordered]@{id='verify';title='Produktion bestätigen';state='pending'}
)
function Write-Event([string]$Level,[string]$Message){
$safe=$Message -replace '(?i)(https://[^\s]*(?:token|webhook)[^\s]*)','[VERDECKT]'
$entry=[ordered]@{at=(Get-Date).ToString('o');level=$Level.ToUpperInvariant();message=$safe}|ConvertTo-Json -Compress
[IO.File]::AppendAllText($EventFile,$entry+[Environment]::NewLine,(New-Object Text.UTF8Encoding($false)))
[IO.File]::AppendAllText($LogFile,"$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [$($Level.ToUpperInvariant())] $safe$([Environment]::NewLine)",(New-Object Text.UTF8Encoding($false)))
}
function Write-State([int]$Percent,[string]$Phase,[string]$Detail,[string]$State='running',[string]$Error='',[string]$Remediation='', $Request=$null){
$payload=[ordered]@{version=$TargetVersion;updaterVersion=$UpdaterVersion;percent=[Math]::Max(0,[Math]::Min(100,$Percent));phase=$Phase;detail=$Detail;state=$State;error=$Error;remediation=$Remediation;updatedAt=(Get-Date).ToString('o');logFile=$LogFile;sessionFile=$SessionFile;steps=$script:Steps;request=$Request}|ConvertTo-Json -Depth 10
$tmp="$StatusFile.$PID.tmp";[IO.File]::WriteAllText($tmp,$payload,(New-Object Text.UTF8Encoding($false)));Move-Item -LiteralPath $tmp -Destination $StatusFile -Force
}
function Set-Step([string]$Id,[string]$State){foreach($s in $script:Steps){if($s.id -eq $Id){$s.state=$State}elseif($State -eq 'active' -and $s.state -eq 'active'){$s.state='pending'}};$script:CurrentStep=$Id}
function Complete-Step([string]$Id){foreach($s in $script:Steps){if($s.id -eq $Id){$s.state='done'}}}
function Skip-Step([string]$Id){foreach($s in $script:Steps){if($s.id -eq $Id){$s.state='skipped'}}}
function Fail-Step{foreach($s in $script:Steps){if($s.id -eq $script:CurrentStep){$s.state='failed'}}}
function Assert-NotCancelled{if(Test-Path -LiteralPath $CancelFile){throw [System.OperationCanceledException]::new('Das Update wurde durch den Benutzer abgebrochen.')}}
function Add-OrSetProperty($Object,[string]$Name,$Value){if($Object.PSObject.Properties.Name -contains $Name){$Object.$Name=$Value}else{$Object|Add-Member -NotePropertyName $Name -NotePropertyValue $Value}}
function Save-Session($Data){
Add-OrSetProperty $Data 'schema' 'vendoo-updater-session-v2';Add-OrSetProperty $Data 'repository' $Repository;Add-OrSetProperty $Data 'targetVersion' $TargetVersion;Add-OrSetProperty $Data 'updaterVersion' $UpdaterVersion;Add-OrSetProperty $Data 'payloadHash' $script:Manifest.payloadHash;Add-OrSetProperty $Data 'branch' $Branch;Add-OrSetProperty $Data 'updatedAt' (Get-Date).ToString('o')
$tmp="$SessionFile.tmp";$json=$Data|ConvertTo-Json -Depth 10;[IO.File]::WriteAllText($tmp,$json,(New-Object Text.UTF8Encoding($false)));Move-Item $tmp $SessionFile -Force
}
function New-Session{return [pscustomobject]@{schema='vendoo-updater-session-v2';repository=$Repository;targetVersion=$TargetVersion;updaterVersion=$UpdaterVersion;payloadHash=[string]$script:Manifest.payloadHash;branch=$Branch;releaseState='new';prNumber=$null;prUrl='';finalPrHeadSha='';mergeCommit='';productionVersion='';completedAt='';updatedAt=''}}
function Load-Session{
if(Test-Path -LiteralPath $SessionFile){try{$saved=Get-Content $SessionFile -Raw -Encoding UTF8|ConvertFrom-Json;if([string]$saved.schema-eq'vendoo-updater-session-v2' -and [string]$saved.repository-eq$Repository -and [string]$saved.targetVersion-eq$TargetVersion -and [string]$saved.payloadHash-eq[string]$script:Manifest.payloadHash){return $saved};Write-Event 'INFO' 'Veralteter oder anderer Updater-Sitzungsstand wird nicht übernommen.'}catch{Write-Event 'WARN' 'Gespeicherter Sitzungsstand ist ungültig und wird verworfen.'}}
return New-Session
}
function Quote-Arg([string]$Value){if($null -eq $Value){return '""'};if($Value -notmatch '[\s"]'){return $Value};return '"'+($Value -replace '(\\*)"','$1$1\"' -replace '(\\+)$','$1$1')+'"'}
function Stop-Tree([int]$ProcessId){try{& taskkill.exe /PID $ProcessId /T /F 2>$null|Out-Null}catch{try{Stop-Process -Id $ProcessId -Force -ErrorAction SilentlyContinue}catch{}}}
function Get-TailText([string]$Text,[int]$MaxLength){if([string]::IsNullOrWhiteSpace($Text)){return ''};$value=$Text.Trim();if($value.Length -gt $MaxLength){return $value.Substring($value.Length-$MaxLength)};return $value}
function Get-ProcessFailureSummary([string]$StdErr,[string]$StdOut){$parts=New-Object Collections.Generic.List[string];$e=Get-TailText $StdErr 2200;$o=Get-TailText $StdOut 1200;if($e){$parts.Add("Fehlerausgabe:`n$e")};if($o){$parts.Add("Letzte Standardausgabe:`n$o")};if($parts.Count-eq0){return 'Der Prozess lieferte keine Diagnoseausgabe.'};return($parts-join"`n`n")}
function Invoke-External{
param([string]$File,[string[]]$Arguments,[string]$WorkingDirectory=$PackageRoot,[int]$TimeoutSeconds=120,[switch]$AllowFailure,[string]$InputText='',[string]$Label='Prozess',[switch]$Quiet)
Assert-NotCancelled
$psi=New-Object Diagnostics.ProcessStartInfo;$psi.FileName=$File;$psi.Arguments=(@($Arguments|ForEach-Object{Quote-Arg([string]$_)})-join' ');$psi.WorkingDirectory=$WorkingDirectory;$psi.UseShellExecute=$false;$psi.CreateNoWindow=$true;$psi.RedirectStandardOutput=$true;$psi.RedirectStandardError=$true;$psi.RedirectStandardInput=$true
$utf8=New-Object Text.UTF8Encoding($false);if($psi.PSObject.Properties.Name-contains'StandardOutputEncoding'){$psi.StandardOutputEncoding=$utf8};if($psi.PSObject.Properties.Name-contains'StandardErrorEncoding'){$psi.StandardErrorEncoding=$utf8}
$psi.EnvironmentVariables['NO_COLOR']='1';$psi.EnvironmentVariables['FORCE_COLOR']='0';$psi.EnvironmentVariables['NPM_CONFIG_COLOR']='false';$psi.EnvironmentVariables['NPM_CONFIG_UNICODE']='true'
$p=New-Object Diagnostics.Process;$p.StartInfo=$psi;if(-not$Quiet){Write-Event 'INFO' "$Label gestartet."};if(-not$p.Start()){throw "$Label konnte nicht gestartet werden."};$outTask=$p.StandardOutput.ReadToEndAsync();$errTask=$p.StandardError.ReadToEndAsync();if($InputText){$p.StandardInput.Write($InputText)};$p.StandardInput.Close();$sw=[Diagnostics.Stopwatch]::StartNew()
while(-not$p.WaitForExit(500)){if(Test-Path -LiteralPath $CancelFile){Stop-Tree $p.Id;throw [System.OperationCanceledException]::new('Das Update wurde durch den Benutzer abgebrochen.')};if($sw.Elapsed.TotalSeconds-gt$TimeoutSeconds){Stop-Tree $p.Id;throw "$Label hat das Zeitlimit von $TimeoutSeconds Sekunden überschritten."};if(-not$Quiet-and((Get-Date)-$script:LastHeartbeat).TotalSeconds-ge8){Write-Event 'INFO' ("$Label läuft seit {0:mm\:ss}."-f$sw.Elapsed);$script:LastHeartbeat=Get-Date}}
$p.WaitForExit();$stdout=[string]$outTask.Result;$stderr=[string]$errTask.Result;$code=$p.ExitCode;$p.Dispose()
if(-not$Quiet){foreach($line in @($stdout-split"`r?`n")){if($line.Trim()){Write-Event 'OUTPUT' $line.Trim()}};foreach($line in @($stderr-split"`r?`n")){$clean=$line.Trim();if(-not$clean){continue};if($code-ne0){Write-Event 'ERROR' $clean}elseif($clean-match'(?i)\b(warn|warning|deprecated)\b'){Write-Event 'WARN' $clean}else{Write-Event 'OUTPUT' $clean}}}
if(-not$AllowFailure-and$code-ne0){throw "$Label ist mit Exit-Code $code fehlgeschlagen.`n$(Get-ProcessFailureSummary $stderr $stdout)"};return[pscustomobject]@{Code=$code;Output=$stdout;Error=$stderr}
}
function Resolve-Tool([string]$Name,[string[]]$Candidates=@()){if($script:Tools.ContainsKey($Name)-and(Test-Path -LiteralPath $script:Tools[$Name])){return$script:Tools[$Name]};foreach($c in @($Candidates)){if($c-and(Test-Path -LiteralPath $c -PathType Leaf)){$script:Tools[$Name]=(Resolve-Path $c).Path;return$script:Tools[$Name]}};$cmd=Get-Command $Name -CommandType Application -ErrorAction SilentlyContinue|Select-Object -First 1;if($cmd-and$cmd.Source-and(Test-Path $cmd.Source)){$script:Tools[$Name]=$cmd.Source;return$cmd.Source};return$null}
function Get-GitPath{return Resolve-Tool 'git.exe' @("$env:ProgramFiles\Git\cmd\git.exe","$env:LOCALAPPDATA\Programs\Git\cmd\git.exe")}
function Get-NodePath{return Resolve-Tool 'node.exe' @("$env:ProgramFiles\nodejs\node.exe","$env:LOCALAPPDATA\Programs\nodejs\node.exe")}
function Get-NpmPath{return Resolve-Tool 'npm.cmd' @("$env:ProgramFiles\nodejs\npm.cmd","$env:LOCALAPPDATA\Programs\nodejs\npm.cmd")}
function Invoke-WebJson([string]$Uri){for($i=1;$i-le3;$i++){try{return Invoke-RestMethod -Uri $Uri -Headers @{'User-Agent'="Vendoo-Updater/$UpdaterVersion";'Accept'='application/vnd.github+json'} -TimeoutSec 30}catch{if($i-eq3){throw};Start-Sleep -Seconds(2*$i)}}}
function Download-File([string]$Uri,[string]$Destination,[string]$Label){for($attempt=1;$attempt-le3;$attempt++){try{$handler=New-Object Net.Http.HttpClientHandler;$client=[Net.Http.HttpClient]::new($handler);$client.Timeout=[TimeSpan]::FromMinutes(5);$client.DefaultRequestHeaders.UserAgent.ParseAdd("Vendoo-Updater/$UpdaterVersion");$resp=$client.GetAsync($Uri,[Net.Http.HttpCompletionOption]::ResponseHeadersRead).Result;$null=$resp.EnsureSuccessStatusCode();$input=$resp.Content.ReadAsStreamAsync().Result;$output=[IO.File]::Open($Destination,[IO.FileMode]::Create,[IO.FileAccess]::Write,[IO.FileShare]::None);try{$buf=New-Object byte[] 65536;while(($read=$input.Read($buf,0,$buf.Length))-gt0){Assert-NotCancelled;$output.Write($buf,0,$read)}}finally{$output.Dispose();$input.Dispose();$resp.Dispose();$client.Dispose();$handler.Dispose()};return}catch{if($attempt-eq3){throw "$Label konnte nach drei Versuchen nicht geladen werden: $($_.Exception.Message)"};Start-Sleep -Seconds(3*$attempt)}}}
function Ensure-GitHubCli{
$managed=Join-Path $ToolsRoot 'github-cli\current\bin\gh.exe';if(Test-Path $managed){$test=Invoke-External $managed @('--version') -TimeoutSeconds 20 -AllowFailure -Label 'GitHub CLI prüfen' -Quiet;if($test.Code-eq0){$script:Tools['gh.exe']=$managed;return$managed}}
Write-State 18 'GitHub CLI' 'Die offizielle portable GitHub CLI wird sicher geladen und per SHA-256 geprüft.';$release=Invoke-WebJson 'https://api.github.com/repos/cli/cli/releases/latest';$arch=if([Environment]::Is64BitOperatingSystem){'amd64'}else{'386'};$asset=@($release.assets|Where-Object{$_.name-like"gh_*_windows_$arch.zip"}|Select-Object -First 1);$checks=@($release.assets|Where-Object{$_.name-like'*_checksums.txt'}|Select-Object -First 1);if(-not$asset-or-not$checks){throw'Die offiziellen GitHub-CLI-Release-Dateien konnten nicht ermittelt werden.'}
$tmp=Join-Path $env:TEMP "vendoo-gh-$PID";New-Item -ItemType Directory -Path $tmp -Force|Out-Null;$zip=Join-Path $tmp $asset.name;$checkFile=Join-Path $tmp $checks.name;$extract=Join-Path $tmp 'extract'
try{Download-File $asset.browser_download_url $zip 'GitHub CLI';Download-File $checks.browser_download_url $checkFile 'Prüfsummen';$line=Get-Content $checkFile|Where-Object{$_-match[regex]::Escape($asset.name)}|Select-Object -First 1;if(-not$line){throw'Für das GitHub-CLI-Archiv wurde keine offizielle Prüfsumme gefunden.'};$expected=($line-split'\s+')[0].ToLowerInvariant();$actual=(Get-FileHash $zip -Algorithm SHA256).Hash.ToLowerInvariant();if($actual-ne$expected){throw'SHA-256-Prüfung der GitHub CLI fehlgeschlagen.'};New-Item -ItemType Directory -Path $extract -Force|Out-Null;Expand-Archive $zip $extract -Force;$source=Get-ChildItem $extract -Filter gh.exe -File -Recurse|Select-Object -First 1;if(-not$source){throw'Das geprüfte GitHub-CLI-Archiv enthält keine gh.exe.'};$targetRoot=Split-Path -Parent(Split-Path -Parent $managed);if(Test-Path $targetRoot){Remove-Item $targetRoot -Recurse -Force};New-Item -ItemType Directory -Path(Split-Path -Parent $managed)-Force|Out-Null;Copy-Item $source.FullName $managed -Force;$null=Invoke-External $managed @('--version') -TimeoutSeconds 20 -Label 'GitHub CLI validieren';$script:Tools['gh.exe']=$managed;Write-Event 'OK' "GitHub CLI $($release.tag_name) wurde geprüft und lokal bereitgestellt.";return$managed}finally{Remove-Item $tmp -Recurse -Force -ErrorAction SilentlyContinue}
}
function Test-GitHubAuthentication([string]$GhPath){$status=Invoke-External $GhPath @('auth','status','--hostname','github.com') -TimeoutSeconds 20 -AllowFailure -Label 'GitHub-Anmeldung prüfen' -Quiet;return($status.Code-eq0)}
function Invoke-VisibleGitHubLogin([string]$GhPath){
$runtimeDirectory=Split-Path -Parent $StatusFile;$loginScript=Join-Path $runtimeDirectory 'github-login.cmd';$escapedGh=$GhPath.Replace('"','""');$lines=@('@echo off','chcp 65001 >nul','title Vendoo - GitHub Anmeldung','echo Vendoo - Sichere GitHub Anmeldung','echo.','echo Folge dem Browserdialog zur GitHub-Anmeldung.','echo.',('"{0}" auth login --hostname github.com --git-protocol https --web --clipboard'-f$escapedGh),'exit /b %ERRORLEVEL%');[IO.File]::WriteAllLines($loginScript,$lines,(New-Object Text.UTF8Encoding($false)));Write-State 26 'GitHub-Anmeldung' 'Im geöffneten Fenster den Gerätecode bestätigen.' 'waiting';$loginProcess=Start-Process -FilePath 'cmd.exe' -ArgumentList @('/d','/c',('"{0}"'-f$loginScript))-WorkingDirectory $runtimeDirectory -WindowStyle Normal -PassThru;$deadline=(Get-Date).AddMinutes(10);while((Get-Date)-lt$deadline){Assert-NotCancelled;if(Test-GitHubAuthentication $GhPath){return};if($loginProcess.HasExited){throw'Die GitHub-Anmeldung wurde nicht abgeschlossen.'};Start-Sleep -Seconds 2};throw'Die GitHub-Anmeldung wurde nicht innerhalb von zehn Minuten abgeschlossen.'
}
function Check-ProductionVersion{try{$r=Invoke-RestMethod -Uri "$PublicBaseUrl/readyz?updater=$Stamp" -TimeoutSec 20 -Headers @{'Cache-Control'='no-cache';'User-Agent'="Vendoo-Updater/$UpdaterVersion"};return[pscustomobject]@{Ready=[bool]$r.ok;Version=[string]$r.version}}catch{return[pscustomobject]@{Ready=$false;Version='unbekannt'}}}
function Invoke-Lifecycle([string]$Command,$InputObject){$json=$InputObject|ConvertTo-Json -Depth 20 -Compress;$r=Invoke-External $script:Node @($LifecycleScript,$Command)-InputText $json -TimeoutSeconds 60 -Label "Lifecycle $Command" -Quiet;return($r.Output|ConvertFrom-Json)}
function Assert-Package{
foreach($f in @('package.json','package-lock.json','update-manifest.json','release-manifest.json','db\schema.sql','tools\apply-release-manifest.mjs','tools\updater-lifecycle-3.4.mjs','tools\check-release-payload-3.4.mjs')){if(-not(Test-Path(Join-Path $PackageRoot $f))){throw"Paketdatei fehlt: $f"}}
$p=Get-Content(Join-Path $PackageRoot 'package.json')-Raw-Encoding UTF8|ConvertFrom-Json;$m=Get-Content(Join-Path $PackageRoot 'update-manifest.json')-Raw-Encoding UTF8|ConvertFrom-Json;$r=Get-Content(Join-Path $PackageRoot 'release-manifest.json')-Raw-Encoding UTF8|ConvertFrom-Json
if([string]$p.version-ne$TargetVersion-or[string]$m.version-ne$TargetVersion-or[string]$r.targetVersion-ne$TargetVersion){throw"Paket-, Update- und Release-Manifest-Version müssen exakt $TargetVersion sein."};if([string]$r.schema-ne'vendoo-release-package-v2'){if(-not$SelfTest){throw"Auslieferbares Paket benötigt Manifest-Schema vendoo-release-package-v2, gefunden: $($r.schema)"}};if([string]$r.updaterVersion-ne$UpdaterVersion){throw"Release-Manifest gehört zu Updater $($r.updaterVersion), erwartet wird $UpdaterVersion."};foreach($name in @('sourceCommit','baseCommit','createdAt','payloadHash')){if([string]::IsNullOrWhiteSpace([string]$r.$name)){if(-not$SelfTest){throw"Release-Manifest enthält keine gültige Herkunft: $name fehlt."}}};$script:Manifest=$r
}
function Get-MainVersion([string]$GhPath){$encoded=(Invoke-External $GhPath @('api',"repos/$Repository/contents/package.json?ref=$BaseBranch",'--jq','.content')-TimeoutSeconds 60 -Label 'main-Version lesen' -Quiet).Output -replace'\s','';$text=[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($encoded));return[string](($text|ConvertFrom-Json).version)}
function Get-ReleaseObservation([string]$GhPath){
$sha=(Invoke-External $GhPath @('api',"repos/$Repository/commits/$BaseBranch",'--jq','.sha')-TimeoutSeconds 60 -Label 'main-SHA lesen' -Quiet).Output.Trim();$version=Get-MainVersion $GhPath;$prod=Check-ProductionVersion
$prResult=Invoke-External $GhPath @('pr','list','--repo',$Repository,'--head',$Branch,'--state','all','--limit','100','--json','number,url,state,mergedAt,closedAt,updatedAt,headRefOid,title')-TimeoutSeconds 60 -AllowFailure -Label 'Release-PRs ermitteln' -Quiet;$prs=@();if($prResult.Code-eq0-and$prResult.Output.Trim()){$raw=@($prResult.Output|ConvertFrom-Json);foreach($pr in $raw){$prs+=[pscustomobject]@{targetVersion=$TargetVersion;number=$pr.number;url=$pr.url;state=$pr.state;mergedAt=$pr.mergedAt;closedAt=$pr.closedAt;updatedAt=$pr.updatedAt;headRefOid=$pr.headRefOid;title=$pr.title}}}
return[pscustomobject]@{MainSha=$sha;MainVersion=$version;Production=$prod;PullRequests=$prs}
}
function Get-Decision($Observation,$Session){return Invoke-Lifecycle 'decide' ([ordered]@{targetVersion=$TargetVersion;mainVersion=$Observation.MainVersion;productionReady=$Observation.Production.Ready;productionVersion=$Observation.Production.Version;pullRequests=$Observation.PullRequests;session=$Session})}
function Complete-WithoutSource([string]$Reason,$Observation,$Session){foreach($id in @('workspace','tests','pullrequest','merge','deploy','verify')){Skip-Step $id};$Session.releaseState='completed';$Session.productionVersion=$Observation.Production.Version;$Session.completedAt=(Get-Date).ToString('o');Save-Session $Session;Write-State 100 'Bereits vollständig aktuell' "Vendoo $TargetVersion ist in main und Produktion bereits bestätigt. Grund: $Reason" 'success';Write-Event 'OK' 'Zielzustand bereits erreicht; kein Branch, kein PR und kein Redeploy erforderlich.'}
function Get-OpenPrFromDecision($Decision){if($Decision.PSObject.Properties.Name-contains'pullRequest'){return$Decision.pullRequest};return$null}
function Wait-RequiredGitHubChecks([string]$GhPath,[string]$PrUrl,$Session){
$deadline=(Get-Date).AddMinutes(45);$head='';$headSince=Get-Date;$lastMatrix=''
while($true){Assert-NotCancelled;$pr=(Invoke-External $GhPath @('pr','view',$PrUrl,'--repo',$Repository,'--json','number,url,state,mergedAt,headRefOid,statusCheckRollup')-TimeoutSeconds 90 -Label 'PR-Head und Checks lesen' -Quiet).Output|ConvertFrom-Json
if($pr.mergedAt){return[pscustomobject]@{Merged=$true;HeadSha=[string]$pr.headRefOid;Pr=$pr}}
$current=[string]$pr.headRefOid;if(-not$current){throw'Der Pull Request meldet keinen Head-SHA.'};if($current-ne$head){$head=$current;$headSince=Get-Date;$Session.finalPrHeadSha=$head;Save-Session $Session;Write-Event 'INFO' "Aktueller PR-Head-SHA: $head. Check-Grace-Period wurde neu gestartet."}
$runsResult=Invoke-External $GhPath @('api',"repos/$Repository/actions/runs?head_sha=$head&per_page=100")-TimeoutSeconds 90 -AllowFailure -Label 'Workflow-Runs des finalen Heads lesen' -Quiet;$runs=@();if($runsResult.Code-eq0-and$runsResult.Output.Trim()){try{$runs=@(($runsResult.Output|ConvertFrom-Json).workflow_runs)}catch{$runs=@()}}
$elapsed=[int]((Get-Date)-$headSince).TotalSeconds;$timedOut=(Get-Date)-ge$deadline;$classification=Invoke-Lifecycle 'classify-checks' ([ordered]@{checks=@($pr.statusCheckRollup);workflowRuns=$runs;elapsedSeconds=$elapsed;gracePeriodSeconds=90;timedOut=$timedOut})
$matrix=@($classification.rows|ForEach-Object{"$($_.name): $($_.state) ($($_.conclusion)$($_.status))"})-join'; '
if($matrix-ne$lastMatrix-or((Get-Date)-$script:LastCheckLog).TotalSeconds-ge30){Write-Event 'INFO' "GitHub-Status für ${head}: $matrix";$lastMatrix=$matrix;$script:LastCheckLog=Get-Date}
if([string]$classification.state-eq'failed'){throw"Ein erforderlicher GitHub-Check ist fehlgeschlagen.`n$matrix"}
if([string]$classification.state-eq'timeout'){throw"Zeitlimit beim Warten auf GitHub-Checks für finalen Head $head.`n$matrix"}
if([string]$classification.state-eq'success'){$verify=(Invoke-External $GhPath @('pr','view',$PrUrl,'--repo',$Repository,'--json','headRefOid')-TimeoutSeconds 60 -Label 'Finalen PR-Head bestätigen' -Quiet).Output|ConvertFrom-Json;if([string]$verify.headRefOid-eq$head){return[pscustomobject]@{Merged=$false;HeadSha=$head;Pr=$pr}};Write-Event 'INFO' 'PR-Head änderte sich nach erfolgreicher Check-Auswertung; Auswertung wird für den neuen Head wiederholt.'}
Write-State 80 'GitHub-Checks' "Finaler Head ${head}: $($classification.reason).";Start-Sleep -Seconds 10
}
}
function Ensure-Deployment($Session){
$prod=Check-ProductionVersion;if($prod.Ready-and$prod.Version-eq$TargetVersion){Write-Event 'OK' 'Produktion meldet die Zielversion bereits; Redeploy-Wartephase entfällt.';Complete-Step 'deploy';return$prod}
if($SkipProductionDeploy){Skip-Step 'deploy';Skip-Step 'verify';Write-State 100 'Source aktualisiert' 'GitHub wurde aktualisiert; das Produktionsdeployment wurde auf Wunsch übersprungen.' 'success';return$null}
Set-Step 'deploy' 'active';Write-State 88 'Coolify Auto Deploy' 'main ist auf Zielstand. Coolify übernimmt das Deployment über die bestehende GitHub-Verknüpfung.';$Session.releaseState='deploying';Save-Session $Session;$deadline=(Get-Date).AddMinutes(15);$nextLog=Get-Date;$last=''
do{Assert-NotCancelled;$prod=Check-ProductionVersion;$last="ready=$($prod.Ready), version=$($prod.Version)";if((Get-Date)-ge$nextLog){Write-Event 'INFO' "Coolify Auto Deploy: $last";$nextLog=(Get-Date).AddSeconds(30)};if($prod.Ready-and$prod.Version-eq$TargetVersion){break};Start-Sleep -Seconds 10}while((Get-Date)-lt$deadline)
if(-not($prod.Ready-and$prod.Version-eq$TargetVersion)){throw"Coolify hat die Zielversion innerhalb von 15 Minuten nicht bereitgestellt. Letzter Stand: $last."};Complete-Step 'deploy';return$prod
}
try{
Write-Event 'INFO' "Vendoo Production Updater $UpdaterVersion gestartet. Zielversion $TargetVersion."
Set-Step 'package' 'active';Write-State 3 'Paketprüfung' 'Version, Herkunft, Payload-Hash und Manifest werden geprüft.';Assert-Package;$session=Load-Session;if($SelfTestFailAt-eq'package'){throw'Simulierter Paketfehler.'};Complete-Step 'package';Write-Event 'OK' "Paketprüfung erfolgreich. payloadHash=$($script:Manifest.payloadHash), sourceCommit=$($script:Manifest.sourceCommit), baseCommit=$($script:Manifest.baseCommit)."
Set-Step 'tools' 'active';Write-State 9 'Systemprüfung' 'Git, Node.js, npm und GitHub CLI werden geprüft.';$script:Git=Get-GitPath;if(-not$script:Git){throw'Git für Windows wurde nicht gefunden.'};$script:Node=Get-NodePath;$script:Npm=Get-NpmPath;if(-not$script:Node-or-not$script:Npm){throw'Node.js 22 LTS oder npm wurde nicht gefunden.'};$nv=Invoke-External $script:Node @('--version')-TimeoutSeconds 20 -Label 'Node.js prüfen';if($nv.Output-notmatch'v?(\d+)\.'-or[int]$Matches[1]-lt22){throw'Node.js 22 oder neuer ist erforderlich.'};$gh=Ensure-GitHubCli;Complete-Step 'tools'
Set-Step 'github' 'active';Write-State 22 'GitHub-Verbindung' 'Anmeldung und Repository-Berechtigungen werden geprüft.';if(-not(Test-GitHubAuthentication $gh)){Invoke-VisibleGitHubLogin $gh};Invoke-External $gh @('auth','setup-git','--hostname','github.com')-TimeoutSeconds 60 -Label 'Git-Anmeldung konfigurieren'|Out-Null;$repo=(Invoke-External $gh @('repo','view',$Repository,'--json','viewerPermission,defaultBranchRef,isPrivate')-TimeoutSeconds 60 -Label 'Repository-Zugriff prüfen').Output|ConvertFrom-Json;if([string]$repo.viewerPermission-notin@('ADMIN','MAINTAIN','WRITE')){throw"Für $Repository fehlt Schreibzugriff."};Complete-Step 'github'
Set-Step 'production' 'active';Write-State 31 'Zielzustand' 'main, Produktion, bestehende PRs und Sitzung werden vor jeder Änderung abgeglichen.';$observation=Get-ReleaseObservation $gh;$decision=Get-Decision $observation $session;Write-Event 'INFO' "Lifecycle-Entscheidung: $($decision.action) / $($decision.reason); main=$($observation.MainVersion)@$($observation.MainSha); production=$($observation.Production.Version).";Complete-Step 'production'
if($SelfTest){foreach($id in @('workspace','tests','pullrequest','merge','deploy','verify')){Set-Step $id 'active';Start-Sleep -Milliseconds 120;Complete-Step $id};Write-State 100 'Selbsttest erfolgreich' 'Updater-3.4-Oberfläche und Host-Vertrag funktionieren.' 'success';return}
if([string]$decision.action-eq'complete'){Complete-WithoutSource $decision.reason $observation $session;return}
$prUrl='';$prNumber=$null;$sourceRequired=([string]$decision.action-eq'source')
if([string]$decision.action-eq'resume-pr'){$pr=Get-OpenPrFromDecision $decision;$prUrl=[string]$pr.url;$prNumber=$pr.number;$session.prUrl=$prUrl;$session.prNumber=$prNumber;$session.releaseState='checks';Save-Session $session;foreach($id in @('workspace','tests')){Skip-Step $id};Complete-Step 'pullrequest';Write-Event 'INFO' "Vorhandener offener Pull Request wird übernommen: $prUrl"}
elseif([string]$decision.action-eq'deploy'){foreach($id in @('workspace','tests','pullrequest','merge')){Skip-Step $id};$sourceRequired=$false}
if($sourceRequired){
Set-Step 'workspace' 'active';Write-State 40 'Payload-Sicherheitsprüfung' 'main wird isoliert geklont; Herkunft und mögliche Rückschritte werden vor dem Overlay geprüft.';if(Test-Path -LiteralPath $WorkRoot){Remove-Item $WorkRoot -Recurse -Force};New-Item -ItemType Directory -Path $WorkRoot -Force|Out-Null;Invoke-External $gh @('repo','clone',$Repository,$CloneDir,'--','--branch',$BaseBranch,'--single-branch')-WorkingDirectory $WorkRoot -TimeoutSeconds 300 -Label 'Repository klonen'|Out-Null
$guard=Invoke-External $script:Node @($PayloadGuardScript,$PackageRoot,$CloneDir,[string]$script:Manifest.baseCommit,[string]$observation.MainSha)-TimeoutSeconds 300 -AllowFailure -Label 'Veralteten Payload ausschließen';if($guard.Code-ne0){$detail='';try{$g=$guard.Output|ConvertFrom-Json;$detail=@($g.conflicts)-join', '}catch{$detail=$guard.Output};throw"Payload-Rückschritt blockiert. baseCommit=$($script:Manifest.baseCommit), main=$($observation.MainSha). Konflikte: $detail"}
Invoke-External $script:Git @('switch','-c',$Branch,$observation.MainSha)-WorkingDirectory $CloneDir -TimeoutSeconds 60 -Label 'Deterministischen Release-Branch erstellen'|Out-Null;Invoke-External $script:Node @((Join-Path $PackageRoot 'tools\apply-release-manifest.mjs'),$PackageRoot,$CloneDir)-TimeoutSeconds 300 -Label 'SHA-256-geprüftes Release-Manifest anwenden'|Out-Null;Complete-Step 'workspace'
Set-Step 'tests' 'active';Write-State 55 'Abnahmetests' 'npm ci und der vollständige Releasevertrag werden auf dem sicheren Overlay ausgeführt.';Invoke-External $script:Npm @('ci','--no-audit','--no-fund')-WorkingDirectory $CloneDir -TimeoutSeconds 1200 -Label 'npm ci'|Out-Null;Invoke-External $script:Npm @('run','verify:all')-WorkingDirectory $CloneDir -TimeoutSeconds 3600 -Label 'npm run verify:all'|Out-Null;Complete-Step 'tests'
$latest=Get-ReleaseObservation $gh;$lateDecision=Get-Decision $latest $session;if([string]$lateDecision.action-eq'complete'){Complete-WithoutSource 'target-reached-during-local-tests' $latest $session;return}elseif([string]$lateDecision.action-eq'resume-pr'){$pr=Get-OpenPrFromDecision $lateDecision;$prUrl=[string]$pr.url;$prNumber=$pr.number;Write-Event 'INFO' "Paralleler Lauf hat bereits einen PR erzeugt; dieser wird übernommen: $prUrl"}elseif([string]$lateDecision.action-eq'deploy'){foreach($id in @('pullrequest','merge')){Skip-Step $id};$sourceRequired=$false}else{
Set-Step 'pullrequest' 'active';Write-State 70 'Pull Request' 'Branch und Pull Request werden idempotent abgeglichen.';Invoke-External $script:Git @('config','user.name','Vendoo Production Updater')-WorkingDirectory $CloneDir -Label 'Git-Name setzen'|Out-Null;$uid=(Invoke-External $gh @('api','user','--jq','.id')-Label 'GitHub-Benutzer-ID').Output.Trim();$ulogin=(Invoke-External $gh @('api','user','--jq','.login')-Label 'GitHub-Benutzername').Output.Trim();Invoke-External $script:Git @('config','user.email',"$uid+$ulogin@users.noreply.github.com")-WorkingDirectory $CloneDir -Label 'Git-E-Mail setzen'|Out-Null;Invoke-External $script:Git @('add','--all')-WorkingDirectory $CloneDir -Label 'Änderungen vormerken'|Out-Null;$diff=Invoke-External $script:Git @('diff','--cached','--quiet')-WorkingDirectory $CloneDir -AllowFailure -Label 'Änderungen prüfen';if($diff.Code-ne1){throw'Payload erzeugt gegenüber main keinen gültigen neuen Source-Stand.'};Invoke-External $script:Git @('commit','-m',"release: Vendoo $TargetVersion production update via updater 3.4")-WorkingDirectory $CloneDir -TimeoutSeconds 120 -Label 'Release committen'|Out-Null
$remoteLine=(Invoke-External $script:Git @('ls-remote','--heads','origin',"refs/heads/$Branch")-WorkingDirectory $CloneDir -TimeoutSeconds 60 -AllowFailure -Label 'Remote-Branch prüfen' -Quiet).Output.Trim();if($remoteLine){$remoteSha=($remoteLine-split'\s+')[0];Invoke-External $script:Git @('push',"--force-with-lease=refs/heads/$Branch`:$remoteSha",'--set-upstream','origin',$Branch)-WorkingDirectory $CloneDir -TimeoutSeconds 300 -Label 'Bestehenden Release-Branch sicher aktualisieren'|Out-Null}else{Invoke-External $script:Git @('push','--set-upstream','origin',$Branch)-WorkingDirectory $CloneDir -TimeoutSeconds 300 -Label 'Release-Branch pushen'|Out-Null}
$existing=Invoke-External $gh @('pr','list','--repo',$Repository,'--head',$Branch,'--state','open','--json','number,url','--jq','.[0]')-AllowFailure -Label 'Vorhandenen Pull Request erneut suchen' -Quiet;if($existing.Output.Trim()){$existingObj=$existing.Output|ConvertFrom-Json;$prUrl=[string]$existingObj.url;$prNumber=$existingObj.number}else{$body=Join-Path $WorkRoot 'pr-body.md';$bodyText="## Vendoo $TargetVersion Production Update`r`n`r`nErstellt durch Vendoo Production Updater $UpdaterVersion.`r`n`r`n- vollständiges ``npm run verify:all`` erfolgreich`r`n- Payload-Herkunft und Rückschrittschutz geprüft`r`n- keine Datenbanken, Uploads, Secrets oder ``/app/data``-Inhalte`r`n- GitHub-Checks werden am finalen Head-SHA ausgewertet`r`n";[IO.File]::WriteAllText($body,$bodyText,(New-Object Text.UTF8Encoding($false)));$created=(Invoke-External $gh @('pr','create','--repo',$Repository,'--base',$BaseBranch,'--head',$Branch,'--title',"Vendoo $TargetVersion Production Update",'--body-file',$body)-WorkingDirectory $CloneDir -TimeoutSeconds 180 -Label 'Pull Request erstellen').Output.Trim();$prUrl=$created;$prInfo=(Invoke-External $gh @('pr','view',$prUrl,'--repo',$Repository,'--json','number')-TimeoutSeconds 60 -Label 'PR-Nummer lesen' -Quiet).Output|ConvertFrom-Json;$prNumber=$prInfo.number}
$session.prUrl=$prUrl;$session.prNumber=$prNumber;$session.releaseState='checks';Save-Session $session;Complete-Step 'pullrequest';Write-Event 'OK' "Pull Request: $prUrl"
}
}
if($prUrl){Set-Step 'merge' 'active';Write-State 78 'GitHub-Checks' 'Workflow-Runs und Pflichtchecks werden für den jeweils aktuellen PR-Head verfolgt.';$checkResult=Wait-RequiredGitHubChecks $gh $prUrl $session;if(-not$checkResult.Merged){Write-State 84 'Merge' 'Der geprüfte finale Head wird atomar per Squash nach main übernommen.';Invoke-External $gh @('pr','merge',$prUrl,'--repo',$Repository,'--squash','--delete-branch','--subject',"Vendoo $TargetVersion Production Update",'--match-head-commit',[string]$checkResult.HeadSha)-WorkingDirectory $CloneDir -TimeoutSeconds 300 -Label 'Pull Request mergen'|Out-Null};$mergedInfo=(Invoke-External $gh @('pr','view',$prUrl,'--repo',$Repository,'--json','mergedAt,mergeCommit,headRefOid,state')-TimeoutSeconds 60 -Label 'Merge-Ergebnis bestätigen' -Quiet).Output|ConvertFrom-Json;if(-not$mergedInfo.mergedAt){throw'Der Pull Request wurde nicht als gemergt bestätigt.'};$session.finalPrHeadSha=[string]$mergedInfo.headRefOid;$session.mergeCommit=[string]$mergedInfo.mergeCommit.oid;$session.releaseState='merged';Save-Session $session;Complete-Step 'merge';Write-Event 'OK' "Merge bestätigt: $($session.mergeCommit)"}
$prod=Ensure-Deployment $session;if($SkipProductionDeploy){return};Set-Step 'verify' 'active';Write-State 97 'Produktionsprüfung' 'Die öffentliche Readiness und die exakte Zielversion werden abschließend bestätigt.';if($null-eq$prod){$prod=Check-ProductionVersion};if(-not($prod.Ready-and$prod.Version-eq$TargetVersion)){throw"Die abschließende Produktionsprüfung ist fehlgeschlagen: ready=$($prod.Ready), version=$($prod.Version)"};Complete-Step 'verify';$session.productionVersion=$prod.Version;$session.releaseState='completed';$session.completedAt=(Get-Date).ToString('o');Save-Session $session;Write-State 100 'Update erfolgreich' "Vendoo $TargetVersion läuft geprüft in Produktion." 'success';Write-Event 'OK' "Vendoo $TargetVersion wurde vollständig und idempotent aktualisiert."
} catch [System.OperationCanceledException] {Fail-Step;Write-State 100 'Update abgebrochen' $_.Exception.Message 'cancelled' $_.Exception.Message 'Der gespeicherte Wiederaufnahmepunkt bleibt erhalten.';Write-Event 'WARN' $_.Exception.Message
}catch{Fail-Step;$msg=$_.Exception.Message;$remediation=if($script:CurrentStep -in @('deploy','verify')){'main kann bereits aktualisiert sein. Ein erneuter Start setzt bei Deployment und Produktionsprüfung fort.'}elseif($msg-match'Payload-Rückschritt'){'Das Paket ist gegenüber main veraltet. Kein Branch und kein PR wurden aus diesem Payload erstellt. Ein neues Paket aus aktuellem main erzeugen.'}else{'Ein erneuter Start gleicht main, PR, finalen Head-SHA und Sitzung erneut ab; bereits abgeschlossene Phasen werden nicht dupliziert.'};Write-State 100 'Update sicher gestoppt' $msg 'failed' $msg $remediation;Write-Event 'ERROR' $msg
}finally{if(Test-Path $InputFile){Remove-Item $InputFile -Force -ErrorAction SilentlyContinue}}
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env python3
"""Build a byte-reproducible ZIP archive from a release directory."""
from __future__ import annotations
import argparse
import os
from pathlib import Path
from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo
FIXED_TIMESTAMP = (1980, 1, 1, 0, 0, 0)
def iter_files(source: Path):
for path in sorted(source.rglob("*"), key=lambda item: item.as_posix()):
if path.is_file() and not path.is_symlink():
yield path
def build_archive(source: Path, destination: Path) -> None:
source = source.resolve(strict=True)
if not source.is_dir():
raise ValueError(f"Source is not a directory: {source}")
destination = destination.resolve()
if source == destination or source in destination.parents:
raise ValueError("Destination must be outside the source directory.")
destination.parent.mkdir(parents=True, exist_ok=True)
temporary = destination.with_suffix(destination.suffix + ".tmp")
temporary.unlink(missing_ok=True)
try:
with ZipFile(temporary, "w", compression=ZIP_DEFLATED, compresslevel=9) as archive:
for file_path in iter_files(source):
relative = file_path.relative_to(source.parent).as_posix()
info = ZipInfo(relative, FIXED_TIMESTAMP)
info.compress_type = ZIP_DEFLATED
info.create_system = 3
mode = file_path.stat().st_mode & 0o777
info.external_attr = (mode & 0xFFFF) << 16
info.flag_bits |= 0x800
archive.writestr(info, file_path.read_bytes(), compress_type=ZIP_DEFLATED, compresslevel=9)
os.replace(temporary, destination)
finally:
temporary.unlink(missing_ok=True)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("source", type=Path)
parser.add_argument("destination", type=Path)
args = parser.parse_args()
build_archive(args.source, args.destination)
print(f"Deterministic ZIP created: {args.destination}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+23 -5
View File
@@ -8,22 +8,38 @@ import {
writeFileSync, 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}`);
+86
View File
@@ -0,0 +1,86 @@
import { createHash } from 'node:crypto';
import { readFileSync } from 'node:fs';
import { resolve, join } from 'node:path';
import { spawnSync } from 'node:child_process';
import { evaluatePayloadGuard } from './updater-lifecycle-3.4.mjs';
const sha256 = value => createHash('sha256').update(value).digest('hex');
function git(repo, args, { allowFailure = false, encoding = 'utf8' } = {}) {
const result = spawnSync('git', ['-C', repo, ...args], { encoding, maxBuffer: 64 * 1024 * 1024 });
if (!allowFailure && result.status !== 0) {
throw new Error(`git ${args.join(' ')} fehlgeschlagen: ${(result.stderr || result.stdout || '').trim()}`);
}
return result;
}
function readGitFile(repo, commit, path) {
const result = git(repo, ['show', `${commit}:${path}`], { allowFailure: true, encoding: null });
if (result.status !== 0) return null;
return Buffer.from(result.stdout);
}
function normalize(path) {
return String(path || '').replaceAll('\\', '/').replace(/^\.\//, '');
}
export function inspectPayload({ packageRoot, repositoryRoot, baseCommit, currentMainCommit }) {
const source = resolve(packageRoot);
const repo = resolve(repositoryRoot);
const manifest = JSON.parse(readFileSync(join(source, 'release-manifest.json'), 'utf8'));
const baseExists = git(repo, ['cat-file', '-e', `${baseCommit}^{commit}`], { allowFailure: true }).status === 0;
const mainExists = git(repo, ['cat-file', '-e', `${currentMainCommit}^{commit}`], { allowFailure: true }).status === 0;
if (!baseExists || !mainExists) {
return {
...evaluatePayloadGuard({ baseCommit: baseExists ? baseCommit : '', currentMainCommit: mainExists ? currentMainCommit : '', baseIsAncestor: false }),
payloadChangedPaths: [],
mainChangedPaths: [],
};
}
const baseIsAncestor = git(repo, ['merge-base', '--is-ancestor', baseCommit, currentMainCommit], { allowFailure: true }).status === 0;
const mainChangedPaths = new Set(
String(git(repo, ['diff', '--name-only', `${baseCommit}..${currentMainCommit}`, '--']).stdout || '')
.split(/\r?\n/).map(normalize).filter(Boolean),
);
const payloadChangedPaths = new Set();
const desired = new Map();
for (const entry of manifest.files || []) {
const path = normalize(entry.path);
const baseBytes = readGitFile(repo, baseCommit, path);
const baseHash = baseBytes === null ? null : sha256(baseBytes);
if (baseHash !== String(entry.sha256)) payloadChangedPaths.add(path);
desired.set(path, String(entry.sha256));
}
for (const value of manifest.removedFiles || []) {
const path = normalize(value);
if (readGitFile(repo, baseCommit, path) !== null) payloadChangedPaths.add(path);
desired.set(path, null);
}
const conflictingPaths = [];
for (const path of payloadChangedPaths) {
if (!mainChangedPaths.has(path)) continue;
const currentBytes = readGitFile(repo, currentMainCommit, path);
const currentHash = currentBytes === null ? null : sha256(currentBytes);
if (currentHash !== desired.get(path)) conflictingPaths.push(path);
}
return {
...evaluatePayloadGuard({ baseCommit, currentMainCommit, baseIsAncestor, conflictingPaths }),
payloadChangedPaths: [...payloadChangedPaths].sort(),
mainChangedPaths: [...mainChangedPaths].sort(),
};
}
if (process.argv[1] && import.meta.url === new URL(`file://${process.argv[1].replaceAll('\\', '/')}`).href) {
const [packageRoot, repositoryRoot, baseCommit, currentMainCommit] = process.argv.slice(2);
try {
if (!packageRoot || !repositoryRoot || !baseCommit || !currentMainCommit) {
throw new Error('Aufruf: node check-release-payload-3.4.mjs <packageRoot> <repositoryRoot> <baseCommit> <currentMainCommit>');
}
const result = inspectPayload({ packageRoot, repositoryRoot, baseCommit, currentMainCommit });
process.stdout.write(`${JSON.stringify(result)}\n`);
if (!result.allowed) process.exitCode = 2;
} catch (error) {
process.stderr.write(`[FEHLER] ${error.message}\n`);
process.exit(1);
}
}
+16 -2
View File
@@ -10,6 +10,7 @@ import {
statSync, 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();
+200
View File
@@ -0,0 +1,200 @@
import { createHash } from 'node:crypto';
const ACTIVE = new Set(['queued', 'pending', 'in_progress', 'waiting', 'requested']);
const SUCCESS = new Set(['success', 'neutral', 'skipped']);
const FAILURE = new Set(['failure', 'timed_out', 'action_required', 'startup_failure', 'stale']);
const CANCELLED = new Set(['cancelled']);
export const UPDATER_VERSION = '3.4.0';
export const SESSION_SCHEMA = 'vendoo-updater-session-v2';
export const PACKAGE_MANIFEST_SCHEMA = 'vendoo-release-package-v2';
export const REQUIRED_CHECKS = Object.freeze([
'Syntax, Sicherheit und Release-Gates',
'Windows vollständiger Releasevertrag',
'Updater-3.4-Paket nach Abnahme',
]);
export const REQUIRED_WORKFLOWS = Object.freeze([
'Vendoo CI',
'Vendoo Release Manifest Sync',
]);
const lower = value => String(value || '').trim().toLowerCase();
const time = value => {
const parsed = Date.parse(value || '');
return Number.isFinite(parsed) ? parsed : 0;
};
const stablePayload = manifest => ({
schema: PACKAGE_MANIFEST_SCHEMA,
targetVersion: String(manifest.targetVersion || manifest.version || ''),
updaterVersion: String(manifest.updaterVersion || ''),
sourceCommit: String(manifest.sourceCommit || ''),
baseCommit: String(manifest.baseCommit || ''),
createdAt: String(manifest.createdAt || ''),
mode: String(manifest.mode || ''),
requiredFiles: [...(manifest.requiredFiles || [])],
removedFiles: [...(manifest.removedFiles || [])],
files: [...(manifest.files || [])],
});
export function computePackagePayloadHash(manifest) {
return createHash('sha256').update(JSON.stringify(stablePayload(manifest))).digest('hex');
}
export function createPackageManifest(sourceManifest, { sourceCommit, baseCommit, createdAt }) {
const manifest = {
...sourceManifest,
schema: PACKAGE_MANIFEST_SCHEMA,
targetVersion: String(sourceManifest.version || sourceManifest.targetVersion || ''),
sourceCommit: String(sourceCommit || ''),
baseCommit: String(baseCommit || ''),
createdAt: String(createdAt || ''),
};
manifest.payloadHash = computePackagePayloadHash(manifest);
return manifest;
}
export function payloadIdentity(manifest) {
const targetVersion = String(manifest?.targetVersion || manifest?.version || '');
const payloadHash = String(manifest?.payloadHash || '');
return `${targetVersion}:${payloadHash}`;
}
export function createSession({ repository, targetVersion, updaterVersion = UPDATER_VERSION, payloadHash, branch }) {
return {
schema: SESSION_SCHEMA,
repository,
targetVersion,
updaterVersion,
payloadHash,
branch,
releaseState: 'new',
prNumber: null,
prUrl: '',
finalPrHeadSha: '',
mergeCommit: '',
productionVersion: '',
completedAt: '',
updatedAt: new Date().toISOString(),
};
}
export function sessionMatches(session, { repository, targetVersion, payloadHash }) {
return Boolean(session && session.schema === SESSION_SCHEMA && session.repository === repository && session.targetVersion === targetVersion && session.payloadHash === payloadHash);
}
export function decideReleasePath({ targetVersion, mainVersion, productionReady, productionVersion, pullRequests = [], session = null }) {
const productionAtTarget = Boolean(productionReady && productionVersion === targetVersion);
if (session?.releaseState === 'completed' && productionAtTarget) return { action: 'complete', reason: 'completed-session' };
if (mainVersion === targetVersion) {
return productionAtTarget ? { action: 'complete', reason: 'main-and-production-at-target' } : { action: 'deploy', reason: 'main-at-target-production-behind' };
}
const relevant = pullRequests.filter(pr => pr && pr.targetVersion === targetVersion);
const open = relevant.filter(pr => lower(pr.state) === 'open').sort((a, b) => time(b.updatedAt) - time(a.updatedAt))[0];
if (open) return { action: 'resume-pr', reason: 'matching-open-pr', pullRequest: open };
const merged = relevant.filter(pr => Boolean(pr.mergedAt || pr.merged)).sort((a, b) => time(b.mergedAt || b.updatedAt) - time(a.mergedAt || a.updatedAt))[0];
if (merged) return { action: 'deploy', reason: 'matching-pr-already-merged', pullRequest: merged };
return { action: 'source', reason: 'source-update-required' };
}
function normalizeCheck(check) {
const status = lower(check?.status);
const conclusion = lower(check?.conclusion);
const updatedAt = check?.completedAt || check?.startedAt || check?.updatedAt || '';
let state = 'missing';
if (ACTIVE.has(status) || (!conclusion && status && status !== 'completed')) state = 'pending';
else if (SUCCESS.has(conclusion)) state = 'success';
else if (FAILURE.has(conclusion)) state = 'failure';
else if (CANCELLED.has(conclusion)) state = 'cancelled';
else if (status === 'completed' && !conclusion) state = 'pending';
return { name: String(check?.name || ''), workflowName: String(check?.workflowName || ''), state, status, conclusion, updatedAt, detailsUrl: String(check?.detailsUrl || '') };
}
function bestCheck(checks) {
return [...checks].sort((a, b) => {
const dateDelta = time(b.updatedAt) - time(a.updatedAt);
if (dateDelta) return dateDelta;
const rank = { success: 4, pending: 3, failure: 2, cancelled: 1, missing: 0 };
return (rank[b.state] || 0) - (rank[a.state] || 0);
})[0];
}
function normalizeRun(run) {
const status = lower(run?.status);
const conclusion = lower(run?.conclusion);
let state = 'missing';
if (ACTIVE.has(status)) state = 'pending';
else if (SUCCESS.has(conclusion)) state = 'success';
else if (FAILURE.has(conclusion)) state = 'failure';
else if (CANCELLED.has(conclusion)) state = 'cancelled';
return { name: String(run?.name || run?.workflowName || ''), state, status, conclusion, updatedAt: run?.updated_at || run?.updatedAt || run?.run_started_at || '', url: String(run?.html_url || run?.url || '') };
}
function bestRun(runs) {
return [...runs].sort((a, b) => {
const dateDelta = time(b.updatedAt) - time(a.updatedAt);
if (dateDelta) return dateDelta;
const rank = { success: 4, pending: 3, failure: 2, cancelled: 1, missing: 0 };
return (rank[b.state] || 0) - (rank[a.state] || 0);
})[0];
}
export function classifyChecks({ checks = [], workflowRuns = [], requiredChecks = REQUIRED_CHECKS, requiredWorkflows = REQUIRED_WORKFLOWS, elapsedSeconds = 0, gracePeriodSeconds = 90, timedOut = false }) {
const normalizedChecks = checks.map(normalizeCheck);
const normalizedRuns = workflowRuns.map(normalizeRun);
const checkRows = requiredChecks.map(name => {
const candidates = normalizedChecks.filter(check => check.name === name);
return candidates.length ? bestCheck(candidates) : { name, state: 'missing', status: '', conclusion: '', updatedAt: '', detailsUrl: '' };
});
const workflowRows = requiredWorkflows.map(name => {
const candidates = normalizedRuns.filter(run => run.name === name);
return candidates.length ? bestRun(candidates) : { name, state: 'missing', status: '', conclusion: '', updatedAt: '', url: '' };
});
const rows = [...checkRows, ...workflowRows];
const terminalFailure = rows.find(row => row.state === 'failure');
if (terminalFailure) return { state: 'failed', reason: `${terminalFailure.name}: ${terminalFailure.conclusion || terminalFailure.status}`, rows };
const terminalCancelled = rows.find(row => row.state === 'cancelled');
if (terminalCancelled) return { state: 'failed', reason: `${terminalCancelled.name}: cancelled`, rows };
if (rows.every(row => row.state === 'success')) return { state: 'success', reason: 'all-required-checks-successful', rows };
const missing = rows.filter(row => row.state === 'missing');
if (timedOut) return { state: 'timeout', reason: 'required-checks-not-terminal-before-timeout', rows };
if (missing.length && elapsedSeconds < gracePeriodSeconds) return { state: 'waiting-registration', reason: 'checks-within-registration-grace-period', rows };
if (missing.length) return { state: 'waiting-missing', reason: 'required-checks-not-yet-registered', rows };
return { state: 'pending', reason: 'required-checks-still-running', rows };
}
export function formatStatusMatrix(rows = []) {
return rows.map(row => `${row.name}: ${row.state} (${row.conclusion || row.status || 'not-reported'})`).join('\n');
}
export function evaluatePayloadGuard({ baseCommit, currentMainCommit, baseIsAncestor, conflictingPaths = [] }) {
if (!baseCommit || !currentMainCommit) return { allowed: false, reason: 'missing-commit-provenance', conflicts: [] };
if (!baseIsAncestor) return { allowed: false, reason: 'base-is-not-ancestor-of-main', conflicts: [] };
if (conflictingPaths.length) return { allowed: false, reason: 'payload-would-overwrite-newer-main', conflicts: [...conflictingPaths].sort() };
return { allowed: true, reason: baseCommit === currentMainCommit ? 'exact-base' : 'non-conflicting-main-advance', conflicts: [] };
}
function parseStdin() {
return new Promise((resolve, reject) => {
let input = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', chunk => { input += chunk; });
process.stdin.on('end', () => { try { resolve(input.trim() ? JSON.parse(input) : {}); } catch (error) { reject(error); } });
process.stdin.on('error', reject);
});
}
if (process.argv[1] && import.meta.url === new URL(`file://${process.argv[1].replaceAll('\\', '/')}`).href) {
const command = process.argv[2];
try {
const input = await parseStdin();
let output;
if (command === 'decide') output = decideReleasePath(input);
else if (command === 'classify-checks') output = classifyChecks(input);
else if (command === 'payload-guard') output = evaluatePayloadGuard(input);
else throw new Error(`Unbekannter Lifecycle-Befehl: ${command}`);
process.stdout.write(`${JSON.stringify(output)}\n`);
} catch (error) {
process.stderr.write(`[FEHLER] ${error.message}\n`);
process.exit(1);
}
}
+20 -5
View File
@@ -1,5 +1,7 @@
[CmdletBinding()] [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
+170
View File
@@ -0,0 +1,170 @@
import { existsSync, readFileSync, mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { createHash } from 'node:crypto';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { spawnSync } from 'node:child_process';
import {
classifyChecks,
createPackageManifest,
decideReleasePath,
evaluatePayloadGuard,
formatStatusMatrix,
PACKAGE_MANIFEST_SCHEMA,
REQUIRED_CHECKS,
REQUIRED_WORKFLOWS,
SESSION_SCHEMA,
UPDATER_VERSION,
} from './updater-lifecycle-3.4.mjs';
import { inspectPayload } from './check-release-payload-3.4.mjs';
const root = resolve(import.meta.dirname, '..');
const failures = [];
const assert = (condition, message) => { if (!condition) failures.push(message); };
const read = rel => readFileSync(join(root, rel), 'utf8');
for (const file of [
'scripts/updater/Vendoo.Updater.Worker34.ps1',
'tools/updater-lifecycle-3.4.mjs',
'tools/check-release-payload-3.4.mjs',
'tools/verify-production-updater-3.4.mjs',
'docs/PRODUCTION_UPDATER_3_4.md',
]) assert(existsSync(join(root, file)), `Pflichtdatei fehlt: ${file}`);
const lifecycle = read('tools/updater-lifecycle-3.4.mjs');
for (const marker of ['main-and-production-at-target', 'main-at-target-production-behind', 'matching-open-pr', 'matching-pr-already-merged']) {
assert(lifecycle.includes(marker), `Lifecycle-Entscheidung fehlt: ${marker}`);
}
if (existsSync(join(root, 'scripts/updater/Vendoo.Updater.Worker34.ps1'))) {
const worker = read('scripts/updater/Vendoo.Updater.Worker34.ps1');
for (const marker of [
"'3.4.0'", 'production-update-v4', 'payloadHash', 'baseCommit', 'sourceCommit',
'Get-ReleaseObservation', 'Wait-RequiredGitHubChecks', 'headRefOid', 'workflow_runs',
'force-with-lease', 'completedAt', 'finalPrHeadSha',
'check-release-payload-3.4.mjs', '--match-head-commit',
]) assert(worker.includes(marker), `Worker-3.4-Vertrag fehlt: ${marker}`);
assert(!worker.includes("@('pr','checks'"), 'Updater 3.4 darf gh pr checks --watch nicht mehr verwenden.');
assert(!worker.includes("@('push','--force'"), 'Updater 3.4 darf keinen unbedingten Force-Push verwenden.');
}
if (existsSync(join(root, 'scripts/updater/Vendoo.Updater.UI.ps1'))) {
const ui = read('scripts/updater/Vendoo.Updater.UI.ps1');
assert(ui.includes("$UpdaterVersion = '3.4.0'"), 'Updater-UI meldet nicht Version 3.4.0.');
assert(ui.includes("Vendoo.Updater.Worker34.ps1"), 'Updater-UI startet nicht den Worker 3.4.');
}
if (existsSync(join(root, 'scripts/updater/Vendoo.Updater.Host.ps1'))) {
assert(read('scripts/updater/Vendoo.Updater.Host.ps1').includes("$UpdaterVersion = '3.4.0'"), 'Updater-Host meldet nicht Version 3.4.0.');
}
const manifestSync = read('.github/workflows/sync-release-manifest.yml');
assert(manifestSync.includes("'release/*-production-update-v*'"), 'Manifest-Sync ist nicht auf versionsneutrale Releasebranches generalisiert.');
assert(!manifestSync.includes('release/1.43.0-production-update-v3'), 'Manifest-Sync enthält weiterhin den alten festen 3.3-Branch.');
const packageJson = JSON.parse(read('package.json'));
for (const name of ['verify:one-click-update', 'verify:updater-ui', 'verify:updater-hotfix']) {
assert(packageJson.scripts?.[name] === 'node tools/verify-production-updater-3.4.mjs', `${name} verweist nicht auf Updater 3.4.`);
}
assert(packageJson.scripts?.['verify:updater-lifecycle'] === 'node tools/verify-production-updater-3.4.mjs', 'Eigenständiger Lifecycle-Test fehlt.');
const updateManifest = JSON.parse(read('update-manifest.json'));
assert(updateManifest.updaterVersion === UPDATER_VERSION && updateManifest.updater_version === UPDATER_VERSION, 'Update-Manifest meldet nicht 3.4.0.');
const sourceManifest = {
schema: 'vendoo-release-manifest-v1', version: '1.44.0', updaterVersion: UPDATER_VERSION,
mode: 'overlay-with-explicit-removals', requiredFiles: ['package.json'], removedFiles: [],
files: [{ path: 'package.json', sha256: 'a'.repeat(64), size: 1 }],
};
const packageManifest = createPackageManifest(sourceManifest, { sourceCommit: '1'.repeat(40), baseCommit: '2'.repeat(40), createdAt: '2026-07-10T12:00:00Z' });
assert(packageManifest.schema === PACKAGE_MANIFEST_SCHEMA, 'Auslieferungsmanifest verwendet nicht Schema v2.');
assert(/^[a-f0-9]{64}$/.test(packageManifest.payloadHash), 'Auslieferungsmanifest besitzt keinen stabilen Payload-Hash.');
const successChecks = REQUIRED_CHECKS.map(name => ({ name, status: 'COMPLETED', conclusion: 'SUCCESS', completedAt: '2026-07-10T12:00:00Z' }));
const successRuns = REQUIRED_WORKFLOWS.map(name => ({ name, status: 'completed', conclusion: 'success', updated_at: '2026-07-10T12:00:00Z' }));
let result = classifyChecks({ checks: [], workflowRuns: [], elapsedSeconds: 10, gracePeriodSeconds: 90 });
assert(result.state === 'waiting-registration', 'Leere Checks innerhalb der Grace Period müssen warten.');
result = classifyChecks({ checks: successChecks, workflowRuns: successRuns, elapsedSeconds: 120 });
assert(result.state === 'success', 'Vollständig erfolgreiche Checks wurden nicht erkannt.');
for (const status of ['QUEUED', 'PENDING', 'IN_PROGRESS']) {
result = classifyChecks({ checks: [{ name: REQUIRED_CHECKS[0], status }], workflowRuns: [], elapsedSeconds: 120 });
assert(['pending', 'waiting-missing'].includes(result.state), `${status} wurde nicht als laufend behandelt.`);
}
result = classifyChecks({ checks: [...successChecks, { ...successChecks[0], completedAt: '2026-07-10T11:00:00Z', conclusion: 'CANCELLED' }], workflowRuns: successRuns, elapsedSeconds: 120 });
assert(result.state === 'success', 'Paralleler älterer abgebrochener Check darf neueren Erfolg nicht überstimmen.');
result = classifyChecks({ checks: successChecks.map((x, i) => i ? x : ({ ...x, conclusion: 'FAILURE' })), workflowRuns: successRuns, elapsedSeconds: 120 });
assert(result.state === 'failed', 'Fehlgeschlagener Pflichtcheck wurde nicht terminal erkannt.');
assert(formatStatusMatrix(result.rows).includes(REQUIRED_CHECKS[0]), 'Timeout-/Fehlermatrix enthält Checknamen nicht.');
const runBase = successRuns.filter(run => run.name !== REQUIRED_WORKFLOWS[0]);
result = classifyChecks({
checks: successChecks,
workflowRuns: [
...runBase,
{ name: REQUIRED_WORKFLOWS[0], status: 'completed', conclusion: 'success', updated_at: '2026-07-10T11:00:00Z' },
{ name: REQUIRED_WORKFLOWS[0], status: 'completed', conclusion: 'failure', updated_at: '2026-07-10T12:00:00Z' },
],
elapsedSeconds: 120,
});
assert(result.state === 'failed', 'Ein älterer erfolgreicher Workflow-Run darf einen neueren Fehler nicht verdecken.');
result = classifyChecks({
checks: successChecks,
workflowRuns: [
...runBase,
{ name: REQUIRED_WORKFLOWS[0], status: 'completed', conclusion: 'failure', updated_at: '2026-07-10T11:00:00Z' },
{ name: REQUIRED_WORKFLOWS[0], status: 'in_progress', conclusion: null, updated_at: '2026-07-10T12:00:00Z' },
],
elapsedSeconds: 120,
});
assert(result.state === 'pending', 'Ein neuer laufender Workflow-Run muss einen älteren Fehler ablösen.');
result = classifyChecks({
checks: successChecks,
workflowRuns: [
...runBase,
{ name: REQUIRED_WORKFLOWS[0], status: 'completed', conclusion: 'cancelled', updated_at: '2026-07-10T11:00:00Z' },
{ name: REQUIRED_WORKFLOWS[0], status: 'completed', conclusion: 'success', updated_at: '2026-07-10T12:00:00Z' },
],
elapsedSeconds: 120,
});
assert(result.state === 'success', 'Ein neuer erfolgreicher Workflow-Run muss einen älteren Abbruch ablösen.');
let decision = decideReleasePath({ targetVersion: '1.43.0', mainVersion: '1.43.0', productionReady: true, productionVersion: '1.43.0' });
assert(decision.action === 'complete', 'Bereits deployte Zielversion muss sofort erfolgreich enden.');
decision = decideReleasePath({ targetVersion: '1.43.0', mainVersion: '1.43.0', productionReady: true, productionVersion: '1.42.0' });
assert(decision.action === 'deploy', 'main auf Zielversion darf keinen Quell-PR erzeugen.');
decision = decideReleasePath({ targetVersion: '1.44.0', mainVersion: '1.43.0', productionReady: true, productionVersion: '1.43.0', pullRequests: [{ targetVersion: '1.44.0', state: 'OPEN', number: 30, updatedAt: '2026-07-10T12:00:00Z' }] });
assert(decision.action === 'resume-pr', 'Vorhandener offener PR muss übernommen werden.');
decision = decideReleasePath({ targetVersion: '1.44.0', mainVersion: '1.43.0', productionReady: true, productionVersion: '1.43.0', pullRequests: [{ targetVersion: '1.44.0', state: 'CLOSED', mergedAt: '2026-07-10T12:00:00Z' }] });
assert(decision.action === 'deploy', 'Bereits gemergter PR muss die Sourcephase überspringen.');
assert(evaluatePayloadGuard({ baseCommit: 'a', currentMainCommit: 'b', baseIsAncestor: true, conflictingPaths: ['x'] }).allowed === false, 'Veralteter Payload mit Konflikt wurde nicht blockiert.');
assert(evaluatePayloadGuard({ baseCommit: 'a', currentMainCommit: 'b', baseIsAncestor: true, conflictingPaths: [] }).allowed === true, 'Nicht kollidierender Main-Fortschritt muss zulässig sein.');
const temp = mkdtempSync(join(tmpdir(), 'vendoo-updater34-'));
try {
const repo = join(temp, 'repo');
const pkg = join(temp, 'pkg');
mkdirSync(repo); mkdirSync(pkg);
const run = (args, cwd = repo) => {
const r = spawnSync('git', args, { cwd, encoding: 'utf8' });
if (r.status !== 0) throw new Error(r.stderr || r.stdout);
return r.stdout.trim();
};
run(['init']); run(['config', 'user.email', 'ci@example.invalid']); run(['config', 'user.name', 'CI']);
writeFileSync(join(repo, 'a.txt'), 'base\n'); writeFileSync(join(repo, 'b.txt'), 'base\n');
run(['add', '.']); run(['commit', '-m', 'base']); const base = run(['rev-parse', 'HEAD']);
writeFileSync(join(repo, 'a.txt'), 'main-new\n'); run(['add', '.']); run(['commit', '-m', 'main']); const main = run(['rev-parse', 'HEAD']);
const content = Buffer.from('payload-old\n');
writeFileSync(join(pkg, 'a.txt'), content);
writeFileSync(join(pkg, 'release-manifest.json'), JSON.stringify({ schema: PACKAGE_MANIFEST_SCHEMA, files: [{ path: 'a.txt', sha256: createHash('sha256').update(content).digest('hex'), size: content.length }], removedFiles: [] }));
const guard = inspectPayload({ packageRoot: pkg, repositoryRoot: repo, baseCommit: base, currentMainCommit: main });
assert(!guard.allowed && guard.conflicts.includes('a.txt'), 'Realer Git-Payload-Rückschritt wurde nicht blockiert.');
} catch (error) {
failures.push(`Payload-Regressionstest fehlgeschlagen: ${error.message}`);
} finally {
rmSync(temp, { recursive: true, force: true });
}
assert(SESSION_SCHEMA === 'vendoo-updater-session-v2', 'Session-Schema ist nicht v2.');
if (failures.length) {
console.error(`Production-Updater-3.4-Gate fehlgeschlagen (${failures.length})`);
failures.forEach(failure => console.error(`- ${failure}`));
process.exit(1);
}
console.log('Production-Updater-3.4-Gate erfolgreich: Zustandsautomat, Grace Period, finaler Head-SHA, Idempotenz, parallele Runs und Payload-Rückschrittschutz geprüft.');
+26 -9
View File
@@ -10,6 +10,7 @@ import {
import { tmpdir } from 'node:os'; import { 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 });
} }
+51
View File
@@ -0,0 +1,51 @@
import { readFileSync } from 'node:fs';
import { resolve, join } from 'node:path';
import { REQUIRED_CHECKS, REQUIRED_WORKFLOWS } from './updater-lifecycle-3.4.mjs';
const root = resolve(import.meta.dirname, '..');
const read = path => readFileSync(join(root, path), 'utf8');
const failures = [];
const assert = (condition, message) => { if (!condition) failures.push(message); };
const ci = read('.github/workflows/ci.yml');
const sync = read('.github/workflows/sync-release-manifest.yml');
assert(ci.includes('workflow_dispatch:'), 'Vendoo CI kann nicht explizit auf dem finalen Head gestartet werden.');
assert(ci.includes('name: Updater-3.4-Paket nach Abnahme'), 'Nachgelagerter Paket-Pflichtcheck fehlt.');
assert(ci.includes('needs: [quality, windows-release-contract]'), 'Das Paket ist nicht strikt von Linux- und Windows-Abnahme abhängig.');
assert(ci.includes('git diff --exit-code -- release-manifest.json'), 'Paketjob bestätigt kein reproduzierbares Root-Manifest.');
assert(ci.includes('vendoo-release-package-v2'), 'Paketjob validiert das Provenienzschema v2 nicht.');
assert(ci.includes('actions/upload-artifact@v4'), 'Das geprüfte Paket wird nicht als CI-Artefakt bereitgestellt.');
assert(ci.includes('powershell-parser-diagnostics'), 'Windows-Parserfehler werden nicht als Diagnoseartefakt gesichert.');
assert(ci.includes('tools/build-deterministic-zip.py'), 'Paketjob verwendet keinen deterministischen ZIP-Build.');
assert(ci.includes('first_hash') && ci.includes('second_hash'), 'Paketjob weist die Byte-Reproduzierbarkeit des ZIPs nicht nach.');
assert(!ci.includes('zip -qr'), 'Paketjob verwendet weiterhin den zeitstempelabhängigen Standard-ZIP-Build.');
const deterministicZip = read('tools/build-deterministic-zip.py');
assert(deterministicZip.includes('FIXED_TIMESTAMP = (1980, 1, 1, 0, 0, 0)'), 'Deterministischer ZIP-Build besitzt keinen fixierten Zeitstempel.');
assert(deterministicZip.includes('sorted(source.rglob'), 'Deterministischer ZIP-Build sortiert die Payload nicht.');
assert(sync.includes("'release/*-production-update-v*'"), 'Manifest-Sync ist nicht versionsneutral auf Releasebranches begrenzt.');
assert(sync.includes('workflow_dispatch:'), 'Manifest-Sync kann nicht auf dem finalen Head registriert werden.');
assert(sync.includes('actions: write'), 'Manifest-Sync darf keine Final-Head-Workflows dispatchen.');
assert(sync.includes('gh workflow run ci.yml'), 'Manifest-Sync dispatcht Vendoo CI nicht auf den finalen Head.');
assert(sync.includes('gh workflow run sync-release-manifest.yml'), 'Manifest-Sync registriert keinen eigenen Lauf auf dem finalen Head.');
assert(sync.includes('git ls-remote origin'), 'Manifest-Sync bestätigt den gepushten Remote-Head nicht.');
assert(sync.includes('cancel-in-progress: false'), 'Manifest-Sync könnte seinen eigenen Final-Head-Dispatch abbrechen.');
assert(!sync.includes('release/1.43.0-production-update-v3'), 'Manifest-Sync enthält weiterhin den alten festen 3.3-Branch.');
for (const name of [
'Syntax, Sicherheit und Release-Gates',
'Windows vollständiger Releasevertrag',
'Updater-3.4-Paket nach Abnahme',
]) assert(REQUIRED_CHECKS.includes(name), `Lifecycle-Pflichtcheck fehlt: ${name}`);
for (const name of ['Vendoo CI', 'Vendoo Release Manifest Sync']) {
assert(REQUIRED_WORKFLOWS.includes(name), `Lifecycle-Pflichtworkflow fehlt: ${name}`);
}
if (failures.length) {
console.error(`Updater-3.4-CI-Lifecycle-Gate fehlgeschlagen (${failures.length})`);
failures.forEach(failure => console.error(`- ${failure}`));
process.exit(1);
}
console.log('Updater-3.4-CI-Lifecycle-Gate erfolgreich: finaler Head, Windows-Diagnose und reproduzierbares Paket sind verbindlich abgesichert.');
+60
View File
@@ -0,0 +1,60 @@
import { existsSync, readFileSync } from 'node:fs';
import { join, resolve } from 'node:path';
const root = resolve(import.meta.dirname, '..');
const failures = [];
const assert = (condition, message) => { if (!condition) failures.push(message); };
const read = path => readFileSync(join(root, path), 'utf8');
const files = [
'UPDATE_VENDOO.bat',
'scripts/updater/Vendoo.Updater.Preflight.ps1',
'scripts/update-vendoo-gui.ps1',
'scripts/updater/Vendoo.Updater.UI.ps1',
'scripts/updater/Vendoo.Updater.Host.ps1',
'scripts/updater/Vendoo.Updater.Worker34.ps1',
];
for (const file of files) assert(existsSync(join(root, file)), `Updater-Startdatei fehlt: ${file}`);
if (existsSync(join(root, 'UPDATE_VENDOO.bat'))) {
const starter = read('UPDATE_VENDOO.bat');
assert(starter.includes('Production Updater 3.4 preflight'), 'Root-Starter meldet nicht Updater 3.4.');
assert(starter.includes('Vendoo.Updater.Preflight.ps1'), 'Root-Starter startet den Preflight nicht.');
assert(!starter.includes('Production Updater 3.3'), 'Root-Starter enthält noch die alte 3.3-Bezeichnung.');
}
if (existsSync(join(root, 'scripts/updater/Vendoo.Updater.Preflight.ps1'))) {
const preflight = read('scripts/updater/Vendoo.Updater.Preflight.ps1');
assert(preflight.includes("$UpdaterVersion = '3.4.0'"), 'Preflight meldet nicht Version 3.4.0.');
assert(preflight.includes('Vendoo.Updater.Worker34.ps1'), 'Preflight prüft Worker34 nicht.');
assert(!preflight.includes("Vendoo.Updater.Worker.ps1'"), 'Preflight verlangt weiterhin den entfernten Worker 3.3.');
}
if (existsSync(join(root, 'scripts/update-vendoo-gui.ps1'))) {
assert(read('scripts/update-vendoo-gui.ps1').includes('Vendoo.Updater.UI.ps1'), 'GUI-Bootstrap startet die Updater-UI nicht.');
}
if (existsSync(join(root, 'scripts/updater/Vendoo.Updater.UI.ps1'))) {
const ui = read('scripts/updater/Vendoo.Updater.UI.ps1');
assert(ui.includes("$UpdaterVersion = '3.4.0'"), 'UI meldet nicht Version 3.4.0.');
assert(ui.includes('Vendoo.Updater.Host.ps1'), 'UI startet den Host nicht.');
assert(ui.includes('Vendoo.Updater.Worker34.ps1'), 'UI übergibt Worker34 nicht an den Host.');
assert(!ui.includes("Vendoo.Updater.Worker.ps1'"), 'UI verweist weiterhin auf den entfernten Worker 3.3.');
}
if (existsSync(join(root, 'scripts/updater/Vendoo.Updater.Host.ps1'))) {
const host = read('scripts/updater/Vendoo.Updater.Host.ps1');
assert(host.includes("$UpdaterVersion = '3.4.0'"), 'Host meldet nicht Version 3.4.0.');
assert(host.includes('[System.Management.Automation.Language.Parser]::ParseFile'), 'Host parst den Worker nicht vor der Ausführung.');
}
if (existsSync(join(root, 'scripts/updater/Vendoo.Updater.Worker34.ps1'))) {
assert(read('scripts/updater/Vendoo.Updater.Worker34.ps1').includes("$UpdaterVersion='3.4.0'"), 'Worker34 meldet nicht Version 3.4.0.');
}
if (failures.length) {
console.error(`Updater-3.4-Startketten-Gate fehlgeschlagen (${failures.length})`);
failures.forEach(failure => console.error(`- ${failure}`));
process.exit(1);
}
console.log('Updater-3.4-Startketten-Gate erfolgreich: Root-Starter, Preflight, UI, Host und Worker34 sind konsistent verdrahtet.');
+7 -5
View File
@@ -1,13 +1,15 @@
{ {
"schema": "vendoo-update-manifest-v1", "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"
]
} }