diff --git a/.gitea/workflows/BuildAndroid.yml b/.gitea/workflows/BuildAndroid.yml new file mode 100644 index 0000000..da26ee0 --- /dev/null +++ b/.gitea/workflows/BuildAndroid.yml @@ -0,0 +1,86 @@ +name: Build Android Apk + +# Only a release built through "Releases -> New Release" starts a build. A tag +# pushed with plain git does not publish a release and is therefore ignored. +on: + release: + types: + - published + +env: + # Keep in sync with the other workflows and with `ac_web/Dockerfile`. + FLUTTER_VERSION: '3.44.9' + # The project produces java 17 bytecode (`android/app/build.gradle.kts`), but + # gradle 9.1 and the android gradle plugin 9 need a newer jdk to run on. + JAVA_VERSION: '21' + +jobs: + android: + runs-on: ubuntu-latest + container: + image: catthehacker/ubuntu:act-latest + + # The flutter project lives in a sub directory of the repository. + defaults: + run: + working-directory: ac_web + + steps: + - name: Checkout + uses: https://github.com/actions/checkout@v4 + + # The flutter tool runs git inside the workspace, which the container + # user does not own after the checkout. + - name: Trust the workspace + run: git config --global --add safe.directory '*' + + - name: Set up Java + uses: https://github.com/actions/setup-java@v4 + with: + distribution: temurin + java-version: ${{ env.JAVA_VERSION }} + + # Installs the command line tools and accepts the sdk licenses. The + # platform, the build tools and the ndk pinned by the flutter sdk are + # then downloaded by gradle itself, which is what makes this job slow on + # a cold runner. + - name: Set up the Android sdk + uses: https://github.com/android-actions/setup-android@v3 + + - name: Set up Flutter + uses: https://github.com/subosito/flutter-action@v2 + with: + flutter-version: ${{ env.FLUTTER_VERSION }} + channel: stable + cache: true + + - name: Install dependencies + run: flutter pub get + + - name: Run tests + run: flutter test + + # One apk for all abis. `--split-per-abi` would produce three smaller + # ones instead, at the cost of having to pick the right one manually. + # + # The release build is signed with a debug key, because + # `android/app/build.gradle.kts` has no release signing config yet. To + # ship the apk, add a keystore as a secret, write it into + # `android/key.properties` here and reference it from the gradle file. + - name: Build the apk + run: flutter build apk --release + + # Gradle always writes the same file name, so the release the apk belongs + # to would not be visible after downloading it. + - name: Name the apk after the release + run: mv build/app/outputs/flutter-apk/app-release.apk ac_web-${{ github.ref_name }}.apk + + # Artifacts of v4 need gitea 1.22 or newer, older instances need @v3. + - name: Upload the apk + uses: https://github.com/actions/upload-artifact@v4 + with: + name: ac_web-android-${{ github.ref_name }} + # Artifact paths are relative to the repository, not to the + # working-directory above. + path: ac_web/ac_web-${{ github.ref_name }}.apk + if-no-files-found: error diff --git a/.gitea/workflows/BuildLinux.yml b/.gitea/workflows/BuildLinux.yml new file mode 100644 index 0000000..86ca983 --- /dev/null +++ b/.gitea/workflows/BuildLinux.yml @@ -0,0 +1,75 @@ +name: Build Linux Application + +# Only a release built through "Releases -> New Release" starts a build. A tag +# pushed with plain git does not publish a release and is therefore ignored. +on: + release: + types: + - published + +env: + # Keep in sync with the other workflows and with `ac_web/Dockerfile`. + FLUTTER_VERSION: '3.44.9' + +jobs: + linux: + runs-on: ubuntu-latest + # Same base image as the docker job, it already brings the tools the + # actions below expect (git, curl, unzip, node). + container: + image: catthehacker/ubuntu:act-latest + + # The flutter project lives in a sub directory of the repository. + defaults: + run: + working-directory: ac_web + + steps: + - name: Checkout + uses: https://github.com/actions/checkout@v4 + + # The flutter tool runs git inside the workspace, which the container + # user does not own after the checkout. + - name: Trust the workspace + run: git config --global --add safe.directory '*' + + # Everything `flutter build linux` needs to compile the gtk runner. + - name: Install the linux desktop toolchain + run: | + apt-get update + apt-get install -y --no-install-recommends \ + clang cmake ninja-build pkg-config \ + libgtk-3-dev liblzma-dev libstdc++-12-dev \ + curl unzip xz-utils + + - name: Set up Flutter + uses: https://github.com/subosito/flutter-action@v2 + with: + flutter-version: ${{ env.FLUTTER_VERSION }} + channel: stable + cache: true + + - name: Install dependencies + run: flutter pub get + + - name: Run tests + run: flutter test + + - name: Build the linux application + run: flutter build linux --release + + # The build produces a directory, not a single file: the executable next + # to the flutter engine and its data. It is packed so that the artifact + # keeps the executable bit. + - name: Pack the bundle + run: tar -czf ac_web-linux-x64-${{ github.ref_name }}.tar.gz -C build/linux/x64/release bundle + + # Artifacts of v4 need gitea 1.22 or newer, older instances need @v3. + - name: Upload the bundle + uses: https://github.com/actions/upload-artifact@v4 + with: + name: ac_web-linux-x64-${{ github.ref_name }} + # Artifact paths are relative to the repository, not to the + # working-directory above. + path: ac_web/ac_web-linux-x64-${{ github.ref_name }}.tar.gz + if-no-files-found: error diff --git a/.gitea/workflows/PublishDocker.yml b/.gitea/workflows/PublishDocker.yml new file mode 100644 index 0000000..79a8e19 --- /dev/null +++ b/.gitea/workflows/PublishDocker.yml @@ -0,0 +1,66 @@ +name: Publish Docker Image + +# Only a release built through "Releases -> New Release" starts a build. A tag +# pushed with plain git does not publish a release and is therefore ignored. +on: + release: + types: + - published + +env: + # Gitea container registry. Set a different host in + # "Settings -> Actions -> Variables" if the instance ever moves. + REGISTRY: git.srv.kingeldarion.de + OWNER: ${{ github.repository_owner }} + IMAGE: ac-web + +jobs: + docker: + runs-on: ubuntu-latest + # This base image makes Docker-in-Docker with Gitea's act-runner painless. + container: + image: catthehacker/ubuntu:act-latest + + steps: + - name: Checkout + uses: https://github.com/actions/checkout@v4 + + - name: Set up QEMU + uses: https://github.com/docker/setup-qemu-action@v3 + + - name: Set up Buildx + uses: https://github.com/docker/setup-buildx-action@v3 + + - name: Login to Gitea Container Registry + uses: https://github.com/docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.repository_owner }} + # A personal access token, created in the repo settings as BUILDTOKEN. + password: ${{ secrets.BUILDTOKEN }} + + - name: Docker meta (tags & labels) + id: meta + uses: https://github.com/docker/metadata-action@v5 + with: + images: | + ${{ env.REGISTRY }}/${{ env.OWNER }}/${{ env.IMAGE }} + # Only releases end up here, so every build is the newest one and + # branch tags can never occur. Move `latest` behind a condition on + # `github.event.release.prerelease` if pre-releases are used. + tags: | + type=raw,value=latest + type=ref,event=tag + type=sha,format=short + + # The dockerfile compiles the web bundle itself, so the context is the + # flutter project and nothing has to be built before this step. + - name: Build & Push the website image + uses: https://github.com/docker/build-push-action@v5 + with: + context: ./ac_web + file: ./ac_web/Dockerfile + push: true + platforms: linux/amd64 + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/CLAUDE.md b/CLAUDE.md index 8d2e1dc..550a298 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -32,6 +32,25 @@ Widget tests run against a mocked backend: build a `MockClient` that answers by `flutter analyze` reports ~40 pre-existing `file_names` / `non_constant_identifier_names` infos caused by the naming conventions below. They are expected — only care about new warnings and errors. +## CI (Gitea Actions) + +`.gitea/workflows/` holds one workflow per delivery target. All of them are triggered by `release: types: [published]`, so they run when a release is published through "Releases -> New Release" and never on an ordinary push or on a tag pushed with plain git: + +| Workflow | Builds | Result | +| --- | --- | --- | +| `BuildLinux.yml` | `flutter build linux --release` | `ac_web-linux-x64-.tar.gz` (the whole bundle directory) as an artifact | +| `BuildAndroid.yml` | `flutter build apk --release` | `ac_web-.apk` as an artifact | +| `PublishDocker.yml` | `ac_web/Dockerfile` | image pushed to the Gitea registry as ``, `latest` and the short sha | + +Notes that are easy to get wrong: + +- The Flutter version is pinned in three places (`FLUTTER_VERSION` in both build workflows, `ARG FLUTTER_VERSION` in `ac_web/Dockerfile`) and must be kept in sync. +- Steps run with `working-directory: ac_web`, but `upload-artifact` paths are relative to the repository root. +- The workflows are read from the tag the release points at, so they have to be committed **before** the release is created. +- Artifacts and the built files carry the tag name (`github.ref_name`); the docker image is pushed as the tag, as `latest` and as the short sha. +- The apk is signed with a debug key, because `android/app/build.gradle.kts` still has no release signing config. +- The docker image is self contained: it compiles the web bundle itself, so `docker build -f ac_web/Dockerfile ac_web` gives the same result locally. It serves the static bundle with nginx (`ac_web/nginx.conf`), which falls back to `index.html` so deep links survive a reload. The backend address is compiled into the bundle, so the host in `main.dart` must be reachable from the browser of every visitor, not only from the container. + ## Backend connection The backend host/port is configured once in `lib/main.dart` via `AcApi.configure(const ApiConfig(host: 'localhost', port: 5118))`. Plain http is intentional — the API is only reached inside a trusted network. There is no environment/config file; changing the backend address means editing `main.dart`. diff --git a/ac_web/.dockerignore b/ac_web/.dockerignore new file mode 100644 index 0000000..33c045b --- /dev/null +++ b/ac_web/.dockerignore @@ -0,0 +1,12 @@ +# Build output and caches of local builds - the image builds everything itself. +build/ +.dart_tool/ +.idea/ +*.iml + +# Platform folders that are irrelevant for the web build. +android/ +ios/ +linux/ +macos/ +windows/ diff --git a/ac_web/Dockerfile b/ac_web/Dockerfile new file mode 100644 index 0000000..676ecd2 --- /dev/null +++ b/ac_web/Dockerfile @@ -0,0 +1,52 @@ +# Serves the Flutter web build of "AC Control" as a plain static website. +# +# The image is self contained: it compiles the web bundle itself instead of +# expecting a prepared `build/web` directory, so +# `docker build -f ac_web/Dockerfile ac_web` produces exactly the same image +# locally as the gitea action does. + +# ---------------------------------------------------------------- build stage +FROM debian:bookworm-slim AS build + +# Keep the version in sync with `.gitea/workflows/*.yml`. The sdk is taken from +# the official archive instead of a prebuilt flutter image, because that archive +# has every released version and can therefore be pinned exactly. +ARG FLUTTER_VERSION=3.44.9 + +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates curl git unzip xz-utils \ + && rm -rf /var/lib/apt/lists/* + +RUN curl -fsSL "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_${FLUTTER_VERSION}-stable.tar.xz" \ + | tar -xJ -C /opt + +ENV PATH="/opt/flutter/bin:${PATH}" + +# The flutter tool calls git on its own sdk during every command, and git +# refuses to work in a directory it considers to belong to somebody else. +RUN git config --global --add safe.directory /opt/flutter + +WORKDIR /app + +# The dependencies are copied on their own so that this layer is only rebuilt +# when they actually changed, not on every source change. +COPY pubspec.yaml pubspec.lock ./ +RUN flutter pub get + +COPY . . + +# The result is a completely static bundle - the app talks to the AcAPI from +# the browser of the user, not from this container. +RUN flutter build web --release + +# -------------------------------------------------------------- runtime stage +FROM nginx:alpine AS runtime + +# The bundle is built with the backend address that is compiled into +# `lib/main.dart`, and the browser of the visitor resolves it - not this +# container. Serving this image to other machines therefore needs a host name +# in `main.dart` that those machines can reach as well. +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /app/build/web /usr/share/nginx/html + +EXPOSE 80 diff --git a/ac_web/nginx.conf b/ac_web/nginx.conf new file mode 100644 index 0000000..6fc6f10 --- /dev/null +++ b/ac_web/nginx.conf @@ -0,0 +1,24 @@ +# Nginx configuration for the static Flutter web bundle. +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + # Everything the bundle cannot resolve itself is answered with the app, so + # that reloading a deep link does not end in a 404. + location / { + try_files $uri $uri/ /index.html; + } + + # Both files decide which version of the app the browser gets. They must + # never be cached, otherwise a new deployment is not picked up. + location = /index.html { + add_header Cache-Control "no-store"; + } + + location = /flutter_service_worker.js { + add_header Cache-Control "no-store"; + } +}