diff --git a/flake.nix b/flake.nix index 23aa1fc..08ce7a3 100644 --- a/flake.nix +++ b/flake.nix @@ -273,6 +273,20 @@ additionalBuildInputs = [ pkgs.git ]; }; + rainix-check-deploy-constants = mkTask { + name = "check-published-deploy-constants"; + body = '' + set -euo pipefail + exec ${./lib/check-published-deploy-constants.sh} "$@" + ''; + additionalBuildInputs = [ + pkgs.curl + pkgs.gnugrep + # cut/sort/tr in the version-parsing pipeline. + pkgs.coreutils + ]; + }; + rainix-rs-static = mkTask { name = "rainix-rs-static"; body = '' @@ -286,6 +300,7 @@ sol-tasks = [ rainix-sol-artifacts rainix-sol-single-contract + rainix-check-deploy-constants ]; rs-tasks = [ @@ -381,6 +396,7 @@ bats test/bats/task/subgraph-build.test.bats bats test/bats/task/subgraph-deploy-version.test.bats bats test/bats/task/sol-single-contract.test.bats + bats test/bats/task/check-published-deploy-constants.test.bats ''; additionalBuildInputs = [ pkgs.bats ] ++ sol-build-inputs ++ node-build-inputs; }; @@ -586,6 +602,7 @@ inherit rainix-sol-artifacts rainix-sol-single-contract + rainix-check-deploy-constants rainix-rs-static prettier-bundle sol-shell-test diff --git a/lib/check-published-deploy-constants.sh b/lib/check-published-deploy-constants.sh new file mode 100755 index 0000000..fbe13c7 --- /dev/null +++ b/lib/check-published-deploy-constants.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LicenseRef-DCL-1.0 +# SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +# +# Checks that every version published to the soldeer registry for a given +# package has a full suite of pinned deploy constants in the specified Solidity +# file. For each published version and each constant prefix, asserts that both +# a DEPLOYED_ADDRESS_ and DEPLOYED_CODEHASH_ constant exist (where +# is the version string with dots replaced by underscores). +# +# Usage: +# check-published-deploy-constants [ ...] +# +# Arguments: +# soldeer-package soldeer package name to query (e.g. "raindex") +# deploy-lib-path path to the Solidity file holding the constants +# prefix... one or more constant name prefixes +# +# Output for a well-formed invocation (always exits 0, so registry state never +# reds a consumer pipeline): +# OK every published version has its full constant suite +# MISSING: one or more expected constants are absent +# SKIP: nothing was verified; the reason distinguishes an +# unreachable registry from a reachable registry whose +# response yielded no parseable versions +# +# A malformed invocation (fewer than 3 arguments) is a caller bug, not a +# registry state: usage goes to stderr and the exit status is 1, so a miswired +# CI step fails loud instead of silently skipping forever. + +set -euo pipefail + +if [ "$#" -lt 3 ]; then + printf 'Usage: check-published-deploy-constants [...]\n' >&2 + exit 1 +fi + +package="$1" +lib="$2" +shift 2 + +if ! response=$(curl -fsS "https://api.soldeer.xyz/api/v1/revision?project_name=${package}" 2>/dev/null); then + printf 'SKIP: could not fetch published soldeer versions' + exit 0 +fi + +# grep exits non-zero on zero matches; that is the legitimate +# "registry reachable but no versions parsed" case (e.g. a package with no +# published releases yet), kept distinct from the connectivity SKIP above so +# parser/API drift is never masked as an unreachable registry. +versions=$( + printf '%s' "$response" \ + | grep -oE '"version":"[0-9][0-9.]*"' | cut -d'"' -f4 | sort -u +) || true + +if [ -z "$versions" ]; then + printf 'SKIP: no versions parsed from registry response' + exit 0 +fi + +missing="" +for v in $versions; do + suffix=$(printf '%s' "$v" | tr . _) + for p in "$@"; do + for kind in ADDRESS CODEHASH; do + name="${p}_${kind}_${suffix}" + grep -qE "constant ${name} =" "$lib" || missing="${missing} ${name}" + done + done +done + +if [ -n "$missing" ]; then + printf 'MISSING:%s' "$missing" +else + printf 'OK' +fi diff --git a/test/bats/devshell/sol-shell/sol-tasks.test.bats b/test/bats/devshell/sol-shell/sol-tasks.test.bats index 298eab0..510917e 100644 --- a/test/bats/devshell/sol-shell/sol-tasks.test.bats +++ b/test/bats/devshell/sol-shell/sol-tasks.test.bats @@ -8,3 +8,8 @@ run command -v rainix-sol-artifacts [ "$status" -eq 0 ] } + +@test "check-published-deploy-constants should be available on PATH" { + run command -v check-published-deploy-constants + [ "$status" -eq 0 ] +} diff --git a/test/bats/task/check-published-deploy-constants.test.bats b/test/bats/task/check-published-deploy-constants.test.bats new file mode 100644 index 0000000..3685225 --- /dev/null +++ b/test/bats/task/check-published-deploy-constants.test.bats @@ -0,0 +1,85 @@ +setup() { + TESTDIR="$(mktemp -d)" + # Stub curl on PATH so no test touches the network. STUB_CURL_FAIL simulates + # an unreachable registry; otherwise the stub prints the file named by + # STUB_CURL_PAYLOAD as the registry response. + mkdir -p "$TESTDIR/bin" + cat > "$TESTDIR/bin/curl" << 'EOF' +#!/usr/bin/env bash +if [ -n "${STUB_CURL_FAIL:-}" ]; then + exit 22 +fi +cat "$STUB_CURL_PAYLOAD" +EOF + chmod +x "$TESTDIR/bin/curl" + PATH="$TESTDIR/bin:$PATH" + SCRIPT="./lib/check-published-deploy-constants.sh" +} + +teardown() { + rm -rf "$TESTDIR" +} + +_write_payload() { + STUB_CURL_PAYLOAD="$TESTDIR/payload.json" + export STUB_CURL_PAYLOAD + cat > "$STUB_CURL_PAYLOAD" +} + +# The flake task execs the script's nix store copy directly, and the store +# canonicalizes the git file mode (644 -> 0444, 755 -> 0555), so the tracked +# file must carry the exec bit or the task dies with EACCES. +@test "script is executable" { + [ -x "$SCRIPT" ] +} + +@test "malformed invocation prints usage to stderr and exits 1" { + run "$SCRIPT" only-two-args "$TESTDIR/lib.sol" + [ "$status" -eq 1 ] + [[ "$output" == *"Usage: check-published-deploy-constants"* ]] +} + +@test "unreachable registry prints connectivity SKIP and exits 0" { + export STUB_CURL_FAIL=1 + run "$SCRIPT" somepkg "$TESTDIR/lib.sol" DEPLOYED + [ "$status" -eq 0 ] + [ "$output" = "SKIP: could not fetch published soldeer versions" ] +} + +@test "reachable registry with no parseable versions prints a distinct SKIP and exits 0" { + _write_payload << 'EOF' +{"data":[],"status":"success"} +EOF + run "$SCRIPT" somepkg "$TESTDIR/lib.sol" DEPLOYED + [ "$status" -eq 0 ] + [ "$output" = "SKIP: no versions parsed from registry response" ] +} + +@test "prints OK when every published version has its full constant suite" { + _write_payload << 'EOF' +{"data":[{"version":"1.0.0"},{"version":"1.2.3"}]} +EOF + cat > "$TESTDIR/lib.sol" << 'EOF' +address constant DEPLOYED_ADDRESS_1_0_0 = address(1); +bytes32 constant DEPLOYED_CODEHASH_1_0_0 = bytes32(0); +address constant DEPLOYED_ADDRESS_1_2_3 = address(2); +bytes32 constant DEPLOYED_CODEHASH_1_2_3 = bytes32(0); +EOF + run "$SCRIPT" somepkg "$TESTDIR/lib.sol" DEPLOYED + [ "$status" -eq 0 ] + [ "$output" = "OK" ] +} + +@test "prints MISSING with each absent constant name across prefixes and exits 0" { + _write_payload << 'EOF' +{"data":[{"version":"1.0.0"}]} +EOF + cat > "$TESTDIR/lib.sol" << 'EOF' +address constant OBV2_ADDRESS_1_0_0 = address(1); +bytes32 constant OBV2_CODEHASH_1_0_0 = bytes32(0); +address constant OBV3_ADDRESS_1_0_0 = address(2); +EOF + run "$SCRIPT" somepkg "$TESTDIR/lib.sol" OBV2 OBV3 + [ "$status" -eq 0 ] + [ "$output" = "MISSING: OBV3_CODEHASH_1_0_0" ] +}