Skip to content

Modernization pass: signing/notarization CI, crash reporting, dialog a11y, full mypy, watch mixin carve#102

Merged
hayhamLT merged 19 commits into
mainfrom
claude/modest-wright-6lljus
Jul 2, 2026
Merged

Modernization pass: signing/notarization CI, crash reporting, dialog a11y, full mypy, watch mixin carve#102
hayhamLT merged 19 commits into
mainfrom
claude/modest-wright-6lljus

Conversation

@hayhamLT

@hayhamLT hayhamLT commented Jul 1, 2026

Copy link
Copy Markdown
Owner

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)

  • macOS: Developer ID signing (temp keychain, hardened runtime, PyInstaller entitlements), notarytool submission 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.
  • Windows: guarded Authenticode steps (app exe before packaging, Setup.exe after Inno Setup) that activate when WINDOWS_CERTIFICATE / WINDOWS_CERTIFICATE_PWD secrets exist and skip with a warning otherwise — same pattern as the macOS steps.
  • All steps are no-ops without secrets, so dev builds keep working unsigned.

2 · mypy owns the whole repo

[tool.mypy] files = ["."] — CI's named-file list let ui_dialogs.py and 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 switched app_window/base.py to 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, and faulthandler is 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 uses OpenProcess; 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 are autoDefault, 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.py into app_window/watch_mixin.py, following the established mixin pattern; base.py declares 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).

⚠️ After merge: signing/notarization activates once the 6 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

claude added 19 commits July 1, 2026 02:54
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
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
@hayhamLT hayhamLT merged commit b203876 into main Jul 2, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants