Skip to content

Reject an impossible address locally, and name the sliders #151

Reject an impossible address locally, and name the sliders

Reject an impossible address locally, and name the sliders #151

Workflow file for this run

name: CI
# Deliberately small. `scripts/ci-local.sh` runs ruff, mypy, pytest, the build,
# and the fixture scan on the developer's machine before every commit, so
# running them again here on every push buys a second opinion on the same
# question at the cost of minutes per push.
#
# What remains is what a macOS/3.12 laptop genuinely cannot answer:
# - Linux and the oldest supported Python, on every push (cheap, and the
# version floor is where syntax mistakes surface);
# - Windows, the rest of the version matrix, Docker, Nix, and uvx, on demand.
#
# The on-demand set is not decoration. Windows caught a socket guard that broke
# the proactor event loop's self-pipe, and the Nix job is the only thing that
# has ever verified the flake --- it was written on a machine without Nix and
# was missing a keccak backend. Run `full` before tagging a release, after
# touching packaging, or when anything reaches for the network or the loopback
# server.
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
inputs:
full:
description: "Also run Windows, the version matrix, Docker, Nix, and uvx"
type: boolean
default: true
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
# Third-party actions are pinned to commit SHAs, not tags. A tag is a mutable
# pointer: whoever controls the repository can move v31 to different code, and
# that code runs with this workflow's token. GitHub's own actions/* are left on
# tags -- same reasoning would apply, but the trust anchor is the same platform
# already running the job.
permissions:
contents: read
env:
# Deterministic Hypothesis runs; a property test that fails only on Tuesdays
# is worse than no property test.
HYPOTHESIS_PROFILE: ci
PYTHONHASHSEED: "0"
jobs:
# The one job that runs on every push. Python 3.10 rather than 3.12 because
# the local gates already cover 3.12 --- this is the floor, and the floor is
# where a 3.12-only syntax feature shows up as a syntax error.
check:
name: lint, types & tests (linux, 3.10)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: "3.10"
cache: pip
- run: pip install -e ".[dev,all]"
- run: ruff check src tests
- run: ruff format --check src tests
- run: mypy
# Note the absence of --network. The suite blocks sockets from the moment
# tests/conftest.py is imported --- before collection, so module-level
# code and every fixture scope are covered too. This is the second line
# of defence, and the reason a green run here means something: a test
# that quietly fell back to a live fetch would pass on a laptop.
- run: pytest -q --tb=short
# ------------------------------------------------------------------ on demand
matrix:
name: pytest ${{ matrix.python }} / ${{ matrix.os }}
if: github.event_name == 'workflow_dispatch' && inputs.full
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- { python: "3.11", os: ubuntu-latest }
- { python: "3.12", os: ubuntu-latest }
- { python: "3.13", os: ubuntu-latest }
# Path handling and SQLite behaviour differ enough to be worth one
# run each. Windows is the one that has actually caught something.
- { python: "3.12", os: macos-latest }
- { python: "3.12", os: windows-latest }
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python }}
cache: pip
- run: pip install -e ".[dev,all]"
- run: pytest -q --tb=short
loopback:
name: local server (loopback only)
if: github.event_name == 'workflow_dispatch' && inputs.full
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: "3.12"
cache: pip
- run: pip install -e ".[dev,all]"
# These bind a socket on 127.0.0.1, so they carry the `network` marker and
# are deselected by default. They reach no remote host --- the marker is
# about binding --- and the security model they cover is the reason the
# browser extension is safe to run at all.
- run: pytest tests/unit/test_local_server.py -q --network
package:
name: build, docker, nix, uvx
if: github.event_name == 'workflow_dispatch' && inputs.full
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: "3.12"
cache: pip
- run: pip install build twine
- run: python -m build
- name: validate package metadata
run: twine check dist/*
- uses: actions/upload-artifact@v7
with:
name: dist
path: dist/
- run: docker build -t chainscope:ci .
# A forensics tool reads untrusted data all day and mounts a case
# directory; uid 0 is a bad trade for nothing.
- name: it does not run as root
run: |
uid=$(docker run --rm --entrypoint id chainscope:ci -u)
test "$uid" != "0" || { echo "container runs as root"; exit 1; }
# Both halves. BSC has no keyless source -- there is no public Blockscout
# instance for it -- so `doctor` must fail there; Ethereum does, since
# Blockscout needs no credential, so it must pass. Asserting only the
# first would be satisfied by a doctor that always fails.
- name: doctor reports the missing key rather than passing silently
run: |
if docker run --rm chainscope:ci doctor --chain bsc; then
echo "doctor exited 0 on BSC with no ETHERSCAN_API_KEY set"; exit 1
fi
docker run --rm chainscope:ci doctor --chain bsc 2>&1 | grep -q ADDRESS_HISTORY
- name: and passes where an unkeyed source covers it
run: docker run --rm chainscope:ci doctor --chain eth
- name: the end-to-end path works in the container
run: |
mkdir -p "$RUNNER_TEMP/case"
# As the invoking user. The image defaults to uid 1000 and a runner is
# 1001, so without this the bind mount is unwritable -- exactly what a
# user on any shared machine would hit.
docker run --rm --user "$(id -u):$(id -g)" \
-v "$RUNNER_TEMP/case:/case" chainscope:ci \
tag 0x28C6c06298d514Db089934071355E5743bf21d60 \
-l "Binance 14" -t cex -C high -s "ci smoke test"
test -f "$RUNNER_TEMP/case/.chainscope/store.db"
- name: the MCP agent answers a handshake over stdio
run: |
# The whole reply, then matched. An earlier version piped through
# `head -c 400` and grepped for "protocolVersion" -- the server's
# `instructions` string pushes that field past the cut, so a perfectly
# good handshake was reported as a failure. The container was fine;
# the assertion was written against an assumed shape.
reply=$(printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"ci","version":"1"}}}' \
| docker run --rm -i --entrypoint chainscope-mcp chainscope:ci)
echo "$reply" | grep -q '"result"'
echo "$reply" | grep -q '"protocolVersion"'
echo "$reply" | grep -q 'chainscope'
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31
with:
extra_nix_config: "experimental-features = nix-command flakes"
# The flake was written on a machine without Nix, so this is its only
# verification. If it breaks, the flake is wrong -- not the job.
- run: nix flake check --no-build
- run: nix build .#chainscope --print-build-logs
- run: ./result/bin/chainscope --help
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
# The zero-install path. If this breaks, the console scripts or the extras
# are wrong, and the first thing a new user tries fails.
- run: uvx --from '.[all]' chainscope --help