#!/usr/bin/env bash
# release-version-assert.sh — refuse to publish a release whose built binary
# disagrees with the tag.
#
# A tag pushed without bumping `[workspace.package] version` ships a binary that
# self-reports the PREVIOUS release. release.yml's changelog guard catches a
# missing `## [<version>]` section but says nothing about the artifact, so such a
# release looks correct in every place a human reads and is wrong in the one
# place the self-updater reads.
#
# Usage: bash .github/ci/release-version-assert.sh <dist-dir>
#   GITHUB_REF_NAME  the tag (vX.Y.Z). Required.
#   RUNNER_TEMP      scratch dir for the executable copies. Optional.
#
# WHY IT LIVES HERE AND NOT INLINE IN release.yml. The release workflow is
# tag-triggered: it cannot be run from a dev box, so logic inline in its YAML is
# logic nobody can exercise before the one run that matters. As a script beside
# `ci-notify.sh` and `reap-census.sh` it gets `release-version-assert-selftest.sh`,
# which drives the whole matrix in milliseconds on any box.
#
# WHY THE ASSEMBLE JOB. The assert has to EXECUTE a binary, and a cross-compiled
# artifact cannot run on the runner that produced it. `assemble` is the one place
# the whole asset set exists at once, on a Linux box that can natively execute
# part of it — and these are the exact bytes about to be published, not a
# build-tree binary that merely resembles them.
#
# THE WINDOWS ASSET IS NOT COVERED, stated rather than glossed. All three
# binaries come from ONE checkout of ONE tag and take their version from the same
# `[workspace.package] version` through `env!("CARGO_PKG_VERSION")`, so a Linux
# artifact carrying the right version is evidence the workspace was bumped. It is
# NOT evidence that the Windows leg built from that same tree, which is what a
# real cross-platform assert would add. The gap is the runner's, not a choice
# about rigor.
# [impl->REQ-RELEASE-VERSION-TAG-ASSERT]
set -euo pipefail

dist="${1:-dist}"
: "${GITHUB_REF_NAME:?GITHUB_REF_NAME (the tag) must be set}"
want="${GITHUB_REF_NAME#v}" # v0.1.2 -> 0.1.2

# Natively executable on a Linux runner. Deliberately a LITERAL LIST and not a
# glob over $dist: a glob that matched nothing would assert nothing and exit
# green, which reads exactly like a passing check.
#
# SPT_RELEASE_ASSERT_ASSETS overrides it for the selftest. It is echoed loudly
# below whenever it is in play, because a seam that can NARROW a release gate
# must never be able to do so quietly — and the zero-count refusal at the bottom
# is what stops it narrowing all the way to nothing.
default_assets="spt-x86_64-linux spt-x86_64-linux-musl"
runnable="${SPT_RELEASE_ASSERT_ASSETS-$default_assets}"
if [ "$runnable" != "$default_assets" ]; then
  echo "version assert: NON-DEFAULT asset list in play: '${runnable}' (default: '${default_assets}')"
fi

expected=0
for _ in $runnable; do expected=$((expected + 1)); done

probe="${RUNNER_TEMP:-$(mktemp -d)}/version-probe"
rm -rf "$probe"
mkdir -p "$probe"

checked=0
for asset in $runnable; do
  src="$dist/$asset"
  if [ ! -f "$src" ]; then
    echo "::error title=Release asset missing::expected $src in the assembled set — the build matrix row for $asset produced no artifact, so its version could not be asserted."
    exit 1
  fi

  # Copy before chmod: `actions/download-artifact` does not preserve the
  # executable bit, and SHA256SUMS is computed over $dist — nothing here may
  # touch a byte or a mode of what gets published.
  cp "$src" "$probe/$asset"
  chmod +x "$probe/$asset"

  # Captured through `if`, not as a bare assignment followed by `$?`: under
  # `set -e` a non-zero `--version` aborts on the assignment and the named
  # diagnostic below never prints. A command in an `if` condition is exempt.
  if out="$("$probe/$asset" --version 2>&1)"; then
    :
  else
    rc=$?
    echo "::error title=Release binary will not run::$asset exited $rc on \`--version\`: ${out:-<no output>}"
    exit 1
  fi

  # clap renders `spt <version>`; take the last field so the assert does not
  # depend on the program-name prefix. The raw output is echoed so a future
  # change in that shape is diagnosable from the log rather than surfacing as a
  # version mismatch.
  got="$(printf '%s' "$out" | awk '{ print $NF }')"
  echo "version assert: asset=$asset raw='$out' got=$got want=$want"

  if [ "$got" != "$want" ]; then
    echo "::error title=Release version does not match tag::$asset reports version '$got' but the tag is '${GITHUB_REF_NAME}' (expected '$want'). The tag was pushed without bumping the workspace version: bump \`[workspace.package] version\` in Cargo.toml AND the first-party \`spt-*\` lines in Cargo.lock, then re-tag. Do NOT publish this draft."
    exit 1
  fi
  checked=$((checked + 1))
done

# An assert that ran zero times is indistinguishable from one that passed, and
# an empty list is the one way to reach here having compared nothing: every
# other short-fall (a missing artifact, an unrunnable binary) already exited 1
# inside the loop.
#
# A `[ "$checked" -ne "$expected" ]` row was written here and then REMOVED: it
# cannot fail unless one of those in-loop exits already has, so it would have
# been a green row standing in for a check. The mutation run that caught it —
# delete the row, selftest still 12/12 — is the evidence, and the reason the
# zero-list refusal below is kept is the same run: deleting THAT one reds the
# empty-list case immediately.
if [ "$expected" -eq 0 ]; then
  echo "::error title=Version assert did not run::the asset list is empty — nothing was asserted."
  exit 1
fi

echo "version assert: $checked/$expected natively-runnable assets report $want (the windows asset is not executable on this runner — see the header)"
