Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: CI

on:
pull_request:
workflow_dispatch:

jobs:
dry-run:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Install dependencies (Linux)
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y zstd stress

- name: Install dependencies (macOS)
if: runner.os == 'macOS'
run: brew install zstd stress

- name: Verify Makefile parses and dependency check passes
run: make -n up

# Linux-only: macOS runners lack Docker. The Darwin Makefile branch is
# already covered by dry-run; the live stack itself is OS-independent.
live-run:
needs: dry-run
runs-on: ubuntu-latest
timeout-minutes: 20

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

timeout-minutes: 20, but make up cold-builds stacks-core from source (stacks-node and stacks-signer) with no caching anywhere in the repo. A cold 2-core compile of those two binaries might run past 20 minutes so in practice this might time out during the build and never reaches make test.

We could add buildx cache-from/to: type=gha, raise the timeout to around 60, and add a concurrency: group (with linter.yml you now have two pull_request workflows and neither cancels an in-progress run). Or even better, we could pull a prebuilt stacks-core image, or actions/cache the binary keyed on the resolved commit SHA, and move the from-source build to a schedule or workflow_dispatch.

One more thing: the macOS leg only runs make -n, so the single OS-specific step, the zstd extract, never actually runs on a runner. A quick tar -xf docker/chainstate.tar.zstd into a temp dir on that leg would at least smoke-test the codec

steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Install dependencies
run: sudo apt-get update && sudo apt-get install -y zstd stress jq

- name: Boot network from chainstate archive
run: make up

- name: Wait for network to be live
run: |
for i in $(seq 1 30); do
if make test; then
echo "Network is live"
exit 0
fi
echo "Attempt $i/30 — not ready yet, sleeping 10s"
sleep 10
done
echo "Network did not become live within timeout"
exit 1

- name: Tear down
if: always()
run: make down-force
110 changes: 52 additions & 58 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,9 +1,34 @@
# List of binaries hacknet needs to function properly
COMMANDS := sudo tar zstd getent stress
$(foreach bin,$(COMMANDS),\
$(if $(shell command -v $(bin) 2> /dev/null),$(info),$(error Missing required dependency: `$(bin)`)))
# OS Detection and Cross-Platform Support
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Darwin)
OS := macos
export UID := $(shell id -u)
export GID := $(shell id -g)
# macOS: use sysctl for CPU count
STRESS_CORES ?= $(shell sysctl -n hw.ncpu)
# List of binaries hacknet needs to function properly
COMMANDS := sudo tar zstd stress

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jq is needed by make test and make monitor but isn't in COMMANDS, so a fresh Mac gets a bare jq: command not found instead of the friendly dependency error. CI already installs it by hand, so it's a real dependency:

Suggested change
COMMANDS := sudo tar zstd stress
COMMANDS := sudo tar zstd jq stress

Same for the Linux branch (sudo tar zstd getent jq stress).

# macOS Docker Desktop maps host UID into its VM; running BSD tar via sudo
# restores archive ownership and leaves bind-mount sources unwritable.
# Extract as the current user so everything lands user-owned.
TAR_EXTRACT := tar -xf
else
Comment thread
radu-stacks marked this conversation as resolved.
OS := linux
# Linux: use getent
export UID := $(shell getent passwd $$(whoami) | cut -d":" -f 3)
export GID := $(shell getent passwd $$(whoami) | cut -d":" -f 4)
# Linux: use /proc/cpuinfo for CPU count
STRESS_CORES ?= $(shell cat /proc/cpuinfo | grep processor | wc -l)
# List of binaries hacknet needs to function properly
COMMANDS := sudo tar zstd getent stress
TAR_EXTRACT := sudo tar --same-owner -xf
endif

# Verify required dependencies exist
$(foreach bin,$(COMMANDS),$(if $(shell command -v $(bin) 2> /dev/null),$(info),$(error Missing required dependency: `$(bin)`)))
TARGET := $(firstword $(MAKECMDGOALS))
PARAMS := $(filter-out $(TARGET),$(MAKECMDGOALS))

# Hardcode the chainstate dir if we're booting from genesis
ifeq ($(TARGET),up-genesis)
export CHAINSTATE_DIR := $(PWD)/docker/chainstate/genesis
Expand All @@ -12,9 +37,6 @@ ifeq ($(TARGET),genesis)
export CHAINSTATE_DIR := $(PWD)/docker/chainstate/genesis
endif

# UID and GID are not currently used, but may be later to ensure consistent file permissions
export UID := $(shell getent passwd $$(whoami) | cut -d":" -f 3)
export GID := $(shell getent passwd $$(whoami) | cut -d":" -f 4)
EPOCH := $(shell date +%s)
PWD = $(shell pwd)
# Set a unique project name (used for checking if the network is running)
Expand All @@ -26,22 +48,15 @@ SERVICES := $(shell CHAINSTATE_DIR="" docker compose -f docker/docker-compose.ym
# Pauses the bitcoin miner script. Default is set to nearly 1 trillion blocks
PAUSE_HEIGHT ?= 999999999999
# Used for the stress testing target. modifies how much cpu to consume for how long
STRESS_CORES ?= $(shell cat /proc/cpuinfo | grep processor | wc -l)
STRESS_TIMEOUT ?= 120

# Create the chainstate dir and extract an archive to it when the "up" target is used
$(CHAINSTATE_DIR): /usr/bin/tar /usr/bin/zstd
@if [ ! -d "$(CHAINSTATE_DIR)" ]; then \
mkdir -p $(CHAINSTATE_DIR)
@if [ "$(TARGET)" = "up" ]; then
if [ -f "$(CHAINSTATE_ARCHIVE)" ]; then
sudo tar --same-owner -xf $(CHAINSTATE_ARCHIVE) -C $(CHAINSTATE_DIR) || exit 1
else
@echo "Chainstate archive ($(CHAINSTATE_ARCHIVE)) not found. Exiting"
rm -rf $(CHAINSTATE_DIR)
exit 1
fi
fi
$(CHAINSTATE_DIR):
Comment thread
wileyj marked this conversation as resolved.
@if [ ! -d "$(CHAINSTATE_DIR)" ]; then mkdir -p $(CHAINSTATE_DIR) && \
if [ "$(TARGET)" = "up" ]; then \
[ -f "$(CHAINSTATE_ARCHIVE)" ] && $(TAR_EXTRACT) $(CHAINSTATE_ARCHIVE) -C $(CHAINSTATE_DIR) || \
{ echo "Chainstate archive ($(CHAINSTATE_ARCHIVE)) not found. Exiting"; rm -rf $(CHAINSTATE_DIR); exit 1; }; \
fi; \
Comment thread
radu-stacks marked this conversation as resolved.
fi

# Build the images with a cache if present
Expand Down Expand Up @@ -70,33 +85,26 @@ check-not-running:

# If the network is not running, we need to exit (ex: trying to restart a container)
check-running:
@if test ! `docker compose ls --filter name=$(PROJECT) -q`; then \
echo "Network not running. exiting"; \
exit 1; \
fi
@test `docker compose ls --filter name=$(PROJECT) -q` || { echo "Network not running. exiting"; exit 1; }

# For targets that need an arg, check that *something* is provided. it not, exit
# For targets that need an arg, check that *something* is provided. if not, exit
check-params: | check-running
@if [ ! "$(PARAMS)" ]; then \
echo "No service defined. Exiting"; \
exit 1; \
fi
@[ "$(PARAMS)" ] || { echo "No service defined. Exiting"; exit 1; }

# Boot the network from a local chainstate archive
up: check-not-running | build $(CHAINSTATE_DIR)
@echo "Starting $(PROJECT) network from chainstate archive"
@echo " OS: $(OS)"
Comment thread
radu-stacks marked this conversation as resolved.
@echo " Chainstate Dir: $(CHAINSTATE_DIR)"
@echo " Chainstate Archive: $(CHAINSTATE_ARCHIVE)"
echo "$(CHAINSTATE_DIR)" > .current-chainstate-dir
docker compose -f docker/docker-compose.yml --profile default -p $(PROJECT) up -d

# Boot the network from genesis
genesis: check-not-running | build $(CHAINSTATE_DIR) /usr/bin/sudo
genesis: check-not-running | build $(CHAINSTATE_DIR)
@echo "Starting $(PROJECT) network from genesis"
@if [ -d "$(CHAINSTATE_DIR)" ]; then \
echo " Removing existing genesis chainstate dir: $(CHAINSTATE_DIR)"; \
sudo rm -rf $(CHAINSTATE_DIR); \
fi
@echo " OS: $(OS)"
@[ -d "$(CHAINSTATE_DIR)" ] && { echo " Removing existing genesis chainstate dir: $(CHAINSTATE_DIR)"; sudo rm -rf $(CHAINSTATE_DIR); }
@echo " Chainstate Dir: $(CHAINSTATE_DIR)"
mkdir -p "$(CHAINSTATE_DIR)"
echo "$(CHAINSTATE_DIR)" > .current-chainstate-dir
Expand All @@ -117,9 +125,7 @@ down-prom:
down: backup-logs current-chainstate-dir
@echo "Shutting down $(PROJECT) network"
docker compose -f docker/docker-compose.yml --profile default -p $(PROJECT) down
@if [ -f .current-chainstate-dir ]; then \
rm -f .current-chainstate-dir
fi
@[ -f .current-chainstate-dir ] && rm -f .current-chainstate-dir

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both down and down-force now end with @[ -f .current-chainstate-dir ] && rm -f .current-chainstate-dir. With the global .ONESHELL, the recipe's exit status is whatever the last command returned, and [ -f ] returns 1 when the file isn't there, so the target fails with Error 1. The old if [ -f ]; then rm -f; fi always returned 0. I reproduced it on Make 4.4.1.

It bites in the two spots where you'd least want it to. make down-force on an already-stopped network errors out, and that's the stuck-state recovery the README points people to. make clean runs down-force as a prerequisite, so it aborts before sudo rm -rf ./docker/chainstate/*: the containers stop but the chainstate never gets cleaned.

rm -f already does nothing on a missing file, so the guard isn't buying anything. Same fix on both lines:

Suggested change
@[ -f .current-chainstate-dir ] && rm -f .current-chainstate-dir
@rm -f .current-chainstate-dir


# Secondary name to bring down the genesis network
down-genesis: down
Expand All @@ -128,9 +134,7 @@ down-genesis: down
down-force:
@echo "Force Shutting down $(PROJECT) network"
docker compose -f docker/docker-compose.yml --profile default -p $(PROJECT) down
@if [ -f .current-chainstate-dir ]; then \
rm -f .current-chainstate-dir
fi
@[ -f .current-chainstate-dir ] && rm -f .current-chainstate-dir

# Stream specified service logs to STDOUT. Does not validate if PARAMS is supplied
log: current-chainstate-dir
Expand All @@ -142,31 +146,21 @@ log-all: current-chainstate-dir
docker compose -f docker/docker-compose.yml --profile default -p $(PROJECT) logs -t -f

# Backup all service logs to $ACTIVE_CHAINSTATE_DIR/logs/<service-name>.log
backup-logs: current-chainstate-dir /usr/bin/sudo
backup-logs: current-chainstate-dir
@if [ -f .current-chainstate-dir ]; then \
$(eval ACTIVE_CHAINSTATE_DIR=$(shell cat .current-chainstate-dir))
if [ ! -d "$(ACTIVE_CHAINSTATE_DIR)" ]; then \
echo "Chainstate Dir ($(ACTIVE_CHAINSTATE_DIR)) not found";\
exit 1; \
fi; \
if [ ! -d "$(ACTIVE_CHAINSTATE_DIR)/logs" ]; then \
mkdir -p $(ACTIVE_CHAINSTATE_DIR)/logs;\
fi; \
$(eval ACTIVE_CHAINSTATE_DIR=$(shell cat .current-chainstate-dir)) \
[ -d "$(ACTIVE_CHAINSTATE_DIR)" ] || { echo "Chainstate Dir ($(ACTIVE_CHAINSTATE_DIR)) not found"; exit 1; }; \
mkdir -p $(ACTIVE_CHAINSTATE_DIR)/logs; \
echo "Backing up logs to $(ACTIVE_CHAINSTATE_DIR)/logs"; \
for service in $(SERVICES); do \
docker logs -t $$service > $(ACTIVE_CHAINSTATE_DIR)/logs/$$service.log 2>&1; \
done; \
for service in $(SERVICES); do docker logs -t $$service > $(ACTIVE_CHAINSTATE_DIR)/logs/$$service.log 2>&1; done; \
fi

# Replace the existing chainstate archive. Will be used with target `up`
snapshot: current-chainstate-dir down
@echo "Creating $(PROJECT) chainstate snapshot from $(ACTIVE_CHAINSTATE_DIR)"
@if [ -d "$(ACTIVE_CHAINSTATE_DIR)/logs" ]; then \
rm -rf $(ACTIVE_CHAINSTATE_DIR)/logs; \
fi
@[ -d "$(ACTIVE_CHAINSTATE_DIR)/logs" ] && rm -rf $(ACTIVE_CHAINSTATE_DIR)/logs
@echo "Creating snapshot: $(CHAINSTATE_ARCHIVE)"
@echo "cd $(ACTIVE_CHAINSTATE_DIR); sudo tar --zstd -cf $(CHAINSTATE_ARCHIVE) *; cd $(PWD)"
cd $(ACTIVE_CHAINSTATE_DIR); sudo tar --zstd -cf $(CHAINSTATE_ARCHIVE) *; cd $(PWD)
(cd $(ACTIVE_CHAINSTATE_DIR) && sudo tar --zstd -cf $(CHAINSTATE_ARCHIVE) *)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The no-sudo tar -xf extract is the right call, but snapshot still hardcodes sudo tar, so on macOS it writes a root-owned archive: the same ownership problem the extract change was meant to avoid. Worth gating it by OS the same way, e.g. a TAR_CREATE that drops sudo on macOS, used here. (--zstd is fine, bsdtar shells out to the zstd binary.)


# Pause all services in the network (netork is down, but recoverably with target 'unpause')
pause:
Expand Down Expand Up @@ -220,5 +214,5 @@ monitor:
clean: down-force
sudo rm -rf ./docker/chainstate/*

.PHONY: build build-no-cache current-chainstate-dir check-not-running check-running check-params up genesis up-genesis down down-genesis down-force log log-all backup-logs snapshot pause unpause stop start restart stress test monitor clean
.PHONY: build build-no-cache current-chainstate-dir check-not-running check-running check-params up genesis up-genesis down down-genesis down-force log log-all backup-logs snapshot pause unpause stop start restart stress test monitor clean up-prom down-prom
.ONESHELL: all-in-one-shell
Loading
Loading