From ff2b43f1e06c0951c28602b96f38024221ab9652 Mon Sep 17 00:00:00 2001
From: ml
Date: Sat, 18 Jul 2026 07:52:40 +0200
Subject: [PATCH] Add a Docker-based build image and wire it into CI for the
watch app
---
.gitea/workflows/build.yml | 36 +-
.gitignore | 4 +
...n-is-being-continued-from-a-previous-c.txt | 4457 -----------------
CLAUDE.md | 42 +-
README.md | 18 +
build-image.sh | 34 +
build-image/Dockerfile | 49 +
build-image/VERSION | 1 +
registry.env.example | 7 +
run-image.sh | 69 +
upload-image.sh | 41 +
11 files changed, 286 insertions(+), 4472 deletions(-)
delete mode 100644 2026-07-17-060832-this-session-is-being-continued-from-a-previous-c.txt
create mode 100755 build-image.sh
create mode 100644 build-image/Dockerfile
create mode 100644 build-image/VERSION
create mode 100644 registry.env.example
create mode 100755 run-image.sh
create mode 100755 upload-image.sh
diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml
index 1d2e28a..8201eee 100644
--- a/.gitea/workflows/build.yml
+++ b/.gitea/workflows/build.yml
@@ -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 <transiting_planet), aspect_disp
- +lay_name(a->type),
- 191 + ui("ui.natal", "natal"), body_display_name(a->natal_
- +planet));
- 192 +}
- 193 +
- 194 static void print_top_transits_header(FILE *out, int count) {
- 195 if (count == 1) {
- 196 fprintf(out, "%s\n", ui("interp.top_transits.one", "Top si
- gnificant transit:"));
-
-● Now the text format reordering:
-
-● Update(interpreter/src/main.c) ⎿ Added 9 lines, removed 5 lines
- 223 }
- 224 fprintf(out, "\n");
- 225
- 226 + fprintf(out, "%s\n", ui("interp.heading_guidance", "Guidance
- +"));
- 227 + if (interp->top_item_count > 0) {
- 228 + print_top_transit_line(out, &interp->top_items[0].aspect);
- 229 + }
- 230 + char guidance_buf[GUIDANCE_TEXT_MAX];
- 231 + guidance_write(guidance_buf, sizeof guidance_buf, "", interp
- +, &reading->spread);
- 232 + fputs(guidance_buf, out);
- 233 + fprintf(out, "\n");
- 234 +
- 235 if (interp->top_item_count == 0) {
- 236 fprintf(out, "%s\n", ui("interp.no_notable_transits", "No
- notable transits today."));
- 237 } else {
- ...
- 258 fprintf(out, "\n");
- 259
- 260 print_celtic_cross_text(out, &reading->spread);
- 252 - fprintf(out, "\n");
- 253 -
- 254 - char guidance_buf[GUIDANCE_TEXT_MAX];
- 255 - guidance_write(guidance_buf, sizeof guidance_buf, "", interp
- -, &reading->spread);
- 256 - fputs(guidance_buf, out);
- 261 }
- 262
- 263 /* ===== --format html ===== */
-
-● Now the HTML format:
-
-● Update(interpreter/src/main.c)
- ⎿ Added 19 lines
- 293 }
- 294 fprintf(out, "
\n");
- 295
- 296 + char *guidance = capture_guidance(interp, &reading->spread);
- 297 + fprintf(out, "%s
\n\n", ui("
- +interp.heading_guidance", "Guidance"));
- 298 + if (interp->top_item_count > 0) {
- 299 + const Aspect *top_a = &interp->top_items[0].aspect;
- 300 + fprintf(out, "
%s %s %s %s %s
\n", u
- +i("ui.transiting", "Transiting"),
- 301 + body_display_name(top_a->transiting_planet), aspec
- +t_display_name(top_a->type),
- 302 + ui("ui.natal", "natal"), body_display_name(top_a->
- +natal_planet));
- 303 + }
- 304 + const char *start = guidance;
- 305 + for (const char *p = guidance; ; p++) {
- 306 + if (*p == '\n' || *p == '\0') {
- 307 + if (p > start) fprintf(out, "
%.*s
\n", (int)(p - s
- +tart), start);
- 308 + if (*p == '\0') break;
- 309 + start = p + 1;
- 310 + }
- 311 + }
- 312 + free(guidance);
- 313 + fprintf(out, "
\n");
- 314 +
- 315 fprintf(out, "%s
\n", ui("interp.heading_transits",
- "Significant Transits"));
- 316 if (interp->top_item_count == 0) {
- 317 fprintf(out, "%s
\n", ui("interp.no_notable_transits
- ", "No notable transits today."));
-
-● Now remove the old guidance block that used to sit at the end of the HTML
- output:
-
- Read 1 file (ctrl+o to expand)
-
-● Update(interpreter/src/main.c)
- ⎿ Removed 13 lines
- 352 }
- 353 fprintf(out, "\n");
- 354
- 355 - char *guidance = capture_guidance(interp, &reading->spread);
- 356 - fprintf(out, "%s
\n\n", ui("
- -interp.heading_guidance", "Guidance"));
- 357 - const char *start = guidance;
- 358 - for (const char *p = guidance; ; p++) {
- 359 - if (*p == '\n' || *p == '\0') {
- 360 - if (p > start) fprintf(out, "
%.*s
\n", (int)(p - s
- -tart), start);
- 361 - if (*p == '\0') break;
- 362 - start = p + 1;
- 363 - }
- 364 - }
- 365 - free(guidance);
- 366 - fprintf(out, "
\n");
- 367 -
- 355 fprintf(out, "