diff --git a/.agents/cloud-setup.sh b/.agents/cloud-setup.sh new file mode 100755 index 00000000..ec198873 --- /dev/null +++ b/.agents/cloud-setup.sh @@ -0,0 +1,460 @@ +#!/usr/bin/env bash +# Generated by stll-cloud-setup. Regenerate with stll-cloud-setup. +# Networked installation and offline service startup are separate lifecycle steps. +# Cached filesystems may not preserve processes; run start at every session start. +# Supported platform: Ubuntu 24.04 (noble), amd64/arm64, root or passwordless sudo. +set -euo pipefail + +usage() { echo 'Usage: cloud-setup.sh install|start' >&2; exit 2; } +[[ "$#" == 1 ]] || usage +case "$1" in install|start) MODE="$1" ;; *) usage ;; esac +fail() { echo "cloud-setup: $*" >&2; exit 1; } +trap 'echo "cloud-setup: command failed at line $LINENO" >&2' ERR +# Each phase logs its start and duration so a stalled phase is visible in the task log. +step() { + local name="$1" started="$SECONDS" + shift + echo "cloud-setup: $name started" >&2 + "$@" + echo "cloud-setup: $name done in $((SECONDS - started))s" >&2 +} +[[ -f /etc/os-release ]] || fail 'Ubuntu 24.04 is required' +. /etc/os-release +[[ "$ID" == ubuntu && "$VERSION_ID" == 24.04 && "$VERSION_CODENAME" == noble ]] || fail 'Only Ubuntu 24.04 (noble) is supported' +SCRIPT="$(realpath -- "$0")" +REPO_ROOT="$(cd -- "$(dirname -- "$SCRIPT")/.." && pwd -P)" +[[ "$SCRIPT" == "$REPO_ROOT/.agents/cloud-setup.sh" ]] || fail 'Run the generated repository script' +if [[ "$(id -u)" != 0 ]]; then + command -v sudo >/dev/null && sudo -n true || fail 'Root or passwordless sudo is required' + exec sudo -n -- bash "$SCRIPT" "$MODE" +fi +OUTPUT_UID="$(printenv SUDO_UID || id -u)" +[[ "$OUTPUT_UID" =~ ^[0-9]+$ ]] || fail 'Invalid invoking user' +OUTPUT_GID="$(getent passwd "$OUTPUT_UID" | cut -d: -f4)" +[[ "$OUTPUT_GID" =~ ^[0-9]+$ ]] || fail 'Invoking user is unavailable' +OUTPUT_USER="$(getent passwd "$OUTPUT_UID" | cut -d: -f1)" +OUTPUT_HOME="$(getent passwd "$OUTPUT_UID" | cut -d: -f6)" +[[ -n "$OUTPUT_USER" && "$OUTPUT_HOME" == /* && -d "$OUTPUT_HOME" ]] || fail 'Invoking user requires an existing home directory' +root() { "$@"; } +# Service daemons outlive setup, so they inherit no descriptor beyond stdio: a cloud +# runner can pass its terminal as extra descriptors and wait until every copy closes. +service_run() ( + for descriptor in "/proc/$BASHPID/fd/"*; do + descriptor="$(basename -- "$descriptor")" + if [[ "$descriptor" =~ ^[0-9]+$ ]] && (( descriptor > 2 )); then eval "exec $descriptor>&-"; fi + done + exec runuser -u stll-cloud -- "$@" +) +BUN_VERSION='1.4.3' +NODE_VERSION='26.10.0' +NODE_DIR="/opt/stll-cloud/node/$NODE_VERSION" +BUN_DIR="/opt/stll-cloud/bun/$BUN_VERSION" +export PATH="$NODE_DIR/bin:$BUN_DIR:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" +BUN_CACHE="/var/cache/stll-cloud/bun/$OUTPUT_UID" + +[[ -f "$REPO_ROOT/.node-version" && ! -L "$REPO_ROOT/.node-version" ]] || fail 'A regular root .node-version is required' +[[ "$(awk 'NF { if (++count > 1 || NF != 1) exit 1; value=$1 } END { if (count != 1) exit 1; print value }' "$REPO_ROOT/.node-version")" == "$NODE_VERSION" ]] || fail 'Node selector changed; regenerate cloud-setup.sh' +cd -- "$REPO_ROOT" +case "$(uname -m)" in + x86_64) ARCH=amd64; NODE_ARCH=x64; BUN_ARCH=x64 ;; + aarch64|arm64) ARCH=arm64; NODE_ARCH=arm64; BUN_ARCH=aarch64 ;; + *) fail 'Only amd64 and arm64 are supported' ;; +esac + + +release_log_holders() { + # Processes left by package scripts may still hold a step log after the command exits. + # Setup starts its own services later, so end them rather than let them write into a + # deleted log for their whole lifetime. + # One find over all descriptor links; no process per descriptor. + local log="$1" root="${CLOUD_PROC_ROOT:-/proc}" fd pid + while IFS= read -r fd; do + pid="${fd%/fd/*}" + pid="${pid##*/}" + [[ "$pid" =~ ^[0-9]+$ && "$pid" != "$$" ]] || continue + kill -TERM "$pid" 2>/dev/null || true + done < <(find "$root" -mindepth 3 -maxdepth 3 -path "$root/[0-9]*/fd/*" -lname "$log" 2>/dev/null) +} +network_run() ( + local host="$1" seconds="$2" status=0 + shift 2 + network_log="$(mktemp)" || return "$?" + trap 'rm -f -- "$network_log"' EXIT + trap 'exit 129' HUP + trap 'exit 130' INT + trap 'exit 143' TERM + # Output goes to a file, never a pipe: package scripts may leave processes that keep + # inherited descriptors open, and a pipe reader would wait for them past the command. + # Input is never the terminal: timeout runs the command outside the foreground process + # group, where apt restoring terminal settings on a terminal stdin stops it for good. + timeout --signal=TERM --kill-after=5s "$seconds"s "$@" < /dev/null > "$network_log" 2>&1 || status=$? + release_log_holders "$network_log" + cat -- "$network_log" || fail 'Cannot capture network command output' + [[ "$status" != 0 ]] || return 0 + if [[ "$status" == 124 || "$status" == 137 || "$status" == 28 ]] || grep -Eiq 'timed[[:space:]-]+out|time[[:space:]-]+out|timeout[[:space:]]+(was[[:space:]]+)?(reached|expired)|(connection|operation|socket)[[:space:]-]+timeout' "$network_log"; then + fail "download timed out: $host" + fi + return "$status" +) +download_file() { + local url="$1" destination="$2" seconds="$3" host + host="$(printf '%s\n' "$url" | cut -d/ -f3 | sed 's/.*@//')" + [[ "$url" == https://* && -n "$host" ]] || fail 'Downloads require an HTTPS host' + network_run "$host" "$seconds" curl --fail --silent --show-error --location --proto '=https' --proto-redir '=https' --tlsv1.2 --connect-timeout 20 --max-time "$seconds" --retry 3 --retry-all-errors --retry-max-time "$seconds" "$url" -o "$destination" +} +apt_network() { + local hosts + # --print-uris reads configured indexes without contacting their servers. + hosts="$(set -o pipefail; apt-get --print-uris update | awk -F/ '/^\047https?:\/\// { host=$3; sub(/.*@/, "", host); if (!seen[host]++) { printf "%s%s", separator, host; separator="," } }')" || return "$?" + [[ -n "$hosts" ]] || hosts='configured APT hosts' + # APT_DEADLINE: whole-call seconds; callers that also unpack packages may extend it. + network_run "$hosts" "${APT_DEADLINE:-180}" apt-get -o Acquire::http::Timeout=20 -o Acquire::https::Timeout=20 -o Acquire::Retries=3 -o APT::Update::Error-Mode=any "$@" +} +apt_refresh_for() { + # Skip the refresh only when the install downloads nothing: then no package + # source is contacted and stale cached indexes cannot point at missing files. + local uris + if uris="$(DEBIAN_FRONTEND=noninteractive apt-get --print-uris -qq -y --no-install-recommends install "$@" 2>/dev/null)" && [[ -z "$uris" ]]; then + return 0 + fi + apt_network update +} + + +node_ready() { + local node_version npm_version npx_version + [[ -x "$NODE_DIR/bin/node" && -x "$NODE_DIR/bin/npm" && -x "$NODE_DIR/bin/npx" ]] || return 1 + node_version="$("$NODE_DIR/bin/node" --version)" || return 1 + [[ "$node_version" == "v$NODE_VERSION" ]] || return 1 + npm_version="$("$NODE_DIR/bin/npm" --version)" || return 1 + [[ "$npm_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || return 1 + npx_version="$("$NODE_DIR/bin/npx" --version)" || return 1 + [[ "$npx_version" == "$npm_version" ]] || return 1 +} +runtime_ready() { + node_ready || fail 'Pinned Node, npm or npx is unavailable; run install' + [[ -x "$BUN_DIR/bun" && "$($BUN_DIR/bun --version)" == "$BUN_VERSION" ]] || fail 'Pinned Bun is unavailable; run install' +} +install_runtimes() ( + if [[ ! -s /etc/ssl/certs/ca-certificates.crt ]]; then + command -v update-ca-certificates >/dev/null || fail 'System CA certificate bundle is missing; restore ca-certificates in the base image offline before install' + update-ca-certificates || fail 'System CA certificate bundle could not be repaired offline; restore ca-certificates in the base image before install' + fi + [[ -s /etc/ssl/certs/ca-certificates.crt ]] || fail 'System CA certificate bundle is unavailable after offline repair; restore ca-certificates in the base image before install' + if ! command -v curl >/dev/null || ! command -v unzip >/dev/null || ! command -v xz >/dev/null || ! command -v openssl >/dev/null || ! command -v gpg >/dev/null || ! ldconfig -p | awk '$1 == "libatomic.so.1" { found=1 } END { exit !found }'; then + apt_refresh_for ca-certificates curl unzip xz-utils openssl gnupg util-linux libatomic1 + DEBIAN_FRONTEND=noninteractive apt_install ca-certificates curl unzip xz-utils openssl gnupg util-linux libatomic1 + fi + local node_archive bun_archive bun_folder + # Subshell scope retains scratch for EXIT cleanup after a failed command. + scratch="$(mktemp -d)" + trap 'rm -rf -- "$scratch"' EXIT + if ! node_ready; then + node_archive="node-v$NODE_VERSION-linux-$NODE_ARCH.tar.xz" + download_file "https://nodejs.org/dist/v$NODE_VERSION/$node_archive" "$scratch/$node_archive" 120 + download_file "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt" "$scratch/node-sums" 45 + (cd "$scratch"; awk -v name="$node_archive" '$2 == name { print; count++ } END { if (count != 1) exit 1 }' node-sums | sha256sum --strict -c -) + install -d -m 755 "$NODE_DIR" + tar -xJf "$scratch/$node_archive" --strip-components=1 -C "$NODE_DIR" + fi + if [[ ! -x "$BUN_DIR/bun" ]] || [[ "$($BUN_DIR/bun --version)" != "$BUN_VERSION" ]]; then + bun_folder="bun-linux-$BUN_ARCH" + bun_archive="$bun_folder.zip" + download_file "https://github.com/oven-sh/bun/releases/download/bun-v$BUN_VERSION/$bun_archive" "$scratch/$bun_archive" 120 + download_file "https://github.com/oven-sh/bun/releases/download/bun-v$BUN_VERSION/SHASUMS256.txt" "$scratch/bun-sums" 45 + (cd "$scratch"; awk -v name="$bun_archive" '$2 == name || $2 == "*" name { print $1 " " name; count++ } END { if (count != 1) exit 1 }' bun-sums | sha256sum --strict -c -) + unzip -q "$scratch/$bun_archive" -d "$scratch/bun" + install -d -m 755 "$BUN_DIR" + install -m 755 "$scratch/bun/$bun_folder/bun" "$BUN_DIR/bun" + fi + runtime_ready + ln -sfnT -- "$NODE_DIR/bin/node" /usr/local/bin/node + ln -sfnT -- "$NODE_DIR/bin/npm" /usr/local/bin/npm + ln -sfnT -- "$NODE_DIR/bin/npx" /usr/local/bin/npx + ln -sfnT -- "$BUN_DIR/bun" /usr/local/bin/bun +) + +dependency_install() { + local directory download_hosts + for directory in /var/cache/stll-cloud /var/cache/stll-cloud/bun; do + [[ ! -L "$directory" ]] || fail 'Dependency cache parent must not be a symlink' + if [[ -e "$directory" ]]; then + [[ -d "$directory" && "$(stat -c '%u' "$directory")" == 0 ]] || fail 'Dependency cache parent must be root-owned' + fi + install -d -m 755 -o root -g root "$directory" + done + [[ ! -L "$BUN_CACHE" ]] || fail 'Dependency cache must not be a symlink' + if [[ -e "$BUN_CACHE" ]]; then + [[ -d "$BUN_CACHE" && "$(stat -c '%u' "$BUN_CACHE")" == "$OUTPUT_UID" ]] || fail 'Dependency cache ownership differs from the invoking user' + fi + install -d -m 700 -o "$OUTPUT_UID" -g "$OUTPUT_GID" "$BUN_CACHE" + # Report hostnames only: lockfile/config URLs may contain credentials. + # These are known install hosts; lifecycle scripts can contact other hosts. + download_hosts="$(HOME="$OUTPUT_HOME" "$NODE_DIR/bin/node" -e ' +const fs = require("node:fs"); +const path = require("node:path"); +const hosts = new Set(["registry.npmjs.org"]); +const inputs = [process.env.BUN_CONFIG_REGISTRY ?? "", process.env.NPM_CONFIG_REGISTRY ?? "", process.env.npm_config_registry ?? ""]; +for (const file of ["bun.lock", ".npmrc", "bunfig.toml", path.join(process.env.HOME, ".npmrc"), path.join(process.env.HOME, ".bunfig.toml"), path.join(process.env.XDG_CONFIG_HOME ?? process.env.HOME, ".bunfig.toml")]) { + if (fs.existsSync(file)) inputs.push(fs.readFileSync(file, "utf8")); +} +for (const input of inputs) { + for (const match of input.matchAll(/(?:git\+)?https?:\/\/[^\s"\x27<>\\]+/g)) { + try { hosts.add(new URL(match[0].replace(/^git\+/, "")).hostname); } catch {} + } + if (/github:/.test(input)) { hosts.add("github.com"); hosts.add("codeload.github.com"); } +} +process.stdout.write([...hosts].sort().join(",")); +')" + # Bun 1.4.3 socket inactivity timeout, in seconds; the outer bound includes scripts. + network_run "$download_hosts" 240 runuser -u "$OUTPUT_USER" -- env HOME="$OUTPUT_HOME" BUN_INSTALL_CACHE_DIR="$BUN_CACHE" PATH="$PATH" BUN_CONFIG_HTTP_IDLE_TIMEOUT=20 "$BUN_DIR/bun" install --frozen-lockfile +} + + +# Prevent package maintainer scripts from starting unmanaged system services. +apt_install() ( + # EXIT runs after function locals expire; this subshell owns the trap state. + backup='' + policy_state=untouched + [[ ! -L /var/lock/stll-cloud-apt.lock ]] || fail 'Invalid package install lock' + exec 8>/var/lock/stll-cloud-apt.lock + flock -x -w 120 8 || fail 'Another package installation is active' + [[ ! -L /usr/sbin/policy-rc.d ]] || fail 'Unsupported service-start policy symlink' + [[ ! -e /usr/sbin/policy-rc.d || -f /usr/sbin/policy-rc.d ]] || fail 'Unsupported service-start policy file' + backup="$(mktemp -d)" + cleanup_package_policy() { + local status=$? + trap - EXIT HUP INT TERM + if [[ "$policy_state" == installed ]]; then + if [[ -f "$backup/original" ]]; then + if ! cp -p "$backup/original" /usr/sbin/policy-rc.d; then status=1; fi + else + if ! rm -f /usr/sbin/policy-rc.d; then status=1; fi + fi + fi + rm -rf -- "$backup" + exit "$status" + } + trap cleanup_package_policy EXIT + trap 'exit 129' HUP + trap 'exit 130' INT + trap 'exit 143' TERM + if [[ -e /usr/sbin/policy-rc.d ]]; then cp -p /usr/sbin/policy-rc.d "$backup/original"; fi + policy_state=installed + printf '%s\n' '#!/bin/sh' 'exit 101' > /usr/sbin/policy-rc.d + chmod 755 /usr/sbin/policy-rc.d + local package + for package in "$@"; do + if [[ "$package" == */* ]]; then + # apt cannot fetch local archives with --download-only, so their installs + # stay one call. Package triggers can be slow on cloud filesystems; the + # longer whole-call deadline covers them while Acquire timeouts still + # bound each network request. + APT_DEADLINE=600 DEBIAN_FRONTEND=noninteractive apt_network install -y --no-install-recommends "$@" + return + fi + done + # The network deadline covers downloads only; unpacking and maintainer scripts + # (for example locale generation or cluster creation) run offline afterwards. + DEBIAN_FRONTEND=noninteractive apt_network install -y --no-install-recommends --download-only "$@" + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends --no-download "$@" +) + + + +prepare_state() { :; } + +install_services() { + + : +} + +start_services() { :; } + + + +fetch_verified() { + download_file "$1" "$3" 120 + printf '%s %s\n' "$2" "$3" | sha256sum --strict -c - >/dev/null +} + +export RUSTUP_HOME=/opt/stll-cloud/rustup +export CARGO_HOME=/opt/stll-cloud/cargo +export RUSTUP_TOOLCHAIN='1.99.0' +export RUSTUP_AUTO_INSTALL=0 +rust_run() { + local mode="$1" + shift + set -- runuser -u "$OUTPUT_USER" -- env HOME="$OUTPUT_HOME" RUSTUP_HOME="$RUSTUP_HOME" CARGO_HOME="$CARGO_HOME" RUSTUP_AUTO_INSTALL=0 RUSTUP_TOOLCHAIN=1.99.0 RUSTUP_DIST_SERVER=https://static.rust-lang.org RUSTUP_UPDATE_ROOT=https://static.rust-lang.org/rustup PATH="$PATH" "$@" + case "$mode" in + local) "$@" ;; + network) network_run static.rust-lang.org 180 "$@" ;; + *) fail 'Invalid Rust execution mode' ;; + esac +} +primary_rust_ready() { + local directory + for directory in "$RUSTUP_HOME" "$CARGO_HOME"; do + [[ -d "$directory" && ! -L "$directory" && "$(stat -c '%u' "$directory")" == "$OUTPUT_UID" ]] || return 1 + done + [[ -x "$CARGO_HOME/bin/rustup" ]] || return 1 + [[ "$(rust_run local "$CARGO_HOME/bin/rustup" --version 2>/dev/null)" == 'rustup 1.29.1 '* ]] || return 1 + [[ "$(rust_run local "$CARGO_HOME/bin/rustc" +1.99.0 --version)" == 'rustc 1.99.0 '* ]] || return 1 + local host + host="$(rust_run local "$CARGO_HOME/bin/rustc" +1.99.0 -vV | awk '$1 == "host:" { print $2 }')" || return 1 + rust_run local "$CARGO_HOME/bin/rustup" component list --toolchain '1.99.0' --installed | awk -v name='clippy' -v host="$host" '$1 == name || $1 == name "-" host { found=1 } END { exit !found }' || return 1 + rust_run local "$CARGO_HOME/bin/rustup" component list --toolchain '1.99.0' --installed | awk -v name='rustfmt' -v host="$host" '$1 == name || $1 == name "-" host { found=1 } END { exit !found }' || return 1 + rust_run local "$CARGO_HOME/bin/rustup" target list --toolchain '1.99.0' --installed | awk -v name='wasm32-unknown-unknown' '$1 == name { found=1 } END { exit !found }' || return 1 + return 0 +} +rust_ready() { + local additional_version + primary_rust_ready || return 1 + + return 0 +} +verify_rust() { rust_ready || fail 'Pinned Rust, components or targets are unavailable; run install'; } +install_rust() ( + if ! command -v cc >/dev/null || ! command -v ld >/dev/null || [[ ! -f /usr/include/stdlib.h ]]; then + apt_refresh_for build-essential + apt_install build-essential + fi + local artifact_url artifact_sha additional_version + # Subshell scope retains scratch for EXIT cleanup after a failed command. + scratch="$(mktemp -d)" + trap 'rm -rf -- "$scratch"' EXIT + for directory in "$RUSTUP_HOME" "$CARGO_HOME"; do + [[ ! -L "$directory" ]] || fail 'Rust home must not be a symlink' + if [[ -e "$directory" ]]; then + [[ -d "$directory" && "$(stat -c '%u' "$directory")" == "$OUTPUT_UID" ]] || fail 'Rust home ownership differs from the invoking user' + else + install -d -m 755 -o "$OUTPUT_UID" -g "$OUTPUT_GID" "$directory" + fi + done + if [[ ! -x "$CARGO_HOME/bin/rustup" ]] || [[ "$(rust_run local "$CARGO_HOME/bin/rustup" --version 2>/dev/null)" != 'rustup 1.29.1 '* ]]; then + case "$ARCH" in + amd64) artifact_url='https://static.rust-lang.org/rustup/archive/1.29.1/x86_64-unknown-linux-gnu/rustup-init'; artifact_sha='dda7234360b7f578ca8b0ddcb80145646fa61a67c1720a5abc7051b35c9fcb71' ;; + arm64) artifact_url='https://static.rust-lang.org/rustup/archive/1.29.1/aarch64-unknown-linux-gnu/rustup-init'; artifact_sha='15f6e4ce9f583b929c996c91562bad6d4454f3281de858b02cdfdef615fac433' ;; +esac + fetch_verified "$artifact_url" "$artifact_sha" "$scratch/rustup-init" + chmod 755 "$scratch" "$scratch/rustup-init" + rust_run network "$scratch/rustup-init" -y --no-modify-path --default-toolchain none --profile minimal + fi + rust_run local "$CARGO_HOME/bin/rustup" set auto-self-update disable + if ! primary_rust_ready; then + rust_run network "$CARGO_HOME/bin/rustup" toolchain install 1.99.0 --profile minimal --no-self-update '--component' 'clippy' '--component' 'rustfmt' '--target' 'wasm32-unknown-unknown' + fi + + rust_run local "$CARGO_HOME/bin/rustup" default 1.99.0 + verify_rust + for tool in rustup cargo rustc rustdoc cargo-clippy clippy-driver rustfmt cargo-fmt; do + # Launchers persist the system homes in fresh shells without profile sourcing. + printf '%s\n' '#!/bin/sh' 'export RUSTUP_HOME=/opt/stll-cloud/rustup' 'export CARGO_HOME=/opt/stll-cloud/cargo' 'export RUSTUP_AUTO_INSTALL=0' 'export RUSTUP_TOOLCHAIN=1.99.0' "exec /opt/stll-cloud/cargo/bin/$tool \"\$@\"" > "$scratch/$tool" + if ! cmp -s "$scratch/$tool" "/usr/local/bin/$tool"; then + install -m 755 "$scratch/$tool" "/usr/local/bin/$tool" + fi + done +) + + +install_tools() { + install_rust + +} +verify_tools() { + verify_rust + +} +install_shell_environment() ( + # Subshell scope retains temporary for EXIT cleanup after a failed command. + [[ ! -L '/etc/profile.d/stll-cloud.sh' ]] || fail 'Managed shell profile must not be a symlink' + if [[ -e '/etc/profile.d/stll-cloud.sh' ]]; then + [[ -f '/etc/profile.d/stll-cloud.sh' && "$(stat -c '%h:%u' '/etc/profile.d/stll-cloud.sh')" == 1:0 ]] || fail 'Managed shell profile must be a root-owned single-link file' + fi + temporary="$(mktemp)" + trap 'rm -f -- "$temporary"' EXIT + cat > "$temporary" <<'STLL_CLOUD_PROFILE' +# Generated by stll-cloud-setup; managed cloud shell environment. +# Remove managed entries before prepending, including repeated shell startup. +_stll_cloud_remaining="${PATH-}" +_stll_cloud_result='' +_stll_cloud_separator='' +while :; do + case "$_stll_cloud_remaining" in + *:*) _stll_cloud_part="${_stll_cloud_remaining%%:*}"; _stll_cloud_remaining="${_stll_cloud_remaining#*:}"; _stll_cloud_more=yes ;; + *) _stll_cloud_part="$_stll_cloud_remaining"; _stll_cloud_more=no ;; + esac + case "$_stll_cloud_part" in + '/opt/stll-cloud/node/26.10.0/bin'|'/opt/stll-cloud/bun/1.4.3'|/usr/local/bin) ;; + *) _stll_cloud_result="$_stll_cloud_result$_stll_cloud_separator$_stll_cloud_part"; _stll_cloud_separator=: ;; + esac + [ "$_stll_cloud_more" = yes ] || break +done +export PATH='/opt/stll-cloud/node/26.10.0/bin:/opt/stll-cloud/bun/1.4.3:/usr/local/bin'"${_stll_cloud_separator:+:}$_stll_cloud_result" +unset _stll_cloud_remaining _stll_cloud_result _stll_cloud_separator _stll_cloud_part _stll_cloud_more +export RUSTUP_HOME=/opt/stll-cloud/rustup +export CARGO_HOME=/opt/stll-cloud/cargo +export RUSTUP_TOOLCHAIN='1.99.0' +export RUSTUP_AUTO_INSTALL=0 +STLL_CLOUD_PROFILE + if ! cmp -s "$temporary" '/etc/profile.d/stll-cloud.sh'; then install -m 644 "$temporary" '/etc/profile.d/stll-cloud.sh'; fi + runuser -u "$OUTPUT_USER" -- env HOME="$OUTPUT_HOME" bash -c ' +set -euo pipefail +for name in .bashrc .profile .bash_profile .bash_login; do + file="$HOME/$name" + if [[ ! -e "$file" && ! -L "$file" && "$name" != .bashrc && "$name" != .profile ]]; then continue; fi + [[ ! -L "$file" ]] || { echo "cloud-setup: shell startup file must not be a symlink: $name" >&2; exit 1; } + if [[ -e "$file" ]]; then + [[ -f "$file" && "$(stat -c '\''%h:%u'\'' "$file")" == "1:$(id -u)" ]] || { echo "cloud-setup: shell startup file must be owned and single-link: $name" >&2; exit 1; } + fi + temporary="$(mktemp "$HOME/.stll-cloud-shell.XXXXXX")" + trap '\''rm -f -- "$temporary"'\'' EXIT + if [[ "$name" == .bashrc ]]; then + printf '\''%s\n'\'' '\''# stll-cloud: early environment'\'' '\''. /etc/profile.d/stll-cloud.sh'\'' > "$temporary" + fi + if [[ -f "$file" ]]; then + # Remove only exact managed blocks; preserve unrelated startup lines and permissions. + awk '\'' + $0 == "# stll-cloud: early environment" || $0 == "# stll-cloud: session environment" { + if ((getline following) <= 0 || following != ". /etc/profile.d/stll-cloud.sh") exit 1 + next + } + { print } + '\'' "$file" >> "$temporary" || { echo "cloud-setup: managed shell startup block differs: $name" >&2; exit 1; } + fi + printf '\''%s\n'\'' '\''# stll-cloud: session environment'\'' '\''. /etc/profile.d/stll-cloud.sh'\'' >> "$temporary" + if [[ -f "$file" ]] && cmp -s "$temporary" "$file"; then rm -- "$temporary"; continue; fi + if [[ -f "$file" ]]; then chmod --reference="$file" "$temporary"; else chmod 644 "$temporary"; fi + mv -f -- "$temporary" "$file" +done +' +) +verify_shell_environment() { + [[ -f '/etc/profile.d/stll-cloud.sh' && ! -L '/etc/profile.d/stll-cloud.sh' && "$(stat -c '%h:%u' '/etc/profile.d/stll-cloud.sh')" == 1:0 ]] || fail 'Managed shell environment is unavailable; run install' + [[ "$(sha256sum -- '/etc/profile.d/stll-cloud.sh' | cut -d ' ' -f1)" == '056727e845ab7ff91ead2816d447c2c56c18249546768769d36468d7b38dc595' ]] || fail 'Managed shell environment differs; run install' +} + + +[[ -f 'rust-toolchain.toml' && ! -L 'rust-toolchain.toml' ]] && [[ "$(sha256sum -- 'rust-toolchain.toml' | cut -d ' ' -f1)" == '72f7dd8c9fdf9c1fcd8eb8bf9326d4bc66833e4ebf4bc2932636e2b85efcb7af' ]] || fail 'rust-toolchain.toml changed; regenerate cloud-setup.sh' +[[ ! -e 'rust-toolchain' && ! -L 'rust-toolchain' ]] || fail 'rust-toolchain appeared; regenerate cloud-setup.sh' + + + + + +if [[ "$MODE" == install ]]; then + step runtimes install_runtimes + step services install_services + step tools install_tools + step shell install_shell_environment + step dependencies dependency_install + + exit 0 +fi +step runtime-check runtime_ready +step tool-check verify_tools +step shell install_shell_environment +step shell-check verify_shell_environment + +step state prepare_state +step service-start start_services diff --git a/.cargo/config.toml b/.cargo/config.toml index 5941c233..b5880752 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -5,7 +5,7 @@ protocol = "sparse" [alias] ci-fmt = "fmt --all -- --check" ci-clippy = "clippy --workspace --all-targets --all-features --locked -- -D warnings" -ci-dylint = "dylint --workspace --all" +ci-stella-lint = "stella-lint ." # nextest runs each test in its own process (a panic in one test cannot poison # the run) and is faster than the built-in harness. It does NOT run doctests, # so `ci-test-doc` covers those separately; keep both wired in `rust:test`. diff --git a/.changeset/shared-tooling-adoption.md b/.changeset/shared-tooling-adoption.md new file mode 100644 index 00000000..a845151c --- /dev/null +++ b/.changeset/shared-tooling-adoption.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 00000000..fe7a9311 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,19 @@ +{ + "attribution": { + "commit": "", + "pr": "" + }, + "hooks": { + "SessionStart": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "if [ \"${CLAUDE_CODE_REMOTE:-}\" = \"true\" ]; then\n cloud_setup_incomplete() {\n printf '%s\\n' 'Cloud setup could not establish a verified session environment. From the repository root, run bash .agents/cloud-setup.sh install and then bash .agents/cloud-setup.sh start. Do not assume pinned tools or services are ready until start succeeds.'\n printf '%s\\n' \"cloud-setup: $1\" >&2\n exit 0\n }\n bash \"$CLAUDE_PROJECT_DIR/.agents/cloud-setup.sh\" start >&2 || cloud_setup_incomplete 'startup failed; run install'\n if [ -n \"${CLAUDE_ENV_FILE:-}\" ]; then\n if [ ! -s /etc/profile.d/stll-cloud.sh ]; then\n cloud_setup_incomplete 'cloud shell profile is unavailable; run install'\n fi\n cloud_shell_exports=\"$(cat /etc/profile.d/stll-cloud.sh)\" || cloud_setup_incomplete 'cloud shell profile could not be read; run install'\n [ -n \"$cloud_shell_exports\" ] || cloud_setup_incomplete 'cloud shell profile is unavailable; run install'\n printf '\\n%s\\n' \"$cloud_shell_exports\" >> \"$CLAUDE_ENV_FILE\" || cloud_setup_incomplete 'session environment could not be persisted; check its write access and rerun start'\n fi\nfi" + } + ] + } + ] + } +} diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c888d374..5c819ac8 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,141 +1,212 @@ version: 2 - updates: - - package-ecosystem: "github-actions" - directory: "/" + - package-ecosystem: bun + directory: / schedule: - interval: "cron" - cronjob: "0 3 * * *" - timezone: "Europe/Prague" + interval: weekly + day: saturday + time: "07:00" + timezone: Europe/Prague cooldown: default-days: 5 groups: - minor-and-patch: + dependencies: patterns: - "*" update-types: - - "minor" - - "patch" - labels: - - "dependencies" - commit-message: - prefix: "chore" - reviewers: - - "jan-kubica" - - - package-ecosystem: "bun" - directories: - - "/" + - minor + - patch + ignore: + - dependency-name: oxlint + update-types: + - version-update:semver-major + - version-update:semver-minor + - version-update:semver-patch + - dependency-name: oxlint-tsgolint + update-types: + - version-update:semver-major + - version-update:semver-minor + - version-update:semver-patch + - dependency-name: "@oxlint/plugins" + update-types: + - version-update:semver-major + - version-update:semver-minor + - version-update:semver-patch + - dependency-name: "@stll/oxlint-plugin" + update-types: + - version-update:semver-major + - version-update:semver-minor + - version-update:semver-patch + - dependency-name: oxfmt + update-types: + - version-update:semver-major + - version-update:semver-minor + - version-update:semver-patch + - dependency-name: lefthook + update-types: + - version-update:semver-major + - version-update:semver-minor + - version-update:semver-patch + - dependency-name: typescript + update-types: + - version-update:semver-major + - version-update:semver-minor + - version-update:semver-patch + - dependency-name: "@typescript/native" + update-types: + - version-update:semver-major + - version-update:semver-minor + - version-update:semver-patch + - dependency-name: typescript-compat + update-types: + - version-update:semver-major + - version-update:semver-minor + - version-update:semver-patch + - dependency-name: bun-types + update-types: + - version-update:semver-major + - version-update:semver-minor + - version-update:semver-patch + - package-ecosystem: bundler + directory: /scripts/oracle schedule: - interval: "cron" - cronjob: "0 3 * * *" - timezone: "Europe/Prague" + interval: weekly + day: saturday + time: "07:00" + timezone: Europe/Prague cooldown: default-days: 5 groups: - minor-and-patch: + dependencies: patterns: - "*" update-types: - - "minor" - - "patch" - oxc: - patterns: - - "oxlint" - - "oxfmt" - update-types: - - "major" - commit-message: - prefix: "chore" - reviewers: - - "jan-kubica" - - - package-ecosystem: "pip" + - minor + - patch + - package-ecosystem: cargo directories: - - "/scripts/oracle" - - "/crates/stdnum-py/tests" + - / + - /crates/stdnum-wasm-size-fixture + - /fuzz + - /scripts/rust-oracle schedule: - interval: "cron" - cronjob: "0 3 * * *" - timezone: "Europe/Prague" + interval: weekly + day: saturday + time: "07:00" + timezone: Europe/Prague cooldown: default-days: 5 - labels: - - "dependencies" - commit-message: - prefix: "chore" - reviewers: - - "jan-kubica" groups: - minor-and-patch: + dependencies: patterns: - "*" update-types: - - "minor" - - "patch" - - - package-ecosystem: "bundler" - directory: "/scripts/oracle" + - minor + - patch + - package-ecosystem: composer + directory: /scripts/oracle schedule: - interval: "cron" - cronjob: "0 3 * * *" - timezone: "Europe/Prague" + interval: weekly + day: saturday + time: "07:00" + timezone: Europe/Prague cooldown: default-days: 5 - labels: - - "dependencies" - commit-message: - prefix: "chore" - reviewers: - - "jan-kubica" groups: - minor-and-patch: + dependencies: patterns: - "*" update-types: - - "minor" - - "patch" - - - package-ecosystem: "composer" - directory: "/scripts/oracle" + - minor + - patch + - package-ecosystem: github-actions + directory: / schedule: - interval: "cron" - cronjob: "0 3 * * *" - timezone: "Europe/Prague" + interval: weekly + day: saturday + time: "07:00" + timezone: Europe/Prague cooldown: default-days: 5 - labels: - - "dependencies" - commit-message: - prefix: "chore" - reviewers: - - "jan-kubica" groups: - minor-and-patch: + dependencies: patterns: - "*" update-types: - - "minor" - - "patch" - - - package-ecosystem: "cargo" - directory: "/scripts/rust-oracle" + - minor + - patch + ignore: + - dependency-name: actions/checkout + update-types: + - version-update:semver-major + - version-update:semver-minor + - version-update:semver-patch + - dependency-name: oven-sh/setup-bun + update-types: + - version-update:semver-major + - version-update:semver-minor + - version-update:semver-patch + - dependency-name: actions/setup-node + update-types: + - version-update:semver-major + - version-update:semver-minor + - version-update:semver-patch + - dependency-name: actions/setup-python + update-types: + - version-update:semver-major + - version-update:semver-minor + - version-update:semver-patch + - dependency-name: astral-sh/setup-uv + update-types: + - version-update:semver-major + - version-update:semver-minor + - version-update:semver-patch + - dependency-name: actions/cache + update-types: + - version-update:semver-major + - version-update:semver-minor + - version-update:semver-patch + - dependency-name: actions/cache/restore + update-types: + - version-update:semver-major + - version-update:semver-minor + - version-update:semver-patch + - dependency-name: actions/cache/save + update-types: + - version-update:semver-major + - version-update:semver-minor + - version-update:semver-patch + - dependency-name: actions/upload-artifact + update-types: + - version-update:semver-major + - version-update:semver-minor + - version-update:semver-patch + - dependency-name: actions/download-artifact + update-types: + - version-update:semver-major + - version-update:semver-minor + - version-update:semver-patch + - dependency-name: stella/.github/.github/workflows/package-consumer-compat.yml + update-types: + - version-update:semver-major + - version-update:semver-minor + - version-update:semver-patch + - package-ecosystem: pip + directories: + - /crates/stdnum-py + - /crates/stdnum-py/tests + - /scripts/oracle schedule: - interval: "cron" - cronjob: "0 3 * * *" - timezone: "Europe/Prague" + interval: weekly + day: saturday + time: "07:00" + timezone: Europe/Prague cooldown: default-days: 5 - labels: - - "dependencies" - commit-message: - prefix: "chore" - reviewers: - - "jan-kubica" groups: - minor-and-patch: + dependencies: patterns: - "*" update-types: - - "minor" - - "patch" + - minor + - patch diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ce5afce3..840b3a7a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,7 +64,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version-file: "package.json" - run: bun install --frozen-lockfile @@ -90,12 +90,25 @@ jobs: contents: read steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + with: + fetch-depth: 0 + persist-credentials: false + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version-file: package.json - run: bun install --frozen-lockfile - run: bun run check:toolchain + - name: Detect shared toolchain changes + id: toolchain + env: + BASE_REF: ${{ github.event.pull_request.base.sha || github.event.before || 'unavailable-toolchain-base' }} + run: | + result=$(bunx --no-install stll-toolchain-changed --since "$BASE_REF") + printf '%s\n' "$result" + printf 'changed=%s\n' "$(printf '%s' "$result" | jq -r '.changed')" >> "$GITHUB_OUTPUT" - run: bun run check:typecheck-parity + if: steps.toolchain.outputs.changed == 'true' + - run: bun run test:tooling - run: bun run lint - run: bun run format:check @@ -113,7 +126,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: "22.21.1" + node-version-file: .node-version - run: node scripts/version-sync.mjs check test: @@ -128,7 +141,7 @@ jobs: contents: read steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version-file: package.json - run: bun install --frozen-lockfile @@ -147,7 +160,7 @@ jobs: contents: read steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version-file: package.json - run: bun install --frozen-lockfile @@ -185,23 +198,20 @@ jobs: with: persist-credentials: false - - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version-file: package.json - run: bun install --frozen-lockfile - - name: Setup Rust - # 1.96.0 is the pinned toolchain (rust-toolchain.toml); the nightly with - # rustc-dev is only needed to build the shared Dylint lint libraries. + - name: Setup shared stable Rust run: | - rustup toolchain install 1.96.0 --profile minimal \ - --component rustfmt,clippy - rustup toolchain install nightly-2026-04-16 --profile minimal \ - --component rustc-dev \ - --component llvm-tools-preview - rustup default 1.96.0 - cargo install cargo-dylint --version 6.0.1 --locked - cargo install dylint-link --version 6.0.1 --locked + rustup toolchain install 1.99.0 --profile minimal --component rustfmt,clippy + rustup default 1.99.0 + - name: Install shared stable source checker + run: >- + cargo install --git https://github.com/stella/tooling + --rev 0ad75da354a5efff11626f59dcf8e07a0d0aea48 + --locked stella_source_lint - name: Install pinned Hawk shell: bash @@ -219,7 +229,7 @@ jobs: mkdir -p "$install_dir" tar -xzf "$archive" -C "$install_dir" --strip-components=1 echo "$install_dir" >> "$GITHUB_PATH" - rustup toolchain install "$HAWK_RUST_TOOLCHAIN" --profile minimal + rustup toolchain install 1.97.1 --profile minimal - name: Install pinned Rust CI tools (checksum-verified) # Prebuilt nextest (the `cargo ci-test` runner), cargo-deny (the @@ -239,11 +249,11 @@ jobs: ~/.cargo/registry/cache/ ~/.cargo/git/db/ target/ - key: ${{ runner.os }}-cargo-1.96.0-${{ hashFiles('Cargo.lock') }} + key: ${{ runner.os }}-cargo-1.99.0-${{ hashFiles('Cargo.lock') }} restore-keys: | - ${{ runner.os }}-cargo-1.96.0- + ${{ runner.os }}-cargo-1.99.0- - - name: Rust checks (fmt, clippy, dylint, test) + - name: Rust checks (fmt, clippy, source lint, test) run: bun run rust:check - name: Hawk visibility lint @@ -279,13 +289,13 @@ jobs: contents: read steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version-file: package.json - run: bun install --frozen-lockfile - name: Install the WebAssembly build toolchain run: | - rustup target add wasm32-unknown-unknown --toolchain 1.96.0 + rustup target add wasm32-unknown-unknown --toolchain 1.99.0 cargo install wasm-bindgen-cli --version 0.2.129 --locked - run: bun run typecheck - run: bun run build @@ -300,7 +310,7 @@ jobs: - name: Build the Python abi3 wheel uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: - rust-toolchain: "1.96.0" + rust-toolchain: "1.99.0" args: >- --release --locked @@ -331,8 +341,8 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: "22.21.1" - - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + node-version-file: .node-version + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version-file: package.json - run: bun install --frozen-lockfile diff --git a/.github/workflows/mutants.yml b/.github/workflows/mutants.yml index bec7406e..49cf8581 100644 --- a/.github/workflows/mutants.yml +++ b/.github/workflows/mutants.yml @@ -28,8 +28,8 @@ jobs: - name: Setup Rust run: | - rustup toolchain install 1.96.0 --profile minimal - rustup default 1.96.0 + rustup toolchain install 1.99.0 --profile minimal + rustup default 1.99.0 - name: Install pinned Rust tools (checksum-verified) # nextest is the test runner used by cargo-mutants. taiki-e/install-action @@ -46,9 +46,9 @@ jobs: ~/.cargo/registry/cache/ ~/.cargo/git/db/ target/ - key: ${{ runner.os }}-mutants-1.96.0-${{ hashFiles('Cargo.lock') }} + key: ${{ runner.os }}-mutants-1.99.0-${{ hashFiles('Cargo.lock') }} restore-keys: | - ${{ runner.os }}-mutants-1.96.0- + ${{ runner.os }}-mutants-1.99.0- - name: Run cargo-mutants # Scoped to the validation core (lib.rs). The fast unit cases diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f4177bac..59e31a4b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,13 +33,13 @@ jobs: fetch-depth: 0 persist-credentials: false - - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version-file: package.json - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: "22.21.1" + node-version-file: .node-version - name: Require main for manual publishing if: >- @@ -84,8 +84,8 @@ jobs: - name: Install Rust toolchain run: | - rustup toolchain install 1.96.0 --profile minimal --component rustfmt,clippy - rustup default 1.96.0 + rustup toolchain install 1.99.0 --profile minimal --component rustfmt,clippy + rustup default 1.99.0 - name: Verify versions and generated sources run: | @@ -133,21 +133,21 @@ jobs: with: persist-credentials: false - - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version-file: package.json - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: "22.21.1" + node-version-file: .node-version - name: Install dependencies run: bun install --frozen-lockfile - name: Install Rust toolchain run: | - rustup toolchain install 1.96.0 --profile minimal - rustup default 1.96.0 + rustup toolchain install 1.99.0 --profile minimal + rustup default 1.99.0 - name: Build native addon run: node scripts/build-native-node.mjs @@ -188,22 +188,22 @@ jobs: with: persist-credentials: false - - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version-file: package.json - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: "22.21.1" + node-version-file: .node-version - name: Install dependencies run: bun install --frozen-lockfile - name: Install Rust and wasm-bindgen run: | - rustup toolchain install 1.96.0 --profile minimal - rustup target add wasm32-unknown-unknown --toolchain 1.96.0 - rustup default 1.96.0 + rustup toolchain install 1.99.0 --profile minimal + rustup target add wasm32-unknown-unknown --toolchain 1.99.0 + rustup default 1.99.0 cargo install wasm-bindgen-cli --version 0.2.129 --locked - name: Build portable packages @@ -311,7 +311,7 @@ jobs: with: target: ${{ matrix.target }} manylinux: ${{ matrix.manylinux || 'auto' }} - rust-toolchain: "1.96.0" + rust-toolchain: "1.99.0" args: >- --release --locked diff --git a/.node-version b/.node-version new file mode 100644 index 00000000..8a94b991 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +26.10.0 diff --git a/Cargo.toml b/Cargo.toml index 8cb2065d..3e883f08 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,9 +11,8 @@ resolver = "3" [workspace.package] version = "2.3.2" edition = "2024" -# Minimum supported Rust version, matching the pinned toolchain -# (rust-toolchain.toml). With resolver "3" this also makes dependency -# resolution MSRV-aware and fails fast on an older rustc. +# Minimum supported Rust version; CI uses the shared stable toolchain. +# Resolver "3" keeps dependency resolution MSRV-aware. rust-version = "1.96" license = "Apache-2.0" publish = false diff --git a/bun.lock b/bun.lock index cb19e3be..a447614f 100644 --- a/bun.lock +++ b/bun.lock @@ -8,8 +8,9 @@ "@brazilian-utils/brazilian-utils": "^2.5.0", "@changesets/changelog-github": "^1.0.1", "@changesets/cli": "^3.0.3", - "@stll/oxlint-config": "0.10.0", - "@stll/typescript-config": "0.6.0", + "@stll/oxlint-config": "1.4.0", + "@stll/oxlint-plugin": "0.2.2", + "@stll/typescript-config": "0.8.0", "@types/node": "^26.6.5", "bun-types": "1.4.3", "fast-check": "^4.10.2", @@ -17,7 +18,7 @@ "iban": "^0.0.14", "ibantools": "^4.5.4", "jsvat": "^2.5.4", - "lefthook": "^2.1.14", + "lefthook": "2.1.16", "luhn": "^2.4.1", "oxfmt": "0.72.0", "oxlint": "1.87.0", @@ -117,6 +118,58 @@ "@clack/prompts": ["@clack/prompts@1.8.1", "", { "dependencies": { "@clack/core": "1.5.1", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-dlT1m5e/0yUL0kRNcQn7yGLVThkgbB0Ga/1AmfDDC/8ik6AIiSf2QLQO2zPYvefsHP0aFgxO93cVLCCfDp7kzQ=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.28.2", "", { "os": "android", "cpu": "arm" }, "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.2", "", { "os": "android", "cpu": "arm64" }, "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.28.2", "", { "os": "android", "cpu": "x64" }, "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.2", "", { "os": "linux", "cpu": "arm" }, "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.2", "", { "os": "linux", "cpu": "x64" }, "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.2", "", { "os": "none", "cpu": "x64" }, "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g=="], + "@manypkg/find-root": ["@manypkg/find-root@3.1.0", "", { "dependencies": { "@manypkg/tools": "^2.1.0" } }, "sha512-BcSqCyKhBVZ5YkSzOiheMCV41kqAFptW6xGqYSTjkVTl9XQpr+pqHhwgGCOHQtjDCv7Is6EFyA14Sm5GVbVABA=="], "@manypkg/get-packages": ["@manypkg/get-packages@3.1.0", "", { "dependencies": { "@manypkg/find-root": "^3.1.0", "@manypkg/tools": "^2.1.0" } }, "sha512-0TbBVyvPrP7xGYBI/cP8UP+yl/z+HtbTttAD7FMAJgn/kXOTwh5/60TsqP9ZYY710forNfyV0N8P/IE/ujGZJg=="], @@ -213,6 +266,8 @@ "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.87.0", "", { "os": "win32", "cpu": "x64" }, "sha512-k+m/EShyXE29J/r1fjk0HlAzhN5JhIvj044oSJkXTEkCLrWZQgXb/3r4q5LTw/eaKW/4GuMXG2xmiJs3s5rPNw=="], + "@oxlint/plugins": ["@oxlint/plugins@1.87.0", "", {}, "sha512-OtSZ6NxNkS7bkVy7zk9zGnajz2hVIu0uRPP/dJvJesGRaXtimpA4lpxap7FaIJndG2UIoKhnBU4DxyDQIWv6yg=="], + "@pnpm/deps.graph-sequencer": ["@pnpm/deps.graph-sequencer@1100.0.1", "", {}, "sha512-pOr5+q1fLYKwFN3LAJuGZEnfXDcQ73zqgDHMtGy+K+uIoUqyY+6MeDCWFwfu+4EFuq76I5EPFofoNAI+Bmmq4A=="], "@quansync/fs": ["@quansync/fs@1.1.0", "", { "dependencies": { "quansync": "^1.0.0" } }, "sha512-qAPG/t3HqML1TlN7sY/pTbEjzFVAKsMjNNMGheyDosro+kT4iw2KCUoHcVdmliwWjorm4elZbgNQyU2eD97eDg=="], @@ -249,7 +304,9 @@ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], - "@stll/oxlint-config": ["@stll/oxlint-config@0.10.0", "", { "peerDependencies": { "oxlint": ">=1.87.0 <2", "oxlint-tsgolint": ">=7.0.2003 <8" }, "bin": { "stll-toolchain-check": "dist/toolchain-check-cli.mjs", "stll-typecheck-parity": "dist/typecheck-parity-cli.mjs" } }, "sha512-9ADgivk/H+9VzIpsODyjUzny/pWMQCCi8fOX2LstF/derjeHhG8Bj8rnP+xWhRwsAGsioJEkEV9dxzaa7uZXfA=="], + "@stll/oxlint-config": ["@stll/oxlint-config@1.4.0", "", { "dependencies": { "acorn": "8.19.0", "esbuild": "0.28.2", "picomatch": "4.0.7", "semver": "7.8.5", "smol-toml": "1.9.1", "tsx": "4.23.15", "yaml": "2.9.1" }, "peerDependencies": { "oxlint": ">=1.87.0 <2", "oxlint-tsgolint": ">=7.0.2003 <8", "vite": "8.1.5" }, "optionalPeers": ["vite"], "bin": { "stll-dependabot": "dist/dependabot-cli.mjs", "stll-cloud-setup": "dist/cloud-setup-cli.mjs", "stll-consumer-compat": "dist/consumer-compat-cli.mjs", "stll-toolchain-check": "dist/toolchain-check-cli.mjs", "stll-typecheck-probe": "dist/typecheck-probe-cli.mjs", "stll-publish-contract": "dist/publish-contract-cli.mjs", "stll-typecheck-parity": "dist/typecheck-parity-cli.mjs", "stll-toolchain-changed": "dist/toolchain-changed-cli.mjs" } }, "sha512-F5E4Jsq8LfIivOn5ssv8j3QaGnApg6CM9x3PKBpS3S+3FJexbJJcTdka2sZpCGfQwOxlLkIQ1J4RgIH81o9v7w=="], + + "@stll/oxlint-plugin": ["@stll/oxlint-plugin@0.2.2", "", { "peerDependencies": { "@oxlint/plugins": ">=1.75.0 <2", "oxlint": ">=1.75.0 <2" } }, "sha512-ZhE55byN2+Y+S/0734kckX8cC5IKb9ulCEg296ww9psTUEzhwHiBF0/WdFx9q8tclkhpbEQ17mgO0WWb91ZHYw=="], "@stll/stdnum": ["@stll/stdnum@workspace:packages/stdnum"], @@ -265,7 +322,7 @@ "@stll/stdnum-win32-x64-msvc": ["@stll/stdnum-win32-x64-msvc@workspace:packages/stdnum-win32-x64-msvc"], - "@stll/typescript-config": ["@stll/typescript-config@0.6.0", "", { "peerDependencies": { "typescript": ">=7.0.2 <8" } }, "sha512-A9efDLMjjjdn4iBOY4qFrTtkghX96NP3vvKgIKC14xwuX55rtBUFpICsdFZ2mVIjxNcNZysO+el7pCVn+UFA3A=="], + "@stll/typescript-config": ["@stll/typescript-config@0.8.0", "", { "peerDependencies": { "typescript": ">=6.0.3 <8" } }, "sha512-ioOZAsouFU/1oFhHmlLPcWkSOkeq+JXQ8XNIUWa1o7V1pQa3GfJocBDQI+fi1u6dJVlppEHfby/b8G1CyfNodg=="], "@types/node": ["@types/node@26.6.5", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg=="], @@ -359,6 +416,8 @@ "@yuku-toolchain/types": ["@yuku-toolchain/types@0.10.2", "", {}, "sha512-sSeo4SSSToiS+sSD+bwn/s94EEcaLJ7tG5LCp8gFYC1G5VzxzX7fqB6m9RU1yMD8KTpu6zdU/I1NlQf8hVBJ9Q=="], + "acorn": ["acorn@8.19.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw=="], + "bun-types": ["bun-types@1.4.3", "", { "dependencies": { "@types/node": "*", "undici-types": "*" } }, "sha512-tinRJNGOmUN+tEAsn2y2e942n9JHRHR42iUYzDH75NmhJqVBokGSQkkfSFdNWwsANIdrK6ACH9t01M452YyTwg=="], "cac": ["cac@7.0.0", "", {}, "sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ=="], @@ -373,6 +432,8 @@ "empathic": ["empathic@2.1.0", "", {}, "sha512-AnfC1ATldl49/cvZdLPDjBfrRNwbDO05aibiOtzQu3qtlbJtomNLhF30HEtn/7iBz50dlMECqATo3fG0LrdEgw=="], + "esbuild": ["esbuild@0.28.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.2", "@esbuild/android-arm": "0.28.2", "@esbuild/android-arm64": "0.28.2", "@esbuild/android-x64": "0.28.2", "@esbuild/darwin-arm64": "0.28.2", "@esbuild/darwin-x64": "0.28.2", "@esbuild/freebsd-arm64": "0.28.2", "@esbuild/freebsd-x64": "0.28.2", "@esbuild/linux-arm": "0.28.2", "@esbuild/linux-arm64": "0.28.2", "@esbuild/linux-ia32": "0.28.2", "@esbuild/linux-loong64": "0.28.2", "@esbuild/linux-mips64el": "0.28.2", "@esbuild/linux-ppc64": "0.28.2", "@esbuild/linux-riscv64": "0.28.2", "@esbuild/linux-s390x": "0.28.2", "@esbuild/linux-x64": "0.28.2", "@esbuild/netbsd-arm64": "0.28.2", "@esbuild/netbsd-x64": "0.28.2", "@esbuild/openbsd-arm64": "0.28.2", "@esbuild/openbsd-x64": "0.28.2", "@esbuild/openharmony-arm64": "0.28.2", "@esbuild/sunos-x64": "0.28.2", "@esbuild/win32-arm64": "0.28.2", "@esbuild/win32-ia32": "0.28.2", "@esbuild/win32-x64": "0.28.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA=="], + "fast-check": ["fast-check@4.10.2", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-iK2f+YrcmoeGqk6fA0ea2bptcu/itMIm4NfEozq6N25+aG6h7s5HZbB/k1aV7b5w5sFLMCbbtRUsTVR+BgC3xw=="], "fast-luhn": ["fast-luhn@2.0.2", "", {}, "sha512-aaIb41cPmEJd2kltUEskBGx4YfASSdTdvuvjfdb70OvUtn+mUAjDzz6o7SnV0h5p2JC63cIYyY+bQ+WnY1/9HA=="], @@ -409,27 +470,27 @@ "launch-editor": ["launch-editor@2.14.1", "", { "dependencies": { "picocolors": "^1.1.1", "shell-quote": "^1.8.4" } }, "sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA=="], - "lefthook": ["lefthook@2.1.14", "", { "optionalDependencies": { "lefthook-darwin-arm64": "2.1.14", "lefthook-darwin-x64": "2.1.14", "lefthook-freebsd-arm64": "2.1.14", "lefthook-freebsd-x64": "2.1.14", "lefthook-linux-arm64": "2.1.14", "lefthook-linux-x64": "2.1.14", "lefthook-openbsd-arm64": "2.1.14", "lefthook-openbsd-x64": "2.1.14", "lefthook-windows-arm64": "2.1.14", "lefthook-windows-x64": "2.1.14" }, "bin": { "lefthook": "bin/index.js" } }, "sha512-Y9TL1dmpRlIdSp73SJ6bb+xAZ4s2SinGTK3pgC9PRWMSjMr2iOfowPGnlSOfAo5fu6S5rdiItsQDxeLEWCGk4g=="], + "lefthook": ["lefthook@2.1.16", "", { "optionalDependencies": { "lefthook-darwin-arm64": "2.1.16", "lefthook-darwin-x64": "2.1.16", "lefthook-freebsd-arm64": "2.1.16", "lefthook-freebsd-x64": "2.1.16", "lefthook-linux-arm64": "2.1.16", "lefthook-linux-x64": "2.1.16", "lefthook-openbsd-arm64": "2.1.16", "lefthook-openbsd-x64": "2.1.16", "lefthook-windows-arm64": "2.1.16", "lefthook-windows-x64": "2.1.16" }, "bin": { "lefthook": "bin/index.js" } }, "sha512-U69YGDNsZ7R+umeSzdLzORNCHQFz/w4LKyNTjJQcGrQRQCr8hlCJfJwwm9sqq9Uc5N6dImS58ywURA0FkqoHlg=="], - "lefthook-darwin-arm64": ["lefthook-darwin-arm64@2.1.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-VMUAqY81+8ph7Mc0iROd3JWqj2sOuK7SYDyd8ZgzYVKChCt+Nnz8ehd4gUwprJy7QI28IFSUh1M3wnIRiYh+fA=="], + "lefthook-darwin-arm64": ["lefthook-darwin-arm64@2.1.16", "", { "os": "darwin", "cpu": "arm64" }, "sha512-oOjS1mn3UGNfo72UtR9VJMyulgbjvLkv5KEmJVgfWiO8wZ4Z469o2ZllvH5FrHluzv4Hlkc9OG+vnCP+5/ztMA=="], - "lefthook-darwin-x64": ["lefthook-darwin-x64@2.1.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-uSmjcPRU+yB+D6vk+u6WJDnyHlB+XIv1QZw97N2+qNGG2bNSLH/7MuJ6puCdJv+lVPBEO5wBjE3JIbaEzPOQCw=="], + "lefthook-darwin-x64": ["lefthook-darwin-x64@2.1.16", "", { "os": "darwin", "cpu": "x64" }, "sha512-/thON8TbzsqXBjF0hkVPsisBJOKGfss0+lVqcJ0jNwaRDIQSn6Frsn7c9eqs7uyMNEQKUVLTeIkpwPrNldT8JA=="], - "lefthook-freebsd-arm64": ["lefthook-freebsd-arm64@2.1.14", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-po/pmjbp/7BjE9RFfeDnh+CeNsA0d+utppOYKB425I7mgI1GcmT8GAZ9DDut4DIutNjVqKL/lzrj+F87Gm40Rg=="], + "lefthook-freebsd-arm64": ["lefthook-freebsd-arm64@2.1.16", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-Eg7s0gcGwZnQqYyWTfOqb+2qupXtmHt/XHftXJvLhTaTMXR6uQbAyl7ceYTl2wH43AOCLrJ5BMMdqlWJaj/EbA=="], - "lefthook-freebsd-x64": ["lefthook-freebsd-x64@2.1.14", "", { "os": "freebsd", "cpu": "x64" }, "sha512-+KiXiFjJf7YdHUYjzhDsklcA5bAFZN+pNcz/NbBftrhVTwzNALNoK+SYEKt+9QJsrzL6tpDWmHHFpeLGp+8t8w=="], + "lefthook-freebsd-x64": ["lefthook-freebsd-x64@2.1.16", "", { "os": "freebsd", "cpu": "x64" }, "sha512-vCmyKLO8mtAf+hm8rpfAlt4wQ23S7AGnlT6JlrLPBSt5V8J2kXuI/N1W2XQbWx6OqckqchhBdLQP3Xh4IiYBXA=="], - "lefthook-linux-arm64": ["lefthook-linux-arm64@2.1.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-bjeJB16ftJaPWGTedssh8ZrqRy1ACfQuTV7a0O3NU1FoIsJtfHOm0o0mV4cPf4q+ZCVCwq1Xf2I11qUpuJFV4Q=="], + "lefthook-linux-arm64": ["lefthook-linux-arm64@2.1.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-zMEZ8x+PzXqjouEFsf2lWt2o0p5yvvT0YP0CpJC41y4FIRusx0sy3l+cjvKRIysLGOzKMr4V3/fE6w7f2KgwUw=="], - "lefthook-linux-x64": ["lefthook-linux-x64@2.1.14", "", { "os": "linux", "cpu": "x64" }, "sha512-gYExzjU2w3uKrP1MklbHENS1cXUdCQRIiEJkykI7q4RjhIHi+mQEUAZLxZw93a6jE2pNWaO0me5tB8EbEt4/GQ=="], + "lefthook-linux-x64": ["lefthook-linux-x64@2.1.16", "", { "os": "linux", "cpu": "x64" }, "sha512-HdfNz3z5hzrEXyP5KC51os+2M6BThL4qMfu8rB777tbXzf7WMoxf/FV6mnzoqM1RN0K+KJAQYbnJatfPQhqusQ=="], - "lefthook-openbsd-arm64": ["lefthook-openbsd-arm64@2.1.14", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MCW76gTlEMBF3Ds8O63UTxjaSguha/5zT0U3Vr719I1sXueSpttfRmm/5YfIB4MygB116yA/Vi+DsizT/X0kcg=="], + "lefthook-openbsd-arm64": ["lefthook-openbsd-arm64@2.1.16", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-zKkyN/ocUXOzFaLPn3ciucJ6CYcnUmqFsIwp7ge96sQ8l83omxL9rK0/KvSsMcus+TfHTNN+DFWVJxeMepPVtQ=="], - "lefthook-openbsd-x64": ["lefthook-openbsd-x64@2.1.14", "", { "os": "openbsd", "cpu": "x64" }, "sha512-salAc1PAhLI6xrB9pth5UxALQlTOGvZAfaAvgFwixhkdu8D//uW8zS3faFq31ktd1bDAy5wDI8p9yg+fF5+2uA=="], + "lefthook-openbsd-x64": ["lefthook-openbsd-x64@2.1.16", "", { "os": "openbsd", "cpu": "x64" }, "sha512-Ptu0D2JK1COlDK1lcr7F6WDGkCzQ80ozeTrqNzPEyBjUdZAFLwR5K5gWgaRtR06RSoYDC72RFxKM1QYnsfF0rQ=="], - "lefthook-windows-arm64": ["lefthook-windows-arm64@2.1.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-q3JC6lBrYLzkhXEhfnmFxJF47gSd5JeDweH3aNrHoLGX/KkRiOrk9RSyoRwSuGq03EYxSkJhl1FLj2GUxF7VqQ=="], + "lefthook-windows-arm64": ["lefthook-windows-arm64@2.1.16", "", { "os": "win32", "cpu": "arm64" }, "sha512-YTzFJwlF3qbHx+CCuiyDmDPASh6mYBp1I8zfTeGKI3L24Eoct6FUaGUlfAoz8SI3DECPmBhfNgAK4dZlX9Hd5g=="], - "lefthook-windows-x64": ["lefthook-windows-x64@2.1.14", "", { "os": "win32", "cpu": "x64" }, "sha512-WwIxdwW1lDAaJUU3ETcKlCtdqluGr3Cy+LjKvP4QIQnFf0FEXL78THstIkZ6k6qhfUn7/WvyqSIf0cbTAKw2nQ=="], + "lefthook-windows-x64": ["lefthook-windows-x64@2.1.16", "", { "os": "win32", "cpu": "x64" }, "sha512-mQVpfaagdIZPJCGDeedRnslyd9JTc13vjmp02uu4X7cr/VZK94obwk96dhgsRp9+LL1XMhX5zTcb0zNLo1hC+A=="], "lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="], @@ -493,6 +554,8 @@ "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + "smol-toml": ["smol-toml@1.9.1", "", {}, "sha512-j7WyPdf0CJoutMzMoPLF/bVXHtKZIT9Rb4eGeEbWw8X2t5oirjPgpL+OpaENZrILYaXqYwnVH+qgUwVbl+pUJQ=="], + "source-map-js": ["source-map-js@1.2.2", "", {}, "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw=="], "stdnum": ["stdnum@1.12.6", "", {}, "sha512-UhWlPQ0FCwMuNKEPvX42Hk0ddS/YivTvmJ8k7y0fxM0dea1Cx//AScKFNtT59lHCqYXIr3gfYI4EvqvH9DRrpQ=="], @@ -509,6 +572,8 @@ "tslib": ["tslib@2.4.1", "", {}, "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA=="], + "tsx": ["tsx@4.23.15", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw=="], + "typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="], "unconfig-core": ["unconfig-core@7.5.0", "", { "dependencies": { "@quansync/fs": "^1.0.0", "quansync": "^1.0.0" } }, "sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w=="], diff --git a/bunfig.toml b/bunfig.toml index 56d2cb1b..2a1b0849 100644 --- a/bunfig.toml +++ b/bunfig.toml @@ -3,7 +3,7 @@ # less than 5 days ago to mitigate supply chain attacks. minimumReleaseAge = 432_000 minimumReleaseAgeExcludes = [ - "bun-types", # quarantine-expires: 2026-10-15T05:29:43.845Z + "@stll/oxlint-plugin", # First-party tooling releases are exempt. "@stll/oxlint-config", # quarantine-excluded-since: 2026-06-03T14:49:06.000Z "@stll/typescript-config", # quarantine-excluded-since: 2026-06-03T14:49:06.000Z ] diff --git a/dylint.toml b/dylint.toml deleted file mode 100644 index 0966fea2..00000000 --- a/dylint.toml +++ /dev/null @@ -1,4 +0,0 @@ -[workspace.metadata.dylint] -libraries = [ - { git = "https://github.com/stella/tooling", rev = "fe7012b863ad2fcdd788a3fab759b31181bbf9c8", pattern = "rust-lints/*" }, -] diff --git a/oxlint.config.ts b/oxlint.config.ts index 57fa3d15..4b008ca6 100644 --- a/oxlint.config.ts +++ b/oxlint.config.ts @@ -1,15 +1,10 @@ import { library } from "@stll/oxlint-config"; +import { + portableSafetyPluginSpecifiers, + portableSafetyRules, +} from "@stll/oxlint-plugin"; export default library({ - ignorePatterns: ["scripts/rust-oracle/target/"], - overrides: [ - { - files: ["scripts/**"], - rules: { - "no-console": "off", - "typescript/no-unnecessary-condition": "off", - "typescript/strict-boolean-expressions": "off", - }, - }, - ], + jsPlugins: portableSafetyPluginSpecifiers, + rules: portableSafetyRules, }); diff --git a/package.json b/package.json index 11d73f9f..e3a045f9 100644 --- a/package.json +++ b/package.json @@ -32,11 +32,10 @@ "rust:fmt:check": "cargo ci-fmt", "rust:lint": "cargo ci-clippy", "rust:hawk": "cargo +1.97.1 hawk check --target-dir target/hawk -D warnings -A hawk::dead_public -A hawk::unnecessary_public", - "rust:dylint": "cargo ci-dylint", "rust:test": "cargo ci-test && cargo ci-test-doc", "rust:deny": "cargo ci-deny", "rust:snapshot-check": "cargo ci-snapshot", - "rust:check": "bun run rust:fmt && bun run rust:lint && bun run rust:dylint && bun run rust:test", + "rust:check": "bun run rust:fmt && bun run rust:lint && bun run rust:source-lint && bun run rust:test", "lint": "bun --bun oxlint -c oxlint.config.ts --report-unused-disable-directives-severity=error --deny-warnings --type-aware .", "lint:fix": "bun --bun oxlint -c oxlint.config.ts --type-aware --fix .", "format": "oxfmt . \"!.ai/**\" \"!.agents/**\" \"!.claude/**\" \"!AGENTS.md\" \"!GEMINI.md\"", @@ -51,14 +50,18 @@ "sync-ai:check": "bash scripts/sync-ai-skills.sh --check", "secrets:check:staged": "bash scripts/check-staged-secrets.sh", "hooks:install": "lefthook install", - "hooks:uninstall": "lefthook uninstall" + "hooks:uninstall": "lefthook uninstall", + "cloud:setup": "stll-cloud-setup", + "test:tooling": "bun test scripts/tooling-adoption.test.ts scripts/tooling-workflow.test.ts", + "rust:source-lint": "cargo ci-stella-lint" }, "devDependencies": { "@brazilian-utils/brazilian-utils": "^2.5.0", "@changesets/changelog-github": "^1.0.1", "@changesets/cli": "^3.0.3", - "@stll/oxlint-config": "0.10.0", - "@stll/typescript-config": "0.6.0", + "@stll/oxlint-config": "1.4.0", + "@stll/oxlint-plugin": "0.2.2", + "@stll/typescript-config": "0.8.0", "@types/node": "^26.6.5", "bun-types": "1.4.3", "fast-check": "^4.10.2", @@ -66,7 +69,7 @@ "iban": "^0.0.14", "ibantools": "^4.5.4", "jsvat": "^2.5.4", - "lefthook": "^2.1.14", + "lefthook": "2.1.16", "luhn": "^2.4.1", "oxfmt": "0.72.0", "oxlint": "1.87.0", diff --git a/packages/native-modules.d.ts b/packages/native-modules.d.ts new file mode 100644 index 00000000..91839f61 --- /dev/null +++ b/packages/native-modules.d.ts @@ -0,0 +1,4 @@ +declare module "*.node" { + const binding: unknown; + export = binding; +} diff --git a/packages/stdnum-wasm/tsconfig.json b/packages/stdnum-wasm/tsconfig.json index b01fa67f..797e3247 100644 --- a/packages/stdnum-wasm/tsconfig.json +++ b/packages/stdnum-wasm/tsconfig.json @@ -1,11 +1,10 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "ignoreDeprecations": "6.0", - "noEmit": true, "paths": { "@stll/stdnum/native": ["../stdnum/src/native.ts"] } }, - "include": ["src/**/*.ts"] + "include": ["src/**/*.ts"], + "files": [] } diff --git a/packages/stdnum/index.cjs b/packages/stdnum/index.cjs index 4f73fc3f..dfe8b1ae 100644 --- a/packages/stdnum/index.cjs +++ b/packages/stdnum/index.cjs @@ -11,10 +11,19 @@ // (guarded by test). // Mirrors detectLibc in src/native-node.ts; only glibc builds are published. -const isGlibc = () => - typeof process.report.getReport().header - .glibcVersionRuntime === "string"; +const isGlibc = () => { + const report = process.report.getReport(); + return ( + typeof report !== "string" && + "header" in report && + typeof report.header === "object" && + report.header !== null && + "glibcVersionRuntime" in report.header && + typeof report.header.glibcVersionRuntime === "string" + ); +}; +/** @param {string} packageName @param {unknown} cause */ const platformLoadError = (packageName, cause) => new Error( `${packageName}: ${cause instanceof Error ? cause.message : String(cause)}`, @@ -108,6 +117,7 @@ exports.loadNativeBinding = () => { try { return requirePlatformPackage(); } catch (error) { + /** @param {unknown} cause */ const describe = (cause) => cause instanceof Error ? cause.message diff --git a/packages/stdnum/src/__test__/native-node.test.ts b/packages/stdnum/src/__test__/native-node.test.ts index 6c796001..7343fc42 100644 --- a/packages/stdnum/src/__test__/native-node.test.ts +++ b/packages/stdnum/src/__test__/native-node.test.ts @@ -12,6 +12,7 @@ import { describe, expect, test } from "bun:test"; import { readFile } from "node:fs/promises"; +import { runInNewContext } from "node:vm"; import type { NativeStdnumBinding } from "../native"; import { @@ -155,3 +156,55 @@ describe("index.cjs static loader", () => { } }); }); + +test("CommonJS loader selects glibc only from a runtime report header", async () => { + const source = await readFile( + new URL("../../index.cjs", import.meta.url), + "utf8", + ); + for (const report of [ + "report.json", + {}, + { header: null }, + { header: {} }, + { header: { glibcVersionRuntime: 2 } }, + ]) { + const exports = { + loadNativeBinding: (): unknown => undefined, + }; + runInNewContext(source, { + exports, + process: { + platform: "linux", + arch: "x64", + report: { getReport: () => report }, + }, + require: requireNothing, + }); + expect(exports.loadNativeBinding).toThrow( + "No native stdnum binding is published for linux-x64", + ); + } + const exports = { + loadNativeBinding: (): unknown => undefined, + }; + const binding = {}; + runInNewContext(source, { + exports, + process: { + platform: "linux", + arch: "x64", + report: { + getReport: () => ({ + header: { glibcVersionRuntime: "2.39" }, + }), + }, + }, + require: (specifier: string) => { + if (specifier === "@stll/stdnum-linux-x64-gnu") + return binding; + return requireNothing(specifier); + }, + }); + expect(exports.loadNativeBinding()).toBe(binding); +}); diff --git a/packages/stdnum/tsconfig.json b/packages/stdnum/tsconfig.json index 3b312668..00e24846 100644 --- a/packages/stdnum/tsconfig.json +++ b/packages/stdnum/tsconfig.json @@ -1,12 +1,11 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "noEmit": true, "paths": { "#stdnum-runtime": ["./src/runtime.ts"] }, - "rootDir": "src", - "types": ["node", "bun-types"] + "types": ["bun-types"] }, - "include": ["src/**/*.ts"] + "include": ["src/**/*.ts"], + "files": [] } diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 0e9dcbcd..5a341ac1 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.96.0" +channel = "1.99.0" components = ["rustfmt", "clippy"] profile = "minimal" diff --git a/scripts/tooling-adoption.test.ts b/scripts/tooling-adoption.test.ts new file mode 100644 index 00000000..4af0ed1b --- /dev/null +++ b/scripts/tooling-adoption.test.ts @@ -0,0 +1,119 @@ +import { expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; + +import { library } from "@stll/oxlint-config"; +import { + portableSafetyPluginSpecifiers, + portableSafetyRules, +} from "@stll/oxlint-plugin"; + +import config from "../oxlint.config"; + +const read = (path: string) => + readFileSync( + new URL(`../${path}`, import.meta.url), + "utf8", + ); + +test("uses shared library defaults and the complete portable safety pack", () => { + expect(config).toEqual( + library({ + jsPlugins: portableSafetyPluginSpecifiers, + rules: portableSafetyRules, + }), + ); + for (const override of config.overrides ?? []) { + for (const [rule, severity] of Object.entries( + portableSafetyRules, + )) { + expect(override.rules?.[rule]).toBe(severity); + } + } + const tsconfig = JSON.parse(read("tsconfig.json")); + expect(tsconfig.extends).toBe( + "@stll/typescript-config/node.json", + ); + expect(tsconfig.compilerOptions).toBeUndefined(); + for (const workspace of ["stdnum", "stdnum-wasm"]) { + const workspaceConfig = JSON.parse( + read(`packages/${workspace}/tsconfig.json`), + ); + expect( + Object.keys(workspaceConfig.compilerOptions), + ).toEqual( + workspace === "stdnum" + ? ["paths", "types"] + : ["paths"], + ); + } +}); + +test("declares runtime-only generated cloud setup with Rust checks", () => { + const toolchain = JSON.parse(read("stll-toolchain.json")); + expect(toolchain.cloud.install).toBe( + "bun install --frozen-lockfile", + ); + expect(toolchain.cloud.rust.components).toEqual([ + "clippy", + "rustfmt", + ]); + expect(toolchain.cloud.rust.targets).toEqual([ + "wasm32-unknown-unknown", + ]); + const script = read(".agents/cloud-setup.sh"); + expect(script).toContain("Generated"); + expect(script).toContain(read(".node-version").trim()); + expect(script).toContain("install --frozen-lockfile"); + expect(script).not.toContain(".env.cloud"); + const settings = JSON.parse( + read(".claude/settings.json"), + ); + expect(settings.hooks.SessionStart).toHaveLength(1); +}); + +test("keeps third-party quarantine and exact first-party tooling exemptions", () => { + const bunfig = Bun.TOML.parse(read("bunfig.toml")); + expect(bunfig.install.minimumReleaseAge).toBe(432_000); + const exceptions = + bunfig.install.minimumReleaseAgeExcludes; + for (const name of [ + "@stll/oxlint-config", + "@stll/oxlint-plugin", + "@stll/typescript-config", + ]) { + expect(exceptions).toContain(name); + } + expect( + exceptions.every( + (name: string) => + name.startsWith("@stll/") && !name.includes("*"), + ), + ).toBe(true); +}); + +test("updates each Ruby and PHP oracle dependency root", () => { + const dependabot = read(".github/dependabot.yml"); + for (const [ecosystem, manifest] of [ + ["bundler", "Gemfile"], + ["composer", "composer.json"], + ]) { + const roots = [ + ...new Bun.Glob(`**/${manifest}`).scanSync({ + onlyFiles: true, + }), + ] + .filter((path) => !path.startsWith("node_modules/")) + .map( + (path) => `/${path.slice(0, -manifest.length - 1)}`, + ); + expect(roots.length).toBeGreaterThan(0); + const entry = dependabot + .split(` - package-ecosystem: ${ecosystem}\n`) + .at(1) + ?.split(" - package-ecosystem:") + .at(0); + expect(entry).toBeDefined(); + for (const root of roots) + expect(entry).toContain(`directory: ${root}\n`); + } +}); diff --git a/scripts/tooling-workflow.test.ts b/scripts/tooling-workflow.test.ts new file mode 100644 index 00000000..11574869 --- /dev/null +++ b/scripts/tooling-workflow.test.ts @@ -0,0 +1,75 @@ +import { expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; + +const workflow = readFileSync( + new URL("../.github/workflows/ci.yml", import.meta.url), + "utf8", +); + +test("uses committed history and shared detector to select workspace parity", () => { + const lint = workflow + .split(" lint:\n") + .at(1) + ?.split(" version-sync:") + .at(0); + expect(lint).toContain("fetch-depth: 0"); + expect(lint).toContain("persist-credentials: false"); + expect(lint).toContain( + 'stll-toolchain-changed --since "$BASE_REF"', + ); + expect(lint).toContain( + "github.event.pull_request.base.sha || github.event.before", + ); + expect(lint).toContain("'unavailable-toolchain-base'"); + expect(lint).toContain( + "if: steps.toolchain.outputs.changed == 'true'", + ); + expect(lint).toContain("bun run test:tooling"); + expect(lint).toContain("bun run check:toolchain"); + expect(lint).toContain("bun run lint"); + expect(lint).toContain("bun run format:check"); +}); + +test("replaces compiler-private Dylint with the pinned stable source checker", () => { + expect(workflow).toContain( + "--rev 0ad75da354a5efff11626f59dcf8e07a0d0aea48", + ); + expect(workflow).toContain("--locked stella_source_lint"); + expect(workflow).not.toContain("cargo-dylint"); + expect(workflow).not.toContain("dylint-link"); + expect(workflow).not.toContain("nightly-2026-04-16"); + const packageJson = JSON.parse( + readFileSync("package.json", "utf8"), + ); + expect(packageJson.scripts["rust:source-lint"]).toBe( + "cargo ci-stella-lint", + ); + expect(packageJson.scripts["rust:check"]).toContain( + "bun run rust:source-lint", + ); + const cargoConfig = readFileSync( + ".cargo/config.toml", + "utf8", + ); + expect(cargoConfig).toContain( + 'ci-stella-lint = "stella-lint ."', + ); + expect(cargoConfig).not.toContain("ci-dylint"); +}); + +test("keeps the Hawk install pin aligned with its authorized toolchain", () => { + const configuration = JSON.parse( + readFileSync("stll-toolchain.json", "utf8"), + ); + const installPins = [ + ...workflow.matchAll( + /rustup toolchain install (\d+\.\d+\.\d+) --profile minimal/g, + ), + ].map((match) => match[1]); + for (const { pin } of configuration.rustExceptions) { + expect(installPins).toContain(pin); + expect(workflow).toContain( + `HAWK_RUST_TOOLCHAIN: "${pin}"`, + ); + } +}); diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json new file mode 100644 index 00000000..69d3e27d --- /dev/null +++ b/scripts/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@stll/typescript-config/bun.json", + "include": ["**/*.ts", "**/*.mjs"] +} diff --git a/stll-toolchain.json b/stll-toolchain.json new file mode 100644 index 00000000..77363071 --- /dev/null +++ b/stll-toolchain.json @@ -0,0 +1,17 @@ +{ + "cloud": { + "install": "bun install --frozen-lockfile", + "rust": { + "components": ["clippy", "rustfmt"], + "targets": ["wasm32-unknown-unknown"] + } + }, + "rustExceptions": [ + { + "pin": "1.97.1", + "reason": "cargo-hawk compiler-private APIs require Rust 1.97.1; ordinary Rust checks use shared Rust 1.99.0.", + "tracking": "https://github.com/stella/tooling/issues/99", + "expires": "2026-11-11" + } + ] +} diff --git a/tsconfig.json b/tsconfig.json index c55f06cb..a06aa487 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,9 +1,12 @@ { - "extends": "@stll/typescript-config/library.json", - "compilerOptions": { - "types": ["node", "bun-types"], - "noEmit": true - }, - "include": ["oxlint.config.ts"], - "exclude": ["node_modules"] + "extends": "@stll/typescript-config/node.json", + "include": [ + "oxlint.config.ts", + "packages/*/index.cjs", + "packages/*/index.d.cts", + "packages/*/tsdown.config.ts", + "packages/native-modules.d.ts" + ], + "exclude": ["node_modules"], + "files": ["packages/stdnum/index.cjs"] }