feat: live tracker suite - WIKITOOL_TASKS_CONFIG override, real-tracker tests for Super Productivity and CalDAV, nightly workflow and test image (#156)
CI / verify (push) Failing after 2m1s
Release / release (push) Successful in 38s

Files changed:
- .gitea/scripts/start-radicale.sh
- .gitea/sp-live/Dockerfile
- .gitea/sp-live/resolve-version.sh
- .gitea/workflows/ci.yml
- .gitea/workflows/sp-live-image.yml
- .gitea/workflows/tracker-live.yml
- .gitignore
- CHANGES.md
- DEVELOPMENT.md
- INSTALL.md
- VERSION
- instructions/dev/doc-pull-through.md
- instructions/dev/stack-dev/SKILL.md
- instructions/dev/testing-conventions.md
- instructions/dev/tracker-testing.md
- kb/gtd/INDEX.md
- kb/gtd/technik/Chemenu 8.0.0 freigeben.md
- kb/gtd/technik/Windows nativ unterstützen.md
- kb/index.md
- tools/CONTRACT.md
- tools/chemenu/commands/docs_verify.py
- tools/chemenu/commands/doctor.py
- tools/chemenu/commands/review_cmd.py
- tools/chemenu/commands/task_cmd.py
- tools/chemenu/config.py
- tools/chemenu/tasks/config.py
- tools/chemenu/tests/conftest.py
- tools/chemenu/tests/fixtures/sp/MANIFEST.json
- tools/chemenu/tests/fixtures/sp/api/health.json
- tools/chemenu/tests/fixtures/sp/api/projects.json
- tools/chemenu/tests/fixtures/sp/api/tags.json
- tools/chemenu/tests/fixtures/sp/api/tasks.json
- tools/chemenu/tests/fixtures/sp/seed-backup.json
- tools/chemenu/tests/record_sp_fixtures.py
- tools/chemenu/tests/sp_headless.py
- tools/chemenu/tests/test_doctor.py
- tools/chemenu/tests/test_review.py
- tools/chemenu/tests/test_sp_recorded.py
- tools/chemenu/tests/test_task_cmd.py
- tools/chemenu/tests/test_tasks_config.py
- tools/chemenu/tests/test_tracker_live.py
- tools/chemenu/tests/tracker_live.py
- tools/pytest.ini
This commit is contained in:
torben committed 2026-09-30 13:23:24 +02:00
1 parent 529793b255
commit b0c64772cc
43 files changed
+2279 -32

No files matched your search

+44
View File
@@ -0,0 +1,44 @@
#!/bin/sh
# Start a throwaway Radicale (a small CalDAV server) for the live tracker suite (Gitea #156).
#
# start-radicale.sh <python-with-radicale> <work-dir>
#
# Radicale runs as a background process, not a service container: the jobs that call this
# already run inside a job container, and a process on 127.0.0.1 needs no network wiring.
# Appends the CHEMENU_LIVE_CALDAV_* variables to $GITHUB_ENV, so the pytest step that follows
# runs the CalDAV half of the live suite - with CHEMENU_LIVE_REQUIRE=caldav set by the
# workflow, so a server that did not come up fails the run instead of skipping it.
set -eu
python="$1"
work="$2"
mkdir -p "$work"
printf 'ci:ci-live-secret\n' > "$work/users"
cat > "$work/config" <<CONF
[server]
hosts = 127.0.0.1:5232
[auth]
type = htpasswd
htpasswd_filename = $work/users
htpasswd_encryption = plain
[storage]
filesystem_folder = $work/data
CONF
"$python" -m radicale --config "$work/config" > "$work/radicale.log" 2>&1 &
i=0
until curl -fsS -o /dev/null -u ci:ci-live-secret -X PROPFIND -H 'Depth: 0' http://127.0.0.1:5232/ci/; do
i=$((i + 1))
if [ "$i" -gt 30 ]; then
echo "radicale did not come up:" >&2
cat "$work/radicale.log" >&2
exit 1
fi
sleep 1
done
version="$("$python" -m radicale --version 2>/dev/null | tail -n 1)"
{
echo "CHEMENU_LIVE_CALDAV_URL=http://127.0.0.1:5232/ci/"
echo "CHEMENU_LIVE_CALDAV_USER=ci"
echo "CHEMENU_LIVE_CALDAV_PASSWORD=ci-live-secret"
echo "CHEMENU_LIVE_CALDAV_VERSION=radicale $version"
} >> "${GITHUB_ENV:?start-radicale.sh runs inside a workflow step}"
echo "radicale $version is up"
+30
View File
@@ -0,0 +1,30 @@
# The image the nightly `tracker-live` workflow runs in: the packaged Super Productivity
# desktop app, an X server to hold it, and what the suite itself needs (Gitea #156).
# Built by `.gitea/workflows/sp-live-image.yml`, never by hand.
FROM debian:trixie-slim
ARG SP_VERSION
ARG SP_SHA512
# `nodejs` is for act_runner, which executes JavaScript actions (checkout) inside the
# job container. `libasound2t64` and `libgbm1` are the two libraries the .deb does not
# pull in and the app will not start without.
RUN set -eu; \
test -n "$SP_VERSION" && test -n "$SP_SHA512"; \
apt-get update -qq; \
apt-get install -y --no-install-recommends \
ca-certificates curl git nodejs python3 python3-venv ripgrep \
xvfb xauth libasound2t64 libgbm1; \
curl -fsSL -o /tmp/sp.deb \
"https://github.com/super-productivity/super-productivity/releases/download/v${SP_VERSION}/superProductivity-amd64.deb"; \
expected="$(printf '%s' "$SP_SHA512" | base64 -d | od -An -v -tx1 | tr -d ' \n')"; \
echo "${expected} /tmp/sp.deb" | sha512sum -c -; \
apt-get install -y --no-install-recommends /tmp/sp.deb; \
rm -rf /tmp/sp.deb /var/lib/apt/lists/*
LABEL org.opencontainers.image.title="chemenu-sp-live" \
org.opencontainers.image.description="Packaged Super Productivity for chemenu's live tracker suite" \
chemenu.sp-version="${SP_VERSION}"
ENV CHEMENU_LIVE_SP_BINARY="/opt/Super Productivity/superproductivity" \
CHEMENU_LIVE_SP_VERSION="${SP_VERSION}"
+26
View File
@@ -0,0 +1,26 @@
#!/bin/sh
# Which Super Productivity release, and the sha512 of its .deb, straight from the
# update channel the desktop clients themselves follow (`latest-linux.yml`).
#
# resolve-version.sh the newest release
# resolve-version.sh 19.1.0 that release
#
# Prints two lines, `version=<x>` and `sha512=<base64>`, so a workflow can append the
# output to $GITHUB_OUTPUT as it is. Gitea #156: the test image follows the channel
# rather than a pin, because installed apps update on their own and a pinned old
# version would be tested against while users run the new one.
set -eu
base=https://github.com/super-productivity/super-productivity/releases
if [ "${1:-latest}" = latest ]; then
url="$base/latest/download/latest-linux.yml"
else
url="$base/download/v$1/latest-linux.yml"
fi
yml="$(curl -fsSL "$url")"
version="$(printf '%s\n' "$yml" | sed -n 's/^version: *//p' | head -n 1)"
sha512="$(printf '%s\n' "$yml" | awk '/url: superProductivity-amd64\.deb/ {found=1; next} found && /sha512:/ {print $2; exit}')"
if [ -z "$version" ] || [ -z "$sha512" ]; then
echo "resolve-version: no version/sha512 for the amd64 .deb in $url" >&2
exit 1
fi
printf 'version=%s\nsha512=%s\n' "$version" "$sha512"
+15
View File
@@ -132,6 +132,21 @@ jobs:
.venv/bin/python -m pytest -q \
--cov --cov-report=term --cov-report=xml --cov-report=html
- name: Live tracker suite (CalDAV)
# The one live tracker that needs no app and no display: a throwaway Radicale on
# loopback. `CHEMENU_LIVE_REQUIRE=caldav` turns "no server" into a failure - without
# it the suite would skip and stay green, which is exactly the outcome this step
# exists to rule out (Gitea #156). The Super Productivity half is nightly, in
# `tracker-live.yml`; see instructions/dev/tracker-testing.md.
env:
CHEMENU_LIVE_REQUIRE: caldav
run: |
set -eu
tools/.venv/bin/pip install --quiet radicale
.gitea/scripts/start-radicale.sh tools/.venv/bin/python /tmp/radicale
cd tools
.venv/bin/python -m pytest -q -m live_tracker -k caldav -s
- name: Coverage report
# `always()`: a red suite is exactly when the per-module numbers are
# worth reading, and the upload must not disappear with the failure.
+139
View File
@@ -0,0 +1,139 @@
# Builds the image the nightly `tracker-live` run executes in: Debian, the packaged Super
# Productivity, a virtual display and the tools the suite needs (Gitea #156). It lives in
# this Gitea instance's registry as `gitea.nehmer.net/torben/chemenu-sp-live`.
#
# The image follows the update channel, not a pin. Installed desktop apps update themselves,
# so a pinned old version would be tested while users already run the new one. Every day this
# workflow asks `latest-linux.yml` (`.gitea/sp-live/resolve-version.sh`) which release is
# current, and builds only when the registry does not hold that tag yet. It also rebuilds once
# a month regardless, so the Debian layers behind the app do not age unnoticed.
#
# Tags: `:<sp-version>` always, `:latest` only when that version is what the channel says.
# A manual run with `sp_version` builds an older release (to reproduce a red night against
# the version it went red on) and therefore never moves `:latest`.
#
# Runner shape follows torben/gitea-mcp, `.gitea/workflows/binford-release.yaml`: the
# `container-builder` label, a remote BuildKit on the runner host, and the registry login from
# 1Password. `OP_SERVICE_ACCOUNT_TOKEN` is a user-level secret that covers `torben/*`.
#
# After the very first push the package has to be linked to this repository once, by hand, in
# the Gitea UI - a step no workflow can do. Until then the image builds and pulls fine; only
# the package page shows no repository.
name: SP live image
on:
schedule:
# 04:10 UTC, an hour after `nightly` and well before `tracker-live` (05:00), so a new
# release is in the registry by the time the suite looks for it.
- cron: '10 4 * * *'
workflow_dispatch:
inputs:
sp_version:
description: 'Super Productivity release to build (default: the current one)'
required: false
force:
description: 'Rebuild even if the tag already exists (true/false)'
required: false
default: 'false'
env:
REGISTRY: gitea.nehmer.net/torben
IMAGE_NAME: chemenu-sp-live
jobs:
build-and-push:
runs-on: container-builder
container:
image: debian:trixie-slim
steps:
- name: Install CI dependencies
# `nodejs` is for act_runner's JavaScript actions, not for us - see ci.yml.
run: |
set -eu
apt-get update -qq
apt-get install -y --no-install-recommends \
git nodejs curl docker-cli docker-buildx ca-certificates iproute2 gawk
- uses: actions/checkout@v7
- name: Resolve the Super Productivity release
id: sp
env:
REQUESTED: ${{ inputs.sp_version }}
run: |
set -eu
channel="$(.gitea/sp-live/resolve-version.sh latest)"
channel_version="$(printf '%s\n' "$channel" | sed -n 's/^version=//p')"
if [ -n "${REQUESTED:-}" ]; then
wanted="$(.gitea/sp-live/resolve-version.sh "$REQUESTED")"
else
wanted="$channel"
fi
{
printf '%s\n' "$wanted"
echo "channel_version=$channel_version"
} >> "$GITHUB_OUTPUT"
printf '%s\n' "$wanted"
- name: Load secrets from 1Password
uses: 1password/load-secrets-action@v2
with:
export-env: true
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
REGISTRY_USER: op://CI-CD/gitea-package-token/username
REGISTRY_PAT: op://CI-CD/gitea-package-token/password
- name: BuildKit setup (remote builder)
run: |
HOST_IP=$(ip route | awk '/default/ { print $3 }')
docker buildx create --name remote-builder --driver remote tcp://$HOST_IP:1234 --use --bootstrap
- name: Log in to the container registry
run: |
echo "$REGISTRY_PAT" | docker login gitea.nehmer.net -u "$REGISTRY_USER" --password-stdin
- name: Decide whether to build
id: decide
env:
SP_VERSION: ${{ steps.sp.outputs.version }}
CHANNEL_VERSION: ${{ steps.sp.outputs.channel_version }}
FORCE: ${{ inputs.force }}
run: |
set -eu
ref="$REGISTRY/$IMAGE_NAME:$SP_VERSION"
build=false
why=""
if [ "${FORCE:-false}" = true ]; then
build=true; why="forced"
elif [ "$(date -u +%d)" = 01 ]; then
build=true; why="monthly rebuild"
elif ! docker buildx imagetools inspect "$ref" > /dev/null 2>&1; then
build=true; why="$ref is not in the registry yet"
fi
tags="$ref"
if [ "$SP_VERSION" = "$CHANNEL_VERSION" ]; then
tags="$tags
$REGISTRY/$IMAGE_NAME:latest"
fi
{
echo "build=$build"
echo "tags<<EOF"
echo "$tags"
echo "EOF"
} >> "$GITHUB_OUTPUT"
echo "build=$build ${why:+($why)}; tags: $tags"
- name: Build and push
if: steps.decide.outputs.build == 'true'
uses: docker/build-push-action@v6
with:
context: .gitea/sp-live
file: .gitea/sp-live/Dockerfile
platforms: linux/amd64
push: true
tags: ${{ steps.decide.outputs.tags }}
build-args: |
SP_VERSION=${{ steps.sp.outputs.version }}
SP_SHA512=${{ steps.sp.outputs.sha512 }}
+69
View File
@@ -0,0 +1,69 @@
# The live tracker suite, nightly (Gitea #156): the documented `task` and `review` workflow
# against a real Super Productivity and a real CalDAV server, not against fakes.
#
# `ci.yml` already runs the CalDAV half on every push (Radicale is a pip install). This
# workflow adds the half that needs the desktop app, inside the prebuilt
# `chemenu-sp-live` image (`sp-live-image.yml`), and runs both, so one green night covers both
# providers.
#
# Red after a new Super Productivity release is the finding this workflow exists for, not a
# flaky night: `instructions/dev/tracker-testing.md` says what to do with it. An agent that
# touched the Super Productivity surface (`tasks/superproductivity.py`, `tasks/config.py`,
# the fixtures) dispatches it by hand instead of waiting for the clock.
#
# Runner: `linux-docker`, with the image as the job container. The image carries `nodejs`, so
# `actions/checkout` runs; see ci.yml for why that is the workflow's business.
name: Tracker live
on:
schedule:
- cron: '0 5 * * *'
workflow_dispatch:
jobs:
live:
runs-on: linux-docker
container:
image: gitea.nehmer.net/torben/chemenu-sp-live:latest
env:
WIKITOOL_SESSION_ID: tracker-live-${{ github.run_id }}
WIKI_TRACE_DIR: /tmp/wikitool-trace
steps:
- uses: actions/checkout@v7
- name: Which Super Productivity is this
# The image tag says which release it was built for; whether `:latest` was pulled
# fresh or served from the runner's cache is not certain. So the run compares what is
# installed with what the update channel names now, and says so in the log.
run: |
set -eu
installed="$(dpkg-query -W -f='${Version}' superproductivity)"
channel="$(.gitea/sp-live/resolve-version.sh latest | sed -n 's/^version=//p')"
echo "installed: $installed, update channel: $channel"
if [ "$installed" != "$channel" ]; then
echo "::warning::the image carries Super Productivity $installed, the update channel names $channel - this run tests an outdated app (stale runner cache or an image not rebuilt yet)"
fi
- name: Tool environment
run: |
set -eu
git config --global --add safe.directory "$GITHUB_WORKSPACE"
python3 -m venv tools/.venv
tools/.venv/bin/pip install --quiet --upgrade pip
tools/.venv/bin/pip install --quiet -r tools/requirements.txt
tools/.venv/bin/pip install --quiet pytest radicale
- name: Start Radicale
run: .gitea/scripts/start-radicale.sh tools/.venv/bin/python /tmp/radicale
- name: Live tracker suite
# Both kinds are required: a night in which the app or the server was not there must
# fail rather than skip.
env:
CHEMENU_LIVE_REQUIRE: sp,caldav
run: |
set -eu
cd tools
.venv/bin/python -m pytest -q -m live_tracker -s