Skip to content

Fix packaging, release versioning and the untracked test suite - #2

Merged
SecretLUL merged 5 commits into
mainfrom
fix/packaging-and-release-versioning
Aug 11, 2026
Merged

SecretLUL merged 5 commits into
mainfrom
fix/packaging-and-release-versioning

Conversation

@SecretLUL

Copy link
Copy Markdown
Owner

The application code was in good shape — the problems were all at the boundary between the repo and everything that leaves it. As shipped, neither a fresh clone nor any release artifact worked for anyone else.

Packaging

The released Windows binary could not start. ui/icons.py imports Pillow at module level, but PIL was listed in UNNEEDED_PACKAGES (--exclude-module PIL) and deleted again by prune_unneeded(). Verified against the previously built dist/: zero PIL files. Every download failed with No module named 'PIL'.

  • Removed PIL from the exclude list and added Pillow to requirements.txt — a fresh clone could not start the app either.
  • Added verify_bundle(): the build now fails if a module the app imports at startup is missing from the bundle, rather than surfacing on a user's desktop.
  • ui/app.py now falls back to plain pyaudio when pyaudiowpatch is absent, matching what capture.py already did. The Linux and macOS artifacts could not start without this, since CI installs plain PyAudio there.
  • requirements.txt split by platform: PyAudioWPatch on Windows, PyAudio elsewhere.

Verified on the real artifact — the built .exe starts and has Pillow's native extensions loaded at runtime:

_imaging.cp312-win_amd64.pyd
_imagingft.cp312-win_amd64.pyd
_portaudiowpatch.cp312-win_amd64.pyd
libsndfile_x64.dll

Release versioning

Version was hardcoded as v1.0.0 in build_release.py and again in the Linux and macOS CI steps, while the release notes linked ${{ github.ref_name }} filenames. Tagging v1.1.0 produced assets named v1.0.0 and download links that 404.

  • resolve_version() resolves once, in order: CLI argument → RELEASE_VERSION → git tag → v0.0.0-dev.
  • build_release.py is now cross-platform and derives the archive name and format per OS, so all three CI jobs call the same script instead of duplicating naming rules.
  • A version job resolves the tag once; every job and the release notes read that single value.
  • CI runs the test suite before building, and publishing is gated on github.ref_type == 'tag'.

macOS assets were labelled macos-universal, but PyInstaller builds host-architecture-only unless target_arch=universal2 is set — the arm64 runner produced an arm64-only binary advertised to Intel users. The slug now follows platform.machine(): macos-arm64 or macos-x64.

platform machine archive
darwin arm64 AudioTranscriber-v1.4.2-macos-arm64.zip
darwin x86_64 AudioTranscriber-v1.4.2-macos-x64.zip
win32 AMD64 AudioTranscriber-v1.4.2-windows-x64.zip
linux x86_64 AudioTranscriber-v1.4.2-linux-x64.tar.gz

Test suite

.gitignore contained tests/ and run_tests.py, so the entire suite was excluded from the repository despite the README documenting python run_tests.py and "128+ unit tests". git ls-files confirmed not a single test file was tracked. Both entries removed and the suite committed.

While fixing the unused OUT_DIR import, 13 tests failed and exposed a second issue: they patched pipeline.OUT_DIR, which _process never reads — the output path comes from Settings.get_output_dir(). The patch was always a no-op and every finalizer test wrote into the real output/ directory. Tests now steer it via settings.output_dir.

Correctness

  • FileFinalizer.run_async called paths.safe_output_name() while pipeline.py never imported pathsNameError on the default-name path. Latent because the GUI and every test passed base_name explicitly; a regression test now covers it.
  • Removed a duplicated write of the transcript file.
  • ElevenLabsBackend.cancel() only set a flag nothing read; it now closes the live response, so a cancelled upload no longer sits on its 900 s timeout.
  • seg_end is reset when flushing an ElevenLabs segment, so a following segment cannot inherit the previous end time.
  • loader.py no longer swallows its own "no sample data" error into the FFmpeg fallback, which reported a misleading "FFmpeg not found".
  • Slider honours state="disabled"_enabled was assigned but never initialised or checked.
  • app.py no longer binds two different buttons to self.save_btn.
  • Redacted the (already revoked) example API key in legacy/settings.v1.json.

Verification

  • 129 tests pass (128 existing + 1 regression), pyflakes clean.
  • Release build runs end to end and the resulting binary launches with all native dependencies loaded.
  • Version resolution verified in all four modes; workflow YAML parses and job graph checked.

Not verified: the Linux and macOS builds cannot be exercised from Windows — the import fallback and CI changes are correct by inspection, but the first tag push will be the real test.

🤖 Generated with Claude Code

SecretLUL and others added 5 commits August 11, 2026 17:46
The application code was sound, but nothing that left the machine worked:
the repo shipped without tests, the Windows build shipped without Pillow,
and every release archive was named v1.0.0 regardless of the tag.

Packaging
  * Add Pillow to requirements.txt. ui/icons.py imports it at module level,
    so a fresh clone could never start the app.
  * Stop excluding PIL from the PyInstaller build. It was in UNNEEDED_PACKAGES
    and additionally deleted by prune_unneeded(), so every released binary
    died with "No module named 'PIL'".
  * Add verify_bundle(): the build now fails if a module the app imports at
    startup is missing from the bundle, instead of finding out on a user's
    desktop.
  * ui/app.py falls back to plain pyaudio when pyaudiowpatch is absent, the
    same way capture.py already did. The Linux and macOS artifacts could not
    start without this.
  * Split requirements by platform: PyAudioWPatch on Windows, PyAudio elsewhere.

Release versioning
  * build_release.py resolves the version from an argument, RELEASE_VERSION,
    the git tag, or a dev fallback - the hardcoded v1.0.0 is gone.
  * The script is now cross-platform and picks the archive name and format per
    OS, so all three CI jobs call it instead of duplicating the naming rules.
  * A version job resolves the tag once and every job and release note reads
    that single value.
  * CI runs the test suite before building, and publishing is gated on a tag.

Test suite
  * Remove tests/ and run_tests.py from .gitignore - the entire suite was
    excluded from the repository despite being documented in the README.
  * Tests patched pipeline.OUT_DIR, which _process never reads: the output
    path comes from Settings.get_output_dir(). Every finalizer test wrote into
    the real output/ folder. They now steer it via settings.output_dir.
  * Add a regression test for FileFinalizer.run_async without base_name.

Correctness
  * FileFinalizer.run_async called paths.safe_output_name() while pipeline.py
    never imported paths - NameError on the default-name path.
  * Drop a duplicated write of the transcript file.
  * ElevenLabsBackend.cancel() closes the live response instead of only
    setting a flag nothing read; a cancelled upload no longer sits on its
    900 s timeout.
  * Reset seg_end when flushing an ElevenLabs segment, so a following segment
    cannot inherit the previous end time.
  * loader.py no longer swallows its own "no sample data" error into the
    FFmpeg fallback, which reported a misleading "FFmpeg not found".
  * Slider honours state="disabled"; _enabled was assigned but never
    initialised or checked.
  * app.py no longer binds two different buttons to self.save_btn.
  * Redact the revoked example API key in legacy/settings.v1.json.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PyInstaller builds for the host architecture unless target_arch=universal2 is
set, so the macos-latest runner (arm64) produced an arm64-only binary that was
published as "macos-universal". Intel users downloaded a build that could not
run on their machine.

The slug now follows platform.machine(): macos-arm64 or macos-x64. README and
release notes say Apple Silicon and point Intel users at running from source or
building locally.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI
  * New ci.yml runs the suite on every push to main and every pull request,
    on Windows and Linux. Until now the tests only ran on a release tag, so
    nothing guarded day-to-day changes.
  * The Linux job adds an explicit import check of ui.app. No test imports
    that module unless a display is available, which is exactly why the
    unconditional pyaudiowpatch import and the missing Pillow dependency
    reached two published release assets unnoticed.
  * Linux runs the suite under xvfb so the GUI smoke test executes instead of
    skipping itself.

Device reconfiguration race
  * AudioEngine.configure() now holds a dedicated lock for the whole
    reconfiguration. _gen_lock was released between stop_streams() and opening
    the new streams, so two rapid device changes could interleave as
    A.stop / B.stop / A.assign / B.assign and leak A's streams. Verified: the
    unlocked path produces enter-A, enter-B, leave-A, leave-B.
  * Added AudioEngine.is_configuring so callers can tell "reconfiguring right
    now" apart from "no working device".
  * start_recording() waits for an in-flight reconfiguration instead of
    failing. configure() runs off the GUI thread and briefly holds no active
    track; pressing Start in that window reported "Neither audio source is
    active" straight after a perfectly valid device change. It now polls via
    root.after so the interface stays responsive, disables Start immediately
    against double-triggering, and after a 6 s deadline falls through to the
    engine's real error rather than hiding it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Caught by the new Linux CI job on its first run: os.path.basename() follows
host rules, so on Linux and macOS a backslash is an ordinary character. The
Windows-style input '..\..\windows\system32\evil' sanitised to 'evil' on
Windows and to '_' on Linux - the same settings.json produced a different
output file name depending on where it ran.

Both separators are now stripped everywhere. The result was safe on either
platform; it just was not the same one.

Also add a property test stating the actual guarantee - the result contains no
separator, no '..', is non-empty, and stays inside the directory it is joined
onto - rather than only checking a table of Windows-shaped strings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every action in both workflows still targeted the Node 20 runtime, which
the runner now force-migrates to Node 24 with a deprecation annotation on
each run.

  checkout             v4 -> v7
  setup-python         v5 -> v7
  upload-artifact      v4 -> v7
  download-artifact    v4 -> v8
  action-gh-release    v2 -> v3

The two artifact actions land on different majors on purpose: their
version lines diverged after v4 (download-artifact took an extra major
for an artifact-ids path fix we do not use). Only v3-and-below artifacts
are cross-incompatible; upstream's own examples pair the two lines freely.

Checked every breaking change against how these workflows actually call
the actions - the only one that touches us is download-artifact v8
turning a digest mismatch from a warning into an error, which is what a
release pipeline wants anyway. setup-python v7 drops the pip-install
input, unused here; all other majors are runtime moves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@SecretLUL
SecretLUL merged commit 536d872 into main Aug 11, 2026
8 checks passed
@SecretLUL
SecretLUL deleted the fix/packaging-and-release-versioning branch August 11, 2026 16:37
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.

1 participant