Files
grm/docs/tech/decision-log.md
T
emil f712a4493e
Post-merge / detect-and-configure (push) Waiting to run
Post-merge / release-and-maintain (push) Waiting to run
GRM-152: fix: fetch rootless Docker scripts on Arch Linux
2026-07-16 15:56:59 +00:00

11 KiB

Decision Log

Key technical decisions for the GRM project, extracted from CHANGELOG.md and AGENTS.md.


ADR-001: Dynamic Versioning via __init__.py

Date: 2026-06-21 (v0.2.0 unreleased)

Decision: Use dynamic = ["version"] in pyproject.toml with setuptools attr to source the version from __version__ in src/grm/__init__.py.

Rationale: __init__.py is the single source of truth for the version. The release script (devx.ci.release) only updates __init__.py — there is no need to touch pyproject.toml. grm --version reports this version directly. This eliminates version duplication across files and ensures the runtime version always matches the tagged release.

Source: CHANGELOG.md (Unreleased — Added), AGENTS.md (Version Bumping Rules)


ADR-002: Rootless Docker per Runner

Date: Project inception (documented in README Architecture)

Decision: Each runner instance runs in an isolated rootless Docker environment under a dedicated system user (grm-<name>), with its own Docker socket at /run/user/<UID>/docker.sock.

Rationale: Rootless Docker per-runner avoids conflicts with the host's Docker installation and enables true parallel execution of multiple runners on the same host. Each instance has fully isolated resources: user, home, data directory, config directory, systemd user service, and Docker socket. This is a core feature of GRM — enabling multiple isolated runners on the same host. User namespace mapping is configured via /etc/subuid and /etc/subgid entries (range: 100000-165535). Lingering is enabled so the user's systemd services run without an active login session.

Source: README.md (Architecture, Features), AGENTS.md (Architecture)


ADR-003: Conventional Commits + git-cliff for Automated Versioning

Date: 2026-06-21 (v0.2.0 unreleased)

Decision: Use conventional commits on feature branches and git-cliff (cliff.toml) to calculate the next semver version from commit history, generate the changelog, and automate releases.

Rationale: devx.ci.release uses git-cliff to calculate the next version from conventional commits since the last tag. Merge commits on master have the format GRM-N <conventional commit>, so cliff.toml includes a commit_preprocessors entry that strips the GRM-N prefix before parsing. Version bumping rules: feat: → minor, fix: → patch, feat!:/BREAKING CHANGE → minor (pre-1.0), chore:/ci:/docs: → no bump. This fully automates versioning and changelog generation — no manual version bumps are needed.

Source: CHANGELOG.md (Unreleased — Added), AGENTS.md (Automated Release Pipeline, git-cliff Commit Preprocessing, Version Bumping Rules), cliff.toml


ADR-004: Enforce Tests Pass Before Tagging a Release

Date: 2026-06-21 (v0.2.2)

Decision: The release step in the release-and-maintain job runs make lint-ruff and make pytest-cov before creating a release commit or tag. If lint or tests fail, the release aborts immediately — no commit, no tag.

Rationale: This ensures every tagged release is healthy. A --skip-tests flag exists for emergency use only but is not recommended. This decision was made as a bug fix after identifying that releases could be tagged without verifying test health. Loops are prevented by has_unreleased_changes — after a release commit is tagged, the next run finds no unreleased changes and exits.

Source: CHANGELOG.md (0.2.2 — Bug Fixes: "Enforce tests pass before tagging a release"), AGENTS.md (Automated Release Pipeline)


ADR-005: Branch Protection + Auto-Merge Workflow

Date: 2026-06-21 (v0.2.0 unreleased)

Decision: Require branch protection on master (require pull request, require approval review, require status checks, block force pushes) and use an auto-merge workflow that programmatically enforces the APPROVE review check.

Rationale: Branch protection is the primary gate — no direct pushes to master, at least 1 APPROVE review before merge, CI validate + molecule tests must pass, and no history rewriting. The auto-merge workflow (devx.ci.auto_merge) enforces the APPROVE review check programmatically as a defense-in-depth measure. When the ready-to-merge label is added, the workflow validates PR title format, checks for APPROVE review, waits for CI, and squash-merges with title GRM-N <conventional commit message>. The post-merge workflow then marks the Vikunja task as done. Branch protection is automatically configured by the configure-repo step in the detect-and-configure job.

Source: CHANGELOG.md (Unreleased — Added: mandatory PR review step, auto_merge.py), AGENTS.md (Branch Protection, PR Workflow step 8)


ADR-006: Path-Based CI Filtering for Molecule Tests

Date: 2026-06-21 (v0.2.0 unreleased)

Decision: The CI workflow's validate job includes a detect-changes step that checks whether any files under ansible/ or .ansible-lint have changed. If no Ansible files are changed, molecule tests are skipped.

Rationale: This prevents non-Ansible changes (e.g., Python scripts, workflow YAML, docs) from being blocked by molecule test infrastructure flakiness. Molecule tests are only relevant when Ansible files change. The molecule-tests job depends on the validate job (which includes the detect-changes step), and only runs if ansible-changed == 'true'. CI triggers only on opened and synchronize PR events (not labeled) to avoid redundant runs.

Source: AGENTS.md (CI Path Filtering), .gitea/workflows/ci.yml (detect-changes step in the validate job)


ADR-007: Secrets via Temporary JSON Files (CWE-214)

Date: Project inception

Decision: Pass secrets (registration tokens, admin API tokens) to Ansible via temporary JSON files with 0600 permissions, never on the command line.

Rationale: Passing secrets as command-line arguments (e.g., --extra-vars '{"token": "..."}') makes them visible in the process list (ps aux), which is a known security weakness (CWE-214). The RunnerManager._extra_vars_file() context manager writes extra-vars to a temporary file via tempfile.mkstemp(), sets permissions to 0600, passes the file to Ansible via --extra-vars @tempfile, and deletes the file in a finally block — even if an exception occurs. This ensures secrets are never visible in the process list.

Source: AGENTS.md (Key Conventions), src/grm/runner_manager.py (_extra_vars_file method)


ADR-008: Smart CI — User-Facing vs Workflow-Only Change Classification

Date: 2026-06-21 (v0.2.0 unreleased)

Decision: Classify changed files into user-facing and workflow-only categories using devx.ci.classify_changes. Only user-facing changes trigger a release; workflow-only changes (CI, docs, tests, lint config) do not.

Rationale: Not all changes require a new release. CI workflow updates, documentation improvements, and test additions should not produce a new version tag. The classification is config-driven via [tool.devx.classify] in pyproject.toml. The strategy is safe-by-default: any file NOT in the explicit workflow-only allowlist is treated as user-facing, preventing new file types from accidentally skipping releases. User-facing paths include src/grm/** (except __init__.py) and ansible/**. Workflow-only paths include .gitea/**, docs/**, tests/**, scripts/**, and other config files.

Source: AGENTS.md (Smart CI: User-Facing vs Workflow-Only Changes), pyproject.toml ([tool.devx.classify])


ADR-009: devx Package Separation

Date: 2026-06-21 (v0.6.2)

Decision: Separate CI/CD and development tooling into the devx package (installed from git), keeping the GRM tool itself self-contained in src/grm/.

Rationale: The GRM CLI tool must be self-contained — it never imports from devx. This ensures the installed package has no dependency on CI infrastructure. devx MAY import from grm (one-way dependency), as it uses the tool's API clients, config, and i18n for CI automation. Cross-module imports within devx are allowed. This separation was formalised when scripts were migrated from the scripts/ directory to the devx package in GRM-64.

Source: AGENTS.md (Source Code Separation and devx Integration), CHANGELOG.md (0.6.2 — Refactor: "Migrate from scripts/ to devx package")


ADR-010: Dynamic Runner Discovery for Molecule CI

Date: 2026-06-21 (v0.5.0+)

Decision: Molecule tests are distributed across available Gitea Actions runners dynamically via devx.molecule.discover_runners, which queries the Gitea API for runners at all levels (repo, org, instance) and generates a dynamic matrix.

Rationale: Hardcoding the number of CI runners would require manual updates when runners are added or removed. Dynamic discovery auto-detects repo/org-level runners via the API. For instance-level runners (which may not be visible without admin scope), it falls back to the MOLECULE_RUNNERS repo variable, then to a default of 3. The workflow automatically scales the matrix to match available runners, distributing test pairs evenly.

Source: AGENTS.md (Dynamic Runner Discovery), .gitea/workflows/ci.yml (discover-runners step in the validate job)


ADR-011: Fetch Rootless Docker Scripts from moby/moby on Arch Linux

Date: 2026-07-16 (v0.19.0)

Decision: On Arch Linux, fetch the rootless Docker setup scripts (dockerd-rootless-setuptool.sh and dockerd-rootless.sh) from the upstream moby/moby contrib/ directory at a pinned git ref (gitea_runner_rootless_scripts_ref, default v28.5.1) and install them into /usr/bin. Install the rootlesskit package explicitly.

Rationale: Arch's docker package does not ship the rootless setup scripts (unlike Debian/Ubuntu's docker-ce-rootless-extras), and no official Arch package provides them (pkgfile confirms zero providers). rootlesskit — the required userspace networking/namespace driver — is also not a dependency of the docker package and must be installed explicitly. Without these, grm install fails with No such file or directory: dockerd-rootless-setuptool.sh. The scripts are installed into /usr/bin (not /usr/local/bin) because dockerd-rootless-setuptool.sh derives its BIN directory from its own location and expects docker, dockerd, and rootlesskit to be co-located. The scripts are stable, version-agnostic bash wrappers, so a pinned ref is safe across dockerd versions; the ref is overridable via gitea_runner_rootless_scripts_ref. This bug was not caught by CI because molecule scenarios run with gitea_runner_docker_rootless_setup: false (rootless daemon startup needs kernel userns unavailable in CI containers), so the Arch rootless path was never exercised — the script-fetch tasks now run regardless of that flag to provide URL-validation coverage.