1 Commits

Author SHA1 Message Date
ml 4be350d758 dist.sh builds versioned, signed artifacts for both apps from a git tag
release / build (push) Has been cancelled
(semver drives Android versionCode/versionName and package.json). The
build-image/ Dockerfile pins the same toolchain for a portable,
containerized Gitea Actions runner (build-image.sh builds and pushes
it) so releases don't depend on any one machine's local setup.
Pushing vX.Y.Z now builds and publishes a Gitea Release automatically.
2026-07-02 13:07:52 +02:00
15 changed files with 54 additions and 356 deletions
-32
View File
@@ -1,32 +0,0 @@
# Build context for build-image/Dockerfile. The context is the repo root
# (see build-image.sh) so the Gradle-cache-warming stage can COPY in the
# companion app's real project files — everything else is excluded to keep
# the context small and to make sure secrets never reach the Docker daemon.
.git
dist
docs
versions
SchwertUndMagieOnPebbleWatchApp
# Companion app: only the Gradle project files are needed (see the
# gradle-cache-warm stage) — not generated build output or the VICE/nibtools
# source tarballs (buildVice/buildNibtools are excluded from that stage's
# gradle invocation, so they're never unpacked there).
SchwertUndMagieOnPebbleCompanionApp/app/build
SchwertUndMagieOnPebbleCompanionApp/app/.cxx
SchwertUndMagieOnPebbleCompanionApp/app/src/main/jni/vice-src
SchwertUndMagieOnPebbleCompanionApp/app/src/main/jni/vice-libs
SchwertUndMagieOnPebbleCompanionApp/app/src/main/jni/nibtools-src
SchwertUndMagieOnPebbleCompanionApp/app/src/main/jni/nibtools-libs
SchwertUndMagieOnPebbleCompanionApp/res/*.tar.gz
SchwertUndMagieOnPebbleCompanionApp/.gradle
SchwertUndMagieOnPebbleCompanionApp/.idea
SchwertUndMagieOnPebbleCompanionApp/local.properties
SchwertUndMagieOnPebbleCompanionApp/build
SchwertUndMagieOnPebbleCompanionApp/captures
# Secrets — must never reach the Docker daemon, even unused.
registry.env
**/keystore.properties
**/*.keystore
**/*.jks
+4 -49
View File
@@ -13,25 +13,13 @@ jobs:
# regardless of this instance's default Actions permission mode. # regardless of this instance's default Actions permission mode.
permissions: permissions:
contents: write contents: write
# Runner defaults `run:` steps to `sh`, which doesn't understand # Must match the label your self-hosted act_runner registers with.
# `set -o pipefail` used below — force bash explicitly. runs-on: self-hosted
defaults:
run:
shell: bash
# Must match a label your act_runner is registered with. The runner's
# own default label-image is irrelevant here since `container:` below
# overrides the actual build image per-job.
runs-on: ubuntu-latest
container: container:
# :latest — always the most recently pushed build-image.sh output. # :latest — always the most recently pushed build-image.sh output.
# Pin to a specific tag here (see build-image/VERSION) if you need a # Pin to a specific tag here (see build-image/VERSION) if you need a
# release build to be reproducible against an exact toolchain image. # release build to be reproducible against an exact toolchain image.
image: cr.ladkau.de/schwert-und-magie/builder:latest image: cr.ladkau.de/schwert-und-magie/builder:latest
# Lets the runner pull a private image without a manual `docker login`
# on the runner host — see docs/publish.md §4.3.
credentials:
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -56,46 +44,13 @@ jobs:
set -euo pipefail set -euo pipefail
API="${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}" API="${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}"
AUTH="Authorization: token ${{ secrets.GITEA_TOKEN }}" AUTH="Authorization: token ${{ secrets.GITEA_TOKEN }}"
TAG="${{ gitea.ref_name }}"
# Reuse an existing release for this tag instead of failing outright
# (curl -f exit 22) if a prior run already created it — e.g. a retry
# after a later step failed. (No -f here: a 404 for "no release yet"
# is expected, not an error — it just leaves .id empty below.)
RELEASE_ID="$(curl -s "$API/releases/tags/$TAG" -H "$AUTH" | jq -r '.id // empty')"
if [ -z "$RELEASE_ID" ]; then
RELEASE_ID="$(curl -sf -X POST "$API/releases" \ RELEASE_ID="$(curl -sf -X POST "$API/releases" \
-H "$AUTH" -H "Content-Type: application/json" \ -H "$AUTH" -H "Content-Type: application/json" \
-d "{\"tag_name\": \"$TAG\", \"name\": \"$TAG\"}" \ -d "{\"tag_name\": \"${{ gitea.ref_name }}\", \"name\": \"${{ gitea.ref_name }}\"}" \
| jq -r .id)" | jq -r .id)"
fi
for f in dist/*; do for f in dist/*; do
NAME="$(basename "$f")" curl -sf -X POST "$API/releases/$RELEASE_ID/assets?name=$(basename "$f")" \
# Same idempotency concern for assets: a retry re-uploading a name
# that's already attached would 409, so replace it instead.
EXISTING_ID="$(curl -sf "$API/releases/$RELEASE_ID/assets" -H "$AUTH" \
| jq -r --arg n "$NAME" '.[] | select(.name == $n) | .id')"
if [ -n "$EXISTING_ID" ]; then
curl -sf -X DELETE "$API/releases/$RELEASE_ID/assets/$EXISTING_ID" -H "$AUTH"
fi
curl -sf -X POST "$API/releases/$RELEASE_ID/assets?name=$NAME" \
-H "$AUTH" -F "attachment=@$f" -H "$AUTH" -F "attachment=@$f"
done done
- name: Publish to dl.ladkau.de
run: |
set -euo pipefail
mkdir -p ~/.ssh
echo "${{ secrets.DL_SFTP_KEY }}" > ~/.ssh/dl_sftp_key
chmod 600 ~/.ssh/dl_sftp_key
BATCH="$(mktemp)"
{
echo "-mkdir files/schwert-und-magie"
for f in dist/*; do
echo "put $f files/schwert-und-magie/$(basename "$f")"
done
} > "$BATCH"
sftp -i ~/.ssh/dl_sftp_key -P 2223 \
-o StrictHostKeyChecking=accept-new \
-b "$BATCH" uploader@dl.ladkau.de
+1 -1
View File
@@ -6,7 +6,7 @@
.cxx .cxx
local.properties local.properties
.lock* .lock*
.idea
/SchwertUndMagieOnPebbleCompanionApp/local.properties /SchwertUndMagieOnPebbleCompanionApp/local.properties
/SchwertUndMagieOnPebbleCompanionApp/.idea/caches /SchwertUndMagieOnPebbleCompanionApp/.idea/caches
/SchwertUndMagieOnPebbleCompanionApp/.idea/libraries /SchwertUndMagieOnPebbleCompanionApp/.idea/libraries
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2026 Matthias Ladkau
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+2 -5
View File
@@ -150,10 +150,8 @@ Pushing a tag `vX.Y.Z` builds both apps in a containerized runner and
publishes a Gitea Release with the versioned artifacts attached — see publishes a Gitea Release with the versioned artifacts attached — see
`docs/publish.md` §4. One-time setup: `docs/publish.md` §4. One-time setup:
1. Build the build environment image locally and push it (`./build-image.sh` 1. Build and push the build environment image (`./build-image.sh`, needs
then `./upload-image.sh`, needs `registry.env` — copy from `registry.env` — copy from `registry.env.example`).
`registry.env.example`). Use `./run-image.sh` in between to sanity-check
the image before pushing.
2. Register a self-hosted `act_runner` with a Docker executor. 2. Register a self-hosted `act_runner` with a Docker executor.
3. Add repo secrets under **Settings → Actions → Secrets**: 3. Add repo secrets under **Settings → Actions → Secrets**:
@@ -161,7 +159,6 @@ publishes a Gitea Release with the versioned artifacts attached — see
|---|---| |---|---|
| `RELEASE_KEYSTORE_B64` | `base64 -w0 SchwertUndMagieOnPebbleCompanionApp/release.keystore` | | `RELEASE_KEYSTORE_B64` | `base64 -w0 SchwertUndMagieOnPebbleCompanionApp/release.keystore` |
| `RELEASE_KEYSTORE_PROPERTIES` | full contents of `SchwertUndMagieOnPebbleCompanionApp/keystore.properties` | | `RELEASE_KEYSTORE_PROPERTIES` | full contents of `SchwertUndMagieOnPebbleCompanionApp/keystore.properties` |
| `REGISTRY_USER` / `REGISTRY_PASSWORD` | same as in `registry.env`, so the runner can pull the private build image |
`GITEA_TOKEN` is injected automatically per job — nothing to add for it. `GITEA_TOKEN` is injected automatically per job — nothing to add for it.
4. `git tag v1.2.3 && git push origin v1.2.3`. 4. `git tag v1.2.3 && git push origin v1.2.3`.
@@ -42,11 +42,7 @@ fi
if [ ! -d "${SRC}" ]; then if [ ! -d "${SRC}" ]; then
echo "Unpacking nibtools..." echo "Unpacking nibtools..."
tar -xzf "${TARBALL}" -C "${SCRIPT_DIR}" tar -xzf "${TARBALL}" -C "${SCRIPT_DIR}"
# Hardcoded, not a glob: on a non-ephemeral runner workspace, a mv "${SCRIPT_DIR}"/nibtools-*/ "${SRC}"
# nibtools-*/ glob can also match SRC itself once it exists, or other
# stray nibtools-prefixed leftovers, and mv then fails with "target is
# not a directory" (multiple sources, no existing target dir).
mv "${SCRIPT_DIR}/nibtools-91344e0ee3" "${SRC}"
echo "Unpacked to ${SRC}" echo "Unpacked to ${SRC}"
fi fi
@@ -32,7 +32,7 @@ echo " TOOLCHAIN : ${TOOLCHAIN} [OK]"
# ---- Ensure required host tools are present --------------------------------- # ---- Ensure required host tools are present ---------------------------------
MISSING=() MISSING=()
for tool in dos2unix autoconf automake pkg-config xa flex; do for tool in dos2unix autoconf automake pkg-config xa; do
command -v "$tool" &>/dev/null || MISSING+=("$tool") command -v "$tool" &>/dev/null || MISSING+=("$tool")
done done
if [ ${#MISSING[@]} -gt 0 ]; then if [ ${#MISSING[@]} -gt 0 ]; then
@@ -807,11 +807,7 @@ JNI_FN(jboolean, getDriveLed)(JNIEnv *env, jobject obj) {
JNI_FN(jint, getFrameCount)(JNIEnv *env, jobject obj) { JNI_FN(jint, getFrameCount)(JNIEnv *env, jobject obj) {
(void)env; (void)obj; (void)env; (void)obj;
#ifdef HAVE_VICE_SRC
return (jint)g_frame_count; return (jint)g_frame_count;
#else
return 0;
#endif
} }
/* Schwert und Magie uploads a custom character set that redefines a handful of /* Schwert und Magie uploads a custom character set that redefines a handful of
@@ -899,11 +895,7 @@ JNI_FN(void, injectKey)(JNIEnv *env, jobject obj, jint keyCode, jboolean pressed
JNI_FN(void, setSoundEnabled)(JNIEnv *env, jobject obj, jboolean enabled) { JNI_FN(void, setSoundEnabled)(JNIEnv *env, jobject obj, jboolean enabled) {
(void)env; (void)obj; (void)env; (void)obj;
#ifdef HAVE_VICE_SRC
g_sound_enabled = enabled ? 1 : 0; g_sound_enabled = enabled ? 1 : 0;
#else
(void)enabled;
#endif
} }
JNI_FN(jboolean, saveState)(JNIEnv *env, jobject obj, jstring jpath) { JNI_FN(jboolean, saveState)(JNIEnv *env, jobject obj, jstring jpath) {
+16 -9
View File
@@ -1,7 +1,8 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Builds the release build environment image (build-image/Dockerfile) # Builds and pushes the release build environment image (build-image/Dockerfile)
# locally, tagged with build-image/VERSION and :latest. Does not push — # to the container registry. The Gitea Actions release workflow pulls this
# test it with ./run-image.sh first, then publish with ./upload-image.sh. # image to run dist.sh. Bump build-image/VERSION whenever the Dockerfile
# changes so the workflow can pin a stable tag.
set -euo pipefail set -euo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")" cd "$(dirname "${BASH_SOURCE[0]}")"
@@ -18,21 +19,27 @@ command -v docker >/dev/null 2>&1 \
# shellcheck disable=SC1091 # shellcheck disable=SC1091
source "$ROOT/registry.env" source "$ROOT/registry.env"
: "${REGISTRY:?registry.env must set REGISTRY}"
: "${REGISTRY_IMAGE:?registry.env must set REGISTRY_IMAGE}" : "${REGISTRY_IMAGE:?registry.env must set REGISTRY_IMAGE}"
: "${REGISTRY_USER:?registry.env must set REGISTRY_USER}"
: "${REGISTRY_PASSWORD:?registry.env must set REGISTRY_PASSWORD}"
VERSION="$(<"$ROOT/build-image/VERSION")" VERSION="$(<"$ROOT/build-image/VERSION")"
[ -n "$VERSION" ] || fail "build-image/VERSION is empty" [ -n "$VERSION" ] || fail "build-image/VERSION is empty"
echo "== Logging in to $REGISTRY =="
echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY" -u "$REGISTRY_USER" --password-stdin
echo "== Building $REGISTRY_IMAGE:$VERSION ==" echo "== Building $REGISTRY_IMAGE:$VERSION =="
# Context is the repo root (not build-image/) so the Dockerfile's
# gradle-cache-warm stage can COPY in the companion app's real Gradle
# project files — see .dockerignore for what's excluded from that context.
docker build \ docker build \
-t "$REGISTRY_IMAGE:$VERSION" \ -t "$REGISTRY_IMAGE:$VERSION" \
-t "$REGISTRY_IMAGE:latest" \ -t "$REGISTRY_IMAGE:latest" \
-f "$ROOT/build-image/Dockerfile" \ -f "$ROOT/build-image/Dockerfile" \
"$ROOT" "$ROOT/build-image"
echo "== Pushing $REGISTRY_IMAGE:$VERSION and :latest =="
docker push "$REGISTRY_IMAGE:$VERSION"
docker push "$REGISTRY_IMAGE:latest"
echo "== Done ==" echo "== Done =="
echo "Image: $REGISTRY_IMAGE:$VERSION (and :latest)" echo "Image: $REGISTRY_IMAGE:$VERSION"
echo "Test it with ./run-image.sh, then publish with ./upload-image.sh"
+10 -54
View File
@@ -1,18 +1,17 @@
# Build environment for schwert_und_magie_on_pebble release artifacts: # Build environment for schwert_und_magie_on_pebble release artifacts:
# Android SDK/NDK (companion app) + Pebble SDK (watch app), matching the # Android SDK/NDK (companion app) + Pebble SDK (watch app), matching the
# versions pinned in app/build.gradle.kts and validated on the maintainer's # versions pinned in app/build.gradle.kts and validated on the maintainer's
# dev machine. Rebuild with ../build-image.sh whenever a version below, or # dev machine. Rebuild and push with ../build-image.sh whenever a version
# the companion app's ndkVersion/compileSdk, changes. # below, or the companion app's ndkVersion/compileSdk, changes.
# #
# Does NOT contain the release keystore or any secrets — those are injected # Does NOT contain the release keystore or any secrets — those are injected
# at job runtime from Gitea Actions secrets, never baked into this image. # at job runtime from Gitea Actions secrets, never baked into this image.
FROM eclipse-temurin:21-jdk-jammy AS base FROM eclipse-temurin:21-jdk-jammy
ARG ANDROID_CMDLINE_TOOLS_VERSION=11076708 ARG ANDROID_CMDLINE_TOOLS_VERSION=11076708
ARG ANDROID_PLATFORM=android-36 ARG ANDROID_PLATFORM=android-36
ARG ANDROID_BUILD_TOOLS=36.1.0 ARG ANDROID_BUILD_TOOLS=36.1.0
ARG ANDROID_NDK=30.0.14904198 ARG ANDROID_NDK=30.0.14904198
ARG ANDROID_CMAKE=3.22.1
ARG PEBBLE_TOOL_VERSION=5.0.35 ARG PEBBLE_TOOL_VERSION=5.0.35
ARG PEBBLE_SDK_CORE_VERSION=4.9.169 ARG PEBBLE_SDK_CORE_VERSION=4.9.169
ARG NODE_VERSION=24.16.0 ARG NODE_VERSION=24.16.0
@@ -25,21 +24,17 @@ ENV DEBIAN_FRONTEND=noninteractive \
# git/unzip/curl/jq: checkout, SDK downloads, and the release workflow's # git/unzip/curl/jq: checkout, SDK downloads, and the release workflow's
# calls to the Gitea API (create release, upload assets). # calls to the Gitea API (create release, upload assets).
# openssh-client: the release workflow's `sftp` upload to dl.ladkau.de.
# python3-venv: pebble-tool's `sdk install` creates a venv per SDK version. # python3-venv: pebble-tool's `sdk install` creates a venv per SDK version.
# dos2unix/autoconf/automake/pkg-config/xa65/build-essential/gettext/flex/bison: # dos2unix/autoconf/automake/pkg-config/xa65/build-essential/gettext: host
# host tools required by the companion app's build_vice.sh (see that file's # tools required by the companion app's build_vice.sh (see that file's own
# own preflight check, plus flex/bison for VICE's AC_PROG_LEX/AC_PROG_YACC- # preflight check) to cross-compile VICE via autotools before NDK clang
# based configure) to cross-compile VICE via autotools before NDK clang takes # takes over for the actual target compilation.
# over for the actual target compilation.
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
curl ca-certificates git unzip tar xz-utils python3 python3-venv file jq openssh-client \ curl ca-certificates git unzip tar xz-utils python3 python3-venv file jq \
dos2unix autoconf automake pkg-config xa65 build-essential gettext flex bison \ dos2unix autoconf automake pkg-config xa65 build-essential gettext \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# --- Android SDK: cmdline-tools, platform, build-tools, NDK, CMake --- # --- Android SDK: cmdline-tools, platform, build-tools, NDK ---
# CMake version must match app/build.gradle.kts's externalNativeBuild.cmake.version
# — baking it in here avoids AGP installing it on first `docker run` instead.
RUN mkdir -p "$ANDROID_HOME/cmdline-tools" \ RUN mkdir -p "$ANDROID_HOME/cmdline-tools" \
&& curl -sSL -o /tmp/cmdline-tools.zip \ && curl -sSL -o /tmp/cmdline-tools.zip \
"https://dl.google.com/android/repository/commandlinetools-linux-${ANDROID_CMDLINE_TOOLS_VERSION}_latest.zip" \ "https://dl.google.com/android/repository/commandlinetools-linux-${ANDROID_CMDLINE_TOOLS_VERSION}_latest.zip" \
@@ -52,7 +47,6 @@ RUN mkdir -p "$ANDROID_HOME/cmdline-tools" \
"platforms;${ANDROID_PLATFORM}" \ "platforms;${ANDROID_PLATFORM}" \
"build-tools;${ANDROID_BUILD_TOOLS}" \ "build-tools;${ANDROID_BUILD_TOOLS}" \
"ndk;${ANDROID_NDK}" \ "ndk;${ANDROID_NDK}" \
"cmake;${ANDROID_CMAKE}" \
>/dev/null >/dev/null
# Node.js: `pebble sdk install` below runs `npm install` for the SDK-core's # Node.js: `pebble sdk install` below runs `npm install` for the SDK-core's
@@ -69,42 +63,4 @@ RUN curl -LsSf https://astral.sh/uv/install.sh | sh \
&& /root/.local/bin/uv tool install "pebble-tool==${PEBBLE_TOOL_VERSION}" \ && /root/.local/bin/uv tool install "pebble-tool==${PEBBLE_TOOL_VERSION}" \
&& /root/.local/bin/pebble sdk install "${PEBBLE_SDK_CORE_VERSION}" && /root/.local/bin/pebble sdk install "${PEBBLE_SDK_CORE_VERSION}"
# --- Warm the Gradle dependency cache ---
# The companion app's real Gradle project files (not the generated build/
# output, and not the VICE/nibtools source tarballs — see .dockerignore) are
# COPYed into a throwaway location and built once here, so every actual
# `docker run` of this image (which bind-mounts a fresh checkout over
# /workspace) hits a warm ~/.gradle cache instead of re-downloading the same
# Maven dependencies from dl.google.com/mavenCentral every single run.
#
# buildVice/buildNibtools are excluded (their source tarballs aren't in the
# build context) — CMake already handles that gracefully, falling back to a
# placeholder (see vice_jni.c / build_vice.sh's own header comment) — so this
# stage only ever warms the Gradle/Maven dependency cache, never bakes in
# compiled VICE/nibtools output.
#
# packageRelease/signReleaseBundle are also excluded — they're the only tasks
# that need the release keystore, which never exists here (secrets are
# injected at job runtime, never baked into the image). Excluding them lets
# everything upstream (dependency resolution, Kotlin/native compilation,
# resource merging, dexing) still run and get cached, without two guaranteed,
# noisy "missing storeFile" failures cluttering every image build.
#
# This is a pure optimization: if the app's dependencies change after this
# image was built, Gradle just downloads the delta against the warm cache at
# `docker run` time — same as it would without this stage, just slower for
# that one run, never broken. `|| true` means a transient network failure
# here only costs a slower first `docker run`, never breaks the image build.
FROM base AS gradle-cache-warm
COPY SchwertUndMagieOnPebbleCompanionApp /tmp/warm/SchwertUndMagieOnPebbleCompanionApp
WORKDIR /tmp/warm/SchwertUndMagieOnPebbleCompanionApp
RUN chmod +x gradlew \
&& (./gradlew bundleRelease assembleRelease \
-x buildVice -x buildNibtools \
-x packageRelease -x signReleaseBundle \
--continue || true)
FROM base
COPY --from=gradle-cache-warm /root/.gradle /root/.gradle
WORKDIR /workspace WORKDIR /workspace
+1 -1
View File
@@ -1 +1 @@
4 1
+1 -8
View File
@@ -74,9 +74,6 @@ fi
[[ "$VERSION" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+) ]] \ [[ "$VERSION" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+) ]] \
|| fail "VERSION '$VERSION' is not a semantic version (expected X.Y.Z, optionally with a -pre+meta suffix)" || fail "VERSION '$VERSION' is not a semantic version (expected X.Y.Z, optionally with a -pre+meta suffix)"
VERSION_CODE=$(( ${BASH_REMATCH[1]} * 10000 + ${BASH_REMATCH[2]} * 100 + ${BASH_REMATCH[3]} )) VERSION_CODE=$(( ${BASH_REMATCH[1]} * 10000 + ${BASH_REMATCH[2]} * 100 + ${BASH_REMATCH[3]} ))
# Android requires a positive versionCode — the untagged dev placeholder
# (0.0.0-dev+<sha>) would otherwise compute to 0 and fail Gradle configuration.
[ "$VERSION_CODE" -gt 0 ] || VERSION_CODE=1
echo "Version: $VERSION (Android versionCode $VERSION_CODE)" echo "Version: $VERSION (Android versionCode $VERSION_CODE)"
# Patch versions into the tracked source files for this build only, then # Patch versions into the tracked source files for this build only, then
@@ -90,11 +87,7 @@ sed -i \
-e "s/versionCode = [0-9]\+/versionCode = $VERSION_CODE/" \ -e "s/versionCode = [0-9]\+/versionCode = $VERSION_CODE/" \
-e "s/versionName = \"[^\"]*\"/versionName = \"$VERSION\"/" \ -e "s/versionName = \"[^\"]*\"/versionName = \"$VERSION\"/" \
"$GRADLE_KTS" "$GRADLE_KTS"
# Pebble's own build tooling parses package.json's version strictly as sed -i -e "s/\"version\": \"[^\"]*\"/\"version\": \"$VERSION\"/" "$WATCH_PKG_JSON"
# X.Y.Z integers — it rejects the -pre+meta suffix dist.sh otherwise allows
# (including the default "0.0.0-dev+<sha>" placeholder), so strip it here.
PEBBLE_VERSION="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.${BASH_REMATCH[3]}"
sed -i -e "s/\"version\": \"[^\"]*\"/\"version\": \"$PEBBLE_VERSION\"/" "$WATCH_PKG_JSON"
mkdir -p "$DIST" mkdir -p "$DIST"
+14 -45
View File
@@ -249,11 +249,11 @@ be bumped in source files beforehand (`dist.sh` patches `versionCode`/
running the same thing locally). running the same thing locally).
The whole toolchain (Android SDK/NDK, Pebble SDK) lives in The whole toolchain (Android SDK/NDK, Pebble SDK) lives in
`build-image/Dockerfile`, built locally and pushed to a container registry by `build-image/Dockerfile`, built and pushed to a container registry by
three separate scripts (below). This keeps the setup portable: the runner `build-image.sh`. This keeps the setup portable: the runner just needs Docker
just needs Docker and pulls that image, so it isn't tied to any one machine's and pulls that image, so it isn't tied to any one machine's local toolchain
local toolchain install and can be moved or re-registered elsewhere without install and can be moved or re-registered elsewhere without touching this
touching this repo's build scripts. repo's build scripts.
### 4.1 Build environment image ### 4.1 Build environment image
@@ -262,14 +262,9 @@ pinned in `app/build.gradle.kts`, matching what's validated for local builds.
It does **not** contain the release keystore — that's injected at job runtime It does **not** contain the release keystore — that's injected at job runtime
from Actions secrets (§4.3), never baked into the image. from Actions secrets (§4.3), never baked into the image.
Three scripts, kept separate so a Dockerfile change can be built and tested
locally before anything is pushed to the registry:
```bash ```bash
cp registry.env.example registry.env # fill in your registry credentials cp registry.env.example registry.env # fill in your registry credentials
./build-image.sh # builds :latest and the pinned VERSION tag, locally only ./build-image.sh # builds + pushes :latest and the pinned VERSION tag
./run-image.sh # runs dist.sh inside that local image — sanity-check before publishing
./upload-image.sh # pushes the already-built :latest and VERSION tag to cr.ladkau.de
``` ```
Rebuild and push whenever `build-image/Dockerfile` changes (e.g. a Pebble SDK Rebuild and push whenever `build-image/Dockerfile` changes (e.g. a Pebble SDK
@@ -278,16 +273,6 @@ meaningful. The workflow (§4.2) pulls `:latest` by default; if you need a
release to be reproducible against an exact toolchain image, pin the `image:` release to be reproducible against an exact toolchain image, pin the `image:`
line in `.gitea/workflows/release.yml` to the versioned tag instead. line in `.gitea/workflows/release.yml` to the versioned tag instead.
`run-image.sh` bind-mounts this repo straight into the container, so before
each run it wipes generated build artifacts (`vice-src`, `vice-libs`,
`nibtools-src`, `nibtools-libs`, `app/build`, `app/.cxx`, the watch app's
`build/`) — otherwise leftovers from a previous local run would let
`build_vice.sh`/`build_nibtools.sh` skip work a real fresh CI checkout always
does, hiding bugs that only show up in CI. The container also runs as root
(needed for the baked-in SDK/NDK/Gradle setup), so it chowns the whole repo
back to your host user on exit — you shouldn't ever need `sudo` to clean up
after it.
### 4.2 Runner setup (one-time) ### 4.2 Runner setup (one-time)
1. Enable Actions for the repo: repo Settings → Actions → enable, if not 1. Enable Actions for the repo: repo Settings → Actions → enable, if not
@@ -300,21 +285,18 @@ after it.
# https://gitea.com/gitea/act_runner — grab the latest release binary # https://gitea.com/gitea/act_runner — grab the latest release binary
./act_runner register --no-interactive \ ./act_runner register --no-interactive \
--instance <your gitea URL> --token <token> \ --instance <your gitea URL> --token <token> \
--name <runner-name> --labels ubuntu-latest:docker://node:20-bookworm --name <runner-name> --labels self-hosted:docker://node:20-bookworm
./act_runner daemon ./act_runner daemon
``` ```
The image after `docker://` in `--labels` is only a fallback for jobs that The image after `docker://` in `--labels` is only a fallback for jobs that
don't specify their own `container:` — irrelevant here since don't specify their own `container:` — irrelevant here since
`.gitea/workflows/release.yml` always pins its own image, but the runner `.gitea/workflows/release.yml` always pins its own image, but the runner
still needs a Docker-executor label registered to use that executor at still needs a Docker-executor label registered to use that executor at
all. The label name itself (`ubuntu-latest` above) must match `runs-on:` all. The label name itself (`self-hosted`) must match `runs-on:` in
in `.gitea/workflows/release.yml` — edit both together if you rename it, `.gitea/workflows/release.yml` — edit both together if you rename it.
or reuse a label an existing runner already advertises (check Site Admin → 4. The runner's Docker daemon needs pull access to the registry — run
Actions → Runners) to skip registering a new one entirely. `docker login <registry>` once on that machine with the same credentials
as `registry.env`.
No manual `docker login` needed on the runner host — the workflow's
`container:` block authenticates the image pull itself via the
`REGISTRY_USER`/`REGISTRY_PASSWORD` secrets (§4.3).
### 4.3 Repo secrets ### 4.3 Repo secrets
@@ -324,9 +306,6 @@ Settings → Actions → Secrets, add:
|---|---| |---|---|
| `RELEASE_KEYSTORE_B64` | `base64 -w0 SchwertUndMagieOnPebbleCompanionApp/release.keystore` | | `RELEASE_KEYSTORE_B64` | `base64 -w0 SchwertUndMagieOnPebbleCompanionApp/release.keystore` |
| `RELEASE_KEYSTORE_PROPERTIES` | the full contents of `SchwertUndMagieOnPebbleCompanionApp/keystore.properties` (§2.2) | | `RELEASE_KEYSTORE_PROPERTIES` | the full contents of `SchwertUndMagieOnPebbleCompanionApp/keystore.properties` (§2.2) |
| `REGISTRY_USER` | same as `REGISTRY_USER` in `registry.env` |
| `REGISTRY_PASSWORD` | same as `REGISTRY_PASSWORD` in `registry.env` |
| `DL_SFTP_KEY` | private key (PEM) for the `uploader` SFTP account on dl.ladkau.de |
`secrets.GITEA_TOKEN` (used to create the release and upload assets) is `secrets.GITEA_TOKEN` (used to create the release and upload assets) is
Gitea's own auto-generated per-job token — nothing to create or add yourself. Gitea's own auto-generated per-job token — nothing to create or add yourself.
@@ -342,25 +321,15 @@ git push origin v1.2.3
``` ```
Watch the run under the repo's Actions tab. On success, the release appears Watch the run under the repo's Actions tab. On success, the release appears
under the repo's Releases page with the three versioned artifacts attached, under the repo's Releases page with the three versioned artifacts attached.
and the same three files are uploaded over SFTP to
`dl.ladkau.de:files/schwert-und-magie/` (using the `DL_SFTP_KEY` secret,
§4.3) for direct download outside of Gitea.
To build the same versioned artifacts locally without pushing a tag (e.g. to To build the same versioned artifacts locally without pushing a tag (e.g. to
test before releasing), either run `dist.sh` directly with the host toolchain: test before releasing):
```bash ```bash
VERSION=1.2.3 ./dist.sh VERSION=1.2.3 ./dist.sh
``` ```
or run it inside the build-image container (same environment the runner
uses — see §4.1):
```bash
./run-image.sh 1.2.3
```
## 5. Pebble watch app → Rebble app store / direct distribution ## 5. Pebble watch app → Rebble app store / direct distribution
The official Pebble app store shut down years ago; the community-run The official Pebble app store shut down years ago; the community-run
-73
View File
@@ -1,73 +0,0 @@
#!/usr/bin/env bash
# Runs dist.sh inside the local build-image container, mirroring what the
# Gitea Actions release workflow does — useful for testing build-image
# changes (or dist.sh/build_vice.sh/build_nibtools.sh changes) locally
# before pushing anything to cr.ladkau.de.
#
# Usage: ./run-image.sh [VERSION]
# VERSION is passed through to dist.sh; omit it for dist.sh's own
# git-tag-based default (see dist.sh's header comment).
set -euo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")"
ROOT="$(pwd)"
fail() { echo "PREFLIGHT FAIL: $*" >&2; exit 1; }
command -v docker >/dev/null 2>&1 \
|| fail "docker not found in PATH"
[ -f "$ROOT/registry.env" ] \
|| fail "registry.env not found — copy registry.env.example to registry.env and fill in your cr.ladkau.de credentials"
# shellcheck disable=SC1091
source "$ROOT/registry.env"
: "${REGISTRY_IMAGE:?registry.env must set REGISTRY_IMAGE}"
IMAGE_TAG="$(<"$ROOT/build-image/VERSION")"
[ -n "$IMAGE_TAG" ] || fail "build-image/VERSION is empty"
IMAGE="$REGISTRY_IMAGE:$IMAGE_TAG"
docker image inspect "$IMAGE" >/dev/null 2>&1 \
|| fail "$IMAGE not found locally — run ./build-image.sh first"
VERSION="${1:-${VERSION:-}}"
# CI always starts from a fresh checkout, but this script bind-mounts the live
# host repo — so build outputs left over from a previous local run (e.g. a
# vice-src/ already configured, or a libvice.a that's already built) would
# make build_vice.sh/build_nibtools.sh skip work they'd have to do on a real
# fresh checkout, silently hiding bugs (like a missing host build tool) that
# only show up in CI. Wipe them first so every run exercises a true from-
# scratch build, same as CI.
JNI="$ROOT/SchwertUndMagieOnPebbleCompanionApp/app/src/main/jni"
echo "== Cleaning generated build artifacts for a fresh build =="
rm -rf \
"$JNI/vice-src" "$JNI/vice-libs" \
"$JNI/nibtools-src" "$JNI/nibtools-libs" \
"$ROOT/SchwertUndMagieOnPebbleCompanionApp/app/build" \
"$ROOT/SchwertUndMagieOnPebbleCompanionApp/app/.cxx" \
"$ROOT/SchwertUndMagieOnPebbleWatchApp/build"
echo "== Running dist.sh inside $IMAGE =="
# The container runs as root (needed for the SDK/NDK/Gradle setup baked into
# the image), so anything it writes into this bind mount — dist/, app/build,
# .cxx, etc. — would otherwise come back owned by root, leaving the host repo
# unusable without sudo. Chown everything back to the host user on exit,
# whether dist.sh succeeds or fails.
docker run --rm \
-v "$ROOT:/workspace" \
-w /workspace \
-e VERSION="$VERSION" \
-e HOST_UID="$(id -u)" \
-e HOST_GID="$(id -g)" \
"$IMAGE" \
bash -c '
git config --global --add safe.directory /workspace
trap "chown -R \"$HOST_UID:$HOST_GID\" /workspace" EXIT
./dist.sh
'
echo "== Done — artifacts in dist/ =="
ls -la "$ROOT/dist"
-41
View File
@@ -1,41 +0,0 @@
#!/usr/bin/env bash
# Pushes the build environment image — already built locally with
# ./build-image.sh, and ideally verified with ./run-image.sh — to
# cr.ladkau.de. The Gitea Actions release workflow pulls this image to
# run dist.sh.
set -euo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")"
ROOT="$(pwd)"
fail() { echo "PREFLIGHT FAIL: $*" >&2; exit 1; }
command -v docker >/dev/null 2>&1 \
|| fail "docker not found in PATH"
[ -f "$ROOT/registry.env" ] \
|| fail "registry.env not found — copy registry.env.example to registry.env and fill in your cr.ladkau.de credentials"
# shellcheck disable=SC1091
source "$ROOT/registry.env"
: "${REGISTRY:?registry.env must set REGISTRY}"
: "${REGISTRY_IMAGE:?registry.env must set REGISTRY_IMAGE}"
: "${REGISTRY_USER:?registry.env must set REGISTRY_USER}"
: "${REGISTRY_PASSWORD:?registry.env must set REGISTRY_PASSWORD}"
VERSION="$(<"$ROOT/build-image/VERSION")"
[ -n "$VERSION" ] || fail "build-image/VERSION is empty"
docker image inspect "$REGISTRY_IMAGE:$VERSION" >/dev/null 2>&1 \
|| fail "$REGISTRY_IMAGE:$VERSION not found locally — run ./build-image.sh first"
echo "== Logging in to $REGISTRY =="
echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY" -u "$REGISTRY_USER" --password-stdin
echo "== Pushing $REGISTRY_IMAGE:$VERSION and :latest =="
docker push "$REGISTRY_IMAGE:$VERSION"
docker push "$REGISTRY_IMAGE:latest"
echo "== Done =="
echo "Image: $REGISTRY_IMAGE:$VERSION"