Build local release artifacts and prepare Compose deployment [skip ci]
This commit is contained in:
1 parent
6deff62a3a
commit
52de8c3031
25 files changed
+137
-23
No files matched your search
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -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);
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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.
|
||||||
Binary file not shown.
@@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in new issue
Block a user