Skip to content

Commit c6e527e

Browse files
committed
merge: clarify connection fields and add macOS/Linux/Windows-installer builds
2 parents b537a38 + 0fbfbeb commit c6e527e

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)