From 8bec0af20abcac8b2e56ad49bbeb1149687df4d0 Mon Sep 17 00:00:00 2001 From: MacBuchi <63749065+MacBuchi@users.noreply.github.com> Date: Thu, 5 Mar 2026 01:20:34 +0100 Subject: [PATCH 1/3] refactor: split release workflow into bump.yml + release.yml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bump.yml — triggers on push to Main; runs commitizen and pushes vX.Y.Z tag release.yml — triggers on tag push (v*); builds all platforms, smoke-tests, and publishes the GitHub Release Benefits: - Fixes checkout failure (tag-triggered run checks out the tagged commit automatically; no fragile 'ref: vX.Y.Z' branch-name lookup) - Failed builds can be rerun without re-bumping the version - workflow_dispatch on release.yml accepts a tag name to rebuild/re-release --- .github/workflows/bump.yml | 51 ++++++++++++++++ .github/workflows/release.yml | 108 ++++------------------------------ 2 files changed, 61 insertions(+), 98 deletions(-) create mode 100644 .github/workflows/bump.yml diff --git a/.github/workflows/bump.yml b/.github/workflows/bump.yml new file mode 100644 index 0000000..64b4030 --- /dev/null +++ b/.github/workflows/bump.yml @@ -0,0 +1,51 @@ +name: Bump Version + +on: + push: + branches: + - Main + +jobs: + bump: + name: Bump Version + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout code (full history for commitizen) + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + + - name: Set up Python 3.13 + run: uv python install 3.13 + + - name: Install dependencies (incl. dev) + run: uv sync --all-groups + + - name: Configure git bot identity + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + # Exit code 21 = "nothing to release" — treated as success (no-op). + - name: Bump version with commitizen + run: | + if uv run cz bump --yes; then + echo "Bumped — pushing tag" + git push --follow-tags + else + EXIT=$? + if [ $EXIT -eq 21 ]; then + echo "ℹ️ No releasable commits since last tag — skipping." + else + exit $EXIT + fi + fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d8d9698..a747bdf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,105 +2,18 @@ name: Release on: push: - branches: - - Main + tags: + - 'v*' workflow_dispatch: inputs: - version: - description: 'Version to release (e.g. 1.0.0) — skips commit analysis' + tag: + description: 'Tag to release (e.g. v1.2.3) — rebuilds/re-releases an existing tag' required: true - default: '1.0.0' jobs: - # ── 1. Bump version, create tag ──────────────────────────────────────────── - bump: - name: Bump Version - runs-on: ubuntu-latest - permissions: - contents: write - outputs: - bumped: ${{ steps.cz.outputs.bumped }} - new_version: ${{ steps.cz.outputs.new_version }} - - steps: - - name: Checkout code (full history for commitizen) - uses: actions/checkout@v4 - with: - fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Install uv - uses: astral-sh/setup-uv@v5 - with: - enable-cache: true - - - name: Set up Python 3.13 - run: uv python install 3.13 - - - name: Install dependencies (incl. dev) - run: uv sync --all-groups - - - name: Configure git bot identity - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - # ── Manual dispatch: stamp exact version ────────────────────────────── - - name: Bump to exact version (manual dispatch) - id: cz_manual - if: github.event_name == 'workflow_dispatch' - run: | - VERSION="${{ inputs.version }}" - # Update version in pyproject.toml if it differs - sed -i "s/^version = \".*\"/version = \"${VERSION}\"/" pyproject.toml - if ! git diff --quiet pyproject.toml; then - git add pyproject.toml - git commit -m "bump: release v${VERSION}" - fi - git tag -a "v${VERSION}" -m "Release v${VERSION}" - echo "bumped=true" >> "$GITHUB_OUTPUT" - echo "new_version=${VERSION}" >> "$GITHUB_OUTPUT" - - # ── Auto: derive version from conventional commits ──────────────────── - # Exit code 21 = "nothing to release" — treated as success (no-op). - - name: Bump version with commitizen (auto) - id: cz_auto - if: github.event_name == 'push' - run: | - if uv run cz bump --yes; then - echo "bumped=true" >> "$GITHUB_OUTPUT" - echo "new_version=$(uv run cz version --project)" >> "$GITHUB_OUTPUT" - else - EXIT=$? - if [ $EXIT -eq 21 ]; then - echo "bumped=false" >> "$GITHUB_OUTPUT" - echo "ℹ️ No releasable commits since last tag — skipping release." - else - exit $EXIT - fi - fi - - # ── Merge outputs from both branches into a single step output ──────── - - name: Resolve bump outputs - id: cz - run: | - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - echo "bumped=${{ steps.cz_manual.outputs.bumped }}" >> "$GITHUB_OUTPUT" - echo "new_version=${{ steps.cz_manual.outputs.new_version }}" >> "$GITHUB_OUTPUT" - else - echo "bumped=${{ steps.cz_auto.outputs.bumped }}" >> "$GITHUB_OUTPUT" - echo "new_version=${{ steps.cz_auto.outputs.new_version }}" >> "$GITHUB_OUTPUT" - fi - - - name: Push version bump commit and tag - if: steps.cz.outputs.bumped == 'true' - run: git push --follow-tags - - # ── 2. Build per platform (only when version was bumped) ────────────────── + # ── Build per platform ───────────────────────────────────────────────────── build: name: Build — ${{ matrix.os-label }} - needs: bump - if: needs.bump.outputs.bumped == 'true' runs-on: ${{ matrix.runner }} permissions: contents: read @@ -122,10 +35,10 @@ jobs: artifact: MultiChannelWavMixer-Windows-x64.zip steps: - - name: Checkout code at new tag + - name: Checkout code at tag uses: actions/checkout@v4 with: - ref: v${{ needs.bump.outputs.new_version }} + ref: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref }} - name: Install uv uses: astral-sh/setup-uv@v5 @@ -182,11 +95,10 @@ jobs: path: dist/${{ matrix.artifact }} retention-days: 7 - # ── 3. Publish GitHub Release with all platform artifacts ───────────────── + # ── Publish GitHub Release ───────────────────────────────────────────────── publish: name: Publish GitHub Release - needs: [bump, build] - if: needs.bump.outputs.bumped == 'true' + needs: build runs-on: ubuntu-latest permissions: contents: write @@ -201,6 +113,6 @@ jobs: - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: - tag_name: v${{ needs.bump.outputs.new_version }} + tag_name: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }} files: release-artifacts/*.zip generate_release_notes: true From 3972e1c5ea0632605c9f6ac1398f9c3d7752b80a Mon Sep 17 00:00:00 2001 From: MacBuchi <63749065+MacBuchi@users.noreply.github.com> Date: Thu, 5 Mar 2026 01:31:34 +0100 Subject: [PATCH 2/3] docs: add AGENTS.md and update README for workflow changes - Add AGENTS.md with full agent instructions: architecture rules, dev environment, git workflow, CI/CD pipeline, PyInstaller notes, and README maintenance policy - Update README: reflect bump.yml/release.yml split, 3-platform build matrix, lint step, smoke tests, uv.lock and AGENTS.md in project structure, updated changelog --- AGENTS.md | 151 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 24 +++++---- 2 files changed, 164 insertions(+), 11 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..77fc87b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,151 @@ +# Agent Instructions — MultiChannelWavMixer + +This file provides guidance for AI coding agents (GitHub Copilot, Codex, etc.) working on this repository. + +--- + +## Project overview + +MultiChannelWavMixer is a cross-platform desktop GUI application (customtkinter / Tkinter) for downmixing multichannel WAV files to stereo. It targets **RME Durec** recorder output and reads embedded iXML metadata. + +Key characteristics: +- Pure Python, no C extensions written by the project itself +- GUI code lives entirely in `MultiChannelWavMixer.py`; all audio logic is in `mixer_utils.py` +- Distributed as a self-contained native app via PyInstaller (`.app` on macOS, folder + `.exe` on Windows) +- Managed with [uv](https://docs.astral.sh/uv/); never use bare `pip` or `python` commands + +--- + +## Repository layout + +``` +MultiChannelWavMixer.py # GUI layer — customtkinter, no business logic +mixer_utils.py # Pure audio logic — no GUI imports, fully unit-tested +MultiChannelWavMixer.spec # PyInstaller spec (cross-platform) +pyi_rth_env.py # PyInstaller runtime hook +build.sh # Local build helper (wraps pyinstaller spec) +MixConf.json # Persisted channel configuration (runtime artefact) +pyproject.toml # Dependencies, tool config, commitizen version source +uv.lock # Locked dependency graph — always commit alongside pyproject.toml changes +requirements.txt # Legacy reference only — do not modify +.github/ + workflows/ + ci.yml # PR checks: lint → tests → build matrix + smoke test + bump.yml # Merge to Main: commitizen version bump + tag push + release.yml # Tag push: build matrix + smoke test + GitHub Release +tests/ + test_mixer_utils.py # Unit tests (GUI-free) +doc/ # Screenshots / assets +``` + +--- + +## Architecture rules + +1. **`mixer_utils.py` must remain GUI-free.** No `tkinter`, `customtkinter`, or display imports. All audio processing, file I/O, and playback logic belongs here. +2. **`MultiChannelWavMixer.py` must not contain reusable logic.** It wires the GUI to `mixer_utils` functions only. +3. **All new `mixer_utils` functions must have unit tests** in `tests/test_mixer_utils.py`. Tests run without a display and must pass on all three platforms. + +--- + +## Development environment + +```sh +uv sync --all-groups # install all deps including dev (ruff, pytest, pyinstaller, commitizen) +uv run pytest -v # run the test suite +uv run ruff check . # lint +uv run ruff format . # format +bash build.sh --clean # local PyInstaller build (macOS .app) +``` + +Python version is pinned to **3.13** via `.python-version` and `pyproject.toml`. + +### Dependency changes + +When adding or removing dependencies: +1. Edit `pyproject.toml` (`[project].dependencies` for runtime, `[dependency-groups].dev` for dev tools). +2. Run `uv lock` to regenerate `uv.lock`. +3. Commit **both** `pyproject.toml` and `uv.lock` together. + +> **macOS Intel (x86_64) constraint**: `llvmlite` (a `numba` dependency) has no pre-built wheel for Python 3.13 on macOS x86_64. A `[tool.uv.override-dependencies]` entry in `pyproject.toml` conditionally skips `numba` on that platform. Do not remove this override. + +--- + +## Git workflow + +``` +dev → Pull Request → Main + (CI checks) (bump.yml tags → release.yml builds & releases) +``` + +- All work is done on `dev` (or a feature branch off `dev`). +- **Never commit directly to `Main`.** +- Merge to `Main` only via a reviewed Pull Request. +- The CI pipeline must be green before merging. + +### Commit message convention (Conventional Commits) + +| Prefix | Bump | When to use | +|---|---|---| +| `fix:` | patch | Bug fixes | +| `feat:` | minor | New user-visible features | +| `feat!:` / `BREAKING CHANGE:` | major | Breaking API or behaviour change | +| `refactor:` | none | Code restructuring with no behaviour change | +| `chore:` | none | Maintenance, dependency updates | +| `ci:` | none | Workflow / pipeline changes | +| `docs:` | none | Documentation only | +| `test:` | none | Tests only | + +Commitizen reads these prefixes automatically on every merge to `Main` to decide whether and how to bump the version. Getting the prefix right is important. + +--- + +## CI/CD pipeline + +### `ci.yml` — runs on every PR to `Main` + +1. **Lint** (`ubuntu-latest`) — `ruff check` and `ruff format --check` +2. **Test** (`macos-latest`, needs lint) — `pytest` with JUnit XML; results posted as a PR check +3. **Build** (matrix: `macos-14`, `macos-15-intel`, `windows-latest`, needs test): + - PyInstaller build + - **Smoke test**: launches the built binary and verifies it stays alive for 10 seconds + +### `bump.yml` — runs on push to `Main` + +- Commitizen inspects commits since the last tag +- If releasable commits exist: bumps `pyproject.toml`, commits the change, pushes a `vX.Y.Z` annotated tag +- If nothing to release: exits cleanly (no-op) + +### `release.yml` — runs on tag push (`v*`) + +1. **Build** (same matrix as CI) — build + smoke test per platform +2. **Publish** — creates a GitHub Release with the zip artifacts and auto-generated release notes + +`workflow_dispatch` accepts a tag name to rebuild/re-release without re-bumping. + +--- + +## PyInstaller build notes + +- The spec file `MultiChannelWavMixer.spec` is the single source of truth for the build. +- `numba` and `llvmlite` are listed in `excludes` — they must not be bundled. +- Hidden imports and data files are declared explicitly in the spec; update them when adding new dependencies that PyInstaller cannot auto-detect. +- On macOS the output is `dist/MultiChannelWavMixer.app`; on Windows it is `dist/MultiChannelWavMixer/` (a folder, no single-file exe). + +--- + +## README maintenance + +**After every significant change, review `README.md` and update it if needed.** + +Changes that typically require README updates: +- New or removed features visible to the end user +- Changes to the GUI layout or controls +- New or changed CLI / run commands +- Dependency or Python version changes +- Changes to the CI/CD pipeline description +- Changes to the project structure (files added/removed/renamed) +- Changes to the `mixer_utils.py` public API table +- Version bump strategy or commit convention changes + +The README is the primary user-facing and contributor-facing documentation. Keep it accurate. diff --git a/README.md b/README.md index 30be4e9..b3ffb02 100644 --- a/README.md +++ b/README.md @@ -131,20 +131,18 @@ This project uses [Conventional Commits](https://www.conventionalcommits.org/). ### CI workflow (on every PR to `Main`) -1. **Run Tests** — `uv run pytest` with JUnit XML output; results are posted as a named check on the PR -2. **Build macOS App** — verifies the PyInstaller build completes without error +1. **Lint** — `ruff check` and `ruff format --check` (ubuntu-latest) +2. **Run Tests** — `uv run pytest` with JUnit XML output; results are posted as a named check on the PR +3. **Build** (parallel matrix — macOS ARM, macOS Intel, Windows) — verifies the PyInstaller build completes and the app survives a 10-second smoke test ### Release workflow (on merge to `Main`) -Triggered automatically. Three sequential jobs: +Two separate workflows fire in sequence: -1. **Bump Version** — [commitizen](https://commitizen-tools.github.io/commitizen/) reads commits since the last tag, bumps the version in `pyproject.toml`, appends to `CHANGELOG.md`, and pushes a git tag (e.g. `v1.1.0`). If no releasable commits are found the workflow exits cleanly with no release. -2. **Build** (parallel matrix) — builds the app on `macos-latest` and `windows-latest`: - - macOS → `MultiChannelWavMixer-macOS.zip` (contains `MultiChannelWavMixer.app`) - - Windows → `MultiChannelWavMixer-Windows.zip` (contains the PyInstaller output folder) -3. **Publish** — creates a [GitHub Release](https://github.com/MacBuchi/MultiChannelWavMixer/releases) with both zip files attached and auto-generated release notes. +1. **`bump.yml`** — [commitizen](https://commitizen-tools.github.io/commitizen/) reads commits since the last tag, bumps the version in `pyproject.toml`, and pushes an annotated git tag (e.g. `v1.1.0`). If no releasable commits are found the workflow exits cleanly with no release. +2. **`release.yml`** — triggered by the new tag; builds the app on all three platforms (macOS ARM, macOS Intel, Windows), runs a smoke test on each, and publishes a [GitHub Release](https://github.com/MacBuchi/MultiChannelWavMixer/releases) with the zip artifacts attached. -> The version in `pyproject.toml` is the single source of truth and is always in sync with the git tag. +> A failed build job can be re-run directly from the Actions tab without re-bumping the version, because the build and the bump are decoupled. --- @@ -159,12 +157,15 @@ MultiChannelWavMixer/ ├── build.sh # Local build helper (wraps pyinstaller spec) ├── MixConf.json # Persisted channel configuration ├── pyproject.toml # UV project, dependencies & commitizen config +├── uv.lock # Locked dependency graph +├── AGENTS.md # AI coding agent instructions ├── .python-version # Pins Python 3.13 ├── requirements.txt # Legacy reference (use uv sync instead) ├── .github/ │ └── workflows/ -│ ├── ci.yml # PR checks: tests + build verification -│ └── release.yml # Merge to Main: version bump + release build +│ ├── ci.yml # PR checks: lint → tests → build + smoke test +│ ├── bump.yml # Merge to Main: commitizen version bump + tag push +│ └── release.yml # Tag push: build matrix + smoke test + GitHub Release ├── tests/ │ └── test_mixer_utils.py # Unit tests for mixer_utils └── doc/ @@ -230,6 +231,7 @@ graph TD | Date | Change | |---|---| +| 2026-03-05 | CI/CD: split release into `bump.yml` (tag) + `release.yml` (tag-triggered build); add macOS Intel runner (`macos-15-intel`); add lint step; add smoke test on all platforms; fix `llvmlite` x86_64 build failure via uv override | | 2026-03-04 | CI/CD: add `ci.yml` (PR checks) and `release.yml` (multi-platform release); conventional commits drive automatic versioning via commitizen | | 2026-02-20 | Volume fader changed to dB scale (-60 – +6 dB); -60 dB treated as silence; double-click resets to 0 dB | | 2026-02-20 | Waveform y-axis fixed (-1 to +1); amplitude scaled by volume fader for visual comparability | From b64d26fc73d6ee46a3c75ca8be5d1c3d45cdd0ba Mon Sep 17 00:00:00 2001 From: MacBuchi <63749065+MacBuchi@users.noreply.github.com> Date: Thu, 5 Mar 2026 01:33:49 +0100 Subject: [PATCH 3/3] fix: add ad-hoc codesign to macOS builds to prevent Gatekeeper 'damaged' error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apps downloaded from GitHub have the quarantine xattr applied; without any signature Gatekeeper reports 'damaged' instead of 'unidentified developer'. codesign --deep --force --sign - (ad-hoc) creates a valid local signature so the user can right-click → Open or run xattr -cr to bypass Gatekeeper. Also documents the Gatekeeper workaround and pre-built release artifacts in README. --- .github/workflows/ci.yml | 6 +++++- .github/workflows/release.yml | 6 +++++- README.md | 24 ++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 339ae96..67edc0f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -109,9 +109,13 @@ jobs: run: uv sync --all-groups - name: Build with PyInstaller (macOS) + if: runner.os == 'macOS' + run: uv run pyinstaller MultiChannelWavMixer.spec --noconfirm --log-level WARN + + - name: Ad-hoc code sign + zip (macOS) if: runner.os == 'macOS' run: | - uv run pyinstaller MultiChannelWavMixer.spec --noconfirm --log-level WARN + codesign --deep --force --sign - dist/MultiChannelWavMixer.app cd dist && zip -r ${{ matrix.artifact }} MultiChannelWavMixer.app - name: Build with PyInstaller (Windows) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a747bdf..109805e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -52,9 +52,13 @@ jobs: run: uv sync --all-groups - name: Build with PyInstaller (macOS) + if: runner.os == 'macOS' + run: uv run pyinstaller MultiChannelWavMixer.spec --noconfirm --log-level WARN + + - name: Ad-hoc code sign + zip (macOS) if: runner.os == 'macOS' run: | - uv run pyinstaller MultiChannelWavMixer.spec --noconfirm --log-level WARN + codesign --deep --force --sign - dist/MultiChannelWavMixer.app cd dist && zip -r ${{ matrix.artifact }} MultiChannelWavMixer.app - name: Build with PyInstaller (Windows) diff --git a/README.md b/README.md index b3ffb02..f14a9c5 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,30 @@ That's it — `uv sync` reads `pyproject.toml`, creates `.venv`, and installs ev --- +## Pre-built releases + +Download the latest zip for your platform from the [GitHub Releases](https://github.com/MacBuchi/MultiChannelWavMixer/releases) page, extract it, and run the app directly — no Python installation required. + +| Platform | Artifact | +|---|---| +| macOS Apple Silicon | `MultiChannelWavMixer-macOS-arm64.zip` → `MultiChannelWavMixer.app` | +| macOS Intel | `MultiChannelWavMixer-macOS-x64.zip` → `MultiChannelWavMixer.app` | +| Windows x64 | `MultiChannelWavMixer-Windows-x64.zip` → `MultiChannelWavMixer\MultiChannelWavMixer.exe` | + +### macOS Gatekeeper note + +The app is ad-hoc signed but **not notarized** (notarization requires an Apple Developer account). macOS may show *"unidentified developer"* and block the first launch. To open it: + +- **Right-click** the `.app` → **Open** → confirm in the dialog, **or** +- Run once in Terminal to strip the quarantine flag: + ```sh + xattr -cr /path/to/MultiChannelWavMixer.app + ``` + +After this one-time step the app opens normally. + +--- + ## Usage ```sh