diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 387bed2b9..dd0f477a6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -52,10 +52,12 @@ jobs: uses: ./.github/actions/setup # STT is the bundled whisper-stt-server (whisper.cpp with native DTW token - # timestamps); no VAD model is fetched here. The binary is built by - # build-whisper-stt.yml and staged below — without that step the installer - # ships without speech-to-text. See - # technical-documentation/architecture/transcription-and-captions.md. + # timestamps); no model is fetched here. The binary is built by + # build-whisper-stt.yml and staged below, from the run built from this + # commit's helper sources — without that step the installer ships without + # speech-to-text. See + # technical-documentation/architecture/transcription-and-captions.md, and + # technical-documentation/engineering/release-and-secrets.md for the run. - name: Stage whisper-stt binaries shell: bash env: diff --git a/scripts/stage-whisper-stt.sh b/scripts/stage-whisper-stt.sh index cb09ca555..a3b955cb6 100755 --- a/scripts/stage-whisper-stt.sh +++ b/scripts/stage-whisper-stt.sh @@ -79,20 +79,56 @@ fi TMP="$(mktemp -d)" trap 'rm -rf "${TMP}"' EXIT -echo "Fetching ${ARTIFACT} from the latest successful build-whisper-stt run..." -# No run id: gh resolves the most recent run that published this artifact. -# Artifacts expire (retention-days in build-whisper-stt.yml), so a stale branch +# Which run: the most recent successful build of THIS commit's helper sources. +# Not simply the most recent artifact, which is whatever branch last pushed a +# helper change: on 2026-09-30 that was a PR branch built from main without the +# fix 2.0.0-rc.2 was cut for, and rc.1 had shipped whatever main last built. +# Sources are compared by git object id, so the release branch's cherry-pick of +# a change matches the run built from main. +SOURCES=(electron/native/whisper-stt scripts/build-whisper-stt.sh .github/workflows/build-whisper-stt.yml) +object_at() { # : the path's git object id at that commit, from the API + gh api "repos/${REPO}/contents/$(dirname "$2")?ref=$1" --jq ".[] | select(.path == \"$2\") | .sha" +} +same_sources() { # : were the helper's sources there the ones checked out here? + local path + for path in "${SOURCES[@]}"; do + [ "$(object_at "$1" "${path}")" = "$(git rev-parse "HEAD:${path}")" ] || return 1 + done +} +RUN_ID="" +while read -r id sha; do + if same_sources "${sha}"; then RUN_ID="${id}"; break; fi +done < <(gh run list --repo "${REPO}" --workflow build-whisper-stt.yml --status success \ + --limit 50 --json databaseId,headSha --jq '.[] | "\(.databaseId) \(.headSha)"') +if [ -z "${RUN_ID}" ]; then + cat >&2 < + +Refusing to package a helper built from other sources than the ones this +release ships. +EOF + exit 1 +fi + +echo "Fetching ${ARTIFACT} from build-whisper-stt run ${RUN_ID} (same helper sources)..." +# Artifacts expire (retention-days in build-whisper-stt.yml), so an old commit # can legitimately find nothing — say so in terms someone can act on. -if ! gh run download --repo "${REPO}" --name "${ARTIFACT}" --dir "${TMP}" 2>"${TMP}/err"; then +if ! gh run download "${RUN_ID}" --repo "${REPO}" --name "${ARTIFACT}" --dir "${TMP}" 2>"${TMP}/err"; then cat "${TMP}/err" >&2 cat >&2 < Refusing to package: the installer would ship with speech-to-text silently dead (no transcription, no captions). diff --git a/technical-documentation/engineering/release-and-secrets.md b/technical-documentation/engineering/release-and-secrets.md index 0895b98b2..f41637705 100644 --- a/technical-documentation/engineering/release-and-secrets.md +++ b/technical-documentation/engineering/release-and-secrets.md @@ -21,6 +21,8 @@ The workflow computes `X.Y.Z-rc.N`, migrates items from `Next Release` to the `v The two workflows push their tags with different credentials, which is deliberate: `promote.yml` uses `GITHUB_TOKEN` (a tag is a ref, not a file change), while `prerelease.yml` pushes the RC tag with `OPENSCREEN_RELEASE_TOKEN`. A `GITHUB_TOKEN` tag push is answered with `remote: Internal Server Error` — a 500, not a 403 — by a tag ruleset that rejects the Actions token, and that failure took down the whole `v1.8.0-rc.1` cut, skipping the build trigger and the Discord announce with it. +**Which whisper helper a build ships.** `build.yml` does not compile `whisper-stt-server`: `scripts/stage-whisper-stt.sh` downloads it from a `build-whisper-stt.yml` run, which runs on the pushes that touch the helper. It takes the most recent successful run built from the **same helper sources** as the commit being packaged (the `electron/native/whisper-stt` tree, `scripts/build-whisper-stt.sh` and the workflow, compared by git object id), so the release branch's cherry-pick of a helper change matches the run built from `main`. No such run fails the build, with the command that makes one. Until 2026-09-30 it took the most recent artifact of any branch: `v2.0.0-rc.1` shipped whatever `main` had last built, and a PR branch pushed an hour later would have decided what rc.2 shipped. So after cherry-picking a helper change, let a `build-whisper-stt` run of those sources finish before dispatching `prerelease.yml`. + RC tags are signed and notarized exactly like stable ones. That keeps testers out of `xattr -rd com.apple.quarantine`, and exercises the whole credential path on every candidate instead of first proving it on the promotion build. ### Promote to stable