134 lines
11 KiB
Markdown
134 lines
11 KiB
Markdown
# 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.
|