Dockerize the API with an embedded web UI, Fyne-for-Windows, and real releases

cmd/api/main.go: registerWebUI serves a built web/ as a single-page app -
Echo's static middleware in HTML5 mode, with a Skipper that exempts
/api/*, /hello, /docs, /schemas/* and openapi.json/.yaml so it can never
shadow a real API route. It reads WEB_DIST_DIR (default /frontend, where
the Dockerfile puts it) and no-ops if that directory doesn't exist, so
`go run ./cmd/api` and CI stay Node-free and API-only; only the Docker
image actually turns the web UI on.

Dockerfile: new node:22-alpine stage builds web/, copied into the final
image at /frontend alongside the existing Go-built binary. Added
.dockerignore (web/node_modules, web/dist, web/.angular, .git) now that a
second language's tooling is in the build context.

Looked at how your other Gitea projects (MusicServer et al.) structure
this, since I can't exercise Gitea Actions itself from here: MusicServer's
own Dockerfile already does "copy a prebuilt frontend into the Go image,
serve it from Go" (there, from a separately-published frontend image) -
registerWebUI does the same thing in spirit, just within one image rather
than two, since this UI isn't meant to be published separately.

release.yml:
- build-gui now also cross-compiles the Fyne GUI for windows/amd64 via
  mingw-w64, alongside the existing linux/amd64. Verified for real, not
  just written: installed mingw-w64 locally and cross-built cmd/gui,
  first plain (got a working PE32+ console exe), then with
  -ldflags "-H=windowsgui" (got the GUI-subsystem exe actually wanted, no
  console window). macOS isn't attempted - cgo has no equivalent
  cross-toolchain for Darwin and there's no registered macOS runner here.
- new build-docker job pushes the API image (now carrying the web build)
  to gitea.sanplex.xyz/sansan/completed, tagged with the version and
  latest.
- replaced the hand-rolled curl+jq Gitea-release creation with
  akkuman/gitea-release-action, and artifact upload with the Gitea generic
  package registry - both are what your other projects (HanjaKanaMun,
  MusicPlayer) actually use, found by reading their workflows rather than
  guessing. Same for the registry host, owner, and secrets.TOKEN name
  (previously secrets.GITHUB_TOKEN, which nothing else here uses).

ci.yml: new docker job builds the same image on every push to main/develop
(push: false) as a health check, matching MusicServer's build-vs-release
split.

Verified for real wherever I could: a full `docker build` of the new
Dockerfile, then running that image against a real Postgres container and
hitting it with curl - / serves the actual built Angular index.html,
/games (a client-side route) correctly falls back to it, /api/games and
/hello are untouched (still return real API responses, not shadowed by
the static handler), and register+list-games works end to end through the
container. Also ran it the documented way, `docker compose up --build`,
with the same results. Separately confirmed the Windows Fyne cross-build
produces a real, correctly-subsystemed .exe. What I could not run from
here is Gitea Actions itself - the workflow YAML is written to match your
other projects' established, working conventions, but its first real
execution on your Gitea instance is still the first real test of it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
SansanandClaude Sonnet 5 committed 2026-10-03 12:49:36 +02:00
1 parent 9972537b63
commit 786fdbb0d5
6 files changed
+231 -68

No files matched your search

+17
View File
@@ -79,3 +79,20 @@ jobs:
- name: Test
run: npm test
# Builds the image on every push as a health check (catches a broken
# Dockerfile immediately); doesn't push anywhere. release.yml does the
# same build and pushes it.
docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- name: Build
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: false
+102 -53
View File
@@ -1,12 +1,21 @@
#
# .gitea/workflows/release.yml
#
# On a version tag (e.g. v1.2.3): builds every client, pushes the API's
# Docker image, uploads all build artifacts to the Gitea generic package
# registry, and creates a Gitea release with those same files attached.
#
name: Release
# A pushed tag like v1.2.3 builds every client and attaches the result to a
# Gitea release of the same name. Tag a commit that has already passed CI
# on main; this workflow doesn't re-run those checks.
on:
push:
tags: ["v*.*.*"]
env:
PROJECT_NAME: Completed
REGISTRY: gitea.sanplex.xyz
REGISTRY_OWNER: sansan
jobs:
# cmd/api and cmd/tui are pure Go (no cgo), so they cross-compile cleanly
# from one Linux runner for every OS/arch below.
@@ -47,7 +56,7 @@ jobs:
- name: Archive
run: |
cd dist
base="completed_${{ matrix.goos }}_${{ matrix.goarch }}"
base="${PROJECT_NAME}_${{ matrix.goos }}_${{ matrix.goarch }}"
if [ "${{ matrix.goos }}" = "windows" ]; then
zip "../${base}.zip" completed-api.exe completed-tui.exe
else
@@ -56,18 +65,29 @@ jobs:
- uses: actions/upload-artifact@v4
with:
name: completed_${{ matrix.goos }}_${{ matrix.goarch }}
path: completed_${{ matrix.goos }}_${{ matrix.goarch }}.*
name: ${{ env.PROJECT_NAME }}_${{ matrix.goos }}_${{ matrix.goarch }}
path: ${{ env.PROJECT_NAME }}_${{ matrix.goos }}_${{ matrix.goarch }}.*
retention-days: 7
# cmd/gui (Fyne) uses cgo and links the system's native GL/windowing
# libraries, so - unlike build-go above - it can't be cross-compiled for
# other operating systems from this one Linux runner. This job only
# produces a linux/amd64 build. A macOS or Windows build of the GUI would
# need this workflow extended with a job running on a matching
# self-hosted runner (there isn't one registered for this repo today).
build-gui-linux:
# cmd/gui (Fyne) uses cgo, so unlike build-go above it can't just cross-
# compile with CGO_ENABLED=0. Linux is native to this runner; Windows
# cross-compiles cleanly with the mingw-w64 toolchain (verified locally:
# a real PE32+ GUI-subsystem .exe, linking Fyne's OpenGL/glfw backend,
# with no changes needed beyond CC and the usual GOOS/GOARCH). macOS
# isn't included - cross-compiling cgo to Darwin from Linux has no
# equivalent well-supported toolchain, so that would need an actual
# macOS runner, which isn't registered for this repo.
build-gui:
runs-on: ubuntu-latest
strategy:
matrix:
include:
- goos: linux
goarch: amd64
packages: libgl1-mesa-dev xorg-dev
- goos: windows
goarch: amd64
packages: gcc-mingw-w64-x86-64
steps:
- uses: actions/checkout@v4
@@ -78,20 +98,35 @@ jobs:
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y libgl1-mesa-dev xorg-dev
sudo apt-get install -y ${{ matrix.packages }}
- name: Build gui
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
CGO_ENABLED: "1"
run: |
mkdir -p dist
go build -o dist/completed-gui ./cmd/gui
if [ "${{ matrix.goos }}" = "windows" ]; then
CC=x86_64-w64-mingw32-gcc go build -ldflags "-H=windowsgui" -o dist/completed-gui.exe ./cmd/gui
else
go build -o dist/completed-gui ./cmd/gui
fi
- name: Archive
run: tar czf completed-gui_linux_amd64.tar.gz -C dist completed-gui
run: |
cd dist
base="../${PROJECT_NAME}-gui_${{ matrix.goos }}_${{ matrix.goarch }}"
if [ "${{ matrix.goos }}" = "windows" ]; then
zip "${base}.zip" completed-gui.exe
else
tar czf "${base}.tar.gz" completed-gui
fi
- uses: actions/upload-artifact@v4
with:
name: completed-gui_linux_amd64
path: completed-gui_linux_amd64.tar.gz
name: ${{ env.PROJECT_NAME }}-gui_${{ matrix.goos }}_${{ matrix.goarch }}
path: ${{ env.PROJECT_NAME }}-gui_${{ matrix.goos }}_${{ matrix.goarch }}.*
retention-days: 7
build-web:
@@ -112,57 +147,71 @@ jobs:
npm run build
- name: Archive
run: tar czf completed-web_dist.tar.gz -C web/dist/web .
run: tar czf ${PROJECT_NAME}-web_dist.tar.gz -C web/dist/web/browser .
- uses: actions/upload-artifact@v4
with:
name: completed-web_dist
path: completed-web_dist.tar.gz
name: ${{ env.PROJECT_NAME }}-web_dist
path: ${{ env.PROJECT_NAME }}-web_dist.tar.gz
retention-days: 7
release:
needs: [build-go, build-gui-linux, build-web]
# Builds and pushes the API's image, which embeds the web build as its
# static UI (see the Dockerfile and cmd/api/main.go's registerWebUI).
build-docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
config-inline: |
[registry."${{ env.REGISTRY }}/${{ env.REGISTRY_OWNER }}"]
http = true
insecure = true
- name: Login to Gitea
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.repository_owner }}
password: ${{ secrets.TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
tags: |
${{ env.REGISTRY }}/${{ env.REGISTRY_OWNER }}/completed:${{ gitea.ref_name }}
${{ env.REGISTRY }}/${{ env.REGISTRY_OWNER }}/completed:latest
release:
needs: [build-go, build-gui, build-web]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: true
- name: Create the Gitea release and upload each artifact
- name: Upload to the Gitea generic package registry
env:
# Gitea Actions auto-provides this for the compatibility GITHUB_*
# context; it needs the repo's Settings > Actions permissions set
# to allow writing contents. If your instance doesn't populate it,
# replace with a secret holding a personal/repo access token that
# has repo write access instead.
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SERVER_URL: ${{ github.server_url }}
REPO: ${{ github.repository }}
TAG: ${{ github.ref_name }}
VERSION: ${{ gitea.ref_name }}
run: |
set -euo pipefail
sudo apt-get update && sudo apt-get install -y jq
release_json=$(curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"tag_name\":\"${TAG}\",\"name\":\"${TAG}\",\"draft\":false,\"prerelease\":false}" \
"${SERVER_URL}/api/v1/repos/${REPO}/releases")
release_id=$(echo "$release_json" | jq -r '.id')
if [ -z "$release_id" ] || [ "$release_id" = "null" ]; then
echo "Failed to create release: $release_json" >&2
exit 1
fi
for f in artifacts/*; do
name=$(basename "$f")
name="$(basename "$f")"
echo "Uploading $name"
curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-F "attachment=@${f}" \
"${SERVER_URL}/api/v1/repos/${REPO}/releases/${release_id}/assets?name=${name}"
curl --fail --user "${{ github.repository_owner }}:${{ secrets.TOKEN }}" \
--upload-file "$f" \
"https://${{ env.REGISTRY }}/api/packages/${{ env.REGISTRY_OWNER }}/generic/${{ env.PROJECT_NAME }}/${VERSION}/${name}"
done
- name: Create release
uses: akkuman/gitea-release-action@v1
env:
NODE_OPTIONS: "--experimental-fetch" # if nodejs < 18
with:
files: |-
artifacts/**