From d6dd2626110da9c7b5284b9fee4430b8d09bdc00 Mon Sep 17 00:00:00 2001 From: Dominic Rubas <1042243+rubas@users.noreply.github.com> Date: Thu, 12 Nov 2026 15:52:27 +0100 Subject: [PATCH 1/3] ci: force the source build with WHISPER_CPP_BUILD, not with a config keyed on CI The Hex publish job compiles the docs on a runner with CI=true. With the config, that compile built whisper.cpp from source. With the variable, only ci.yml and integration.yml build from source, and the publish job downloads the precompiled NIF and checks it against the checksum file. --- .formatter.exs | 2 +- .github/workflows/ci.yml | 2 ++ .github/workflows/integration.yml | 2 ++ config/config.exs | 8 -------- 4 files changed, 5 insertions(+), 9 deletions(-) delete mode 100644 config/config.exs diff --git a/.formatter.exs b/.formatter.exs index b551cdc..395b04d 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,7 +1,7 @@ [ inputs: [ "{mix,.formatter,.credo}.exs", - "{config,lib,test}/**/*.{ex,exs}" + "{lib,test}/**/*.{ex,exs}" ], line_length: 120 ] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 50d7951..592c0d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,6 +13,8 @@ concurrency: env: MIX_ENV: test + # Build the NIF from source: no release has the precompiled NIF of the code under test. + WHISPER_CPP_BUILD: "1" ELIXIR_VERSION: "1.20.4" OTP_VERSION: "29.1.1" RUST_TOOLCHAIN: "1.98.1" diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 8d47806..43bdc27 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -18,6 +18,8 @@ concurrency: env: MIX_ENV: test + # Build the NIF from source: no release has the precompiled NIF of the code under test. + WHISPER_CPP_BUILD: "1" ELIXIR_VERSION: "1.20.4" OTP_VERSION: "29.1.1" RUST_TOOLCHAIN: "1.98.1" diff --git a/config/config.exs b/config/config.exs deleted file mode 100644 index d6839fa..0000000 --- a/config/config.exs +++ /dev/null @@ -1,8 +0,0 @@ -import Config - -# Force source build during this library's own CI / local dev. Downstream -# consumers do *not* see this file - `import_config` in their own -# `config/config.exs` is not transitive across deps - so they continue to -# fetch a precompiled artefact unless they set `WHISPER_CPP_BUILD=1` or the -# equivalent `:rustler_precompiled` config themselves. -config :rustler_precompiled, :force_build, whisper_cpp: System.get_env("CI") == "true" From 57f66f9c1964190eff31abb70573c363d5a2285c Mon Sep 17 00:00:00 2001 From: Dominic Rubas <1042243+rubas@users.noreply.github.com> Date: Thu, 12 Nov 2026 15:52:27 +0100 Subject: [PATCH 2/3] release: publish to Hex when the checksum PR merges --- .github/workflows/release.yml | 57 +++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 714c095..614da3a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -31,6 +31,7 @@ jobs: cancel-in-progress: false outputs: should_release: ${{ steps.resolve.outputs.should_release }} + should_publish: ${{ steps.resolve.outputs.should_publish }} tag: ${{ steps.resolve.outputs.tag }} version: ${{ steps.resolve.outputs.version }} sha: ${{ steps.resolve.outputs.sha }} @@ -48,6 +49,7 @@ jobs: env: EVENT_NAME: ${{ github.event_name }} INPUT_TAG: ${{ inputs.tag }} + BEFORE: ${{ github.event.before }} run: | VERSION="$(sed -n 's/^ @version "\([^"]*\)"$/\1/p' mix.exs | head -n1)" if [ -z "$VERSION" ]; then @@ -55,6 +57,7 @@ jobs: exit 1 fi TAG="v$VERSION" + SHOULD_PUBLISH=false if [ "$EVENT_NAME" = "workflow_dispatch" ]; then if [ "$TAG" != "$INPUT_TAG" ]; then @@ -64,6 +67,12 @@ jobs: SHOULD_RELEASE=true elif git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then SHOULD_RELEASE=false + + # The merge of the checksum PR publishes: only then does main hold the checksums of the + # release assets, and the Hex package must carry them. + if git diff --name-only "$BEFORE" "$GITHUB_SHA" | grep -qxF checksum-Elixir.WhisperCpp.Native.exs; then + SHOULD_PUBLISH=true + fi else SHOULD_RELEASE=true fi @@ -76,6 +85,7 @@ jobs: fi echo "should_release=$SHOULD_RELEASE" >> "$GITHUB_OUTPUT" + echo "should_publish=$SHOULD_PUBLISH" >> "$GITHUB_OUTPUT" echo "tag=$TAG" >> "$GITHUB_OUTPUT" echo "version=$VERSION" >> "$GITHUB_OUTPUT" echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" @@ -376,3 +386,50 @@ jobs: artifacts/*.tar.gz artifacts/SHA256SUMS generate_release_notes: true + + publish: + name: Publish to Hex + needs: detect + if: needs.detect.outputs.should_publish == 'true' + runs-on: ubuntu-24.04 + # The Hex workload identity of the package names this environment and this workflow file. + environment: hex + permissions: + contents: read # check out the repository + id-token: write # mix hex.publish exchanges the job's OIDC token for a Hex token + concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-publish + cancel-in-progress: false + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ needs.detect.outputs.sha }} + persist-credentials: false + + # Hex versions are immutable, so a package with checksums of other archives stays broken. + - name: Compare the checksum file with the release + env: + TAG: ${{ needs.detect.outputs.tag }} + run: | + diff \ + <(curl -fsSL "https://github.com/$GITHUB_REPOSITORY/releases/download/$TAG/SHA256SUMS" | awk '{print $2 " sha256:" $1}' | sort) \ + <(sed -n 's/^ "\(.*\)" => "\(sha256:.*\)",$/\1 \2/p' checksum-Elixir.WhisperCpp.Native.exs | sort) + + - uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1 + env: + INSTALL_DIR_FOR_OTP: ${{ runner.tool_cache }}/setup-beam/otp-29.1.2 + INSTALL_DIR_FOR_ELIXIR: ${{ runner.tool_cache }}/setup-beam/elixir-1.20.5 + with: + elixir-version: "1.20.5" + otp-version: "29.1.2" + + - name: Install dependencies + run: | + mix local.hex --force + mix local.rebar --force + mix deps.get + + # With no Hex credential in the job, Hex publishes through the workload identity. The docs + # compile downloads the precompiled NIF of this runner and checks it against the checksum file. + - name: Publish the package and its docs + run: mix hex.publish --yes From 3be2f41984f548c28bfc1e674e582e661c8e50aa Mon Sep 17 00:00:00 2001 From: Dominic Rubas <1042243+rubas@users.noreply.github.com> Date: Thu, 12 Nov 2026 15:52:27 +0100 Subject: [PATCH 3/3] AGENTS.md: the checksum PR publishes to Hex --- AGENTS.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 2e16def..2fa577d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,7 @@ users never build Rust. ## Checks - `ci.yml` runs `task check` on pushes to `main` and on pull requests. A push to a branch without a pull request runs - nothing. + nothing. `ci.yml` and `integration.yml` set `WHISPER_CPP_BUILD=1`, so they test the NIF of the code under test. - `task test:integration` runs real inference. `integration.yml` runs it weekly and on manual dispatch, never on a pull request. Run it locally when you change the NIF boundary. - `release.yml` builds six NIFs (target and variant). One backend takes minutes to build from source, so leave the @@ -54,11 +54,21 @@ users never build Rust. 3. After the tag exists, a manual dispatch builds the tag's commit. It fails when the tag is missing or its `mix.exs` version differs, and it uploads only the assets the release does not have yet. It never replaces a published tarball, because a new tarball breaks the checksum file in the Hex package. -4. Regenerate `checksum-Elixir.WhisperCpp.Native.exs` from the published assets, then commit and push it. This keeps +4. Regenerate `checksum-Elixir.WhisperCpp.Native.exs` from the published assets, and open a PR with it. This keeps the checksum of each tag reproducible from the repo: ```bash mix rustler_precompiled.download WhisperCpp.Native --all --no-config --ignore-unavailable --print ``` -5. Run `mix hex.publish` from a clean tree. `mix.exs` puts `checksum-*.exs` in the Hex tarball. +5. Merge the checksum PR. A push to `main` that changes the checksum file, for a version that has its tag, starts the + `Publish to Hex` job of `release.yml`. It compares the file with the release's `SHA256SUMS`, stops when they differ, + and runs `mix hex.publish --yes` in the GitHub environment `hex`, which only `main` can use. The job keeps no Hex + key: the package has a workload identity on hex.pm for `rubas/whisper_cpp`, workflow `release.yml`, environment + `hex`, and Hex trades the job's OIDC token for a short token that can only publish `whisper_cpp`. +6. The package has staged publishing on. The job ends with `Package staged for approval at `, and an owner + approves the release on hex.pm with their 2FA within 7 days. A failed docs upload prints `Publishing failed` but + leaves the job green, so read the end of its log. When the job fails, fix the cause and re-run it. + +Merge nothing else to `main` between the version bump and the checksum PR, or the package holds sources that the NIFs +were not built from. Hex versions are immutable, so change the checksum file only in the checksum PR of a release.