CI: only the mod on push (1 job, low priority); the game on demand, refused when the disk is low
build / mod (push) Successful in 5m9s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-29 12:01:08 +03:00
parent 2b5ad8d888
commit 1ba57ee465
14 changed files with 190 additions and 63 deletions
+15 -44
View File
@@ -1,6 +1,7 @@
# Сборка на каждый пуш: мод (.hmod, для всех ОС) и игра-форк для Linux, в одной задаче
# (без передачи артефактов между задачами). Пуш в main публикует релиз build-<номер>.
# Сборки для Windows и macOS — desktop.yml (вручную).
# На каждый пуш: только мод (.hmod, один для всех ОС) и релиз build-<номер> с ним.
# Раннер живёт на одной машине с Gitea и другими сервисами (2 ядра, 4 ГБ): сборка мода лёгкая —
# один поток компиляции, низший приоритет, без системных пакетов и без кэша target.
# Игра-форк собирается отдельно и только вручную: game.yml (Linux) и desktop.yml (macOS/Windows).
name: build
on:
@@ -9,55 +10,28 @@ on:
env:
CARGO_TERM_COLOR: never
# The runner shares its machine with Gitea: two compile jobs at a time, no incremental
# files, so the build doesn't starve the server (Gitea answered 500 under a full build).
CARGO_BUILD_JOBS: "2"
CARGO_BUILD_JOBS: "1"
CARGO_INCREMENTAL: "0"
# Lowest CPU priority for the long steps (ionice too where the image has it, below).
LOW: nice -n 19
jobs:
build:
mod:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Системные библиотеки
run: |
SUDO=$(command -v sudo || true)
$SUDO apt-get update -qq
$SUDO apt-get install -y -qq build-essential pkg-config libasound2-dev libudev-dev
- name: Низкий приоритет
run: command -v ionice >/dev/null && echo "LOW=nice -n 19 ionice -c 3" >> "$GITHUB_ENV" || true
- name: Rust
run: |
curl -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --target wasm32-unknown-unknown
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
# Only the small things are cached: the build's target folder is gigabytes, and moving it
# through the cache server timed out and loaded the machine.
# Инструмент упаковки (cargo-horizon-mod без пробного запуска) — единственное, что стоит кэшировать.
- uses: actions/cache@v3
continue-on-error: true
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
key: cargo-${{ hashFiles('HORIZON_COMMIT') }}
restore-keys: cargo-
path: ~/.cargo/bin/cargo-horizon-mod
key: tool-${{ hashFiles('HORIZON_COMMIT', 'horizon-patches/*') }}
- name: Форк Horizon
run: $LOW python3 tools/airdef.py fork
run: nice -n 19 python3 tools/airdef.py fork
- name: Мод
run: $LOW python3 tools/airdef.py mod
- name: Игра
run: $LOW python3 tools/airdef.py game
- name: Архивы
run: |
mkdir -p out
cp airdef-mod/target/horizon-mod/airdef.hmod out/airdef.hmod
D=airdef-linux-x86_64
mkdir -p "$D"
cp horizon/target/fast/horizon-client horizon/target/fast/horizon-ctl out/airdef.hmod dist/play.sh docs/INSTALL.md "$D/"
tar czf "out/$D.tar.gz" "$D"
ls -la out
run: nice -n 19 python3 tools/airdef.py mod
- name: Релиз
if: github.ref == 'refs/heads/main'
env:
@@ -67,17 +41,14 @@ jobs:
SHA: ${{ github.sha }}
run: |
set -e
BODY=$(printf 'Сборка %s (%s).\n\n- airdef.hmod — мод, для всех ОС;\n- airdef-linux-x86_64.tar.gz — игра-форк Horizon для Linux с модом и play.sh.\n\nСборки для Windows и macOS добавляет workflow desktop (запуск вручную с этим тегом).\nКак запускать: docs/INSTALL.md.' "$TAG" "$SHA")
mkdir -p out && cp airdef-mod/target/horizon-mod/airdef.hmod out/airdef.hmod
BODY=$(printf 'Сборка %s (%s).\n\n- airdef.hmod — мод, для всех ОС.\n\nИгра-форк Horizon (нужна для мода): собрать у себя — python3 tools/airdef.py all, или запустить вручную workflow game (Linux) / desktop (macOS, Windows) с этим тегом.\nКак запускать: docs/INSTALL.md.' "$TAG" "$SHA")
DATA=$(BODY="$BODY" python3 -c 'import json,os;print(json.dumps({"tag_name":os.environ["TAG"],"target_commitish":os.environ["SHA"],"name":os.environ["TAG"],"body":os.environ["BODY"]}))')
if [ -z "$TOKEN" ]; then echo "::error::нет токена: добавь секрет RELEASE_TOKEN"; exit 1; fi
CODE=$(curl -sS -o release.json -w '%{http_code}' -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" "$API/releases" -d "$DATA")
if [ "$CODE" != "201" ]; then
echo "::error::Gitea не создала релиз (HTTP $CODE): $(cat release.json). Встроенному токену может не хватать прав — добавь секрет RELEASE_TOKEN (токен с правом записи в репозиторий)."
echo "::error::Gitea не создала релиз (HTTP $CODE): $(cat release.json). Проверь секрет RELEASE_TOKEN (токен с правом записи в репозиторий)."
exit 1
fi
ID=$(python3 -c 'import json;print(json.load(open("release.json"))["id"])')
for f in out/*; do
name=$(basename "$f")
echo "upload $name"
curl -sf -X POST -H "Authorization: token $TOKEN" -F "attachment=@$f" "$API/releases/$ID/assets?name=$name" > /dev/null
done
curl -sf -X POST -H "Authorization: token $TOKEN" -F "attachment=@out/airdef.hmod" "$API/releases/$ID/assets?name=airdef.hmod" > /dev/null
+56
View File
@@ -0,0 +1,56 @@
# Игра-форк для Linux: только вручную (Actions → game → Run workflow) с тегом готового релиза
# (build-N), дописывает в него airdef-linux-x86_64.tar.gz. Тяжёлая сборка (час на 2 ядрах,
# несколько ГБ диска и памяти), поэтому один поток, низший приоритет, и отказ, если на сервере
# мало места: забитый диск роняет Gitea.
name: game
on:
workflow_dispatch:
inputs:
tag:
description: Тег релиза, например build-7
required: true
env:
CARGO_TERM_COLOR: never
CARGO_BUILD_JOBS: "1"
CARGO_INCREMENTAL: "0"
jobs:
game-linux:
runs-on: ubuntu-latest
steps:
- name: Место на диске
run: |
FREE=$(df --output=avail -BG / | tail -1 | tr -dc 0-9)
echo "свободно: ${FREE} ГБ"
if [ "$FREE" -lt 15 ]; then echo "::error::на сервере свободно ${FREE} ГБ, нужно хотя бы 15: сборка игры забьёт диск и уронит Gitea"; exit 1; fi
- uses: actions/checkout@v4
- name: Системные библиотеки
run: |
SUDO=$(command -v sudo || true)
$SUDO apt-get update -qq
$SUDO apt-get install -y -qq --no-install-recommends build-essential pkg-config libasound2-dev libudev-dev
- name: Rust
run: |
curl -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --target wasm32-unknown-unknown
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- name: Форк Horizon
run: nice -n 19 python3 tools/airdef.py fork
- name: Мод
run: nice -n 19 python3 tools/airdef.py mod
- name: Игра
run: nice -n 19 python3 tools/airdef.py game
- name: В релиз
env:
TOKEN: ${{ secrets.RELEASE_TOKEN || gitea.token }}
API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
TAG: ${{ inputs.tag }}
run: |
set -e
D=airdef-linux-x86_64
mkdir -p "$D" out
cp horizon/target/fast/horizon-client horizon/target/fast/horizon-ctl airdef-mod/target/horizon-mod/airdef.hmod dist/play.sh docs/INSTALL.md "$D/"
tar czf "out/$D.tar.gz" "$D"
ID=$(curl -sf -H "Authorization: token $TOKEN" "$API/releases/tags/$TAG" | python3 -c 'import json,sys;print(json.load(sys.stdin)["id"])')
curl -sf -X POST -H "Authorization: token $TOKEN" -F "attachment=@out/$D.tar.gz" "$API/releases/$ID/assets?name=$D.tar.gz" > /dev/null
+7 -6
View File
@@ -19,13 +19,14 @@ AirDef работает на своей сборке (форке) Horizon: в н
## 2а. Готовая сборка из релиза (проще)
В репозитории → **Releases** → последний `build-N`. Там лежат:
В репозитории → **Releases** → последний `build-N`. В каждом релизе есть `airdef.hmod` — сам мод (один для всех
ОС). Игру-форк сервер сборок не собирает сам: это тяжело для него. Архивы с игрой появляются в релизе, только если
их собрали вручную:
- `airdef-linux-x86_64.tar.gz` — Actions → **game** → Run workflow, указать тег релиза;
- `airdef-windows-x86_64.zip`, `airdef-macos-arm64.tar.gz` — Actions → **desktop** (нужны раннеры с метками
`windows-latest` / `macos-latest`).
- `airdef.hmod` — сам мод;
- `airdef-windows-x86_64.zip`, `airdef-macos-arm64.tar.gz`, `airdef-linux-x86_64.tar.gz` — игра-форк + мод +
скрипт запуска. Сборка для Linux есть всегда; для Windows и macOS её добавляет workflow **desktop**
(Actions → desktop → Run workflow, указать тег релиза), если подключены раннеры с метками
`windows-latest` / `macos-latest`. Нет такой сборки — способ 2б.
Нет архива для твоей ОС — способ 2б: игра соберётся у тебя, а мод можно взять из релиза или собрать там же.
**Windows:** распакуй zip куда угодно и запусти `play.bat`. Он положит мод в папку игры, включит его и запустит
игру. Конкретный мир: `play.bat --world "Имя мира"` (в командной строке, из папки архива).
@@ -1,7 +1,7 @@
From 3de7a6875140a107e28563cc44489b55f5794517 Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com>
Date: Wed, 28 Oct 2026 12:37:45 +0300
Subject: [PATCH 1/9] Mod API (AirDef fork): world models, client particles,
Subject: [PATCH 01/10] Mod API (AirDef fork): world models, client particles,
sounds, ground heights
render.set-models draws block models with an interpolated pose per id;
@@ -1,7 +1,7 @@
From a6d64e6415bf618539af464e521091e72b72d72f Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com>
Date: Wed, 28 Oct 2026 12:39:45 +0300
Subject: [PATCH 2/9] cargo-horizon-mod: report particles and sounds
Subject: [PATCH 02/10] cargo-horizon-mod: report particles and sounds
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---
@@ -1,7 +1,7 @@
From ee79b3562feebe51959a01f1585a9e85bd53ccb5 Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com>
Date: Wed, 28 Oct 2026 13:33:10 +0300
Subject: [PATCH 3/9] Mod API (AirDef fork): full-screen post pass for mods
Subject: [PATCH 03/10] Mod API (AirDef fork): full-screen post pass for mods
render.set-post compiles a mod's WGSL after a prelude (camera matrices,
time, 256 vec4 of params, the world's colour and depth) and runs it after
@@ -1,8 +1,8 @@
From 2b137dbbedd540daeebc7f5e387847aeb56a1355 Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com>
Date: Wed, 28 Oct 2026 22:09:30 +0300
Subject: [PATCH 4/9] Mod models: draw far models as scaled impostors past the
far plane, cull at 4096
Subject: [PATCH 04/10] Mod models: draw far models as scaled impostors past
the far plane, cull at 4096
---
crates/horizon-client/src/game/mod_models.rs | 20 ++++++++++++++++----
@@ -1,8 +1,8 @@
From c8ebca6e0af68630e65211844b34d25c5b985c03 Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com>
Date: Wed, 28 Oct 2026 22:31:00 +0300
Subject: [PATCH 5/9] Mod cameras: render.set-views draws the world from other
points into pictures that HUD view shapes show (API level 9)
Subject: [PATCH 05/10] Mod cameras: render.set-views draws the world from
other points into pictures that HUD view shapes show (API level 9)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---
@@ -1,7 +1,7 @@
From 8d2261fc4f8072c32e454139cfea7a8c0cd9d664 Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com>
Date: Wed, 28 Oct 2026 23:08:21 +0300
Subject: [PATCH 6/9] Mods: more fuel per tick and per event (AirDef plans
Subject: [PATCH 06/10] Mods: more fuel per tick and per event (AirDef plans
raids)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@@ -1,7 +1,7 @@
From 6c548de34def8e4a624d734243418090798bd16a Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com>
Date: Wed, 28 Oct 2026 23:25:29 +0300
Subject: [PATCH 7/9] Mod cameras up to 1920x1080 (full-screen replays)
Subject: [PATCH 07/10] Mod cameras up to 1920x1080 (full-screen replays)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---
@@ -1,8 +1,8 @@
From 9b9e93269d67401cc620567a884d5ce97f68d76e Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com>
Date: Thu, 29 Oct 2026 11:15:11 +0300
Subject: [PATCH 8/9] Mod cameras: distant terrain tiles, fog measured from the
camera, no impostors while one is shown
Subject: [PATCH 08/10] Mod cameras: distant terrain tiles, fog measured from
the camera, no impostors while one is shown
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---
@@ -1,7 +1,7 @@
From 065d9fa02966238a5bd9ff460392a1a809db0b11 Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com>
Date: Thu, 29 Oct 2026 11:25:45 +0300
Subject: [PATCH 9/9] Mods: game.world-id, so mods keep state per world (API
Subject: [PATCH 09/10] Mods: game.world-id, so mods keep state per world (API
level 9)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@@ -0,0 +1,98 @@
From 1bf28135fd9a31f837323dc3a8d81ed73343d816 Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com>
Date: Thu, 29 Oct 2026 12:01:08 +0300
Subject: [PATCH 10/10] cargo-horizon-mod: trial runs behind feature full, so a
packing-only tool skips wasmtime
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---
crates/cargo-horizon-mod/Cargo.toml | 8 +++++++-
crates/cargo-horizon-mod/src/main.rs | 16 ++++++++++++++--
2 files changed, 21 insertions(+), 3 deletions(-)
diff --git a/crates/cargo-horizon-mod/Cargo.toml b/crates/cargo-horizon-mod/Cargo.toml
index 34eefe227..21b8a9221 100644
--- a/crates/cargo-horizon-mod/Cargo.toml
+++ b/crates/cargo-horizon-mod/Cargo.toml
@@ -6,8 +6,14 @@ license.workspace = true
rust-version = "1.86"
description = "cargo horizon-mod new/build/run/check: make Horizon mods in Rust (docs/mods.md)"
+[features]
+default = ["full"]
+# The trial runs (`check`, `test`) and the registry's CI need wasmtime. Without this feature the
+# tool is new, build and run only, and builds in a fraction of the time (CI that only packs mods).
+full = ["horizon-mods/runtime", "horizon-mods/index"]
+
[dependencies]
-horizon-mods = { path = "../horizon-mods", default-features = false, features = ["pack", "runtime", "index"] }
+horizon-mods = { path = "../horizon-mods", default-features = false, features = ["pack"] }
serde_json = "1.0.151"
[lints]
diff --git a/crates/cargo-horizon-mod/src/main.rs b/crates/cargo-horizon-mod/src/main.rs
index 95d0a9eb3..ff4ab663b 100644
--- a/crates/cargo-horizon-mod/src/main.rs
+++ b/crates/cargo-horizon-mod/src/main.rs
@@ -11,12 +11,15 @@
//! - `check <file> [--json]`: what the registry checks of a submission
//! (`.hmod`, `.hpack`, resource pack `.zip`), with a trial run for a mod.
//! - `registry build|review|check|list`: the registry repository's CI.
+#[cfg(feature = "full")]
+use horizon_mods::testing::{Script, TestGame};
use horizon_mods::{
Capability, Content, Manifest, Package,
manifest::{MANIFEST, component_imports, componentize},
- testing::{Script, TestGame},
};
+#[cfg(feature = "full")]
mod check;
+#[cfg(feature = "full")]
mod registry;
use std::{
@@ -39,9 +42,16 @@ fn main() -> ExitCode {
println!("Built {}", p.display());
}),
Some("run") => run(&args[1..]),
+ #[cfg(feature = "full")]
Some("test") => test(&args[1..]),
+ #[cfg(feature = "full")]
Some("check") => check(&args[1..]),
+ #[cfg(feature = "full")]
Some("registry") => registry::main(&args[1..]),
+ #[cfg(not(feature = "full"))]
+ Some("test" | "check" | "registry") => {
+ Err("this cargo-horizon-mod was built without trial runs (feature `full`)".into())
+ }
_ => Err("usage: cargo horizon-mod new <path> [--template code|content] | build [--debug] | run [--game <horizon-client>] [-- <game args>] | test [--debug] [--hmod <file>] [<name>] | check <file> [--kind <kind>] [--key <public key> --signature <file.minisig>] [--json] | registry build|review|check|list".into()),
};
match result {
@@ -249,6 +259,7 @@ fn compile(root: &Path, release: bool) -> Result<(Vec<u8>, PathBuf), String> {
/// `check <file>`: the registry's checks of a submission, printed; fails
/// when one does.
+#[cfg(feature = "full")]
fn check(args: &[String]) -> Result<(), String> {
let file = args
.first()
@@ -280,6 +291,7 @@ fn check(args: &[String]) -> Result<(), String> {
/// `test`: every `tests/*.toml` script (those whose name holds `<name>`)
/// against the built mod, or the `.hmod` given with `--hmod`.
+#[cfg(feature = "full")]
fn test(args: &[String]) -> Result<(), String> {
let hmod = match flag(args, "--hmod") {
Some(p) => PathBuf::from(p),
@@ -360,7 +372,7 @@ fn run(args: &[String]) -> Result<(), String> {
.ok_or_else(|| format!("the game exited with {status}"))
}
-#[cfg(test)]
+#[cfg(all(test, feature = "full"))]
mod tests {
use super::*;
--
2.54.0 (Apple Git-157)
+2 -1
View File
@@ -88,7 +88,8 @@ def mod():
if not Path(tool).exists():
if not (FORK / "Cargo.toml").exists():
fork()
run([cargo(), "install", "--locked", "--path", FORK / "crates" / "cargo-horizon-mod"])
# Without trial runs (wasmtime): building a mod needs only packing, a fraction of the compile.
run([cargo(), "install", "--locked", "--no-default-features", "--path", FORK / "crates" / "cargo-horizon-mod"])
run([cargo(), "horizon-mod", "build"], cwd=ROOT / "airdef-mod")