From 7529a4043b5b9ee0dd9ab88377f15225cca059b4 Mon Sep 17 00:00:00 2001 From: Zeyu Yang <40936154+zeyuyang42@users.noreply.github.com> Date: Thu, 20 Aug 2026 01:39:21 +0200 Subject: [PATCH 1/3] Replace the workflow_run chain with a single same-run build graph The core and PureData workflows were chained with `workflow_run`. That is not merely awkward, it is wrong: `workflow_run` checks out the *default branch* rather than the ref that triggered it, while downloading the core artifact from the triggering run. Dispatching the core build on core_modernization therefore made the PureData workflow compile old main sources against a new libzerr_core.a, failing with undefined reference to zerr::EnvelopeGenerator::EnvelopeGenerator( zerr::SystemConfigs, std::string, std::string) because the constructor had become GenMode-typed. That looked like a real defect and was not. ci.yml puts every job in one run. Wrapper jobs take the core artifact from their own run -- no run-id, no github-token, no "Ensure core artifact id present" guard -- so a cross-commit mix is not representable rather than merely unlikely. Artifacts are keyed by TOOLCHAIN rather than by platform. Windows needs two distinct core builds: MinGW for PureData, and MSVC with static CRT for Max/MSP, whose max-pretarget.cmake forces /MT. A single zerr-core-windows artifact could never express that, so the old structure could not reach the target matrix this project is heading for (Mac/Linux/Windows x PD/Max/JACK/SuperCollider). Adding a platform or a compiler is now one matrix row plus one row in the consumer. Max/MSP gains its first CI job on any platform. macOS only for now -- Max does not exist on Linux, and Windows needs the MSVC/static-CRT path above. Also folded in: - The Windows core build moves off its inline conan settings onto profiles/mingw. Those inline settings omitted compiler.cppstd, compiler.threads and compiler.exception, all of which are package_id inputs, so the core and PureData jobs could resolve *different binary packages* of fftw and yaml-cpp for one logical build -- the single-resolve invariant broken at the CI layer. - A composite setup-deps action replaces the conan block that was duplicated 6 times, and caches ~/.conan2. fftw and yaml-cpp currently build from source on macOS and Windows every run; the Windows job spent most of its 3m41s there. - Path filters dropped. A core change must rebuild every wrapper and a wrapper-only change must still build; the old filters are why a puredata- or maxmsp-only change triggered nothing at all. - concurrency group so superseded runs are cancelled, and checkout v3 -> v4 (v3 targets the deprecated Node 20). deploy-doxygen.yml is untouched: it fails by design pending the organization owner's decision on Pages, and is documented in docs/design/github-pages-deployment.md. Co-Authored-By: Claude Opus 5 (1M context) --- .github/actions/setup-deps/action.yml | 46 ++++ ...d-puredata-plugins-with-pd-lib-builder.yml | 244 ------------------ .../build-zerr-core-static-library.yml | 135 ---------- .github/workflows/ci.yml | 239 +++++++++++++++++ 4 files changed, 285 insertions(+), 379 deletions(-) create mode 100644 .github/actions/setup-deps/action.yml delete mode 100644 .github/workflows/build-puredata-plugins-with-pd-lib-builder.yml delete mode 100644 .github/workflows/build-zerr-core-static-library.yml create mode 100644 .github/workflows/ci.yml diff --git a/.github/actions/setup-deps/action.yml b/.github/actions/setup-deps/action.yml new file mode 100644 index 0000000..b13f021 --- /dev/null +++ b/.github/actions/setup-deps/action.yml @@ -0,0 +1,46 @@ +name: Set up Zerr* dependencies +description: > + Resolve fftw and yaml-cpp once, at the repo root, using a committed profile -- + the same single resolve that build.sh performs locally, so CI and developer + machines share one dependency configuration. + +inputs: + profile: + description: 'Committed conan profile to resolve with, e.g. profiles/macos' + required: true + +runs: + using: composite + steps: + # Keyed on the profile as well as the OS: windows-mingw and windows-msvc are + # both runner.os == Windows but must never share a cache entry, since their + # package_ids differ. Any change to conanfile.txt or to any profile + # invalidates every entry, which is coarse but cheap -- profiles change rarely, + # and a stale hit here would silently reintroduce the mismatched-dependency + # problem the single root resolve exists to prevent. + - name: Cache conan packages + uses: actions/cache@v4 + with: + path: ~/.conan2 + key: conan-${{ runner.os }}-${{ inputs.profile }}-${{ hashFiles('conanfile.txt', 'profiles/*') }} + restore-keys: | + conan-${{ runner.os }}-${{ inputs.profile }}- + + # Pinned below the next major: a bare `pip install conan` would float to + # conan 3.x and break these jobs with no change on our side. + - name: Install conan + shell: bash + run: pip install "conan>=2.13,<3" + + # profiles/macos and profiles/linux are `include(default)`, so the default + # profile has to exist even though every invocation passes an explicit one. + # profiles/mingw is standalone and does not need it. + - name: Detect default profile + shell: bash + run: conan profile detect --force + + - name: Resolve dependencies + shell: bash + run: | + conan install . --output-folder=build --build=missing \ + -pr:h="${{ inputs.profile }}" -pr:b="${{ inputs.profile }}" diff --git a/.github/workflows/build-puredata-plugins-with-pd-lib-builder.yml b/.github/workflows/build-puredata-plugins-with-pd-lib-builder.yml deleted file mode 100644 index 30507cc..0000000 --- a/.github/workflows/build-puredata-plugins-with-pd-lib-builder.yml +++ /dev/null @@ -1,244 +0,0 @@ ---- -name: build pd-lib-builder based pure data externals - -on: - workflow_call: - workflow_dispatch: - inputs: - core_run_id: - description: "Run ID of a successful 'build zerr-core static library' workflow (needed when not triggered via workflow_run)" - required: false - type: string - # push: - # branches: - # - main - # paths: - # - 'puredata/**' # Trigger when Pure Data related code changes - - workflow_run: - workflows: ["build zerr-core static library"] - types: - - completed - -jobs: - macOS: - runs-on: macos-latest - env: - cflags: -mmacosx-version-min=10.13 - steps: - - name: Checkout Code - uses: actions/checkout@v3 - with: - submodules: true - - - name: Ensure core artifact id present - env: - CORE_RUN_ID: ${{ github.event.workflow_run.id || inputs.core_run_id }} - run: | - if [ -z "${CORE_RUN_ID}" ]; then - echo "CORE_RUN_ID missing. Provide inputs.core_run_id when dispatching manually or trigger via workflow_run." >&2 - exit 1 - fi - - - name: Download zerr-core library - uses: actions/download-artifact@v4 - with: - name: zerr-core-macos - path: core/lib/ - run-id: ${{ github.event.workflow_run.id || inputs.core_run_id }} - github-token: ${{ github.token }} - - - name: Display structure of downloaded files - run: ls -R core/lib/ - - - name: Install Pure Data - run: brew install --cask pd - - - name: Set up Conan - run: | - pip install "conan>=2.13,<3" - conan profile detect - - # Resolved at the repo root so this wrapper and zerr_core share one profile; - # puredata/Makefile reads ../build/conandeps.mk by default. - - name: Configure Conan - run: | - conan install . --output-folder=build --build=missing \ - -pr:h=profiles/macos -pr:b=profiles/macos - - - name: Build & Install - working-directory: puredata - run: | - make - make install PDLIBDIR=build - - - name: Upload macOS artifacts - uses: actions/upload-artifact@v4 - with: - name: zerr-pd-macos - path: puredata/build/zerr - linux: - runs-on: ubuntu-latest - steps: - - name: Checkout Code - uses: actions/checkout@v3 - with: - submodules: true - - - name: Ensure core artifact id present - env: - CORE_RUN_ID: ${{ github.event.workflow_run.id || inputs.core_run_id }} - run: | - if [ -z "${CORE_RUN_ID}" ]; then - echo "CORE_RUN_ID missing. Provide inputs.core_run_id when dispatching manually or trigger via workflow_run." >&2 - exit 1 - fi - - - name: Download zerr-core library - uses: actions/download-artifact@v4 - with: - name: zerr-core-linux - path: core/lib/ - run-id: ${{ github.event.workflow_run.id || inputs.core_run_id }} - github-token: ${{ github.token }} - - - name: Display structure of downloaded files - run: ls -R core/lib/ - - - name: Install Pure Data - run: sudo apt update && sudo apt install puredata-dev - - - name: Set up Conan - run: | - pip install "conan>=2.13,<3" - conan profile detect - - - name: Configure Conan - run: | - conan install . --output-folder=build --build=missing \ - -pr:h=profiles/linux -pr:b=profiles/linux - - - name: Build & Install - working-directory: puredata - run: | - make - make install PDLIBDIR=build - - - name: Upload Linux artifacts - uses: actions/upload-artifact@v4 - with: - name: zerr-pd-linux - path: puredata/build/zerr - - windows: - runs-on: windows-latest - steps: - - name: Checkout Code - uses: actions/checkout@v3 - with: - submodules: true - - - name: Ensure core artifact id present - shell: bash - env: - CORE_RUN_ID: ${{ github.event.workflow_run.id || inputs.core_run_id }} - run: | - if [ -z "${CORE_RUN_ID}" ]; then - echo "CORE_RUN_ID missing. Provide inputs.core_run_id when dispatching manually or trigger via workflow_run." >&2 - exit 1 - fi - - - name: Set up MinGW - uses: egor-tensin/setup-mingw@v3 - with: - arch: x64 - - - name: Install Pd - id: pd - shell: pwsh - run: | - $ErrorActionPreference = "Stop" - - $url = "http://msp.ucsd.edu/Software/pd-0.53-2.msw.zip" - $zip = Join-Path $env:RUNNER_TEMP "Pd.zip" - $pdDir = Join-Path $env:ProgramFiles "pd" - - Invoke-WebRequest $url -OutFile $zip - - if (Test-Path $pdDir) { Remove-Item $pdDir -Recurse -Force } - New-Item -ItemType Directory -Path $pdDir | Out-Null - - Expand-Archive -LiteralPath $zip -DestinationPath $pdDir -Force - Remove-Item $zip -Force - - # flatten nested directory - $top = Get-ChildItem $pdDir | Where-Object { $_.PSIsContainer } | Select-Object -First 1 - if ($top) { - Get-ChildItem $top.FullName | Move-Item -Destination $pdDir -Force - Remove-Item $top.FullName -Recurse -Force - } - - $pd = "$pdDir\bin\pd.com" - "PD=$pd" | Out-File -FilePath $env:GITHUB_ENV -Append - "PD=$pd" | Out-File -FilePath $env:GITHUB_OUTPUT -Append - - - name: Download zerr-core library - uses: actions/download-artifact@v4 - with: - name: zerr-core-windows - path: core/lib/ - run-id: ${{ github.event.workflow_run.id || inputs.core_run_id }} - github-token: ${{ github.token }} - - # - name: Set up Conan - # shell: bash - # run: | - # pip install conan - - # conan profile detect --name mingw --force - - # PROFILE_PATH=$(conan profile path default) - - # echo "Default profile path: $PROFILE_PATH" - - # conan profile set settings.os=Windows mingw - # conan profile set settings.arch=x86_64 mingw - # conan profile set settings.compiler=gcc mingw - # conan profile set settings.compiler.version=13 mingw - # conan profile set settings.compiler.libcxx=libstdc++11 mingw - # conan profile set settings.build_type=Release mingw - - # conan profile set conf.tools.build:compiler_executables.c=x86_64-w64-mingw32-gcc mingw - # conan profile set conf.tools.build:compiler_executables.cpp=x86_64-w64-mingw32-g++ mingw - - # conan profile show mingw - - # - name: Install Dependencies - # shell: bash - # working-directory: puredata - # run: | - # mkdir -p build - # conan install . --output-folder=build --build=missing -pr:b=mingw -pr:h=mingw - - - name: Set up Conan - run: | - pip install "conan>=2.13,<3" - conan profile detect --force - - # mingw-profile.txt moved to profiles/mingw alongside the other profiles. - - name: Install Dependencies - run: | - conan install . -pr:h=profiles/mingw -pr:b=profiles/mingw --output-folder=build --build=missing - - - name: Build External - shell: bash - working-directory: puredata - run: | - mingw32-make - mingw32-make install PDLIBDIR=build - - - name: Upload Windows artifacts - uses: actions/upload-artifact@v4 - with: - name: zerr-pd-windows - path: puredata/build/zerr diff --git a/.github/workflows/build-zerr-core-static-library.yml b/.github/workflows/build-zerr-core-static-library.yml deleted file mode 100644 index 0e471de..0000000 --- a/.github/workflows/build-zerr-core-static-library.yml +++ /dev/null @@ -1,135 +0,0 @@ -name: build zerr-core static library - -on: - workflow_dispatch: - # Deliberately unfiltered, unlike the push trigger below: a pull request should - # be gated on the core still building regardless of which directory it touched. - # Nothing ran on PRs #13-#16 at all, so they merged on manual dispatch alone. - # - # Only this workflow gains the trigger for now. The PureData workflow takes its - # core artifact from `workflow_run`, so on a pull_request event its CORE_RUN_ID - # would be empty and it would fail at its own guard; the Doxygen deploy fails by - # design pending the org owner, and would paint every PR red. Both are addressed - # by the ci.yml restructure that follows this change. - pull_request: - push: - branches: - - main - paths: - - "core/**" - - "conanfile.txt" - - "profiles/**" - -# Dependencies are resolved once at the repo root (see /conanfile.txt) using a -# committed profile, so CI and local builds share one dependency configuration. -# conan is pinned below the next major: a bare `pip install conan` would float to -# conan 3.x and break these jobs with no change on our side. - -jobs: - macOS: - runs-on: macos-latest - steps: - - name: Checkout Code - uses: actions/checkout@v3 - - - name: Set up Conan - run: | - pip install "conan>=2.13,<3" - conan profile detect - - - name: Install Dependencies - run: | - conan install . --output-folder=build --build=missing \ - -pr:h=profiles/macos -pr:b=profiles/macos - - - name: Build Static Library - run: | - cmake -S core -B core/build \ - -DCMAKE_TOOLCHAIN_FILE="$GITHUB_WORKSPACE/build/conan_toolchain.cmake" \ - -DCMAKE_BUILD_TYPE=Release - cmake --build core/build - cmake --install core/build - - - name: Upload zerr-core library - uses: actions/upload-artifact@v4 - with: - name: zerr-core-macos - path: core/lib - retention-days: 3 - - linux: - runs-on: ubuntu-latest - steps: - - name: Checkout Code - uses: actions/checkout@v3 - - - name: Set up Conan - run: | - pip install "conan>=2.13,<3" - conan profile detect - - - name: Install Dependencies - run: | - conan install . --output-folder=build --build=missing \ - -pr:h=profiles/linux -pr:b=profiles/linux - - - name: Build Static Library - run: | - export CXXFLAGS="-fPIC" - cmake -S core -B core/build \ - -DCMAKE_TOOLCHAIN_FILE="$GITHUB_WORKSPACE/build/conan_toolchain.cmake" \ - -DCMAKE_BUILD_TYPE=Release - cmake --build core/build - cmake --install core/build - - - name: Upload zerr-core library - uses: actions/upload-artifact@v4 - with: - name: zerr-core-linux - path: core/lib - retention-days: 3 - - windows: - runs-on: windows-latest - steps: - - name: Checkout Code - uses: actions/checkout@v3 - - - name: Set up MinGW - uses: egor-tensin/setup-mingw@v3 - with: - arch: x64 - - - name: Set up Conan - run: | - pip install "conan>=2.13,<3" - conan profile detect --force - - # Deliberately keeps its inline settings rather than -pr:h=profiles/mingw: - # this job builds natively with "MinGW Makefiles" on windows-latest, whereas - # profiles/mingw additionally pins compiler_executables for the - # cross-compiling PureData job. Switching it is untested — see docs/design. - - name: Install Dependencies - run: | - conan install . --output-folder=build --build=missing ` - -s compiler=gcc ` - -s compiler.version=13 ` - -s compiler.libcxx=libstdc++11 ` - -s build_type=Release - - - name: Build Static Library - run: | - cmake -S core -B core/build ` - -G "MinGW Makefiles" ` - -DCMAKE_TOOLCHAIN_FILE="${{ github.workspace }}/build/conan_toolchain.cmake" ` - -DCMAKE_BUILD_TYPE=Release ` - -DCMAKE_CXX_STANDARD=17 - cmake --build core/build --config Release - cmake --install core/build - - - name: Upload zerr-core library - uses: actions/upload-artifact@v4 - with: - name: zerr-core-windows - path: core/lib - retention-days: 3 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4872c91 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,239 @@ +name: CI + +# One run = one commit = one consistent set of artifacts. +# +# This replaces a pair of workflows chained with `workflow_run`. That chaining was +# not merely awkward, it was wrong: `workflow_run` checks out the *default branch* +# rather than the ref that triggered it, while downloading the core artifact from +# the triggering run. Dispatching the core build on a feature branch therefore made +# the PureData workflow link old main sources against a new libzerr_core.a, which +# failed with an undefined reference that looked like a real defect and was not. +# +# Here every wrapper job takes its core artifact from its own run via `needs:`, so +# a cross-commit mix is not representable rather than merely unlikely. + +on: + pull_request: + push: + branches: + - main + workflow_dispatch: + +# Deliberately no `paths:` filters. A core change must rebuild every wrapper, and +# a wrapper-only change must still build -- the old per-workflow filters are why a +# puredata/- or maxmsp/-only change used to trigger nothing at all. The extra +# minutes are paid back by the conan cache and by cancelling superseded runs. +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + # Artifacts are keyed by TOOLCHAIN, not by platform. Windows needs two distinct + # core builds -- MinGW for PureData, and later MSVC/static-CRT for Max/MSP, whose + # max-pretarget.cmake forces /MT. A single `zerr-core-windows` artifact could + # never express that, which is the structural reason this file exists. + # + # Adding a platform or a compiler is one row here plus one row in the consumer. + core: + name: core (${{ matrix.id }}) + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - id: macos + runner: macos-latest + profile: profiles/macos + - id: linux + runner: ubuntu-latest + profile: profiles/linux + cxxflags: -fPIC + - id: windows-mingw + runner: windows-latest + profile: profiles/mingw + mingw: true + # Without this CMake picks Visual Studio whenever one is installed and + # compiles with MSVC against the gcc/libstdc++ packages profiles/mingw + # resolved -- objects that cannot link together. + cmake_args: -G "MinGW Makefiles" + + steps: + - uses: actions/checkout@v4 + + - name: Set up MinGW + if: matrix.mingw + uses: egor-tensin/setup-mingw@v3 + with: + arch: x64 + + - uses: ./.github/actions/setup-deps + with: + profile: ${{ matrix.profile }} + + # github.workspace yields a mixed-separator path on Windows + # (D:\a\Zerr\Zerr/build/...). CMake normalises it -- verified in run + # 32297202438, which logged `Using Conan toolchain: D:/a/Zerr/Zerr/build/...`. + - name: Build and install + shell: bash + env: + CXXFLAGS: ${{ matrix.cxxflags }} + run: | + cmake -S core -B core/build ${{ matrix.cmake_args }} \ + -DCMAKE_TOOLCHAIN_FILE="${{ github.workspace }}/build/conan_toolchain.cmake" \ + -DCMAKE_BUILD_TYPE=Release + cmake --build core/build --config Release + cmake --install core/build + + - name: Upload core library + uses: actions/upload-artifact@v4 + with: + name: zerr-core-${{ matrix.id }} + path: core/lib + retention-days: 7 + + puredata: + name: puredata (${{ matrix.id }}) + needs: core + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - id: macos + runner: macos-latest + profile: profiles/macos + cflags: -mmacosx-version-min=10.13 + - id: linux + runner: ubuntu-latest + profile: profiles/linux + # The only job that can catch core/wrapper API drift at build time. + # macOS links externals with `-undefined suppress` and Linux permits + # undefined symbols in a .so, so both happily produce an external that + # fails at load; a .dll must resolve every symbol at link time. + - id: windows-mingw + runner: windows-latest + profile: profiles/mingw + mingw: true + make: mingw32-make + + steps: + - uses: actions/checkout@v4 + with: + submodules: true + + - name: Set up MinGW + if: matrix.mingw + uses: egor-tensin/setup-mingw@v3 + with: + arch: x64 + + # No run-id and no github-token: this resolves to the current run, which is + # what makes the artifact and the checked-out sources the same commit. + - name: Download core library + uses: actions/download-artifact@v4 + with: + name: zerr-core-${{ matrix.id }} + path: core/lib + + - name: Install Pure Data (macOS) + if: matrix.id == 'macos' + run: brew install --cask pd + + - name: Install Pure Data (Linux) + if: matrix.id == 'linux' + run: sudo apt update && sudo apt install -y puredata-dev + + - name: Install Pure Data (Windows) + if: matrix.id == 'windows-mingw' + shell: pwsh + run: | + $ErrorActionPreference = "Stop" + + $url = "http://msp.ucsd.edu/Software/pd-0.53-2.msw.zip" + $zip = Join-Path $env:RUNNER_TEMP "Pd.zip" + $pdDir = Join-Path $env:ProgramFiles "pd" + + Invoke-WebRequest $url -OutFile $zip + + if (Test-Path $pdDir) { Remove-Item $pdDir -Recurse -Force } + New-Item -ItemType Directory -Path $pdDir | Out-Null + + Expand-Archive -LiteralPath $zip -DestinationPath $pdDir -Force + Remove-Item $zip -Force + + # flatten nested directory + $top = Get-ChildItem $pdDir | Where-Object { $_.PSIsContainer } | Select-Object -First 1 + if ($top) { + Get-ChildItem $top.FullName | Move-Item -Destination $pdDir -Force + Remove-Item $top.FullName -Recurse -Force + } + + "PD=$pdDir\bin\pd.com" | Out-File -FilePath $env:GITHUB_ENV -Append + + - uses: ./.github/actions/setup-deps + with: + profile: ${{ matrix.profile }} + + - name: Build and install externals + shell: bash + working-directory: puredata + env: + cflags: ${{ matrix.cflags }} + run: | + ${{ matrix.make || 'make' }} + ${{ matrix.make || 'make' }} install PDLIBDIR=build + + - name: Upload PureData externals + uses: actions/upload-artifact@v4 + with: + name: zerr-pd-${{ matrix.id }} + path: puredata/build/zerr + retention-days: 7 + + # Max/MSP has never had a CI job on any platform. macOS only for now: Max does + # not exist on Linux, and Windows needs an MSVC toolchain with static CRT to + # match max-pretarget.cmake's /MT, which means a profiles/windows-msvc and a + # second Windows core build. That is a follow-up, tracked in the PR body. + maxmsp: + name: maxmsp (${{ matrix.id }}) + needs: core + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - id: macos + runner: macos-latest + profile: profiles/macos + + steps: + # recursive: min-api carries max-sdk-base as a nested submodule, and the + # Max headers live there. + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Download core library + uses: actions/download-artifact@v4 + with: + name: zerr-core-${{ matrix.id }} + path: core/lib + + - uses: ./.github/actions/setup-deps + with: + profile: ${{ matrix.profile }} + + - name: Build externals + shell: bash + run: | + cmake -S maxmsp -B maxmsp/build \ + -DCMAKE_TOOLCHAIN_FILE="${{ github.workspace }}/build/conan_toolchain.cmake" \ + -DCMAKE_BUILD_TYPE=Release + cmake --build maxmsp/build --config Release + + - name: Upload Max/MSP externals + uses: actions/upload-artifact@v4 + with: + name: zerr-max-${{ matrix.id }} + path: maxmsp/externals + retention-days: 7 From 8f811acfd44fe93c441e82302b81ed96a2ee22e1 Mon Sep 17 00:00:00 2001 From: Zeyu Yang <40936154+zeyuyang42@users.noreply.github.com> Date: Tue, 25 Aug 2026 01:06:04 +0200 Subject: [PATCH 2/3] Fix review findings in the CI restructure Seven fixes from review of the previous commit. The `arch` one had been wrong since long before this branch. Workflow: - setup-mingw takes `platform`, not `arch`. Run 32314194864 logged "Unexpected input(s) 'arch', valid inputs are ['platform', 'cc', 'version']" in both windows-mingw jobs -- the x64 pin was silently discarded and the builds were 64-bit only because that is the action's default. Carried over verbatim from the deleted workflows, so it had never worked. - Wrapper jobs gain `if: !cancelled()`. `needs` cannot name a single matrix leg, so a macOS-only core failure used to *skip* every wrapper -- including puredata (windows-mingw), which the file itself identifies as the only job that can catch core/wrapper API drift at build time. Now the leg whose artifact is genuinely missing fails at download and the rest still report. The wait on the slowest core leg is inherent to matrix-level `needs` and is left as is, noted in a comment. - if-no-files-found: error on all three uploads. The two leaf jobs had nothing downstream to notice an empty publish, so a wrapper that stopped emitting into puredata/build/zerr or maxmsp/externals would have stayed green. - cancel-in-progress no longer applies to main. Two merges in quick succession would have cancelled the first one's run, leaving that commit without the artifact set the file's own header promises. - Explicit `permissions: contents: read`. No cross-run artifact download remains, so no job needs a token beyond checkout. setup-deps: - ImageVersion added to the cache key. profiles/mingw pins compiler.version=13 while the runners compile with gcc 16.1.0, so neither the package_id nor a conanfile/profile hash changes when the image bumps its toolchain: conan would have restored fftw and yaml-cpp built by the previous compiler and linked them into a core built by the new one -- exactly what the key's own comment claimed to prevent. - restore-keys dropped. A prefix restore is by definition the stale hit that key exists to avoid, and it re-saved the restored tree under the new key, so ~/.conan2 would accrete every historical revision until it hit the 10GB repository ceiling and began evicting live entries. Docs: - dependency-fallbacks.md section 3.7 asserted that the core CI job deliberately bypasses profiles/mingw, and cited a workflow file this branch deletes. Both halves are now wrong: the core job resolves through the profile, and the pinned compiler_executables names verifiably work. Rewritten, with the surviving compiler.version drift and how the cache key compensates. - repo-audit section 2.6 gets a status block, matching how 2.3 was handled, so the dated audit is not read as current state. The arm64-only macOS artifacts are deliberately not addressed here. That is a real design decision -- CI runners are arm64, so a universal binary needs either -G Xcode or an explicit CMAKE_OSX_ARCHITECTURES -- and it is recorded against section 2.5, which already flagged the same gap for build.sh. Co-Authored-By: Claude Opus 5 (1M context) --- .github/actions/setup-deps/action.yml | 23 +++++++++++----- .github/workflows/ci.yml | 39 ++++++++++++++++++++++++--- docs/design/dependency-fallbacks.md | 16 ++++++++--- docs/design/repo-audit-2026-07-30.md | 17 ++++++++++++ 4 files changed, 82 insertions(+), 13 deletions(-) diff --git a/.github/actions/setup-deps/action.yml b/.github/actions/setup-deps/action.yml index b13f021..67820d0 100644 --- a/.github/actions/setup-deps/action.yml +++ b/.github/actions/setup-deps/action.yml @@ -14,17 +14,26 @@ runs: steps: # Keyed on the profile as well as the OS: windows-mingw and windows-msvc are # both runner.os == Windows but must never share a cache entry, since their - # package_ids differ. Any change to conanfile.txt or to any profile - # invalidates every entry, which is coarse but cheap -- profiles change rarely, - # and a stale hit here would silently reintroduce the mismatched-dependency - # problem the single root resolve exists to prevent. + # package_ids differ. + # + # ImageVersion is in the key because the compiler is not. profiles/mingw pins + # compiler.version=13 while the runner actually compiles with gcc 16.1.0, so + # neither the package_id nor a conanfile/profile hash changes when the image + # bumps its toolchain -- conan would restore fftw and yaml-cpp built by the + # previous compiler and link them into a core built by the new one. Keying on + # the image makes a toolchain change a cache miss, which is the honest outcome. + # + # No restore-keys, deliberately. A prefix restore is by definition a stale hit, + # which is what this key exists to prevent; it also re-saves the restored tree + # under the new key, so ~/.conan2 would accrete every historical revision until + # it hit the 10GB repository ceiling and started evicting live entries. A full + # miss costs one from-source build of fftw and yaml-cpp, which is the correct + # price for a changed toolchain. - name: Cache conan packages uses: actions/cache@v4 with: path: ~/.conan2 - key: conan-${{ runner.os }}-${{ inputs.profile }}-${{ hashFiles('conanfile.txt', 'profiles/*') }} - restore-keys: | - conan-${{ runner.os }}-${{ inputs.profile }}- + key: conan-${{ runner.os }}-${{ env.ImageVersion }}-${{ inputs.profile }}-${{ hashFiles('conanfile.txt', 'profiles/*') }} # Pinned below the next major: a bare `pip install conan` would float to # conan 3.x and break these jobs with no change on our side. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4872c91..ed677c1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,13 +19,23 @@ on: - main workflow_dispatch: +# Jobs only ever read the repository: there is no cross-run artifact download left, +# so nothing here needs a token beyond checkout. State it rather than inherit +# whatever the repository default happens to be, as deploy-doxygen.yml already does. +permissions: + contents: read + # Deliberately no `paths:` filters. A core change must rebuild every wrapper, and # a wrapper-only change must still build -- the old per-workflow filters are why a # puredata/- or maxmsp/-only change used to trigger nothing at all. The extra # minutes are paid back by the conan cache and by cancelling superseded runs. +# +# Superseding is right for a branch, wrong for main: two merges in quick succession +# would cancel the first one's run, and that commit would never get the artifact set +# the header above promises. Only cancel outside main. concurrency: group: ci-${{ github.ref }} - cancel-in-progress: true + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} jobs: # Artifacts are keyed by TOOLCHAIN, not by platform. Windows needs two distinct @@ -64,7 +74,11 @@ jobs: if: matrix.mingw uses: egor-tensin/setup-mingw@v3 with: - arch: x64 + # `platform`, not `arch`. The old workflows passed `arch: x64`, which the + # action rejects -- run 32314194864 logged "Unexpected input(s) 'arch', + # valid inputs are ['platform', 'cc', 'version']". The builds were 64-bit + # only because that is the action's default, never because we asked. + platform: x64 - uses: ./.github/actions/setup-deps with: @@ -89,11 +103,20 @@ jobs: with: name: zerr-core-${{ matrix.id }} path: core/lib + if-no-files-found: error retention-days: 7 puredata: name: puredata (${{ matrix.id }}) + # `needs` cannot name a single matrix leg, so this waits on all three core legs. + # Without the `if`, one platform's core failure would *skip* every wrapper job -- + # including puredata (windows-mingw), the only job that can catch core/wrapper + # API drift at build time. `!cancelled()` runs them anyway: the leg whose core + # artifact is genuinely missing fails at download, and the others still report. + # (The wait itself is inherent to matrix-level `needs`; splitting core into + # per-platform jobs would remove it at the cost of the matrix.) needs: core + if: ${{ !cancelled() }} runs-on: ${{ matrix.runner }} strategy: fail-fast: false @@ -125,7 +148,11 @@ jobs: if: matrix.mingw uses: egor-tensin/setup-mingw@v3 with: - arch: x64 + # `platform`, not `arch`. The old workflows passed `arch: x64`, which the + # action rejects -- run 32314194864 logged "Unexpected input(s) 'arch', + # valid inputs are ['platform', 'cc', 'version']". The builds were 64-bit + # only because that is the action's default, never because we asked. + platform: x64 # No run-id and no github-token: this resolves to the current run, which is # what makes the artifact and the checked-out sources the same commit. @@ -188,6 +215,10 @@ jobs: with: name: zerr-pd-${{ matrix.id }} path: puredata/build/zerr + # Nothing consumes this artifact, so without `error` a wrapper that stopped + # emitting into puredata/build/zerr would leave the job green and publish + # nothing. The core job is only incidentally protected by its consumers. + if-no-files-found: error retention-days: 7 # Max/MSP has never had a CI job on any platform. macOS only for now: Max does @@ -197,6 +228,7 @@ jobs: maxmsp: name: maxmsp (${{ matrix.id }}) needs: core + if: ${{ !cancelled() }} # see the note on puredata above runs-on: ${{ matrix.runner }} strategy: fail-fast: false @@ -236,4 +268,5 @@ jobs: with: name: zerr-max-${{ matrix.id }} path: maxmsp/externals + if-no-files-found: error retention-days: 7 diff --git a/docs/design/dependency-fallbacks.md b/docs/design/dependency-fallbacks.md index ced0000..b932ac9 100644 --- a/docs/design/dependency-fallbacks.md +++ b/docs/design/dependency-fallbacks.md @@ -144,9 +144,19 @@ These cannot share one conan resolve or one CMake configure. `build.sh` refuses Windows for this reason, and it is also why there is no CMake superbuild in this repo: one configure means one toolchain. -Note that `profiles/mingw` pins `tools.build:compiler_executables` to the `x86_64-w64-mingw32-*` -names, and the core CI job deliberately bypasses the profile for that reason — see the comment in -`.github/workflows/build-zerr-core-static-library.yml`. +`profiles/mingw` pins `tools.build:compiler_executables` to the `x86_64-w64-mingw32-*` names. The +core CI job used to bypass the profile with inline settings because that pin was untested on a +runner; it no longer does. Since the `ci.yml` restructure both the core and the PureData jobs +resolve through `profiles/mingw`, and the pinned names do resolve — verified in run 32297202438, +which logged `tools.build:compiler_executables={'c': 'x86_64-w64-mingw32-gcc', ...}` on a green +Windows core build. + +One caveat that survives: the profile declares `compiler.version=13` while the runners now carry +gcc 16.1.0. It works because the `libstdc++11` ABI is stable across those versions, but the +declared version is drifting from the real one, and `compiler.version` is a `package_id` input — so +nothing in the resolve notices when the runner's toolchain moves. `.github/actions/setup-deps` +compensates by keying its conan cache on `ImageVersion`, which turns a runner toolchain bump into a +cache miss rather than a silent reuse of libraries built by the previous compiler. ### 3.8 Core and wrappers must come from one resolve diff --git a/docs/design/repo-audit-2026-07-30.md b/docs/design/repo-audit-2026-07-30.md index a85498a..b64eb93 100644 --- a/docs/design/repo-audit-2026-07-30.md +++ b/docs/design/repo-audit-2026-07-30.md @@ -202,6 +202,23 @@ Net effect: 23 commits rewriting the core have **never been built on Linux or Wi locally on arm64 macOS. No workflow covers Max/MSP or JACK at all. The Windows job in the PureData workflow also carries two "Set up Conan" blocks, one commented out — leftover experimentation. +> **Status: resolved.** Both workflows named above were deleted and replaced by a single +> `.github/workflows/ci.yml`; the description here is kept for the record, not as current state. +> +> - `pull_request` now triggers the whole graph, so PRs are gated. Nothing ran on PRs #13–#16. +> - The `workflow_run` chain is gone. It was not merely fragile: `workflow_run` checks out the +> *default branch* rather than the triggering ref while downloading the triggering run's artifact, +> so dispatching the core build on a feature branch compiled old `main` sources against a new +> `libzerr_core.a`. Wrapper jobs now take the core artifact from their own run via `needs:`. +> - Artifacts are keyed by **toolchain**, not platform (`zerr-core-windows-mingw`), because Windows +> needs a MinGW core for PureData and an MSVC/static-CRT core for Max/MSP. +> - **Max/MSP has CI for the first time**, on macOS. Windows Max needs the MSVC profile described in +> [`dependency-fallbacks.md`](dependency-fallbacks.md) §3.7 and is still outstanding. +> - JACK remains uncovered, and cannot be — see §2.4, it does not build at all. +> - The fat-binary gap in §2.5 above now applies to published artifacts too: the macOS CI runners +> are arm64, so `zerr-max-macos` and `zerr-pd-macos` ship a single arm64 slice while CLAUDE.md +> still advertises Intel support. Knowingly deferred, not fixed. + ### 2.7 Minor - **`doxygen` not installed locally**, so the docs pipeline this branch added cannot be exercised From 02944afe7257b9e746564010b5e21ce88fdcfb51 Mon Sep 17 00:00:00 2001 From: Zeyu Yang <40936154+zeyuyang42@users.noreply.github.com> Date: Tue, 25 Aug 2026 01:13:27 +0200 Subject: [PATCH 3/3] Read ImageVersion in a shell so it reaches the cache key The previous commit put ${{ env.ImageVersion }} in the cache key to make a runner toolchain bump a cache miss. It did not work: the `env` context is not populated with the runner image's variables inside a composite action's `with:` block, so the expression expanded to an empty string. Run 32787801135 shows the result -- `conan-Windows--profiles/mingw-b46d38c5...`, with a blank field where the image id should be. The key changed once, which forced a one-time miss, and then went back to being blind to the compiler. Read it in a shell step instead, where the variable is actually set, and pass it through a step output. Falls back to "unknown" rather than empty so a self-hosted runner without the variable still produces a well-formed key. Co-Authored-By: Claude Opus 5 (1M context) --- .github/actions/setup-deps/action.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/actions/setup-deps/action.yml b/.github/actions/setup-deps/action.yml index 67820d0..c709d9a 100644 --- a/.github/actions/setup-deps/action.yml +++ b/.github/actions/setup-deps/action.yml @@ -29,11 +29,21 @@ runs: # it hit the 10GB repository ceiling and started evicting live entries. A full # miss costs one from-source build of fftw and yaml-cpp, which is the correct # price for a changed toolchain. + # ImageVersion has to be read in a shell: the `env` context is not populated + # with the runner image's variables inside a composite action's `with:`, so + # `${{ env.ImageVersion }}` there silently expands to an empty string and the + # key gains a blank field instead of the image id. Verified the wrong way + # round in run 32787801135, whose key came out `conan-Windows--profiles/...`. + - name: Identify runner image + id: image + shell: bash + run: echo "id=${ImageVersion:-unknown}" >> "$GITHUB_OUTPUT" + - name: Cache conan packages uses: actions/cache@v4 with: path: ~/.conan2 - key: conan-${{ runner.os }}-${{ env.ImageVersion }}-${{ inputs.profile }}-${{ hashFiles('conanfile.txt', 'profiles/*') }} + key: conan-${{ runner.os }}-${{ steps.image.outputs.id }}-${{ inputs.profile }}-${{ hashFiles('conanfile.txt', 'profiles/*') }} # Pinned below the next major: a bare `pip install conan` would float to # conan 3.x and break these jobs with no change on our side.