From 42e6f5271d9cac1f1eaa73f6ae7a5b89643cca3a Mon Sep 17 00:00:00 2001 From: Bastian de Byl Date: Mon, 14 Sep 2026 16:40:57 -0400 Subject: [PATCH 01/10] fix(gitea-actions): serve CI images from the Gitea registry The CI job images only existed under localhost/ in the gitea-runner store, and the nightly CI prune deletes any image older than 48h that no container holds. After every idle stretch CI failed in 0-1s pulling localhost/gitea-ci:latest until the role was re-run and the images rebuilt. - Build under git.debyl.io/gitbot/..., push after every run, and pull from the registry instead of rebuilding when the Containerfile is unchanged. - Log gitea-runner in via ~/.docker/config.json, which both act_runner (job image pulls) and podman read. - Label the base images io.debyl.ci-base and skip that label in the CI prune; its `until` counts from build time, so a re-pulled image would otherwise be deleted again the next night. Workflows pinning `container: image: localhost/gitea-ci-*` must move to the registry paths. Co-Authored-By: Claude Opus 5 --- ansible/roles/gitea-actions/defaults/main.yml | 49 ++++++++- ansible/roles/gitea-actions/tasks/images.yml | 101 ++++++++++-------- ansible/roles/podman/defaults/main.yml | 3 + .../roles/podman/templates/podman-prune.sh.j2 | 10 +- 4 files changed, 108 insertions(+), 55 deletions(-) diff --git a/ansible/roles/gitea-actions/defaults/main.yml b/ansible/roles/gitea-actions/defaults/main.yml index d182741..2ba8593 100644 --- a/ansible/roles/gitea-actions/defaults/main.yml +++ b/ansible/roles/gitea-actions/defaults/main.yml @@ -22,18 +22,57 @@ act_runner_bin: /usr/local/bin/act_runner act_runner_config_dir: /etc/act_runner act_runner_work_dir: /var/lib/act_runner -# Job container images (built locally into the gitea-runner rootless image -# store by tasks/images.yml; never pulled — force_pull is false). -gitea_ci_image: localhost/gitea-ci:latest +# Job container images. tasks/images.yml builds them into the gitea-runner +# rootless store and pushes them to the Gitea container registry. +# +# They used to live only under localhost/, and the nightly podman prune +# (roles/podman: podman_prune_ci_until) deletes any CI-user image older than +# 48h that no container is using -- so every idle weekend CI failed in 0-1s on +# `docker pull localhost/gitea-ci:latest` until someone re-ran this role and +# waited out a full rebuild. With a registry copy, a pruned image is simply +# re-pulled by the next job (force_pull stays false, so a present image is +# never re-pulled), and this role pulls instead of rebuilding when the +# Containerfile has not changed. +# +# Workflows that pin `container: image:` must use these registry paths too +# (esp-mg-tpms, skudak/esp32-stm32-vcu, skudak/esp32-web-interface). +gitea_ci_registry: git.debyl.io +# Namespace = the owner of gitea_registry_username / gitea_registry_token (vault). +gitea_ci_registry_namespace: gitbot +gitea_ci_image: "{{ gitea_ci_registry }}/{{ gitea_ci_registry_namespace }}/gitea-ci:latest" # ESP-IDF firmware image tag tracks the upstream espressif/idf release we build from. esp_idf_version: v5.4.1 -gitea_ci_espidf_image: "localhost/gitea-ci-espidf:{{ esp_idf_version }}" +gitea_ci_espidf_image: "{{ gitea_ci_registry }}/{{ gitea_ci_registry_namespace }}/gitea-ci-espidf:{{ esp_idf_version }}" # PlatformIO image for Arduino-framework ESP32 builds (esp32-web-interface). # Tag tracks the pre-baked espressif32 platform version; both pins match the # hardware-validated local build. platformio_core_version: "6.1.19" pio_espressif32_version: "7.0.1" -gitea_ci_platformio_image: "localhost/gitea-ci-platformio:{{ pio_espressif32_version }}" +gitea_ci_platformio_image: "{{ gitea_ci_registry }}/{{ gitea_ci_registry_namespace }}/gitea-ci-platformio:{{ pio_espressif32_version }}" + +# Registry credentials for the gitea-runner user. The Docker-format path is read +# by both act_runner (to authenticate job image pulls) and podman (as its +# fallback auth file), so one login covers the runner and this role. +gitea_ci_registry_authfile: "{{ gitea_runner_home }}/.docker/config.json" + +# Label stamped on the CI base images so the nightly prune skips them; must match +# podman_prune_ci_keep_label in roles/podman/defaults/main.yml. Without it the +# prune (whose `until` counts from build time, not pull time) would delete a +# re-pulled image again the next night -- a 7.8 GB ESP-IDF re-download after +# every idle day. Superseded tags (e.g. after an esp_idf_version bump) are +# therefore kept too; remove them by hand. +gitea_ci_keep_label: io.debyl.ci-base + +gitea_ci_images: + - image: "{{ gitea_ci_image }}" + containerfile: Containerfile.ci + template: Containerfile.ci + - image: "{{ gitea_ci_espidf_image }}" + containerfile: Containerfile.espidf + template: Containerfile.espidf.j2 + - image: "{{ gitea_ci_platformio_image }}" + containerfile: Containerfile.platformio + template: Containerfile.platformio.j2 # Default labels for every runner — map runs-on values to the local CI image. # Firmware jobs opt into the ESP-IDF image per-job via `container:` in their workflow. diff --git a/ansible/roles/gitea-actions/tasks/images.yml b/ansible/roles/gitea-actions/tasks/images.yml index e33b5e4..8c41e34 100644 --- a/ansible/roles/gitea-actions/tasks/images.yml +++ b/ansible/roles/gitea-actions/tasks/images.yml @@ -1,4 +1,8 @@ --- +# CI job images: stage each Containerfile, restore the image from the Gitea +# registry if the nightly prune removed it, rebuild only when the Containerfile +# changed, then push so the registry always holds what the runner uses. +# See gitea_ci_images in defaults/main.yml. - name: create CI image build directory become: true become_user: "{{ gitea_runner_user }}" @@ -8,71 +12,74 @@ mode: "0755" tags: gitea-actions -- name: stage default CI Containerfile +- name: create gitea-runner registry auth directory + become: true + become_user: "{{ gitea_runner_user }}" + ansible.builtin.file: + path: "{{ gitea_ci_registry_authfile | dirname }}" + state: directory + mode: "0700" + tags: gitea-actions + +- name: log gitea-runner in to the Gitea container registry + become: true + become_user: "{{ gitea_runner_user }}" + containers.podman.podman_login: + registry: "{{ gitea_ci_registry }}" + username: "{{ gitea_registry_username }}" + password: "{{ gitea_registry_token }}" + authfile: "{{ gitea_ci_registry_authfile }}" + environment: + XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" + no_log: true + tags: gitea-actions + +- name: stage CI Containerfiles become: true become_user: "{{ gitea_runner_user }}" ansible.builtin.template: - src: Containerfile.ci - dest: "{{ gitea_runner_home }}/ci-images/Containerfile.ci" + src: "{{ item.template }}" + dest: "{{ gitea_runner_home }}/ci-images/{{ item.containerfile }}" mode: "0644" - register: ci_containerfile + loop: "{{ gitea_ci_images }}" + loop_control: + label: "{{ item.containerfile }}" + register: ci_containerfiles tags: gitea-actions -- name: stage ESP-IDF CI Containerfile - become: true - become_user: "{{ gitea_runner_user }}" - ansible.builtin.template: - src: Containerfile.espidf.j2 - dest: "{{ gitea_runner_home }}/ci-images/Containerfile.espidf" - mode: "0644" - register: espidf_containerfile - tags: gitea-actions - -- name: build default CI image ({{ gitea_ci_image }}) +# A missing image here is normal (first push, or a new tag): the build below +# creates it. Anything already present locally is left untouched. +- name: restore CI images from the registry become: true become_user: "{{ gitea_runner_user }}" containers.podman.podman_image: - name: "{{ gitea_ci_image }}" - path: "{{ gitea_runner_home }}/ci-images" - build: - file: "{{ gitea_runner_home }}/ci-images/Containerfile.ci" - force: "{{ ci_containerfile is changed }}" + name: "{{ item.image }}" + auth_file: "{{ gitea_ci_registry_authfile }}" environment: XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" + loop: "{{ gitea_ci_images }}" + loop_control: + label: "{{ item.image }}" + when: not (ci_containerfiles.results | selectattr('item.image', 'equalto', item.image) | first).changed + failed_when: false tags: gitea-actions -- name: stage PlatformIO CI Containerfile - become: true - become_user: "{{ gitea_runner_user }}" - ansible.builtin.template: - src: Containerfile.platformio.j2 - dest: "{{ gitea_runner_home }}/ci-images/Containerfile.platformio" - mode: "0644" - register: platformio_containerfile - tags: gitea-actions - -- name: build ESP-IDF CI image ({{ gitea_ci_espidf_image }}) +- name: build and push CI images become: true become_user: "{{ gitea_runner_user }}" containers.podman.podman_image: - name: "{{ gitea_ci_espidf_image }}" + name: "{{ item.image }}" path: "{{ gitea_runner_home }}/ci-images" build: - file: "{{ gitea_runner_home }}/ci-images/Containerfile.espidf" - force: "{{ espidf_containerfile is changed }}" - environment: - XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" - tags: gitea-actions - -- name: build PlatformIO CI image ({{ gitea_ci_platformio_image }}) - become: true - become_user: "{{ gitea_runner_user }}" - containers.podman.podman_image: - name: "{{ gitea_ci_platformio_image }}" - path: "{{ gitea_runner_home }}/ci-images" - build: - file: "{{ gitea_runner_home }}/ci-images/Containerfile.platformio" - force: "{{ platformio_containerfile is changed }}" + file: "{{ gitea_runner_home }}/ci-images/{{ item.containerfile }}" + # Exempts the image from the nightly CI prune (gitea_ci_keep_label). + extra_args: "--label {{ gitea_ci_keep_label }}=true" + force: "{{ (ci_containerfiles.results | selectattr('item.image', 'equalto', item.image) | first).changed }}" + push: true + auth_file: "{{ gitea_ci_registry_authfile }}" environment: XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" + loop: "{{ gitea_ci_images }}" + loop_control: + label: "{{ item.image }}" tags: gitea-actions diff --git a/ansible/roles/podman/defaults/main.yml b/ansible/roles/podman/defaults/main.yml index ba18998..7f962cc 100644 --- a/ansible/roles/podman/defaults/main.yml +++ b/ansible/roles/podman/defaults/main.yml @@ -311,6 +311,9 @@ podman_prune_ci_users: - gitea-runner - actions-runner podman_prune_ci_until: 48h +# CI base images built by roles/gitea-actions carry this label (gitea_ci_keep_label +# there -- keep the two in sync) and are skipped by the CI image prune. +podman_prune_ci_keep_label: io.debyl.ci-base # Daily rather than weekly: CI turns over many images a day, and a week of that # is what let the store reach 113 GB between runs. diff --git a/ansible/roles/podman/templates/podman-prune.sh.j2 b/ansible/roles/podman/templates/podman-prune.sh.j2 index 380b532..f59431f 100644 --- a/ansible/roles/podman/templates/podman-prune.sh.j2 +++ b/ansible/roles/podman/templates/podman-prune.sh.j2 @@ -43,9 +43,10 @@ run() { exec podman "$@"' _ "$@" } -# prune_user +# prune_user [image prune filter...] prune_user() { local u=$1 keep=$2 do_containers=$3 + shift 3 local before after img vol con if ! id "$u" >/dev/null 2>&1; then @@ -66,7 +67,7 @@ prune_user() { con=$(run "$u" container prune -f --filter "until=$keep" 2>&1 | tail -1) fi - img=$(run "$u" image prune -af --filter "until=$keep" 2>&1 | tail -1) + img=$(run "$u" image prune -af --filter "until=$keep" "$@" 2>&1 | tail -1) vol=$(run "$u" volume prune -f 2>&1 | tail -1) after=$(run "$u" system df --format '{{ '{{' }}.Size{{ '}}' }}' 2>/dev/null | head -1) @@ -80,7 +81,10 @@ for u in {{ podman_prune_users | join(' ') }}; do done for u in {{ podman_prune_ci_users | join(' ') }}; do - prune_user "$u" "{{ podman_prune_ci_until }}" yes + # CI base images (gitea-ci, -espidf, -platformio) carry the keep label: they + # are rebuilt or re-pulled from the registry only when missing, so pruning + # them just forces a multi-GB re-download on the next job. + prune_user "$u" "{{ podman_prune_ci_until }}" yes --filter "label!={{ podman_prune_ci_keep_label }}" done log "status=ok" From d0e76bd6cf7d5dc245d1393b111ca1a8164ca7b6 Mon Sep 17 00:00:00 2001 From: Bastian de Byl Date: Mon, 21 Sep 2026 11:09:53 -0400 Subject: [PATCH 02/10] feat(gitea-actions): build the CI job images in Gitea CI The runner's job images were built by ansible into localhost/ only, so the nightly CI prune deleted them and every idle stretch ended with CI failing in under a second on `docker pull localhost/gitea-ci:latest` until someone re-ran the role and waited out a rebuild. The previous commit moved them to the Gitea registry; this moves the *build* off the deploy path entirely. - .gitea/workflows/ci-images.yml builds files/Containerfile.* and pushes to git.debyl.io/gitbot/. Per-image change detection, so an ESP-IDF pin bump does not rebuild the other two; weekly schedule for base-image updates; a workflow_dispatch selector. PRs build under a throwaway :pr- tag and drop it -- the build lands in the live runner's store, and act_runner will not re-pull a tag it already has, so a PR using the real tag would hand every later job on this host an unmerged image. - The Containerfiles stop being ansible templates: their version vars are now --build-arg, read by the workflow out of the same defaults/main.yml the role interpolates, so CI and ansible build the same bytes from one set of pins. - LABEL io.debyl.ci-base moves into each Containerfile so neither builder can forget the prune exemption; the workflow re-checks it before pushing. - roles/gitea-actions pulls instead of building. gitea_ci_build_local=true restores the local build+push for seeding a cold registry or when CI is down -- the workflow that builds gitea-ci runs in gitea-ci. - Lint .gitea/ alongside ansible/, and document the flow in the role README. Co-Authored-By: Claude Opus 5 --- .gitea/workflows/ci-images.yml | 242 ++++++++++++++++++ CLAUDE.md | 13 + Makefile | 4 +- ansible/roles/gitea-actions/README.md | 91 +++++++ ansible/roles/gitea-actions/defaults/main.yml | 55 ++-- .../{templates => files}/Containerfile.ci | 10 + .../Containerfile.espidf} | 14 +- .../Containerfile.platformio} | 22 +- ansible/roles/gitea-actions/tasks/images.yml | 76 +++--- ansible/roles/podman/defaults/main.yml | 9 +- ansible/roles/podman/tasks/main.yml | 2 +- 11 files changed, 475 insertions(+), 63 deletions(-) create mode 100644 .gitea/workflows/ci-images.yml create mode 100644 ansible/roles/gitea-actions/README.md rename ansible/roles/gitea-actions/{templates => files}/Containerfile.ci (75%) rename ansible/roles/gitea-actions/{templates/Containerfile.espidf.j2 => files/Containerfile.espidf} (69%) rename ansible/roles/gitea-actions/{templates/Containerfile.platformio.j2 => files/Containerfile.platformio} (51%) diff --git a/.gitea/workflows/ci-images.yml b/.gitea/workflows/ci-images.yml new file mode 100644 index 0000000..4898516 --- /dev/null +++ b/.gitea/workflows/ci-images.yml @@ -0,0 +1,242 @@ +--- +# Builds the Gitea Actions job images and publishes them to the Gitea container +# registry, so the runner can re-pull one the nightly podman prune removed +# instead of waiting for a human to re-run `make deploy TAGS=gitea-actions`. +# +# Source of truth is ansible/roles/gitea-actions: files/Containerfile.* for the +# image contents, defaults/main.yml for the version pins and the registry path. +# This workflow reads those vars rather than repeating them. roles/gitea-actions +# then only pulls what lands here (gitea_ci_build_local is the escape hatch for +# seeding an empty namespace, since the job below runs *in* gitea-ci). +# +# `docker build` here talks to the gitea-runner user's rootless podman socket, +# mounted into every job container by roles/gitea-actions (config.yaml.j2), so +# the build happens in the same image store the runner pulls from and the layer +# cache survives between runs. That also means a build writes tags the live +# runner will use -- which is why pull requests build under a throwaway +# :pr- tag and delete it again. +name: CI Images + +on: + push: + branches: [master] + paths: + - ansible/roles/gitea-actions/files/Containerfile.* + - ansible/roles/gitea-actions/defaults/main.yml + - .gitea/workflows/ci-images.yml + pull_request: + branches: [master] + paths: + - ansible/roles/gitea-actions/files/Containerfile.* + - ansible/roles/gitea-actions/defaults/main.yml + - .gitea/workflows/ci-images.yml + workflow_dispatch: + inputs: + image: + description: Which image to rebuild + type: choice + options: [all, ci, espidf, platformio] + default: all + schedule: + # Weekly rebuild so base-image security updates land without a commit. + # Sunday 04:00, after the 02:00 podman prune has finished. + - cron: "0 4 * * 0" + +env: + DEFAULTS: ansible/roles/gitea-actions/defaults/main.yml + CONTEXT: ansible/roles/gitea-actions/files + REGISTRY: git.debyl.io + # Not a secret: the same namespace is in defaults/main.yml. It must be the + # owner of REGISTRY_TOKEN -- Gitea authorises a package push by the token's + # user, not by the path, so pushing to gitbot/ means logging in as gitbot. + REGISTRY_USER: gitbot + KEEP_LABEL: io.debyl.ci-base + +# One publisher at a time. Two runs pushing :latest concurrently would leave the +# registry holding whichever finished last, which need not be the newest commit. +concurrency: + group: ci-images + cancel-in-progress: false + +jobs: + plan: + name: Plan + runs-on: fedora + outputs: + matrix: ${{ steps.plan.outputs.matrix }} + any: ${{ steps.plan.outputs.any }} + steps: + - uses: actions/checkout@v4 + with: + # Full history so the change detection below can diff against the + # pushed-from commit / the PR base. + fetch-depth: 0 + + - name: Decide which images to build + id: plan + env: + EVENT: ${{ github.event_name }} + SELECTED: ${{ github.event.inputs.image }} + BEFORE: ${{ github.event.before }} + PR_BASE: ${{ github.event.pull_request.base.sha }} + run: | + set -euo pipefail + python3 - <<'PY' >> "$GITHUB_OUTPUT" + import json, os, subprocess, sys, yaml + + defaults = yaml.safe_load(open(os.environ["DEFAULTS"])) + ctx = os.environ["CONTEXT"] + reg, ns = os.environ["REGISTRY"], os.environ["REGISTRY_USER"] + + # Mirrors gitea_ci_images in defaults/main.yml. The tags are rebuilt + # from the same version vars the role interpolates, so a pin bump in + # that file moves the image tag here and in ansible together. + images = [ + { + "key": "ci", + "containerfile": "Containerfile.ci", + "tag": f"{reg}/{ns}/gitea-ci:latest", + "build_args": "", + }, + { + "key": "espidf", + "containerfile": "Containerfile.espidf", + "tag": f"{reg}/{ns}/gitea-ci-espidf:{defaults['esp_idf_version']}", + "build_args": f"ESP_IDF_VERSION={defaults['esp_idf_version']}", + }, + { + "key": "platformio", + "containerfile": "Containerfile.platformio", + "tag": f"{reg}/{ns}/gitea-ci-platformio:{defaults['pio_espressif32_version']}", + "build_args": ( + f"PLATFORMIO_CORE_VERSION={defaults['platformio_core_version']} " + f"PIO_ESPRESSIF32_VERSION={defaults['pio_espressif32_version']}" + ), + }, + ] + + event = os.environ["EVENT"] + + def changed_files(base): + """Paths touched since `base`, or None if the diff is not usable.""" + if not base or set(base) == {"0"}: + return None + try: + out = subprocess.run( + ["git", "diff", "--name-only", f"{base}...HEAD"], + capture_output=True, text=True, check=True, + ).stdout + except subprocess.CalledProcessError: + # Force push, shallow clone, first push of a branch: fall back + # to building everything rather than silently skipping a real + # change. + return None + return set(out.split()) + + if event == "workflow_dispatch": + selected = os.environ.get("SELECTED") or "all" + picked = images if selected == "all" else [i for i in images if i["key"] == selected] + elif event == "schedule": + picked = images + else: + base = os.environ["PR_BASE"] if event == "pull_request" else os.environ["BEFORE"] + touched = changed_files(base) + if touched is None: + picked = images + else: + # defaults/main.yml holds every pin, so a change there could + # retag any image; the workflow file itself changes how all of + # them are built. Either one rebuilds the lot. + wide = {os.environ["DEFAULTS"], ".gitea/workflows/ci-images.yml"} + if touched & wide: + picked = images + else: + picked = [i for i in images if f"{ctx}/{i['containerfile']}" in touched] + + print(f"matrix={json.dumps({'include': picked})}") + print(f"any={'true' if picked else 'false'}") + print("building: " + (", ".join(i["tag"] for i in picked) or "nothing"), file=sys.stderr) + PY + + build: + name: Build ${{ matrix.key }} + needs: plan + if: needs.plan.outputs.any == 'true' + runs-on: fedora + strategy: + # One image failing must not cancel the others: they are independent, and + # a half-published set is what this whole workflow exists to avoid. + fail-fast: false + matrix: ${{ fromJSON(needs.plan.outputs.matrix) }} + steps: + - uses: actions/checkout@v4 + + - name: Log in to the Gitea Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ env.REGISTRY_USER }} + password: ${{ secrets.REGISTRY_TOKEN }} + + # The build lands in the live runner's image store, and act_runner will + # not re-pull a tag it already has locally. Tagging a PR build with the + # real tag would therefore hand every later job on this host an unmerged + # image, so PRs get a throwaway tag that the cleanup step removes. + - name: Resolve build tag + id: tag + run: | + set -euo pipefail + if [ "${{ github.event_name }}" = "pull_request" ]; then + echo "image=${{ matrix.tag }}-pr${{ github.event.number }}" >> "$GITHUB_OUTPUT" + else + echo "image=${{ matrix.tag }}" >> "$GITHUB_OUTPUT" + fi + + - name: Build ${{ matrix.key }} + env: + IMAGE: ${{ steps.tag.outputs.image }} + BUILD_ARGS: ${{ matrix.build_args }} + run: | + set -euo pipefail + args=() + for a in $BUILD_ARGS; do args+=(--build-arg "$a"); done + # --pull so a scheduled run actually picks up a refreshed base image; + # without it an unchanged FROM line just hits the local layer cache. + docker build --pull \ + "${args[@]}" \ + -t "$IMAGE" \ + -f "$CONTEXT/${{ matrix.containerfile }}" \ + "$CONTEXT" + + - name: Verify the prune-exemption label survived the build + env: + IMAGE: ${{ steps.tag.outputs.image }} + run: | + set -euo pipefail + # roles/podman's nightly prune keeps an image only if it carries this + # label (podman_prune_ci_keep_label). Publishing one without it would + # quietly restore the nightly-deletion behaviour this replaced, and + # nothing would notice until CI failed on a Monday morning. + got=$(docker inspect -f "{{ index .Config.Labels \"$KEEP_LABEL\" }}" "$IMAGE") + test "$got" = "true" || { + echo "::error::$IMAGE is missing LABEL $KEEP_LABEL=true" + exit 1 + } + + - name: Push ${{ matrix.key }} + if: github.event_name != 'pull_request' + env: + IMAGE: ${{ steps.tag.outputs.image }} + run: | + set -euo pipefail + docker push "$IMAGE" + echo "Pushed: $IMAGE" + + # Always, including on failure: the throwaway tag carries the keep label, + # so the nightly prune will not reclaim it and a few skipped cleanups add + # up to gigabytes in the runner's store. + - name: Drop the pull-request image + if: always() && github.event_name == 'pull_request' + env: + IMAGE: ${{ steps.tag.outputs.image }} + run: docker rmi -f "$IMAGE" || true diff --git a/CLAUDE.md b/CLAUDE.md index 3745e8f..f32b1a7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -45,6 +45,7 @@ ansible/ │ ├── ssl/ # Legacy SSL management (deprecated - Caddy handles certificates automatically) │ ├── github-actions/# CI/CD runner setup │ ├── labelprint/ # 4x6 label print proxy (Raspberry Pi, CUPS/TSPL) +│ ├── gitea-actions/ # Gitea Actions runners + CI job images (see its README) │ └── pihole/ # DNS filtering └── vars/ └── vault.yml # Encrypted secrets @@ -90,6 +91,7 @@ Tasks are tagged by service/component for selective deployment: - `ddns` - Dynamic DNS tasks - ~~`drone` - CI/CD tasks (decommissioned)~~ - `hass` - Home Assistant tasks +- `gitea-actions` - Gitea Actions runners and their CI job images - Common infrastructure tags like `common`, `ssl` ## Configuration Files @@ -122,6 +124,17 @@ Tasks are tagged by service/component for selective deployment: - Falls back to its own rescue Wi-Fi AP at 192.168.4.1 when the home SSID is unreachable +### Gitea Actions CI images + +The runner's job images (`gitea-ci`, `gitea-ci-espidf`, `gitea-ci-platformio`) +are built by `.gitea/workflows/ci-images.yml` and published to the Gitea +container registry under `git.debyl.io/gitbot/`. The `gitea-actions` role only +pulls them - do NOT add build steps back to it. To change an image, edit +`ansible/roles/gitea-actions/files/Containerfile.*` (or a version pin in that +role's `defaults/main.yml`) and push to master; CI rebuilds only what changed. +See `ansible/roles/gitea-actions/README.md` for the registry rationale, the +prune-exemption label, and the bootstrap path when CI itself cannot build. + ### Remote SSH Commands for Service Users The `podman` user (and other service users) have `/bin/nologin` as their shell. To run commands as these users via SSH: diff --git a/Makefile b/Makefile index 63a478f..4c652c5 100644 --- a/Makefile +++ b/Makefile @@ -54,7 +54,9 @@ ${VAULT_FILE}: ${VAULT_PASS_FILE} touch $@ # Linting -YAML_FILES=$(shell find ansible/ -name '*.yml' -not -name '*vault*') +# .gitea/workflows is linted too: the CI-image workflow is as much part of the +# deployment as the roles it publishes for. +YAML_FILES=$(shell find ansible/ .gitea/ -name '*.yml' -not -name '*vault*') SKIP_FILE=./.lint-vars.sh # Targets diff --git a/ansible/roles/gitea-actions/README.md b/ansible/roles/gitea-actions/README.md new file mode 100644 index 0000000..906d1eb --- /dev/null +++ b/ansible/roles/gitea-actions/README.md @@ -0,0 +1,91 @@ +# gitea-actions + +Runs the Gitea Actions runners on `home.debyl.io`. One `act_runner` process per +Gitea instance (`git.debyl.io`, `git.skudak.com`), both as the `gitea-runner` +user, both backed by the same rootless podman image store. + +## CI job images + +Jobs do not run on the host. Each one gets an ephemeral container from one of +three images: + +| `runs-on` / `container:` | Image | Used by | +| --- | --- | --- | +| `fedora`, `ubuntu-latest`, `ubuntu-22.04` | `git.debyl.io/gitbot/gitea-ci:latest` | Go / node / web jobs, `docker build` | +| `container: image:` | `git.debyl.io/gitbot/gitea-ci-espidf:` | esp-mg-tpms, skudak/esp32-stm32-vcu | +| `container: image:` | `git.debyl.io/gitbot/gitea-ci-platformio:` | skudak/esp32-web-interface | + +**This role does not build them.** `.gitea/workflows/ci-images.yml` builds +`files/Containerfile.*` and pushes to the Gitea registry; the role logs +`gitea-runner` in and pulls. Version pins live in `defaults/main.yml` and are +read by both the role and the workflow, so a bump moves the image tag in one +place. + +### Why the registry + +The images used to exist only as `localhost/gitea-ci*` in the runner's store. +The nightly prune (`roles/podman`, `podman_prune_ci_until: 48h`) deletes any +CI-user image older than that which no container holds, so after an idle +weekend every job failed in under a second on `docker pull +localhost/gitea-ci:latest`, and the only fix was re-running this role and +waiting out a full rebuild. + +Two things now keep that from happening: + +- **A registry copy.** `force_pull` stays `false`, which in act_runner means + *pull only when missing* — so a present image is never re-fetched, and a + pruned one is restored by the next job without anyone noticing. +- **A prune exemption.** Each Containerfile declares + `LABEL io.debyl.ci-base="true"`, and the prune skips that label + (`podman_prune_ci_keep_label`). Its `until` counts from build time, not pull + time, so without this a re-pulled image would be deleted again the same night + — a 7.8 GB ESP-IDF download every single day. + +The label is declared in the Containerfile rather than passed as `--label` so +neither builder can omit it; the workflow re-checks it with `docker inspect` +before pushing. + +### Authentication + +Both the role and act_runner read `/home/gitea-runner/.docker/config.json`. +act_runner uses it for the job-image pull it performs when a label's image is +missing; podman falls back to the same file. The role writes it from +`gitea_registry_username` / `gitea_registry_token` (vault), so one login covers +both. The `skudak` runner pulls from `git.debyl.io` too — same host, same user, +same file. + +The workflow pushes with a `REGISTRY_TOKEN` secret on `bastian/deploy_home`, +belonging to the same `gitbot` user: Gitea authorises a package push by the +token's owner, not by the path, so pushing to `gitbot/` means logging in as +`gitbot`. + +### Rebuilding + +Normally nothing to do — edit a `files/Containerfile.*` or a version pin, push +to `master`, and the workflow rebuilds only the affected images. It also +rebuilds everything weekly so base-image updates land without a commit, and +takes a `workflow_dispatch` with an image selector. + +Pull requests build but do not push, under a throwaway `:pr-` tag that is +deleted afterwards. The build runs in the live runner's image store, so a PR +tagged with the real name would hand every later job on this host an unmerged +image. + +### Bootstrap / CI is down + +The workflow that builds `gitea-ci` runs *in* `gitea-ci`, so a registry that has +never held it cannot bootstrap itself. Build on the host instead: + +```sh +make deploy TAGS=gitea-actions EXTRA_VARS="gitea_ci_build_local=true" +``` + +That builds all three from the same Containerfiles and pushes them. One run is +enough even on a cold registry: `tasks/main.yml` imports `images.yml` before +`runner.yml`, so the images are published before the runner labels are flipped +to point at them. + +The alternative first-time path is to merge the workflow and dispatch it while +the deployed labels still say `localhost/` — the job then builds inside the old +local image and seeds the registry — then run a plain +`make deploy TAGS=gitea-actions` to switch the labels over. diff --git a/ansible/roles/gitea-actions/defaults/main.yml b/ansible/roles/gitea-actions/defaults/main.yml index 2ba8593..c958aab 100644 --- a/ansible/roles/gitea-actions/defaults/main.yml +++ b/ansible/roles/gitea-actions/defaults/main.yml @@ -22,17 +22,18 @@ act_runner_bin: /usr/local/bin/act_runner act_runner_config_dir: /etc/act_runner act_runner_work_dir: /var/lib/act_runner -# Job container images. tasks/images.yml builds them into the gitea-runner -# rootless store and pushes them to the Gitea container registry. +# Job container images, served from the Gitea container registry. # -# They used to live only under localhost/, and the nightly podman prune -# (roles/podman: podman_prune_ci_until) deletes any CI-user image older than -# 48h that no container is using -- so every idle weekend CI failed in 0-1s on -# `docker pull localhost/gitea-ci:latest` until someone re-ran this role and -# waited out a full rebuild. With a registry copy, a pruned image is simply -# re-pulled by the next job (force_pull stays false, so a present image is -# never re-pulled), and this role pulls instead of rebuilding when the -# Containerfile has not changed. +# They used to live only under localhost/, built by this role. The nightly +# podman prune (roles/podman: podman_prune_ci_until) deletes any CI-user image +# older than 48h that no container is using, so every idle weekend CI failed in +# 0-1s on `docker pull localhost/gitea-ci:latest` until someone re-ran the role +# and waited out a full rebuild. +# +# Now .gitea/workflows/ci-images.yml builds them from files/Containerfile.* and +# pushes them here, and this role only pulls. A pruned image is re-pulled by the +# next job on its own (force_pull stays false, which means "pull only when +# missing", so a present image is never re-fetched). # # Workflows that pin `container: image:` must use these registry paths too # (esp-mg-tpms, skudak/esp32-stm32-vcu, skudak/esp32-web-interface). @@ -55,26 +56,36 @@ gitea_ci_platformio_image: "{{ gitea_ci_registry }}/{{ gitea_ci_registry_namespa # fallback auth file), so one login covers the runner and this role. gitea_ci_registry_authfile: "{{ gitea_runner_home }}/.docker/config.json" -# Label stamped on the CI base images so the nightly prune skips them; must match -# podman_prune_ci_keep_label in roles/podman/defaults/main.yml. Without it the -# prune (whose `until` counts from build time, not pull time) would delete a -# re-pulled image again the next night -- a 7.8 GB ESP-IDF re-download after -# every idle day. Superseded tags (e.g. after an esp_idf_version bump) are -# therefore kept too; remove them by hand. -gitea_ci_keep_label: io.debyl.ci-base - +# The images this role keeps present on the runner. `build_args` is a literal +# podman-build argument string (podman_image has no structured build-arg +# option) and is only used by the gitea_ci_build_local fallback below -- the +# workflow passes the same --build-arg values, read out of the version vars +# above, so there is one source of truth for the pins. gitea_ci_images: - image: "{{ gitea_ci_image }}" containerfile: Containerfile.ci - template: Containerfile.ci + build_args: "" - image: "{{ gitea_ci_espidf_image }}" containerfile: Containerfile.espidf - template: Containerfile.espidf.j2 + build_args: "--build-arg ESP_IDF_VERSION={{ esp_idf_version }}" - image: "{{ gitea_ci_platformio_image }}" containerfile: Containerfile.platformio - template: Containerfile.platformio.j2 + build_args: >- + --build-arg PLATFORMIO_CORE_VERSION={{ platformio_core_version }} + --build-arg PIO_ESPRESSIF32_VERSION={{ pio_espressif32_version }} -# Default labels for every runner — map runs-on values to the local CI image. +# Escape hatch: build the images on the host and push them, instead of pulling +# what CI published. Needed to seed a brand-new registry namespace, and when CI +# itself is down -- the workflow that builds gitea-ci runs *in* gitea-ci, so a +# registry that has never held it cannot bootstrap itself. +# +# make deploy TAGS=gitea-actions EXTRA_VARS="gitea_ci_build_local=true" +# +# Off by default: a plain deploy should never sit through a 15-minute ESP-IDF +# rebuild, and two publishers racing on the same tag is worth avoiding. +gitea_ci_build_local: false + +# Default labels for every runner — map runs-on values to the registry CI image. # Firmware jobs opt into the ESP-IDF image per-job via `container:` in their workflow. gitea_runner_labels: - "fedora:docker://{{ gitea_ci_image }}" diff --git a/ansible/roles/gitea-actions/templates/Containerfile.ci b/ansible/roles/gitea-actions/files/Containerfile.ci similarity index 75% rename from ansible/roles/gitea-actions/templates/Containerfile.ci rename to ansible/roles/gitea-actions/files/Containerfile.ci index 12f4440..3e3bd38 100644 --- a/ansible/roles/gitea-actions/templates/Containerfile.ci +++ b/ansible/roles/gitea-actions/files/Containerfile.ci @@ -1,8 +1,18 @@ # Default Gitea Actions job image (managed by ansible: roles/gitea-actions). # Covers Go/web/node jobs plus `docker build` (talks to the mounted rootless # podman socket). Go toolchains are provided per-job by actions/setup-go. +# +# Built and published by .gitea/workflows/ci-images.yml; roles/gitea-actions +# only pulls the result (see gitea_ci_build_local for the local-build fallback). +# A plain Containerfile, not a template, so CI and ansible build the same bytes. FROM node:20-bookworm-slim +# Exempts the image from the nightly CI prune -- see podman_prune_ci_keep_label +# in roles/podman/defaults/main.yml. Declared here rather than passed as a +# --label at build time so neither builder can forget it: without the label the +# prune deletes the image every night and the next job re-pulls a gigabyte. +LABEL io.debyl.ci-base="true" + ARG DOCKER_CLI_VERSION=27.3.1 RUN apt-get update && apt-get install -y --no-install-recommends \ diff --git a/ansible/roles/gitea-actions/templates/Containerfile.espidf.j2 b/ansible/roles/gitea-actions/files/Containerfile.espidf similarity index 69% rename from ansible/roles/gitea-actions/templates/Containerfile.espidf.j2 rename to ansible/roles/gitea-actions/files/Containerfile.espidf index 6a64850..15fc8fd 100644 --- a/ansible/roles/gitea-actions/templates/Containerfile.espidf.j2 +++ b/ansible/roles/gitea-actions/files/Containerfile.espidf @@ -14,7 +14,19 @@ # the release aborts *after* the firmware and version.json are already live — # clients get the new build while the tag, Gitea release and protocol manifest # are never written. Keep it installed. -FROM espressif/idf:{{ esp_idf_version }} +# +# Built and published by .gitea/workflows/ci-images.yml; roles/gitea-actions +# only pulls the result. ESP_IDF_VERSION is a build arg rather than an ansible +# template var so CI and ansible build the same bytes -- its value is read from +# esp_idf_version in roles/gitea-actions/defaults/main.yml by both. +ARG ESP_IDF_VERSION +FROM espressif/idf:${ESP_IDF_VERSION} + +# Exempts the image from the nightly CI prune -- see podman_prune_ci_keep_label +# in roles/podman/defaults/main.yml. Declared here rather than passed as a +# --label at build time so neither builder can forget it: without the label the +# prune deletes the image every night and the next job re-pulls 7.8 GB. +LABEL io.debyl.ci-base="true" RUN apt-get update && apt-get install -y --no-install-recommends \ curl ca-certificates unzip jq python3-yaml python3-jinja2 \ diff --git a/ansible/roles/gitea-actions/templates/Containerfile.platformio.j2 b/ansible/roles/gitea-actions/files/Containerfile.platformio similarity index 51% rename from ansible/roles/gitea-actions/templates/Containerfile.platformio.j2 rename to ansible/roles/gitea-actions/files/Containerfile.platformio index da21e24..fb548a3 100644 --- a/ansible/roles/gitea-actions/templates/Containerfile.platformio.j2 +++ b/ansible/roles/gitea-actions/files/Containerfile.platformio @@ -8,8 +8,23 @@ # was validated on hardware — bump pio_espressif32_version / # platformio_core_version in defaults/main.yml to upgrade (the image tag # tracks the platform version). +# +# Built and published by .gitea/workflows/ci-images.yml; roles/gitea-actions +# only pulls the result. The pins are build args rather than ansible template +# vars so CI and ansible build the same bytes -- their values are read from +# platformio_core_version / pio_espressif32_version in +# roles/gitea-actions/defaults/main.yml by both. FROM python:3.12-slim-bookworm +ARG PLATFORMIO_CORE_VERSION +ARG PIO_ESPRESSIF32_VERSION + +# Exempts the image from the nightly CI prune -- see podman_prune_ci_keep_label +# in roles/podman/defaults/main.yml. Declared here rather than passed as a +# --label at build time so neither builder can forget it: without the label the +# prune deletes the image every night and the next job re-pulls a gigabyte. +LABEL io.debyl.ci-base="true" + ENV PLATFORMIO_CORE_DIR=/opt/platformio RUN apt-get update && apt-get install -y --no-install-recommends \ @@ -18,12 +33,15 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ && apt-get install -y --no-install-recommends nodejs \ && rm -rf /var/lib/apt/lists/* -RUN pip install --no-cache-dir platformio=={{ platformio_core_version }} +RUN pip install --no-cache-dir platformio==${PLATFORMIO_CORE_VERSION} # Seed project mirroring the real projects' platformio.ini so `pio pkg install` # pulls the platform + toolchain + framework packages into the core dir. +# %s + a quoted argument, not ${...} inside the single-quoted format string: +# RUN is `sh -c`, and sh does not expand inside single quotes, so an inlined +# ${PIO_ESPRESSIF32_VERSION} would be written to platformio.ini literally. RUN mkdir -p /tmp/seed/src \ - && printf '[env:seed]\nplatform = espressif32@{{ pio_espressif32_version }}\nframework = arduino\nboard = esp32dev\nboard_build.filesystem = spiffs\nplatform_packages = platformio/tool-esptoolpy\n' > /tmp/seed/platformio.ini \ + && printf '[env:seed]\nplatform = espressif32@%s\nframework = arduino\nboard = esp32dev\nboard_build.filesystem = spiffs\nplatform_packages = platformio/tool-esptoolpy\n' "${PIO_ESPRESSIF32_VERSION}" > /tmp/seed/platformio.ini \ && pio pkg install -d /tmp/seed \ && pio pkg install -d /tmp/seed --tool platformio/tool-mkspiffs \ && rm -rf /tmp/seed \ diff --git a/ansible/roles/gitea-actions/tasks/images.yml b/ansible/roles/gitea-actions/tasks/images.yml index 8c41e34..9f1f484 100644 --- a/ansible/roles/gitea-actions/tasks/images.yml +++ b/ansible/roles/gitea-actions/tasks/images.yml @@ -1,17 +1,10 @@ --- -# CI job images: stage each Containerfile, restore the image from the Gitea -# registry if the nightly prune removed it, rebuild only when the Containerfile -# changed, then push so the registry always holds what the runner uses. -# See gitea_ci_images in defaults/main.yml. -- name: create CI image build directory - become: true - become_user: "{{ gitea_runner_user }}" - ansible.builtin.file: - path: "{{ gitea_runner_home }}/ci-images" - state: directory - mode: "0755" - tags: gitea-actions - +# CI job images. .gitea/workflows/ci-images.yml builds files/Containerfile.* +# and pushes them to the Gitea registry; this role only logs the runner in and +# makes sure the images are present, so a plain deploy never waits on a build. +# +# Set gitea_ci_build_local=true to build and push from here instead -- see the +# comment on that variable in defaults/main.yml. - name: create gitea-runner registry auth directory become: true become_user: "{{ gitea_runner_user }}" @@ -21,6 +14,9 @@ mode: "0700" tags: gitea-actions +# Docker-format path on purpose: act_runner reads ~/.docker/config.json to +# authenticate the job-image pull it does when a label's image is missing, and +# podman falls back to the same file. One login covers both. - name: log gitea-runner in to the Gitea container registry become: true become_user: "{{ gitea_runner_user }}" @@ -34,22 +30,7 @@ no_log: true tags: gitea-actions -- name: stage CI Containerfiles - become: true - become_user: "{{ gitea_runner_user }}" - ansible.builtin.template: - src: "{{ item.template }}" - dest: "{{ gitea_runner_home }}/ci-images/{{ item.containerfile }}" - mode: "0644" - loop: "{{ gitea_ci_images }}" - loop_control: - label: "{{ item.containerfile }}" - register: ci_containerfiles - tags: gitea-actions - -# A missing image here is normal (first push, or a new tag): the build below -# creates it. Anything already present locally is left untouched. -- name: restore CI images from the registry +- name: pull CI images from the registry become: true become_user: "{{ gitea_runner_user }}" containers.podman.podman_image: @@ -60,8 +41,35 @@ loop: "{{ gitea_ci_images }}" loop_control: label: "{{ item.image }}" - when: not (ci_containerfiles.results | selectattr('item.image', 'equalto', item.image) | first).changed - failed_when: false + when: not (gitea_ci_build_local | bool) + tags: gitea-actions + +# --- local build fallback (gitea_ci_build_local=true) ------------------------ +# Only reached when seeding a new namespace or when CI cannot build for us. +- name: create CI image build directory + become: true + become_user: "{{ gitea_runner_user }}" + ansible.builtin.file: + path: "{{ gitea_runner_home }}/ci-images" + state: directory + mode: "0755" + when: gitea_ci_build_local | bool + tags: gitea-actions + +# copy, not template: these are plain Containerfiles that CI builds verbatim. +# Versions come in as --build-arg from the same defaults/main.yml the workflow +# reads, so neither builder can drift from the other. +- name: stage CI Containerfiles + become: true + become_user: "{{ gitea_runner_user }}" + ansible.builtin.copy: + src: "{{ item.containerfile }}" + dest: "{{ gitea_runner_home }}/ci-images/{{ item.containerfile }}" + mode: "0644" + loop: "{{ gitea_ci_images }}" + loop_control: + label: "{{ item.containerfile }}" + when: gitea_ci_build_local | bool tags: gitea-actions - name: build and push CI images @@ -72,9 +80,8 @@ path: "{{ gitea_runner_home }}/ci-images" build: file: "{{ gitea_runner_home }}/ci-images/{{ item.containerfile }}" - # Exempts the image from the nightly CI prune (gitea_ci_keep_label). - extra_args: "--label {{ gitea_ci_keep_label }}=true" - force: "{{ (ci_containerfiles.results | selectattr('item.image', 'equalto', item.image) | first).changed }}" + extra_args: "{{ item.build_args }}" + force: true push: true auth_file: "{{ gitea_ci_registry_authfile }}" environment: @@ -82,4 +89,5 @@ loop: "{{ gitea_ci_images }}" loop_control: label: "{{ item.image }}" + when: gitea_ci_build_local | bool tags: gitea-actions diff --git a/ansible/roles/podman/defaults/main.yml b/ansible/roles/podman/defaults/main.yml index 1667d0c..3d2f7cb 100644 --- a/ansible/roles/podman/defaults/main.yml +++ b/ansible/roles/podman/defaults/main.yml @@ -311,8 +311,13 @@ podman_prune_ci_users: - gitea-runner - actions-runner podman_prune_ci_until: 48h -# CI base images built by roles/gitea-actions carry this label (gitea_ci_keep_label -# there -- keep the two in sync) and are skipped by the CI image prune. +# The CI base images declare LABEL io.debyl.ci-base="true" in +# roles/gitea-actions/files/Containerfile.* (and .gitea/workflows/ci-images.yml +# verifies it before publishing -- keep all three in sync). Images carrying it +# are skipped below: `until` counts from build time and not pull time, so +# without the exemption a re-pulled image would be deleted again the next +# night, a 7.8 GB ESP-IDF re-download after every idle day. Superseded tags +# (e.g. after an esp_idf_version bump) survive too; remove those by hand. podman_prune_ci_keep_label: io.debyl.ci-base # Daily rather than weekly: CI turns over many images a day, and a week of that diff --git a/ansible/roles/podman/tasks/main.yml b/ansible/roles/podman/tasks/main.yml index 6269ecd..e6f70d1 100644 --- a/ansible/roles/podman/tasks/main.yml +++ b/ansible/roles/podman/tasks/main.yml @@ -123,7 +123,7 @@ - import_tasks: containers/home/gregtime.yml vars: - image: localhost/greg-time-bot:3.18.1 + image: localhost/greg-time-bot:3.18.3 tags: gregtime # Built and loaded by `make deploy-remote` in ~/src/rsvp-debylio; bump this to From ba0936bc8d4cefac9527aee083792d92d3f78dae Mon Sep 17 00:00:00 2001 From: Bastian de Byl Date: Tue, 22 Sep 2026 10:30:07 -0400 Subject: [PATCH 03/10] chore(gregtime): bump to 3.18.4 The daily quote keeps a ledger of what it has posted and re-rolls ZenQuotes until it finds something the channel has not read, with the header framing and the offline fallback pool drawn against that same ledger. Co-Authored-By: Claude Opus 5 --- ansible/roles/podman/tasks/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/roles/podman/tasks/main.yml b/ansible/roles/podman/tasks/main.yml index e6f70d1..28a0c9a 100644 --- a/ansible/roles/podman/tasks/main.yml +++ b/ansible/roles/podman/tasks/main.yml @@ -123,7 +123,7 @@ - import_tasks: containers/home/gregtime.yml vars: - image: localhost/greg-time-bot:3.18.3 + image: localhost/greg-time-bot:3.18.4 tags: gregtime # Built and loaded by `make deploy-remote` in ~/src/rsvp-debylio; bump this to From 64850995ecda9f2fdefad913ac6209f667427af8 Mon Sep 17 00:00:00 2001 From: Bastian de Byl Date: Tue, 22 Sep 2026 11:06:31 -0400 Subject: [PATCH 04/10] chore(gitea): bump git.debyl.io to 1.27.3, pin the two instances separately 1.26.1 -> 1.27.3 picks up the security fixes in 1.27.0 through 1.27.3. The image was one shared variable, so the two instances could only move together; split it into gitea_debyl_image / gitea_skudak_image and tag the debyl tasks gitea-debyl so each can be upgraded and verified on its own. Skudak stays on 1.26.1 in this commit. Co-Authored-By: Claude Fable 5.1 --- ansible/roles/git/defaults/main.yml | 4 +++- ansible/roles/git/tasks/gitea-skudak.yml | 2 +- ansible/roles/git/tasks/gitea.yml | 12 ++++++------ 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/ansible/roles/git/defaults/main.yml b/ansible/roles/git/defaults/main.yml index ac7f150..daa005d 100644 --- a/ansible/roles/git/defaults/main.yml +++ b/ansible/roles/git/defaults/main.yml @@ -4,9 +4,11 @@ git_home: "/srv/{{ git_user }}" # Gitea configuration gitea_debyl_server_name: git.debyl.io -gitea_image: docker.gitea.com/gitea:1.26.1 +# Pinned per instance so one can be upgraded (and verified) before the other. +gitea_debyl_image: docker.gitea.com/gitea:1.27.3 gitea_db_image: docker.io/library/postgres:14-alpine # Skudak Gitea configuration gitea_skudak_server_name: git.skudak.com gitea_skudak_ssh_port: 2222 +gitea_skudak_image: docker.gitea.com/gitea:1.26.1 diff --git a/ansible/roles/git/tasks/gitea-skudak.yml b/ansible/roles/git/tasks/gitea-skudak.yml index ce4b174..827f6c9 100644 --- a/ansible/roles/git/tasks/gitea-skudak.yml +++ b/ansible/roles/git/tasks/gitea-skudak.yml @@ -43,7 +43,7 @@ become_user: "{{ git_user }}" containers.podman.podman_container: name: gitea-skudak - image: "{{ gitea_image }}" + image: "{{ gitea_skudak_image }}" pod: gitea-skudak-pod restart_policy: on-failure:3 log_driver: journald diff --git a/ansible/roles/git/tasks/gitea.yml b/ansible/roles/git/tasks/gitea.yml index b0cc991..094d80c 100644 --- a/ansible/roles/git/tasks/gitea.yml +++ b/ansible/roles/git/tasks/gitea.yml @@ -10,7 +10,7 @@ state: started ports: - "3100:3000" - tags: gitea + tags: gitea, gitea-debyl # PostgreSQL container in pod - name: create gitea-debyl-postgres container @@ -28,7 +28,7 @@ POSTGRES_PASSWORD: "{{ gitea_debyl_db_pass }}" volumes: - "{{ git_home }}/volumes/gitea/psql:/var/lib/postgresql/data" - tags: gitea + tags: gitea, gitea-debyl # Gitea container in pod - name: create gitea-debyl container @@ -36,7 +36,7 @@ become_user: "{{ git_user }}" containers.podman.podman_container: name: gitea-debyl - image: "{{ gitea_image }}" + image: "{{ gitea_debyl_image }}" pod: gitea-debyl-pod restart_policy: on-failure:3 log_driver: journald @@ -66,7 +66,7 @@ volumes: - "{{ git_home }}/volumes/gitea/data:/data" - /etc/localtime:/etc/localtime:ro - tags: gitea + tags: gitea, gitea-debyl # Generate systemd service for the pod - name: create systemd job for gitea-debyl-pod @@ -80,7 +80,7 @@ args: chdir: "{{ git_home }}" changed_when: false - tags: gitea + tags: gitea, gitea-debyl - name: enable gitea-debyl-pod service become: true @@ -91,4 +91,4 @@ enabled: true state: started scope: user - tags: gitea + tags: gitea, gitea-debyl From 15a8ec693e6f9d3d865d23f629291bfd9a82d9cd Mon Sep 17 00:00:00 2001 From: Bastian de Byl Date: Tue, 22 Sep 2026 11:10:07 -0400 Subject: [PATCH 05/10] chore(gitea): bump git.skudak.com to 1.27.3 Same security fixes as git.debyl.io, deployed and verified after it. Co-Authored-By: Claude Fable 5.1 --- ansible/roles/git/defaults/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/roles/git/defaults/main.yml b/ansible/roles/git/defaults/main.yml index daa005d..3ad16a8 100644 --- a/ansible/roles/git/defaults/main.yml +++ b/ansible/roles/git/defaults/main.yml @@ -11,4 +11,4 @@ gitea_db_image: docker.io/library/postgres:14-alpine # Skudak Gitea configuration gitea_skudak_server_name: git.skudak.com gitea_skudak_ssh_port: 2222 -gitea_skudak_image: docker.gitea.com/gitea:1.26.1 +gitea_skudak_image: docker.gitea.com/gitea:1.27.3 From fd985e014c3fe1f35adb262c2e44374bc8aeb996 Mon Sep 17 00:00:00 2001 From: Bastian de Byl Date: Fri, 25 Sep 2026 12:42:33 -0400 Subject: [PATCH 06/10] fix(hass): driveway lights ignore TV mode, ramped evening dimming, bump to 2026.9.3 The sunset automation required TV mode off, so an afternoon of TV skipped the driveway string lights entirely (Sep 22 and 23) - not an outage. The driveway now has its own sunset -1h automation, with catch-up on restart or switch reconnect before 23:00. Evening brightness lives in one script (evening_lights_apply) that blends between the old step levels; a 5-minute ramp from 20:30 eases lights that are on and leaves alone any a person has changed by hand. The Dining Hall no longer bumps to 50% at 21:30. TV off after 23:30 now only turns off the living room glow instead of bringing the whole house back to full brightness, and TV on/off leave the lights alone in daylight. Lights-out moves to 23:30, and a 01:00 sweep catches anything switched back on at the wall. Co-Authored-By: Claude Opus 5.5 --- .../roles/podman/files/hass/automations.yaml | 554 +++++------------- .../podman/files/hass/configuration.yaml | 1 + ansible/roles/podman/files/hass/scripts.yaml | 76 +++ .../podman/tasks/containers/home/hass.yml | 1 + ansible/roles/podman/tasks/main.yml | 2 +- 5 files changed, 237 insertions(+), 397 deletions(-) create mode 100644 ansible/roles/podman/files/hass/scripts.yaml diff --git a/ansible/roles/podman/files/hass/automations.yaml b/ansible/roles/podman/files/hass/automations.yaml index fabd5c6..13969e0 100644 --- a/ansible/roles/podman/files/hass/automations.yaml +++ b/ansible/roles/podman/files/hass/automations.yaml @@ -192,7 +192,8 @@ mode: single - id: '1762116115638' alias: Light - TV On - description: '' + description: TV mode on; once it's dark, dim the living room and turn off the + lights that glare on the TV triggers: - type: turned_on device_id: 18a9bb7a2a32be4371da447767ef50a9 @@ -204,28 +205,32 @@ - action: input_boolean.turn_on target: entity_id: input_boolean.tv_mode - - action: light.turn_on - metadata: {} - data: - brightness_pct: 5 - target: - area_id: living_room - - type: turn_off - device_id: 03a12d2360d9954aed19c2449070725a - entity_id: 7c1e7db73799cc3f90948b5118596985 - domain: light - - type: turn_off - device_id: 800eddbeeda071225f181a14cb9527e0 - entity_id: 521a92ddd8be76c7eddfc544f81f6020 - domain: light - - type: turn_off - device_id: 3f7f65571d9bb0833433996f1f6725bd - entity_id: 7407afe14783543252c666d5ff7c5d5c - domain: light + - if: + - condition: or + conditions: + - condition: sun + after: sunset + after_offset: "-01:00:00" + - condition: sun + before: sunrise + then: + - action: light.turn_on + data: + brightness_pct: 5 + target: + area_id: living_room + - action: light.turn_off + target: + entity_id: + - light.kitchen_wall_light + - light.dining_hall + - light.bathroom_hallway mode: single - id: new_tv_off alias: Light - TV Off - Restore - description: Restores appropriate lighting level when TV turns off based on time + description: Evening (sunset -1h to 23:30) brings the lights back to the + scheduled level; late night (23:30 to sunrise) only turns off the TV glow; + daytime leaves the lights alone triggers: - type: turned_off device_id: 18a9bb7a2a32be4371da447767ef50a9 @@ -239,399 +244,156 @@ entity_id: input_boolean.tv_mode - choose: - conditions: + - condition: sun + after: sunset + after_offset: "-01:00:00" - condition: time - after: '22:30:00' - before: '23:45:00' + before: '23:30:00' sequence: - # Late dim levels - - type: turn_on - device_id: f31e4f9bf8fa3687a07aeb4430eaef38 - entity_id: b79934d97f3bb9d8a3da47c76d03ded4 - domain: light - brightness_pct: 1 - - type: turn_on - device_id: 03a12d2360d9954aed19c2449070725a - entity_id: 7c1e7db73799cc3f90948b5118596985 - domain: light - brightness_pct: 1 - - type: turn_on - device_id: 800eddbeeda071225f181a14cb9527e0 - entity_id: 521a92ddd8be76c7eddfc544f81f6020 - domain: light - brightness_pct: 25 - - type: turn_on - device_id: 3f7f65571d9bb0833433996f1f6725bd - entity_id: 7407afe14783543252c666d5ff7c5d5c - domain: light - brightness_pct: 10 - - type: turn_on - device_id: 21eb2bd28aba2ee361a22af92e8b2d16 - entity_id: 81c486d682afcc94e98e377475cc92fc - domain: light - brightness_pct: 10 + - action: script.evening_lights_apply + data: + apply: force - conditions: - - condition: time - after: '21:30:00' - before: '22:30:00' + - condition: or + conditions: + - condition: time + after: '23:30:00' + - condition: sun + before: sunrise sequence: - # Mid dim levels - - type: turn_on - device_id: f31e4f9bf8fa3687a07aeb4430eaef38 - entity_id: b79934d97f3bb9d8a3da47c76d03ded4 - domain: light - brightness_pct: 25 - - type: turn_on - device_id: 03a12d2360d9954aed19c2449070725a - entity_id: 7c1e7db73799cc3f90948b5118596985 - domain: light - brightness_pct: 25 - - type: turn_on - device_id: 800eddbeeda071225f181a14cb9527e0 - entity_id: 521a92ddd8be76c7eddfc544f81f6020 - domain: light - brightness_pct: 50 - - type: turn_on - device_id: 3f7f65571d9bb0833433996f1f6725bd - entity_id: 7407afe14783543252c666d5ff7c5d5c - domain: light - brightness_pct: 25 - - type: turn_on - device_id: 21eb2bd28aba2ee361a22af92e8b2d16 - entity_id: 81c486d682afcc94e98e377475cc92fc - domain: light - brightness_pct: 25 - - conditions: - - condition: time - after: '21:00:00' - before: '21:30:00' - sequence: - # Early dim levels - - type: turn_on - device_id: 03a12d2360d9954aed19c2449070725a - entity_id: 7c1e7db73799cc3f90948b5118596985 - domain: light - brightness_pct: 50 - - type: turn_on - device_id: f31e4f9bf8fa3687a07aeb4430eaef38 - entity_id: b79934d97f3bb9d8a3da47c76d03ded4 - domain: light - brightness_pct: 50 - - type: turn_on - device_id: 3f7f65571d9bb0833433996f1f6725bd - entity_id: 7407afe14783543252c666d5ff7c5d5c - domain: light - brightness_pct: 50 - - type: turn_on - device_id: 21eb2bd28aba2ee361a22af92e8b2d16 - entity_id: 81c486d682afcc94e98e377475cc92fc - domain: light - brightness_pct: 50 - default: - # Full brightness (before 21:00 after sunset) - - type: turn_on - device_id: 800eddbeeda071225f181a14cb9527e0 - entity_id: 521a92ddd8be76c7eddfc544f81f6020 - domain: light - brightness_pct: 25 - - type: turn_on - device_id: f31e4f9bf8fa3687a07aeb4430eaef38 - entity_id: b79934d97f3bb9d8a3da47c76d03ded4 - domain: light - brightness_pct: 100 - - type: turn_on - device_id: 03a12d2360d9954aed19c2449070725a - entity_id: 7c1e7db73799cc3f90948b5118596985 - domain: light - brightness_pct: 100 - - type: turn_on - device_id: 3f7f65571d9bb0833433996f1f6725bd - entity_id: 7407afe14783543252c666d5ff7c5d5c - domain: light - brightness_pct: 75 - - type: turn_on - device_id: 21eb2bd28aba2ee361a22af92e8b2d16 - entity_id: 81c486d682afcc94e98e377475cc92fc - domain: light - brightness_pct: 100 + - action: light.turn_off + target: + area_id: living_room mode: single -- id: 'sunset_lights_on' - alias: Lights - Sunset On - description: Turn on lights 1 hour before sunset +- id: driveway_lights_on + alias: Driveway String Lights On + description: On 1 hour before sunset whether or not the TV is on. Also catches + up if Home Assistant restarts or the switch reconnects before the 23:00 off triggers: - trigger: sun event: sunset offset: "-01:00:00" + id: sunset + - trigger: homeassistant + event: start + - trigger: state + entity_id: switch.driveway_string_lights + from: unavailable + to: 'off' conditions: - condition: state - entity_id: input_boolean.tv_mode + entity_id: switch.driveway_string_lights state: 'off' + - condition: or + conditions: + - condition: trigger + id: sunset + - condition: and + conditions: + - condition: sun + after: sunset + after_offset: "-01:00:00" + - condition: time + before: '23:00:00' actions: - - type: turn_on - device_id: 1fa1aca8f90daf94a2a7baf8a3abc158 - entity_id: 58d101e63456fd8e088d3a3b63f3a0f9 - domain: switch - - type: turn_on - device_id: 800eddbeeda071225f181a14cb9527e0 - entity_id: 521a92ddd8be76c7eddfc544f81f6020 - domain: light - brightness_pct: 25 - - type: turn_on - device_id: f31e4f9bf8fa3687a07aeb4430eaef38 - entity_id: b79934d97f3bb9d8a3da47c76d03ded4 - domain: light - brightness_pct: 100 - - type: turn_on - device_id: 03a12d2360d9954aed19c2449070725a - entity_id: 7c1e7db73799cc3f90948b5118596985 - domain: light - brightness_pct: 100 - - type: turn_on - device_id: 3f7f65571d9bb0833433996f1f6725bd - entity_id: 7407afe14783543252c666d5ff7c5d5c - domain: light - brightness_pct: 75 - - type: turn_on - device_id: 21eb2bd28aba2ee361a22af92e8b2d16 - entity_id: 81c486d682afcc94e98e377475cc92fc - domain: light - brightness_pct: 100 + - action: switch.turn_on + target: + entity_id: switch.driveway_string_lights mode: single -- id: 'evening_dim_2100' - alias: Lights - Evening Dim (21:00) - description: Dim lights at 21:00 - only affects lights that are ON +- id: 'sunset_lights_on' + alias: Lights - Sunset On + description: Turn on lights 1 hour before sunset. If the TV is on, the living + room gets the TV glow and the lights that glare on the TV stay off triggers: - - trigger: time - at: "21:00:00" - conditions: - - condition: state - entity_id: input_boolean.tv_mode - state: 'off' - actions: - - if: - - condition: device - device_id: 03a12d2360d9954aed19c2449070725a - domain: light - entity_id: 7c1e7db73799cc3f90948b5118596985 - type: is_on - then: - - type: turn_on - device_id: 03a12d2360d9954aed19c2449070725a - entity_id: 7c1e7db73799cc3f90948b5118596985 - domain: light - brightness_pct: 50 - - if: - - condition: device - device_id: f31e4f9bf8fa3687a07aeb4430eaef38 - domain: light - entity_id: b79934d97f3bb9d8a3da47c76d03ded4 - type: is_on - then: - - type: turn_on - device_id: f31e4f9bf8fa3687a07aeb4430eaef38 - entity_id: b79934d97f3bb9d8a3da47c76d03ded4 - domain: light - brightness_pct: 50 - - if: - - condition: device - device_id: 3f7f65571d9bb0833433996f1f6725bd - domain: light - entity_id: 7407afe14783543252c666d5ff7c5d5c - type: is_on - then: - - type: turn_on - device_id: 3f7f65571d9bb0833433996f1f6725bd - entity_id: 7407afe14783543252c666d5ff7c5d5c - domain: light - brightness_pct: 50 - - if: - - condition: device - device_id: 21eb2bd28aba2ee361a22af92e8b2d16 - domain: light - entity_id: 81c486d682afcc94e98e377475cc92fc - type: is_on - then: - - type: turn_on - device_id: 21eb2bd28aba2ee361a22af92e8b2d16 - entity_id: 81c486d682afcc94e98e377475cc92fc - domain: light - brightness_pct: 50 - mode: single -- id: 'mid_dim_2130' - alias: Lights - Mid Dim (21:30) - description: Dim lights at 21:30 - only affects lights that are ON - triggers: - - trigger: time - at: "21:30:00" - conditions: - - condition: state - entity_id: input_boolean.tv_mode - state: 'off' - actions: - - if: - - condition: device - device_id: f31e4f9bf8fa3687a07aeb4430eaef38 - domain: light - entity_id: b79934d97f3bb9d8a3da47c76d03ded4 - type: is_on - then: - - type: turn_on - device_id: f31e4f9bf8fa3687a07aeb4430eaef38 - entity_id: b79934d97f3bb9d8a3da47c76d03ded4 - domain: light - brightness_pct: 25 - - if: - - condition: device - device_id: 03a12d2360d9954aed19c2449070725a - domain: light - entity_id: 7c1e7db73799cc3f90948b5118596985 - type: is_on - then: - - type: turn_on - device_id: 03a12d2360d9954aed19c2449070725a - entity_id: 7c1e7db73799cc3f90948b5118596985 - domain: light - brightness_pct: 25 - - if: - - condition: device - device_id: 800eddbeeda071225f181a14cb9527e0 - domain: light - entity_id: 521a92ddd8be76c7eddfc544f81f6020 - type: is_on - then: - - type: turn_on - device_id: 800eddbeeda071225f181a14cb9527e0 - entity_id: 521a92ddd8be76c7eddfc544f81f6020 - domain: light - brightness_pct: 50 - - if: - - condition: device - device_id: 3f7f65571d9bb0833433996f1f6725bd - domain: light - entity_id: 7407afe14783543252c666d5ff7c5d5c - type: is_on - then: - - type: turn_on - device_id: 3f7f65571d9bb0833433996f1f6725bd - entity_id: 7407afe14783543252c666d5ff7c5d5c - domain: light - brightness_pct: 25 - - if: - - condition: device - device_id: 21eb2bd28aba2ee361a22af92e8b2d16 - domain: light - entity_id: 81c486d682afcc94e98e377475cc92fc - type: is_on - then: - - type: turn_on - device_id: 21eb2bd28aba2ee361a22af92e8b2d16 - entity_id: 81c486d682afcc94e98e377475cc92fc - domain: light - brightness_pct: 25 - mode: single -- id: 'late_dim_2230' - alias: Lights - Late Dim (22:30) - description: Dim lights at 22:30 - only affects lights that are ON - triggers: - - trigger: time - at: "22:30:00" - conditions: - - condition: state - entity_id: input_boolean.tv_mode - state: 'off' - actions: - - if: - - condition: device - device_id: f31e4f9bf8fa3687a07aeb4430eaef38 - domain: light - entity_id: b79934d97f3bb9d8a3da47c76d03ded4 - type: is_on - then: - - type: turn_on - device_id: f31e4f9bf8fa3687a07aeb4430eaef38 - entity_id: b79934d97f3bb9d8a3da47c76d03ded4 - domain: light - brightness_pct: 1 - - if: - - condition: device - device_id: 03a12d2360d9954aed19c2449070725a - domain: light - entity_id: 7c1e7db73799cc3f90948b5118596985 - type: is_on - then: - - type: turn_on - device_id: 03a12d2360d9954aed19c2449070725a - entity_id: 7c1e7db73799cc3f90948b5118596985 - domain: light - brightness_pct: 1 - - if: - - condition: device - device_id: 800eddbeeda071225f181a14cb9527e0 - domain: light - entity_id: 521a92ddd8be76c7eddfc544f81f6020 - type: is_on - then: - - type: turn_on - device_id: 800eddbeeda071225f181a14cb9527e0 - entity_id: 521a92ddd8be76c7eddfc544f81f6020 - domain: light - brightness_pct: 25 - - if: - - condition: device - device_id: 3f7f65571d9bb0833433996f1f6725bd - domain: light - entity_id: 7407afe14783543252c666d5ff7c5d5c - type: is_on - then: - - type: turn_on - device_id: 3f7f65571d9bb0833433996f1f6725bd - entity_id: 7407afe14783543252c666d5ff7c5d5c - domain: light - brightness_pct: 10 - - if: - - condition: device - device_id: 21eb2bd28aba2ee361a22af92e8b2d16 - domain: light - entity_id: 81c486d682afcc94e98e377475cc92fc - type: is_on - then: - - type: turn_on - device_id: 21eb2bd28aba2ee361a22af92e8b2d16 - entity_id: 81c486d682afcc94e98e377475cc92fc - domain: light - brightness_pct: 10 - mode: single -- id: 'lights_out_2345' - alias: Lights - Out (23:45) - description: Turn off all lights at 23:45 - triggers: - - trigger: time - at: "23:45:00" + - trigger: sun + event: sunset + offset: "-01:00:00" conditions: [] actions: - - type: turn_off - device_id: 3f7f65571d9bb0833433996f1f6725bd - entity_id: 7407afe14783543252c666d5ff7c5d5c - domain: light - - type: turn_off - device_id: f31e4f9bf8fa3687a07aeb4430eaef38 - entity_id: b79934d97f3bb9d8a3da47c76d03ded4 - domain: light - - type: turn_off - device_id: 03a12d2360d9954aed19c2449070725a - entity_id: 7c1e7db73799cc3f90948b5118596985 - domain: light - - type: turn_off - device_id: 800eddbeeda071225f181a14cb9527e0 - entity_id: 521a92ddd8be76c7eddfc544f81f6020 - domain: light - - type: turn_off - device_id: 03eb359bf2344a58bebfe1e9c5bcfadd - entity_id: a30b2da3cd80a5b4c927e1608b91eb65 - domain: light - - type: turn_off - device_id: 21eb2bd28aba2ee361a22af92e8b2d16 - entity_id: 81c486d682afcc94e98e377475cc92fc - domain: light + - action: script.evening_lights_apply + data: + apply: force + - if: + - condition: state + entity_id: input_boolean.tv_mode + state: 'on' + then: + - action: light.turn_on + data: + brightness_pct: 5 + target: + area_id: living_room + mode: single +- id: evening_dim_ramp + alias: Lights - Evening Dim Ramp + description: Every 5 minutes from 20:30 to 23:30, ease the lights that are on + toward the schedule in script.evening_lights_apply + triggers: + - trigger: time_pattern + minutes: /5 + conditions: + - condition: time + after: '20:30:00' + before: '23:30:00' + actions: + - action: script.evening_lights_apply + data: + apply: ramp + mode: single +- id: 'lights_out_2345' + alias: Lights - Out (23:30) + description: Turn off all lights at 23:30. While the TV is on the living room + is left as it is + triggers: + - trigger: time + at: "23:30:00" + conditions: [] + actions: + - action: light.turn_off + target: + entity_id: + - light.kitchen_lights + - light.kitchen_wall_light + - light.dining_hall + - light.dining_room + - light.bathroom_hallway + - if: + - condition: state + entity_id: input_boolean.tv_mode + state: 'off' + then: + - action: light.turn_off + target: + entity_id: light.living_room + mode: single +- id: lights_sweep_0100 + alias: Lights - Sweep (01:00) + description: Catch anything turned back on after lights-out. While the TV is + on the living room is left as it is + triggers: + - trigger: time + at: "01:00:00" + conditions: [] + actions: + - action: light.turn_off + target: + entity_id: + - light.kitchen_lights + - light.kitchen_wall_light + - light.dining_hall + - light.dining_room + - light.bathroom_hallway + - action: switch.turn_off + target: + entity_id: switch.driveway_string_lights + - if: + - condition: state + entity_id: input_boolean.tv_mode + state: 'off' + then: + - action: light.turn_off + target: + entity_id: light.living_room mode: single - id: '1768862300896' alias: Bedroom On diff --git a/ansible/roles/podman/files/hass/configuration.yaml b/ansible/roles/podman/files/hass/configuration.yaml index 04c73ae..a25d51e 100644 --- a/ansible/roles/podman/files/hass/configuration.yaml +++ b/ansible/roles/podman/files/hass/configuration.yaml @@ -23,6 +23,7 @@ homeassistant: media: /share automation: !include automations.yaml +script: !include scripts.yaml input_boolean: tv_mode: diff --git a/ansible/roles/podman/files/hass/scripts.yaml b/ansible/roles/podman/files/hass/scripts.yaml new file mode 100644 index 0000000..83cbd32 --- /dev/null +++ b/ansible/roles/podman/files/hass/scripts.yaml @@ -0,0 +1,76 @@ +evening_lights_apply: + alias: Evening Lights - Apply Schedule + description: >- + Sets each evening light to its scheduled brightness for the current time, + blending linearly between the points in `schedule`. apply=force turns the + lights on (sunset, TV off); apply=ramp only eases lights that are already + on, and leaves alone any light someone has changed by hand. While TV mode + is on the living room and the lights that glare on the TV are left alone. + mode: queued + fields: + apply: + description: "force: turn lights on at the target. ramp: only adjust lights that are already on." + example: ramp + selector: + select: + options: + - force + - ramp + variables: + # [minute of day, brightness %] - 1230 = 20:30, 1260 = 21:00, + # 1290 = 21:30, 1350 = 22:30. Before the first point a light sits at the + # first value; after the last it holds the last value until lights-out. + schedule: + light.kitchen_lights: [[1230, 100], [1260, 50], [1290, 25], [1350, 1]] + light.kitchen_wall_light: [[1230, 100], [1260, 50], [1290, 25], [1350, 1]] + light.bathroom_hallway: [[1230, 75], [1260, 50], [1290, 25], [1350, 10]] + light.living_room: [[1230, 100], [1260, 50], [1290, 25], [1350, 10]] + light.dining_hall: [[1290, 25], [1350, 15]] + tv_mode_lights: + - light.living_room + - light.kitchen_wall_light + - light.dining_hall + - light.bathroom_hallway + apply_mode: "{{ apply | default('ramp') }}" + sequence: + - repeat: + for_each: "{{ schedule.keys() | list }}" + sequence: + - variables: + light: "{{ repeat.item }}" + # [target now, target 5 minutes ago - what the last ramp tick set] + levels: >- + {%- macro at(pts, t) -%} + {%- if t <= pts[0][0] -%}{{ pts[0][1] }} + {%- elif t >= pts[-1][0] -%}{{ pts[-1][1] }} + {%- else -%} + {%- for i in range(pts | length - 1) if pts[i][0] <= t < pts[i + 1][0] -%} + {{ (pts[i][1] + (pts[i + 1][1] - pts[i][1]) * (t - pts[i][0]) / (pts[i + 1][0] - pts[i][0])) | round(0) | int }} + {%- endfor -%} + {%- endif -%} + {%- endmacro -%} + {%- set t = now().hour * 60 + now().minute -%} + {{ [at(schedule[repeat.item], t) | int, at(schedule[repeat.item], t - 5) | int] }} + current: "{{ ((state_attr(repeat.item, 'brightness') or 0) / 2.55) | round(0) | int }}" + skip: "{{ is_state('input_boolean.tv_mode', 'on') and repeat.item in tv_mode_lights }}" + - choose: + - conditions: "{{ not skip and apply_mode == 'force' }}" + sequence: + - action: light.turn_on + target: + entity_id: "{{ light }}" + data: + brightness_pct: "{{ levels[0] }}" + transition: 2 + # A light more than 10 points off the last tick was set by hand; the + # biggest scheduled change in 5 minutes is ~8 + - conditions: >- + {{ not skip and apply_mode == 'ramp' and is_state(light, 'on') + and (current - levels[1]) | abs <= 10 and current != levels[0] }} + sequence: + - action: light.turn_on + target: + entity_id: "{{ light }}" + data: + brightness_pct: "{{ levels[0] }}" + transition: 60 diff --git a/ansible/roles/podman/tasks/containers/home/hass.yml b/ansible/roles/podman/tasks/containers/home/hass.yml index d47cfbd..786c4e8 100644 --- a/ansible/roles/podman/tasks/containers/home/hass.yml +++ b/ansible/roles/podman/tasks/containers/home/hass.yml @@ -25,6 +25,7 @@ loop: - configuration.yaml - automations.yaml + - scripts.yaml - name: flush handlers ansible.builtin.meta: flush_handlers diff --git a/ansible/roles/podman/tasks/main.yml b/ansible/roles/podman/tasks/main.yml index 28a0c9a..16f8815 100644 --- a/ansible/roles/podman/tasks/main.yml +++ b/ansible/roles/podman/tasks/main.yml @@ -39,7 +39,7 @@ - import_tasks: containers/home/hass.yml vars: - image: ghcr.io/home-assistant/home-assistant:2026.8.3 + image: ghcr.io/home-assistant/home-assistant:2026.9.3 tags: hass - import_tasks: containers/home/partsy.yml From f674f61b8d65f61b1b88255c47a8f4919db2f9d4 Mon Sep 17 00:00:00 2001 From: Bastian de Byl Date: Mon, 28 Sep 2026 11:32:24 -0400 Subject: [PATCH 07/10] fix(hass): don't fire on-off automations when a device reconnects The Bedroom Light HS200 dropped off Wi-Fi for 5 s at 03:01 and came back reporting "on"; the Bedroom On device trigger treated unavailable -> on as someone flipping the switch and lit the bedroom Hue lamps at 100%. Bedroom On/Off and TV On/Off now use state triggers with not_from unavailable/unknown, so reconnects and HA restarts no longer count as a flip. The driveway's switch-reconnect catch-up is dropped for the same reason (it would undo a manual off); the HA-restart catch-up stays. Co-Authored-By: Claude Opus 5.5 --- .../roles/podman/files/hass/automations.yaml | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/ansible/roles/podman/files/hass/automations.yaml b/ansible/roles/podman/files/hass/automations.yaml index 13969e0..aee6a0b 100644 --- a/ansible/roles/podman/files/hass/automations.yaml +++ b/ansible/roles/podman/files/hass/automations.yaml @@ -195,11 +195,12 @@ description: TV mode on; once it's dark, dim the living room and turn off the lights that glare on the TV triggers: - - type: turned_on - device_id: 18a9bb7a2a32be4371da447767ef50a9 - entity_id: c05688f2610e27e2d86380e2945ceae5 - domain: remote - trigger: device + - trigger: state + entity_id: remote.samsung_tv + to: 'on' + not_from: + - unavailable + - unknown conditions: [] actions: - action: input_boolean.turn_on @@ -232,11 +233,12 @@ scheduled level; late night (23:30 to sunrise) only turns off the TV glow; daytime leaves the lights alone triggers: - - type: turned_off - device_id: 18a9bb7a2a32be4371da447767ef50a9 - entity_id: c05688f2610e27e2d86380e2945ceae5 - domain: remote - trigger: device + - trigger: state + entity_id: remote.samsung_tv + to: 'off' + not_from: + - unavailable + - unknown conditions: [] actions: - action: input_boolean.turn_off @@ -268,7 +270,7 @@ - id: driveway_lights_on alias: Driveway String Lights On description: On 1 hour before sunset whether or not the TV is on. Also catches - up if Home Assistant restarts or the switch reconnects before the 23:00 off + up if Home Assistant restarts before the 23:00 off triggers: - trigger: sun event: sunset @@ -276,10 +278,6 @@ id: sunset - trigger: homeassistant event: start - - trigger: state - entity_id: switch.driveway_string_lights - from: unavailable - to: 'off' conditions: - condition: state entity_id: switch.driveway_string_lights @@ -399,11 +397,12 @@ alias: Bedroom On description: '' triggers: - - type: turned_on - device_id: afb9734fe9b187ab6881a64d24e1c2f5 - entity_id: 27efa149b9ebb388e7c21ba89e671b42 - domain: switch - trigger: device + - trigger: state + entity_id: switch.bedroom_light + to: 'on' + not_from: + - unavailable + - unknown conditions: [] actions: - action: light.turn_on @@ -417,11 +416,12 @@ alias: Bedroom Off description: '' triggers: - - type: turned_off - device_id: afb9734fe9b187ab6881a64d24e1c2f5 - entity_id: 27efa149b9ebb388e7c21ba89e671b42 - domain: switch - trigger: device + - trigger: state + entity_id: switch.bedroom_light + to: 'off' + not_from: + - unavailable + - unknown conditions: [] actions: - action: light.turn_off From 1852af5fc9dabd1f8b8ab29b8d41941c30967e18 Mon Sep 17 00:00:00 2001 From: Bastian de Byl Date: Mon, 28 Sep 2026 11:51:45 -0400 Subject: [PATCH 08/10] chore: bump gregtime to 3.19.0, rsvp to 1.0.7 Co-Authored-By: Claude Opus 5.5 --- ansible/roles/podman/tasks/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ansible/roles/podman/tasks/main.yml b/ansible/roles/podman/tasks/main.yml index 16f8815..c74cd54 100644 --- a/ansible/roles/podman/tasks/main.yml +++ b/ansible/roles/podman/tasks/main.yml @@ -123,14 +123,14 @@ - import_tasks: containers/home/gregtime.yml vars: - image: localhost/greg-time-bot:3.18.4 + image: localhost/greg-time-bot:3.19.0 tags: gregtime # Built and loaded by `make deploy-remote` in ~/src/rsvp-debylio; bump this to # the VERSION it loaded. The Caddy vhost ships with the caddy-config tag. - import_tasks: containers/home/rsvp.yml vars: - image: localhost/rsvpd:1.0.5 + image: localhost/rsvpd:1.0.7 tags: rsvp # Gated on zomboid_enabled (roles/podman/defaults/main.yml) so it can be taken From 5d6aa187ccad80e3d34642b72bf5001f53ed12c1 Mon Sep 17 00:00:00 2001 From: Bastian de Byl Date: Mon, 28 Sep 2026 11:51:45 -0400 Subject: [PATCH 09/10] chore(vault): update secrets Co-Authored-By: Claude Opus 5.5 --- ansible/vars/vault.yml | Bin 34980 -> 44829 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/ansible/vars/vault.yml b/ansible/vars/vault.yml index 340d8be2508dc53b18cecf889304a2e7555949d1..e2a3b5c8d367b5914d47667990d7996ed478577c 100644 GIT binary patch literal 44829 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 Zn^yhjrL4c7qGFsq6bb{M|4|u7za^g*K8*kX literal 34980 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 E>>7+UKL7v# From 73c552303b28238e71d3f640c5dd5557195fcc9e Mon Sep 17 00:00:00 2001 From: Bastian de Byl Date: Mon, 28 Sep 2026 11:53:14 -0400 Subject: [PATCH 10/10] docs(claude): work directly on master, no branches or PRs Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index f32b1a7..17e0608 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -28,6 +28,11 @@ The project uses Python virtualenv for dependency management: - Makefile automatically creates `.venv/` and installs dependencies - Vault password is sourced from password manager via `.pass.sh` +### Git Workflow +- Work directly on `master` - no feature branches and no pull requests in this repo. +- Commit to `master` and push to `origin/master` when asked; don't create a branch first. +- Pushing to `master` also triggers `.gitea/workflows/ci-images.yml` (see Gitea Actions CI images below), which only rebuilds images whose inputs changed. + ## Architecture ### Directory Structure