chore: version packages #3044
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # This Source Code Form is subject to the terms of the Mozilla Public | |
| # License, v. 2.0. If a copy of the MPL was not distributed with this | |
| # file, You can obtain one at https://mozilla.org/MPL/2.0/. | |
| name: Server Binaries | |
| on: | |
| # push:main intentionally dropped — branch-protected PR runs already | |
| # validate the same SHA, and the release event re-validates before | |
| # uploading binaries. See PR that introduced this change. | |
| pull_request: | |
| paths: | |
| - '.github/workflows/server-binaries.yml' | |
| - '.github/workflows/release.yml' | |
| - '.cargo/**' | |
| - 'Cargo.lock' | |
| - 'Cargo.toml' | |
| - 'apps/server/**' | |
| - 'rust/**' | |
| - 'rust-toolchain.toml' | |
| release: | |
| types: | |
| - published | |
| workflow_dispatch: | |
| inputs: | |
| upload-to-tag: | |
| # Backfill lever: a plain dispatch only produces 1-day artifacts, so | |
| # before this input existed a release missing an archive (issue | |
| # #2619, v1.16.6 shipped without the win32-x64 zip) could not be | |
| # repaired self-service. Supplying an existing v* tag uploads the | |
| # rebuilt archives to that release and re-verifies its assets. | |
| description: 'Existing release tag (e.g. v1.16.6) to upload the built archives to; leave empty for artifacts only' | |
| required: false | |
| type: string | |
| default: '' | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| # Cancel obsolete runs on force-push; release/workflow_dispatch | |
| # runs share the workflow group too, but those are gated by | |
| # event_name so they won't trample real releases. | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| permissions: | |
| contents: read | |
| jobs: | |
| validate-server-binaries: | |
| name: Validate Server Binary (${{ matrix.target }}) | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - target: linux-x64 | |
| os: ubuntu-latest | |
| rust-target: x86_64-unknown-linux-gnu | |
| archive: tar.gz | |
| - target: linux-arm64 | |
| os: ubuntu-latest | |
| rust-target: aarch64-unknown-linux-gnu | |
| archive: tar.gz | |
| cross: true | |
| - target: linux-x64-musl | |
| os: ubuntu-latest | |
| rust-target: x86_64-unknown-linux-musl | |
| archive: tar.gz | |
| steps: | |
| - name: Checkout Repo | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| lfs: false | |
| persist-credentials: false | |
| - name: Setup Rust | |
| # Pin to a master-branch SHA + explicit `toolchain: stable`; | |
| # see docs.yml note for rationale on the pinning strategy. | |
| # | |
| # `toolchain: stable` here names what gets INSTALLED (and what | |
| # `targets:` below adds the target std for) — it is NOT what actually | |
| # compiles this crate. The repo's `rust-toolchain.toml` pins nightly, | |
| # and rustup's per-directory toolchain file always wins over | |
| # `rustup default` (which is all this action sets); every `cargo` | |
| # invocation below runs on the pinned nightly regardless of this | |
| # `stable` input. Confirmed locally: `rustup default stable` followed | |
| # by `rustc --version` inside a checkout of this repo still reports | |
| # the nightly pinned in rust-toolchain.toml, with rustup noting the | |
| # override. See the CARGO_UNSTABLE_BUILD_STD env var on the "Build | |
| # Server Binary" step below for the consequence: the aarch64/cross-arch | |
| # target std was only ever installed for `stable`, never for the | |
| # nightly that actually runs, so those targets need std rebuilt from | |
| # source instead of relying on a prebuilt one that was never fetched. | |
| uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # master 2026-05 | |
| with: | |
| toolchain: stable | |
| targets: ${{ matrix.rust-target }} | |
| - name: Install cross | |
| if: matrix.target == 'linux-x64-musl' | |
| uses: taiki-e/install-action@b47b3b9ab771927700bcf4ef8a4f79e1d7c9a8cd # cross (snapshot pinned 2026-05) | |
| - name: Install Linux Cross-Compilation Tools | |
| if: matrix.cross == true && runner.os == 'Linux' | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y gcc-aarch64-linux-gnu | |
| - name: Install musl Tools | |
| if: matrix.target == 'linux-x64-musl' | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y musl-tools musl-dev | |
| - name: Build Server Binary | |
| # Force bash so the if/else runs under Git Bash on Windows runners too | |
| # (default shell there is PowerShell, which choked on the POSIX test → | |
| # win32-x64 release build failed with a ParserError). | |
| shell: bash | |
| run: | | |
| if [ "${{ matrix.target }}" = "linux-x64-musl" ]; then | |
| cross build --release --package ifc-lite-server --target ${{ matrix.rust-target }} | |
| else | |
| # See the "Setup Rust" step above: this actually runs on the | |
| # nightly pinned by rust-toolchain.toml, not the `stable` named | |
| # there, so cross-arch legs (linux-arm64, darwin-x64 on the | |
| # arm64 macos-14 runner) need their target's std rebuilt from | |
| # source — it was never installed for nightly. Was previously | |
| # papered over by a global `[unstable] build-std` in | |
| # .cargo/config.toml (removed — see scripts/build-wasm.sh); | |
| # scoped here instead. It rebuilds std from source on every | |
| # non-`cross` leg, including the host-matching ones | |
| # (linux-x64, darwin-arm64, win32-x64) — build-std doesn't | |
| # check for a prebuilt std first, so it's redundant there, not | |
| # a no-op. That matches what `main`'s global build-std already | |
| # did for all of them; narrowing it to just the cross-arch legs | |
| # is deliberate follow-up work, not done here. Deliberately NOT | |
| # set for the `cross build` branch above: that runs inside | |
| # `cross`'s own Docker image with its own toolchain setup, | |
| # wasn't reported broken, and forwarding host env vars into | |
| # that container isn't verified here. | |
| CARGO_UNSTABLE_BUILD_STD=std,panic_abort cargo build --release --package ifc-lite-server --target ${{ matrix.rust-target }} | |
| fi | |
| env: | |
| CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc | |
| # The `cargo build` step above is the validation — it fails the job | |
| # if the binary doesn't compile. We deliberately don't archive or | |
| # upload the binary here: nothing downloads a PR-run server binary, | |
| # and 90-day-retained artifacts on every Rust PR were burning Actions | |
| # storage credits for no benefit. | |
| release-server-binaries: | |
| name: Release Server Binary (${{ matrix.target }}) | |
| # Only build + upload server binaries for version-tagged releases (e.g. | |
| # `v2.1.9`). Without this filter every published release fires this job, | |
| # so non-version tags like `fixtures-v1` end up with stray server | |
| # tarballs uploaded to the wrong release. Manual `workflow_dispatch` | |
| # is still allowed for ad-hoc rebuilds. | |
| if: | | |
| (github.event_name == 'release' && startsWith(github.event.release.tag_name, 'v')) | |
| || github.event_name == 'workflow_dispatch' | |
| runs-on: ${{ matrix.os }} | |
| permissions: | |
| contents: write | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - target: linux-x64 | |
| os: ubuntu-latest | |
| rust-target: x86_64-unknown-linux-gnu | |
| archive: tar.gz | |
| - target: linux-arm64 | |
| os: ubuntu-latest | |
| rust-target: aarch64-unknown-linux-gnu | |
| archive: tar.gz | |
| cross: true | |
| - target: linux-x64-musl | |
| os: ubuntu-latest | |
| rust-target: x86_64-unknown-linux-musl | |
| archive: tar.gz | |
| - target: darwin-x64 | |
| os: macos-14 | |
| rust-target: x86_64-apple-darwin | |
| archive: tar.gz | |
| - target: darwin-arm64 | |
| os: macos-14 | |
| rust-target: aarch64-apple-darwin | |
| archive: tar.gz | |
| - target: win32-x64 | |
| os: windows-latest | |
| rust-target: x86_64-pc-windows-msvc | |
| archive: zip | |
| steps: | |
| - name: Checkout Repo | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| # The build ref and the workflow ref deliberately differ on a | |
| # manual backfill. A dispatch runs the workflow DEFINITION from the | |
| # dispatch ref (normally the default branch, which is the only ref | |
| # that has the `upload-to-tag` input), but the SOURCE being built | |
| # must be the tag under repair: the install-time resolver in | |
| # packages/server-bin picks its release by the installed package | |
| # version, so an archive built from any other revision would ship | |
| # different code under that old version number. Do not "simplify" | |
| # this to the default checkout. On a `release` event | |
| # `inputs.upload-to-tag` is empty, so this falls back to | |
| # `github.ref` (the release tag), identical to the default. | |
| ref: ${{ inputs.upload-to-tag || github.ref }} | |
| lfs: false | |
| persist-credentials: false | |
| - name: Setup Rust | |
| # Pin to a master-branch SHA + explicit `toolchain: stable`; | |
| # see docs.yml note for rationale on the pinning strategy. | |
| # | |
| # `toolchain: stable` here names what gets INSTALLED (and what | |
| # `targets:` below adds the target std for) — it is NOT what actually | |
| # compiles this crate. The repo's `rust-toolchain.toml` pins nightly, | |
| # and rustup's per-directory toolchain file always wins over | |
| # `rustup default` (which is all this action sets); every `cargo` | |
| # invocation below runs on the pinned nightly regardless of this | |
| # `stable` input. Confirmed locally: `rustup default stable` followed | |
| # by `rustc --version` inside a checkout of this repo still reports | |
| # the nightly pinned in rust-toolchain.toml, with rustup noting the | |
| # override. See the CARGO_UNSTABLE_BUILD_STD env var on the "Build | |
| # Server Binary" step below for the consequence: the aarch64/cross-arch | |
| # target std was only ever installed for `stable`, never for the | |
| # nightly that actually runs, so those targets need std rebuilt from | |
| # source instead of relying on a prebuilt one that was never fetched. | |
| uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # master 2026-05 | |
| with: | |
| toolchain: stable | |
| targets: ${{ matrix.rust-target }} | |
| - name: Install cross | |
| if: matrix.target == 'linux-x64-musl' | |
| uses: taiki-e/install-action@b47b3b9ab771927700bcf4ef8a4f79e1d7c9a8cd # cross (snapshot pinned 2026-05) | |
| - name: Install Linux Cross-Compilation Tools | |
| if: matrix.cross == true && runner.os == 'Linux' | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y gcc-aarch64-linux-gnu | |
| - name: Install musl Tools | |
| if: matrix.target == 'linux-x64-musl' | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y musl-tools musl-dev | |
| - name: Build Server Binary | |
| # Force bash so the if/else runs under Git Bash on Windows runners too | |
| # (default shell there is PowerShell, which choked on the POSIX test → | |
| # win32-x64 release build failed with a ParserError). | |
| shell: bash | |
| run: | | |
| if [ "${{ matrix.target }}" = "linux-x64-musl" ]; then | |
| cross build --release --package ifc-lite-server --target ${{ matrix.rust-target }} | |
| else | |
| # See the "Setup Rust" step above: this actually runs on the | |
| # nightly pinned by rust-toolchain.toml, not the `stable` named | |
| # there, so cross-arch legs (linux-arm64, darwin-x64 on the | |
| # arm64 macos-14 runner) need their target's std rebuilt from | |
| # source — it was never installed for nightly. Was previously | |
| # papered over by a global `[unstable] build-std` in | |
| # .cargo/config.toml (removed — see scripts/build-wasm.sh); | |
| # scoped here instead. It rebuilds std from source on every | |
| # non-`cross` leg, including the host-matching ones | |
| # (linux-x64, darwin-arm64, win32-x64) — build-std doesn't | |
| # check for a prebuilt std first, so it's redundant there, not | |
| # a no-op. That matches what `main`'s global build-std already | |
| # did for all of them; narrowing it to just the cross-arch legs | |
| # is deliberate follow-up work, not done here. Deliberately NOT | |
| # set for the `cross build` branch above: that runs inside | |
| # `cross`'s own Docker image with its own toolchain setup, | |
| # wasn't reported broken, and forwarding host env vars into | |
| # that container isn't verified here. | |
| CARGO_UNSTABLE_BUILD_STD=std,panic_abort cargo build --release --package ifc-lite-server --target ${{ matrix.rust-target }} | |
| fi | |
| env: | |
| CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc | |
| - name: Prepare Binary (Unix) | |
| if: runner.os != 'Windows' | |
| run: | | |
| mkdir -p dist | |
| cp target/${{ matrix.rust-target }}/release/ifc-lite-server dist/ | |
| chmod +x dist/ifc-lite-server | |
| - name: Prepare Binary (Windows) | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| New-Item -ItemType Directory -Force -Path dist | |
| Copy-Item "target/${{ matrix.rust-target }}/release/ifc-lite-server.exe" -Destination "dist/" | |
| - name: Create Archive (tar.gz) | |
| if: matrix.archive == 'tar.gz' | |
| run: | | |
| cd dist | |
| tar -czf ../ifc-lite-server-${{ matrix.target }}.tar.gz ifc-lite-server | |
| - name: Create Archive (zip) | |
| if: matrix.archive == 'zip' | |
| shell: pwsh | |
| run: | | |
| Compress-Archive -Path "dist/ifc-lite-server.exe" -DestinationPath "ifc-lite-server-${{ matrix.target }}.zip" | |
| - name: Create Checksum Sidecar | |
| # The sidecar must hash the SAME bytes that get uploaded, so it is | |
| # computed here, right after the archive is written, on the leg's own | |
| # runner - never from a later rebuild. Forced to bash on all three | |
| # OSes for the same reason as the build step above. sha256sum does | |
| # not exist on the macOS runners (shasum is the macOS spelling), so | |
| # probe for either and fail hard when neither exists: an archive | |
| # published without its sidecar would defeat the fail-closed | |
| # install-time verification in packages/server-bin/src/checksum.ts. | |
| shell: bash | |
| run: | | |
| # Deliberately NOT named `asset`: that binding lives in the upload | |
| # step below, where the parity gate pins it, and a second variable | |
| # of the same name in another step invites edits to the wrong one. | |
| # A drift between this name and the upload step's fails loudly at | |
| # upload time (file not found), never silently. | |
| built="ifc-lite-server-${{ matrix.target }}.${{ matrix.archive }}" | |
| if command -v sha256sum >/dev/null 2>&1; then | |
| sha256_tool="sha256sum" | |
| elif command -v shasum >/dev/null 2>&1; then | |
| sha256_tool="shasum -a 256" | |
| else | |
| echo "ERROR: neither sha256sum nor shasum exists on this runner; refusing to build an unverifiable archive" >&2 | |
| exit 1 | |
| fi | |
| # The backfill branch of the upload step below hashes published | |
| # bytes with the same tool; export the probed spelling once. | |
| echo "SHA256_TOOL=$sha256_tool" >> "$GITHUB_ENV" | |
| $sha256_tool "$built" > "$built.sha256" | |
| # Self-check before anything can reach a release: exactly the | |
| # "<64-hex> <archive>" line the install-time parser accepts. | |
| grep -Eq "^[0-9a-f]{64} [ *]?$built\$" "$built.sha256" | |
| cat "$built.sha256" | |
| - name: Upload Build Artifact | |
| if: github.event_name == 'workflow_dispatch' | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: ifc-lite-server-${{ matrix.target }} | |
| path: | | |
| ifc-lite-server-${{ matrix.target }}.${{ matrix.archive }} | |
| ifc-lite-server-${{ matrix.target }}.${{ matrix.archive }}.sha256 | |
| if-no-files-found: error | |
| # Ad-hoc rebuild artifacts only - keep them briefly so they | |
| # don't accumulate against Actions storage. | |
| retention-days: 1 | |
| - name: Upload to GitHub Release | |
| # Also runs on a manual dispatch that names an existing tag, so a | |
| # release with a missing archive can be backfilled (issue #2619). | |
| if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && inputs.upload-to-tag != '') | |
| # Force bash so multi-line scripts parse on Windows runners too | |
| # (default shell there is PowerShell, which once read a bare | |
| # `--clobber` line-continuation as the unary decrement operator and | |
| # failed the win32-x64 release upload with a ParserError). | |
| shell: bash | |
| run: | | |
| asset="ifc-lite-server-${{ matrix.target }}.${{ matrix.archive }}" | |
| sidecar="$asset.sha256" | |
| if [ "$GITHUB_EVENT_NAME" = "release" ]; then | |
| # Fresh release: assets are being created for the first time, so | |
| # --clobber is the correct idempotent choice for job re-runs. The | |
| # sidecar is listed first so no upload ordering exists where the | |
| # archive is downloadable while its checksum is not. | |
| gh release upload "$RELEASE_TAG" "$sidecar" "$asset" --clobber | |
| else | |
| # Manual backfill of an existing release: every matrix leg runs, | |
| # including platforms whose assets are already healthy, and | |
| # `gh release upload --clobber` DELETES the existing asset before | |
| # uploading ("If the upload fails, the original assets will be | |
| # lost"). A transient failure would then destroy a working | |
| # archive for a platform that was never broken. So only upload | |
| # what is actually missing, and never clobber a healthy archive. | |
| existing="$(gh release view "$RELEASE_TAG" --json assets --jq '.assets[].name')" | |
| have() { printf '%s\n' "$existing" | grep -qxF "$1"; } | |
| if have "$asset"; then | |
| echo "$asset already exists on $RELEASE_TAG; leaving the healthy asset untouched" | |
| if have "$sidecar"; then | |
| echo "$sidecar already exists on $RELEASE_TAG; leaving it untouched" | |
| else | |
| # The sidecar must hash the bytes installs actually download, | |
| # which are the PUBLISHED archive's: a fresh rebuild is byte- | |
| # different (archive timestamps at minimum), so hashing this | |
| # job's rebuild would make every install of the platform fail | |
| # closed. Hashing the published bytes pins them against future | |
| # tampering or corruption; it cannot retroactively prove where | |
| # they came from, which is the honest limit of a backfilled | |
| # checksum. | |
| mkdir -p published | |
| gh release download "$RELEASE_TAG" --pattern "$asset" --dir published | |
| # Probe the published archive before certifying it: a corrupt | |
| # or truncated upload must not receive a valid sidecar, or the | |
| # fail-closed install check would bless exactly the bytes it | |
| # exists to reject. A listing probe catches corruption only, | |
| # NOT tampering - a deliberately altered but well-formed | |
| # archive still gets certified (see the limit above). Each | |
| # matrix leg backfills its own archive type on its own runner: | |
| # tar ships with ubuntu and macos; the win32 leg runs bash on | |
| # windows-latest, where unzip may be absent but 7z is on PATH. | |
| # No probe tool at all is a hard error, never a skip. | |
| case "$asset" in | |
| *.zip) | |
| if command -v unzip >/dev/null 2>&1; then | |
| unzip -t "published/$asset" >/dev/null | |
| elif command -v 7z >/dev/null 2>&1; then | |
| 7z t "published/$asset" >/dev/null | |
| else | |
| echo "ERROR: neither unzip nor 7z exists on this runner; refusing to certify an unprobed zip" >&2 | |
| exit 1 | |
| fi | |
| ;; | |
| *.tar.gz) | |
| tar -tzf "published/$asset" >/dev/null | |
| ;; | |
| *) | |
| echo "ERROR: no integrity probe for $asset; refusing to certify unprobed bytes" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| if [ -z "${SHA256_TOOL:-}" ]; then | |
| echo "ERROR: SHA256_TOOL is unset; refusing to backfill a sidecar without a checksum tool" >&2 | |
| exit 1 | |
| fi | |
| (cd published && $SHA256_TOOL "$asset" > "../$sidecar") | |
| # Same self-check the build path applies before anything can | |
| # reach a release: exactly the "<64-hex> <archive>" line the | |
| # install-time parser accepts. A backfilled sidecar is what | |
| # fail-closed installs will trust, so it must be validated on | |
| # this path too, not only on the build path. | |
| grep -Eq "^[0-9a-f]{64} [ *]?$asset\$" "$sidecar" | |
| cat "$sidecar" | |
| gh release upload "$RELEASE_TAG" "$sidecar" | |
| fi | |
| else | |
| # The archive is missing, so any sidecar left on the release | |
| # hashes bytes that no longer exist; --clobber replaces that | |
| # stale sidecar (and is a no-op for the missing archive itself). | |
| gh release upload "$RELEASE_TAG" "$sidecar" "$asset" --clobber | |
| fi | |
| fi | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # The tag must reach bash as DATA, never as source: `${{ }}` expands | |
| # before bash parses the script, so inlining it would let a tag name | |
| # or `upload-to-tag` input containing shell metacharacters (git | |
| # refnames allow $, (, ), ; and friends) execute in a job holding a | |
| # contents:write token. The matrix.* values inlined above are | |
| # literals defined in this file, so they are not injectable. | |
| RELEASE_TAG: ${{ github.event_name == 'release' && github.event.release.tag_name || inputs.upload-to-tag }} | |
| # Verify the release actually carries one archive per supported platform. | |
| # The upload matrix above can partially fail, and a missing archive makes | |
| # every install on that platform 404 at postinstall (issue #2619). | |
| verify-release-assets: | |
| name: Verify release assets | |
| needs: release-server-binaries | |
| # `always()` is the load-bearing part. A failed `needs:` skips the | |
| # dependent whatever the condition says, so without it the job that | |
| # exists to name a missing archive could not run on the case it exists | |
| # for: a matrix leg that failed or was cancelled. That is exactly how | |
| # v1.16.6 shipped without the win32-x64 zip and nothing said so. | |
| # | |
| # Runs on version-tagged releases, and on a manual dispatch that names | |
| # a tag to backfill - the same two paths that upload assets. | |
| if: | | |
| always() | |
| && ( | |
| (github.event_name == 'release' && startsWith(github.event.release.tag_name, 'v')) | |
| || (github.event_name == 'workflow_dispatch' && inputs.upload-to-tag != '') | |
| ) | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout Repo | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| lfs: false | |
| persist-credentials: false | |
| - name: Verify release carries every supported platform archive | |
| # Also verifies one .sha256 sidecar per archive when the tag's own | |
| # workflow publishes them (the checker inspects the tag's | |
| # server-binaries.yml, so pre-sidecar releases stay verifiable | |
| # without demanding assets they never claimed to ship). | |
| # The script reads the expected archive set from the TAG'S OWN | |
| # platform.ts (git show), not from this checkout: on a manual | |
| # backfill the checkout is the workflow ref (main), whose | |
| # SUPPORTED_TARGETS may have drifted since the tag - main's set | |
| # would demand archives the old release never claimed to ship, a | |
| # false red on exactly the repair path this job exists for. The | |
| # checker code itself still runs from the workflow ref. A `release` | |
| # event checkout already carries the tag ref, a backfill checkout | |
| # does not, so fetch it explicitly (anonymous fetch works: the repo | |
| # is public and persist-credentials is off); if the ref is absent | |
| # the script fails closed rather than falling back to today's set. | |
| run: | | |
| git fetch --depth=1 origin "+refs/tags/${RELEASE_TAG}:refs/tags/${RELEASE_TAG}" | |
| node scripts/check-server-bin-targets.mjs --release "$RELEASE_TAG" | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # Same hardening as the upload step: the tag reaches bash as data | |
| # through env, never spliced into the script source via ${{ }}. | |
| RELEASE_TAG: ${{ github.event_name == 'release' && github.event.release.tag_name || inputs.upload-to-tag }} |