fix(gitea-actions): serve CI images from the Gitea registry #12
@@ -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-<n> 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
|
||||||
@@ -28,6 +28,11 @@ The project uses Python virtualenv for dependency management:
|
|||||||
- Makefile automatically creates `.venv/` and installs dependencies
|
- Makefile automatically creates `.venv/` and installs dependencies
|
||||||
- Vault password is sourced from password manager via `.pass.sh`
|
- 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
|
## Architecture
|
||||||
|
|
||||||
### Directory Structure
|
### Directory Structure
|
||||||
@@ -45,6 +50,7 @@ ansible/
|
|||||||
│ ├── ssl/ # Legacy SSL management (deprecated - Caddy handles certificates automatically)
|
│ ├── ssl/ # Legacy SSL management (deprecated - Caddy handles certificates automatically)
|
||||||
│ ├── github-actions/# CI/CD runner setup
|
│ ├── github-actions/# CI/CD runner setup
|
||||||
│ ├── labelprint/ # 4x6 label print proxy (Raspberry Pi, CUPS/TSPL)
|
│ ├── labelprint/ # 4x6 label print proxy (Raspberry Pi, CUPS/TSPL)
|
||||||
|
│ ├── gitea-actions/ # Gitea Actions runners + CI job images (see its README)
|
||||||
│ └── pihole/ # DNS filtering
|
│ └── pihole/ # DNS filtering
|
||||||
└── vars/
|
└── vars/
|
||||||
└── vault.yml # Encrypted secrets
|
└── vault.yml # Encrypted secrets
|
||||||
@@ -90,6 +96,7 @@ Tasks are tagged by service/component for selective deployment:
|
|||||||
- `ddns` - Dynamic DNS tasks
|
- `ddns` - Dynamic DNS tasks
|
||||||
- ~~`drone` - CI/CD tasks (decommissioned)~~
|
- ~~`drone` - CI/CD tasks (decommissioned)~~
|
||||||
- `hass` - Home Assistant tasks
|
- `hass` - Home Assistant tasks
|
||||||
|
- `gitea-actions` - Gitea Actions runners and their CI job images
|
||||||
- Common infrastructure tags like `common`, `ssl`
|
- Common infrastructure tags like `common`, `ssl`
|
||||||
|
|
||||||
## Configuration Files
|
## Configuration Files
|
||||||
@@ -122,6 +129,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
|
- Falls back to its own rescue Wi-Fi AP at 192.168.4.1 when the home SSID is
|
||||||
unreachable
|
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
|
### 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:
|
The `podman` user (and other service users) have `/bin/nologin` as their shell. To run commands as these users via SSH:
|
||||||
|
|||||||
@@ -54,7 +54,9 @@ ${VAULT_FILE}: ${VAULT_PASS_FILE}
|
|||||||
touch $@
|
touch $@
|
||||||
|
|
||||||
# Linting
|
# 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
|
SKIP_FILE=./.lint-vars.sh
|
||||||
|
|
||||||
# Targets
|
# Targets
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ git_home: "/srv/{{ git_user }}"
|
|||||||
|
|
||||||
# Gitea configuration
|
# Gitea configuration
|
||||||
gitea_debyl_server_name: git.debyl.io
|
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
|
gitea_db_image: docker.io/library/postgres:14-alpine
|
||||||
|
|
||||||
# Skudak Gitea configuration
|
# Skudak Gitea configuration
|
||||||
gitea_skudak_server_name: git.skudak.com
|
gitea_skudak_server_name: git.skudak.com
|
||||||
gitea_skudak_ssh_port: 2222
|
gitea_skudak_ssh_port: 2222
|
||||||
|
gitea_skudak_image: docker.gitea.com/gitea:1.27.3
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
become_user: "{{ git_user }}"
|
become_user: "{{ git_user }}"
|
||||||
containers.podman.podman_container:
|
containers.podman.podman_container:
|
||||||
name: gitea-skudak
|
name: gitea-skudak
|
||||||
image: "{{ gitea_image }}"
|
image: "{{ gitea_skudak_image }}"
|
||||||
pod: gitea-skudak-pod
|
pod: gitea-skudak-pod
|
||||||
restart_policy: on-failure:3
|
restart_policy: on-failure:3
|
||||||
log_driver: journald
|
log_driver: journald
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
state: started
|
state: started
|
||||||
ports:
|
ports:
|
||||||
- "3100:3000"
|
- "3100:3000"
|
||||||
tags: gitea
|
tags: gitea, gitea-debyl
|
||||||
|
|
||||||
# PostgreSQL container in pod
|
# PostgreSQL container in pod
|
||||||
- name: create gitea-debyl-postgres container
|
- name: create gitea-debyl-postgres container
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
POSTGRES_PASSWORD: "{{ gitea_debyl_db_pass }}"
|
POSTGRES_PASSWORD: "{{ gitea_debyl_db_pass }}"
|
||||||
volumes:
|
volumes:
|
||||||
- "{{ git_home }}/volumes/gitea/psql:/var/lib/postgresql/data"
|
- "{{ git_home }}/volumes/gitea/psql:/var/lib/postgresql/data"
|
||||||
tags: gitea
|
tags: gitea, gitea-debyl
|
||||||
|
|
||||||
# Gitea container in pod
|
# Gitea container in pod
|
||||||
- name: create gitea-debyl container
|
- name: create gitea-debyl container
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
become_user: "{{ git_user }}"
|
become_user: "{{ git_user }}"
|
||||||
containers.podman.podman_container:
|
containers.podman.podman_container:
|
||||||
name: gitea-debyl
|
name: gitea-debyl
|
||||||
image: "{{ gitea_image }}"
|
image: "{{ gitea_debyl_image }}"
|
||||||
pod: gitea-debyl-pod
|
pod: gitea-debyl-pod
|
||||||
restart_policy: on-failure:3
|
restart_policy: on-failure:3
|
||||||
log_driver: journald
|
log_driver: journald
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
volumes:
|
volumes:
|
||||||
- "{{ git_home }}/volumes/gitea/data:/data"
|
- "{{ git_home }}/volumes/gitea/data:/data"
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
tags: gitea
|
tags: gitea, gitea-debyl
|
||||||
|
|
||||||
# Generate systemd service for the pod
|
# Generate systemd service for the pod
|
||||||
- name: create systemd job for gitea-debyl-pod
|
- name: create systemd job for gitea-debyl-pod
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
args:
|
args:
|
||||||
chdir: "{{ git_home }}"
|
chdir: "{{ git_home }}"
|
||||||
changed_when: false
|
changed_when: false
|
||||||
tags: gitea
|
tags: gitea, gitea-debyl
|
||||||
|
|
||||||
- name: enable gitea-debyl-pod service
|
- name: enable gitea-debyl-pod service
|
||||||
become: true
|
become: true
|
||||||
@@ -91,4 +91,4 @@
|
|||||||
enabled: true
|
enabled: true
|
||||||
state: started
|
state: started
|
||||||
scope: user
|
scope: user
|
||||||
tags: gitea
|
tags: gitea, gitea-debyl
|
||||||
|
|||||||
@@ -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_idf_version>` | esp-mg-tpms, skudak/esp32-stm32-vcu |
|
||||||
|
| `container: image:` | `git.debyl.io/gitbot/gitea-ci-platformio:<pio_espressif32_version>` | 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-<n>` 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.
|
||||||
@@ -22,20 +22,70 @@ act_runner_bin: /usr/local/bin/act_runner
|
|||||||
act_runner_config_dir: /etc/act_runner
|
act_runner_config_dir: /etc/act_runner
|
||||||
act_runner_work_dir: /var/lib/act_runner
|
act_runner_work_dir: /var/lib/act_runner
|
||||||
|
|
||||||
# Job container images (built locally into the gitea-runner rootless image
|
# Job container images, served from the Gitea container registry.
|
||||||
# store by tasks/images.yml; never pulled — force_pull is false).
|
#
|
||||||
gitea_ci_image: localhost/gitea-ci:latest
|
# 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).
|
||||||
|
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 firmware image tag tracks the upstream espressif/idf release we build from.
|
||||||
esp_idf_version: v5.4.1
|
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).
|
# PlatformIO image for Arduino-framework ESP32 builds (esp32-web-interface).
|
||||||
# Tag tracks the pre-baked espressif32 platform version; both pins match the
|
# Tag tracks the pre-baked espressif32 platform version; both pins match the
|
||||||
# hardware-validated local build.
|
# hardware-validated local build.
|
||||||
platformio_core_version: "6.1.19"
|
platformio_core_version: "6.1.19"
|
||||||
pio_espressif32_version: "7.0.1"
|
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 }}"
|
||||||
|
|
||||||
# Default labels for every runner — map runs-on values to the local CI image.
|
# 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"
|
||||||
|
|
||||||
|
# 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
|
||||||
|
build_args: ""
|
||||||
|
- image: "{{ gitea_ci_espidf_image }}"
|
||||||
|
containerfile: Containerfile.espidf
|
||||||
|
build_args: "--build-arg ESP_IDF_VERSION={{ esp_idf_version }}"
|
||||||
|
- image: "{{ gitea_ci_platformio_image }}"
|
||||||
|
containerfile: Containerfile.platformio
|
||||||
|
build_args: >-
|
||||||
|
--build-arg PLATFORMIO_CORE_VERSION={{ platformio_core_version }}
|
||||||
|
--build-arg PIO_ESPRESSIF32_VERSION={{ pio_espressif32_version }}
|
||||||
|
|
||||||
|
# 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.
|
# Firmware jobs opt into the ESP-IDF image per-job via `container:` in their workflow.
|
||||||
gitea_runner_labels:
|
gitea_runner_labels:
|
||||||
- "fedora:docker://{{ gitea_ci_image }}"
|
- "fedora:docker://{{ gitea_ci_image }}"
|
||||||
|
|||||||
+10
@@ -1,8 +1,18 @@
|
|||||||
# Default Gitea Actions job image (managed by ansible: roles/gitea-actions).
|
# Default Gitea Actions job image (managed by ansible: roles/gitea-actions).
|
||||||
# Covers Go/web/node jobs plus `docker build` (talks to the mounted rootless
|
# 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.
|
# 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
|
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
|
ARG DOCKER_CLI_VERSION=27.3.1
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
+13
-1
@@ -14,7 +14,19 @@
|
|||||||
# the release aborts *after* the firmware and version.json are already live —
|
# 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
|
# clients get the new build while the tag, Gitea release and protocol manifest
|
||||||
# are never written. Keep it installed.
|
# 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 \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
curl ca-certificates unzip jq python3-yaml python3-jinja2 \
|
curl ca-certificates unzip jq python3-yaml python3-jinja2 \
|
||||||
+20
-2
@@ -8,8 +8,23 @@
|
|||||||
# was validated on hardware — bump pio_espressif32_version /
|
# was validated on hardware — bump pio_espressif32_version /
|
||||||
# platformio_core_version in defaults/main.yml to upgrade (the image tag
|
# platformio_core_version in defaults/main.yml to upgrade (the image tag
|
||||||
# tracks the platform version).
|
# 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
|
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
|
ENV PLATFORMIO_CORE_DIR=/opt/platformio
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
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 \
|
&& apt-get install -y --no-install-recommends nodejs \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& 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`
|
# Seed project mirroring the real projects' platformio.ini so `pio pkg install`
|
||||||
# pulls the platform + toolchain + framework packages into the core dir.
|
# 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 \
|
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 \
|
||||||
&& pio pkg install -d /tmp/seed --tool platformio/tool-mkspiffs \
|
&& pio pkg install -d /tmp/seed --tool platformio/tool-mkspiffs \
|
||||||
&& rm -rf /tmp/seed \
|
&& rm -rf /tmp/seed \
|
||||||
@@ -1,4 +1,51 @@
|
|||||||
---
|
---
|
||||||
|
# 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 }}"
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ gitea_ci_registry_authfile | dirname }}"
|
||||||
|
state: directory
|
||||||
|
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 }}"
|
||||||
|
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: pull CI images from the registry
|
||||||
|
become: true
|
||||||
|
become_user: "{{ gitea_runner_user }}"
|
||||||
|
containers.podman.podman_image:
|
||||||
|
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 (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
|
- name: create CI image build directory
|
||||||
become: true
|
become: true
|
||||||
become_user: "{{ gitea_runner_user }}"
|
become_user: "{{ gitea_runner_user }}"
|
||||||
@@ -6,73 +53,41 @@
|
|||||||
path: "{{ gitea_runner_home }}/ci-images"
|
path: "{{ gitea_runner_home }}/ci-images"
|
||||||
state: directory
|
state: directory
|
||||||
mode: "0755"
|
mode: "0755"
|
||||||
|
when: gitea_ci_build_local | bool
|
||||||
tags: gitea-actions
|
tags: gitea-actions
|
||||||
|
|
||||||
- name: stage default CI Containerfile
|
# 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: true
|
||||||
become_user: "{{ gitea_runner_user }}"
|
become_user: "{{ gitea_runner_user }}"
|
||||||
ansible.builtin.template:
|
ansible.builtin.copy:
|
||||||
src: Containerfile.ci
|
src: "{{ item.containerfile }}"
|
||||||
dest: "{{ gitea_runner_home }}/ci-images/Containerfile.ci"
|
dest: "{{ gitea_runner_home }}/ci-images/{{ item.containerfile }}"
|
||||||
mode: "0644"
|
mode: "0644"
|
||||||
register: ci_containerfile
|
loop: "{{ gitea_ci_images }}"
|
||||||
|
loop_control:
|
||||||
|
label: "{{ item.containerfile }}"
|
||||||
|
when: gitea_ci_build_local | bool
|
||||||
tags: gitea-actions
|
tags: gitea-actions
|
||||||
|
|
||||||
- name: stage ESP-IDF CI Containerfile
|
- name: build and push CI images
|
||||||
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 }})
|
|
||||||
become: true
|
become: true
|
||||||
become_user: "{{ gitea_runner_user }}"
|
become_user: "{{ gitea_runner_user }}"
|
||||||
containers.podman.podman_image:
|
containers.podman.podman_image:
|
||||||
name: "{{ gitea_ci_image }}"
|
name: "{{ item.image }}"
|
||||||
path: "{{ gitea_runner_home }}/ci-images"
|
path: "{{ gitea_runner_home }}/ci-images"
|
||||||
build:
|
build:
|
||||||
file: "{{ gitea_runner_home }}/ci-images/Containerfile.ci"
|
file: "{{ gitea_runner_home }}/ci-images/{{ item.containerfile }}"
|
||||||
force: "{{ ci_containerfile is changed }}"
|
extra_args: "{{ item.build_args }}"
|
||||||
environment:
|
force: true
|
||||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
push: true
|
||||||
tags: gitea-actions
|
auth_file: "{{ gitea_ci_registry_authfile }}"
|
||||||
|
|
||||||
- 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 }})
|
|
||||||
become: true
|
|
||||||
become_user: "{{ gitea_runner_user }}"
|
|
||||||
containers.podman.podman_image:
|
|
||||||
name: "{{ gitea_ci_espidf_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 }}"
|
|
||||||
environment:
|
environment:
|
||||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||||
|
loop: "{{ gitea_ci_images }}"
|
||||||
|
loop_control:
|
||||||
|
label: "{{ item.image }}"
|
||||||
|
when: gitea_ci_build_local | bool
|
||||||
tags: gitea-actions
|
tags: gitea-actions
|
||||||
|
|||||||
@@ -311,6 +311,14 @@ podman_prune_ci_users:
|
|||||||
- gitea-runner
|
- gitea-runner
|
||||||
- actions-runner
|
- actions-runner
|
||||||
podman_prune_ci_until: 48h
|
podman_prune_ci_until: 48h
|
||||||
|
# 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
|
# 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.
|
# is what let the store reach 113 GB between runs.
|
||||||
|
|||||||
@@ -192,46 +192,53 @@
|
|||||||
mode: single
|
mode: single
|
||||||
- id: '1762116115638'
|
- id: '1762116115638'
|
||||||
alias: Light - TV On
|
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:
|
triggers:
|
||||||
- type: turned_on
|
- trigger: state
|
||||||
device_id: 18a9bb7a2a32be4371da447767ef50a9
|
entity_id: remote.samsung_tv
|
||||||
entity_id: c05688f2610e27e2d86380e2945ceae5
|
to: 'on'
|
||||||
domain: remote
|
not_from:
|
||||||
trigger: device
|
- unavailable
|
||||||
|
- unknown
|
||||||
conditions: []
|
conditions: []
|
||||||
actions:
|
actions:
|
||||||
- action: input_boolean.turn_on
|
- action: input_boolean.turn_on
|
||||||
target:
|
target:
|
||||||
entity_id: input_boolean.tv_mode
|
entity_id: input_boolean.tv_mode
|
||||||
|
- if:
|
||||||
|
- condition: or
|
||||||
|
conditions:
|
||||||
|
- condition: sun
|
||||||
|
after: sunset
|
||||||
|
after_offset: "-01:00:00"
|
||||||
|
- condition: sun
|
||||||
|
before: sunrise
|
||||||
|
then:
|
||||||
- action: light.turn_on
|
- action: light.turn_on
|
||||||
metadata: {}
|
|
||||||
data:
|
data:
|
||||||
brightness_pct: 5
|
brightness_pct: 5
|
||||||
target:
|
target:
|
||||||
area_id: living_room
|
area_id: living_room
|
||||||
- type: turn_off
|
- action: light.turn_off
|
||||||
device_id: 03a12d2360d9954aed19c2449070725a
|
target:
|
||||||
entity_id: 7c1e7db73799cc3f90948b5118596985
|
entity_id:
|
||||||
domain: light
|
- light.kitchen_wall_light
|
||||||
- type: turn_off
|
- light.dining_hall
|
||||||
device_id: 800eddbeeda071225f181a14cb9527e0
|
- light.bathroom_hallway
|
||||||
entity_id: 521a92ddd8be76c7eddfc544f81f6020
|
|
||||||
domain: light
|
|
||||||
- type: turn_off
|
|
||||||
device_id: 3f7f65571d9bb0833433996f1f6725bd
|
|
||||||
entity_id: 7407afe14783543252c666d5ff7c5d5c
|
|
||||||
domain: light
|
|
||||||
mode: single
|
mode: single
|
||||||
- id: new_tv_off
|
- id: new_tv_off
|
||||||
alias: Light - TV Off - Restore
|
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:
|
triggers:
|
||||||
- type: turned_off
|
- trigger: state
|
||||||
device_id: 18a9bb7a2a32be4371da447767ef50a9
|
entity_id: remote.samsung_tv
|
||||||
entity_id: c05688f2610e27e2d86380e2945ceae5
|
to: 'off'
|
||||||
domain: remote
|
not_from:
|
||||||
trigger: device
|
- unavailable
|
||||||
|
- unknown
|
||||||
conditions: []
|
conditions: []
|
||||||
actions:
|
actions:
|
||||||
- action: input_boolean.turn_off
|
- action: input_boolean.turn_off
|
||||||
@@ -239,409 +246,163 @@
|
|||||||
entity_id: input_boolean.tv_mode
|
entity_id: input_boolean.tv_mode
|
||||||
- choose:
|
- choose:
|
||||||
- conditions:
|
- conditions:
|
||||||
|
- condition: sun
|
||||||
|
after: sunset
|
||||||
|
after_offset: "-01:00:00"
|
||||||
- condition: time
|
- condition: time
|
||||||
after: '22:30:00'
|
before: '23:30:00'
|
||||||
before: '23:45:00'
|
|
||||||
sequence:
|
sequence:
|
||||||
# Late dim levels
|
- action: script.evening_lights_apply
|
||||||
- type: turn_on
|
data:
|
||||||
device_id: f31e4f9bf8fa3687a07aeb4430eaef38
|
apply: force
|
||||||
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
|
|
||||||
- conditions:
|
- conditions:
|
||||||
|
- condition: or
|
||||||
|
conditions:
|
||||||
- condition: time
|
- condition: time
|
||||||
after: '21:30:00'
|
after: '23:30:00'
|
||||||
before: '22:30:00'
|
- condition: sun
|
||||||
|
before: sunrise
|
||||||
sequence:
|
sequence:
|
||||||
# Mid dim levels
|
- action: light.turn_off
|
||||||
- type: turn_on
|
target:
|
||||||
device_id: f31e4f9bf8fa3687a07aeb4430eaef38
|
area_id: living_room
|
||||||
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
|
|
||||||
mode: single
|
mode: single
|
||||||
- id: 'sunset_lights_on'
|
- id: driveway_lights_on
|
||||||
alias: Lights - Sunset On
|
alias: Driveway String Lights On
|
||||||
description: Turn on lights 1 hour before sunset
|
description: On 1 hour before sunset whether or not the TV is on. Also catches
|
||||||
|
up if Home Assistant restarts before the 23:00 off
|
||||||
triggers:
|
triggers:
|
||||||
- trigger: sun
|
- trigger: sun
|
||||||
event: sunset
|
event: sunset
|
||||||
offset: "-01:00:00"
|
offset: "-01:00:00"
|
||||||
|
id: sunset
|
||||||
|
- trigger: homeassistant
|
||||||
|
event: start
|
||||||
conditions:
|
conditions:
|
||||||
- condition: state
|
- condition: state
|
||||||
entity_id: input_boolean.tv_mode
|
entity_id: switch.driveway_string_lights
|
||||||
state: 'off'
|
state: 'off'
|
||||||
actions:
|
- condition: or
|
||||||
- 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
|
|
||||||
mode: single
|
|
||||||
- id: 'evening_dim_2100'
|
|
||||||
alias: Lights - Evening Dim (21:00)
|
|
||||||
description: Dim lights at 21:00 - only affects lights that are ON
|
|
||||||
triggers:
|
|
||||||
- trigger: time
|
|
||||||
at: "21:00:00"
|
|
||||||
conditions:
|
conditions:
|
||||||
- condition: state
|
- condition: trigger
|
||||||
entity_id: input_boolean.tv_mode
|
id: sunset
|
||||||
state: 'off'
|
- condition: and
|
||||||
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:
|
conditions:
|
||||||
- condition: state
|
- condition: sun
|
||||||
entity_id: input_boolean.tv_mode
|
after: sunset
|
||||||
state: 'off'
|
after_offset: "-01:00:00"
|
||||||
|
- condition: time
|
||||||
|
before: '23:00:00'
|
||||||
actions:
|
actions:
|
||||||
- if:
|
- action: switch.turn_on
|
||||||
- condition: device
|
target:
|
||||||
device_id: f31e4f9bf8fa3687a07aeb4430eaef38
|
entity_id: switch.driveway_string_lights
|
||||||
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
|
mode: single
|
||||||
- id: 'late_dim_2230'
|
- id: 'sunset_lights_on'
|
||||||
alias: Lights - Late Dim (22:30)
|
alias: Lights - Sunset On
|
||||||
description: Dim lights at 22:30 - only affects lights that are 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:
|
triggers:
|
||||||
- trigger: time
|
- trigger: sun
|
||||||
at: "22:30:00"
|
event: sunset
|
||||||
conditions:
|
offset: "-01:00:00"
|
||||||
- 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"
|
|
||||||
conditions: []
|
conditions: []
|
||||||
actions:
|
actions:
|
||||||
- type: turn_off
|
- action: script.evening_lights_apply
|
||||||
device_id: 3f7f65571d9bb0833433996f1f6725bd
|
data:
|
||||||
entity_id: 7407afe14783543252c666d5ff7c5d5c
|
apply: force
|
||||||
domain: light
|
- if:
|
||||||
- type: turn_off
|
- condition: state
|
||||||
device_id: f31e4f9bf8fa3687a07aeb4430eaef38
|
entity_id: input_boolean.tv_mode
|
||||||
entity_id: b79934d97f3bb9d8a3da47c76d03ded4
|
state: 'on'
|
||||||
domain: light
|
then:
|
||||||
- type: turn_off
|
- action: light.turn_on
|
||||||
device_id: 03a12d2360d9954aed19c2449070725a
|
data:
|
||||||
entity_id: 7c1e7db73799cc3f90948b5118596985
|
brightness_pct: 5
|
||||||
domain: light
|
target:
|
||||||
- type: turn_off
|
area_id: living_room
|
||||||
device_id: 800eddbeeda071225f181a14cb9527e0
|
mode: single
|
||||||
entity_id: 521a92ddd8be76c7eddfc544f81f6020
|
- id: evening_dim_ramp
|
||||||
domain: light
|
alias: Lights - Evening Dim Ramp
|
||||||
- type: turn_off
|
description: Every 5 minutes from 20:30 to 23:30, ease the lights that are on
|
||||||
device_id: 03eb359bf2344a58bebfe1e9c5bcfadd
|
toward the schedule in script.evening_lights_apply
|
||||||
entity_id: a30b2da3cd80a5b4c927e1608b91eb65
|
triggers:
|
||||||
domain: light
|
- trigger: time_pattern
|
||||||
- type: turn_off
|
minutes: /5
|
||||||
device_id: 21eb2bd28aba2ee361a22af92e8b2d16
|
conditions:
|
||||||
entity_id: 81c486d682afcc94e98e377475cc92fc
|
- condition: time
|
||||||
domain: light
|
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
|
mode: single
|
||||||
- id: '1768862300896'
|
- id: '1768862300896'
|
||||||
alias: Bedroom On
|
alias: Bedroom On
|
||||||
description: ''
|
description: ''
|
||||||
triggers:
|
triggers:
|
||||||
- type: turned_on
|
- trigger: state
|
||||||
device_id: afb9734fe9b187ab6881a64d24e1c2f5
|
entity_id: switch.bedroom_light
|
||||||
entity_id: 27efa149b9ebb388e7c21ba89e671b42
|
to: 'on'
|
||||||
domain: switch
|
not_from:
|
||||||
trigger: device
|
- unavailable
|
||||||
|
- unknown
|
||||||
conditions: []
|
conditions: []
|
||||||
actions:
|
actions:
|
||||||
- action: light.turn_on
|
- action: light.turn_on
|
||||||
@@ -655,11 +416,12 @@
|
|||||||
alias: Bedroom Off
|
alias: Bedroom Off
|
||||||
description: ''
|
description: ''
|
||||||
triggers:
|
triggers:
|
||||||
- type: turned_off
|
- trigger: state
|
||||||
device_id: afb9734fe9b187ab6881a64d24e1c2f5
|
entity_id: switch.bedroom_light
|
||||||
entity_id: 27efa149b9ebb388e7c21ba89e671b42
|
to: 'off'
|
||||||
domain: switch
|
not_from:
|
||||||
trigger: device
|
- unavailable
|
||||||
|
- unknown
|
||||||
conditions: []
|
conditions: []
|
||||||
actions:
|
actions:
|
||||||
- action: light.turn_off
|
- action: light.turn_off
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ homeassistant:
|
|||||||
media: /share
|
media: /share
|
||||||
|
|
||||||
automation: !include automations.yaml
|
automation: !include automations.yaml
|
||||||
|
script: !include scripts.yaml
|
||||||
|
|
||||||
input_boolean:
|
input_boolean:
|
||||||
tv_mode:
|
tv_mode:
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
loop:
|
loop:
|
||||||
- configuration.yaml
|
- configuration.yaml
|
||||||
- automations.yaml
|
- automations.yaml
|
||||||
|
- scripts.yaml
|
||||||
|
|
||||||
- name: flush handlers
|
- name: flush handlers
|
||||||
ansible.builtin.meta: flush_handlers
|
ansible.builtin.meta: flush_handlers
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
|
|
||||||
- import_tasks: containers/home/hass.yml
|
- import_tasks: containers/home/hass.yml
|
||||||
vars:
|
vars:
|
||||||
image: ghcr.io/home-assistant/home-assistant:2026.8.3
|
image: ghcr.io/home-assistant/home-assistant:2026.9.3
|
||||||
tags: hass
|
tags: hass
|
||||||
|
|
||||||
- import_tasks: containers/home/partsy.yml
|
- import_tasks: containers/home/partsy.yml
|
||||||
@@ -123,14 +123,14 @@
|
|||||||
|
|
||||||
- import_tasks: containers/home/gregtime.yml
|
- import_tasks: containers/home/gregtime.yml
|
||||||
vars:
|
vars:
|
||||||
image: localhost/greg-time-bot:3.18.1
|
image: localhost/greg-time-bot:3.19.0
|
||||||
tags: gregtime
|
tags: gregtime
|
||||||
|
|
||||||
# Built and loaded by `make deploy-remote` in ~/src/rsvp-debylio; bump this to
|
# 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.
|
# the VERSION it loaded. The Caddy vhost ships with the caddy-config tag.
|
||||||
- import_tasks: containers/home/rsvp.yml
|
- import_tasks: containers/home/rsvp.yml
|
||||||
vars:
|
vars:
|
||||||
image: localhost/rsvpd:1.0.5
|
image: localhost/rsvpd:1.0.7
|
||||||
tags: rsvp
|
tags: rsvp
|
||||||
|
|
||||||
# Gated on zomboid_enabled (roles/podman/defaults/main.yml) so it can be taken
|
# Gated on zomboid_enabled (roles/podman/defaults/main.yml) so it can be taken
|
||||||
|
|||||||
@@ -43,9 +43,10 @@ run() {
|
|||||||
exec podman "$@"' _ "$@"
|
exec podman "$@"' _ "$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
# prune_user <user> <until> <prune_containers: yes|no>
|
# prune_user <user> <until> <prune_containers: yes|no> [image prune filter...]
|
||||||
prune_user() {
|
prune_user() {
|
||||||
local u=$1 keep=$2 do_containers=$3
|
local u=$1 keep=$2 do_containers=$3
|
||||||
|
shift 3
|
||||||
local before after img vol con
|
local before after img vol con
|
||||||
|
|
||||||
if ! id "$u" >/dev/null 2>&1; then
|
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)
|
con=$(run "$u" container prune -f --filter "until=$keep" 2>&1 | tail -1)
|
||||||
fi
|
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)
|
vol=$(run "$u" volume prune -f 2>&1 | tail -1)
|
||||||
|
|
||||||
after=$(run "$u" system df --format '{{ '{{' }}.Size{{ '}}' }}' 2>/dev/null | head -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
|
done
|
||||||
|
|
||||||
for u in {{ podman_prune_ci_users | join(' ') }}; do
|
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
|
done
|
||||||
|
|
||||||
log "status=ok"
|
log "status=ok"
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user