Build local release artifacts and prepare Compose deployment [skip ci]

This commit is contained in:
SCOPEDD committed 2026-09-30 07:22:41 -04:00
1 parent 6deff62a3a
commit 52de8c3031
25 files changed
+137 -23

No files matched your search

+1
View File
@@ -5,6 +5,7 @@ target
.github .github
data data
docs docs
/release-artifacts
launcher/src launcher/src
launcher/src-tauri/icons launcher/src-tauri/icons
launcher/src-tauri/gen launcher/src-tauri/gen
+3 -2
View File
@@ -1,7 +1,8 @@
# Copy to .env and adjust. Only ADMIN_PASSWORD really matters. # Copy to .env and adjust. If ADMIN_PASSWORD is blank, a random first-start
# password is printed in `docker compose logs panel`.
PANEL_PORT=8080 PANEL_PORT=8080
ADMIN_USERNAME=admin ADMIN_USERNAME=admin
ADMIN_PASSWORD=change-me-please ADMIN_PASSWORD=
JWT_SECRET= JWT_SECRET=
CURSEFORGE_API_KEY= CURSEFORGE_API_KEY=
MAX_UPLOAD_MB=2048 MAX_UPLOAD_MB=2048
+9 -5
View File
@@ -52,10 +52,12 @@ Brand it, publish vanilla versions or modpacks, manage players, and push changes
### 1. Run the panel ### 1. Run the panel
```bash ```bash
curl -O https://raw.githubusercontent.com/scopeddlol/SCOPENET-MC/main/docker-compose.yml git clone https://github.com/scopeddlol/SCOPENET-MC.git
curl -o .env https://raw.githubusercontent.com/scopeddlol/SCOPENET-MC/main/.env.example cd SCOPENET-MC
# edit .env → set ADMIN_PASSWORD cp .env.example .env
docker compose up -d # edit .env → set ADMIN_PASSWORD and PUBLIC_URL for your domain
docker compose config --quiet
docker compose up -d --build
``` ```
Open `http://your-server:8080`, sign in as `admin`, then: Open `http://your-server:8080`, sign in as `admin`, then:
@@ -64,7 +66,7 @@ Open `http://your-server:8080`, sign in as `admin`, then:
2. **Instances** → *New instance* → a version, or a Modrinth / CurseForge / .zip modpack. 2. **Instances** → *New instance* → a version, or a Modrinth / CurseForge / .zip modpack.
3. **Settings** → how players sign in. 3. **Settings** → how players sign in.
> Put the panel behind HTTPS (Caddy, Traefik, nginx, Cloudflare Tunnel…) before sharing it. See [docs/admin-guide.md](docs/admin-guide.md). > Put the panel behind HTTPS (Caddy, Traefik, nginx, Cloudflare Tunnel…) before sharing it. See [docs/deployment.md](docs/deployment.md).
### 2. Build your branded launcher ### 2. Build your branded launcher
@@ -99,6 +101,8 @@ docker-compose.yml Panel deployment
- [Official server setup](docs/server-integration.md) — private authentication, supported versions, activity and UUID protection - [Official server setup](docs/server-integration.md) — private authentication, supported versions, activity and UUID protection
- [Architecture](docs/architecture.md) — how the pieces fit, API reference - [Architecture](docs/architecture.md) — how the pieces fit, API reference
- [Development](docs/development.md) — running everything locally, tests - [Development](docs/development.md) — running everything locally, tests
- [Deployment](docs/deployment.md) — local Compose build, health check, upgrades and backups
- [Local 0.4.0 artifacts](release-artifacts/0.4.0/README.md) — Windows installer, server jars and checksums
## Configuration reference ## Configuration reference
+1 -7
View File
@@ -68,7 +68,7 @@ impl Default for Branding {
} }
} }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(default)] #[serde(default)]
pub struct AboutContent { pub struct AboutContent {
pub title: String, pub title: String,
@@ -77,12 +77,6 @@ pub struct AboutContent {
pub links: Vec<SocialLink>, pub links: Vec<SocialLink>,
} }
impl Default for AboutContent {
fn default() -> Self {
Self { title: String::new(), icon_url: None, body: String::new(), links: Vec::new() }
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(default)] #[serde(default)]
pub struct Background { pub struct Background {
+3 -3
View File
@@ -1,13 +1,13 @@
# SCOPENET admin panel # SCOPENET admin panel
# #
# cp .env.example .env # set ADMIN_PASSWORD at least # cp .env.example .env # set ADMIN_PASSWORD and PUBLIC_URL
# docker compose up -d --build # docker compose up -d --build
# #
# Then open http://localhost:8080 (or your domain behind a reverse proxy). # Then open http://localhost:8080 (or your domain behind a reverse proxy).
services: services:
panel: panel:
image: ghcr.io/scopeddlol/scopenet-mc-panel:latest # Tag the image built from this checkout so an older GHCR release is not used.
# Or build from source: docker compose build image: scopenet-panel:local
build: . build: .
container_name: scopenet-panel container_name: scopenet-panel
restart: unless-stopped restart: unless-stopped
+40
View File
@@ -0,0 +1,40 @@
# Docker Compose deployment
This deployment builds the panel and web UI from the checked-out source. It does
not need GitHub Actions or a published GHCR image.
1. Install Docker Engine with the Compose plugin on a Linux host. Point a DNS
name at that host and arrange HTTPS through your reverse proxy.
2. Check out the commit you want to deploy, then copy `.env.example` to `.env`.
Set `ADMIN_PASSWORD` to a unique password and `PUBLIC_URL` to the public
`https://` URL. Set `SCOPENET_TRUSTED_PROXIES` only to the IP addresses of
proxies that send forwarding headers.
3. Run:
```bash
docker compose config --quiet
docker compose up -d --build
docker compose ps
docker compose logs --tail=100 panel
```
The panel listens on `PANEL_PORT` (default `8080`). Docker checks
`/scopenet-panel healthcheck` inside the container. Wait for `healthy` in
`docker compose ps` before directing players to it. If `ADMIN_PASSWORD` is
blank on first start, the generated password appears in the panel logs.
The named `panel-data` volume holds the database, uploads, hosted launcher
downloads, and the generated JWT signing key. Preserve that volume when
upgrading. To back it up, stop the panel, archive the volume with a temporary
container, then start it again:
```bash
docker compose stop panel
docker run --rm -v scopenet-mc_panel-data:/data:ro -v "$PWD":/backup alpine tar czf /backup/panel-data.tar.gz -C /data .
docker compose up -d
```
If your Compose project has a different name, use the volume name shown by
`docker volume ls`. To upgrade, pull the desired commit and run
`docker compose up -d --build` again. Do not use `docker compose down -v` unless
you intend to delete the panel data.
+1 -1
View File
@@ -71,6 +71,6 @@ gradle -p integrations -Ploader=forge -PmcVersion=1.21.1 :forge:build
gradle -p integrations -Ploader=fabric -PmcVersion=26.3 :fabric:build gradle -p integrations -Ploader=fabric -PmcVersion=26.3 :fabric:build
``` ```
Substitute `fabric`/`forge` and the exact version as required. JARs are in the selected module's `build/libs/`. `-PreleaseVersion=X.Y.Z` stamps the artifact and metadata. The **Server integrations** workflow builds the full matrix and uploads each JAR; **Release** attaches them beside the Windows installer. Substitute `fabric`/`forge` and the exact version as required. JARs are in the selected module's `build/libs/`. `-PreleaseVersion=X.Y.Z` stamps the artifact and metadata. The locally built 0.4.0 matrix for 1.20.1, 1.21.1, and 26.3 plus Paper is in [`release-artifacts/0.4.0`](../release-artifacts/0.4.0/README.md), with SHA-256 checksums. The **Server integrations** workflow can rebuild the matrix when Actions usage is available again.
Before production, test a valid login, wrong password, disabled account, denied group, missing launcher launch, panel outage, name-change/reconnect with the same inventory, chat count, heartbeat retry, and restart on a copy of your server world. Build/API tests do not replace a live Minecraft client/server compatibility check with your modpack. Before production, test a valid login, wrong password, disabled account, denied group, missing launcher launch, panel outage, name-change/reconnect with the same inventory, chat count, heartbeat retry, and restart on a copy of your server world. Build/API tests do not replace a live Minecraft client/server compatibility check with your modpack.
@@ -0,0 +1,13 @@
package net.scopenet.minecraft;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
public final class Compat {
private Compat() {}
public static ServerLevel level(ServerPlayer player) { return player.serverLevel(); }
public static String dimension(ServerPlayer player) { return level(player).dimension().location().toString(); }
public static void actionbar(ServerPlayer player, Component message) { player.displayClientMessage(message, true); }
}
@@ -0,0 +1,13 @@
package net.scopenet.minecraft;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
public final class Compat {
private Compat() {}
public static ServerLevel level(ServerPlayer player) { return player.serverLevel(); }
public static String dimension(ServerPlayer player) { return level(player).dimension().location().toString(); }
public static void actionbar(ServerPlayer player, Component message) { player.displayClientMessage(message, true); }
}
@@ -0,0 +1,13 @@
package net.scopenet.minecraft;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
public final class Compat {
private Compat() {}
public static ServerLevel level(ServerPlayer player) { return player.level(); }
public static String dimension(ServerPlayer player) { return level(player).dimension().identifier().toString(); }
public static void actionbar(ServerPlayer player, Component message) { player.sendSystemMessage(message, true); }
}
@@ -78,10 +78,10 @@ public final class Bridge {
Integration current = integration; Integration current = integration;
if (current == null) return false; if (current == null) return false;
if (!current.settings().guildsEnabled() || !current.settings().landClaimingEnabled()) return true; if (!current.settings().guildsEnabled() || !current.settings().landClaimingEnabled()) return true;
String dimension = player.serverLevel().dimension().location().toString(); String dimension = Compat.dimension(player);
ChunkCheckResult check = current.checkChunk(dimension, pos.getX() >> 4, pos.getZ() >> 4, player.getUUID()); ChunkCheckResult check = current.checkChunk(dimension, pos.getX() >> 4, pos.getZ() >> 4, player.getUUID());
if (check.allowed()) return true; if (check.allowed()) return true;
player.displayClientMessage(Component.literal("This chunk is protected by a guild or the claim check is unavailable."), true); Compat.actionbar(player, Component.literal("This chunk is protected by a guild or the claim check is unavailable."));
return false; return false;
} }
@@ -6,6 +6,7 @@ import net.minecraft.world.item.BlockItem;
import net.minecraft.world.item.context.BlockPlaceContext; import net.minecraft.world.item.context.BlockPlaceContext;
import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.BuiltInRegistries;
import net.scopenet.minecraft.Bridge; import net.scopenet.minecraft.Bridge;
import net.scopenet.minecraft.Compat;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.*; import org.spongepowered.asm.mixin.injection.*;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@@ -25,7 +26,7 @@ public abstract class BlockItemMixin {
Bridge.stat(player, "blocks_placed", 1); Bridge.stat(player, "blocks_placed", 1);
String material = BuiltInRegistries.BLOCK.getKey(context.getLevel().getBlockState(context.getClickedPos()).getBlock()) String material = BuiltInRegistries.BLOCK.getKey(context.getLevel().getBlockState(context.getClickedPos()).getBlock())
.getPath().toUpperCase(java.util.Locale.ROOT); .getPath().toUpperCase(java.util.Locale.ROOT);
Bridge.action(player, "block_placed:" + material + "@" + player.serverLevel().dimension().location(), 1); Bridge.action(player, "block_placed:" + material + "@" + Compat.dimension(player), 1);
} }
} }
} }
@@ -6,6 +6,7 @@ import net.minecraft.world.level.block.Block;
import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.EntityType;
import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.BuiltInRegistries;
import net.scopenet.minecraft.Bridge; import net.scopenet.minecraft.Bridge;
import net.scopenet.minecraft.Compat;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.*; import org.spongepowered.asm.mixin.injection.*;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@@ -20,7 +21,7 @@ public abstract class PlayerMixin {
key = "blocks_broken"; key = "blocks_broken";
String material = BuiltInRegistries.BLOCK.getKey((Block) stat.getValue()).getPath().toUpperCase(java.util.Locale.ROOT); String material = BuiltInRegistries.BLOCK.getKey((Block) stat.getValue()).getPath().toUpperCase(java.util.Locale.ROOT);
Bridge.action((ServerPlayer) (Object) this, "block_broken:" + material + "@" Bridge.action((ServerPlayer) (Object) this, "block_broken:" + material + "@"
+ ((ServerPlayer) (Object) this).serverLevel().dimension().location(), amount); + Compat.dimension((ServerPlayer) (Object) this), amount);
} else if (stat.getType() == Stats.ENTITY_KILLED) { } else if (stat.getType() == Stats.ENTITY_KILLED) {
String entity = BuiltInRegistries.ENTITY_TYPE.getKey((EntityType<?>) stat.getValue()).getPath().toUpperCase(java.util.Locale.ROOT); String entity = BuiltInRegistries.ENTITY_TYPE.getKey((EntityType<?>) stat.getValue()).getPath().toUpperCase(java.util.Locale.ROOT);
Bridge.action((ServerPlayer) (Object) this, "mob_kills:" + entity, amount); Bridge.action((ServerPlayer) (Object) this, "mob_kills:" + entity, amount);
+3 -1
View File
@@ -35,7 +35,9 @@ const SERVICE: &str = "net.scopenet.launcher";
fn load_or_create_key(dir: &std::path::Path) -> [u8; 32] { fn load_or_create_key(dir: &std::path::Path) -> [u8; 32] {
let b64 = base64::engine::general_purpose::STANDARD; let b64 = base64::engine::general_purpose::STANDARD;
#[cfg(any(windows, target_os = "macos"))] // Unit tests use their own temporary key file; the OS keychain can prompt
// for desktop access and block unattended checks.
#[cfg(all(not(test), any(windows, target_os = "macos")))]
{ {
if let Ok(entry) = keyring::Entry::new(SERVICE, "secrets-key") { if let Ok(entry) = keyring::Entry::new(SERVICE, "secrets-key") {
if let Ok(existing) = entry.get_password() { if let Ok(existing) = entry.get_password() {
+3
View File
@@ -1,4 +1,7 @@
//! SCOPENET admin panel. //! SCOPENET admin panel.
// SQLx maps several multi-column queries directly to tuples; aliases would
// obscure the selected column order without simplifying the query boundary.
#![allow(clippy::type_complexity)]
pub mod auth; pub mod auth;
pub mod config; pub mod config;
+20
View File
@@ -0,0 +1,20 @@
# Local 0.4.0 build artifacts
These files were built from PR #3 source with GitHub Actions disabled.
- `SCOPENET Launcher_0.4.0_x64-setup.exe`: Windows x64 NSIS installer,
cross-compiled on macOS with Tauri and cargo-xwin. It is unsigned and asks
the player for the panel URL on first launch.
- `scopenet-fabric-{1.20.1,1.21.1,26.3}-0.4.0.jar`: server-side Fabric mods.
- `scopenet-forge-{1.20.1,1.21.1,26.3}-0.4.0.jar`: server-side Forge mods.
- `scopenet-paper-0.4.0.jar`: Paper plugin, built against Spigot API 1.20.1.
The repository has a Forge server mod but no separate NeoForge server mod, so
these artifacts do not include a NeoForge JAR.
The build commands and integration tests passed locally. Every JAR passed ZIP
integrity checks and contains its loader descriptor. `SHA256SUMS` records the
artifact hashes; run `shasum -a 256 -c SHA256SUMS` from this folder to verify.
Minecraft client/server runtime testing and installation of the Windows
installer on Windows still need to be done before a public release.
+8
View File
@@ -0,0 +1,8 @@
e8578861c71edaf4ffe8d6e4edad98c048a0d595e67196041a484eaa2e1bd6fe SCOPENET Launcher_0.4.0_x64-setup.exe
3dd93d31de1bf82d4e5e81a6f964b6f7179b44f4cc47e8ebed7e9751c8022d48 scopenet-fabric-1.20.1-0.4.0.jar
fb7ac37ed2313ec489f0b355b074904809fdec6a271b4c1e0d545f6f88cde9e2 scopenet-fabric-1.21.1-0.4.0.jar
2ef7eef23bfbe6731372cba76a433e3201d3a080d24d0567094b1c59f998740b scopenet-fabric-26.3-0.4.0.jar
1c8256bb3cef5ebc44090b891e1588c04c78158170b81b2a853a11f8b64b4561 scopenet-forge-1.20.1-0.4.0.jar
e94fbd0bc76a8a93a284d6bbf8a52db8fa014dd702c4159278bee0dfe71afdaa scopenet-forge-1.21.1-0.4.0.jar
9a1f4c5796f582a7c636226b72467fc5b734519a0ac845c6c2b33ba0898b738c scopenet-forge-26.3-0.4.0.jar
75560c64d08b512dc53845a0da7de468d01951963af02d10fd0b236f1e71e4f8 scopenet-paper-0.4.0.jar
Binary file not shown.