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
51 changes: 51 additions & 0 deletions .github/workflows/bump.yml
Original file line number Diff line number Diff line change
@@ -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
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
114 changes: 15 additions & 99 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -139,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)
Expand Down Expand Up @@ -182,11 +99,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
Expand All @@ -201,6 +117,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
151 changes: 151 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -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.
Loading