Skip to content

Commit 0fbfbeb

Browse files
committed
feat: clarify connection fields and build for macOS and Linux
Setting up a connection required guessing what each field referred to, and the two editions sharing one form made it worse. The cause is that nothing said what the fields describe: Admincraft always connects to the WebSocket bridge, never to Minecraft, on both editions. Choosing Java changes which backend the bridge drives, and the RCON host, port and password live in the bridge's own compose file where RCON stays on the internal network. Name that in the form. The section says the fields describe the bridge, the host, port and key fields say so in their labels, and each carries the value to expect and where it comes from. The edition selector states that Java's RCON details are configured on the bridge, which is the detail that made the shared form confusing. Add a guide covering every field, why Java asks for no RCON details, a worked example per edition, and a checklist that rules out the usual mistakes: the game port instead of the bridge port, the RCON password instead of the bridge key, a security mode that cannot match the address. The editor now links there rather than to the security page, since that is the question in front of someone filling the form in. Releases also gain macOS, Linux and a real Windows installer, matching what palette-studio publishes: dmg and zip for both Apple architectures, deb and AppImage, and an Inno Setup installer beside the portable zip. Artifacts are collected by pattern so another platform needs no change to the release job, and fail-fast is off so one platform cannot hide the others.
1 parent c10a7bd commit 0fbfbeb

7 files changed

Lines changed: 362 additions & 41 deletions

File tree

.github/workflows/build-and-release-app.yml

Lines changed: 112 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -42,97 +42,177 @@ jobs:
4242
fi
4343
echo "version=$VERSION" >> $GITHUB_OUTPUT
4444
echo "prerelease=$PRERELEASE" >> $GITHUB_OUTPUT
45+
4546
build:
4647
needs: get-version
4748
runs-on: ${{ matrix.os }}
4849
strategy:
50+
# One platform failing should not hide whether the others work.
51+
fail-fast: false
4952
matrix:
5053
include:
5154
- platform: android
5255
os: ubuntu-latest
53-
build_cmd: flutter build apk --release
54-
output: build/app/outputs/flutter-apk/app-release.apk
55-
ext: apk
56+
artifact: android
5657
- platform: windows
5758
os: windows-latest
58-
build_cmd: flutter build windows --release
59-
output: build/windows/x64/runner/Release/admincraft.zip
60-
ext: zip
59+
artifact: windows
60+
- platform: macos
61+
os: macos-latest
62+
artifact: macos-arm64
63+
- platform: macos
64+
os: macos-26-intel
65+
artifact: macos-x64
66+
- platform: linux
67+
os: ubuntu-latest
68+
artifact: linux-amd64
6169
steps:
6270
- uses: actions/checkout@v5
6371
with:
6472
fetch-depth: 0
73+
6574
- name: Set up Flutter
6675
uses: subosito/flutter-action@v2
6776
with:
6877
flutter-version: 3.47.0
78+
79+
# Flutter's Linux build needs the GTK toolchain, which the runner image
80+
# does not carry by default.
81+
- name: Install Linux build dependencies
82+
if: matrix.platform == 'linux'
83+
run: |
84+
sudo apt-get update
85+
sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev libstdc++-12-dev
86+
6987
- name: Install dependencies
7088
run: flutter pub get
71-
- name: Install zip (Windows only)
72-
if: matrix.platform == 'windows'
73-
run: choco install zip -y
89+
7490
- name: Build Android
7591
if: matrix.platform == 'android'
7692
run: >-
7793
flutter build apk --release
7894
--dart-define=ADMINCRAFT_GOOGLE_WEB_CLIENT_ID=${{ vars.ADMINCRAFT_GOOGLE_WEB_CLIENT_ID }}
95+
7996
- name: Build Windows
8097
if: matrix.platform == 'windows'
8198
run: >-
8299
flutter build windows --release
83100
--dart-define=ADMINCRAFT_GOOGLE_DESKTOP_CLIENT_ID=${{ vars.ADMINCRAFT_GOOGLE_DESKTOP_CLIENT_ID }}
84101
--dart-define=ADMINCRAFT_GOOGLE_DESKTOP_CLIENT_SECRET=${{ secrets.ADMINCRAFT_GOOGLE_DESKTOP_CLIENT_SECRET }}
85-
- name: Prepare Windows zip
102+
103+
- name: Build macOS
104+
if: matrix.platform == 'macos'
105+
run: >-
106+
flutter build macos --release
107+
--dart-define=ADMINCRAFT_GOOGLE_DESKTOP_CLIENT_ID=${{ vars.ADMINCRAFT_GOOGLE_DESKTOP_CLIENT_ID }}
108+
--dart-define=ADMINCRAFT_GOOGLE_DESKTOP_CLIENT_SECRET=${{ secrets.ADMINCRAFT_GOOGLE_DESKTOP_CLIENT_SECRET }}
109+
110+
- name: Build Linux
111+
if: matrix.platform == 'linux'
112+
run: >-
113+
flutter build linux --release
114+
--dart-define=ADMINCRAFT_GOOGLE_DESKTOP_CLIENT_ID=${{ vars.ADMINCRAFT_GOOGLE_DESKTOP_CLIENT_ID }}
115+
--dart-define=ADMINCRAFT_GOOGLE_DESKTOP_CLIENT_SECRET=${{ secrets.ADMINCRAFT_GOOGLE_DESKTOP_CLIENT_SECRET }}
116+
117+
- name: Package Android
118+
if: matrix.platform == 'android'
119+
shell: bash
120+
run: |
121+
mkdir -p dist
122+
mv build/app/outputs/flutter-apk/app-release.apk "dist/admincraft-${VERSION}-android.apk"
123+
env:
124+
VERSION: ${{ needs.get-version.outputs.version }}
125+
126+
# Portable zip plus an Inno Setup installer, so the download page offers
127+
# the same pair as every other desktop platform.
128+
- name: Package Windows
86129
if: matrix.platform == 'windows'
87130
shell: bash
88131
run: |
89-
cd build/windows/x64/runner/Release
90-
zip -r admincraft.zip *
91-
- name: Move output to build dir with version
132+
mkdir -p dist
133+
DIST="$PWD/dist"
134+
RELEASE_DIR="$PWD/build/windows/x64/runner/Release"
135+
( cd "$RELEASE_DIR" && 7z a -tzip "$DIST/admincraft-${VERSION}-windows-portable.zip" ./* )
136+
137+
# Inno Setup ships on the runner image, but the path has moved between
138+
# image versions, so look for it before falling back to installing it.
139+
ISCC=""
140+
for candidate in "/c/Program Files (x86)/Inno Setup 6/ISCC.exe" "/c/Program Files/Inno Setup 6/ISCC.exe"; do
141+
[ -x "$candidate" ] && ISCC="$candidate" && break
142+
done
143+
if [ -z "$ISCC" ]; then
144+
choco install innosetup -y --no-progress
145+
ISCC="/c/Program Files (x86)/Inno Setup 6/ISCC.exe"
146+
fi
147+
echo "Using $ISCC"
148+
149+
"$ISCC" "//DAppVersion=${VERSION#v}" "//DSourceDir=${RELEASE_DIR}" "//DOutputDir=${DIST}" windows/packaging/admincraft.iss
150+
mv dist/admincraft-setup.exe "dist/admincraft-${VERSION}-windows-installer.exe"
151+
env:
152+
VERSION: ${{ needs.get-version.outputs.version }}
153+
154+
# Unsigned: without an Apple Developer certificate Gatekeeper asks the user
155+
# to allow the app on first launch. Signing would need a MAC_CSC_LINK
156+
# secret and an Apple Developer account.
157+
- name: Package macOS
158+
if: matrix.platform == 'macos'
92159
shell: bash
93160
run: |
94161
mkdir -p dist
95-
mv ${{ matrix.output }} dist/admincraft-${{ matrix.platform }}-${{ needs.get-version.outputs.version }}.${{ matrix.ext }}
162+
APP=build/macos/Build/Products/Release/admincraft.app
163+
BASE="dist/admincraft-${VERSION}-${{ matrix.artifact }}"
164+
ditto -c -k --keepParent "$APP" "${BASE}-portable.zip"
165+
hdiutil create -volname Admincraft -srcfolder "$APP" -ov -format UDZO "${BASE}-installer.dmg"
166+
env:
167+
VERSION: ${{ needs.get-version.outputs.version }}
168+
169+
- name: Package Linux
170+
if: matrix.platform == 'linux'
171+
shell: bash
172+
run: |
173+
linux/packaging/build-packages.sh "$VERSION" build/linux/x64/release/bundle dist
174+
env:
175+
VERSION: ${{ needs.get-version.outputs.version }}
176+
96177
- name: Upload artifact
97178
uses: actions/upload-artifact@v4
98179
with:
99-
name: admincraft-${{ matrix.platform }}-${{ needs.get-version.outputs.version }}
100-
path: dist/admincraft-${{ matrix.platform }}-${{ needs.get-version.outputs.version }}.${{ matrix.ext }}
180+
name: admincraft-${{ needs.get-version.outputs.version }}-${{ matrix.artifact }}
181+
path: dist/*
182+
if-no-files-found: error
183+
101184
release:
102185
needs: [get-version, build]
103186
runs-on: ubuntu-latest
104187
steps:
105188
- uses: actions/checkout@v5
106189
with:
107190
fetch-depth: 0
108-
- name: Collect built files
109-
run: |
110-
mkdir -p dist
111-
- name: Download android artifact
112-
uses: actions/download-artifact@v4
113-
with:
114-
name: admincraft-android-${{ needs.get-version.outputs.version }}
115-
path: dist
116-
- name: Download windows artifact
191+
192+
# Collected by pattern rather than one step per platform, so adding a
193+
# platform to the matrix above needs no change here.
194+
- name: Download all artifacts
117195
uses: actions/download-artifact@v4
118196
with:
119-
name: admincraft-windows-${{ needs.get-version.outputs.version }}
197+
pattern: admincraft-${{ needs.get-version.outputs.version }}-*
120198
path: dist
199+
merge-multiple: true
200+
201+
- name: List collected files
202+
run: ls -lh dist
203+
121204
- name: Create GitHub Release
122205
uses: softprops/action-gh-release@v1
123206
with:
124207
tag_name: ${{ needs.get-version.outputs.version }}
125208
name: Release ${{ needs.get-version.outputs.version }}
126209
prerelease: ${{ needs.get-version.outputs.prerelease }}
127210
generate_release_notes: true
128-
files: |
129-
dist/admincraft-android-${{ needs.get-version.outputs.version }}.apk
130-
dist/admincraft-windows-${{ needs.get-version.outputs.version }}.zip
211+
files: dist/*
131212
env:
132213
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
214+
133215
- name: Delete build artifacts from GitHub
134216
uses: geekyeggo/delete-artifact@v5
135217
with:
136-
name: |
137-
admincraft-android-${{ needs.get-version.outputs.version }}
138-
admincraft-windows-${{ needs.get-version.outputs.version }}
218+
name: admincraft-${{ needs.get-version.outputs.version }}-*

docs/guides/connection-fields.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# What each connection field means
2+
3+
One idea makes the rest of this page obvious:
4+
5+
!!! tip "Every field describes the bridge, not the Minecraft server"
6+
Admincraft never talks to Minecraft directly. It talks to the **Admincraft WebSocket bridge**, and the bridge talks to Minecraft. So **Host**, **Port** and **Secret key** always describe the bridge container, on both editions.
7+
8+
This is the part that catches people out. A Java server has an RCON port and an RCON password, and neither of them goes in Admincraft.
9+
10+
## The fields
11+
12+
| Field | What it is | Where the value comes from |
13+
| --- | --- | --- |
14+
| **Alias** | A label for your own benefit. Shown in the server picker. | Anything you like. |
15+
| **Minecraft edition** | Which kind of server the bridge should drive. Bedrock uses the container console; Java uses RCON. | Match your server. |
16+
| **Host or IP of the bridge** | The machine running the `websocket` container. | Tailscale address, `ts.net` hostname, or public IP. |
17+
| **Bridge port** | The port the bridge listens on. | `8080` normally, `443` behind Tailscale Funnel. |
18+
| **Bridge secret key** | The bridge's own key, used to sign the token Admincraft sends. | `SECRET_KEY` in the bridge's `docker-compose.yml`. |
19+
| **Connection security** | How the app-to-bridge hop is protected. | See [connection security](connection-security.md). |
20+
21+
## Why Java does not ask for RCON details
22+
23+
Choosing **Java Edition** changes which backend the *bridge* uses, not where Admincraft connects. The RCON host, port and password are configured on the bridge:
24+
25+
```yaml
26+
websocket:
27+
environment:
28+
SECRET_KEY: YOUR_SECRET_KEY_HERE # ← this is the "Bridge secret key"
29+
RCON_HOST: minecraft
30+
RCON_PORT: "25575"
31+
RCON_PASSWORD: CHANGE_THIS_RCON_PASSWORD
32+
```
33+
34+
RCON stays on the internal Docker network and is never published to a host port. That is deliberate: RCON has no encryption, so exposing it would hand out server control in plain text.
35+
36+
So the edition selector is a statement about your server, and the RCON password never leaves the machine it runs on.
37+
38+
## Worked example: Bedrock over Tailscale Funnel
39+
40+
The setup from the [Bedrock guide](../server/SERVER_SETUP.md#alternative-tailscale-funnel-no-app-on-the-client):
41+
42+
| Field | Value |
43+
| --- | --- |
44+
| Minecraft edition | `Bedrock Edition` |
45+
| Host or IP of the bridge | `my-server.tailnet-name.ts.net` |
46+
| Bridge port | `443` |
47+
| Bridge secret key | the `SECRET_KEY` from your compose file |
48+
| Connection security | `Public certificate` |
49+
50+
The address preview under the dropdown should read `wss://my-server.tailnet-name.ts.net:443`.
51+
52+
## Worked example: Java on a private Tailscale network
53+
54+
| Field | Value |
55+
| --- | --- |
56+
| Minecraft edition | `Java Edition` |
57+
| Host or IP of the bridge | `100.101.102.103` |
58+
| Bridge port | `8080` |
59+
| Bridge secret key | the `SECRET_KEY` from your compose file |
60+
| Connection security | `Private network` |
61+
62+
The preview reads `ws://100.101.102.103:8080`. That is unencrypted by design, and safe only because Tailscale already encrypts the route. It will not work from the hosted web app: see [using the web app](web-app.md#tailscale-in-the-web-app).
63+
64+
## If it will not connect
65+
66+
Work down the chain, since each step rules out everything before it:
67+
68+
1. **Is the preview the address you expect?** It is shown live under the security dropdown.
69+
2. **Is the port the bridge's port?** Not `19132` (Bedrock game), not `25575` (Java RCON).
70+
3. **Is the key the bridge's `SECRET_KEY`?** Not the RCON password, not the Minecraft allowlist.
71+
4. **Does the security mode match the address?** `Private network` gives `ws://` and only works over a private route. `Public certificate` needs a certificate the device already trusts.
72+
5. **Can the device reach the host at all?** With Tailscale, both ends must be on the tailnet and connected.

lib/views/server_editor_view.dart

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,12 @@ class _ServerEditorViewState extends State<ServerEditorView> {
201201
initialValue: _edition,
202202
isExpanded: true,
203203
decoration: const InputDecoration(
204-
labelText: 'Minecraft edition'),
204+
labelText: 'Minecraft edition',
205+
helperMaxLines: 3,
206+
helperText:
207+
'Which server the bridge controls. Java uses RCON, '
208+
'configured on the bridge itself, not here.',
209+
),
205210
items: MinecraftEdition.values
206211
.map((value) => DropdownMenuItem(
207212
value: value,
@@ -232,7 +237,10 @@ class _ServerEditorViewState extends State<ServerEditorView> {
232237
const SizedBox(height: 14),
233238
_SectionCard(
234239
title: 'Connection',
235-
subtitle: 'How Admincraft reaches the WebSocket bridge.',
240+
subtitle:
241+
'Every field here describes the Admincraft WebSocket '
242+
'bridge, never the Minecraft server itself. The bridge is '
243+
'what reaches Minecraft, on both editions.',
236244
child: Column(
237245
crossAxisAlignment: CrossAxisAlignment.stretch,
238246
children: [
@@ -241,8 +249,13 @@ class _ServerEditorViewState extends State<ServerEditorView> {
241249
final wide = constraints.maxWidth >= 580;
242250
final host = TextFormField(
243251
controller: _hostController,
244-
decoration:
245-
const InputDecoration(labelText: 'Host or IP'),
252+
decoration: const InputDecoration(
253+
labelText: 'Host or IP of the bridge',
254+
helperMaxLines: 3,
255+
helperText:
256+
'Where the websocket container runs. A Tailscale '
257+
'address, a ts.net hostname, or a public IP.',
258+
),
246259
onChanged: (_) => setState(() {}),
247260
validator: (value) =>
248261
value == null || value.trim().isEmpty
@@ -251,8 +264,11 @@ class _ServerEditorViewState extends State<ServerEditorView> {
251264
);
252265
final port = TextFormField(
253266
controller: _portController,
254-
decoration:
255-
const InputDecoration(labelText: 'Port'),
267+
decoration: const InputDecoration(
268+
labelText: 'Bridge port',
269+
helperMaxLines: 3,
270+
helperText: '8080 normally. 443 behind Funnel.',
271+
),
256272
keyboardType: TextInputType.number,
257273
onChanged: (_) => setState(() {}),
258274
validator: (value) {
@@ -282,7 +298,12 @@ class _ServerEditorViewState extends State<ServerEditorView> {
282298
enableSuggestions: false,
283299
autocorrect: false,
284300
decoration: InputDecoration(
285-
labelText: 'Secret key',
301+
labelText: 'Bridge secret key',
302+
helperMaxLines: 3,
303+
helperText:
304+
'SECRET_KEY from the bridge docker-compose.yml. On '
305+
'Java this is still the bridge key, not the RCON '
306+
'password.',
286307
suffixIcon: IconButton(
287308
tooltip: _secretVisible ? 'Hide key' : 'Show key',
288309
icon: Icon(_secretVisible
@@ -333,10 +354,10 @@ class _ServerEditorViewState extends State<ServerEditorView> {
333354
),
334355
TextButton.icon(
335356
onPressed: () => UrlUtils.openDocumentation(
336-
'guides/connection-security/'),
357+
'guides/connection-fields/'),
337358
icon: const Icon(Icons.menu_book_outlined,
338359
size: 18),
339-
label: const Text('Connection security guide'),
360+
label: const Text('What do these fields mean?'),
340361
),
341362
],
342363
),

linux/packaging/admincraft.desktop

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[Desktop Entry]
2+
Type=Application
3+
Name=Admincraft
4+
Comment=Manage Minecraft Bedrock and Java servers
5+
Exec=admincraft
6+
Icon=admincraft
7+
Categories=Game;Utility;
8+
Terminal=false
9+
StartupWMClass=admincraft

0 commit comments

Comments
 (0)