Repository navigation
920 lines (828 loc) · 44.5 KB
/
Copy pathrelease.yml
File metadata and controls
920 lines (828 loc) · 44.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
# Builds the Windows, Linux + (experimental) macOS players and publishes a GitHub Release.
#
# Triggered ONLY by pushing a version tag (or the manual "Run workflow" button), never by
# pull_request — so forked PRs can never reach the Unity license secret. In a PUBLIC repo,
# GitHub secrets stay masked in logs and are unavailable to fork PRs, so UNITY_LICENSE is safe.
#
# One-time setup (Unity Personal):
# 1. Get your license file: run the GameCI activation flow
# (https://game.ci/docs/github/activation) to obtain a Unity_v6.x.ulf.
# 2. Repo -> Settings -> Secrets and variables -> Actions -> add three repository secrets:
# UNITY_LICENSE = the full contents of that .ulf file
# UNITY_EMAIL = your Unity account email
# UNITY_PASSWORD = your Unity account password
# 3. Push a tag: git tag v1.0.0 && git push origin v1.0.0
#
# Why split jobs: the Unity build runs in GameCI's Linux Docker image (Windows, Linux and macOS
# players are all cross-built with the Mono backend — macOS too, so no Mac runner is needed). The
# WinForms launcher needs the Windows Desktop SDK, so it is built on a windows-latest runner and
# merged into the Windows player afterwards. Per-platform package jobs publish the installers/zips,
# and a final job builds the optional dedicated-server Docker image (reusable docker.yml) and
# pushes it to GHCR on a tag.
#
# macOS is EXPERIMENTAL: the .app is unsigned and un-notarized (Phase 1 = plain zip, no Velopack,
# no splash launcher). Users clear the Gatekeeper quarantine once — see README "Platforms".
name: Release
on:
push:
tags: ['v*']
workflow_dispatch:
permissions:
contents: write # create the GitHub Release
# Serialize release runs: a second tag push (or a manual dispatch landing on top of one) queues rather than
# racing the first. We do NOT cancel-in-progress here (unlike ci.yml) — a half-published release is worse
# than a slightly delayed one, so an in-flight run is allowed to finish.
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
# Single source of truth for the version, resolved ONCE and consumed by every build/package/docker job
# (so the resolve+validate logic is not copy-pasted per platform). A tag push (vX.Y.Z) defines the version
# for the whole build — in-game (Application.version), the launcher and the Velopack installers all derive
# from it. A manual workflow_dispatch run has no tag, so it builds a 0.1.0-dev test artifact (and never
# publishes). (Velopack requires packVersion >= 0.0.1, so the dev value can't be 0.0.0-*.) No checkout
# needed — it only reads GITHUB_REF, so this job is a few seconds.
version:
name: Resolve version
runs-on: ubuntu-latest
outputs:
version: ${{ steps.resolve.outputs.version }}
steps:
- name: Resolve + validate version
id: resolve
shell: bash
run: |
if [ "$GITHUB_REF_TYPE" = "tag" ]; then
v="${GITHUB_REF_NAME#v}"
if ! printf '%s' "$v" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$'; then
echo "::error::Tag '$GITHUB_REF_NAME' is not SemVer — expected vX.Y.Z (e.g. v0.3.0)."
exit 1
fi
else
v="0.1.0-dev"
fi
echo "version=$v" >> "$GITHUB_OUTPUT"
echo "Resolved build version: $v"
# SINGLE WRITER for the GitHub Release: creates it exactly once, as soon as the test gate passes, so
# the per-platform publish steps below only ever ATTACH assets to an existing release. Without this,
# the first two package jobs to finish raced to create the release (softprops creates one whenever its
# tag lookup comes back empty) — the loser ended up with an orphaned duplicate draft holding its assets
# and a failed "already_exists" finalize (seen on v0.7.0, where the Linux AppImage landed on the draft).
# The body set here is PREPENDED to the auto-generated changelog (GitHub API: a supplied body is kept
# and the generated notes are appended after it), so the Docker pull lines show at the top. The image
# itself is pushed to GHCR by the `docker` job — it is NOT a release asset, so this is how it surfaces
# on the release page. On a manual workflow_dispatch run (no tag) the step is skipped and the job is a
# cheap no-op success, so the package jobs' `needs` chains stay intact.
create-release:
name: Create GitHub release
needs: [version, test]
runs-on: ubuntu-latest
steps:
- name: Create the release for this tag (idempotent)
if: startsWith(github.ref, 'refs/tags/')
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
tag="$GITHUB_REF_NAME"
if gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
echo "Release $tag already exists — nothing to create."
exit 0
fi
body=$(cat <<'EOF'
### 🐳 Docker (dedicated server)
Run the dedicated server (game server + admin/portal/download UI, optional AI backend) on any OS with Docker:
```bash
docker pull ghcr.io/marceld23/blocks-beyond-the-stars-server:${{ needs.version.outputs.version }}
# or the rolling tag:
docker pull ghcr.io/marceld23/blocks-beyond-the-stars-server:latest
```
Setup & configuration: [SELF_HOSTING.md §10](https://github.com/marceld23/BlocksBeyondTheStars/blob/main/docs/developer/SELF_HOSTING.md#10-running-in-docker).
EOF
)
# The heredoc lines above share the script's own indentation, which the YAML block scalar strips —
# so the terminator is recognized and the markdown reaches GitHub column-0 clean.
gh release create "$tag" --repo "$GITHUB_REPOSITORY" --title "$tag" \
--verify-tag --generate-notes --notes "$body"
# RELEASE TEST GATE: the FULL headless test suite on the exact tagged commit. ci.yml's PR check runs
# only the fast tier (Category!=Slow), and nothing ties a tag to a green CI run on main — this job
# closes that gap: every package/publish job (installers, itch.io, Docker image) `needs` it, so no
# asset ships unless all tests pass. It is the same sharded matrix ci.yml uses (reusable
# tests.yml, #1067), tier full and without -warnaserror (warnings are a PR-gate concern; this gate is
# about behaviour). Sharded it finishes well inside the ~30 min WebGL player build, so it adds no
# wall-clock time to a release — on a single runner it took 33–36 min and, with the Docker image
# queued behind it, made every release ~57 min instead of ~35. Per-shard .trx artifacts land as
# release-test-results-<shard>.
test:
name: Full test suite (release gate)
uses: ./.github/workflows/tests.yml
with:
tier: full
warnaserror: false
artifact-prefix: release-test-results
build-player:
name: Build Unity player (Windows)
needs: version
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Set up .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: '10.0.x'
# StreamingAssets/ (data + web + bundled server) and Plugins/ (shared libs) are gitignored
# generated content, so they must be produced before the Unity build. pwsh ships on the runner.
- name: Sync shared libs + content
shell: pwsh
run: ./scripts/sync-client-libs.ps1
# The client's in-app updater (ClientUpdater.cs) references Velopack in the player build
# (#if !UNITY_EDITOR). Velopack is a client-only dep with its own sync step; without it the
# StandaloneWindows64 script compile fails with CS0246 'Velopack'. Must run after sync-client-libs
# so it skips the System.* facades that script already placed.
- name: Sync Velopack libs
shell: pwsh
run: ./scripts/sync-velopack-libs.ps1
- name: Publish bundled local server (win-x64)
shell: pwsh
run: ./scripts/publish-local-server.ps1 -Runtime win-x64 -Version ${{ needs.version.outputs.version }}
# The key MUST carry the platform. Unity's Library is per-build-target, and this key used to be a
# bare `Library-` / `restore-keys: Library-`, which prefix-matched `Library-WebGL-…` — so this job
# downloaded 1.7 GB of WebGL Library that Unity then discarded and re-imported for Windows (#528).
# Nothing writes a `Library-Windows-` cache today (see build-player-webgl for why only WebGL saves),
# so this restore is normally a clean miss — which is correct, and far cheaper than the wrong hit.
- name: Restore Unity Library cache
uses: actions/cache/restore@v5
with:
path: client/Library
key: Library-Windows-${{ hashFiles('client/Assets/**', 'client/Packages/**', 'client/ProjectSettings/**') }}
restore-keys: |
Library-Windows-
# The ~6 GB GameCI editor image plus the restored Library cache overflow the hosted runner's
# ~14 GB free disk ("no space left on device"). Drop preinstalled toolchains we don't need
# (our .NET steps already ran) to free ~25 GB before the Docker pull.
#
# No `docker image prune` here: a hosted runner boots with a near-empty local image store, so it
# reclaimed nothing while costing 0.6-1.4 min per job (~3.5 min per release across the four Unity
# jobs, on the WebGL critical path). The rm -rf above does all the real work (#532).
- name: Free disk space for the Unity image
run: |
echo "Before:"; df -h /
sudo rm -rf /usr/local/lib/android /opt/ghc /usr/share/dotnet \
/opt/hostedtoolcache/CodeQL /usr/local/share/boost /usr/local/.ghcup
echo "After:"; df -h /
- name: Inject player-feedback API key
# Writes the git-ignored BugReportBuildSecrets.Generated.cs so the player can POST feedback to the
# report inbox (ReportHost on the VPS — docs/developer/REPORT_HOST.md; the secret = its write key).
# Guarded: without the secret the build still works (feedback just stays disabled). The key
# is only a spam gate, not a real secret; GitHub masks it in logs.
env:
BBS_BUGREPORT_API_KEY: ${{ secrets.BBS_BUGREPORT_API_KEY }}
run: |
if [ -z "$BBS_BUGREPORT_API_KEY" ]; then
echo "BBS_BUGREPORT_API_KEY not set — player feedback will be disabled in this build."
exit 0
fi
{
echo '// CI-generated from the BBS_BUGREPORT_API_KEY secret — not committed.'
echo 'namespace BlocksBeyondTheStars.Build'
echo '{'
echo ' public static partial class BugReportBuildSecrets'
echo ' {'
echo " static partial void ApplyApiKey(ref string key) => key = \"$BBS_BUGREPORT_API_KEY\";"
echo ' }'
echo '}'
} > client/Assets/BlocksBeyondTheStars/Scripts/BugReportBuildSecrets.Generated.cs
echo "Injected feedback API key into the build."
- name: Build Windows player
uses: game-ci/unity-builder@ae0171202cd8b5ea3fc3f87b03bb6e676e4ea17e # v6.0.0 + #852: thin wrapper over game-ci/cli, which runs the editor with --shm-size (Unity 6.6 needs it, game-ci/unity-builder#840)
env:
# Unity Personal needs all three: the .ulf contents + the account email & password.
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
with:
cliVersion: v0.1.72 # the game-ci/cli release the wrapper installs — pinned like every action
projectPath: client
unityVersion: 6000.6.5f1
targetPlatform: StandaloneWindows64
buildMethod: BlocksBeyondTheStars.Client.EditorTools.BuildScript.BuildWindows
# GameCI always applies its versioning to PlayerSettings.bundleVersion (overriding anything our
# build method sets), so we drive it directly: Custom + our exact resolved version (the git tag).
# This makes Application.version in-game == the release version. BuildScript writes version.txt
# from the resulting bundleVersion, which the guard checks.
versioning: Custom
version: ${{ needs.version.outputs.version }}
# BuildScript reads -buildOut and writes version.txt from the bundleVersion GameCI applied above.
# allowDirtyBuild because the synced StreamingAssets/Plugins make the tree "dirty" for GameCI.
customParameters: -buildOut /github/workspace/player
allowDirtyBuild: true
# GameCI runs Unity as root; make the output readable by the (non-root) runner user.
- name: Fix output permissions
run: sudo chmod -R a+rwX player
# Guard: the version baked into the player must equal the resolved tag version (single-source-of-truth).
- name: Verify baked version matches the tag
shell: bash
run: |
baked="$(cat player/version.txt)"
echo "Baked into player: '$baked' — expected: '${{ needs.version.outputs.version }}'"
test "$baked" = "${{ needs.version.outputs.version }}"
- name: Upload player artifact
uses: actions/upload-artifact@v6
with:
name: player-windows
path: player
# Intermediate hand-off to the packaging job in the SAME run; the shipped copy is attached to the
# GitHub Release. 703 artifacts / 7.4 GB had piled up at the 90-day default (#533). Storage is
# free on a public repo — this is about keeping the run pages and the storage view usable.
retention-days: 7
build-player-linux:
name: Build Unity player (Linux)
needs: version
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Set up .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: '10.0.x'
- name: Sync shared libs + content (bash)
run: ./scripts/sync-client-libs.sh
- name: Sync Velopack libs (bash)
run: ./scripts/sync-velopack-libs.sh
- name: Publish bundled local server (linux-x64)
run: ./scripts/publish-local-server.sh linux-x64 ${{ needs.version.outputs.version }}
# Restore-only, and in practice always a miss: nothing writes a `Library-Linux-` cache. Only WebGL
# saves one, because only WebGL is on the release critical path — see build-player-webgl (#528).
# Kept so this job picks the cache up for free if a warming run is ever added for this platform.
- name: Restore Unity Library cache
uses: actions/cache/restore@v5
with:
path: client/Library
key: Library-Linux-${{ hashFiles('client/Assets/**', 'client/Packages/**', 'client/ProjectSettings/**') }}
restore-keys: |
Library-Linux-
- name: Free disk space for the Unity image
run: |
echo "Before:"; df -h /
sudo rm -rf /usr/local/lib/android /opt/ghc /usr/share/dotnet \
/opt/hostedtoolcache/CodeQL /usr/local/share/boost /usr/local/.ghcup
echo "After:"; df -h /
- name: Inject player-feedback API key
env:
BBS_BUGREPORT_API_KEY: ${{ secrets.BBS_BUGREPORT_API_KEY }}
run: |
if [ -z "$BBS_BUGREPORT_API_KEY" ]; then
echo "BBS_BUGREPORT_API_KEY not set — player feedback will be disabled in this build."
exit 0
fi
{
echo '// CI-generated from the BBS_BUGREPORT_API_KEY secret — not committed.'
echo 'namespace BlocksBeyondTheStars.Build'
echo '{'
echo ' public static partial class BugReportBuildSecrets'
echo ' {'
echo " static partial void ApplyApiKey(ref string key) => key = \"$BBS_BUGREPORT_API_KEY\";"
echo ' }'
echo '}'
} > client/Assets/BlocksBeyondTheStars/Scripts/BugReportBuildSecrets.Generated.cs
echo "Injected feedback API key into the build."
- name: Build Linux player
uses: game-ci/unity-builder@ae0171202cd8b5ea3fc3f87b03bb6e676e4ea17e # v6.0.0 + #852: thin wrapper over game-ci/cli, which runs the editor with --shm-size (Unity 6.6 needs it, game-ci/unity-builder#840)
env:
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
with:
cliVersion: v0.1.72 # the game-ci/cli release the wrapper installs — pinned like every action
projectPath: client
unityVersion: 6000.6.5f1
targetPlatform: StandaloneLinux64
buildMethod: BlocksBeyondTheStars.Client.EditorTools.BuildScript.BuildLinux
versioning: Custom
version: ${{ needs.version.outputs.version }}
customParameters: -buildOut /github/workspace/player-linux
allowDirtyBuild: true
- name: Fix output permissions
run: sudo chmod -R a+rwX player-linux
- name: Verify baked version matches the tag
shell: bash
run: |
baked="$(cat player-linux/version.txt)"
echo "Baked into player: '$baked' — expected: '${{ needs.version.outputs.version }}'"
test "$baked" = "${{ needs.version.outputs.version }}"
# The "Free disk space" step above deletes /usr/share/dotnet to make room for the Unity image, so the
# SDK is gone by now. Re-install it before publishing the launcher (cheap; the heavy build is done).
- name: Re-install .NET (disk-cleanup removed it)
uses: actions/setup-dotnet@v5
with:
dotnet-version: '10.0.x'
- name: Build Linux console launcher
run: |
dotnet publish src/BlocksBeyondTheStars.Launcher.Console/BlocksBeyondTheStars.Launcher.Console.csproj \
-c Release -r linux-x64 --self-contained true \
-p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true \
-p:Version=${{ needs.version.outputs.version }} \
-o artifacts/launcher-linux
cp artifacts/launcher-linux/BlocksBeyondTheStars.Launcher.Console player-linux/
- name: Upload Linux player artifact
uses: actions/upload-artifact@v6
with:
name: player-linux
path: player-linux
# Intermediate hand-off / already on the Release — see player-windows above (#533).
retention-days: 7
# macOS player (experimental). Built on a Linux runner: the project uses the Mono scripting backend, so
# GameCI cross-compiles StandaloneOSX without Mac hardware. The result is an unsigned/un-notarized .app
# bundle (Phase 1: zipped as-is, no Velopack, no splash launcher). Mirrors build-player-linux step-for-step.
build-player-mac:
name: Build Unity player (macOS)
needs: version
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Set up .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: '10.0.x'
- name: Sync shared libs + content (bash)
run: ./scripts/sync-client-libs.sh
- name: Sync Velopack libs (bash)
run: ./scripts/sync-velopack-libs.sh
- name: Publish bundled local server (osx-x64)
run: ./scripts/publish-local-server.sh osx-x64
# Restore-only, and in practice a miss: the only writer of `Library-macOS-` is macos-build.yml, which
# is workflow_dispatch-only, so the cache exists just after someone ran it by hand. This workflow does
# not save one — only WebGL is on the release critical path, see build-player-webgl (#528).
- name: Restore Unity Library cache
uses: actions/cache/restore@v5
with:
path: client/Library
key: Library-macOS-${{ hashFiles('client/Assets/**', 'client/Packages/**', 'client/ProjectSettings/**') }}
restore-keys: |
Library-macOS-
- name: Free disk space for the Unity image
run: |
echo "Before:"; df -h /
sudo rm -rf /usr/local/lib/android /opt/ghc /usr/share/dotnet \
/opt/hostedtoolcache/CodeQL /usr/local/share/boost /usr/local/.ghcup
echo "After:"; df -h /
- name: Inject player-feedback API key
env:
BBS_BUGREPORT_API_KEY: ${{ secrets.BBS_BUGREPORT_API_KEY }}
run: |
if [ -z "$BBS_BUGREPORT_API_KEY" ]; then
echo "BBS_BUGREPORT_API_KEY not set — player feedback will be disabled in this build."
exit 0
fi
{
echo '// CI-generated from the BBS_BUGREPORT_API_KEY secret — not committed.'
echo 'namespace BlocksBeyondTheStars.Build'
echo '{'
echo ' public static partial class BugReportBuildSecrets'
echo ' {'
echo " static partial void ApplyApiKey(ref string key) => key = \"$BBS_BUGREPORT_API_KEY\";"
echo ' }'
echo '}'
} > client/Assets/BlocksBeyondTheStars/Scripts/BugReportBuildSecrets.Generated.cs
echo "Injected feedback API key into the build."
- name: Build macOS player
uses: game-ci/unity-builder@ae0171202cd8b5ea3fc3f87b03bb6e676e4ea17e # v6.0.0 + #852: thin wrapper over game-ci/cli, which runs the editor with --shm-size (Unity 6.6 needs it, game-ci/unity-builder#840)
env:
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
with:
cliVersion: v0.1.72 # the game-ci/cli release the wrapper installs — pinned like every action
projectPath: client
unityVersion: 6000.6.5f1
targetPlatform: StandaloneOSX
buildMethod: BlocksBeyondTheStars.Client.EditorTools.BuildScript.BuildMacOS
versioning: Custom
version: ${{ needs.version.outputs.version }}
customParameters: -buildOut /github/workspace/player-mac
allowDirtyBuild: true
- name: Fix output permissions
run: sudo chmod -R a+rwX player-mac
- name: Verify baked version matches the tag
shell: bash
run: |
baked="$(cat player-mac/version.txt)"
echo "Baked into player: '$baked' — expected: '${{ needs.version.outputs.version }}'"
test "$baked" = "${{ needs.version.outputs.version }}"
- name: Upload macOS player artifact
uses: actions/upload-artifact@v6
with:
name: player-mac
path: player-mac
# Intermediate hand-off / already on the Release — see player-windows above (#533).
retention-days: 7
# WebGL (browser) player. Unlike the standalone builds it bundles no local server and needs no Velopack
# or console launcher — it only ever joins a hosted server over WebSocket (issue #121). The output is a
# static folder served at /play by the dedicated-server image.
build-player-webgl:
name: Build Unity player (WebGL)
needs: version
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Set up .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: '10.0.x'
# Shared libs + content only: the WebGL build strips the bundled native-server StreamingAssets, so no
# publish-local-server / Velopack steps. sync-client-libs makes data/ present so the build writes the
# StreamingAssets manifest the browser fetches at runtime, and copies the music library
# (client/Music → StreamingAssets/music) that the browser streams on demand (#1167).
- name: Sync shared libs + content (bash)
run: ./scripts/sync-client-libs.sh
# WebGL is the ONLY platform whose Library cache is worth keeping, and the only one this workflow
# saves (see the paired save step after the build). Measured on run 30223823967: every Unity job
# starts in parallel, and WebGL ends at +31.6 min while Windows/Linux/macOS end at +11.0/+13.7/+13.7
# — WebGL alone is the release critical path, so only its cache buys wall-clock. A warm Library cuts
# the asset import from 257 s (cold) to 85 s, i.e. ~2.9 min off the release. Caching the other three
# would save nothing measurable while adding ~4.5 GB to the repo's 10 GB cache budget (#528).
#
# Restore and save are split because release normally runs on a TAG ref, and a tag-scoped cache can
# never be restored by any other ref (not even the next release tag) — writing one would only evict
# the useful main-scoped caches. Tag runs still restore main's cache via restore-keys.
- name: Restore Unity Library cache
id: unity-cache
uses: actions/cache/restore@v5
with:
path: client/Library
key: Library-WebGL-${{ hashFiles('client/Assets/**', 'client/Packages/**', 'client/ProjectSettings/**') }}
restore-keys: |
Library-WebGL-
- name: Free disk space for the Unity image
run: |
echo "Before:"; df -h /
sudo rm -rf /usr/local/lib/android /opt/ghc /usr/share/dotnet \
/opt/hostedtoolcache/CodeQL /usr/local/share/boost /usr/local/.ghcup
echo "After:"; df -h /
- name: Inject player-feedback API key
env:
BBS_BUGREPORT_API_KEY: ${{ secrets.BBS_BUGREPORT_API_KEY }}
run: |
if [ -z "$BBS_BUGREPORT_API_KEY" ]; then
echo "BBS_BUGREPORT_API_KEY not set — player feedback will be disabled in this build."
exit 0
fi
{
echo '// CI-generated from the BBS_BUGREPORT_API_KEY secret — not committed.'
echo 'namespace BlocksBeyondTheStars.Build'
echo '{'
echo ' public static partial class BugReportBuildSecrets'
echo ' {'
echo " static partial void ApplyApiKey(ref string key) => key = \"$BBS_BUGREPORT_API_KEY\";"
echo ' }'
echo '}'
} > client/Assets/BlocksBeyondTheStars/Scripts/BugReportBuildSecrets.Generated.cs
echo "Injected feedback API key into the build."
# glitch.fun wiring (optional): bake the WorldHost portal origin into the build so the SAME
# WebGL artifact serves the fleet's /play AND glitch.fun. The baked bits stay dormant without
# Glitch's ?install_id= URL param (portal players never trigger them); deliberately NO title
# token is baked — heartbeats/cloud saves go through the WorldHost relay. Forks without the
# secret build a plain open-source player.
- name: Inject Glitch portal config
env:
GLITCH_PORTAL_URL: ${{ secrets.GLITCH_PORTAL_URL }}
run: |
if [ -z "$GLITCH_PORTAL_URL" ]; then
echo "GLITCH_PORTAL_URL not set — Glitch arcade/cloud-save wiring stays disabled in this build."
exit 0
fi
{
echo '// CI-generated from the GLITCH_PORTAL_URL secret — not committed. No title token here:'
echo '// the client talks to the WorldHost relay; the token stays server-side.'
echo 'namespace BlocksBeyondTheStars.Build'
echo '{'
echo ' public static partial class GlitchIntegrationSecrets'
echo ' {'
echo ' static partial void ApplyEnabled(ref bool enabled) => enabled = true;'
echo " static partial void ApplyPortalUrl(ref string value) => value = \"$GLITCH_PORTAL_URL\";"
echo ' }'
echo '}'
} > client/Assets/BlocksBeyondTheStars/Scripts/GlitchIntegrationSecrets.Generated.cs
echo "Injected Glitch portal origin into the build."
- name: Build WebGL player
uses: game-ci/unity-builder@ae0171202cd8b5ea3fc3f87b03bb6e676e4ea17e # v6.0.0 + #852: thin wrapper over game-ci/cli, which runs the editor with --shm-size (Unity 6.6 needs it, game-ci/unity-builder#840)
env:
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
with:
cliVersion: v0.1.72 # the game-ci/cli release the wrapper installs — pinned like every action
projectPath: client
unityVersion: 6000.6.5f1
targetPlatform: WebGL
buildMethod: BlocksBeyondTheStars.Client.EditorTools.BuildScript.BuildWebGL
versioning: Custom
version: ${{ needs.version.outputs.version }}
customParameters: -buildOut /github/workspace/player-webgl
allowDirtyBuild: true
- name: Fix output permissions
run: sudo chmod -R a+rwX player-webgl
# Paired with the restore step above. Two guards, both load-bearing:
# * ref_type != 'tag' — a tag-scoped cache is unreadable by every other ref, so writing one on the
# usual tag release would burn ~1.7 GB of the 10 GB budget for nothing. A manual
# `workflow_dispatch` run of this workflow on main is what actually (re)warms the cache.
# * cache-hit != 'true' — the exact key already exists; re-uploading it would be a no-op that
# merely resets its LRU position.
# Before this step existed, the only writer was the MANUAL webgl-only.yml, so the critical-path cache
# was maintained purely by accident and vanished silently whenever it was evicted (#528).
- name: Save Unity Library cache
if: github.ref_type != 'tag' && steps.unity-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v5
with:
path: client/Library
key: Library-WebGL-${{ hashFiles('client/Assets/**', 'client/Packages/**', 'client/ProjectSettings/**') }}
- name: Verify baked version matches the tag
shell: bash
run: |
baked="$(cat player-webgl/version.txt)"
echo "Baked into player: '$baked' — expected: '${{ needs.version.outputs.version }}'"
test "$baked" = "${{ needs.version.outputs.version }}"
- name: Upload WebGL player artifact
uses: actions/upload-artifact@v6
with:
name: player-webgl
path: player-webgl
# Intermediate hand-off / already on the Release — see player-windows above (#533).
retention-days: 7
# Linux packaging. `needs: test` (like every package/publish job below): nothing ships unless the
# full suite passed on this commit.
package-linux:
name: Package + release (Linux)
needs: [version, build-player-linux, test, create-release]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Set up .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: '10.0.x'
- name: Install Velopack CLI (pinned)
run: |
dotnet tool install -g vpk --version 1.2.0
echo "$HOME/.dotnet/tools" >> "$GITHUB_PATH"
- name: Download Linux player artifact
uses: actions/download-artifact@v7
with:
name: player-linux
path: client/Build/Linux
# GitHub artifacts drop the Unix execute bit. Restore it on the main player AND the bundled local
# server before packing, otherwise Singleplayer's Process.Start on the server fails with EACCES
# inside the AppImage (see LocalServerLauncher.cs — it runs StreamingAssets/server/...GameServer).
- name: Restore execute bits (player + bundled server)
run: |
chmod +x client/Build/Linux/BlocksBeyondTheStars.x86_64
chmod +x client/Build/Linux/BlocksBeyondTheStars_Data/StreamingAssets/server/BlocksBeyondTheStars.GameServer
# vpk can produce AppImage + portable zip for Linux. NOTE: the splash options
# (--splashImage / --splashProgressColor) are Windows-installer only — the Linux `vpk pack`
# rejects them ("Unrecognized command or argument"), so they are not passed here.
- name: Package Linux AppImage + Portable
run: |
OUT="artifacts/installer-linux"
mkdir -p "$OUT"
vpk pack \
--packId BlocksBeyondTheStars \
--packVersion ${{ needs.version.outputs.version }} \
--packTitle 'Blocks Beyond the Stars' \
--packAuthors 'JuMaVe Games' \
--packDir client/Build/Linux \
--mainExe BlocksBeyondTheStars.Launcher.Console \
--channel linux \
--outputDir "$OUT"
- name: Upload Linux installer artifacts
uses: actions/upload-artifact@v6
with:
name: linux-installers
path: artifacts/installer-linux
# Intermediate hand-off / already on the Release — see player-windows above (#533).
retention-days: 7
# The feed files (releases.linux.json + the .nupkg payload) ride along so the in-app updater can
# read this release as its feed via Velopack's GithubSource (#543) — without them installed
# clients never learn a new version exists.
- name: Publish GitHub Release (Linux assets)
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3
with:
files: |
artifacts/installer-linux/*AppImage
artifacts/installer-linux/*Portable.zip
artifacts/installer-linux/releases.linux.json
artifacts/installer-linux/*.nupkg
generate_release_notes: false
# macOS packaging (experimental, no signing/notarization). Velopack is skipped on purpose: we just zip
# the .app and document the Gatekeeper quarantine workaround in the README/release notes. Mirrors the
# Linux packaging job; the only macOS-specific care is preserving the bundle's symlinks (zip -y) and the
# execute bits (artifacts strip them).
package-mac:
name: Package + release (macOS)
needs: [version, build-player-mac, test, create-release]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Download macOS player artifact
uses: actions/download-artifact@v7
with:
name: player-mac
path: client/Build/macOS
# Artifacts strip Unix perms — restore +x on the bundle's entry binary AND the bundled local server,
# otherwise Singleplayer can't spawn the server on macOS (LocalServerLauncher.cs). sudo: the files may be
# root-owned (GameCI builds as root); locate the server with find since its exact path inside the bundle
# is version/Unity-dependent.
- name: Restore execute bits (app + bundled server)
run: |
APP="client/Build/macOS/BlocksBeyondTheStars.app"
sudo chmod +x "$APP/Contents/MacOS/"*
server="$(find "$APP/Contents/Resources" -type f -name 'BlocksBeyondTheStars.GameServer' | head -n1)"
if [ -z "$server" ]; then echo "::error::bundled server binary not found in the .app"; exit 1; fi
sudo chmod +x "$server"
echo "Set +x on the app entry binary and bundled server: $server"
# zip -y preserves the symlinks inside the .app bundle; a plain upload-artifact re-zip would corrupt it.
- name: Zip the .app
run: |
OUT="artifacts/installer-mac"
mkdir -p "$OUT"
( cd client/Build/macOS && \
zip -ry "$GITHUB_WORKSPACE/$OUT/BlocksBeyondTheStars-osx-${{ needs.version.outputs.version }}-Portable.zip" \
"BlocksBeyondTheStars.app" )
- name: Upload macOS installer artifact
uses: actions/upload-artifact@v6
with:
name: mac-installers
path: artifacts/installer-mac
# Intermediate hand-off / already on the Release — see player-windows above (#533).
retention-days: 7
- name: Publish GitHub Release (macOS assets)
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3
with:
files: artifacts/installer-mac/*Portable.zip
generate_release_notes: false
# WebGL packaging: just zip the static build folder and attach it to the release. The dedicated-server
# image's entrypoint fetches this `webgl*.zip` (BBS_FETCH_WEBGL=1) and serves it at /play (issue #121).
package-webgl:
name: Package + release (WebGL)
needs: [version, build-player-webgl, test, create-release]
runs-on: ubuntu-latest
steps:
- name: Download WebGL player artifact
uses: actions/download-artifact@v7
with:
name: player-webgl
path: player-webgl
# Zip the CONTENTS (index.html at the zip root) so the entrypoint can unzip straight into /app/webgl.
- name: Zip the WebGL build
run: |
OUT="artifacts/webgl"
mkdir -p "$OUT"
( cd player-webgl && \
zip -ry "$GITHUB_WORKSPACE/$OUT/BlocksBeyondTheStars-webgl-${{ needs.version.outputs.version }}.zip" . )
- name: Upload WebGL package artifact
uses: actions/upload-artifact@v6
with:
name: webgl-package
path: artifacts/webgl
# Intermediate hand-off / already on the Release — see player-windows above (#533).
retention-days: 7
- name: Publish GitHub Release (WebGL asset)
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3
with:
files: artifacts/webgl/*.zip
generate_release_notes: false
# Mirror the SAME WebGL build to glitch.fun via the OFFICIAL Glitch deploy CLI (the no-Node
# `glitch-deploy-basic` shell variant: plain curl/awk, no npm dependency tree near the secret;
# the clone is pinned to a reviewed commit). One artifact serves the fleet's /play and the Glitch
# store; the Glitch-only bits are dormant without Glitch's ?install_id= URL param. Gated exactly
# like the itch.io mirror: publishes only on real tags, skips cleanly when the deploy token is
# absent, and `needs: test` keeps unreviewed builds off the store. A hiccup here never blocks the
# GitHub Release.
publish-glitch:
name: Publish to glitch.fun (WebGL)
needs: [version, build-player-webgl, test, create-release]
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
steps:
- name: Download WebGL player artifact
uses: actions/download-artifact@v7
with:
name: player-webgl
path: player-webgl
- name: Get the Glitch deploy CLI (pinned)
run: |
git clone --no-checkout https://github.com/Glitch-Gaming-Platform/Glitch-Cli-Deploy.git glitch-cli
git -C glitch-cli checkout 71e6e7c6b2e159502c43cae62f9cbc4b332009d3
chmod +x glitch-cli/bin/glitch-deploy-basic
- name: Upload to glitch.fun
env:
GLITCH_TITLE_TOKEN: ${{ secrets.GLITCH_DEPLOY_TOKEN }}
GLITCH_TITLE_ID_SECRET: ${{ secrets.GLITCH_TITLE_ID }}
run: |
if [ -z "$GLITCH_TITLE_TOKEN" ]; then
echo "GLITCH_DEPLOY_TOKEN not set — skipping the glitch.fun mirror."
exit 0
fi
export GLITCH_TITLE_ID="${GLITCH_TITLE_ID_SECRET:-80f5dc18-dc0f-45de-9a57-8599e08669ed}"
./glitch-cli/bin/glitch-deploy-basic deploy ./player-webgl \
--title "$GLITCH_TITLE_ID" \
--token "$GLITCH_TITLE_TOKEN" \
--version "${{ needs.version.outputs.version }}" \
--entry index.html \
--type wasm \
--build-type production \
--wait
# Optional dedicated-server Docker image. Reuses .github/workflows/docker.yml (workflow_call). It only
# needs the resolved version (from the lightweight `version` job, NOT the Unity player), so it starts
# immediately and runs fully in parallel with the player builds — no waiting on the ~13 min Unity build.
# The image is PUSHED to GHCR only on a real tag (multi-arch, tagged with the version + `latest`); a manual
# workflow_dispatch run just build-validates it — same publish gating as the GitHub Release / itch.io steps.
docker:
name: Dedicated-server image
# `test` gates the GHCR push like every other publish job. It costs the image the gate's start delay
# (it no longer launches immediately) — with the sharded gate (~10 min) plus the ~22 min multi-arch
# build that still lands inside the ~30 min WebGL player build + packaging (#1067). If the gate ever
# grows again, the next lever is to build the image up front and only retag/push after the gate.
needs: [version, test]
permissions:
contents: read
packages: write
uses: ./.github/workflows/docker.yml
with:
push: ${{ startsWith(github.ref, 'refs/tags/') }}
latest: ${{ startsWith(github.ref, 'refs/tags/') }}
tag: ${{ needs.version.outputs.version }}
platforms: linux/amd64,linux/arm64
secrets: inherit
package:
name: Package + release (Windows)
needs: [version, build-player, test, create-release]
runs-on: windows-latest
steps:
- uses: actions/checkout@v5
- name: Set up .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: '10.0.x'
# Self-contained single-file launcher (same flags as scripts/build-client.ps1) so it needs no
# .NET runtime on the player's machine. -p:Version stamps it with the same release version.
- name: Build loading-splash launcher
run: >
dotnet publish src/BlocksBeyondTheStars.Launcher/BlocksBeyondTheStars.Launcher.csproj
-c Release -r win-x64 --self-contained true
-p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:EnableCompressionInSingleFile=true
-p:Version=${{ needs.version.outputs.version }}
-o artifacts/launcher
# Download the player into the layout publish-client-installer.ps1 expects (its default -BuildDir).
- name: Download player artifact
uses: actions/download-artifact@v7
with:
name: player-windows
path: client/Build/Windows
- name: Add launcher to the player folder
shell: pwsh
run: Copy-Item artifacts/launcher/BlocksBeyondTheStars.Launcher.exe client/Build/Windows/ -Force
# Pin vpk to the vendored Velopack runtime version (sync-velopack-libs.ps1 = 1.2.0) for reproducible
# packaging, and expose it on PATH so the script's own `Get-Command vpk` check finds it.
- name: Install Velopack CLI (pinned)
shell: pwsh
run: |
dotnet tool install -g vpk --version 1.2.0
"$env:USERPROFILE\.dotnet\tools" | Out-File -FilePath $env:GITHUB_PATH -Append -Encoding utf8
# Produces the Windows installer trio in artifacts/installer: Setup.exe (per-user, no admin),
# the WiX MSI (machine-wide for IT/MDM), and a portable zip — plus the update feed (.nupkg/RELEASES).
- name: Package installers (Setup.exe + MSI + Portable)
shell: pwsh
run: ./scripts/publish-client-installer.ps1 -Version ${{ needs.version.outputs.version }} -Msi
# Only a tag push publishes. A manual workflow_dispatch run still builds the installers
# (verifiable from the run logs) but skips publishing. The release itself (body: Docker pull
# lines + generated changelog) is created by the `create-release` job — this step, like every
# per-platform publish step, only attaches assets to it.
# The feed files (releases.win.json + the .nupkg payload) ride along so the in-app updater can
# read this release as its feed via Velopack's GithubSource (#543): the client's default
# UpdateFeedUrl points at the repository, and the quiet startup check compares against the
# manifest attached here. Without these two assets installed clients never learn a new version
# exists — that was exactly the v0.9.x gap.
- name: Publish GitHub Release (Windows assets)
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3
with:
files: |
artifacts/installer/*Setup.exe
artifacts/installer/*.msi
artifacts/installer/*Portable.zip
artifacts/installer/releases.win.json
artifacts/installer/*.nupkg
generate_release_notes: false
# Mirror the same three installers to itch.io (jumavegames/blocks-beyond-the-stars) via butler — each
# artifact to its own channel (windows-setup / windows-msi / windows-portable), stamped with the release
# version. Runs after the GitHub Release so a butler hiccup never blocks the GitHub publish. The itch.io
# API key is the BUTLER_API_KEY repo secret; the script auto-downloads butler.
- name: Publish to itch.io (butler)
if: startsWith(github.ref, 'refs/tags/')
shell: pwsh
env:
BUTLER_API_KEY: ${{ secrets.BUTLER_API_KEY }}
run: ./scripts/publish-itch.ps1 -Version ${{ needs.version.outputs.version }}