131 lines
5.3 KiB
Markdown
131 lines
5.3 KiB
Markdown
---
|
||
type: guide
|
||
title: Daily Run Guide
|
||
description: Operator- and tester-focused walkthrough of one daily cron tick — repository synchronization, anniversary matching, deduplication, logging, and retry behaviour.
|
||
tags: [guide, run, daily, tester]
|
||
timestamp: 2026-08-04T17:51:00Z
|
||
---
|
||
|
||
# Purpose
|
||
|
||
A tester or operator needs to know **what should happen** each time
|
||
the cron tick fires. This guide describes the contract from the
|
||
outside of the container, in observable terms: log lines, exit codes,
|
||
files appearing on the volume, and the "silent on no matches"
|
||
behaviour that the runner enforces.
|
||
|
||
# Triggering a Run Manually
|
||
|
||
```bash
|
||
docker compose exec tenbackward /usr/local/bin/python -m tenbackward
|
||
```
|
||
|
||
This is the same command the container's cron entry issues once per
|
||
day at `RUN_AT`. Use it to verify behaviour without waiting for the
|
||
scheduled tick.
|
||
|
||
# What the Run Does
|
||
|
||
Before scanning, the process ensures the configured blog repository exists
|
||
under `/app/data/blog` and is fast-forwarded from `BLOG_REPO_URL`. It then
|
||
walks `_posts/blog/**/*.md`, finds posts whose date is exactly ten years
|
||
before today, skips IDs already present in `posted.json`, and records new
|
||
relative paths. The current implementation records matching IDs only;
|
||
Mastodon publication is not yet wired.
|
||
|
||
# What You Should See
|
||
|
||
The bot uses a **JSON-per-line** logger that writes to
|
||
`/app/data/cron.log` (because the cron line redirects with
|
||
`>> /app/data/cron.log 2>&1`). Inspecting the file with
|
||
`docker compose exec tenbackward tail -n 100 /app/data/cron.log`
|
||
should show output similar to:
|
||
|
||
```json
|
||
{"ts": "2026-08-04T09:00:00+00:00", "level": "INFO", "logger": "tenbackward", "message": "startup", "event": "startup", "version": "0.1.0", "site_url": "https://blog.example.com", "run_at": "09:00", "tz": "Europe/Berlin", "hashtags": "#throwback,#10backward", "throwback_prefix": "Throwback:"}
|
||
```
|
||
|
||
On the current scaffold (`_iter_candidates` is intentionally empty),
|
||
this is the **only** line you should see — the runner emits no
|
||
`run_complete` when no candidates were found. This is the
|
||
"silent on no matches" contract.
|
||
|
||
# Expected Container Behaviour
|
||
|
||
| Scenario | Lines in `cron.log` | Container exit |
|
||
|-------------------------------------------|----------------------------------------------------------------|------------------|
|
||
| Config valid, no anniversary matches | 1× `startup` | `0` |
|
||
| Blog clone or pull fails | 1× `blog_repo_error` | `1` |
|
||
| Config valid, matching IDs are new | 1× `startup`, then 1× `run_complete` with `posted=N` | `0` |
|
||
| Config valid, all matches already posted | 1× `startup`, then 1× `run_complete` with `posted=0` | `0` |
|
||
| Pipeline raises once, recovers | 1× `pipeline_error`, then 1× `run_complete` | `0` |
|
||
| Pipeline keeps raising (budget exhausted) | `MAX_RETRIES + 1` × `pipeline_error`, then 1× `pipeline_failed` | `1` |
|
||
| Config invalid | 1× `configuration_error` (extras describe what failed) | `2` |
|
||
|
||
> **Tester tip** — `docker compose ps` should report the container as
|
||
> `running` after a `pipeline_failed` exit *only* if cron has not yet
|
||
> fired again. A single failed pipeline tick does not kill the container
|
||
> itself; cron re-runs it the next day.
|
||
|
||
# Files the Tester Should Look For
|
||
|
||
| Path (inside container) | When it appears |
|
||
|-------------------------|--------------------------------------------|
|
||
| `/app/data/cron.log` | Always (cron appends stdout/stderr here). |
|
||
| `/app/data/posted.json` | Created when the matcher or a state check first loads the store; rewritten only when a new ID is posted. The file contains relative Jekyll paths under a `posted` list. |
|
||
|
||
To verify these from the host:
|
||
|
||
```bash
|
||
docker compose exec tenbackward ls -la /app/data
|
||
docker compose exec tenbackward cat /app/data/cron.log
|
||
```
|
||
|
||
# Visual Elements
|
||
|
||
This is a server-side bot, so there is no UI. The "UI" consists of:
|
||
|
||
* **Log lines on stdout** — one JSON object per `INFO`/`ERROR` event.
|
||
* **Exit code** — `0` (healthy), `1` (pipeline exhausted), or `2` (config).
|
||
* **`posted.json`** — JSON state file; its mtime changes whenever a new id is persisted.
|
||
* **`/app/data/blog`** — synchronized local clone containing the Jekyll posts scanned for anniversaries.
|
||
|
||
# Examples
|
||
|
||
## Healthy scaffold tick
|
||
|
||
```bash
|
||
$ docker compose exec tenbackward python -m tenbackward
|
||
{"ts":"...","event":"startup",...}
|
||
$ echo $?
|
||
0
|
||
```
|
||
|
||
## Bad `RUN_AT`
|
||
|
||
```bash
|
||
$ RUN_AT=25:99 docker compose exec tenbackward python -m tenbackward
|
||
ERROR: RUN_AT='25:99' must be in HH:MM (24-hour) format
|
||
$ docker compose ps
|
||
tenbackward Exit 1
|
||
```
|
||
|
||
(The error message lands on stderr in the entrypoint path and is
|
||
captured by `cron.log`.)
|
||
|
||
## Missing required env var
|
||
|
||
Unset `MAX_RETRIES`, restart the container:
|
||
|
||
```bash
|
||
$ docker compose up --build
|
||
tenbackward | ERROR: missing required environment variable(s): MAX_RETRIES
|
||
tenbackward exited with code 1
|
||
```
|
||
|
||
# Related
|
||
|
||
* [Environment Variable Setup](/operations/environment-setup.md)
|
||
* [Pipeline Runner](/architecture/pipeline-runner.md)
|
||
* [Logging & Run Summary](/architecture/logging.md)
|