Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 66 additions & 3 deletions .github/workflows/portable-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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: |
Expand Down
12 changes: 12 additions & 0 deletions PATCH_MANIFEST.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
12 changes: 6 additions & 6 deletions SHA256SUMS.txt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
8 changes: 8 additions & 0 deletions docs/verification/V1.0.6.37_PORTABLE_RELEASE_PACKAGING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
15 changes: 15 additions & 0 deletions scripts/packaging/build_portable_nuitka.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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 = [
Expand Down Expand Up @@ -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": [
Expand Down
42 changes: 42 additions & 0 deletions tests/test_v10637_portable_release_packaging.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"}',
):
Expand Down Expand Up @@ -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}\"",
Expand Down Expand Up @@ -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
Loading