Files
buh/.gitea/workflows/deploy.yml
rob thijssen 9d5919b119
Some checks failed
deploy / Build buh-api + buh-cli (static musl) (push) Waiting to run
deploy / Deploy buh node to slartibartfast (push) Has been cancelled
feat(deploy): stake BUH_NODE_PORT on 31415 + testnet peering doc
Settle buh's standardised ingress port on 31415/tcp — unassigned in IANA,
clear of the crowded 8443/9443/6443 appliance space, and below the Linux
ephemeral range so it's stable to forward from the edge. Updated the code
default (config.rs node_bind), the CI workflow env, firewalld service,
operator-model deploy.sh/manifest.yml, and the asset readme.

Also drop the SELinux `semanage port` step (and its two sudoers rules):
buh-api runs as unconfined_service_t and may bind any port, so BUH_NODE_PORT
needs no http_port_t label. The firewalld rules are keyed on the service
NAME (buh-node), not the port — so a port change is now purely repo-side and
needs no infra-setup re-run on the host.

Add doc/testnet.md: the per-node-CA trust model, the 31415 convention, a
node roster (incl. the slarti dogfood node + its CA fingerprint), and the
mutual trust / peer ping workflow for joining the testnet.

Gate: cargo fmt + clippy -D warnings clean; config renders node_bind
0.0.0.0:31415; sudoers passes visudo -c; deploy.yml valid YAML.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EB3LjarCdXxqrJ4tFLn8LB
2026-06-30 18:05:45 +03:00

215 lines
9.3 KiB
YAML

name: deploy
# Build and roll out the buh dogfooding node on a push to main:
# - buh-api (the relay/blob node daemon, static musl) -> slartibartfast
# - buh-cli (the operator admin tool, static musl) -> slartibartfast
#
# This workflow is the source of infra truth (host, ports, paths live in `env`
# below — there is no manifest.yml in this model; the operator-driven
# asset/deploy.sh + asset/manifest.yml remain for the §7 model). A Gitea Actions
# runner deploys as the dedicated `gitea_ci` user over SSH with scoped sudo
# (asset/sudoers.d/node-host.conf + script/infra-setup.sh).
#
# buh is its own CA: there is NO central PKI, NO host mTLS cert to provision, and
# NO cert.path/reload unit — the node generates its CA under /var/lib/buh/pki on
# first start and rotates its TLS leaf in process (the per-node-CA deviation,
# doc/design.md §5.1). So unlike the postgres-mTLS apps, this deploy ships no
# certificate material.
#
# The only edge port is BUH_NODE_PORT (PQ-mTLS ingress); the loopback admin API
# (127.0.0.1:8081) is never exposed and is what the health probe hits.
#
# Secrets (Gitea -> repo -> Settings -> Actions -> Secrets):
# RSYNC_SSH_KEY private SSH key whose pubkey script/infra-setup.sh installed
# (buh has no app secrets: the datastore is an embedded Turso file, the blob role
# uses the local filesystem, and the node mints its own CA.)
#
# One-time per-host provisioning (gitea_ci user, authorized_keys, scoped sudoers)
# is done by script/infra-setup.sh — run it once before this workflow can succeed.
on:
push:
branches: [main]
workflow_dispatch:
# Serialize deploys; never cancel an in-flight one (a half-applied rollout is
# worse than a slightly stale one).
concurrency:
group: deploy
cancel-in-progress: false
env:
# --- infra truth ---
NODE_HOST: slartibartfast.kosherinata.internal
NODE_PORT: "31415" # BUH_NODE_PORT — buh's staked port; the only edge port (PQ-mTLS ingress)
ADMIN_PORT: "8081" # loopback operator admin API (health probe target)
NODE_SANS: '["slartibartfast.kosherinata.internal"]' # TOML array for the leaf SANs
MUSL_TARGET: x86_64-unknown-linux-musl
DEPLOY_KEY: |
${{ secrets.RSYNC_SSH_KEY }}
jobs:
build:
name: Build buh-api + buh-cli (static musl)
runs-on: rust
steps:
- uses: actions/checkout@v4
# The `rust` runner image provides cargo/clippy/rustfmt, musl-gcc, and the
# x86_64-unknown-linux-musl std. No package installs at run time (§5).
#
# NOTE: buh links aws-lc-rs (X25519MLKEM768) — building it for musl needs
# cmake + a musl-targeting C compiler in the runner image. If the image
# lacks them, add them there (§5) rather than installing at run time; the
# glibc fallback (build on a runner no newer than the target, §6) is the
# alternative if a static musl aws-lc-rs proves impractical.
- name: Lint + test (deploy gate)
run: |
cargo fmt --check --all
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace
- name: Build buh-api + buh-cli (static musl release, fs blob — no s3 feature)
run: cargo build --release --target "${MUSL_TARGET}" -p buh-api -p buh-cli
- name: Stage binaries
run: |
mkdir --parents artifacts
cp "target/${MUSL_TARGET}/release/buh-api" artifacts/buh-api
cp "target/${MUSL_TARGET}/release/buh-cli" artifacts/buh-cli
- uses: actions/upload-artifact@v3
with: { name: buh-binaries, path: artifacts, retention-days: 1 }
deploy-node:
name: Deploy buh node to slartibartfast
needs: build
runs-on: fedora-44
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v3
with: { name: buh-binaries, path: artifact }
- name: SSH init
run: |
mkdir -p ~/.ssh
echo "${DEPLOY_KEY}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=accept-new \
gitea_ci@"${NODE_HOST}" 'hostname -f'
- name: Render config.toml from infra truth
run: |
mkdir -p rendered
# No secrets in buh's config; render the committed template with the
# infra-truth values (literal substitution so an array/path can't be
# re-split by the shell).
python3 - <<'PY'
import os
node_port = os.environ["NODE_PORT"]
admin_port = os.environ["ADMIN_PORT"]
subs = {
"BIND": "127.0.0.1:8080", # plain fallback; ignored when pki is enabled
"DB_PATH": "/var/lib/buh/relay.db",
"LOG_FORMAT": "json",
"DEFAULT_TTL_SECONDS": "604800",
"MAX_TTL_SECONDS": "2592000",
"MAX_PAYLOAD_BYTES": "262144",
"MAX_PULL_LIMIT": "100",
"MAX_WAIT_SECONDS": "30",
"SWEEP_INTERVAL_SECONDS": "3600",
"BLOB_ENABLED": "true",
"BLOB_BACKEND": "fs",
"BLOB_FS_ROOT": "/var/lib/buh/blobs",
"MAX_BLOB_BYTES": "67108864",
"S3_ENDPOINT": "",
"S3_REGION": "us-east-1",
"S3_ACCESS_KEY": "",
"S3_SECRET_KEY": "",
"PKI_ENABLED": "true",
"PKI_DIR": "/var/lib/buh/pki",
"NODE_BIND": f"0.0.0.0:{node_port}",
"PKI_SANS": os.environ["NODE_SANS"],
"LEAF_TTL_HOURS": "48",
"ROTATE_EVERY_HOURS": "24",
"ADMIN_ENABLED": "true",
"ADMIN_BIND": f"127.0.0.1:{admin_port}",
}
t = open("asset/config/config.toml.tmpl").read()
for k, v in subs.items():
t = t.replace("{{%s}}" % k, v)
open("rendered/config.toml", "w").write(t)
PY
- name: Provision service account + directories
run: |
# --mkpath: /etc/sysusers.d and /etc/firewalld/services don't exist by
# default on Fedora (only the /usr/lib variants ship).
rsync -az --mkpath --rsync-path='sudo rsync' \
asset/systemd/buh.sysusers.conf \
gitea_ci@"${NODE_HOST}":/etc/sysusers.d/buh.conf
ssh gitea_ci@"${NODE_HOST}" '
set -euo pipefail
sudo /usr/bin/systemd-sysusers
sudo /usr/bin/install -d -o root -g buh -m 0750 /etc/buh
sudo /usr/bin/install -d -o buh -g buh -m 0750 /var/lib/buh'
- name: Sync binaries, config, unit, firewalld service
run: |
rsync -az --mkpath --rsync-path='sudo rsync' --chown=root:root --chmod=0755 \
artifact/buh-api gitea_ci@"${NODE_HOST}":/usr/local/bin/buh-api
rsync -az --mkpath --rsync-path='sudo rsync' --chown=root:root --chmod=0755 \
artifact/buh-cli gitea_ci@"${NODE_HOST}":/usr/local/bin/buh-cli
rsync -az --mkpath --rsync-path='sudo rsync' --chown=root:buh --chmod=0640 \
rendered/config.toml gitea_ci@"${NODE_HOST}":/etc/buh/config.toml
rsync -az --mkpath --rsync-path='sudo rsync' --chown=root:root --chmod=0644 \
asset/systemd/buh-node.service \
gitea_ci@"${NODE_HOST}":/etc/systemd/system/buh-node.service
rsync -az --mkpath --rsync-path='sudo rsync' --chown=root:root --chmod=0644 \
asset/firewalld/buh-node.xml \
gitea_ci@"${NODE_HOST}":/etc/firewalld/services/buh-node.xml
- name: Apply SELinux contexts + firewalld (idempotent)
run: |
ssh gitea_ci@"${NODE_HOST}" '
set -euo pipefail
sudo /usr/sbin/restorecon -R /usr/local/bin/buh-api /usr/local/bin/buh-cli /etc/buh /var/lib/buh
# No SELinux port labeling: buh-api runs as unconfined_service_t (a plain
# systemd unit with no policy module), which may bind any port — so
# BUH_NODE_PORT needs no http_port_t entry. Keeping it out means a port
# change is purely repo-side: the firewalld rules below are keyed on the
# service NAME (buh-node), and the freshly-shipped buh-node.xml carries the
# new port, so a reload picks it up with no sudoers/infra-setup change.
sudo /usr/bin/firewall-cmd --reload
if ! sudo /usr/bin/firewall-cmd --query-service=buh-node; then
sudo /usr/bin/firewall-cmd --add-service=buh-node --permanent
fi
sudo /usr/bin/firewall-cmd --reload'
- name: Restart buh-node
run: |
ssh gitea_ci@"${NODE_HOST}" '
set -euo pipefail
sudo /usr/bin/systemctl daemon-reload
sudo /usr/bin/systemctl enable buh-node.service
sudo /usr/bin/systemctl restart buh-node.service'
- name: Health probe (loopback admin API) + report CA fingerprint
run: |
for i in 1 2 3 4 5 6 7 8 9 10; do
if info=$(ssh gitea_ci@"${NODE_HOST}" "curl -fsS http://127.0.0.1:${ADMIN_PORT}/admin/info"); then
echo "buh node healthy: ${info}"
echo "Share this node's CA fingerprint with peers to be trusted."
exit 0
fi
sleep 2
done
echo "buh node did not become healthy" >&2
exit 1
- name: Capture startup journal
if: always()
run: |
sleep 3
ssh gitea_ci@"${NODE_HOST}" 'journalctl --unit buh-node.service --no-pager -n 200'