From 2d0c0d8ef03ee8f729795cfd60b141c1913fe08e Mon Sep 17 00:00:00 2001 From: Ilya Groshev Date: Fri, 22 May 2026 23:12:08 +0300 Subject: [PATCH] Add cross-platform prebuilt release workflow --- .github/workflows/release.yml | 117 ++++++++++++++++++++++ README.md | 181 ++++++++++++++++++---------------- server/cmd/dev/main.go | 8 +- server/cmd/wizard/backup.go | 50 ++++++---- server/cmd/wizard/main.go | 35 ++++--- server/cmd/wizard/prebuilt.go | 40 ++++++++ server/go.mod | 18 ++-- server/go.sum | 28 ++++++ server/migrations/embed.go | 21 ++++ 9 files changed, 368 insertions(+), 130 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 server/cmd/wizard/prebuilt.go create mode 100644 server/migrations/embed.go diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..5d9aa27 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,117 @@ +name: Build and Publish Release Binaries + +on: + push: + tags: + - 'v*' + workflow_dispatch: + +permissions: + contents: write + +jobs: + build: + name: Build ${{ matrix.goos }}/${{ matrix.goarch }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - { goos: linux, goarch: amd64, archive: tar.gz } + - { goos: linux, goarch: arm64, archive: tar.gz } + - { goos: darwin, goarch: amd64, archive: tar.gz } + - { goos: darwin, goarch: arm64, archive: tar.gz } + - { goos: windows, goarch: amd64, archive: zip } + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: '1.25' + cache-dependency-path: server/go.sum + + - name: Install protoc + run: sudo apt-get update && sudo apt-get install -y protobuf-compiler + + - name: Install protoc-gen-go plugins + run: | + go install google.golang.org/protobuf/cmd/protoc-gen-go@latest + go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest + echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" + + - name: Generate proto stubs + working-directory: server + run: make proto + + - name: Cross-compile binaries + working-directory: server + env: + GOOS: ${{ matrix.goos }} + GOARCH: ${{ matrix.goarch }} + CGO_ENABLED: '0' + run: | + set -euo pipefail + stage="staging" + mkdir -p "$stage/bin" + + ext="" + if [ "$GOOS" = "windows" ]; then ext=".exe"; fi + + build() { + local name="$1" dest="$2" + go build -trimpath -ldflags="-s -w" -o "$dest/${name}${ext}" "./cmd/${name}" + } + + # Wizard sits at the root so end-users see it immediately. + build wizard "$stage" + + # Sub-services and admin tools go in bin/ to match the runtime layout. + for name in dev lunar-tear octo-cdn auth-server import-snapshot claim-account register-account wizard-restore; do + build "$name" "$stage/bin" + done + + - name: Stage docs + working-directory: server + run: | + cp ../README.md staging/README.md + cp ../LICENSE staging/LICENSE + + - name: Archive + working-directory: server + run: | + set -euo pipefail + name="lunar-tear-server-${{ github.ref_name }}-${{ matrix.goos }}-${{ matrix.goarch }}" + mv staging "$name" + if [ "${{ matrix.archive }}" = "zip" ]; then + zip -r "${name}.zip" "$name" + else + tar -czf "${name}.tar.gz" "$name" + fi + ls -lh "${name}".* + + - uses: actions/upload-artifact@v4 + with: + name: lunar-tear-server-${{ matrix.goos }}-${{ matrix.goarch }} + path: server/lunar-tear-server-*.${{ matrix.archive }} + if-no-files-found: error + + release: + name: Attach archives to GitHub Release + needs: build + if: startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@v4 + with: + path: artifacts + merge-multiple: true + + - name: List downloaded artifacts + run: ls -lh artifacts/ + + - uses: softprops/action-gh-release@v2 + with: + files: artifacts/* + generate_release_notes: true + draft: true diff --git a/README.md b/README.md index 48b5674..85ed1e2 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,14 @@ Discord server: https://discord.gg/MZAf5aVkJG ## How To Launch The Server -### Prerequisites +### Download & Run (no setup) + +Prebuilt binaries are published for Linux, macOS, and Windows on the [Releases page](https://github.com/Walter-Sparrow/lunar-tear/releases). + +1. Download the archive for your OS/arch (`lunar-tear-server---.{tar.gz,zip}`). +2. Run `./wizard` (macOS/Linux) or double-click `wizard.exe` (Windows). + +### Prerequisites (build from source) - Go 1.25+ - [goose](https://github.com/pressly/goose) migration tool @@ -40,12 +47,12 @@ By default the wizard uses ports 8003 (gRPC), 8080 (CDN), and 3000 (auth). Overr go run ./cmd/wizard --grpc-port 9003 --cdn-port 9080 ``` -| Flag | Default | Description | -| ---------------- | ------- | ---------------------------------- | -| `--prefer-saved` | `false` | Reuse saved config without prompting | -| `--grpc-port` | `8003` | gRPC server port | -| `--cdn-port` | `8080` | CDN server port | -| `--auth-port` | `3000` | Auth server port | +| Flag | Default | Description | +| ---------------- | ------- | ------------------------------------------------------------------------------------------------------------------------ | +| `--prefer-saved` | `false` | Reuse saved config without prompting | +| `--grpc-port` | `8003` | gRPC server port | +| `--cdn-port` | `8080` | CDN server port | +| `--auth-port` | `3000` | Auth server port | | `--admin-port` | `0` | Admin webhook port (`0` = disabled). Bound on `127.0.0.1`; only takes effect when `LUNAR_ADMIN_TOKEN` is set in the env. | Custom ports are saved to `.wizard.json` alongside your other settings. On the next run the saved ports are reused automatically — no need to pass the flags again. If you later pass different port flags, the wizard warns you that the ports changed and asks for confirmation before continuing. @@ -105,9 +112,9 @@ go run ./cmd/import-snapshot \ | Flag | Default | Description | | ------------ | ------------ | --------------------------------------------- | -| `--snapshot` | *(required)* | Path to JSON snapshot file | -| `--uuid` | *(required)* | UUID to assign (must match the client's UUID) | -| `--db` | `db/game.db` | SQLite database path | +| `--snapshot` | _(required)_ | Path to JSON snapshot file | +| `--uuid` | _(required)_ | UUID to assign (must match the client's UUID) | +| `--db` | `db/game.db` | SQLite database path | ### Run @@ -174,40 +181,40 @@ Or via `make`: make dev ARGS="--grpc.listen 0.0.0.0:9000 --grpc.public-addr 10.0.2.2:9000" ``` -| Flag | Default | Description | -| --------------------- | ------------------ | ---------------------------------------- | -| `--auth.listen` | `0.0.0.0:3000` | auth-server listen address | -| `--auth.db` | `db/auth.db` | auth-server SQLite database path | -| `--cdn.listen` | `0.0.0.0:8080` | octo-cdn local bind address | -| `--cdn.public-addr` | `10.0.2.2:8080` | octo-cdn externally-reachable addr | -| `--grpc.listen` | `0.0.0.0:8003` | lunar-tear gRPC listen address | -| `--grpc.public-addr` | `10.0.2.2:8003` | lunar-tear externally-reachable addr | -| `--grpc.octo-url` | `http://10.0.2.2:8080` | Octo CDN base URL passed to lunar-tear | -| `--grpc.auth-url` | `http://localhost:3000` | auth server base URL passed to lunar-tear | -| `--no-register` | `false` | disable new user registrations (only already registered users can connect). | -| `--admin.listen` | *(empty)* | lunar-tear admin webhook bind. Empty = leave default; webhook only binds when `LUNAR_ADMIN_TOKEN` is set in the env. | -| `--no-color` | `false` | disable colored output | +| Flag | Default | Description | +| -------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------- | +| `--auth.listen` | `0.0.0.0:3000` | auth-server listen address | +| `--auth.db` | `db/auth.db` | auth-server SQLite database path | +| `--cdn.listen` | `0.0.0.0:8080` | octo-cdn local bind address | +| `--cdn.public-addr` | `10.0.2.2:8080` | octo-cdn externally-reachable addr | +| `--grpc.listen` | `0.0.0.0:8003` | lunar-tear gRPC listen address | +| `--grpc.public-addr` | `10.0.2.2:8003` | lunar-tear externally-reachable addr | +| `--grpc.octo-url` | `http://10.0.2.2:8080` | Octo CDN base URL passed to lunar-tear | +| `--grpc.auth-url` | `http://localhost:3000` | auth server base URL passed to lunar-tear | +| `--no-register` | `false` | disable new user registrations (only already registered users can connect). | +| `--admin.listen` | _(empty)_ | lunar-tear admin webhook bind. Empty = leave default; webhook only binds when `LUNAR_ADMIN_TOKEN` is set in the env. | +| `--no-color` | `false` | disable colored output | ### Ports -| Protocol | Port | Binary | Notes | -| -------- | ---- | ------------- | ----------------------------------------------------------- | -| gRPC | 443 | `lunar-tear` | default; configurable with `--listen` (requires patched client) | -| HTTP | 8080 | `octo-cdn` | Octo asset API + game web pages | +| Protocol | Port | Binary | Notes | +| -------- | ---- | ------------- | ---------------------------------------------------------------------------------------------------------------- | +| gRPC | 443 | `lunar-tear` | default; configurable with `--listen` (requires patched client) | +| HTTP | 8080 | `octo-cdn` | Octo asset API + game web pages | | HTTP | 8082 | `lunar-tear` | admin webhook (`/api/admin/master-data/reload`); loopback by default, only binds when `LUNAR_ADMIN_TOKEN` is set | -| HTTP | 3000 | `auth-server` | account registration and login | +| HTTP | 3000 | `auth-server` | account registration and login | ### Game Server Flags (`lunar-tear`) -| Flag | Default | Description | -| ---------------- | ----------------- | ---------------------------------------------------- | -| `--listen` | `0.0.0.0:443` | gRPC listen address (host:port) | -| `--public-addr` | `127.0.0.1:443` | externally-reachable host:port advertised to clients | -| `--octo-url` | *(required)* | CDN base URL the client uses for assets (e.g. `http://10.0.2.2:8080`) | -| `--db` | `db/game.db` | SQLite database path | -| `--auth-url` | *(empty)* | Auth server base URL (e.g. `http://localhost:3000`) | -| `--admin-listen` | `127.0.0.1:8082` | Admin webhook listen address. Only binds when `LUNAR_ADMIN_TOKEN` is set. | -| `--no-register` | `false` | Disable new user registrations (only already registered users can connect). | +| Flag | Default | Description | +| ---------------- | ---------------- | --------------------------------------------------------------------------- | +| `--listen` | `0.0.0.0:443` | gRPC listen address (host:port) | +| `--public-addr` | `127.0.0.1:443` | externally-reachable host:port advertised to clients | +| `--octo-url` | _(required)_ | CDN base URL the client uses for assets (e.g. `http://10.0.2.2:8080`) | +| `--db` | `db/game.db` | SQLite database path | +| `--auth-url` | _(empty)_ | Auth server base URL (e.g. `http://localhost:3000`) | +| `--admin-listen` | `127.0.0.1:8082` | Admin webhook listen address. Only binds when `LUNAR_ADMIN_TOKEN` is set. | +| `--no-register` | `false` | Disable new user registrations (only already registered users can connect). | ### Live Master Data Reload @@ -231,11 +238,11 @@ Security defaults are fail-closed: ### CDN Flags (`octo-cdn`) -| Flag | Default | Description | -| --------------- | ----------------- | -------------------------------------------------------- | -| `--listen` | `0.0.0.0:8080` | local bind address | -| `--public-addr` | `127.0.0.1:8080` | externally-reachable address (used in list.bin rewriting) | -| `--assets-dir` | `.` | root directory containing the `assets/` tree | +| Flag | Default | Description | +| --------------- | ---------------- | --------------------------------------------------------- | +| `--listen` | `0.0.0.0:8080` | local bind address | +| `--public-addr` | `127.0.0.1:8080` | externally-reachable address (used in list.bin rewriting) | +| `--assets-dir` | `.` | root directory containing the `assets/` tree | ### Docker @@ -250,22 +257,22 @@ The `db/` directory is mounted as a volume so both `game.db` and `auth.db` persi Each service has its own image and can be deployed independently: -| Service | Image | Default Port | Notes | -| -------- | --------------------------- | ------------ | ------------------------------ | +| Service | Image | Default Port | Notes | +| -------- | --------------------------- | ------------ | -------------------------------- | | `server` | `kretts/lunar-tear:latest` | 8003, 8082 | gRPC game server + admin webhook | -| `cdn` | `kretts/octo-cdn:latest` | 8080 | HTTP asset CDN | -| `auth` | `kretts/auth-server:latest` | 3000 | Account registration and login | +| `cdn` | `kretts/octo-cdn:latest` | 8080 | HTTP asset CDN | +| `auth` | `kretts/auth-server:latest` | 3000 | Account registration and login | The game server is configured via environment variables in the compose file: -| Env var | Description | -| --------------------- | -------------------------------------------------------------------------------------------- | -| `LUNAR_LISTEN` | gRPC bind address | -| `LUNAR_PUBLIC_ADDR` | Client-facing address advertised to the game | -| `LUNAR_OCTO_URL` | CDN base URL the client uses for assets | -| `LUNAR_AUTH_URL` | Auth server base URL (optional) | -| `LUNAR_ADMIN_LISTEN` | Admin webhook bind address inside the container (compose default: `0.0.0.0:8082`) | -| `LUNAR_ADMIN_TOKEN` | Bearer token for the admin webhook. **The webhook does not bind unless this is set.** | +| Env var | Description | +| -------------------- | ------------------------------------------------------------------------------------- | +| `LUNAR_LISTEN` | gRPC bind address | +| `LUNAR_PUBLIC_ADDR` | Client-facing address advertised to the game | +| `LUNAR_OCTO_URL` | CDN base URL the client uses for assets | +| `LUNAR_AUTH_URL` | Auth server base URL (optional) | +| `LUNAR_ADMIN_LISTEN` | Admin webhook bind address inside the container (compose default: `0.0.0.0:8082`) | +| `LUNAR_ADMIN_TOKEN` | Bearer token for the admin webhook. **The webhook does not bind unless this is set.** | Auth is optional — if `LUNAR_AUTH_URL` is unset the game server starts without it. The admin webhook is published to `127.0.0.1:8082` on the host so the master-data reload endpoint stays loopback-only by default; set `LUNAR_ADMIN_TOKEN` (e.g. via a `.env` file) before bringing the stack up. @@ -273,22 +280,22 @@ Auth is optional — if `LUNAR_AUTH_URL` is unset the game server starts without All targets run from the `server/` directory. -| Target | Description | -| -------------- | ------------------------------------------------------- | -| `make proto` | Regenerate protobuf stubs | -| `make build` | Build the game server binary | -| `make build-cdn` | Build the CDN binary | -| `make build-auth` | Build the auth server binary | -| `make build-dev` | Build the dev runner binary to `bin/` | -| `make build-all` | Build all service binaries to `bin/` | -| `make build-import` | Build the import-snapshot tool | -| `make build-claim-account` | Build the claim-account tool | -| `make build-register-account` | Build the register-account tool | -| `make clean` | Remove the `bin/` directory | -| `make dev` | Run all three services with one command | -| `make migrate` | Run goose migrations on `db/game.db` | -| `make restore` | Interactive restore of `db/game.db` from `db/backups/` | -| `make import` | Import a snapshot (`SNAPSHOT=... UUID=...` required) | +| Target | Description | +| ----------------------------- | ------------------------------------------------------ | +| `make proto` | Regenerate protobuf stubs | +| `make build` | Build the game server binary | +| `make build-cdn` | Build the CDN binary | +| `make build-auth` | Build the auth server binary | +| `make build-dev` | Build the dev runner binary to `bin/` | +| `make build-all` | Build all service binaries to `bin/` | +| `make build-import` | Build the import-snapshot tool | +| `make build-claim-account` | Build the claim-account tool | +| `make build-register-account` | Build the register-account tool | +| `make clean` | Remove the `bin/` directory | +| `make dev` | Run all three services with one command | +| `make migrate` | Run goose migrations on `db/game.db` | +| `make restore` | Interactive restore of `db/game.db` from `db/backups/` | +| `make import` | Import a snapshot (`SNAPSHOT=... UUID=...` required) | ## Claim Account @@ -301,10 +308,10 @@ cd server go run ./cmd/claim-account --name "PlayerName" --db db/game.db ``` -| Flag | Default | Description | -| -------- | ------------ | ---------------------------------------------------- | -| `--name` | *(required)* | In-game player name to claim | -| `--db` | `db/game.db` | SQLite database path | +| Flag | Default | Description | +| -------- | ------------ | ---------------------------- | +| `--name` | _(required)_ | In-game player name to claim | +| `--db` | `db/game.db` | SQLite database path | ## Auth Server @@ -323,12 +330,12 @@ The `--secret` flag accepts a hex-encoded HMAC key. If omitted, a random key is ### Flags -| Flag | Default | Description | -| ---------------- | --------------- | -------------------------------------------- | -| `--listen` | `0.0.0.0:3000` | HTTP listen address (host:port) | -| `--db` | `db/auth.db` | SQLite database path for auth users | -| `--secret` | *(generated)* | Hex-encoded HMAC secret for token signing | -| `--no-register` | `false` | Disable new user registrations (only already registered users can log in). | +| Flag | Default | Description | +| --------------- | -------------- | -------------------------------------------------------------------------- | +| `--listen` | `0.0.0.0:3000` | HTTP listen address (host:port) | +| `--db` | `db/auth.db` | SQLite database path for auth users | +| `--secret` | _(generated)_ | Hex-encoded HMAC secret for token signing | +| `--no-register` | `false` | Disable new user registrations (only already registered users can log in). | ## Create account @@ -339,13 +346,13 @@ A primary mean of registering new accounts when `--no-register` flag is passed t go run ./cmd/register-account --name "AccountName" --password "AccountPassword" --platform "android" ``` -| Flag | Default | Description | -| ------------ | ------------ | ------------------------------------------------------------ | -| `--name` | *(required)* | Auth Server account nickname to be registered | -| `--password` | *(required)* | Auth Server account password to be registered | -| `--platform` | `android` | Platform of new user account (`android` or `ios`) | -| `--db` | `db/game.db` | SQLite main database path | -| `--auth-db` | `db/auth.db` | SQLite Auth Server database path | +| Flag | Default | Description | +| ------------ | ------------ | ------------------------------------------------- | +| `--name` | _(required)_ | Auth Server account nickname to be registered | +| `--password` | _(required)_ | Auth Server account password to be registered | +| `--platform` | `android` | Platform of new user account (`android` or `ios`) | +| `--db` | `db/game.db` | SQLite main database path | +| `--auth-db` | `db/auth.db` | SQLite Auth Server database path | This only sets the nickname of Auth Server account, a player can choose their in-game nickname upon first login! diff --git a/server/cmd/dev/main.go b/server/cmd/dev/main.go index e2bc2d4..1cbb3f5 100644 --- a/server/cmd/dev/main.go +++ b/server/cmd/dev/main.go @@ -120,8 +120,12 @@ func main() { colorCyan = "" } - log.Println("building services...") - buildAll() + if _, err := os.Stat("go.mod"); err == nil { + log.Println("building services...") + buildAll() + } else { + log.Println("prebuilt mode: skipping build, using bin/ from archive") + } ext := binExt() ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) diff --git a/server/cmd/wizard/backup.go b/server/cmd/wizard/backup.go index cecc6cf..4f66465 100644 --- a/server/cmd/wizard/backup.go +++ b/server/cmd/wizard/backup.go @@ -27,31 +27,39 @@ func backupGameDB() { return } - _ = spinner.New().Title(" Backing up db/game.db...").Action(func() { - if err := os.MkdirAll(backupDir, 0o755); err != nil { - fmt.Fprintf(os.Stderr, " failed to create %s: %v\n", backupDir, err) - return - } + if !sourceMode { + fmt.Println(" Backing up db/game.db...") + doBackupGameDB() + return + } - ts := time.Now().UTC().Format("20060102T150405Z") - dest := filepath.Join(backupDir, fmt.Sprintf("game.db.%s%s", ts, backupSuffix)) + _ = spinner.New().Title(" Backing up db/game.db...").Action(doBackupGameDB).Run() +} - db, err := sql.Open("sqlite", gameDBPath+"?_pragma=busy_timeout(5000)") - if err != nil { - fmt.Fprintf(os.Stderr, " failed to open %s: %v\n", gameDBPath, err) - return - } - defer db.Close() +func doBackupGameDB() { + if err := os.MkdirAll(backupDir, 0o755); err != nil { + fmt.Fprintf(os.Stderr, " failed to create %s: %v\n", backupDir, err) + return + } - escaped := strings.ReplaceAll(dest, "'", "''") - if _, err := db.Exec(fmt.Sprintf("VACUUM INTO '%s'", escaped)); err != nil { - fmt.Fprintf(os.Stderr, " VACUUM INTO failed: %v\n", err) - _ = os.Remove(dest) - return - } + ts := time.Now().UTC().Format("20060102T150405Z") + dest := filepath.Join(backupDir, fmt.Sprintf("game.db.%s%s", ts, backupSuffix)) - pruneOldBackups() - }).Run() + db, err := sql.Open("sqlite", gameDBPath+"?_pragma=busy_timeout(5000)") + if err != nil { + fmt.Fprintf(os.Stderr, " failed to open %s: %v\n", gameDBPath, err) + return + } + defer db.Close() + + escaped := strings.ReplaceAll(dest, "'", "''") + if _, err := db.Exec(fmt.Sprintf("VACUUM INTO '%s'", escaped)); err != nil { + fmt.Fprintf(os.Stderr, " VACUUM INTO failed: %v\n", err) + _ = os.Remove(dest) + return + } + + pruneOldBackups() } func pruneOldBackups() { diff --git a/server/cmd/wizard/main.go b/server/cmd/wizard/main.go index 9aeebb2..fe87820 100644 --- a/server/cmd/wizard/main.go +++ b/server/cmd/wizard/main.go @@ -74,14 +74,21 @@ func main() { fmt.Print(banner) + sourceMode = isSourceCheckout() + if !*setupOnly { validateAssets() - validateTools() - validateProtocIncludes() - runProtoc() - backupGameDB() - runMigrate() - downloadDeps() + if sourceMode { + validateTools() + validateProtocIncludes() + runProtoc() + backupGameDB() + runMigrate() + downloadDeps() + } else { + backupGameDB() + runMigrateEmbedded() + } } ip, cfg, firstRun := resolveIP(*preferSaved) @@ -901,13 +908,15 @@ func launchDev(ip string, p ports) { } devBin := filepath.Join("bin", "dev"+ext) - _ = spinner.New().Title(" Building services...").Action(func() { - if err := os.MkdirAll("bin", 0755); err != nil { - fmt.Fprintf(os.Stderr, " Failed to create bin/: %v\n", err) - os.Exit(1) - } - runQuiet(exec.Command("go", "build", "-o", devBin, "./cmd/dev"), "build dev") - }).Run() + if sourceMode { + _ = spinner.New().Title(" Building services...").Action(func() { + if err := os.MkdirAll("bin", 0755); err != nil { + fmt.Fprintf(os.Stderr, " Failed to create bin/: %v\n", err) + os.Exit(1) + } + runQuiet(exec.Command("go", "build", "-o", devBin, "./cmd/dev"), "build dev") + }).Run() + } devArgs := []string{ "--grpc.listen", fmt.Sprintf("0.0.0.0:%d", p.GRPC), diff --git a/server/cmd/wizard/prebuilt.go b/server/cmd/wizard/prebuilt.go new file mode 100644 index 0000000..7c1302e --- /dev/null +++ b/server/cmd/wizard/prebuilt.go @@ -0,0 +1,40 @@ +package main + +import ( + "context" + "database/sql" + "fmt" + "os" + + "lunar-tear/server/migrations" +) + +var sourceMode bool + +func isSourceCheckout() bool { + if _, err := os.Stat("go.mod"); err != nil { + return false + } + if _, err := os.Stat("proto"); err != nil { + return false + } + return true +} + +func runMigrateEmbedded() { + fmt.Println(" Running migrations...") + if err := os.MkdirAll("db", 0755); err != nil { + fmt.Fprintf(os.Stderr, " Failed to create db/: %v\n", err) + os.Exit(1) + } + db, err := sql.Open("sqlite", gameDBPath+"?_pragma=busy_timeout(5000)") + if err != nil { + fmt.Fprintf(os.Stderr, " open db: %v\n", err) + os.Exit(1) + } + defer db.Close() + if err := migrations.Up(context.Background(), db); err != nil { + fmt.Fprintf(os.Stderr, " migration failed: %v\n", err) + os.Exit(1) + } +} diff --git a/server/go.mod b/server/go.mod index 4ef075b..2aa76d4 100644 --- a/server/go.mod +++ b/server/go.mod @@ -7,12 +7,12 @@ require ( github.com/pierrec/lz4/v4 v4.1.26 github.com/vmihailenco/msgpack/v5 v5.4.1 golang.org/x/crypto v0.50.0 - golang.org/x/net v0.52.0 + golang.org/x/net v0.53.0 golang.org/x/sys v0.43.0 golang.org/x/term v0.42.0 - google.golang.org/grpc v1.79.1 + google.golang.org/grpc v1.80.0 google.golang.org/protobuf v1.36.11 - modernc.org/sqlite v1.48.2 + modernc.org/sqlite v1.49.1 ) require ( @@ -34,21 +34,25 @@ require ( github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-isatty v0.0.21 // indirect github.com/mattn/go-runewidth v0.0.20 // indirect + github.com/mfridman/interpolate v0.0.2 // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/pressly/goose/v3 v3.27.1 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/sethvargo/go-retry v0.3.0 // indirect github.com/stretchr/testify v1.11.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - go.opentelemetry.io/otel v1.40.0 // indirect + go.opentelemetry.io/otel v1.43.0 // indirect + go.uber.org/multierr v1.11.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/text v0.36.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect - modernc.org/libc v1.70.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 // indirect + modernc.org/libc v1.72.1 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect ) diff --git a/server/go.sum b/server/go.sum index c1f5cc1..4d9314d 100644 --- a/server/go.sum +++ b/server/go.sum @@ -54,8 +54,12 @@ github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQ github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= +github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ= github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= +github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= @@ -66,10 +70,14 @@ github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pressly/goose/v3 v3.27.1 h1:6uEvcprBybDmW4hcz3gYujhARhye+GoWKhEWyzD5sh4= +github.com/pressly/goose/v3 v3.27.1/go.mod h1:maruOxsPnIG2yHHyo8UqKWXYKFcH7Q76csUV7+7KYoM= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= +github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= @@ -82,20 +90,28 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -109,18 +125,25 @@ golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 h1:XF8+t6QQiS0o9ArVan/HW8Q7cycNPGsJf6GA2nXxYAg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= +google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/cc/v4 v4.28.1 h1:XpLbkYVQ24E8tX5u8+yWGvaxerxkR/S4zqxI8ZoSBuc= modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw= modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0= +modernc.org/ccgo/v4 v4.33.0 h1:dspBCm75jsj8Y/ufwAMVfe375L2iYdMyQ2QG/v3hL54= modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= @@ -131,16 +154,21 @@ modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw= modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo= +modernc.org/libc v1.72.1 h1:db1xwJ6u1kE3KHTFTTbe2GCrczHPKzlURP0aDC4NGD0= +modernc.org/libc v1.72.1/go.mod h1:HRMiC/PhPGLIPM7GzAFCbI+oSgE3dhZ8FWftmRrHVlY= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sqlite v1.48.2 h1:5CnW4uP8joZtA0LedVqLbZV5GD7F/0x91AXeSyjoh5c= modernc.org/sqlite v1.48.2/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig= +modernc.org/sqlite v1.49.1 h1:dYGHTKcX1sJ+EQDnUzvz4TJ5GbuvhNJa8Fg6ElGx73U= +modernc.org/sqlite v1.49.1/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= diff --git a/server/migrations/embed.go b/server/migrations/embed.go new file mode 100644 index 0000000..faa47a0 --- /dev/null +++ b/server/migrations/embed.go @@ -0,0 +1,21 @@ +package migrations + +import ( + "context" + "database/sql" + "embed" + + "github.com/pressly/goose/v3" +) + +//go:embed *.sql +var FS embed.FS + +func Up(ctx context.Context, db *sql.DB) error { + goose.SetBaseFS(FS) + goose.SetLogger(goose.NopLogger()) + if err := goose.SetDialect("sqlite3"); err != nil { + return err + } + return goose.UpContext(ctx, db, ".", goose.WithAllowMissing()) +}