Files
daily-blackout-check/README.md
Meghdad b8d27a3298
All checks were successful
Build runner image / build (push) Successful in 17s
Test blackout notifier / test (push) Successful in 3s
Hourly blackout check / check (push) Successful in 4s
Use Gitea-hosted Node base image
2026-07-17 21:32:20 +03:30

167 lines
6.1 KiB
Markdown

# Daily Blackout Check
Checks the official SAAPA planned-outage API and sends Persian notifications to
an Eitaa chat when an outage is added, changed, cancelled, or restored.
The checker is designed for an hourly Gitea Actions workflow. Its deduplication
state is stored in `state/outages.json` and committed back to this repository.
## Behavior
- Queries each configured bill ID from today through five days ahead.
- Uses Tehran time regardless of the runner container's timezone.
- Sends all upcoming outages when starting with empty state.
- Treats an existing outage number with changed time, address, date, reason, or
planned status as an update.
- Reports a future outage as cancelled only after two consecutive successful
checks omit it.
- Records a notification only after Eitaa accepts it. This favors delivery over
perfect deduplication: a failed state push can cause a duplicate next hour.
- Rejects malformed responses and snapshots larger than the configured safety
limit without changing that bill's state.
## Local setup
Python 3.11 or newer is required. The Gitea runner image uses Debian's Python 3.11.
```sh
python -m venv .venv
source .venv/bin/activate
python -m pip install -e '.[test]'
cp .env.example .env
```
Fill in the required values in `.env`, then preview the next run without sending
messages or changing state:
```sh
python -m blackout_notifier check --dry-run
```
Run the live checker with:
```sh
python -m blackout_notifier check
```
## Configuration
| Variable | Required | Default | Purpose |
| --- | --- | --- | --- |
| `BARGHEMAN_TOKEN` | Yes | — | SAAPA bearer token |
| `EITAAYAR_TOKEN` | Yes | — | EitaaYar bot token |
| `CHAT_ID` | Yes | — | Destination Eitaa chat |
| `BILL_IDS` | Yes | — | Comma-separated decimal bill IDs |
| `STATE_FILE` | No | `state/outages.json` | Durable notification state |
| `LOOKAHEAD_DAYS` | No | `5` | Query horizon, from 1 to 30 days |
| `REQUEST_TIMEOUT_SECONDS` | No | `15` | HTTP timeout, from 1 to 120 seconds |
| `MAX_EVENTS_PER_BILL` | No | `25` | Response safety limit, from 1 to 100 |
Shell and Gitea-provided variables take precedence over `.env`.
## Gitea deployment
This repository targets Gitea 1.25.x and a Docker-based runner advertising the
`ubuntu-latest` label. The test and hourly workflows pull this private image from
the Gitea Container Registry:
```text
mahgit.ir/meghdadfadaee/daily-blackout-check-runner:latest
```
It contains Node for the checkout action plus Python, Git, timezone data,
application dependencies, pytest, and Ruff. Scheduled jobs therefore do not
install Python or download Python packages.
The Node base image is also mirrored in Gitea, so image builds do not contact
Docker Hub:
```text
mahgit.ir/meghdadfadaee/daily-blackout-check-base:node-20-bookworm
```
### Registry credentials
Create a Gitea personal access token for `MeghdadFadaee` with package permission
set to **Read and Write**. Add these repository Actions secrets:
| Secret | Value |
| --- | --- |
| `REGISTRY_USERNAME` | Gitea username that owns the package |
| `REGISTRY_TOKEN` | Personal access token with package Read and Write permission |
| `BARGHEMAN_TOKEN` | SAAPA bearer token |
| `EITAAYAR_TOKEN` | EitaaYar bot token |
| `CHAT_ID` | Destination chat ID |
| `BILL_IDS` | Comma-separated bill IDs |
The registry token is used to publish the image and as `container.credentials`
when Gitea Runner pulls the private image before starting a job.
### Runner configuration
The image-build workflow needs access to the Docker daemon used by `act_runner`.
In the runner's generated `config.yaml`, update the existing `container` section:
```yaml
container:
force_pull: true
valid_volumes:
- /var/run/docker.sock
```
The runner container itself must already mount the same socket, as in the normal
Docker-based `act_runner` setup. Restart the runner after changing its config.
Docker socket access is equivalent to control of the Docker host. Use a trusted,
preferably repository-level runner. The build workflow runs only for changes on
`main` under `runner-image/` or its own workflow file; it never runs for pull
requests.
### Image publishing flow
The **Build runner image** workflow:
1. Starts in the Gitea-hosted `daily-blackout-check-base:node-20-bookworm`
image and installs the Docker client.
2. Builds using only `runner-image/` as context, so `.env` and outage state are
never included.
3. Runs import, pytest, and Ruff smoke checks inside the candidate image.
4. Pushes an immutable tag using the first 12 characters of the Git commit.
5. Updates `latest` only after the candidate passes verification.
It runs automatically when `runner-image/**` changes on `main` and can also be
started manually. Image builds require Debian mirrors, PyPI, Gitea, and access
to the Docker socket. Normal test and hourly runs only need Gitea, `gitea.com`,
SAAPA, and EitaaYar. The mirrored Node image is intentionally static; refresh it
from the upstream `node:20-bookworm` image when adopting Node security updates.
Deployment order:
1. Enable Packages and Actions for the repository/owner.
2. Configure the runner socket permission and restart it.
3. Add all repository Actions secrets listed above.
4. Run **Build runner image** manually and confirm both registry tags appear.
5. Run **Test blackout notifier** manually.
6. Run **Hourly blackout check** manually once and inspect the notification and
resulting state commit.
7. Leave the `@hourly` schedule enabled.
The workflow uses fully qualified `https://gitea.com/actions/...` actions rather
than resolving actions through GitHub. Gitea 1.25 ignores workflow `permissions`
and `concurrency`, so the notifier workflow deliberately does not rely on them.
Its checkout credential must retain the default ability to push to the current
repository. Scheduled jobs should not be manually overlapped; in the rare event
of a push race, the job fails and unsaved notices may be repeated on the next run.
## Development
```sh
ruff check .
pytest
```
Tests mock both external services and never send real messages.
Historical state from the previous implementation is retained at
`archive/blackouts-1404.json` for reference only and is not imported.