Files
computer-playbook/docs/contributing/artefact/files/github/workflow.md
T
kevinveenbirkenbachandClaude Opus 5.5 2d7bf740b0 fix(ci): put containerd, Docker and swap on the runner's empty spare disk
The small GitHub runner shape (AWS, 72G root) ran out of disk in swarm and compose rows (#1329: WordPress#2 of run 41261531965, Nextcloud#6 and #7 of run 41262286962) while an empty instance-store disk nvme1n1 sat unused. mount_spare_disk.sh formats and mounts it at /mnt, but only on a GitHub-hosted runner outside act, and only a whole unpartitioned disk without any signature, mount or swap use of at least 75G; every decline leaves a notice. relocate_docker_dataroot.sh then bind-mounts /var/lib/containerd from the spare disk and moves the Docker data-root to /mnt/docker-host (not /mnt/docker, which is the nested daemon's volume that dedicated.sh wipes). It refuses before stopping anything when images, volumes, build cache, foreign containerd namespaces or shims exist or daemon.json is unreadable, and rolls back the bind mount and daemon.json on any later failure or cancel. Swap goes on / once containerd lives on /mnt, dump_state.sh logs disk size, model and free space per storage root, and the swarm watchdog samples / and /var/lib/containerd every 10 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 12:31:02 +01:00

142 lines
9.4 KiB
Markdown

# GitHub workflow files 🔄
This page covers the repository rules that govern GitHub Actions workflow files under `.github/workflows/`.
For the catalog of every workflow (description, triggers, inputs) see [workflows.md](../../../tools/github/actions/workflows.md).
For the script placement rule that applies to extracted shell helpers, see [scripst.md](../../scripst.md).
## Naming 🏷️
Every workflow MUST follow the schema `"[Emoji] Category: Subject (Qualifier)"`, except a scheduled one — see [Scheduled runs](#scheduled-runs-).
- You MUST quote the `name:` value in double quotes because the colon (`:`) in the name is a reserved YAML character.
- You MUST place the emoji before the category, never after.
- You MUST NOT add emojis to `docs/agents/` files, but workflow `name:` fields are not agent files and MUST use emojis.
- The qualifier in parentheses is OPTIONAL. Use it only when two workflows share the same category and subject.
- A workflow with a `workflow_dispatch` trigger MUST keep its `name:` at or below 25 characters, enforced by [test_workflow_dispatch_name_length.py](../../../../../tests/lint/repository/test_workflow_dispatch_name_length.py). The dispatch menu lists those names in one narrow column, where a longer one is truncated. Move the explanatory wording into `run-name:`, which carries no such limit.
### Emoji legend 📋
| Emoji | Category | Used for |
|---|---|---|
| `🔄` | Update / Sync | Automated version or dependency updates |
| `🪞` | Mirror | Image mirroring between registries |
| `🧹` | Cleanup / Images | Repository cleanup, image cleanup, and pruning |
| `🐳` | Build | Docker image builds |
| `🔍` | Lint | Static analysis and linting |
| `🔒` | Scan | Security scanning |
| `⚡` | CI | CI entry points (push, pull request, manual) |
| `🎵` | CI | CI orchestration and coordination workflows |
| `🚫` | Cancel | Run cancellation on PR close, branch delete, or a superseding commit |
| `🧪` | Test | Code tests (unit, integration, lint) |
| `💻` | Test | Development environment tests |
| `💬` | Test | DNS and network resolution tests |
| `📦` | Test | Deployment tests |
| `📥` | Test | Installation tests |
| `🚀` | Release | Version release workflows |
### Examples ✅
```yaml
name: "🧪 Test: Code (Integration)"
name: "🪞 Mirror: Docker Hub → GHCR (only missing)"
name: "🚫 Cancel: PR Runs on Close"
```
### Scheduled runs ⏰
A scheduled workflow (`cron-*.yml`) drops the `Category:` schema: its `name:` MUST read as one short sentence saying what the run does, because the Actions list shows scheduled runs next to each other with no PR or branch to tell them apart.
It MUST NOT carry ⏰ in its `name:`, and MUST carry a `run-name:` that prefixes ⏰ only when the schedule started the run, so a dispatched run is not mislabelled as scheduled:
```yaml
name: "🔄 Update versions"
run-name: "${{ github.event_name == 'schedule' && '⏰ ' || '' }}🔄 Update versions"
```
Enforced by [test_workflow_trigger_prefix.py](../../../../../tests/lint/repository/test_workflow_trigger_prefix.py), which also owns the `entry-` / `call-` / `cron-` file-name rule.
## Shell execution 📜
- Multi-line shell logic in workflow `run:` blocks MUST be extracted into dedicated `.sh` files under `scripts/`.
- Workflow files MUST call those extracted `.sh` entry points instead of embedding longer shell programs inline.
- Short single-command invocations MAY stay inline when they do not contain meaningful control flow.
- Inline shell in workflow files SHOULD stay limited to small command calls, environment wiring, or direct script invocation.
## Disk space 💾
Deploy test workflows use the `jlumbroso/free-disk-space` action to reclaim runner space before Docker pulls start. Because the actual deploy runs **inside** the `infinito` container, the host's language toolchains, build packages, and default Docker layer cache are all dead weight and MUST be reclaimed aggressively.
| Option | Value | Reason |
|---|---|---|
| `tool-cache` | `true` | Deploy runs inside the `infinito` container; host Node/Python/Go/Ruby toolchains go unused |
| `android` | `true` | Android SDK is never needed; safe to remove |
| `dotnet` | `true` | .NET SDK is never needed; safe to remove |
| `haskell` | `true` | Haskell toolchain is never needed; safe to remove |
| `large-packages` | `true` | Ansible, pip and gcc run **inside** the container, not on the host |
| `docker-images` | `true` | Matrix jobs don't share a Docker layer cache; each runner pulls its own `infinito` image |
| `swap-storage` | `true` | Default swap file is replaced by `pierotofy/set-swap-space` (see below) |
Set an option back to `false` only when a new host-side step in the same workflow genuinely needs the removed payload.
## Spare disk 💽
Some GitHub-hosted runners have a 72 GB root disk plus a second, unformatted NVMe disk. [mount_spare_disk.sh](../../../../../scripts/github/runner/mount_spare_disk.sh) formats that disk as ext4 and mounts it at `/mnt`. The nested Docker volume (`/mnt/docker`) and the loop image of [docker_dataroot.sh](../../../../../scripts/tests/deploy/utils/filesystem/docker_dataroot.sh) (`/mnt/docker-fs.img`) then land on it.
When `/mnt` has at least `MIN_FREE_GB` free and more than `/`, [relocate_docker_dataroot.sh](../../../../../scripts/github/runner/relocate_docker_dataroot.sh) stops Docker and containerd, bind-mounts `/mnt/containerd` over `/var/lib/containerd` (images, container layers, the swarm node containers), sets the Docker data-root to `/mnt/docker-host` (named volumes), restarts both and verifies that containerd's metadata and the Docker root sit on `/mnt`. Before the stop it fails if Docker already holds containers, images, volumes or build cache, if another containerd namespace holds containers or tasks, if containerd shims run, or if `daemon.json` cannot be read as JSON. Any failure after the stop, and a cancel, unmounts the bind, restores `daemon.json`, restarts both daemons and fails the step. It refuses under act and changes nothing on a runner that is not GitHub-hosted. Every decline prints a `::notice::` annotation. [run.py](../../../../../utils/tests/swarm/run.py) then watches the free space of both `/` and `/var/lib/containerd`.
Step order: spare disk, relocation, swap. With containerd on `/mnt`, the swapfile goes to `/`.
The script formats a disk only when all of these hold; otherwise the runner stays as it is:
- `ACT` is unset (the script fails under act) and `RUNNER_ENVIRONMENT` is `github-hosted`.
- `/mnt` is an existing, empty directory and no mount.
- The disk is a whole disk other than the root disk, with no partitions or holders, no filesystem or partition-table signature (`lsblk`, `blkid -p` and `wipefs`), no mount or swap use, not read-only, and at least `MIN_SIZE_GB` large.
Workflow step invocations, in this order and gated alike:
```yaml
- name: Mount the empty spare disk at /mnt
if: ${{ !env.ACT }}
shell: bash
run: ./scripts/github/runner/mount_spare_disk.sh /mnt 75
- name: Relocate containerd and the Docker data-root onto /mnt when roomier
if: ${{ !env.ACT }}
shell: bash
run: ./scripts/github/runner/relocate_docker_dataroot.sh 75
```
## Swap 💾
Deploy test workflows enlarge host swap via [enlarge_swap.sh](../../../../../scripts/github/runner/enlarge_swap.sh) to absorb transient memory spikes (e.g. PeerTube plugin install [#162](https://github.com/infinito-nexus/core/issues/162)) that would otherwise trip the host OOM-killer on the 16 GB GitHub-hosted runner.
The script puts the swapfile on `/` when containerd already lives on `/mnt`, **prefers `/mnt`** otherwise when it is a separate partition (classic `ubuntu-latest` layout with `/dev/sdb` mounted at `/mnt`), and falls back to `/`. Current public runners no longer expose a separate `/mnt`, so the swapfile lands on `/` and must not starve the root filesystem.
Swap size is a **fixed constant**, not a "free space minus buffer" calculation. An earlier version took ~99 GB on a 145 GB root partition, leaving so little headroom that later Docker layer writes (Playwright test image pull) failed with `no space left on device`. A fixed cap is large enough to absorb the peertube spike and small enough that Docker layer writes keep succeeding throughout the 30+ min deploy.
| Argument | Default | Reason |
|---|---|---|
| `size-gb` (positional) | `16` | Empirically sufficient for the peertube memory spike ([#162](https://github.com/infinito-nexus/core/issues/162)); raise only when a workload is proven to need more |
Workflow step invocation:
```yaml
- name: Enlarge swap space
shell: bash
run: ./scripts/github/runner/enlarge_swap.sh
```
Ordering:
- Swap step MUST run **after** `actions/checkout` because the script lives inside the repo.
- Swap step SHOULD run **after** `jlumbroso/free-disk-space` so the reclaimed disk on `/` is also a candidate target when `/mnt` is crowded.
- Swap step MUST run **before** any `make compose-up` / container build step so the expanded swap is active when heavy-allocation work begins.
Swap is a host-kernel resource; see [svc-opt-swapfile](../../../../../roles/svc-opt-swapfile/) for why the in-stack swap role is intentionally skipped inside containers.
## Separation of concerns 🧩
- GitHub workflow YAML MUST describe orchestration, permissions, triggers, inputs, and step order.
- Reusable shell behavior MUST live in script files, not in repeated workflow `run:` blocks.
- Non-shell helper logic MUST NOT be embedded as ad-hoc shell blobs inside workflow files.