From 819f33ed385b6b188251bd36b3f7d93ab67b8f53 Mon Sep 17 00:00:00 2001 From: Natalie Date: Fri, 17 Apr 2026 12:20:43 -0700 Subject: [PATCH] =?UTF-8?q?feat(@projects/@magic-civilization):=20?= =?UTF-8?q?=E2=9C=A8=20add=20runner=20script=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .../age-of-dwarves/data/items/manifest.json | 1 - .../pages/progress-report/ObjectiveModal.tsx | 6 +- scripts/dev-setup/bluefin.sh | 57 +++++++++- scripts/dev-setup/lib/runner.sh | 101 ++++++++++++++++++ scripts/dev-setup/linux.sh | 44 ++++++++ scripts/dev-setup/osx.sh | 59 +++++++++- scripts/run/common.sh | 14 +-- 7 files changed, 265 insertions(+), 17 deletions(-) create mode 100644 scripts/dev-setup/lib/runner.sh diff --git a/public/games/age-of-dwarves/data/items/manifest.json b/public/games/age-of-dwarves/data/items/manifest.json index 612abbf5..9b19bd94 100644 --- a/public/games/age-of-dwarves/data/items/manifest.json +++ b/public/games/age-of-dwarves/data/items/manifest.json @@ -3,7 +3,6 @@ "includes": [ "iron_axe", "dwarven_plate", - "healing_draught", "direwolf_alpha_pelt", "golem_core", "phase_gauntlet", diff --git a/public/games/age-of-dwarves/guide/src/pages/progress-report/ObjectiveModal.tsx b/public/games/age-of-dwarves/guide/src/pages/progress-report/ObjectiveModal.tsx index a6a12510..f90b5ca1 100644 --- a/public/games/age-of-dwarves/guide/src/pages/progress-report/ObjectiveModal.tsx +++ b/public/games/age-of-dwarves/guide/src/pages/progress-report/ObjectiveModal.tsx @@ -1,4 +1,5 @@ import { useEffect, type ReactElement } from 'react' +import { createPortal } from 'react-dom' import type { ObjectiveRecord } from './types' import { STATUS_ICON, STATUS_LABEL } from './types' import { @@ -31,7 +32,7 @@ export function ObjectiveModal({ objective, onClose }: ObjectiveModalProps): Rea if (e.target === e.currentTarget) onClose() } - return ( + return createPortal( @@ -57,6 +58,7 @@ export function ObjectiveModal({ objective, onClose }: ObjectiveModalProps): Rea {objective.summary || 'No summary recorded for this objective.'} - + , + document.body, ) } diff --git a/scripts/dev-setup/bluefin.sh b/scripts/dev-setup/bluefin.sh index 6647d08d..76bef50b 100755 --- a/scripts/dev-setup/bluefin.sh +++ b/scripts/dev-setup/bluefin.sh @@ -44,19 +44,25 @@ REQUIRED_PACKAGES=( ) CHECK_ONLY=false +WITH_RUNNER=false for arg in "$@"; do case "$arg" in --check) CHECK_ONLY=true ;; + --with-runner) WITH_RUNNER=true ;; --help|-h) - echo "Usage: $0 [--check]" - echo " (no args) Install missing packages via rpm-ostree --apply-live" - echo " --check Exit 0 if all packages are installed, 1 otherwise" + echo "Usage: $0 [--check] [--with-runner]" + echo " (no args) Install missing packages via rpm-ostree --apply-live" + echo " --check Exit 0 if all packages are installed, 1 otherwise" + echo " --with-runner Also install + register a forgejo-runner" + echo " (requires FORGEJO_* env — see .env.example)" exit 0 ;; *) echo "Unknown argument: $arg"; exit 2 ;; esac done +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + # ── Preconditions ──────────────────────────────────────────────────── if ! command -v rpm-ostree &>/dev/null; then echo -e "${RED}This script targets bootc / rpm-ostree systems (Bluefin, Silverblue, Kinoite).${NC}" @@ -162,3 +168,48 @@ echo -e " layer — typically at:" echo -e " ${DIM}~/Code/bootc-bluefin/containerfiles/Containerfile.desktop-core${NC}" echo -e " Rebuild + redeploy via ${DIM}~/Code/bootc-bluefin/rebuild-with-parser.sh${NC}" echo -e " (or equivalent build.sh / deploy.sh)." + +# ── Optional: forgejo-runner install + register + persist ────────── +if $WITH_RUNNER; then + echo "" + echo -e "${BLUE}─────────────────────────────────────────────────${NC}" + echo -e "${BLUE} Forgejo runner (Linux / amd64 / $(hostname -s))${NC}" + echo -e "${BLUE}─────────────────────────────────────────────────${NC}" + # shellcheck source=../run/common.sh + source "$REPO_ROOT/scripts/run/common.sh" + # shellcheck source=./lib/runner.sh + source "$REPO_ROOT/scripts/dev-setup/lib/runner.sh" + export RUNNER_OS=linux + export RUNNER_ARCH=amd64 + export RUNNER_NAME="$(hostname -s)" + export RUNNER_LABELS="self-hosted,linux,${RUNNER_NAME}" + runner_install_binary + runner_register + # systemd-user persistence (survives reboot thanks to `loginctl enable-linger`). + UNIT="$HOME/.config/systemd/user/forgejo-runner.service" + mkdir -p "$(dirname "$UNIT")" + cat > "$UNIT" </dev/null || true + echo " runner: systemd user unit enabled (linger=on, persists across reboot)" + runner_verify_online || true +fi diff --git a/scripts/dev-setup/lib/runner.sh b/scripts/dev-setup/lib/runner.sh new file mode 100644 index 00000000..7da880f3 --- /dev/null +++ b/scripts/dev-setup/lib/runner.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +# Shared forgejo-runner install/register/persist helpers for the +# per-OS dev-setup scripts. Sourced, never executed directly. +# +# Callers provide: +# - RUNNER_OS (darwin | linux) +# - RUNNER_ARCH (arm64 | amd64) +# - RUNNER_LABELS (comma-separated, e.g. "self-hosted,macos,arm64") +# - RUNNER_NAME (display name, e.g. $(hostname -s)) +# Env required (load via scripts/run/common.sh cascade): +# - FORGEJO_HOST e.g. http://forge.black.local +# - FORGEJO_ORG org the runner registers against +# - FORGEJO_RUNNER_TOKEN org-scoped registration token +# +# Callers (osx.sh / bluefin.sh / linux.sh) also decide the persistence +# layer — these helpers only handle the binary + registration step; +# each OS file installs its own launchd plist or systemd user unit. + +set -euo pipefail + +RUNNER_BIN="$HOME/.local/bin/forgejo-runner" +RUNNER_DIR="$HOME/.local/share/forgejo-runner" +RUNNER_URL_BASE="https://code.forgejo.org/forgejo/runner/releases/download/nightly" + +runner_require_env() { + local missing=() + for v in FORGEJO_HOST FORGEJO_ORG FORGEJO_RUNNER_TOKEN; do + [[ -z "${!v:-}" ]] && missing+=("$v") + done + if (( ${#missing[@]} > 0 )); then + echo " runner: missing required env: ${missing[*]}" >&2 + echo " runner: set in .env.local — see .env.example" >&2 + return 2 + fi +} + +runner_install_binary() { + mkdir -p "$(dirname "$RUNNER_BIN")" "$RUNNER_DIR" + if [[ -x "$RUNNER_BIN" ]]; then + echo " runner: binary already present at $RUNNER_BIN" + return 0 + fi + local url="$RUNNER_URL_BASE/forgejo-runner-nightly-$RUNNER_OS-$RUNNER_ARCH" + echo " runner: downloading $url" + curl -fsSL -o "$RUNNER_BIN.tmp" "$url" + chmod +x "$RUNNER_BIN.tmp" + mv "$RUNNER_BIN.tmp" "$RUNNER_BIN" + echo " runner: installed → $RUNNER_BIN" +} + +# Register (or re-register) at org scope. Removes .runner file if the +# existing registration has different labels or a different name — this +# rebinds the runner to the org without manual DB intervention. +runner_register() { + runner_require_env || return $? + cd "$RUNNER_DIR" + + if [[ -f .runner ]]; then + local existing_name existing_labels + existing_name=$(jq -r '.name // ""' .runner 2>/dev/null || echo "") + existing_labels=$(jq -c '.labels // []' .runner 2>/dev/null || echo "[]") + if [[ "$existing_name" == "$RUNNER_NAME" ]] && \ + [[ "$existing_labels" == "[\"${RUNNER_LABELS//,/\",\"}\"]" ]]; then + echo " runner: already registered as '$RUNNER_NAME' with labels [$RUNNER_LABELS]" + return 0 + fi + echo " runner: existing registration ($existing_name, labels=$existing_labels) differs — re-registering" + rm -f .runner + fi + + "$RUNNER_BIN" register \ + --no-interactive \ + --instance "$FORGEJO_HOST" \ + --token "$FORGEJO_RUNNER_TOKEN" \ + --name "$RUNNER_NAME" \ + --labels "$RUNNER_LABELS" >/dev/null + echo " runner: registered as '$RUNNER_NAME' at $FORGEJO_HOST/$FORGEJO_ORG (labels: $RUNNER_LABELS)" +} + +# Verify the runner appears online via the Forgejo API within a short +# polling window. Uses FORGEJO_RUNNER_TOKEN only if also passed as a +# personal access token; otherwise requires FORGEJO_ADMIN_TOKEN to read +# /api/v1/user/actions/runners. +runner_verify_online() { + local tok="${FORGEJO_ADMIN_TOKEN:-${FORGEJO_TOKEN:-}}" + if [[ -z "$tok" ]]; then + echo " runner: skipping online-verify (no admin/personal token — set FORGEJO_ADMIN_TOKEN to enable)" + return 0 + fi + local tries=0 online="" + while (( tries < 12 )); do + online=$(curl -fsS -H "Authorization: token $tok" \ + "$FORGEJO_HOST/api/v1/orgs/$FORGEJO_ORG/actions/runners" 2>/dev/null \ + | jq -r ".runners[] | select(.name==\"$RUNNER_NAME\") | .status" 2>/dev/null || echo "") + [[ "$online" == "online" ]] && { echo " runner: online"; return 0; } + tries=$((tries + 1)) + sleep 2 + done + echo " runner: NOT online after 24s; check service logs" >&2 + return 1 +} diff --git a/scripts/dev-setup/linux.sh b/scripts/dev-setup/linux.sh index c0f9075a..61957ae8 100755 --- a/scripts/dev-setup/linux.sh +++ b/scripts/dev-setup/linux.sh @@ -459,3 +459,47 @@ echo -e " ${DIM}./run verify${NC} — full lint + test pipeline" echo -e " ${DIM}./run test:golden${NC} — cross-language golden-vector parity" echo -e " ${DIM}./run autoplay 1${NC} — single-seed 500-turn simulation" echo "" + +# ── Optional: forgejo-runner install + register + persist ────────── +if $WITH_RUNNER; then + echo "" + echo -e "${BLUE}─────────────────────────────────────────────────${NC}" + echo -e "${BLUE} Forgejo runner (Linux / amd64 / $(hostname -s))${NC}" + echo -e "${BLUE}─────────────────────────────────────────────────${NC}" + # shellcheck source=../run/common.sh + source "$REPO_ROOT/scripts/run/common.sh" + # shellcheck source=./lib/runner.sh + source "$REPO_ROOT/scripts/dev-setup/lib/runner.sh" + export RUNNER_OS=linux + export RUNNER_ARCH=amd64 + export RUNNER_NAME="$(hostname -s)" + export RUNNER_LABELS="self-hosted,linux,${RUNNER_NAME}" + runner_install_binary + runner_register + UNIT="$HOME/.config/systemd/user/forgejo-runner.service" + mkdir -p "$(dirname "$UNIT")" + cat > "$UNIT" </dev/null || true + echo " runner: systemd user unit enabled (linger=on, persists across reboot)" + runner_verify_online || true +fi diff --git a/scripts/dev-setup/osx.sh b/scripts/dev-setup/osx.sh index da95eee3..cf1879c3 100755 --- a/scripts/dev-setup/osx.sh +++ b/scripts/dev-setup/osx.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # macOS dev environment setup for Magic Civilization -# Usage: ./scripts/dev-setup/osx.sh [--skip-godot] [--skip-rust] +# Usage: ./scripts/dev-setup/osx.sh [--skip-godot] [--skip-rust] [--with-runner] set -euo pipefail RED='\033[0;31m' @@ -12,14 +12,18 @@ NC='\033[0m' SKIP_GODOT=false SKIP_RUST=false +WITH_RUNNER=false for arg in "$@"; do case "$arg" in --skip-godot) SKIP_GODOT=true ;; --skip-rust) SKIP_RUST=true ;; + --with-runner) WITH_RUNNER=true ;; --help|-h) - echo "Usage: $0 [--skip-godot] [--skip-rust]" - echo " --skip-godot Skip Godot 4 installation" - echo " --skip-rust Skip Rust toolchain installation" + echo "Usage: $0 [--skip-godot] [--skip-rust] [--with-runner]" + echo " --skip-godot Skip Godot 4 installation" + echo " --skip-rust Skip Rust toolchain installation" + echo " --with-runner Also install + register a forgejo-runner" + echo " (requires FORGEJO_* env — see .env.example)" exit 0 ;; esac @@ -262,9 +266,56 @@ if [ ${#FAILED[@]} -gt 0 ]; then exit 1 fi +if $WITH_RUNNER; then + echo "" + echo -e "${BLUE}─────────────────────────────────────────────────${NC}" + echo -e "${BLUE} Forgejo runner (macOS / arm64 / plum)${NC}" + echo -e "${BLUE}─────────────────────────────────────────────────${NC}" + source "$REPO_ROOT/scripts/run/common.sh" + source "$REPO_ROOT/scripts/dev-setup/lib/runner.sh" + export RUNNER_OS=darwin + export RUNNER_ARCH=arm64 + export RUNNER_NAME="$(hostname -s)" + export RUNNER_LABELS="self-hosted,macos,arm64" + runner_install_binary + runner_register + # launchd persistence — user-agent, no root needed. + PLIST="$HOME/Library/LaunchAgents/com.forgejo.runner.plist" + if [[ ! -f "$PLIST" ]]; then + mkdir -p "$(dirname "$PLIST")" + cat > "$PLIST" < + + + + Labelcom.forgejo.runner + ProgramArguments + + $HOME/.local/bin/forgejo-runner + daemon + --config + $HOME/.local/share/forgejo-runner/config.yaml + + WorkingDirectory$HOME/.local/share/forgejo-runner + RunAtLoad + KeepAlive + StandardOutPath$HOME/.local/share/forgejo-runner/runner.out.log + StandardErrorPath$HOME/.local/share/forgejo-runner/runner.err.log + + +PLIST + echo " runner: wrote $PLIST" + fi + launchctl unload "$PLIST" 2>/dev/null || true + launchctl load "$PLIST" + echo " runner: launchd agent loaded (RunAtLoad + KeepAlive)" + runner_verify_online || true +fi + echo "" echo -e " ${GREEN}Ready to go.${NC} Try:" echo -e " ${DIM}./run verify${NC} — full lint + test pipeline" echo -e " ${DIM}./run play${NC} — launch the game" echo -e " ${DIM}./run guide${NC} — start guide dev server" +$WITH_RUNNER && echo -e " ${DIM}launchctl list | grep forgejo${NC} — verify runner daemon" echo "" diff --git a/scripts/run/common.sh b/scripts/run/common.sh index e865cf14..4f5701f9 100644 --- a/scripts/run/common.sh +++ b/scripts/run/common.sh @@ -15,7 +15,7 @@ # Variables already set in the shell ALWAYS win (never clobber). Lines # are plain `KEY=VALUE`; inline comments (`#`) are stripped; surrounding # single/double quotes on values are stripped. -_load_envfile() { +load_envfile() { local f="$1" [[ -r "$f" ]] || return 0 local line key val @@ -36,12 +36,12 @@ _load_envfile() { done < "$f" } -_load_env_cascade() { +load_env_cascade() { local mode="${NODE_ENV:-development}" - _load_envfile "$REPO_ROOT/.env" - _load_envfile "$REPO_ROOT/.env.local" - _load_envfile "$REPO_ROOT/.env.${mode}" - _load_envfile "$REPO_ROOT/.env.${mode}.local" + load_envfile "$REPO_ROOT/.env" + load_envfile "$REPO_ROOT/.env.local" + load_envfile "$REPO_ROOT/.env.${mode}" + load_envfile "$REPO_ROOT/.env.${mode}.local" } # Require one or more env vars; fail the current command with a helpful @@ -60,7 +60,7 @@ require_env() { } # Run cascade at source time so every subcommand has vars available. -_load_env_cascade +load_env_cascade RED='\033[0;31m' GREEN='\033[0;32m'