Add a Docker-based build image and wire it into CI for the watch app
build / build (push) Failing after 0s

This commit is contained in:
ml
2026-07-18 07:52:40 +02:00
parent 4e83ebbf50
commit ff2b43f1e0
11 changed files with 286 additions and 4472 deletions
+28 -8
View File
@@ -1,7 +1,11 @@
name: build
# Build a versioned Linux CLI release tarball (see `make package`) on
# every push/PR, plus on-demand via the Gitea "Run workflow" button.
# Build a versioned Linux CLI release tarball (see `make package`) and the
# Pebble watchapp (see `make watchapp`) on every push/PR, plus on-demand
# via the Gitea "Run workflow" button. Runs inside build-image (see
# ../../build-image/Dockerfile, built/pushed via ../../build-image.sh and
# ../../upload-image.sh), which bundles the Pebble SDK alongside the C
# toolchain.
on:
push:
pull_request:
@@ -17,12 +21,18 @@ jobs:
# Must match a label your act_runner is registered with.
runs-on: ubuntu-latest
container:
image: cr.ladkau.de/deck-in-a-dash/builder:latest
credentials:
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }}
steps:
- uses: actions/checkout@v4
with:
# `make package`'s version comes from `git describe --tags` (see
# the Makefile's `package` target) - needs full history/tags,
# not actions/checkout's default shallow single-commit clone.
# `make package`/`make watchapp`'s version comes from `git
# describe --tags` - needs full history/tags, not
# actions/checkout's default shallow single-commit clone.
fetch-depth: 0
- name: Preflight
@@ -30,12 +40,14 @@ jobs:
set -euo pipefail
command -v cc >/dev/null 2>&1 || { echo "PREFLIGHT FAIL: no C compiler (cc) in PATH" >&2; exit 1; }
command -v make >/dev/null 2>&1 || { echo "PREFLIGHT FAIL: make not in PATH" >&2; exit 1; }
command -v pebble >/dev/null 2>&1 || { echo "PREFLIGHT FAIL: pebble not in PATH" >&2; exit 1; }
- name: Build and test
run: |
set -euo pipefail
make test
make package
make watchapp
- name: Upload build artifact
# v4 uses the newer @actions/artifact backend, which this Gitea
@@ -45,14 +57,21 @@ jobs:
name: deck-in-a-dash
path: dist/deck-in-a-dash-*-linux-*.tar.gz
- name: Upload watchapp artifact
uses: actions/upload-artifact@v3
with:
name: deck-in-a-dash-watchapp
path: dist/deck-in-a-dash-*.pbw
- name: Publish to dl.ladkau.de
# Uploads the tarball over SFTP instead of using
# Uploads the tarball and .pbw over SFTP instead of using
# actions/upload-artifact (whose zip wrapping can't be disabled).
# Only runs on push so PR builds don't publish.
if: gitea.event_name == 'push'
run: |
set -euo pipefail
FILE="$(ls dist/deck-in-a-dash-*-linux-*.tar.gz)"
TARBALL="$(ls dist/deck-in-a-dash-*-linux-*.tar.gz)"
PBW="$(ls dist/deck-in-a-dash-*.pbw)"
mkdir -p ~/.ssh
echo "${{ secrets.DL_SFTP_KEY }}" > ~/.ssh/dl_sftp_key
chmod 600 ~/.ssh/dl_sftp_key
@@ -60,5 +79,6 @@ jobs:
-o StrictHostKeyChecking=accept-new \
uploader@dl.ladkau.de <<EOF
-mkdir files/deck-in-a-dash
put $FILE files/deck-in-a-dash/$(basename "$FILE")
put $TARBALL files/deck-in-a-dash/$(basename "$TARBALL")
put $PBW files/deck-in-a-dash/$(basename "$PBW")
EOF
+4
View File
@@ -17,3 +17,7 @@ core
core.*
.DS_Store
# Container registry credentials for build-image.sh/run-image.sh/
# upload-image.sh — never commit; copy registry.env.example instead.
/registry.env
File diff suppressed because it is too large Load Diff
+35 -7
View File
@@ -106,19 +106,47 @@ from the org/instance level).
### Watch app packaging (`make watchapp`)
A separate target, deliberately **not** a prerequisite of `all` and
**not** run by CI - it shells out to `pebble build` inside `watch/`
(via `watch/wscript`'s own Pebble SDK/waf tooling, not this Makefile's
plain-`cc` rules) and copies the resulting `watch/build/watch.pbw` to
A separate target, deliberately **not** a prerequisite of `all` - it
shells out to `pebble build` inside `watch/` (via `watch/wscript`'s own
Pebble SDK/waf tooling, not this Makefile's plain-`cc` rules) and copies
the resulting `watch/build/watch.pbw` to
`dist/deck-in-a-dash-<version>.pbw`, using the exact same version
resolution as `make package` above (current git tag, or `make watchapp
VERSION=1.2.3` to override - see that section, not duplicated here).
Requires the Pebble SDK (`pebble` on `PATH`, plus the Python 3 + Pillow
`watch/wscript` needs for its resource-generation scripts) - a machine
without it can still build/test/package the CLI via every other target
in this file. Not wired into `.gitea/workflows/build.yml`: doing so
would need the Pebble SDK toolchain installed on the CI runner, which
this project doesn't currently provision.
in this file.
`.gitea/workflows/build.yml` **does** run `make watchapp` on every
push/PR, alongside `make test`/`make package` - the whole job runs
inside a container image (`build-image/`, built and pushed with
`build-image.sh`/`upload-image.sh`, see "Build image" below) that has
the Pebble SDK baked in, so the CI runner itself needs no toolchain
beyond Docker access to pull that image. Both the CLI tarball and the
`.pbw` are uploaded as build artifacts and published to
`dl.ladkau.de/files/deck-in-a-dash/` on push.
### Build image
`build-image/Dockerfile` pins the exact toolchain versions (Pebble
Tool, Pebble SDK core, Node.js) needed for `make test`, `make package`,
and `make watchapp`, so builds are reproducible independent of whatever
happens to be installed on a given machine. Three root-level scripts
drive it, each reading registry settings from a gitignored
`registry.env` (copy `registry.env.example` to create it - never commit
the real file, since it holds registry credentials) and the image tag
from `build-image/VERSION`:
- `build-image.sh` - builds and locally tags the image (`:VERSION` and
`:latest`), no push.
- `run-image.sh [VERSION]` - runs `make test package watchapp` inside
the locally-built image against a live bind-mount of the repo, wiping
generated build artifacts first for CI parity. Use this to validate a
`build-image/Dockerfile` change before pushing the image anywhere.
- `upload-image.sh` - logs in and pushes the image (`:VERSION` and
`:latest`) to the registry; `.gitea/workflows/build.yml` pulls
`:latest` to run its build job (see "Watch app packaging" above).
## Architecture
+18
View File
@@ -60,6 +60,10 @@ watch/ The actual Pebble watchapp - links engine/src/
from the rest of this repo (`make watchapp`,
below) - requires the Pebble SDK.
docs/ Architecture/dataflow diagrams, input/output format reference.
build-image/ Dockerfile for the reproducible build environment
(C toolchain + Pebble SDK) used by CI and by the
build-image.sh/run-image.sh/upload-image.sh scripts
at the repo root (see "Building" below).
Makefile Single root Makefile, builds both engine/ and
interpreter/, and packages a release (`make package`).
```
@@ -94,6 +98,20 @@ make watchapp # -> dist/deck-in-a-dash-<version>.pbw, same version
# resolution as `make package`
```
**CI runs `make watchapp` too**`.gitea/workflows/build.yml` builds
and tests everything inside a container built from `build-image/`,
which bundles the Pebble SDK alongside the C toolchain so the runner
itself doesn't need either installed. To build/test that image
yourself, copy `registry.env.example` to `registry.env` and fill in
your registry credentials, then:
```bash
./build-image.sh # build and locally tag the image
./run-image.sh # run `make test package watchapp` inside it, against
# this checkout, for a CI-equivalent local build
./upload-image.sh # push the image so CI can pull it
```
## Running
**Daily use**, via `dist/run-engine.sh`: pulls today's date and the
Executable
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
# Builds the release build environment image (build-image/Dockerfile)
# locally, tagged with build-image/VERSION and :latest. Does not push -
# test it with ./run-image.sh first, then publish with ./upload-image.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_IMAGE:?registry.env must set REGISTRY_IMAGE}"
VERSION="$(<"$ROOT/build-image/VERSION")"
[ -n "$VERSION" ] || fail "build-image/VERSION is empty"
echo "== Building $REGISTRY_IMAGE:$VERSION =="
docker build \
-t "$REGISTRY_IMAGE:$VERSION" \
-t "$REGISTRY_IMAGE:latest" \
"$ROOT/build-image"
echo "== Done =="
echo "Image: $REGISTRY_IMAGE:$VERSION (and :latest)"
echo "Test it with ./run-image.sh, then publish with ./upload-image.sh"
+49
View File
@@ -0,0 +1,49 @@
# Build environment for deck_in_a_dash release artifacts: a plain C
# toolchain for the desktop deck-engine/interpreter-cli binaries (`make
# test`, `make package`) plus the Pebble SDK for the watch app (`make
# watchapp`), pinned to the versions validated on the maintainer's dev
# machine. Rebuild with ../build-image.sh whenever a version below
# changes.
#
# Does NOT contain any secrets - registry/SFTP credentials are injected
# at job runtime from Gitea Actions secrets, never baked into this image.
FROM ubuntu:22.04
ARG PEBBLE_TOOL_VERSION=5.0.39
ARG PEBBLE_SDK_CORE_VERSION=4.17
ARG NODE_VERSION=24.16.0
ENV DEBIAN_FRONTEND=noninteractive \
PATH=/root/.local/bin:${PATH}
# build-essential: gcc/make for the desktop deck-engine/interpreter-cli
# binaries (`make test`, `make package`).
# python3/python3-venv/python3-pip: `pebble sdk install` below creates a
# venv per SDK version; Pillow (installed below) is needed at `pebble
# build` time by watch/scripts/gen_card_images.py and gen_app_icon.py.
# git/curl/ca-certificates/unzip/tar/xz-utils: checkout, tool downloads,
# and `make package`/`make watchapp`'s `git describe --tags` versioning.
# openssh-client: the build workflow's `sftp` publish to dl.ladkau.de.
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential python3 python3-venv python3-pip \
git curl ca-certificates unzip tar xz-utils openssh-client \
&& rm -rf /var/lib/apt/lists/*
RUN pip3 install --no-cache-dir Pillow
# Node.js: `pebble sdk install` below runs `npm install` for the SDK
# core's bundled webpack tooling - it does not bring its own node/npm.
RUN curl -sSL -o /tmp/node.tar.xz \
"https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz" \
&& tar -xJf /tmp/node.tar.xz -C /usr/local --strip-components=1 \
&& rm /tmp/node.tar.xz
# Pebble SDK: pebble-tool (via uv, pipx-style isolated install) + the
# sdk-core toolchain/webpack deps for PEBBLE_SDK_CORE_VERSION, both
# fully resolved here so no `docker run` of this image needs network
# access to install anything before `pebble build` works.
RUN curl -LsSf https://astral.sh/uv/install.sh | sh \
&& uv tool install "pebble-tool==${PEBBLE_TOOL_VERSION}" \
&& pebble sdk install "${PEBBLE_SDK_CORE_VERSION}"
WORKDIR /workspace
+1
View File
@@ -0,0 +1 @@
1
+7
View File
@@ -0,0 +1,7 @@
# Copy to registry.env (gitignored, never commit the real values) and
# fill in your cr.ladkau.de credentials. Used by build-image.sh,
# run-image.sh, and upload-image.sh.
REGISTRY=cr.ladkau.de
REGISTRY_IMAGE=cr.ladkau.de/deck-in-a-dash/builder
REGISTRY_USER=
REGISTRY_PASSWORD=
Executable
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env bash
# Runs `make test package watchapp` inside the local build-image
# container, mirroring what the Gitea Actions build workflow does -
# useful for testing build-image changes locally before pushing
# anything to cr.ladkau.de.
#
# Usage: ./run-image.sh [VERSION]
# VERSION is passed through to `make` (see the Makefile's `package`/
# `watchapp` targets); omit it for their own git-tag-based default.
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 output left over from a previous local run
# (build/, dist/, watch/build, and the generated watch resources below)
# would make this run skip work a real fresh checkout wouldn't, silently
# hiding bugs that only show up in CI. Wipe them first so every run
# exercises a true from-scratch build, same as CI.
echo "== Cleaning generated build artifacts for a fresh build =="
rm -rf \
"$ROOT/build" "$ROOT/dist" "$ROOT/watch/build" \
"$ROOT/watch/resources/img" "$ROOT/watch/resources/app_icon.png" \
"$ROOT/watch/src/c/i18n_tables.auto.c"
echo "== Running make test package watchapp inside $IMAGE =="
# The container runs as root (needed for the toolchain baked into the
# image), so anything it writes into this bind mount — dist/, build/,
# watch/build, 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 the build 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
make test package watchapp
'
echo "== Done — artifacts in dist/ =="
ls -la "$ROOT/dist"
+41
View File
@@ -0,0 +1,41 @@
#!/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 build workflow pulls this image to run
# `make test package watchapp`.
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"