diff --git a/scripts/check-all.sh b/scripts/check-all.sh index 44322fc..409768f 100755 --- a/scripts/check-all.sh +++ b/scripts/check-all.sh @@ -6,11 +6,24 @@ set -euo pipefail +# Exit code 77 from a gate means "skipped: runner/infrastructure unavailable" +# (see scripts/check-linux.sh). A skip is non-fatal and must not block the +# push; a real failure (any other non-zero) still aborts the whole gate. +SKIP=77 + run_gate() { name="$1" shift printf '\n==> %s\n' "$name" + set +e "$@" + status=$? + set -e + if [ "$status" -eq "$SKIP" ]; then + printf ' SKIPPED (runner unavailable; authoritative gate runs on CI)\n' + return 0 + fi + return "$status" } run_gate "style" bash scripts/check-style.sh diff --git a/scripts/check-linux.sh b/scripts/check-linux.sh index 18836aa..ff1357b 100755 --- a/scripts/check-linux.sh +++ b/scripts/check-linux.sh @@ -1,7 +1,41 @@ #!/usr/bin/env bash +# The remote commands below are deliberately composed on the client: local +# config ($REMOTE_DIR, $LIMA_INSTANCE, $REMOTE_BUILD) is expanded here and sent +# to the host as a literal string. That client-side expansion is intended, so +# the SC2029 notes it raises are silenced file-wide rather than per line. +# shellcheck disable=SC2029 + set -euo pipefail +# Exit-code contract (consumed by check-all.sh run_gate): +# +# 0 - Linux gate ran and PASSED. Either this machine is Linux (the gate runs +# natively), or the self-hosted relay was reachable and the relayed +# Swift build/test succeeded. +# 77 - Linux gate SKIPPED because the self-hosted relay is unavailable from +# this machine: ssh/rsync missing locally, the host is unreachable, it +# lacks limactl, the working dir cannot be created, the sync fails, or +# the VM will not start. This is a runner-availability limitation, NOT a +# code defect. The authoritative Linux gate is the GitHub-hosted +# workflow (.github/workflows/linux.yml: ubuntu-latest + swift:6.2), +# which runs on every push regardless of this relay. +# 1 - Linux gate RAN on the relay but the Swift build/test FAILED. A real +# Linux failure. It must be fixed, never skipped. +# +# The 77-vs-1 split is the whole point. An unreachable runner must not block a +# green local push (skip), and it must never mask a real Linux failure (which +# stays a hard 1). When in doubt during the build stage we fail hard, because +# masking a real failure is the worse error. + +SKIP=77 + +skip() { + echo "linux: SKIP ($1)." >&2 + echo "linux: authoritative Linux gate is GitHub-hosted .github/workflows/linux.yml." >&2 + exit "$SKIP" +} + run_linux_gate() { if [ -n "${SWIFTLY_HOME_DIR:-}" ] && [ -f "$SWIFTLY_HOME_DIR/env.sh" ]; then # shellcheck source=/dev/null @@ -14,6 +48,7 @@ run_linux_gate() { swift test -c release } +# Native Linux: this machine IS the runner. Hard gate, no relay, no skip path. if [ "$(uname -s)" = "Linux" ]; then run_linux_gate exit 0 @@ -25,21 +60,41 @@ REMOTE_PARENT="${PUREYAML_LINUX_REMOTE_PARENT:-/Volumes/ClawSSD/tmp}" REMOTE_DIR="$REMOTE_PARENT/pureyaml-linux-check" REMOTE_SWIFTLY_HOME="${PUREYAML_LINUX_SWIFTLY_HOME:-/Volumes/ClawSSD/swiftly-linux/home}" REMOTE_SWIFTLY_BIN="${PUREYAML_LINUX_SWIFTLY_BIN:-/Volumes/ClawSSD/swiftly-linux/bin}" +SSH_CONNECT_TIMEOUT="${PUREYAML_LINUX_SSH_TIMEOUT:-10}" -if ! command -v ssh >/dev/null 2>&1; then - echo "linux: ssh is required outside Linux" >&2 - exit 2 +# --- Stage A: connectivity and provisioning (soft; any failure => skip) ------ +# Everything here is relay infrastructure. If it is not available, the local +# Linux check cannot run, but that is not a code failure: skip with 77. + +command -v ssh >/dev/null 2>&1 || skip "ssh not available on this machine" +command -v rsync >/dev/null 2>&1 || skip "rsync not available on this machine" + +if ! ssh -o BatchMode=yes -o ConnectTimeout="$SSH_CONNECT_TIMEOUT" "$HOST" true >/dev/null 2>&1; then + skip "host '$HOST' is unreachable over ssh" fi -if ! command -v rsync >/dev/null 2>&1; then - echo "linux: rsync is required outside Linux" >&2 - exit 2 +if ! ssh "$HOST" "command -v limactl >/dev/null && mkdir -p '$REMOTE_DIR'" >/dev/null 2>&1; then + skip "host '$HOST' lacks limactl or cannot create '$REMOTE_DIR'" fi -ssh "$HOST" "command -v limactl >/dev/null && mkdir -p '$REMOTE_DIR'" -rsync -a --delete \ - --exclude '.build' \ - --exclude '.git' \ - ./ "$HOST:$REMOTE_DIR/" +if ! rsync -a --delete --exclude '.build' --exclude '.git' ./ "$HOST:$REMOTE_DIR/" >/dev/null 2>&1; then + skip "rsync to '$HOST:$REMOTE_DIR' failed" +fi -ssh "$HOST" "limactl start '$LIMA_INSTANCE' >/tmp/pureyaml-lima-start.log 2>&1 || true; limactl shell '$LIMA_INSTANCE' -- bash -lc 'export SWIFTLY_HOME_DIR=\"$REMOTE_SWIFTLY_HOME\" SWIFTLY_BIN_DIR=\"$REMOTE_SWIFTLY_BIN\"; [ -f \"\$SWIFTLY_HOME_DIR/env.sh\" ] && . \"\$SWIFTLY_HOME_DIR/env.sh\"; cd \"$REMOTE_DIR\" && swift --version && swift build && swift test && swift build -c release && swift test -c release'" +if ! ssh "$HOST" "limactl start '$LIMA_INSTANCE' >/tmp/pureyaml-lima-start.log 2>&1"; then + skip "lima instance '$LIMA_INSTANCE' would not start on '$HOST'" +fi + +# --- Stage B: the real Linux build/test (hard; failure => exit 1) ------------ +# The relay is up and the tree is synced. From here a non-zero exit is a real +# Linux build/test failure and must propagate. (A connection drop mid-build +# also lands here; failing hard is the conservative, correct choice.) + +REMOTE_BUILD="export SWIFTLY_HOME_DIR=\"$REMOTE_SWIFTLY_HOME\" SWIFTLY_BIN_DIR=\"$REMOTE_SWIFTLY_BIN\"; [ -f \"\$SWIFTLY_HOME_DIR/env.sh\" ] && . \"\$SWIFTLY_HOME_DIR/env.sh\"; cd \"$REMOTE_DIR\" && swift --version && swift build && swift test && swift build -c release && swift test -c release" + +if ssh "$HOST" "limactl shell '$LIMA_INSTANCE' -- bash -lc '$REMOTE_BUILD'"; then + exit 0 +fi + +echo "linux: relayed Swift build/test FAILED on '$HOST'. Real failure, not skipped." >&2 +exit 1