From 299e93a89db3d30505350f474f79eefc330ee923 Mon Sep 17 00:00:00 2001 From: vib-tools Date: Mon, 17 Aug 2026 11:18:27 -0700 Subject: [PATCH] test(release): capture portable startup diagnostics v1.0.6.37 --- .github/workflows/portable-release.yml | 69 ++++++++++++++++++- PATCH_MANIFEST.md | 12 ++++ SHA256SUMS.txt | 12 ++-- ...6.37_portable_release_packaging_scope.json | 16 ++++- .../V1.0.6.37_PORTABLE_RELEASE_PACKAGING.md | 8 +++ scripts/packaging/build_portable_nuitka.py | 15 ++++ .../test_v10637_portable_release_packaging.py | 42 +++++++++++ 7 files changed, 164 insertions(+), 10 deletions(-) diff --git a/.github/workflows/portable-release.yml b/.github/workflows/portable-release.yml index 8854b2e..82796fa 100644 --- a/.github/workflows/portable-release.yml +++ b/.github/workflows/portable-release.yml @@ -60,7 +60,13 @@ jobs: - name: Build Nuitka standalone OneDir portable archive shell: pwsh - run: python scripts/packaging/build_portable_nuitka.py + run: | + if ("${{ github.event_name }}" -eq "workflow_dispatch") { + $env:VIBRAPILOT_PORTABLE_DIAGNOSTICS = "1" + } else { + $env:VIBRAPILOT_PORTABLE_DIAGNOSTICS = "0" + } + python scripts/packaging/build_portable_nuitka.py - name: Forensic portable artifact verification shell: pwsh @@ -78,17 +84,74 @@ jobs: run: | $version = python -c "from config.AppConfig.app import VERSION; print(VERSION)" $name = "VibraPilot-$version-Windows-x64-Portable" - $exe = Join-Path $PWD "release/$name/VibraPilot.exe" + $releaseDir = Join-Path $PWD "release/$name" + $exe = Join-Path $releaseDir "VibraPilot.exe" + $stdoutLog = Join-Path $releaseDir "VibraPilot.stdout.log" + $stderrLog = Join-Path $releaseDir "VibraPilot.stderr.log" $env:VIB_TOOLS_DATA_DIR = Join-Path $env:RUNNER_TEMP "VibraPilotPortableSmoke" New-Item -ItemType Directory -Force -Path $env:VIB_TOOLS_DATA_DIR | Out-Null - $process = Start-Process -FilePath $exe -WorkingDirectory (Split-Path $exe) -PassThru + + # A standalone portable acceptance test must not inherit the live source + # checkout from the build runner. Keep only the explicit disposable data root. + Remove-Item Env:PYTHONPATH -ErrorAction SilentlyContinue + Remove-Item Env:PYTHONHOME -ErrorAction SilentlyContinue + $env:PYTHONNOUSERSITE = "1" + $env:QT_DEBUG_PLUGINS = "1" + + $startedAt = Get-Date + $process = Start-Process -FilePath $exe -WorkingDirectory $releaseDir -PassThru Start-Sleep -Seconds 12 $process.Refresh() if ($process.HasExited) { + Write-Host "::group::VibraPilot packaged startup diagnostics" + Write-Host "ExitCode=$($process.ExitCode)" + Write-Host "RuntimeSeconds=$([math]::Round(((Get-Date) - $startedAt).TotalSeconds, 3))" + Write-Host "PYTHONPATH=$env:PYTHONPATH" + Write-Host "PYTHONHOME=$env:PYTHONHOME" + Write-Host "VIB_TOOLS_DATA_DIR=$env:VIB_TOOLS_DATA_DIR" + if (Test-Path $stderrLog) { + Write-Host "--- Nuitka forced stderr ---" + Get-Content $stderrLog -ErrorAction Continue + } else { + Write-Host "Nuitka forced stderr file was not created." + } + if (Test-Path $stdoutLog) { + Write-Host "--- Nuitka forced stdout ---" + Get-Content $stdoutLog -ErrorAction Continue + } else { + Write-Host "Nuitka forced stdout file was not created." + } + $appLogDir = Join-Path $env:VIB_TOOLS_DATA_DIR "Logs" + if (Test-Path $appLogDir) { + Write-Host "--- VibraPilot application logs ---" + Get-ChildItem $appLogDir -File -ErrorAction Continue | ForEach-Object { + Write-Host "### $($_.FullName)" + Get-Content $_.FullName -ErrorAction Continue + } + } + Write-Host "--- Qt platform payload ---" + Get-ChildItem (Join-Path $releaseDir "PySide6\qt-plugins\platforms") -File -ErrorAction Continue | + Select-Object FullName,Length | Format-Table -AutoSize | Out-String | Write-Host + Write-Host "::endgroup::" throw "VibraPilot.exe exited during startup smoke with code $($process.ExitCode)." } Stop-Process -Id $process.Id -Force + - name: Upload portable startup diagnostics on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: VibraPilot-Windows-x64-Portable-Startup-Diagnostics + path: | + release/*-Windows-x64-Portable/*.stdout.log + release/*-Windows-x64-Portable/*.stderr.log + release/*-Windows-x64-Portable-BUILD-INFO.json + release/*-Windows-x64-Portable-VERIFY.json + .build/portable-nuitka/nuitka-compilation-report.xml + ${{ runner.temp }}/VibraPilotPortableSmoke/Logs/*.log + if-no-files-found: warn + retention-days: 7 + - name: Show portable size inventory shell: pwsh run: | diff --git a/PATCH_MANIFEST.md b/PATCH_MANIFEST.md index 1e40da2..83657ea 100644 --- a/PATCH_MANIFEST.md +++ b/PATCH_MANIFEST.md @@ -122,3 +122,15 @@ Before a public `v1.0.6.37` tag/release, the Actions candidate must pass: - The test-only deadlock guard is widened to 300 seconds. Successful runs do not wait for the guard. - The new portable release workflow is pinned to `windows-2022` so the compiler host does not drift with the `windows-latest` label; the historical general CI workflow itself is unchanged. - No application business logic, database schema, browser, workflow, licensing, persistence, reporting, Chromium, WiX, or MSI behavior changes are authorized by this correction. + +## Portable RC startup diagnostic correction — run 32048312168 + +- Failed Actions run/job: `32048312168` / `95441285784` on pinned `windows-2022`. +- Source verification, full pytest/unittest, Nuitka 4.1.3 standalone compilation, OneDir creation, ZIP creation, SHA-256 generation, and the portable forensic verifier all passed before the startup gate. +- Candidate evidence before smoke: 827 files, 324,244,118 uncompressed bytes, 116,446,695 ZIP bytes, ZIP SHA-256 `90f95746a381d089d4ad3fe399f0087cabcba28ea9d26abc680cc95d3c0ad65c`, `bundled_chromium=false`, `wix_msi=false`. +- The packaged executable exited with code `1` before the 12-second smoke checkpoint. +- The failed workflow had `--windows-console-mode=disable`, no forced stdout/stderr capture, no application-log dump, and skipped artifact upload after the smoke failure. Therefore the failed run does not contain the underlying Python traceback; no production runtime root cause is asserted without that evidence. +- Manual `workflow_dispatch` RC builds now embed Nuitka forced stdout/stderr files only for diagnostic acceptance. Version-tag builds keep the normal console-disabled release behavior without forced diagnostic files. +- The packaged smoke clears inherited `PYTHONPATH` and `PYTHONHOME`, sets `PYTHONNOUSERSITE=1`, and uses only the disposable `VIB_TOOLS_DATA_DIR` so the acceptance run cannot accidentally consume the live checkout. +- Failed startup diagnostics are uploaded separately with `if: failure()`; a failed smoke still blocks the normal candidate artifact and all release publication. +- Production `src/`, workflow/browser/licensing/task persistence behavior, Chromium policy, WiX/MSI scope, and version identity are unchanged by this correction. diff --git a/SHA256SUMS.txt b/SHA256SUMS.txt index 72e39c4..3577457 100644 --- a/SHA256SUMS.txt +++ b/SHA256SUMS.txt @@ -1,23 +1,23 @@ -d4181caff95bbb3a005b6297461b3bd8ac63ca0c14800ae392f6885f9e64bc01 .github/workflows/portable-release.yml +342bd815df0fa4e34170eaaecc5c47abc2619a9d600718b7f7ac564fa4586a62 .github/workflows/portable-release.yml 21ef50c53363a36ec2e5f0a88079b6aff66459cbd0ce498cac0e5ba0c5488d1c CHANGELOG.md 64b44c7876de8f1deeb9f6329c8c135e957d49ac292e68d1c28212a4068ad7ce CITATION.cff 06f35d961850efbdbf841970b3928bfe94ab4c05f100f018f848c44136153087 COMPATIBILITY.md 0048c6a6c4ec9ecf07ae0b63787affed86fcdf8599423c3c699eafb9c0d122a0 DELTA_FILE_LIST.txt -cce1116b4dd7fc10fed07bb1ae4a0cb9c3274d0752275553526ea1c11bb27e65 PATCH_MANIFEST.md +2db8605887750367b3ecb1780852837073d19e461e783abed47d33135e986154 PATCH_MANIFEST.md 68d1d9c65a8af8b9ed73ec62d9631a17123bb276ef0b1d96dd03316aa2c5c10b PROJECT_STRUCTURE.md bfe4a2107c8161738146594118bd675b306cfa88b109c04701290f1732bb6a52 README.md 68d07dfda54bb9b149f94aa75f90e5a182a8e3d71ebba2d03288636c2930c2ef ROADMAP.md 46d03c40ca2ebfbc7596baa0115149f852f6caa7b9074cf44ce8a7781d8c4844 UPDATE_LOG.md 2fdcb13b793291c565345eb3d01a591814aafa02cdd101b691e3f05ff286aecc VERSIONING.md 7356a63cdb8b60f6caec3c9041d2ee253e4f8258fe8c58c9b885653bd656c5f4 config/AppConfig/app.py -a2dd0a0957800f62f7cfaf4dbf309047e7ce72d30e9c2b65b0ce501489f13788 config/verification/v1.0.6.37_portable_release_packaging_scope.json +b085bf4adb1635e46ee663f5c9c5468ceaef218216f2a7d8af66ded662f2aefe config/verification/v1.0.6.37_portable_release_packaging_scope.json a23d91b995477c31f2d7d07e6d7a0bfd1d7b9e058be72a345242454714e986ad docs/docs.manifest.ygit 27107b247780cef0948a1d8fa9ea3bef8fff18a3aa5350f0c6f8c4e9e930ebca docs/index.md 1074c30f0fbc197178afecc6cc56c14573448bab97e0ec2a7492f5df98b66284 docs/updates/v1.0.6.37-portable-release-packaging.md -a767d94f812724311dd647baa57839834d087d11fb269e0e032c6b3cebcde911 docs/verification/V1.0.6.37_PORTABLE_RELEASE_PACKAGING.md +07b2bc3eea5dfbbb04340816e8905e216d70e31b664cb0ddf40d5cef1f0272c9 docs/verification/V1.0.6.37_PORTABLE_RELEASE_PACKAGING.md e081e5027f50921e326e82d31876158b53c1213244fa88b9bf90867e77875024 pyproject.toml 4c84508a5399ce66ed3d7393e672fa5658d6a55bb6f86ac386dce9ce27f4f861 requirements-portable.txt -ed1c8e6b72342e066773eaad0efa7ba28b04b70b8da7d1583b9350d68af38c39 scripts/packaging/build_portable_nuitka.py +58c3d77e73b0c1cf9cc5b9c7e92f26ae90890621513e023f484a296c0df50dfd scripts/packaging/build_portable_nuitka.py 902e1070b29d79e1c4f7e3d2dcad0f5834746ec8dff93aeac13f866385ecaff9 scripts/packaging/verify_portable_release.py daaa7aad5ae5ee67ef4def1be2c8709b927e7b72a563dee7c6e0988eabb48d66 scripts/verify_repository.py ae939cf86db45256f4c28fa8d08c86ace026ad5863a5ae3c5f65d181b216b5d6 src/vibrapilot/backend.py @@ -29,5 +29,5 @@ ae939cf86db45256f4c28fa8d08c86ace026ad5863a5ae3c5f65d181b216b5d6 src/vibrapilot f76caed35bf5d121043c4c9be8e8e7d5ef15f4d12054ef3a5bc9e722eb1af589 tests/test_task_runtime_store.py 9b9292fb73bf20e764c2bea7a44f519baa331ec62916231937904451257953ef tests/test_v10613_phase01_verification_fix.py 7fc5a834c523c606070f0f2bae32ed32dc2aa13659a51856b5fdc73b7a5067c2 tests/test_v10628_pr11_windows_multitask_regression.py -239ee59f5e8db5f62ad0fdb8965d39ac75e8e7b861db31312b86c9c04c6bef5b tests/test_v10637_portable_release_packaging.py +12d6fa63044d192dabe7e783c9e553717ff63bf47af0dcaf42e85faf534737f5 tests/test_v10637_portable_release_packaging.py 8224c0af48f318c56dbf2e322825e2c9d6813a7d5e9148342966deed258720b3 vibproject.ygit diff --git a/config/verification/v1.0.6.37_portable_release_packaging_scope.json b/config/verification/v1.0.6.37_portable_release_packaging_scope.json index 23837aa..53f98e5 100644 --- a/config/verification/v1.0.6.37_portable_release_packaging_scope.json +++ b/config/verification/v1.0.6.37_portable_release_packaging_scope.json @@ -93,5 +93,19 @@ "production_task_runtime_store_changed": false, "application_business_logic_changed": false }, - "frozen_task_runtime_store_sha256": "b4b581c936479a6a3f334170c033bae53f1d216e562cc1aff8b3e54e728dcf26" + "frozen_task_runtime_store_sha256": "b4b581c936479a6a3f334170c033bae53f1d216e562cc1aff8b3e54e728dcf26", + "portable_startup_diagnostic_correction": { + "classification": "RC startup observability and clean-environment smoke only; no production runtime change", + "failed_run_id": 32048312168, + "failed_job_id": 95441285784, + "nuitka_compile_passed": true, + "artifact_forensic_verifier_passed": true, + "startup_exit_code": 1, + "evidence_gap": "The GUI executable was built with windows-console-mode=disable and the smoke step captured neither forced stdout/stderr nor application logs; the failed run therefore cannot identify the underlying Python exception.", + "diagnostic_capture_only_on_workflow_dispatch": true, + "clean_smoke_environment": true, + "production_source_changed": false, + "tag_release_behavior_changed": false, + "public_release_still_blocked": true + } } diff --git a/docs/verification/V1.0.6.37_PORTABLE_RELEASE_PACKAGING.md b/docs/verification/V1.0.6.37_PORTABLE_RELEASE_PACKAGING.md index 75ebe8c..3f7d061 100644 --- a/docs/verification/V1.0.6.37_PORTABLE_RELEASE_PACKAGING.md +++ b/docs/verification/V1.0.6.37_PORTABLE_RELEASE_PACKAGING.md @@ -30,3 +30,11 @@ - Record ZIP size, SHA-256 and largest-file inventory from the GitHub Actions artifact. A manually dispatched GitHub Actions candidate is not a public release until these owner checks pass. Tag publication is additionally blocked unless the tag is exactly `v{AppConfig.VERSION}`. + +## RC startup observability correction + +The first real `workflow_dispatch` build (`32048312168`, job `95441285784`) proved that source verification, Nuitka compilation, OneDir packaging, checksum verification, and the no-Chromium/no-WiX policy all work on the pinned Windows 2022 runner. The resulting candidate contained 827 files and produced a 116,446,695-byte ZIP with SHA-256 `90f95746a381d089d4ad3fe399f0087cabcba28ea9d26abc680cc95d3c0ad65c`. + +The executable then exited with code 1 during startup smoke. Because that candidate used `--windows-console-mode=disable` and the workflow did not capture forced stdout/stderr or application logs, the failed run contains no Python traceback. The exact runtime exception is therefore intentionally not guessed. + +The correction remains packaging/verification-only: manual RC builds enable Nuitka forced stdout/stderr capture, smoke execution removes inherited `PYTHONPATH`/`PYTHONHOME` and sets `PYTHONNOUSERSITE=1`, and startup diagnostics are uploaded on failure. Tag builds retain the normal GUI no-console behavior and public release remains blocked unless the startup smoke passes. diff --git a/scripts/packaging/build_portable_nuitka.py b/scripts/packaging/build_portable_nuitka.py index 28e9c98..182638a 100644 --- a/scripts/packaging/build_portable_nuitka.py +++ b/scripts/packaging/build_portable_nuitka.py @@ -90,6 +90,11 @@ def validate_source() -> None: run(sys.executable, "scripts/verify_repository.py") +def diagnostic_build_enabled() -> bool: + """Return True only for explicitly requested RC startup diagnostics.""" + return os.environ.get("VIBRAPILOT_PORTABLE_DIAGNOSTICS", "").strip() == "1" + + def build_nuitka() -> Path: icon_path = ROOT / "assets" / "icons" / "app.ico" env = { @@ -124,6 +129,15 @@ def build_nuitka() -> Path: f"--include-data-files={ROOT / 'frozen_design_source' / 'CURRENT_FOUNDATION_TOKENS.json'}=frozen_design_source/CURRENT_FOUNDATION_TOKENS.json", str(ROOT / "run.py"), ] + if diagnostic_build_enabled(): + # The final GUI build intentionally has no console. During manual RC builds, + # force Python output to files beside the executable so a startup exception + # cannot be lost by the hosted runner smoke test. Tag/release builds omit + # these diagnostic redirections. + args[-1:-1] = [ + "--force-stdout-spec={PROGRAM_BASE}.stdout.log", + "--force-stderr-spec={PROGRAM_BASE}.stderr.log", + ] run(*args, env=env) candidates = [ @@ -215,6 +229,7 @@ def build_info(dist_dir: Path) -> dict[str, object]: "bundled_browser": False, "system_google_chrome_required": True, "wix_msi": False, + "startup_diagnostics_embedded": diagnostic_build_enabled(), "file_count_before_manifests": len(files), "uncompressed_bytes_before_manifests": total_bytes, "largest_files": [ diff --git a/tests/test_v10637_portable_release_packaging.py b/tests/test_v10637_portable_release_packaging.py index 7aacc6c..76ce76c 100644 --- a/tests/test_v10637_portable_release_packaging.py +++ b/tests/test_v10637_portable_release_packaging.py @@ -54,6 +54,9 @@ def test_nuitka_builder_is_standalone_onedir_and_never_installs_browser(): '"--include-package=playwright"', '"--include-distribution-metadata=playwright"', '"--playwright-include-browser=none"', + '"--force-stdout-spec={PROGRAM_BASE}.stdout.log"', + '"--force-stderr-spec={PROGRAM_BASE}.stderr.log"', + "diagnostic_build_enabled", "validate_compiled_runtime", 'browser_binaries = {"chrome.exe", "chromium.exe", "headless_shell.exe"}', ): @@ -88,6 +91,12 @@ def test_github_action_builds_candidate_and_only_tag_path_publishes_release(): "build_portable_nuitka.py", "verify_portable_release.py", "actions/upload-artifact@v4", + "VIBRAPILOT_PORTABLE_DIAGNOSTICS", + "Remove-Item Env:PYTHONPATH", + "Remove-Item Env:PYTHONHOME", + 'PYTHONNOUSERSITE = "1"', + "VibraPilot-Windows-x64-Portable-Startup-Diagnostics", + "if: failure()", "if: startsWith(github.ref, 'refs/tags/v')", "gh release create", "expected = f\"v{VERSION}\"", @@ -184,3 +193,36 @@ def test_ci_stability_fix_keeps_production_store_frozen_and_widens_only_test_gua stress = (ROOT / "tests/test_task_runtime_store.py").read_text(encoding="utf-8") assert "CONCURRENT_STORE_TEST_TIMEOUT_SECONDS = 300.0" in stress + + +def test_rc_startup_diagnostic_correction_is_nonproduction_and_fail_closed(): + scope = json.loads( + (ROOT / "config/verification/v1.0.6.37_portable_release_packaging_scope.json").read_text( + encoding="utf-8" + ) + ) + correction = scope["portable_startup_diagnostic_correction"] + assert correction["failed_run_id"] == 32048312168 + assert correction["failed_job_id"] == 95441285784 + assert correction["nuitka_compile_passed"] is True + assert correction["artifact_forensic_verifier_passed"] is True + assert correction["startup_exit_code"] == 1 + assert correction["production_source_changed"] is False + assert correction["tag_release_behavior_changed"] is False + assert correction["diagnostic_capture_only_on_workflow_dispatch"] is True + assert correction["clean_smoke_environment"] is True + + workflow = (ROOT / ".github/workflows/portable-release.yml").read_text(encoding="utf-8") + assert 'if ("${{ github.event_name }}" -eq "workflow_dispatch")' in workflow + assert '$env:VIBRAPILOT_PORTABLE_DIAGNOSTICS = "1"' in workflow + assert 'Remove-Item Env:PYTHONPATH -ErrorAction SilentlyContinue' in workflow + assert 'Remove-Item Env:PYTHONHOME -ErrorAction SilentlyContinue' in workflow + assert 'name: VibraPilot-Windows-x64-Portable-Startup-Diagnostics' in workflow + assert 'if: failure()' in workflow + assert 'VibraPilot.stderr.log' in workflow + assert 'VibraPilot.stdout.log' in workflow + + builder = (ROOT / "scripts/packaging/build_portable_nuitka.py").read_text(encoding="utf-8") + assert 'VIBRAPILOT_PORTABLE_DIAGNOSTICS' in builder + assert '--force-stdout-spec={PROGRAM_BASE}.stdout.log' in builder + assert '--force-stderr-spec={PROGRAM_BASE}.stderr.log' in builder