Browse / github.com/storytold/craft-common-actions / patch
feat(freebsd): cross-compile on the host and run real aarch64 tests in a FreeBSD VM
feat(freebsd): cross-compile on the host and run real aarch64 tests in a FreeBSD VM # ci(freebsd): cross-compile on the host with clang/lld/mold and run the test binaries in a FreeBSD VM > Draft PR body. Maintained alongside the branch; this file is not committed to the repository. ## Context On `main`, `freebsd.yml` runs a FreeBSD VM on a Linux runner and cross-compiles aarch64 artifacts inside it, but the aarch64 leg **skips the test phase entirely**. Compilation succeeds; no aarch64 test binary is ever executed. Real aarch64 incompatibilities (dynamic loading, threading, timing-sensitive UI code) stay hidden, and the release notes still state aarch64 is dropped. This P
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- william-wwh (imported from pull request #94, open)
- Base
be6bbf3ca5a5de466786908b3986163da7c05314- Size
- +1079 −0 in 1 file
- Review
- pending
- Shared
- 2026-10-28
- Id
971292ffca00e1edf756883f
How it went
No other operator has reported using it yet.
Diff
raw · .github/workflows/freebsd-cross.yml
new file mode 100644@@ -0,0 +1,1079 @@+name: "FreeBSD Cross-Compile"++on:+ workflow_call:+ inputs:+ architectures:+ description: 'JSON architecture array; empty selects x86_64 for PRs and both architectures otherwise'+ type: string+ required: false+ default: ""+ craft-fonts-ref:+ description: "The commit ref for storytold/craft-fonts. Defaults to the suite's pin; empty builds without craft-fonts"+ type: string+ required: false+ default: "bfcbf714e45a3ea8e98f88ab2b7971372a576414"+ require-craft-fonts:+ description: "Require the supplied craft-fonts checkout during builds and tests"+ type: boolean+ required: false+ default: false+ freebsd-release:+ description: "FreeBSD OS release version (STABLE inputs are normalized to RELEASE for sysroot matching)"+ type: string+ required: false+ default: "15.1-STABLE"+ cargo-test-extra-args:+ description: "Extra arguments to append to cargo test compilation"+ type: string+ required: false+ default: ""+ cargo-build-extra-args:+ description: "Extra arguments to append to cargo build"+ type: string+ required: false+ default: ""+ skip-tests:+ description: "Skip running tests (e.g. if the VM lacks GPU/oracle dependencies)"+ type: boolean+ required: false+ default: false+ run-package-check:+ description: "Run the fallback packaging logic and verify produced tarball"+ type: boolean+ required: false+ default: false+ custom-test-command:+ description: "Custom test commands to run instead of default cargo test. This completely bypasses the default architecture checks and timeouts."+ type: string+ required: false+ default: ""+ app-name:+ description: "Name of the application for packaging"+ type: string+ required: false+ default: ""+ cold-start:+ description: "Skip the FreeBSD sysroot cache restore, so the run downloads base.txz and fetches the pkg archives again. The sysroot is still saved. Use false to measure the cached path."+ type: boolean+ required: false+ default: false+ fixture-manifests:+ description: "Space-separated Cargo.toml paths for plugin fixtures to pre-build for the target triple"+ type: string+ required: false+ default: ""+ skip-test-binaries-x86-64:+ description: "Space-separated glob patterns of x86_64 test binaries to skip. Applied on the x86_64 leg only, whether or not KVM is available: even an accelerated guest has a single vCPU here, which can break the same timing assumptions QEMU TCG breaks."+ type: string+ required: false+ default: ""+ skip-test-binaries-aarch64:+ description: "Space-separated glob patterns of aarch64 test binaries to skip. Applied on the aarch64 leg only."+ type: string+ required: false+ default: ""+ skip-test-cases-x86-64:+ description: "Space-separated libtest name filters. Each pattern is passed to x86_64 test binaries as --skip <pattern>, so the binary still runs and only the matching cases are skipped. Ignored for harness = false binaries."+ type: string+ required: false+ default: ""+ skip-test-cases-aarch64:+ description: "Space-separated libtest name filters. Each pattern is passed to aarch64 test binaries as --skip <pattern>. Ignored for harness = false binaries."+ type: string+ required: false+ default: ""+ stub-cargo:+ description: "Install a stub cargo on PATH inside the VM for tests that shell out to cargo"+ type: boolean+ required: false+ default: false+ test-timeout-seconds:+ description: "Timeout in seconds for each test binary. Default 1800s: the value is tuned for timeout profiling across the craft suite, so a binary that exceeds it is a finding, not an accident."+ type: number+ required: false+ default: 1800+ only-test-binaries:+ description: "Space-separated glob patterns. When set, only matching test binaries are packed and run; the rest are not transferred. Matched against the recorded path, e.g. 'target/*/debug/deps/clap_e2e-*'."+ type: string+ required: false+ default: ""+ only-test-cases:+ description: "Space-separated libtest name filters (substring match). When set, only matching cases run inside the selected binaries."+ type: string+ required: false+ default: ""++permissions:+ contents: read++jobs:+ validate-architectures:+ runs-on: ubuntu-24.04+ outputs:+ arch_matrix: ${{ steps.compute.outputs.arch_matrix }}+ steps:+ - name: Validate and normalize architectures+ id: compute+ shell: bash+ env:+ RAW_ARCHITECTURES: ${{ inputs.architectures }}+ EVENT_NAME: ${{ github.event_name }}+ run: |+ set -euo pipefail+ raw="$RAW_ARCHITECTURES"+ if [ -z "$raw" ]; then+ if [ "$EVENT_NAME" = "pull_request" ] || [ "$EVENT_NAME" = "pull_request_target" ]; then+ archs='["x86_64"]'+ else+ archs='["x86_64","aarch64"]'+ fi+ else+ archs="$raw"+ fi+ + if ! printf '%s' "$archs" | jq -e 'type == "array" and length > 0' >/dev/null; then+ echo "Error: architectures must be a non-empty JSON array of strings." >&2+ exit 1+ fi+ + if ! printf '%s' "$archs" | jq -e 'all(.[]; . == "x86_64" or . == "aarch64")' >/dev/null; then+ echo "Error: architectures must only contain \"x86_64\" or \"aarch64\"." >&2+ exit 1+ fi+ + archs=$(printf '%s' "$archs" | jq -c .)+ echo "arch_matrix=$archs" >> "$GITHUB_OUTPUT"++ freebsd:+ name: FreeBSD ${{ matrix.arch }} (build + test)+ needs: [validate-architectures]+ runs-on: ubuntu-24.04+ timeout-minutes: 300+ strategy:+ fail-fast: false+ matrix:+ arch: ${{ fromJSON(needs.validate-architectures.outputs.arch_matrix) }}+ env:+ MATRIX_ARCH: ${{ matrix.arch }}+ CARGO_BUILD_EXTRA_ARGS: ${{ inputs.cargo-build-extra-args }}+ CARGO_TEST_EXTRA_ARGS: ${{ inputs.cargo-test-extra-args }}+ CUSTOM_TEST_COMMAND: ${{ inputs.custom-test-command }}+ FREEBSD_RELEASE: ${{ inputs.freebsd-release }}+ REQUIRE_CRAFT_FONTS: ${{ inputs.require-craft-fonts }}+ RUN_PACKAGE_CHECK: ${{ inputs.run-package-check }}+ APP_NAME: ${{ inputs.app-name }}+ FIXTURE_MANIFESTS: ${{ inputs.fixture-manifests }}+ SKIP_TEST_BINARIES_X86_64: ${{ inputs.skip-test-binaries-x86-64 }}+ SKIP_TEST_BINARIES_AARCH64: ${{ inputs.skip-test-binaries-aarch64 }}+ SKIP_TEST_CASES_X86_64: ${{ inputs.skip-test-cases-x86-64 }}+ SKIP_TEST_CASES_AARCH64: ${{ inputs.skip-test-cases-aarch64 }}+ STUB_CARGO: ${{ inputs.stub-cargo }}+ TEST_TIMEOUT_SECONDS: ${{ inputs.test-timeout-seconds }}+ ONLY_TEST_BINARIES: ${{ inputs.only-test-binaries }}+ ONLY_TEST_CASES: ${{ inputs.only-test-cases }}+ steps:+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1++ - name: Pre-install QEMU dependencies+ run: |+ sudo apt-get update+ if [ "$MATRIX_ARCH" = "x86_64" ]; then+ echo "Installing x86_64 QEMU dependencies..."+ sudo apt-get install -y --no-install-recommends \+ qemu-system-x86 qemu-utils ovmf zstd+ else+ echo "Installing aarch64 QEMU dependencies..."+ sudo apt-get install -y --no-install-recommends \+ qemu-system-arm qemu-efi-aarch64 qemu-utils zstd+ fi++ - name: Check out craft-fonts+ if: inputs.craft-fonts-ref != ''+ uses: storytold/craft-common-actions/actions/checkout-craft-fonts@main+ with:+ ref: ${{ inputs.craft-fonts-ref }}+ - name: Check out craft-i18n+ uses: storytold/craft-common-actions/actions/checkout-craft-i18n@main+ + - name: Resolve FreeBSD release+ id: freebsd-release+ shell: bash+ run: |+ set -euo pipefail+ release="${FREEBSD_RELEASE}"+ release="${release%-STABLE}"+ release="${release%-RELEASE}"+ + if [[ ! "$release" =~ ^[0-9]+\.[0-9]+$ ]]; then+ echo "Error: Invalid FreeBSD release format: $FREEBSD_RELEASE" >&2+ exit 1+ fi+ + echo "release=${release}" >> "$GITHUB_OUTPUT"+ major="${release%%.*}"+ echo "major=${major}" >> "$GITHUB_OUTPUT"++ # Runs before the sysroot so the cache key can encode whether the third-party+ # libraries belong to this repository's dependency tree at all.+ - name: Detect required extra libs+ id: detect-libs+ run: |+ if grep -qiE 'name = "(alsa|alsa-sys|libpulse|libpulse-sys|libpulse-binding|wayland|wayland-client|wayland-sys|x11|x11-sys|libxcb)"' Cargo.lock; then+ echo "needs_extra_libs=true" >> $GITHUB_OUTPUT+ else+ echo "needs_extra_libs=false" >> $GITHUB_OUTPUT+ fi++ - name: Set sysroot path+ run: echo "SYSROOT=$HOME/freebsd-sysroot" >> "$GITHUB_ENV"++ # The sysroot comes from the official FreeBSD release image and the official+ # pkg archives. Neither depends on this repository, so the key carries no+ # Cargo.lock hash and survives a version bump, unlike the Cargo cache. A+ # cold-start run skips the restore, because measuring the uncached path is+ # the whole point of that run.+ # The sysroot also holds packages from the pkg repository's latest branch,+ # which is rebuilt without changing the FreeBSD release. meta.conf is+ # static (version, packing format, file names), so its content never+ # moves. packagesite.pkg is the catalogue itself and is rewritten on every+ # repository update; its ETag identifies that state, so it joins the cache+ # key. Only the header is fetched: the catalogue is close to 10 MB.+ - name: Resolve the pkg catalogue identity+ id: pkg-epoch+ env:+ MAJOR_VER: ${{ steps.freebsd-release.outputs.major }}+ run: |+ set -eu+ if [ "$MATRIX_ARCH" = "x86_64" ]; then abi="amd64"; else abi="aarch64"; fi+ url="https://pkg.FreeBSD.org/FreeBSD:${MAJOR_VER}:${abi}/latest/packagesite.pkg"+ etag=$(curl -fsSI --retry 3 --connect-timeout 20 --max-time 60 "$url" \+ | awk -F": " 'tolower($1) == "etag" { gsub(/"/, "", $2); gsub(/\r/, "", $2); print $2; exit }')+ if [ -z "$etag" ]; then+ echo "Error: could not read the pkg catalogue ETag from $url" >&2+ exit 1+ fi+ echo "epoch=${etag}" >> "$GITHUB_OUTPUT"++ - name: Restore FreeBSD sysroot+ id: sysroot-cache+ if: inputs.cold-start != true+ uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v4.2.3+ with:+ path: ~/freebsd-sysroot+ key: v2-sysroot-${{ matrix.arch }}-${{ steps.freebsd-release.outputs.release }}-${{ steps.detect-libs.outputs.needs_extra_libs }}-${{ steps.pkg-epoch.outputs.epoch }}++ - name: Extract FreeBSD Sysroot+ if: steps.sysroot-cache.outputs.cache-hit != 'true'+ env:+ RELEASE_VER: ${{ steps.freebsd-release.outputs.release }}+ run: |+ set -eu+ if [ "$MATRIX_ARCH" = "x86_64" ]; then+ url="https://download.freebsd.org/releases/amd64/${RELEASE_VER}-RELEASE/base.txz"+ else+ url="https://download.freebsd.org/releases/arm64/aarch64/${RELEASE_VER}-RELEASE/base.txz"+ fi+ echo "Downloading sysroot from: $url"+ + mkdir -p "$SYSROOT"+ + curl -fL --retry 3 --connect-timeout 30 --max-time 600 -o base.txz "$url"+ curl -fL --retry 3 --connect-timeout 30 --max-time 60 -o MANIFEST "${url%/*}/MANIFEST"+ + expected=$(awk '$1=="base.txz"{print $2}' MANIFEST)+ if [ -z "$expected" ]; then+ echo "Error: base.txz checksum not found in MANIFEST" >&2+ exit 1+ fi+ echo "$expected base.txz" | sha256sum -c -+ rm -f MANIFEST+ + tar -xf base.txz -C "$SYSROOT" ./lib/ ./usr/lib/ ./usr/include/ ./libexec/+ rm -f base.txz+ + ln -sf ../usr/lib/libcxxrt.so.1 "$SYSROOT/lib/libcxxrt.so.1" 2>/dev/null || true+ ln -sf ../usr/lib/libc++.so.1 "$SYSROOT/lib/libc++.so.1" 2>/dev/null || true++ - name: Free Disk Space+ uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1+ with:+ tool-cache: false+ android: true+ dotnet: true+ haskell: true+ large-packages: false+ docker-images: false+ swap-storage: true++ - name: Fetch FreeBSD Third-party Packages via VM+ if: steps.sysroot-cache.outputs.cache-hit != 'true' && steps.detect-libs.outputs.needs_extra_libs == 'true'+ uses: vmactions/freebsd-vm@c46abacb49f09938ca4e1702d15d836285d694cc # v1.5.9+ env:+ MAJOR_VER: ${{ steps.freebsd-release.outputs.major }}+ with:+ release: ${{ steps.freebsd-release.outputs.release }}+ usesh: true+ copyback: true+ envs: 'MATRIX_ARCH MAJOR_VER'+ run: |+ set -eu+ if [ "$MATRIX_ARCH" = "x86_64" ]; then+ ABI="FreeBSD:${MAJOR_VER}:amd64"+ else+ ABI="FreeBSD:${MAJOR_VER}:aarch64"+ fi+ OSVERSION=$(sysctl -n kern.osreldate)+ env ABI=$ABI OSVERSION=$OSVERSION pkg update -f+ env ABI=$ABI OSVERSION=$OSVERSION pkg fetch -y -d alsa-lib wayland libX11 libXcursor libXrandr libXi libxcb mesa-libs vulkan-loader gtk3 fontconfig freetype2 || { echo 'Error: pkg fetch failed'; exit 1; }+ mkdir -p packages+ find /var/cache/pkg -maxdepth 1 -type f -name '*.pkg' -exec cp {} packages/ \;++ - name: Clean up leftover VM from fetch step+ if: always() && steps.sysroot-cache.outputs.cache-hit != 'true' && steps.detect-libs.outputs.needs_extra_libs == 'true'+ run: |+ pkill -f 'qemu-system-x86_64.*freebsd' || true+ pkill -f 'qemu-system-aarch64.*freebsd' || true+ sleep 2+ pgrep -af qemu-system || true++ - name: Extract Packages into Sysroot+ if: steps.sysroot-cache.outputs.cache-hit != 'true' && steps.detect-libs.outputs.needs_extra_libs == 'true'+ run: |+ set -eu+ if [ ! -d packages ] || [ "$(find packages -name '*.pkg' | wc -l)" -eq 0 ]; then+ echo "Error: packages directory is empty, but extra libs are required." >&2+ exit 1+ fi+ + for pkg in packages/*.pkg; do+ echo "Extracting $pkg"+ if tar -tf "$pkg" | grep -qx 'data.tar'; then+ tar -xf "$pkg" -O data.tar | tar -xf - -C "$SYSROOT"+ else+ tar -xf "$pkg" -C "$SYSROOT" --exclude '+*'+ fi+ done+ + echo "Verifying sysroot contents:"+ ls -la "$SYSROOT/usr/local/lib" 2>/dev/null || echo "Warning: usr/local/lib not populated"+ ls -la "$SYSROOT/usr/local/libdata/pkgconfig" 2>/dev/null || echo "Warning: pkgconfig dir not populated"+ rm -rf packages++ - name: Save FreeBSD sysroot+ # Only on a miss; re-saving an existing key fails for no benefit.+ if: steps.sysroot-cache.outputs.cache-hit != 'true'+ uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v4.2.3+ with:+ path: ~/freebsd-sysroot+ key: v2-sysroot-${{ matrix.arch }}-${{ steps.freebsd-release.outputs.release }}-${{ steps.detect-libs.outputs.needs_extra_libs }}-${{ steps.pkg-epoch.outputs.epoch }}++ - name: Setup Toolchain+ run: |+ sudo apt-get update+ sudo apt-get install -y --no-install-recommends clang lld pkg-config mold+ if [ "$MATRIX_ARCH" = "aarch64" ]; then+ rustup target add aarch64-unknown-freebsd+ else+ rustup target add x86_64-unknown-freebsd+ fi++ # No target/ cache. Its key would carry the Cargo.lock hash, which changes on+ # every version bump, and Cargo invalidates the fingerprints anyway once the+ # workspace version moves. For a release-driven workflow that entry would never+ # hit, so the compile always starts from an empty target/.+ - name: Cross Compile Workspace+ run: |+ set -eu+ TARGET_TRIPLE="${MATRIX_ARCH}-unknown-freebsd"+ + export PKG_CONFIG_ALLOW_CROSS=1+ export PKG_CONFIG_SYSROOT_DIR="$SYSROOT"+ export PKG_CONFIG_LIBDIR="$SYSROOT/usr/local/libdata/pkgconfig:$SYSROOT/usr/libdata/pkgconfig:$SYSROOT/usr/lib/pkgconfig"+ export PKG_CONFIG_PATH="$SYSROOT/usr/local/libdata/pkgconfig:$SYSROOT/usr/libdata/pkgconfig:$SYSROOT/usr/lib/pkgconfig"+ + target_key="${TARGET_TRIPLE//-/_}"+ target_key_upper="${target_key^^}"+ + printf -v "CC_${target_key}" '%s' "clang"+ printf -v "CXX_${target_key}" '%s' "clang++"+ printf -v "CFLAGS_${target_key}" '%s' "--target=$TARGET_TRIPLE --sysroot=$SYSROOT"+ printf -v "CXXFLAGS_${target_key}" '%s' "--target=$TARGET_TRIPLE --sysroot=$SYSROOT"+ export "CC_${target_key}" "CXX_${target_key}" "CFLAGS_${target_key}" "CXXFLAGS_${target_key}"+ + printf -v "CARGO_TARGET_${target_key_upper}_LINKER" '%s' "clang"+ printf -v "CARGO_TARGET_${target_key_upper}_RUSTFLAGS" '%s' "-Clink-arg=--target=$TARGET_TRIPLE -Clink-arg=--sysroot=$SYSROOT -Clink-arg=-fuse-ld=mold -Clink-arg=-L$SYSROOT/lib -Clink-arg=-L$SYSROOT/usr/lib -Clink-arg=-L$SYSROOT/usr/local/lib"+ export "CARGO_TARGET_${target_key_upper}_LINKER" "CARGO_TARGET_${target_key_upper}_RUSTFLAGS"+ + export CARGO_PROFILE_DEV_DEBUG=0+ export CARGO_PROFILE_TEST_DEBUG=0+ export CARGO_BUILD_JOBS=3+ + if [ -d craft-fonts ]; then+ export CRAFT_FONTS_DIR="$PWD/craft-fonts"+ fi+ if [ "$REQUIRE_CRAFT_FONTS" = "true" ]; then+ if [ ! -d craft-fonts ]; then+ echo "error: required craft-fonts checkout is missing" >&2+ exit 1+ fi+ export CRAFT_FONTS_REQUIRED=1+ fi+ + if [ "$RUN_PACKAGE_CHECK" = "true" ]; then+ # shellcheck disable=SC2086+ cargo build --release --workspace --target=$TARGET_TRIPLE --locked $CARGO_BUILD_EXTRA_ARGS+ fi+ + # shellcheck disable=SC2086+ # json-render-diagnostics keeps the JSON in test-output.json for the jq filter below,+ # while the rendered diagnostics go to stderr and therefore into the job log. With+ # plain --message-format=json the errors exist only inside test-output.json, which+ # this script deletes, so a failed build left no compiler message behind.+ if ! cargo test --workspace --target=$TARGET_TRIPLE --locked --no-run --message-format=json-render-diagnostics $CARGO_TEST_EXTRA_ARGS > test-output.json; then+ echo '=== compiler diagnostics ===' >&2+ jq -r 'select(.reason=="compiler-message" and .message.level=="error") | .message.rendered' test-output.json 2>/dev/null >&2 || true+ exit 1+ fi++ if [ -n "${FIXTURE_MANIFESTS:-}" ]; then+ for manifest in $FIXTURE_MANIFESTS; do+ [ -f "$manifest" ] || { echo "Error: fixture manifest not found: $manifest" >&2; exit 1; }+ fixture_name=$(basename "$(dirname "$(dirname "$(dirname "$manifest")")")" | sed 's/-host$//')+ echo "Pre-building fixture: $manifest"+ FIXTURE_TMPDIR="$PWD/target/$TARGET_TRIPLE/tmp/${fixture_name}-fixture-target"+ mkdir -p "$FIXTURE_TMPDIR"+ fixture_locked=""+ if [ -f "$(dirname "$manifest")/Cargo.lock" ]; then+ fixture_locked="--locked"+ fi+ # shellcheck disable=SC2086+ cargo build --manifest-path "$manifest" --target=$TARGET_TRIPLE --target-dir "$FIXTURE_TMPDIR" $fixture_locked+ for so in "$FIXTURE_TMPDIR/$TARGET_TRIPLE/debug"/*.so; do+ [ -f "$so" ] || continue+ mkdir -p "$FIXTURE_TMPDIR/debug"+ cp "$so" "$FIXTURE_TMPDIR/debug/"+ done+ done+ fi+ + jq -r --arg root "$PWD" 'select(.reason=="compiler-artifact" and .executable!=null and .profile.test==true) + | "\(.manifest_path | sub("/Cargo.toml$";"") | ltrimstr($root) | ltrimstr("/") | if .=="" then "." else . end)\t\(.executable | ltrimstr($root+"/"))"' \+ test-output.json > test-binaries.txt++ if [ -n "${ONLY_TEST_BINARIES:-}" ]; then+ tab=$(printf '\t')+ : > test-binaries.selected+ set -f+ while IFS="$tab" read -r crate_dir test_bin; do+ [ -n "$test_bin" ] || continue+ for pattern in $ONLY_TEST_BINARIES; do+ case "$test_bin" in+ $pattern) printf '%s\t%s\n' "$crate_dir" "$test_bin" >> test-binaries.selected; break ;;+ esac+ done+ done < test-binaries.txt+ set +f+ mv test-binaries.selected test-binaries.txt+ if [ ! -s test-binaries.txt ]; then+ echo "Error: only-test-binaries matched no test binary: $ONLY_TEST_BINARIES" >&2+ exit 1+ fi+ echo "Selected $(wc -l < test-binaries.txt) test binaries."+ fi++ - name: Package+ if: env.RUN_PACKAGE_CHECK == 'true'+ run: |+ set -eu+ if [ -z "$APP_NAME" ]; then+ echo "error: app-name is required for packaging fallback" >&2+ exit 1+ fi+ + VERSION=$(grep -m 1 '^version' Cargo.toml | awk -F'"' '{print $2}' || true)+ if [ -z "$VERSION" ]; then+ echo "error: version not found in Cargo.toml" >&2+ exit 1+ fi+ + NAME="${APP_NAME}-${VERSION}-freebsd-${MATRIX_ARCH}"+ mkdir -p "dist/release/$NAME"+ mkdir -p "dist/release/$NAME/icons"+ + found=$(find target/${MATRIX_ARCH}-unknown-freebsd/release -maxdepth 1 -type f -executable -name "${APP_NAME}*" | head -n 1)+ if [ -z "$found" ]; then+ echo "error: no executable matching ${APP_NAME}* found in release target" >&2+ exit 1+ fi+ + find target/${MATRIX_ARCH}-unknown-freebsd/release -maxdepth 1 -type f -executable -name "${APP_NAME}*" -exec cp {} "dist/release/$NAME/" \;+ + if [ -f "README.md" ]; then cp README.md "dist/release/$NAME/"; fi+ if [ -f "LICENSE-MIT" ]; then cp LICENSE-MIT "dist/release/$NAME/"; fi+ if [ -f "LICENSE-APACHE" ]; then cp LICENSE-APACHE "dist/release/$NAME/"; fi+ if [ -f "NOTICE" ]; then cp NOTICE "dist/release/$NAME/"; fi+ if [ -f "ATTRIBUTION.md" ]; then cp ATTRIBUTION.md "dist/release/$NAME/"; fi+ if [ -d "assets/app-icon/hicolor" ]; then cp -R assets/app-icon/hicolor "dist/release/$NAME/icons/"; fi+ + tar -C dist/release -czf "dist/release/$NAME.tar.gz" "$NAME"+ rm -rf "dist/release/$NAME"+ + echo "Verifying archive contents..."+ tar -tzf "dist/release/$NAME.tar.gz" > "dist/release/$NAME-files.txt"+ + if ! grep -qxF "$NAME/$APP_NAME" "dist/release/$NAME-files.txt"; then+ echo "Error: packaged archive does not contain the main binary." >&2+ exit 1+ fi+ if [ -f "README.md" ] && ! grep -qxF "${NAME}/README.md" "dist/release/$NAME-files.txt"; then+ echo "Error: packaged archive does not contain README.md." >&2+ exit 1+ fi+ echo "Archive contents verified:"+ head -n 10 "dist/release/$NAME-files.txt"++ - name: Upload the packaged FreeBSD archive+ if: always() && env.RUN_PACKAGE_CHECK == 'true'+ uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2+ with:+ name: freebsd-${{ matrix.arch }}-package+ path: |+ dist/release/*.tar.gz+ dist/release/*-files.txt+ if-no-files-found: error+ retention-days: 7++ - name: Write test environment+ run: |+ set -eu+ echo "#!/bin/sh" > test-env.sh+ if [ -d craft-fonts ]; then+ echo "export CRAFT_FONTS_DIR=\"\$PWD/craft-fonts\"" >> test-env.sh+ fi+ if [ "$REQUIRE_CRAFT_FONTS" = "true" ]; then+ echo "export CRAFT_FONTS_REQUIRED=1" >> test-env.sh+ fi++ - name: Write custom test command+ if: inputs.custom-test-command != ''+ run: |+ printf '%s\n' "$CUSTOM_TEST_COMMAND" > custom-test-command.sh++ - name: Pack workspace+ if: inputs.skip-tests != true+ run: |+ set -eu+ TARGET_TRIPLE="${MATRIX_ARCH}-unknown-freebsd"+ + if [ -f test-binaries.txt ] && [ -s test-binaries.txt ]; then+ tab=$(printf '\t')+ while IFS= read -r line || [ -n "$line" ]; do+ [ -z "$line" ] && continue+ case "$line" in+ *"$tab"*) ;;+ *) echo "Error: missing TAB separator: $line" >&2; exit 1 ;;+ esac+ crate_dir=${line%%"$tab"*}+ test_bin=${line#*"$tab"}+ case "$crate_dir:$test_bin" in+ :*|*:) echo "Error: empty field: $line" >&2; exit 1 ;;+ esac+ case "$test_bin" in+ *"$tab"*) echo "Error: too many fields: $line" >&2; exit 1 ;;+ esac+ + case "$test_bin" in+ target/*) ;;+ *) echo "Error: test binary must be under target/: $test_bin" >&2; exit 1 ;;+ esac+ case "/$test_bin/" in+ */../*|*//*) echo "Error: unsafe test binary path: $test_bin" >&2; exit 1 ;;+ esac+ case "$crate_dir" in+ .) ;;+ *) + case "$crate_dir" in+ /*|../*|*/../*|*/..|*//*|./*)+ echo "Error: unsafe crate_dir path: $crate_dir" >&2+ exit 1+ ;;+ esac+ ;;+ esac+ done < test-binaries.txt+ elif [ -z "${CUSTOM_TEST_COMMAND:-}" ]; then+ echo "Error: test-binaries.txt is empty and no custom test command is provided." >&2+ exit 1+ fi+ + mkdir -p "target-slim/target/$TARGET_TRIPLE/debug/deps" "target-slim/target/$TARGET_TRIPLE/tmp"+ + if [ -f test-binaries.txt ] && [ -s test-binaries.txt ]; then+ grep -Ev '^[[:space:]]*$' test-binaries.txt > test-binaries-clean.txt+ while IFS=$'\t' read -r crate_dir test_bin; do+ [ -z "$test_bin" ] && continue+ mkdir -p "target-slim/$(dirname "$test_bin")"+ cp "$test_bin" "target-slim/$test_bin"+ done < test-binaries-clean.txt+ mv test-binaries-clean.txt test-binaries.txt+ fi++ # Copy workspace binaries that tests reference via CARGO_BIN_EXE_<name>.+ # env!("CARGO_BIN_EXE_<name>") bakes an absolute path into the test binary,+ # so the file must exist at the same relative location inside the VM.+ if [ -d "target/$TARGET_TRIPLE/debug" ]; then+ while IFS= read -r bin; do+ [ -n "$bin" ] || continue+ base=$(basename "$bin")+ case "$base" in+ *.so|*.d|*.rlib|*.rmeta|*.dylib|*.pdb|*.o) continue ;;+ esac+ mkdir -p "target-slim/target/$TARGET_TRIPLE/debug"+ cp "$bin" "target-slim/target/$TARGET_TRIPLE/debug/$base"+ done < <(find "target/$TARGET_TRIPLE/debug" -maxdepth 1 -type f -executable)+ fi++ if [ -d "target/$TARGET_TRIPLE/tmp/clap-fixture-target" ]; then+ cp -r "target/$TARGET_TRIPLE/tmp/clap-fixture-target" "target-slim/target/$TARGET_TRIPLE/tmp/"+ fi+ if [ -d "target/$TARGET_TRIPLE/tmp/vst3-fixture-target" ]; then+ cp -r "target/$TARGET_TRIPLE/tmp/vst3-fixture-target" "target-slim/target/$TARGET_TRIPLE/tmp/"+ fi++ if [ -f test-binaries.txt ] && [ -s test-binaries.txt ]; then+ while IFS=$'\t' read -r crate_dir test_bin; do+ hash=$(sha256sum "$test_bin" | awk '{print $1}')+ printf '%s\t%s\n' "$hash" "$test_bin"+ done < test-binaries.txt > test-binaries.sha256+ fi+ + tar --exclude='./target' --exclude='./target-slim' --exclude='./.git' --exclude='./payload.tar' --exclude='./test-output.json' --exclude='./dist' -cf payload.tar .+ tar -rf payload.tar -C target-slim .+ + tar -tf payload.tar > payload-files.txt+ if [ -f test-binaries.txt ]; then+ grep -Fx './test-binaries.txt' payload-files.txt+ fi+ if [ -f test-binaries.sha256 ]; then+ grep -Fx './test-binaries.sha256' payload-files.txt+ fi+ + if [ -f "target/$TARGET_TRIPLE/tmp/clap-fixture-target/debug/libclap_fixture.so" ]; then+ grep -Fxq "./target/$TARGET_TRIPLE/tmp/clap-fixture-target/debug/libclap_fixture.so" payload-files.txt || { + echo "Error: clap fixture .so missing from archive" >&2+ exit 1+ }+ fi+ if [ -f "target/$TARGET_TRIPLE/tmp/vst3-fixture-target/debug/libvst3_fixture.so" ]; then+ grep -Fxq "./target/$TARGET_TRIPLE/tmp/vst3-fixture-target/debug/libvst3_fixture.so" payload-files.txt || { + echo "Error: vst3 fixture .so missing from archive" >&2+ exit 1+ }+ fi++ if [ -f test-binaries.txt ] && [ -s test-binaries.txt ]; then+ while IFS=$'\t' read -r crate_dir test_bin; do+ [ -n "$test_bin" ] || continue+ if ! grep -Fxq "./$test_bin" payload-files.txt; then+ echo "Error: missing test binary in archive: $test_bin" >&2+ exit 1+ fi+ done < test-binaries.txt+ fi+ rm -f payload-files.txt+ + zstd -T0 payload.tar+ rm -f payload.tar+ + # Split with fixed-width numeric suffixes and record both the exact+ # part order and a per-part checksum. The guest cannot trust a glob+ # order or assume every part survived the transfer: a missing or+ # rewritten part used to surface only as "Restored data doesn't match+ # checksum" after the parts were already concatenated.+ split -b 500M --numeric-suffixes=0 --suffix-length=4 payload.tar.zst payload.tar.zst.part.+ rm -f payload.tar.zst+ ls -1v payload.tar.zst.part.* > payload.parts.list+ while IFS= read -r part; do+ [ -n "$part" ] || continue+ printf '%s\t%s\n' "$(sha256sum "$part" | awk '{print $1}')" "$part"+ done < payload.parts.list > payload.parts.sha256+ echo "Packaged $(wc -l < payload.parts.list) payload parts:"+ cat payload.parts.list+ rm -rf target target-slim test-output.json++ - name: Run Tests in FreeBSD VM+ if: inputs.skip-tests != true+ uses: vmactions/freebsd-vm@c46abacb49f09938ca4e1702d15d836285d694cc # v1.5.9+ env:+ # Passed as data: no input expression is ever pasted into shell source.+ INSTALL_RUST: ${{ inputs.custom-test-command != '' }}+ with:+ release: ${{ steps.freebsd-release.outputs.release }}+ arch: ${{ matrix.arch }}+ usesh: true+ copyback: false+ # Transport history on this payload:+ # rsync (default) aborts multi-GB payloads with "Invalid block index+ # (count=0)" in receiver.c with rsync 3.5.0.+ # tar rewrote the first 500 MB part on payloads of three parts or+ # more (per-part SHA-256 mismatch on part.0000 while one- and+ # two-part payloads passed).+ # scp copies each part as an opaque file, which is the granularity+ # the checksum list already verifies.+ sync: scp+ envs: 'MATRIX_ARCH REQUIRE_CRAFT_FONTS STUB_CARGO SKIP_TEST_BINARIES_X86_64 SKIP_TEST_BINARIES_AARCH64 SKIP_TEST_CASES_X86_64 SKIP_TEST_CASES_AARCH64 TEST_TIMEOUT_SECONDS ONLY_TEST_CASES INSTALL_RUST'+ prepare: |+ pkg update -f+ pkg install -y bash file git curl pkgconf cmake python3 libxkbcommon wayland libX11 libXcursor libXrandr libXi libxcb mesa-libs vulkan-loader gtk3 fontconfig freetype2 alsa-lib dejavu zstd+ # A custom command list may call cargo, so it needs the guest toolchain.+ if [ "$INSTALL_RUST" = "true" ]; then pkg install -y rust; fi+ run: |+ set -eu+ + tab=$(printf '\t')+ + if ls payload.tar.zst.part.* >/dev/null 2>&1; then+ echo "Verifying chunked archive..."+ if [ ! -f payload.parts.list ] || [ ! -f payload.parts.sha256 ]; then+ echo "Error: payload parts exist but payload.parts.list or payload.parts.sha256 is missing" >&2+ exit 1+ fi++ expected_parts=$(grep -cve '^[[:space:]]*$' payload.parts.list || true)+ actual_parts=$(ls -1 payload.tar.zst.part.* 2>/dev/null | wc -l | tr -d ' ')+ if [ "$expected_parts" != "$actual_parts" ]; then+ echo "Error: expected $expected_parts payload parts, found $actual_parts" >&2+ ls -l payload.tar.zst.part.* >&2 || true+ exit 1+ fi++ # Verify every part before any of them is concatenated, so a+ # corrupt part is named instead of failing later as a zstd+ # checksum error on the merged stream.+ while IFS="$tab" read -r expected part; do+ [ -n "$part" ] || continue+ if [ ! -f "$part" ]; then+ echo "Error: payload part missing: $part" >&2+ exit 1+ fi+ actual=$(sha256 -q "$part")+ if [ "$actual" != "$expected" ]; then+ echo "Error: payload part checksum mismatch: $part" >&2+ echo " expected $expected" >&2+ echo " actual $actual" >&2+ exit 1+ fi+ done < payload.parts.sha256+ echo "All $expected_parts payload parts verified."++ # Stream the parts through zstd and tar so neither the merged+ # compressed stream nor the uncompressed tar ever lands on disk.+ # FreeBSD /bin/sh has no pipefail, so the three stages run as+ # separate processes joined by FIFOs and each exit code is+ # collected individually.+ extract_dir=$(mktemp -d "${TMPDIR:-/tmp}/payload-extract.XXXXXX")+ parts_pid=+ zstd_pid=+ tar_pid=+ cleanup_extract() {+ for pid in "$parts_pid" "$zstd_pid" "$tar_pid"; do+ if [ -n "$pid" ]; then kill "$pid" 2>/dev/null || :; fi+ done+ rm -rf "$extract_dir"+ }+ trap cleanup_extract EXIT+ trap 'exit 130' INT+ trap 'exit 143' TERM++ mkfifo "$extract_dir/compressed.fifo" "$extract_dir/raw.fifo"++ tar -xf - < "$extract_dir/raw.fifo" > "$extract_dir/tar.log" 2>&1 &+ tar_pid=$!++ zstd -d -c < "$extract_dir/compressed.fifo" > "$extract_dir/raw.fifo" 2> "$extract_dir/zstd.log" &+ zstd_pid=$!++ (+ while IFS= read -r part; do+ [ -n "$part" ] || continue+ cat "$part" || exit 21+ done < payload.parts.list+ ) > "$extract_dir/compressed.fifo" 2> "$extract_dir/parts.log" &+ parts_pid=$!++ set +e+ wait "$parts_pid"; parts_rc=$?; parts_pid=+ wait "$zstd_pid"; zstd_rc=$?; zstd_pid=+ wait "$tar_pid"; tar_rc=$?; tar_pid=+ set -e++ if [ "$parts_rc" -ne 0 ] || [ "$zstd_rc" -ne 0 ] || [ "$tar_rc" -ne 0 ]; then+ echo "Error: streaming payload extraction failed." >&2+ echo "Part reader exit code: $parts_rc" >&2+ echo "Zstd exit code: $zstd_rc" >&2+ echo "Tar exit code: $tar_rc" >&2+ for stage in parts zstd tar; do+ if [ -s "$extract_dir/$stage.log" ]; then+ echo "===== $stage log =====" >&2+ cat "$extract_dir/$stage.log" >&2+ fi+ done+ echo "Payload parts are kept until the VM exits for diagnosis." >&2+ exit 1+ fi++ # Only after a fully successful extraction are the parts removed.+ while IFS= read -r part; do+ [ -n "$part" ] || continue+ rm -f "$part"+ done < payload.parts.list+ rm -f payload.parts.list payload.parts.sha256+ elif [ -f payload.tar.zst ]; then+ echo "Verifying and extracting archive..."+ zstd -t payload.tar.zst+ zstd -d -c payload.tar.zst > payload.tar+ rm -f payload.tar.zst+ tar -xf payload.tar+ rm -f payload.tar+ else+ echo "Error: payload archive not found" >&2+ exit 1+ fi+ + echo "Disk space after extraction:"+ df -h+ + if [ -f test-binaries.sha256 ]; then+ echo "Verifying test binary checksums..."+ while IFS="$tab" read -r expected test_bin; do+ [ -n "$test_bin" ] || continue+ actual=$(sha256 -q "$test_bin")+ if [ "$actual" != "$expected" ]; then+ echo "Error: Checksum mismatch for $test_bin" >&2+ exit 1+ fi+ done < test-binaries.sha256+ echo "All checksums verified."+ fi+ + if [ -f test-env.sh ]; then+ . ./test-env.sh+ fi++ # Tests run as an ordinary user: as root a test that expects "permission+ # denied" fails, and a permission bug root would hide goes unseen.+ if [ "$(id -u)" -eq 0 ]; then+ id craft-test >/dev/null 2>&1 || pw useradd craft-test -m -s /bin/sh+ # The extracted payload, including the test binaries and fixtures.+ chown -R craft-test "$(pwd)"+ # A writable cache directory, since HOME is redirected under it.+ install -d -o craft-test -g craft-test /home/craft-test/.cache+ fi+ + if [ -f custom-test-command.sh ]; then+ if [ "$(id -u)" -eq 0 ]; then+ # -m keeps the caller's environment, which includes HOME=/root.+ # /root is 0700, so the test user could not write anywhere the+ # suite expects to. HOME and XDG_CACHE_HOME are set explicitly.+ su -m craft-test -c "HOME=/home/craft-test XDG_CACHE_HOME=/home/craft-test/.cache PATH='$PATH' && export HOME XDG_CACHE_HOME PATH && sh -eu custom-test-command.sh"+ else+ sh -eu custom-test-command.sh+ fi+ else+ if [ ! -f test-binaries.txt ]; then+ echo "Error: test-binaries.txt not found" >&2+ exit 1+ fi+ + case "$MATRIX_ARCH" in+ aarch64) arch_pattern="aarch64" ;;+ x86_64) arch_pattern="x86-64" ;;+ *) echo "Unsupported architecture: $MATRIX_ARCH" >&2; exit 1 ;;+ esac+ + if ! command -v timeout >/dev/null 2>&1; then+ echo "Error: timeout command not found" >&2+ exit 1+ fi+ + checked=0+ attempted=0+ passed=0+ skipped=0+ failed=""+ root=$(pwd)+ : > excluded-tests.txt+ : > timed-out-tests.txt+ + if [ "${STUB_CARGO:-false}" = "true" ]; then+ printf '#!/bin/sh\necho "stub cargo called with: $*" >&2\nexit 0\n' > "$root/cargo"+ chmod +x "$root/cargo"+ export PATH="$root:$PATH"+ export CARGO="$root/cargo"+ fi+ + total_bins=$(grep -cve '^[[:space:]]*$' test-binaries.txt || true)+ + while IFS="$tab" read -r crate_dir test_bin; do+ [ -n "$test_bin" ] || continue+ checked=$((checked + 1))+ + if [ ! -f "$test_bin" ] || [ ! -x "$test_bin" ]; then+ echo "Error: recorded test executable is missing or not executable: $test_bin" >&2+ failed="$failed $test_bin(missing)"+ continue+ fi+ + file_info="$(file -b "$test_bin")"+ if ! printf '%s' "$file_info" | grep -qi "FreeBSD" || ! printf '%s' "$file_info" | grep -qi -- "$arch_pattern"; then+ echo "Error: $test_bin is not a FreeBSD $arch_pattern binary: $file_info" >&2+ failed="$failed $test_bin(wrong-arch)"+ continue+ fi+ + # The skip lists are per architecture and are always honoured.+ # They are not tied to KVM: a cross-architecture guest runs under+ # TCG, and the x86_64 guest still has a single vCPU, so either+ # leg can overrun timing or resource assumptions a native host+ # would meet.+ case "$MATRIX_ARCH" in+ aarch64) skip_list="${SKIP_TEST_BINARIES_AARCH64:-}" ;;+ x86_64) skip_list="${SKIP_TEST_BINARIES_X86_64:-}" ;;+ *) skip_list="" ;;+ esac+ if [ -n "$skip_list" ]; then+ matched=false+ for pattern in $skip_list; do+ case "$test_bin" in+ $pattern) matched=true; break ;;+ esac+ done+ if [ "$matched" = "true" ]; then+ echo "Skipping (listed for $MATRIX_ARCH): $test_bin" >> excluded-tests.txt+ skipped=$((skipped + 1))+ continue+ fi+ fi+ + case "$MATRIX_ARCH" in+ aarch64) case_skip="${SKIP_TEST_CASES_AARCH64:-}" ;;+ x86_64) case_skip="${SKIP_TEST_CASES_X86_64:-}" ;;+ *) case_skip="" ;;+ esac+ case_skip_flags=""+ if [ -n "$case_skip" ]; then+ # Only libtest harnesses understand --skip. Probe with --list so a+ # harness = false binary is not handed a flag it rejects; the+ # probe is bounded because a custom main() could block.+ if timeout 60 "$root/$test_bin" --list >/dev/null 2>&1; then+ for pattern in $case_skip; do+ # Single-quoted so '::' and '*' reach libtest verbatim; the+ # root path re-parses this string in the su shell, which+ # also runs with globbing disabled (set -f).+ case_skip_flags="$case_skip_flags --skip '$pattern'"+ done+ echo "Skipping cases matching: $case_skip"+ else+ echo "::warning::skip-test-cases-$MATRIX_ARCH ignored for $test_bin: not a libtest harness"+ fi+ fi+ + # Plain words are safe here: patterns contain no whitespace and+ # globbing is off (set -f) in both the su and non-root branches.+ case_only_flags=""+ for pattern in ${ONLY_TEST_CASES:-}; do+ case_only_flags="$case_only_flags $pattern"+ done++ echo "Running test binary: $test_bin in $crate_dir"++ # ========== Original direct-run path (non-libtest or fallback) ==========+ test_log="test-log-$(printf '%s' "$test_bin" | sha256 -q).log"+ set +e+ if [ "$(id -u)" -eq 0 ]; then+ su -m craft-test -c "set -f; HOME=/home/craft-test XDG_CACHE_HOME=/home/craft-test/.cache PATH='$PATH' && export HOME XDG_CACHE_HOME PATH && cd '$root/$crate_dir' && CARGO_MANIFEST_DIR='$root/$crate_dir' && export CARGO_MANIFEST_DIR && timeout ${TEST_TIMEOUT_SECONDS:-1800} '$root/$test_bin'$case_only_flags$case_skip_flags" < /dev/null > "$root/$test_log" 2>&1+ else+ (set -f; cd "$root/$crate_dir" && CARGO_MANIFEST_DIR="$root/$crate_dir" timeout "${TEST_TIMEOUT_SECONDS:-1800}" "$root/$test_bin" $case_only_flags $case_skip_flags < /dev/null > "$root/$test_log" 2>&1)+ fi+ rc=$?+ set -e+ attempted=$((attempted + 1))+ if [ "$rc" -eq 124 ]; then+ echo "$test_bin" >> timed-out-tests.txt+ echo "TIMEOUT after ${TEST_TIMEOUT_SECONDS:-1800}s: $test_bin" >&2+ # libtest runs cases in --list order, so the first case that never+ # printed a result is the likeliest hang.+ if timeout 60 "$root/$test_bin" --list > "$root/all-cases.txt" 2>/dev/null; then+ sed -n 's/: test$//p' "$root/all-cases.txt" > "$root/all.names"+ sed -n 's/^test \(.*\) \.\.\. .*/\1/p' "$root/$test_log" > "$root/finished.names"+ grep -vxF -f "$root/finished.names" "$root/all.names" > "$root/unfinished.names" || true+ echo "Unfinished cases: $(wc -l < "$root/unfinished.names" | tr -d ' ')" >&2+ echo "First unfinished (likely the hang):" >&2+ head -3 "$root/unfinished.names" >&2+ echo " first unfinished: $(head -1 "$root/unfinished.names")" >> timed-out-tests.txt+ rm -f "$root/all-cases.txt" "$root/all.names" "$root/finished.names" "$root/unfinished.names"+ fi+ fi+ if [ "$rc" -ne 0 ]; then+ echo "======================================================"+ echo "FAILED (exit code $rc): $test_bin"+ echo "----------------- Output begins ----------------------"+ cat "$root/$test_log"+ echo "------------------ Output ends -----------------------"+ echo "======================================================"+ failed="$failed $test_bin(failed)"+ else+ passed=$((passed + 1))+ fi+ rm -f "$root/$test_log"+ done < test-binaries.txt+ + if [ -s excluded-tests.txt ]; then+ echo "### Architecture Exclusions"+ echo ""+ echo "The caller listed the following $MATRIX_ARCH test binaries in skip-test-binaries-$MATRIX_ARCH. They were built and architecture-verified but NOT executed, so this run does not validate them. The usual causes are wall-clock dilation under QEMU TCG and the single vCPU the guest is given."+ echo '```text'+ cat excluded-tests.txt+ echo '```'+ fi+ + if [ -s timed-out-tests.txt ]; then+ echo "### Timed Out Test Binaries"+ echo ""+ echo "Each binary below exceeded the per-binary limit of ${TEST_TIMEOUT_SECONDS:-1800} seconds and was killed. They are listed here as well as counted in Failed."+ echo '```text'+ cat timed-out-tests.txt+ echo '```'+ fi++ echo "Summary: Checked $checked, Attempted $attempted, Passed $passed, Skipped $skipped, Failed ${failed:-0}"+ + if [ "$checked" -eq 0 ]; then+ echo "Error: no test binaries were checked" >&2+ exit 1+ fi++ # Checking every binary is not the same as running one. A skip list+ # that matches the whole payload would otherwise leave this phase+ # green with zero tests executed. Callers who mean to skip the suite+ # set skip-tests: true, which is visible in the inputs.+ if [ "$attempted" -eq 0 ]; then+ echo "Error: no test binary was executed (checked $checked, skipped $skipped)." >&2+ exit 1+ fi+ + if [ -n "$failed" ]; then+ echo "Error: The following test binaries failed:$failed" >&2+ exit 1+ fi+ + # Every enumerated binary is visited exactly once and increments+ # checked: executed, skipped (TCG exclusion), or recorded as failed+ # (missing / wrong architecture). attempted + skipped therefore+ # cannot be reconciled against the list when a binary failed.+ if [ "$checked" -ne "$total_bins" ]; then+ echo "Error: Checked $checked binaries, but test-binaries.txt lists $total_bins." >&2+ exit 1+ fi+ fi\ No newline at end of file