Modernization pass: signing/notarization CI, crash reporting, dialog a11y, full mypy, watch mixin carve#102
Merged
Merged
Conversation
Wire up Developer ID signing and notarytool notarization in the GitHub Actions build job. The pipeline now: - Imports a MACOS_CERTIFICATE (.p12) into a per-run keychain - Signs the .app bundle (hardened runtime + entitlements) so Gatekeeper accepts it without a right-click override - Submits the DMG to Apple's notary service and waits for approval - Staples the ticket to both the DMG and the .app (so the zip is also clean), then zips the stapled .app All six steps are guarded by the presence of the secrets so unsigned CI builds continue to work without any secrets configured. New file: installer/entitlements.plist — Python/PyInstaller needs: - cs.allow-jit + cs.allow-unsigned-executable-memory (Python runtime) - cs.disable-library-validation (bundled third-party .so/.dylib) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3rfxvrhYUyjTsifsuJw9s
…gning - Parse notarytool's JSON verdict instead of trusting its exit code; on a non-Accepted status, fetch and print Apple's notarization log and fail. - Windows: sign the app exe before packaging and the Setup.exe after, when WINDOWS_CERTIFICATE / WINDOWS_CERTIFICATE_PWD secrets are present; skip with a warning otherwise (same guarded pattern as the macOS steps). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3rfxvrhYUyjTsifsuJw9s
CI's named-file mypy list let new modules drift out unchecked (ui_dialogs.py and the worker/discover scripts weren't covered). [tool.mypy] files=['.'] now owns the set, with deadline/ excluded deliberately (old Python host, same as ruff). Fixes the handful of test-hygiene errors the wider net surfaced, and switches app_window/base.py to the import-as-alias conditional-base idiom that every mypy version accepts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3rfxvrhYUyjTsifsuJw9s
core/crash.py: structured crash reports from two capture paths — the existing UI excepthook now also writes a report file, and faulthandler is armed into a per-pid dump so native crashes (segfaults in Qt/drivers) leave evidence a clean exit removes. On the next launch the window sweeps dead-pid dumps into reports and offers the newest one: view it, or open a prefilled GitHub issue. Nothing is sent anywhere unless the user submits the issue themselves. Windows pid-liveness uses OpenProcess (os.kill(pid, 0) terminates there). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3rfxvrhYUyjTsifsuJw9s
MessageDialog: window title/accessible name+description set for assistive tech, initial focus lands on the default button, arrow keys walk the button row (wrapping, like native message boxes), body text is selectable, and all buttons are autoDefault so Return activates the FOCUSED button — previously Return with focus on Cancel fired the (possibly destructive) default. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3rfxvrhYUyjTsifsuJw9s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3rfxvrhYUyjTsifsuJw9s
…xin.py The full ingest pipeline — panel load/apply, auto-map + previz job builders, folder/deliver pickers, first-run flag, and the dry-run assembly preview — moves verbatim (14 methods, ~310 lines) out of app_qt.py into the existing app_window mixin pattern. base.py declares the watch-related shared state so the mixin type-checks against the concrete window. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3rfxvrhYUyjTsifsuJw9s
Formalizes what live offscreen bug-hunts previously checked by hand: panel load/apply round-trips, first-run acknowledgement persistence, watch settings surviving a window rebuild, broken-pattern ingest resilience, the hyphenated real-world convention building a previz job end to end, and the crash-report offer (headless no-op, windowed offer + acknowledge). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3rfxvrhYUyjTsifsuJw9s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3rfxvrhYUyjTsifsuJw9s
… QCoreApplication CI's pinned mypy flags app.platformName() on the instance() return type (QCoreApplication); the static/class access is equivalent at runtime and correctly typed. Local mypy accepted the old form, CI's didn't. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3rfxvrhYUyjTsifsuJw9s
…arse An empty app-specific password produced Apple's 401 followed by a raw JSONDecodeError traceback. Now: explicit error if the secret is empty, and the verdict parse falls back to 'Unknown' with a pointer at notarytool's own output instead of tracebacking when the response isn't JSON. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3rfxvrhYUyjTsifsuJw9s
…signing Apple rejected the first real notarization: no secure timestamp anywhere and no hardened runtime on the main executable. Causes: --timestamp was never passed, per-file signing errors were swallowed (2>/dev/null || true), and the bundle was sealed with the deprecated --deep sign (which skips the runtime option on nested code) plus a stray --verify in the sign invocation. Now every Mach-O in the bundle is signed individually and loudly, frameworks and the .app are sealed last, and verification is strict. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3rfxvrhYUyjTsifsuJw9s
The sign step no-ops without the cert secret, so the job sailed on and submitted an unsigned bundle to Apple — guaranteed rejection with a wall of 'no valid Developer ID / no timestamp' errors that pointed away from the real cause. Now the notarize step warns and ships the unsigned zip instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3rfxvrhYUyjTsifsuJw9s
The Mach-O sweep signed the whole bundle, but sealing the outer .app failed with 'No such file or directory' — PyInstaller strips Qt header directories and leaves their symlinks dangling, which codesign refuses to seal over. Broken symlinks serve no runtime purpose; delete them (printed for the log) right before the bundle seal. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3rfxvrhYUyjTsifsuJw9s
…, persisted ingest history Closes the three gaps from the watch/automapper audit: - Silent ignores: previz ingest now counts ready clips that didn't land in any group (pattern/type mismatch, unmapped screen, superseded) and surfaces a warning chip in the Activity header; clicking opens the dry-run preview, which explains every skipped file. - Subfolders: an 'Include subfolders' toggle in the Source card switches the scan to a pruned os.walk (auto-render output excluded as a whole subtree); rename/deletion reconciliation is subtree-aware; persisted in the profile. - Ingest history: the Activity feed writes through to logs/ingest_history.log and preloads its tail on launch, so ingest is auditable across restarts; Clear empties the visible feed only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3rfxvrhYUyjTsifsuJw9s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3rfxvrhYUyjTsifsuJw9s
The instance() or QApplication([]) idiom satisfies CI's mypy (non-Optional after 'or') and keeps the test runnable in isolation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3rfxvrhYUyjTsifsuJw9s
- assets/help/img/: 10 real screenshots generated from the live app offscreen (main window, whole Watch panel, per-card close-ups incl. the chip builder parsing the production naming convention, the skipped-clips badge in action, the dry-run Preview Assembly, and the queue). - In-app Help → User Guide: the Watch & Auto-render tab is rewritten around the panel's actual ①–⑤ flow with embedded screenshots (QTextBrowser search paths resolve the bundled images; degrades to text-only if absent), and the Getting Started tab gains the main-window shot. - docs/user-guide.md: comprehensive GitHub-facing guide — quick start, watch pipeline deep dive, filename pattern reference (with the real-world hyphenated convention), queue, engines, crash reports, troubleshooting/FAQ. - README: links the guide; the macOS download note now reflects the signed + notarized reality. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3rfxvrhYUyjTsifsuJw9s
Ships the modernization pass (signing/notarization CI, crash reporting, dialog a11y, whole-repo mypy, watch-mixin carve), the watch-folder upgrades (skipped- clips badge, subfolder watching, persisted ingest history), and the full user guide with screenshots. First fully signed + notarized release. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3rfxvrhYUyjTsifsuJw9s
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the six gaps from the "is my app modern / industry standard?" audit — the app-quality work the audit called out, done in one pass:
1 · macOS signing + notarization, Windows signing scaffold (CI)
notarytoolsubmission with a parsed verdict — a rejected submission now fails the job loudly with Apple's own notarization log instead of dying obliquely at stapling. Staple → zip ordering preserved so both the DMG and the zip are Gatekeeper-clean.Setup.exeafter Inno Setup) that activate whenWINDOWS_CERTIFICATE/WINDOWS_CERTIFICATE_PWDsecrets exist and skip with a warning otherwise — same pattern as the macOS steps.2 · mypy owns the whole repo
[tool.mypy] files = ["."]— CI's named-file list letui_dialogs.pyand the worker/discover scripts drift out of type-checking entirely. Now a new module can't be forgotten. Surfaced and fixed a handful of test-hygiene errors, and switchedapp_window/base.pyto the import-as-alias conditional-base idiom every mypy version accepts.3 · Crash reporting (opt-in, local-only)
New
core/crash.py: the existing excepthook dialog now also writes a structured report file, andfaulthandleris armed into a per-pid dump so native crashes (segfaults in Qt/drivers) leave evidence — a clean exit removes its own dump. Next launch sweeps dead-pid dumps and offers the report once: view it, or open a prefilled GitHub issue. Nothing leaves the machine unless the user submits it. (Windows pid-liveness usesOpenProcess;os.kill(pid, 0)terminates there.)4 · Dialog accessibility
MessageDialog: window title + accessible name/description for screen readers, initial focus on the default button, arrow keys walk the button row, selectable body text — and all buttons areautoDefault, fixing a real hazard where Return with focus on "Cancel" fired the destructive default.5 · UI-layer wiring tests
tests/test_watch_wiring.py+tests/test_ui_dialogs.py+tests/test_crash.py(24 new tests): watch panel load/apply round-trips, profile persistence across a window rebuild, broken-pattern ingest resilience, the hyphenated real-world convention building a previz job end to end, dialog keyboard semantics, crash-offer gating.6 · First carve of the monolith
The complete watch/auto-render/previz wiring (14 methods, ~310 lines) moved verbatim from
app_qt.pyintoapp_window/watch_mixin.py, following the established mixin pattern;base.pydeclares the shared state so it type-checks.app_qt.py: 4,846 → 4,535 lines.Verification: ruff clean · mypy clean over 83 files · 234 passed + 2 skipped (full suite) · smoke suite green · crash flow verified live offscreen (fault dump → sweep → offer → acknowledge).
MACOS_*/NOTARIZATION_*repo secrets are set (in progress on the owner's machine).🤖 Generated with Claude Code
https://claude.ai/code/session_01F3rfxvrhYUyjTsifsuJw9s
Generated by Claude Code