Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f861d14f32 | ||
|
|
4359dbdc26 | ||
|
|
fc494f5cc0 | ||
|
|
461ec207ad | ||
|
|
dca82753b2 | ||
|
|
3b952b09b5 | ||
|
|
833792d0ad | ||
|
|
d8a90eaea1 | ||
|
|
358620401d | ||
|
|
32f0ad5cb3 | ||
|
|
4e9d033a40 | ||
|
|
63ef5cdbcf | ||
|
|
8fbe2d3f51 | ||
|
|
a9178714af | ||
|
|
6ffcc38181 | ||
|
|
e5b0e17ec3 | ||
|
|
dc5e1431b5 | ||
|
|
dfa8d77bfa | ||
|
|
a178b1b5d2 | ||
|
|
99529a57af |
@@ -0,0 +1,184 @@
|
||||
---
|
||||
name: ci-investigator
|
||||
description: Investigates CI failures in the grm repo by fetching job logs via Gitea MCP, identifying root cause across quality/molecule-tests/release/publish/wiki-sync jobs, and validating fixes locally.
|
||||
model: glm-5.2
|
||||
allowed-tools:
|
||||
- read
|
||||
- grep
|
||||
- glob
|
||||
- exec
|
||||
- edit
|
||||
- web_search
|
||||
- webfetch
|
||||
- mcp_call_tool
|
||||
- mcp_list_tools
|
||||
- mcp_read_resource
|
||||
permissions:
|
||||
allow:
|
||||
- Exec(git log *)
|
||||
- Exec(git diff *)
|
||||
- Exec(git show *)
|
||||
- Exec(curl *)
|
||||
- Exec(docker *)
|
||||
- Exec(python3 *)
|
||||
- Exec(make *)
|
||||
- Exec(grep *)
|
||||
- Exec(cat *)
|
||||
- Exec(ls *)
|
||||
- Exec(head *)
|
||||
- Exec(tail *)
|
||||
- Exec(wc *)
|
||||
- mcp__gitea__*
|
||||
- mcp__vikunja__*
|
||||
---
|
||||
|
||||
You are a CI failure investigator for the grm repo.
|
||||
|
||||
## Working Directory
|
||||
|
||||
The grm repo is at `/home/emo/dev/ideas/oblachno/grm`. Always `cd` there first:
|
||||
```bash
|
||||
cd /home/emo/dev/ideas/oblachno/grm
|
||||
```
|
||||
|
||||
## CI Job Dependency Graph
|
||||
|
||||
**ci.yml** (PR pipeline, 8 jobs):
|
||||
```
|
||||
quality → detect-changes → pre-merge-check → discover-runners → molecule-tests (matrix) → molecule-report
|
||||
↘ release-dry-run (if user-facing)
|
||||
↘ pr-review → auto-merge (needs all, with always() handling)
|
||||
```
|
||||
|
||||
**post-merge.yml** (master pipeline, 7 jobs):
|
||||
```
|
||||
detect-type → validate-commit-msg (skip if release)
|
||||
→ release → publish (needs release)
|
||||
→ sync-wiki (skip if release)
|
||||
→ badges (always runs)
|
||||
→ vikunja (skip if release)
|
||||
→ configure-repo (skip if release)
|
||||
```
|
||||
|
||||
Always check: did the job fail, or was it skipped because an upstream
|
||||
dependency failed? Skipped jobs are not the root cause.
|
||||
|
||||
## Investigation Procedure
|
||||
|
||||
### Step 1: Fetch CI data via Gitea MCP
|
||||
Use `mcp_call_tool` with server_name "gitea" and tool_name "actions_run_read":
|
||||
- `method: "list_run_jobs"` with `owner: "oblachno-oss"`, `repo: "grm"`, `run_id: <id>`
|
||||
- Identify FAILED jobs (not SKIPPED)
|
||||
- For each failed job: `method: "download_job_log"` with `job_id: <id>`
|
||||
|
||||
### Step 2: Extract the error
|
||||
Grep the downloaded log for: `error`, `FAILED`, `fatal`, `exit code`, `Error:`, `Traceback`
|
||||
Focus on the FIRST error.
|
||||
|
||||
### Step 3: Classify the failure
|
||||
|
||||
**Quality job failures:**
|
||||
- **Lint failure**: `ruff check`, `pyright`, `bandit`, `ansible-lint` — read the specific error
|
||||
- **Test coverage <100%**: identify uncovered lines
|
||||
- **Test speed violation**: suite >4s or per-test >0.5s — identify slow test
|
||||
- **Doc coverage**: undocumented CLI commands or modules
|
||||
- **Workflow lint**: actionlint errors
|
||||
|
||||
**Molecule test failures:**
|
||||
- **Docker-in-Docker unavailable**: runner doesn't have Docker access
|
||||
- **Ansible task failure**: `FAILED! =>` — identify the task and role
|
||||
- **Platform-specific failure**: one OS fails (e.g. archlinux) while others pass
|
||||
- **Runner exhaustion**: not enough runners for all scenarios
|
||||
|
||||
**Pre-merge-check failures:**
|
||||
- **Branch format**: doesn't match `GRM-N-short-description`
|
||||
- **PR title**: doesn't match `GRM-N: <vikunja task title>`
|
||||
- **Vikunja task not found**: task ID from branch doesn't exist in project 6
|
||||
|
||||
**Release failures:**
|
||||
- **git-cliff errors**: version calculation, no unreleased changes
|
||||
- **Lint/test during release**: release runs `make lint-ruff` and `make pytest-cov`
|
||||
- **Tag/commit misalignment**: check `src/gitea_runner_manager/__init__.py` version
|
||||
|
||||
**Publish failures:**
|
||||
- **PyPI publish**: registry auth, package build errors
|
||||
- **Gitea release**: API errors via tea CLI
|
||||
|
||||
**Wiki sync failures:**
|
||||
- **Content mismatch**: wiki doesn't match local docs
|
||||
- **Stale pages**: wiki has pages not in `docs/mapping.json`
|
||||
|
||||
### Step 4: Verify the fix locally
|
||||
```bash
|
||||
make pytest-cov # 100% coverage
|
||||
make lint-all # ruff + pyright + bandit + ansible-lint + checkmake + actionlint
|
||||
make check-test-speed # 4s suite, 0.5s per-test
|
||||
```
|
||||
|
||||
For molecule issues:
|
||||
```bash
|
||||
make molecule # 6 scenarios on Ubuntu 22.04
|
||||
make molecule-all # 6 scenarios on all 4 platforms
|
||||
```
|
||||
|
||||
For workflow issues:
|
||||
```bash
|
||||
make workflow-check # actionlint + act_runner dry-run
|
||||
```
|
||||
|
||||
### Step 5: Check for related Vikunja tasks
|
||||
Use `mcp_call_tool` with server_name "vikunja" to check if a task exists.
|
||||
CI auto-creates Gitea issues via `notify_failure`.
|
||||
|
||||
### Step 6: Report
|
||||
1. **Root cause**: the specific error and why it occurred
|
||||
2. **Evidence**: log excerpts, local verification results
|
||||
3. **Affected files**: file paths and line numbers
|
||||
4. **Suggested fix**: specific code change with rationale
|
||||
5. **Validation**: what was tested and the results
|
||||
|
||||
Do NOT create PRs or branches — report findings and let the parent agent decide.
|
||||
|
||||
## Feedback Reporting
|
||||
|
||||
When you encounter a concrete issue with a tool, workflow, or process
|
||||
that would benefit from further investigation, create a Gitea issue
|
||||
in the `oblachno-oss/grm` repo.
|
||||
|
||||
### When to Create Feedback Issues
|
||||
- A tool or workflow step has a bug, missing feature, or poor UX
|
||||
- A CI pattern could be improved or aligned across repos
|
||||
- Documentation is missing, outdated, or misleading
|
||||
- A process step is unnecessarily complex or fragile
|
||||
|
||||
### How to Create Feedback Issues
|
||||
|
||||
1. **Deduplicate first**: Use `mcp_call_tool` with server_name "gitea",
|
||||
tool_name "list_issues", with `labels: "feedback"`, `owner: "oblachno-oss"`,
|
||||
`repo: "grm"`. Check if an open issue already covers the same topic.
|
||||
Do NOT create duplicates.
|
||||
|
||||
2. **Create the issue**: Use `mcp_call_tool` with server_name "gitea",
|
||||
tool_name "issue_write", method "create_issue", `owner: "oblachno-oss"`,
|
||||
`repo: "grm"`:
|
||||
- **Title**: `[feedback] <category>: <short description>`
|
||||
- **Labels**: `feedback` + one of: `tooling`, `ci-improvement`,
|
||||
`doc-improvement`, `workflow-improvement`
|
||||
- **Body** must include these sections:
|
||||
```
|
||||
**Context**: What task you were performing, which repo
|
||||
**Tool/Workflow**: The specific tool or workflow step involved
|
||||
**Issue**: What went wrong or could be improved
|
||||
**Reproduction**: Steps to reproduce (if applicable)
|
||||
**Affected files**: File paths and line numbers
|
||||
**Suggested investigation**: What an agent should look into
|
||||
**Reported by**: <subagent profile name>
|
||||
```
|
||||
|
||||
3. **Report back**: Include the issue URL in your report to the parent agent.
|
||||
|
||||
### When NOT to Create Feedback Issues
|
||||
- Transient failures (network blips, rate limits, Docker pull flakiness)
|
||||
- Issues you can fix yourself — fix them instead
|
||||
- CI run failures — those are handled by `notify_failure` automatically
|
||||
- Missing labels — `configure_repo` creates standard labels on next master push
|
||||
@@ -0,0 +1,151 @@
|
||||
---
|
||||
name: dep-upgrader
|
||||
description: Researches and applies Python/Ansible dependency upgrades in pyproject.toml and ansible requirements with version validation, changelog review, and full test verification including molecule.
|
||||
model: glm-5.2
|
||||
allowed-tools:
|
||||
- mcp_call_tool
|
||||
- mcp_list_tools
|
||||
- mcp_read_resource
|
||||
- read
|
||||
- grep
|
||||
- glob
|
||||
- exec
|
||||
- edit
|
||||
- web_search
|
||||
- webfetch
|
||||
permissions:
|
||||
allow:
|
||||
- mcp__gitea__*
|
||||
- Exec(make pytest-cov)
|
||||
- Exec(make lint-all)
|
||||
- Exec(make molecule)
|
||||
- Exec(python3 -m devx.tools.check_test_speed *)
|
||||
- Exec(python3 -m devx.tools.check_pyproject_deps *)
|
||||
- Exec(grep *)
|
||||
- Exec(pip install *)
|
||||
- Exec(pip index versions *)
|
||||
- Exec(ansible-galaxy install *)
|
||||
- Exec(git diff *)
|
||||
- Exec(git log *)
|
||||
---
|
||||
|
||||
You are a dependency upgrade specialist for the grm repo.
|
||||
|
||||
## Working Directory
|
||||
|
||||
The grm repo is at `/home/emo/dev/ideas/oblachno/grm`. Always `cd` there first.
|
||||
|
||||
## Dependency Reference Locations
|
||||
|
||||
- **Python deps**: `pyproject.toml` — `[project] dependencies` and `[project.optional-dependencies]`
|
||||
- **Ansible deps**: `ansible/requirements.yml` — galaxy collections and roles
|
||||
- **Dep documentation**: Each pyproject.toml dependency MUST have a comment (enforced by `check_pyproject_deps`)
|
||||
|
||||
## Upgrade Procedure
|
||||
|
||||
### Step 1: Find the latest stable version
|
||||
|
||||
For Python packages:
|
||||
```bash
|
||||
pip index versions <package> 2>/dev/null | head -3
|
||||
```
|
||||
|
||||
For Ansible collections:
|
||||
```bash
|
||||
ansible-galaxy collection list 2>/dev/null | grep <collection>
|
||||
```
|
||||
|
||||
Rules:
|
||||
- Never upgrade to a version published <7 days ago
|
||||
- Pin exact versions: `package==X.Y.Z`
|
||||
- For Ansible collections: `community.docker:==3.10.2`
|
||||
|
||||
### Step 2: Review breaking changes
|
||||
Read the changelog/release notes. Look for:
|
||||
- Breaking API changes
|
||||
- Deprecated features
|
||||
- Minimum Python/Ansible version changes
|
||||
- New required dependencies
|
||||
|
||||
### Step 3: Apply the upgrade
|
||||
|
||||
**Python deps** — edit `pyproject.toml`:
|
||||
Each dependency line MUST have a trailing comment:
|
||||
```toml
|
||||
"ruff==0.12.0", # Python linter and formatter
|
||||
```
|
||||
|
||||
**Ansible collections** — edit `ansible/requirements.yml`:
|
||||
```yaml
|
||||
collections:
|
||||
- name: community.docker
|
||||
version: "==3.10.2"
|
||||
```
|
||||
|
||||
### Step 4: Install and verify
|
||||
```bash
|
||||
pip install -e .[dev] # reinstall with new deps
|
||||
ansible-galaxy install -r ansible/requirements.yml # update collections
|
||||
make pytest-cov # 100% coverage
|
||||
make lint-all # ruff + pyright + bandit + ansible-lint + checkmake + actionlint
|
||||
python3 -m devx.tools.check_pyproject_deps
|
||||
python3 -m devx.tools.check_test_speed --max-seconds 4 --max-single-seconds 0.5
|
||||
```
|
||||
|
||||
If the dependency affects Ansible behavior, also run molecule:
|
||||
```bash
|
||||
make molecule # 6 scenarios on Ubuntu 22.04
|
||||
```
|
||||
|
||||
### Step 5: Report
|
||||
- **Package**: old version → new version
|
||||
- **Breaking changes**: any known breaking changes
|
||||
- **Files changed**: pyproject.toml, requirements.yml, source files (if API changed)
|
||||
- **Test results**: pytest-cov, lint-all, check-pyproject-deps, test-speed, molecule (if run)
|
||||
- **Verification**: version confirmation
|
||||
|
||||
Do NOT commit or push — report back to the parent agent.
|
||||
|
||||
## Feedback Reporting
|
||||
|
||||
When you encounter a concrete issue with a tool, workflow, or process
|
||||
that would benefit from further investigation, create a Gitea issue
|
||||
in the `oblachno-oss/grm` repo.
|
||||
|
||||
### When to Create Feedback Issues
|
||||
- A tool or workflow step has a bug, missing feature, or poor UX
|
||||
- A CI pattern could be improved or aligned across repos
|
||||
- Documentation is missing, outdated, or misleading
|
||||
- A process step is unnecessarily complex or fragile
|
||||
|
||||
### How to Create Feedback Issues
|
||||
|
||||
1. **Deduplicate first**: Use `mcp_call_tool` with server_name "gitea",
|
||||
tool_name "list_issues", with `labels: "feedback"`, `owner: "oblachno-oss"`,
|
||||
`repo: "grm"`. Check if an open issue already covers the same topic.
|
||||
Do NOT create duplicates.
|
||||
|
||||
2. **Create the issue**: Use `mcp_call_tool` with server_name "gitea",
|
||||
tool_name "issue_write", method "create_issue", `owner: "oblachno-oss"`,
|
||||
`repo: "grm"`:
|
||||
- **Title**: `[feedback] <category>: <short description>`
|
||||
- **Labels**: `feedback` + one of: `tooling`, `ci-improvement`,
|
||||
`doc-improvement`, `workflow-improvement`
|
||||
- **Body** must include these sections:
|
||||
```
|
||||
**Context**: What task you were performing, which repo
|
||||
**Tool/Workflow**: The specific tool or workflow step involved
|
||||
**Issue**: What went wrong or could be improved
|
||||
**Reproduction**: Steps to reproduce (if applicable)
|
||||
**Affected files**: File paths and line numbers
|
||||
**Suggested investigation**: What an agent should look into
|
||||
**Reported by**: <subagent profile name>
|
||||
```
|
||||
|
||||
3. **Report back**: Include the issue URL in your report to the parent agent.
|
||||
|
||||
### When NOT to Create Feedback Issues
|
||||
- Transient failures (network blips, rate limits, Docker pull flakiness)
|
||||
- Issues you can fix yourself — fix them instead
|
||||
- CI run failures — those are handled by `notify_failure` automatically
|
||||
- Missing labels — `configure_repo` creates standard labels on next master push
|
||||
@@ -0,0 +1,134 @@
|
||||
---
|
||||
name: doc-syncer
|
||||
description: Handles documentation coverage, doc structure linting, and wiki sync for the grm repo. Detects missing docs, fixes broken links, updates mapping.json, and debugs wiki sync failures.
|
||||
model: glm-5.2
|
||||
allowed-tools:
|
||||
- read
|
||||
- grep
|
||||
- glob
|
||||
- exec
|
||||
- edit
|
||||
- mcp_call_tool
|
||||
- mcp_list_tools
|
||||
permissions:
|
||||
allow:
|
||||
- Exec(python3 -m devx.ci.doc_coverage *)
|
||||
- Exec(python3 -m devx.ci.lint_docs *)
|
||||
- Exec(python3 -m devx.ci.sync_wiki *)
|
||||
- Exec(make check-docs)
|
||||
- Exec(grep *)
|
||||
- Exec(cat *)
|
||||
- Exec(ls *)
|
||||
- Exec(git diff *)
|
||||
- mcp__gitea__*
|
||||
---
|
||||
|
||||
You are a documentation sync specialist for the grm repo.
|
||||
|
||||
## Working Directory
|
||||
|
||||
The grm repo is at `/home/emo/dev/ideas/oblachno/grm`. Always `cd` there first.
|
||||
|
||||
## Documentation Structure
|
||||
|
||||
```
|
||||
docs/
|
||||
├── index.md # Wiki homepage
|
||||
├── mapping.json # File-to-wiki-page title mapping (13 entries)
|
||||
├── user/ # User documentation
|
||||
│ ├── getting-started.md
|
||||
│ ├── installation.md
|
||||
│ ├── cli-commands.md
|
||||
│ ├── troubleshooting.md
|
||||
│ └── faq.md
|
||||
└── tech/ # Technical documentation
|
||||
├── architecture.md
|
||||
├── development-setup.md
|
||||
├── ci-cd-workflow.md
|
||||
├── testing-strategy.md
|
||||
├── decision-log.md
|
||||
└── contributing.md
|
||||
```
|
||||
|
||||
## Procedure
|
||||
|
||||
### Step 1: Check documentation coverage
|
||||
```bash
|
||||
python3 -m devx.ci.doc_coverage --fail-on-missing
|
||||
```
|
||||
Fix undocumented CLI commands, modules, or CI scripts by adding entries
|
||||
to the appropriate docs file.
|
||||
|
||||
### Step 2: Lint documentation structure
|
||||
```bash
|
||||
python3 -m devx.ci.lint_docs --root .
|
||||
```
|
||||
Fix: broken internal links, heading hierarchy skips, TODO/FIXME markers,
|
||||
trailing whitespace.
|
||||
|
||||
### Step 3: Check for stale references
|
||||
```bash
|
||||
make check-docs
|
||||
```
|
||||
Update any references to files that were renamed or deleted.
|
||||
|
||||
### Step 4: Verify wiki sync (if investigating a sync failure)
|
||||
```bash
|
||||
python3 -m devx.ci.sync_wiki --repo oblachno-oss/grm --strict
|
||||
```
|
||||
Check `docs/mapping.json` — every docs file should have a mapping entry.
|
||||
If adding a new docs file, add it to mapping.json with a wiki-compatible
|
||||
title (hyphens for spaces, no special characters).
|
||||
|
||||
### Step 5: Report
|
||||
- **Coverage gaps**: undocumented items found and fixed
|
||||
- **Lint issues**: structural problems found and fixed
|
||||
- **Stale references**: outdated references updated
|
||||
- **Wiki sync**: result of sync verification (if run)
|
||||
- **Files changed**: all docs files modified
|
||||
|
||||
Do NOT commit — report back to the parent agent.
|
||||
|
||||
## Feedback Reporting
|
||||
|
||||
When you encounter a concrete issue with a tool, workflow, or process
|
||||
that would benefit from further investigation, create a Gitea issue
|
||||
in the `oblachno-oss/grm` repo.
|
||||
|
||||
### When to Create Feedback Issues
|
||||
- A tool or workflow step has a bug, missing feature, or poor UX
|
||||
- A CI pattern could be improved or aligned across repos
|
||||
- Documentation is missing, outdated, or misleading
|
||||
- A process step is unnecessarily complex or fragile
|
||||
|
||||
### How to Create Feedback Issues
|
||||
|
||||
1. **Deduplicate first**: Use `mcp_call_tool` with server_name "gitea",
|
||||
tool_name "list_issues", with `labels: "feedback"`, `owner: "oblachno-oss"`,
|
||||
`repo: "grm"`. Check if an open issue already covers the same topic.
|
||||
Do NOT create duplicates.
|
||||
|
||||
2. **Create the issue**: Use `mcp_call_tool` with server_name "gitea",
|
||||
tool_name "issue_write", method "create_issue", `owner: "oblachno-oss"`,
|
||||
`repo: "grm"`:
|
||||
- **Title**: `[feedback] <category>: <short description>`
|
||||
- **Labels**: `feedback` + one of: `tooling`, `ci-improvement`,
|
||||
`doc-improvement`, `workflow-improvement`
|
||||
- **Body** must include these sections:
|
||||
```
|
||||
**Context**: What task you were performing, which repo
|
||||
**Tool/Workflow**: The specific tool or workflow step involved
|
||||
**Issue**: What went wrong or could be improved
|
||||
**Reproduction**: Steps to reproduce (if applicable)
|
||||
**Affected files**: File paths and line numbers
|
||||
**Suggested investigation**: What an agent should look into
|
||||
**Reported by**: <subagent profile name>
|
||||
```
|
||||
|
||||
3. **Report back**: Include the issue URL in your report to the parent agent.
|
||||
|
||||
### When NOT to Create Feedback Issues
|
||||
- Transient failures (network blips, rate limits, Docker pull flakiness)
|
||||
- Issues you can fix yourself — fix them instead
|
||||
- CI run failures — those are handled by `notify_failure` automatically
|
||||
- Missing labels — `configure_repo` creates standard labels on next master push
|
||||
@@ -0,0 +1,154 @@
|
||||
---
|
||||
name: molecule-runner
|
||||
description: Runs molecule test scenarios for the gitea-runner Ansible role and reports pass/fail with logs. Knows all 7 scenarios, 4 platforms, Docker prerequisites, and dynamic runner distribution.
|
||||
model: glm-5.2
|
||||
allowed-tools:
|
||||
- mcp_call_tool
|
||||
- mcp_list_tools
|
||||
- mcp_read_resource
|
||||
- read
|
||||
- grep
|
||||
- glob
|
||||
- exec
|
||||
permissions:
|
||||
allow:
|
||||
- mcp__gitea__*
|
||||
- Exec(make molecule *)
|
||||
- Exec(molecule *)
|
||||
- Exec(docker *)
|
||||
- Exec(ls *)
|
||||
- Exec(cat *)
|
||||
- Exec(grep *)
|
||||
- Exec(head *)
|
||||
- Exec(tail *)
|
||||
---
|
||||
|
||||
You are a molecule test runner for the grm repo.
|
||||
|
||||
## Working Directory
|
||||
|
||||
The grm repo is at `/home/emo/dev/ideas/oblachno/grm`. Always `cd` there first.
|
||||
|
||||
## Available Scenarios (7 total)
|
||||
|
||||
| Scenario | Purpose | Makefile target |
|
||||
|----------|---------|-----------------|
|
||||
| default | Basic runner installation | `make molecule` (included) |
|
||||
| multi-instance | 2 runners on same host | `make molecule` (included) |
|
||||
| lifecycle | stop/disable/enable/start | `make molecule` (included) |
|
||||
| template-content | Rendered template verification | `make molecule` (included) |
|
||||
| deregister | Runner cleanup | `make molecule` (included) |
|
||||
| update | Binary update | `make molecule` (included) |
|
||||
| remove | Full removal (destroys container) | CI only (not in `make molecule`) |
|
||||
|
||||
**Platforms** (4): ubuntu-2204, ubuntu-2404, debian-12, archlinux
|
||||
Platform list defined in `devx.molecule.platforms` (single source of truth).
|
||||
|
||||
**Note**: `make molecule` runs 6 scenarios (excludes `remove`).
|
||||
`make molecule-all` runs 6 scenarios on all 4 platforms.
|
||||
CI discovers all 7 scenarios via `devx.molecule.distribute_molecule`.
|
||||
|
||||
## Molecule Weights (for LPT distribution)
|
||||
|
||||
Configured in `pyproject.toml` `[tool.devx.molecule.weights]`:
|
||||
```
|
||||
multi-instance = 8, lifecycle = 6, update = 5, default = 4,
|
||||
deregister = 3, remove = 3, template-content = 2
|
||||
```
|
||||
|
||||
## Docker Prerequisites
|
||||
|
||||
```bash
|
||||
docker info > /dev/null 2>&1 && echo "Docker ready" || echo "Docker not available"
|
||||
```
|
||||
|
||||
If Docker is not running, report immediately — do not attempt to start it.
|
||||
|
||||
## Running Tests
|
||||
|
||||
When given a scenario name or "all":
|
||||
1. Verify Docker is running
|
||||
2. Run the appropriate make target
|
||||
3. Capture full output (do not truncate)
|
||||
4. Parse results
|
||||
|
||||
For a single scenario:
|
||||
```bash
|
||||
molecule test -s <scenario>
|
||||
```
|
||||
|
||||
For all scenarios on one platform:
|
||||
```bash
|
||||
make molecule
|
||||
```
|
||||
|
||||
For all scenarios on all platforms:
|
||||
```bash
|
||||
make molecule-all
|
||||
```
|
||||
|
||||
## Known Issues
|
||||
|
||||
- `ansible-lint` may warn about `command-instead-of-module` for `systemctl --user`
|
||||
calls — this is expected (systemd module doesn't support user services) and
|
||||
skipped in `.ansible-lint`
|
||||
- Molecule Docker driver may print "Event loop is closed" warnings on interrupt — harmless
|
||||
|
||||
## Reporting
|
||||
|
||||
Report:
|
||||
- **PASSED**: scenario name, platform, duration
|
||||
- **FAILED**: scenario name, platform, the failing Ansible task, error message, file:line
|
||||
- **SKIPPED**: if Docker was unavailable
|
||||
|
||||
For failures, extract:
|
||||
- The Ansible task: `TASK [gitea-runner : task_name]` followed by `FAILED!`
|
||||
- The error detail: the `msg` field in the JSON output
|
||||
- The molecule verify step: look for `VERIFY` section
|
||||
- Platform-specific failures: note if only one OS failed
|
||||
|
||||
Do NOT attempt to fix failures — report them with enough detail for the parent agent.
|
||||
|
||||
## Feedback Reporting
|
||||
|
||||
When you encounter a concrete issue with a tool, workflow, or process
|
||||
that would benefit from further investigation, create a Gitea issue
|
||||
in the `oblachno-oss/grm` repo.
|
||||
|
||||
### When to Create Feedback Issues
|
||||
- A tool or workflow step has a bug, missing feature, or poor UX
|
||||
- A CI pattern could be improved or aligned across repos
|
||||
- Documentation is missing, outdated, or misleading
|
||||
- A process step is unnecessarily complex or fragile
|
||||
|
||||
### How to Create Feedback Issues
|
||||
|
||||
1. **Deduplicate first**: Use `mcp_call_tool` with server_name "gitea",
|
||||
tool_name "list_issues", with `labels: "feedback"`, `owner: "oblachno-oss"`,
|
||||
`repo: "grm"`. Check if an open issue already covers the same topic.
|
||||
Do NOT create duplicates.
|
||||
|
||||
2. **Create the issue**: Use `mcp_call_tool` with server_name "gitea",
|
||||
tool_name "issue_write", method "create_issue", `owner: "oblachno-oss"`,
|
||||
`repo: "grm"`:
|
||||
- **Title**: `[feedback] <category>: <short description>`
|
||||
- **Labels**: `feedback` + one of: `tooling`, `ci-improvement`,
|
||||
`doc-improvement`, `workflow-improvement`
|
||||
- **Body** must include these sections:
|
||||
```
|
||||
**Context**: What task you were performing, which repo
|
||||
**Tool/Workflow**: The specific tool or workflow step involved
|
||||
**Issue**: What went wrong or could be improved
|
||||
**Reproduction**: Steps to reproduce (if applicable)
|
||||
**Affected files**: File paths and line numbers
|
||||
**Suggested investigation**: What an agent should look into
|
||||
**Reported by**: <subagent profile name>
|
||||
```
|
||||
|
||||
3. **Report back**: Include the issue URL in your report to the parent agent.
|
||||
|
||||
### When NOT to Create Feedback Issues
|
||||
- Transient failures (network blips, rate limits, Docker pull flakiness)
|
||||
- Issues you can fix yourself — fix them instead
|
||||
- CI run failures — those are handled by `notify_failure` automatically
|
||||
- Missing labels — `configure_repo` creates standard labels on next master push
|
||||
@@ -0,0 +1,150 @@
|
||||
---
|
||||
name: workflow-validator
|
||||
description: Validates Gitea Actions workflow YAML files for the grm repo using actionlint and act_runner dry-run. Fixes syntax errors, job dependency issues, and molecule distribution matrix problems.
|
||||
model: glm-5.2
|
||||
allowed-tools:
|
||||
- mcp_call_tool
|
||||
- mcp_list_tools
|
||||
- mcp_read_resource
|
||||
- read
|
||||
- grep
|
||||
- glob
|
||||
- exec
|
||||
- edit
|
||||
permissions:
|
||||
allow:
|
||||
- mcp__gitea__*
|
||||
- Exec(make workflow-lint)
|
||||
- Exec(make workflow-dryrun)
|
||||
- Exec(make workflow-check)
|
||||
- Exec(make install-tools)
|
||||
- Exec(actionlint *)
|
||||
- Exec(act_runner *)
|
||||
- Exec(cat *)
|
||||
- Exec(grep *)
|
||||
- Exec(git diff *)
|
||||
---
|
||||
|
||||
You are a Gitea Actions workflow validator for the grm repo.
|
||||
|
||||
## Working Directory
|
||||
|
||||
The grm repo is at `/home/emo/dev/ideas/oblachno/grm`. Always `cd` there first.
|
||||
|
||||
## Key Files
|
||||
|
||||
- `.gitea/workflows/ci.yml` — PR pipeline (quality, detect-changes, pre-merge-check, discover-runners, molecule-tests, molecule-report, release-dry-run, pr-review, auto-merge)
|
||||
- `.gitea/workflows/post-merge.yml` — master pipeline (detect-type, validate-commit-msg, release, publish, sync-wiki, badges, vikunja, configure-repo)
|
||||
- `.gitea/actionlint.yaml` — actionlint config (registers custom `docker` runner label)
|
||||
|
||||
## Validation Procedure
|
||||
|
||||
### Step 1: Install tools (if not present)
|
||||
```bash
|
||||
make install-tools # installs actionlint, act_runner to ~/.local/bin
|
||||
```
|
||||
|
||||
### Step 2: Static lint with actionlint
|
||||
```bash
|
||||
make workflow-lint
|
||||
```
|
||||
Fix any: syntax errors, invalid expressions, unknown keys, shellcheck issues,
|
||||
undefined variables, unknown actions, job dependency issues.
|
||||
|
||||
### Step 3: Dry-run with act_runner
|
||||
```bash
|
||||
make workflow-dryrun
|
||||
```
|
||||
Fix any: image not found, circular dependencies, step ordering issues,
|
||||
matrix expansion problems.
|
||||
|
||||
### Step 4: Full check
|
||||
```bash
|
||||
make workflow-check
|
||||
```
|
||||
|
||||
## GRM-Specific Workflow Concerns
|
||||
|
||||
**Molecule test distribution:**
|
||||
The `molecule-tests` job uses a matrix `[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]`
|
||||
with `max-parallel: 3`. Runners beyond the discovered count skip via
|
||||
`--skip-if-excess`. The `discover-runners` job queries the Gitea API
|
||||
for available runners.
|
||||
|
||||
If the matrix is too small, some scenarios won't run. If too large,
|
||||
excess runners skip (no harm). The default 10 slots should be enough.
|
||||
|
||||
**Path filtering:**
|
||||
Molecule tests only run when `ansible/` or `.ansible-lint` files change.
|
||||
The `detect-changes` job sets `ansible-changed` output. If this is false,
|
||||
molecule-tests is skipped — this is expected behavior.
|
||||
|
||||
**auto-merge and always():**
|
||||
```yaml
|
||||
auto-merge:
|
||||
needs: [quality, detect-changes, pre-merge-check, pr-review, molecule-tests]
|
||||
if: >-
|
||||
always() &&
|
||||
github.event_name == 'pull_request' &&
|
||||
needs.quality.result == 'success' &&
|
||||
needs.pre-merge-check.result == 'success' &&
|
||||
needs.pr-review.result == 'success' &&
|
||||
(needs.molecule-tests.result == 'success' || needs.molecule-tests.result == 'skipped')
|
||||
```
|
||||
|
||||
**Gitea Actions limitations (1.26.x):**
|
||||
- No `fromJSON()` in matrix context
|
||||
- `concurrency` blocks can cause stuck jobs
|
||||
- `GITHUB_OUTPUT` for step outputs
|
||||
|
||||
## Report
|
||||
- **actionlint results**: pass/fail per workflow file, specific errors
|
||||
- **dry-run results**: pass/fail per workflow, job dependency issues
|
||||
- **Files changed**: if any workflow YAML was modified
|
||||
- **Verification**: re-run results after fixes
|
||||
|
||||
Do NOT commit — report back to the parent agent.
|
||||
|
||||
## Feedback Reporting
|
||||
|
||||
When you encounter a concrete issue with a tool, workflow, or process
|
||||
that would benefit from further investigation, create a Gitea issue
|
||||
in the `oblachno-oss/grm` repo.
|
||||
|
||||
### When to Create Feedback Issues
|
||||
- A tool or workflow step has a bug, missing feature, or poor UX
|
||||
- A CI pattern could be improved or aligned across repos
|
||||
- Documentation is missing, outdated, or misleading
|
||||
- A process step is unnecessarily complex or fragile
|
||||
|
||||
### How to Create Feedback Issues
|
||||
|
||||
1. **Deduplicate first**: Use `mcp_call_tool` with server_name "gitea",
|
||||
tool_name "list_issues", with `labels: "feedback"`, `owner: "oblachno-oss"`,
|
||||
`repo: "grm"`. Check if an open issue already covers the same topic.
|
||||
Do NOT create duplicates.
|
||||
|
||||
2. **Create the issue**: Use `mcp_call_tool` with server_name "gitea",
|
||||
tool_name "issue_write", method "create_issue", `owner: "oblachno-oss"`,
|
||||
`repo: "grm"`:
|
||||
- **Title**: `[feedback] <category>: <short description>`
|
||||
- **Labels**: `feedback` + one of: `tooling`, `ci-improvement`,
|
||||
`doc-improvement`, `workflow-improvement`
|
||||
- **Body** must include these sections:
|
||||
```
|
||||
**Context**: What task you were performing, which repo
|
||||
**Tool/Workflow**: The specific tool or workflow step involved
|
||||
**Issue**: What went wrong or could be improved
|
||||
**Reproduction**: Steps to reproduce (if applicable)
|
||||
**Affected files**: File paths and line numbers
|
||||
**Suggested investigation**: What an agent should look into
|
||||
**Reported by**: <subagent profile name>
|
||||
```
|
||||
|
||||
3. **Report back**: Include the issue URL in your report to the parent agent.
|
||||
|
||||
### When NOT to Create Feedback Issues
|
||||
- Transient failures (network blips, rate limits, Docker pull flakiness)
|
||||
- Issues you can fix yourself — fix them instead
|
||||
- CI run failures — those are handled by `notify_failure` automatically
|
||||
- Missing labels — `configure_repo` creates standard labels on next master push
|
||||
@@ -0,0 +1,38 @@
|
||||
# devx-workflow
|
||||
|
||||
Quick reference for devx tools when working on this repo.
|
||||
|
||||
## PR Workflow (use these, not raw git/tea/MCP)
|
||||
|
||||
| Task | Command |
|
||||
|------|---------|
|
||||
| Create Vikunja task | `make create-task -- --title "..." --description "..."` |
|
||||
| Create PR | `make create-pr` |
|
||||
| Push + create PR | `make push-with-pr` |
|
||||
| Check CI status | `make devx-pr-status` or `make devx-pr-status PR=42 WAIT=1` |
|
||||
| Fetch CI failure logs | `make devx-pr-logs` or `make devx-pr-logs PR=42 JOB=quality TAIL=50` |
|
||||
| Add ready-to-merge label | `make devx-pr-label` or `make devx-pr-label PR=42` |
|
||||
| Post PR review | `make devx-pr-review PR=42 EVENT=APPROVE BODY="..." CHECKLIST=1,2,3,4,5,6,7,8,9,10,11,12,13` |
|
||||
| Rebase current branch | `make rebase` |
|
||||
| Rebase PR via API | `make pr-rebase` or `make pr-rebase PR=42` |
|
||||
|
||||
## Auto-merge Behavior
|
||||
|
||||
When the `ready-to-merge` label is added and all CI checks pass:
|
||||
1. Auto-merge validates PR title format (`GRM-N: <vikunja task title>`)
|
||||
2. If branch is behind master, auto-merge **rebases via Gitea API** automatically
|
||||
3. The rebase triggers a new CI run; the next auto-merge attempt merges
|
||||
4. No manual rebase needed unless the API rebase fails
|
||||
|
||||
## Pre-merge Check
|
||||
|
||||
CI runs a `pre-merge-check` job early (after quality + detect-changes)
|
||||
that validates branch format, PR title, and Vikunja task match.
|
||||
This fails fast before expensive molecule tests run.
|
||||
|
||||
## Key Rules
|
||||
|
||||
- Never manually merge via API — always use auto-merge with `ready-to-merge` label
|
||||
- Branch naming: `GRM-N-short-description` (N = Vikunja task ID)
|
||||
- Commit format: conventional commits (`feat:`, `fix:`, `docs:`, etc.)
|
||||
- PR title: `GRM-N: <vikunja task title>` (auto-derived by `make create-pr`)
|
||||
+53
-19
@@ -23,12 +23,12 @@ jobs:
|
||||
run: make setup-image EXTRAS=lint
|
||||
- name: Lint all
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
make lint-all
|
||||
- name: Unit tests with 100% coverage
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
make pytest-cov
|
||||
- name: Documentation lint check
|
||||
env:
|
||||
@@ -36,31 +36,31 @@ jobs:
|
||||
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
pip install --upgrade devx \
|
||||
--index-url "https://${CI_GITEA_USERNAME}:${CI_GITEA_TOKEN}@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/" \
|
||||
--no-deps
|
||||
python3 -m devx.ci.lint_docs --root .
|
||||
- name: Translation completeness check
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
python3 -m devx.ci.check_translations --translations src/gitea_runner_manager/translations.json
|
||||
- name: Check unit test speed
|
||||
env:
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
python3 -m devx.tools.check_test_speed --max-seconds 4 --max-single-seconds 0.5
|
||||
- name: Dependency security scan
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
# Install pip in venv if missing (needed by pip-audit)
|
||||
.venv/bin/python -m ensurepip 2>/dev/null || true
|
||||
PIPAPI_PYTHON_LOCATION=$PWD/.venv/bin/python \
|
||||
pip-audit --desc --skip-editable 2>&1 || true
|
||||
- name: Workflow dry-run validation
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
# Best-effort: only runs if act_runner is installed
|
||||
if command -v act_runner >/dev/null 2>&1; then
|
||||
@@ -90,7 +90,7 @@ jobs:
|
||||
DEVX_VERSION_FILE: src/gitea_runner_manager/__init__.py
|
||||
DEVX_TASK_PREFIX: GRM
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
python3 -m devx.ci.release --dry-run
|
||||
|
||||
@@ -116,12 +116,43 @@ jobs:
|
||||
PYTHONPATH: src
|
||||
DEVX_TASK_PREFIX: GRM
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
python3 -m devx.ci.classify_changes \
|
||||
--base "origin/master" \
|
||||
--head "${{ github.event.pull_request.head.sha || github.sha }}" \
|
||||
--github-output
|
||||
|
||||
pre-merge-check:
|
||||
needs: [quality, detect-changes]
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: docker
|
||||
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up environment
|
||||
run: make setup-image EXTRAS=ci
|
||||
- name: Validate auto-merge preconditions
|
||||
env:
|
||||
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }}
|
||||
DEVX_TASK_PREFIX: GRM
|
||||
DEVX_VIKUNJA_PROJECT_ID: 6
|
||||
HEAD_REF: ${{ github.head_ref }}
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
PYTHONPATH: ${{ env.PYTHONPATH }}
|
||||
run: |
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
python3 -m devx.ci.check_auto_merge_ready \
|
||||
--branch "$HEAD_REF" \
|
||||
--pr-title "$PR_TITLE" \
|
||||
--repo "$REPOSITORY" \
|
||||
--pr-number "$PR_NUMBER"
|
||||
|
||||
discover-runners:
|
||||
needs: [detect-changes]
|
||||
if: needs.detect-changes.outputs.ansible-changed == 'true'
|
||||
@@ -145,7 +176,7 @@ jobs:
|
||||
MOLECULE_RUNNERS: ${{ vars.MOLECULE_RUNNERS }}
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
python3 -m devx.molecule.discover_runners \
|
||||
--owner "${{ github.repository_owner }}" \
|
||||
--repo "${{ github.event.repository.name }}" \
|
||||
@@ -158,8 +189,10 @@ jobs:
|
||||
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest
|
||||
timeout-minutes: 10
|
||||
strategy:
|
||||
fail-fast: true
|
||||
max-parallel: 3
|
||||
matrix:
|
||||
runner-index: [1, 2, 3]
|
||||
runner-index: [1, 2, 3, 4, 5, 6]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
@@ -169,7 +202,7 @@ jobs:
|
||||
run: make setup-image EXTRAS=ci,molecule
|
||||
- name: Install Ansible collections
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
python3 -m devx.tools.setup --skip-install --no-pre-commit --no-tea-login
|
||||
- name: Discover assigned test pairs
|
||||
env:
|
||||
@@ -177,7 +210,7 @@ jobs:
|
||||
MAX_RUNNERS: ${{ needs.discover-runners.outputs.runner-count }}
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
python3 -m devx.molecule.distribute_molecule \
|
||||
--runner-index "$RUNNER_INDEX" \
|
||||
--max-runners "$MAX_RUNNERS" \
|
||||
@@ -185,7 +218,7 @@ jobs:
|
||||
- name: Run molecule tests
|
||||
if: env.SKIP != 'true'
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
if [ -z "$TEST_PAIRS" ]; then exit 0; fi
|
||||
if ! python3 -c "import docker; docker.from_env().ping()" 2>/dev/null; then
|
||||
echo "Docker not available in CI container — skipping molecule tests"
|
||||
@@ -227,7 +260,7 @@ jobs:
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
set -euo pipefail
|
||||
. .venv/bin/activate
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
python3 -m devx.ci.pr_review \
|
||||
"${{ github.event.number }}" \
|
||||
"${{ github.repository }}"
|
||||
@@ -237,11 +270,12 @@ jobs:
|
||||
# from the branch name, validates the PR title, and squash-merges.
|
||||
# Uses always() so it evaluates even when molecule-tests is skipped
|
||||
# (Gitea Actions skips dependent jobs of skipped jobs by default).
|
||||
needs: [quality, detect-changes, pr-review, molecule-tests, release-dry-run]
|
||||
needs: [quality, detect-changes, pre-merge-check, pr-review, molecule-tests, release-dry-run]
|
||||
if: >-
|
||||
always() &&
|
||||
github.event_name == 'pull_request' &&
|
||||
needs.quality.result == 'success' &&
|
||||
needs.pre-merge-check.result == 'success' &&
|
||||
needs.pr-review.result == 'success' &&
|
||||
(needs.molecule-tests.result == 'success' || needs.molecule-tests.result == 'skipped') &&
|
||||
(needs.release-dry-run.result == 'success' || needs.release-dry-run.result == 'skipped')
|
||||
@@ -268,14 +302,14 @@ jobs:
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
python3 -m devx.ci.pr_review \
|
||||
"$PR_NUMBER" \
|
||||
"$REPOSITORY" \
|
||||
--event APPROVE \
|
||||
--checklist-confirmed \
|
||||
--checklist-categories 1,2,3,4,5,6,7,8,9,10,11,12,13 \
|
||||
--body "Auto-approved: all CI checks passed (quality, molecule, pr-review)."
|
||||
--body "Auto-approved: all CI checks passed (quality, molecule, pr-review, pre-merge-check)."
|
||||
- name: Squash merge with task ID
|
||||
env:
|
||||
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
@@ -288,7 +322,7 @@ jobs:
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
python3 -m devx.ci.auto_merge \
|
||||
"$HEAD_REF" \
|
||||
"$PR_TITLE" \
|
||||
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
env:
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
python3 -m devx.ci.detect_release_commit
|
||||
|
||||
validate-commit-msg:
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
PYTHONPATH: src
|
||||
DEVX_TASK_PREFIX: GRM
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
git log -1 --format=%B > commit-msg.txt
|
||||
python3 -m devx.ci.validate_commit_msg commit-msg.txt --branch master
|
||||
rm -f commit-msg.txt
|
||||
@@ -114,7 +114,7 @@ jobs:
|
||||
DEVX_TASK_PREFIX: GRM
|
||||
DEVX_VIKUNJA_PROJECT_ID: 6
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
python3 -m devx.ci.release
|
||||
- name: Notify on failure
|
||||
@@ -152,7 +152,7 @@ jobs:
|
||||
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
python3 -m devx.ci.publish \
|
||||
"${{ needs.release.outputs.tag }}" \
|
||||
@@ -191,7 +191,7 @@ jobs:
|
||||
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
python3 -m devx.ci.sync_wiki --repo "${{ github.repository }}" --strict
|
||||
- name: Notify on failure
|
||||
if: failure()
|
||||
@@ -231,7 +231,7 @@ jobs:
|
||||
env:
|
||||
PRE_COMMIT_ALLOW_NO_CONFIG: "1"
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
python3 -m devx.ci.push_badges
|
||||
- name: Notify on failure
|
||||
if: failure()
|
||||
@@ -268,7 +268,7 @@ jobs:
|
||||
DEVX_TASK_PREFIX: GRM
|
||||
DEVX_VIKUNJA_PROJECT_ID: 6
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
python3 -m devx.ci.post_merge --git-sha "${{ github.sha }}"
|
||||
- name: Notify on failure
|
||||
if: failure()
|
||||
@@ -304,7 +304,7 @@ jobs:
|
||||
DEVX_REPO_OWNER: oblachno-oss
|
||||
DEVX_STATUS_CHECKS: "CI / quality (pull_request),CI / molecule-tests (1) (pull_request),CI / molecule-tests (2) (pull_request),CI / molecule-tests (3) (pull_request)"
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
python3 -m devx.tools.configure_repo
|
||||
- name: Notify on failure
|
||||
if: failure()
|
||||
|
||||
@@ -163,6 +163,12 @@ Then add the `ready-to-merge` label. The auto-merge workflow will:
|
||||
5. The post-merge workflow marks the Vikunja task as done
|
||||
6. The release workflow automatically versions, tags, and publishes (see below)
|
||||
|
||||
**If the branch is behind master** (another PR merged first), auto-merge
|
||||
automatically rebases the PR's head branch via the Gitea API. This triggers
|
||||
a new CI run. The next auto-merge attempt will merge successfully.
|
||||
No manual rebase needed. To rebase manually: `make rebase` (local) or
|
||||
`make pr-rebase` (server-side via API).
|
||||
|
||||
> **IMPORTANT**: Never manually merge PRs via the API. Always use the auto-merge
|
||||
> workflow by adding the `ready-to-merge` label. Manual merges bypass the
|
||||
> `GRM-N: <conventional>` format enforcement, producing incorrectly named commits.
|
||||
@@ -171,6 +177,10 @@ Then add the `ready-to-merge` label. The auto-merge workflow will:
|
||||
|
||||
### CI Path Filtering
|
||||
|
||||
The CI workflow includes a `pre-merge-check` job (runs after quality +
|
||||
detect-changes) that validates branch format, PR title, and Vikunja task
|
||||
match. This fails fast before expensive molecule tests run.
|
||||
|
||||
The CI workflow includes a `detect-changes` job that checks whether any files
|
||||
under `ansible/` or `.ansible-lint` have changed. If no Ansible files are
|
||||
changed, molecule tests are skipped — this prevents non-Ansible changes
|
||||
@@ -242,11 +252,11 @@ Not all changes require the full CI pipeline or a new release. The project
|
||||
classifies changes into two categories using `devx.ci.classify_changes`:
|
||||
|
||||
**Classification strategy (safe-by-default):** Any file NOT in the explicit
|
||||
workflow-only allowlist is treated as user-facing. This prevents new file
|
||||
infrastructure allowlist is treated as user-facing. This prevents new file
|
||||
types from accidentally skipping releases. Classification is config-driven
|
||||
via `[tool.devx.classify]` in `pyproject.toml`.
|
||||
|
||||
**Workflow-only paths** (infrastructure → no release needed):
|
||||
**Infrastructure paths** (no release needed):
|
||||
- `.gitea/**` — Gitea Actions workflows
|
||||
- `scripts/**` — Dev tools and CI/CD automation (not part of installed package)
|
||||
- `docs/**` — Documentation
|
||||
@@ -267,7 +277,7 @@ via `[tool.devx.classify]` in `pyproject.toml`.
|
||||
|
||||
**devx module structure** (installed from git, not in this repo):
|
||||
- `devx.ci.*` — CI/CD automation (run by workflows): release, publish, auto_merge, classify_changes, detect_release_commit, push_badges, doc_coverage, sync_wiki, distribute_molecule, molecule_ci_guard, discover_runners, notify_failure, post_merge, pr_review, validate_commit_msg
|
||||
- `devx.tools.*` — Dev tools (run locally): check_test_speed, configure_repo, install_checkmake, install_tools, setup, generate_badges
|
||||
- `devx.tools.*` — Dev tools (run locally): check_test_speed, configure_repo, install_checkmake, install_tools, setup, generate_badges, create_task, create_pr, pr_status, pr_logs, pr_label, rebase, pr_rebase
|
||||
- `devx.molecule.*` — Molecule helpers: molecule_all, platforms, discover_runners, distribute_molecule, molecule_ci_guard
|
||||
- `devx.gitea_cli` — Tea CLI wrapper
|
||||
- `devx.i18n` — i18n translation system
|
||||
@@ -283,7 +293,7 @@ via `[tool.devx.classify]` in `pyproject.toml`.
|
||||
|
||||
**AI agents must follow these rules:**
|
||||
- When working on workflow/CI/docs-only changes, use `ci:` or `docs:` commit prefixes
|
||||
- Do NOT bump the version or create tags for workflow-only changes
|
||||
- Do NOT bump the version or create tags for infrastructure-only changes
|
||||
- The `classify_changes` module enforces this automatically — no manual intervention needed
|
||||
|
||||
## Source Code Separation and devx Integration
|
||||
@@ -397,7 +407,7 @@ The devx package is configured via `DEVX_*` environment variables:
|
||||
- `DEVX_VIKUNJA_PROJECT_ID=6` — Vikunja project ID for task tracking
|
||||
- `DEVX_VERSION_FILE=src/gitea_runner_manager/__init__.py` — Path to the version source file
|
||||
|
||||
Change classification is config-driven via `[tool.devx.classify]` in `pyproject.toml`, which defines the workflow-only and user-facing path patterns.
|
||||
Change classification is config-driven via `[tool.devx.classify]` in `pyproject.toml`, which defines the infrastructure and user-facing path patterns.
|
||||
|
||||
## Key Conventions
|
||||
|
||||
@@ -422,10 +432,12 @@ main.yml → systemd_check → user_setup → rootless_docker → install_runner
|
||||
|
||||
## Molecule Scenarios
|
||||
|
||||
6 scenarios: `default`, `multi-instance`, `lifecycle`, `template-content`, `deregister`, `update`
|
||||
7 scenarios: `default`, `multi-instance`, `lifecycle`, `template-content`, `deregister`, `update`, `remove`
|
||||
4 platforms: `ubuntu-2204`, `ubuntu-2404`, `debian-12`, `archlinux`
|
||||
Platform list is defined in `devx.molecule.platforms` (single source of truth)
|
||||
|
||||
Note: `make molecule` and `make molecule-all` run 6 scenarios (excluding `remove`, which destroys the test container). CI discovers all 7 scenarios via `devx.molecule.distribute_molecule`.
|
||||
|
||||
## Known Issues
|
||||
|
||||
- `ansible-lint` may warn about `command-instead-of-module` for `systemctl --user` calls — this is expected (systemd module doesn't support user services) and skipped in `.ansible-lint`
|
||||
@@ -475,3 +487,86 @@ docs/
|
||||
2. If adding a new page, add it to `docs/mapping.json`
|
||||
3. Commit and create a PR (standard PR workflow)
|
||||
4. On merge, wiki is automatically synced
|
||||
|
||||
## Subagent Delegation Policy
|
||||
|
||||
Custom subagent profiles are defined in `.devin/agents/` (project-specific)
|
||||
and `~/.config/devin/agents/` (global, shared across repos). The agent MUST
|
||||
automatically delegate to the appropriate subagent based on the task —
|
||||
the user should not need to specify which profile to use.
|
||||
|
||||
### Available Profiles
|
||||
|
||||
**Global** (shared with infra and devx):
|
||||
|
||||
| Profile | Location | Purpose |
|
||||
|---------|----------|---------|
|
||||
| `pr-reviewer` | `~/.config/devin/agents/` | 13-category PR checklist + quality gates |
|
||||
| `release-check` | `~/.config/devin/agents/` | Pre-merge readiness validation |
|
||||
|
||||
**grm-specific** (in `.devin/agents/`):
|
||||
|
||||
| Profile | Purpose |
|
||||
|---------|---------|
|
||||
| `ci-investigator` | Investigate CI failures (quality, molecule, release, publish, wiki sync) |
|
||||
| `molecule-runner` | Run 7 molecule scenarios across 4 platforms, report pass/fail |
|
||||
| `dep-upgrader` | Python + Ansible dependency upgrades with molecule verification |
|
||||
| `doc-syncer` | Doc coverage, doc linting, wiki sync for grm docs |
|
||||
| `workflow-validator` | actionlint + act_runner dry-run for grm workflows |
|
||||
|
||||
### When to Delegate Automatically
|
||||
|
||||
| Trigger | Profile | Mode |
|
||||
|---------|---------|------|
|
||||
| CI run failure (quality, molecule-tests, release, publish, sync-wiki) | `ci-investigator` | Background |
|
||||
| PR ready for review | `pr-reviewer` | Foreground |
|
||||
| Molecule tests need to run | `molecule-runner` | Background |
|
||||
| Dependency upgrade requested | `dep-upgrader` | Background |
|
||||
| Doc coverage failure or wiki sync issue | `doc-syncer` | Background |
|
||||
| Workflow YAML modified or validation needed | `workflow-validator` | Background |
|
||||
| Branch ready for merge | `release-check` | Foreground |
|
||||
|
||||
### Delegation Rules
|
||||
|
||||
1. **Auto-select the profile.** Do not ask the user which profile to use.
|
||||
2. **Background by default, foreground when blocking.**
|
||||
3. **Provide full context in the prompt** — subagents don't inherit conversation history.
|
||||
4. **One subagent per concern.** Chain: investigate → fix in main session → review.
|
||||
5. **Don't delegate trivial work** (<30s, <50 lines of context).
|
||||
6. **Compact after subagent returns.**
|
||||
7. **Never skip delegation to save time** — it keeps main context small.
|
||||
|
||||
|
||||
## Feedback Issue Handling
|
||||
|
||||
Subagents create Gitea issues in the current repo when they encounter
|
||||
tool, workflow, or process issues that warrant follow-up. These issues
|
||||
use the `feedback` label plus a category label (`tooling`,
|
||||
`ci-improvement`, `doc-improvement`, `workflow-improvement`).
|
||||
|
||||
Standard labels are created automatically by `configure_repo` (runs in
|
||||
post-merge on every master push). If a label does not exist yet, the
|
||||
subagent's issue creation will still succeed — labels can be added
|
||||
afterwards.
|
||||
|
||||
### When a Subagent Reports a Feedback Issue URL
|
||||
|
||||
1. **Acknowledge it** in your response to the user — mention the issue URL
|
||||
2. **Do NOT close or modify** the issue — it is for follow-up work
|
||||
3. **Do NOT create a PR** to address it unless the user explicitly asks
|
||||
4. If the user asks to address feedback, spawn a subagent to investigate
|
||||
the issue and implement a fix
|
||||
|
||||
### Creating Feedback Issues Manually
|
||||
|
||||
As the parent agent, you can also create feedback issues directly using
|
||||
the Gitea MCP (`issue_write` with `create_issue` method). Follow the
|
||||
same format as subagents:
|
||||
|
||||
- Title: `[feedback] <category>: <short description>`
|
||||
- Labels: `feedback` + category label
|
||||
- Body: include context, tool/workflow, issue, reproduction, affected
|
||||
files, suggested investigation, and "Reported by: parent agent"
|
||||
|
||||
Always deduplicate first via `list_issues` with `labels: "feedback"`.
|
||||
|
||||
|
||||
@@ -2,6 +2,31 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.14.0] - 2026-07-01
|
||||
|
||||
### Features
|
||||
|
||||
- Bump devx to v0.30.0
|
||||
|
||||
## [0.13.0] - 2026-07-01
|
||||
|
||||
### Features
|
||||
|
||||
- Bump devx to v0.29.1, upgrade molecule, ubuntu 26.04
|
||||
|
||||
## [0.12.5] - 2026-06-30
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Right-size molecule-tests matrix to [1-6]
|
||||
- Cast disk threshold to string in template-content verify assertion
|
||||
|
||||
## [0.12.4] - 2026-06-29
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Use hardcoded matrix array for Gitea 1.26 compatibility
|
||||
|
||||
## [0.12.3] - 2026-06-29
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -8,12 +8,12 @@ Each runner runs in an isolated **rootless Docker** environment under a dedicate
|
||||
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/src/branch/master/LICENSE)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/releases)
|
||||
[](https://www.python.org/downloads/)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/releases)
|
||||
[](https://www.python.org/downloads/)
|
||||
|
||||
## Why GRM?
|
||||
|
||||
@@ -151,6 +151,7 @@ GRM provides a single `grm` command with subcommands for the full runner lifecyc
|
||||
| `grm remove <name> --force` | Remove only the local registry entry (skip remote cleanup) |
|
||||
| `grm list` | List all registered runners with live status |
|
||||
| `grm list --no-status` | List registered runners without SSH status checks |
|
||||
| `grm health [name]` | Run health check (Docker, runner service, disk) on one or all runners |
|
||||
| `grm trigger-workflow <workflow_id>` | Trigger a Gitea Actions workflow via the API |
|
||||
| `grm trigger-workflow --list` | List available workflows in the repository |
|
||||
| `grm --version` | Show the installed version |
|
||||
|
||||
@@ -18,6 +18,16 @@
|
||||
when: systemd_available.stat.exists
|
||||
changed_when: true
|
||||
|
||||
- name: Stop and disable healthcheck timer
|
||||
ansible.builtin.command: systemctl --user stop --disable runner-healthcheck.timer
|
||||
become: true
|
||||
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
||||
environment:
|
||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
||||
when: systemd_available.stat.exists
|
||||
changed_when: true
|
||||
failed_when: false
|
||||
|
||||
- name: Include deregistration
|
||||
ansible.builtin.include_role:
|
||||
name: gitea-runner
|
||||
|
||||
@@ -79,6 +79,16 @@
|
||||
tasks_from: deregister.yml
|
||||
when: not skip_runner_registration | default(false)
|
||||
|
||||
- name: Stop and disable healthcheck timer
|
||||
ansible.builtin.command: systemctl --user stop --disable runner-healthcheck.timer
|
||||
become: true
|
||||
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
||||
environment:
|
||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
||||
when: systemd_available.stat.exists
|
||||
changed_when: true
|
||||
failed_when: false
|
||||
|
||||
- name: Remove docker-prune user service file
|
||||
ansible.builtin.file:
|
||||
path: "{{ gitea_runner_home | default('/home/grm-' ~ runner_name) }}/.config/systemd/user/docker-prune.service"
|
||||
@@ -91,6 +101,24 @@
|
||||
state: absent
|
||||
failed_when: false
|
||||
|
||||
- name: Remove healthcheck user service file
|
||||
ansible.builtin.file:
|
||||
path: "{{ gitea_runner_home | default('/home/grm-' ~ runner_name) }}/.config/systemd/user/runner-healthcheck.service"
|
||||
state: absent
|
||||
failed_when: false
|
||||
|
||||
- name: Remove healthcheck user timer file
|
||||
ansible.builtin.file:
|
||||
path: "{{ gitea_runner_home | default('/home/grm-' ~ runner_name) }}/.config/systemd/user/runner-healthcheck.timer"
|
||||
state: absent
|
||||
failed_when: false
|
||||
|
||||
- name: Remove healthcheck script
|
||||
ansible.builtin.file:
|
||||
path: "{{ gitea_runner_config_dir | default('/etc/gitea-runner/' ~ runner_name) }}/healthcheck.sh"
|
||||
state: absent
|
||||
failed_when: false
|
||||
|
||||
- name: Remove systemd user unit file
|
||||
ansible.builtin.file:
|
||||
path: "{{ gitea_runner_home | default('/home/grm-' ~ runner_name) }}/.config/systemd/user/gitea-runner.service"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
gitea_runner_version: "1.0.8"
|
||||
runner_labels: "docker,ubuntu-latest:docker://runner-images:ubuntu-22.04"
|
||||
runner_labels: "docker,ubuntu-latest:docker://runner-images:ubuntu-26.04"
|
||||
skip_runner_registration: false
|
||||
|
||||
# Per-runner user (rootless isolation)
|
||||
@@ -24,6 +24,12 @@ gitea_runner_prune_label: "gitea-runner=true"
|
||||
# Service configuration
|
||||
gitea_runner_service_restart_sec: "5"
|
||||
|
||||
# Health check configuration
|
||||
gitea_runner_healthcheck_interval: "5min"
|
||||
gitea_runner_healthcheck_boot_delay: "2min"
|
||||
gitea_runner_healthcheck_disk_threshold: 85
|
||||
gitea_runner_healthcheck_script_path: "{{ gitea_runner_config_dir }}/healthcheck.sh"
|
||||
|
||||
# Admin token for runner deregistration via Gitea API.
|
||||
# If not set, falls back to registration_token (which likely lacks admin scope).
|
||||
# Set this to a token with admin scope to enable automatic runner cleanup on removal.
|
||||
|
||||
@@ -4,7 +4,7 @@ driver:
|
||||
|
||||
platforms:
|
||||
- name: ${MOLECULE_PLATFORM_NAME:-ubuntu-2204}
|
||||
image: ${MOLECULE_PLATFORM_IMAGE:-ubuntu:22.04}
|
||||
image: ${MOLECULE_PLATFORM_IMAGE:-ubuntu:26.04}
|
||||
command: ${MOLECULE_PLATFORM_COMMAND:-sleep infinity}
|
||||
volumes:
|
||||
- /sys/fs/cgroup:/sys/fs/cgroup:rw
|
||||
|
||||
@@ -75,3 +75,42 @@
|
||||
that:
|
||||
- timer_stat.stat.exists
|
||||
fail_msg: "Docker prune timer is missing"
|
||||
|
||||
- name: Check healthcheck script exists
|
||||
ansible.builtin.stat:
|
||||
path: "{{ gitea_runner_healthcheck_script_path }}"
|
||||
register: healthcheck_script_stat
|
||||
|
||||
- name: Assert healthcheck script exists
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- healthcheck_script_stat.stat.exists
|
||||
fail_msg: "Healthcheck script is missing"
|
||||
|
||||
- name: Assert healthcheck script is executable
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- healthcheck_script_stat.stat.mode == "0755"
|
||||
fail_msg: "Healthcheck script is not executable"
|
||||
|
||||
- name: Check healthcheck service exists
|
||||
ansible.builtin.stat:
|
||||
path: "{{ gitea_runner_home }}/.config/systemd/user/runner-healthcheck.service"
|
||||
register: healthcheck_service_stat
|
||||
|
||||
- name: Assert healthcheck service exists
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- healthcheck_service_stat.stat.exists
|
||||
fail_msg: "Healthcheck systemd service is missing"
|
||||
|
||||
- name: Check healthcheck timer exists
|
||||
ansible.builtin.stat:
|
||||
path: "{{ gitea_runner_home }}/.config/systemd/user/runner-healthcheck.timer"
|
||||
register: healthcheck_timer_stat
|
||||
|
||||
- name: Assert healthcheck timer exists
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- healthcheck_timer_stat.stat.exists
|
||||
fail_msg: "Healthcheck systemd timer is missing"
|
||||
|
||||
@@ -4,7 +4,7 @@ driver:
|
||||
|
||||
platforms:
|
||||
- name: ${MOLECULE_PLATFORM_NAME:-ubuntu-2204}
|
||||
image: ${MOLECULE_PLATFORM_IMAGE:-ubuntu:22.04}
|
||||
image: ${MOLECULE_PLATFORM_IMAGE:-ubuntu:26.04}
|
||||
command: ${MOLECULE_PLATFORM_COMMAND:-sleep infinity}
|
||||
volumes:
|
||||
- /sys/fs/cgroup:/sys/fs/cgroup:rw
|
||||
|
||||
@@ -4,7 +4,7 @@ driver:
|
||||
|
||||
platforms:
|
||||
- name: ${MOLECULE_PLATFORM_NAME:-ubuntu-2204}
|
||||
image: ${MOLECULE_PLATFORM_IMAGE:-ubuntu:22.04}
|
||||
image: ${MOLECULE_PLATFORM_IMAGE:-ubuntu:26.04}
|
||||
command: ${MOLECULE_PLATFORM_COMMAND:-sleep infinity}
|
||||
volumes:
|
||||
- /sys/fs/cgroup:/sys/fs/cgroup:rw
|
||||
|
||||
@@ -4,7 +4,7 @@ driver:
|
||||
|
||||
platforms:
|
||||
- name: ${MOLECULE_PLATFORM_NAME:-ubuntu-2204}
|
||||
image: ${MOLECULE_PLATFORM_IMAGE:-ubuntu:22.04}
|
||||
image: ${MOLECULE_PLATFORM_IMAGE:-ubuntu:26.04}
|
||||
command: ${MOLECULE_PLATFORM_COMMAND:-sleep infinity}
|
||||
volumes:
|
||||
- /sys/fs/cgroup:/sys/fs/cgroup:rw
|
||||
|
||||
@@ -4,7 +4,7 @@ driver:
|
||||
|
||||
platforms:
|
||||
- name: ${MOLECULE_PLATFORM_NAME:-ubuntu-2204}
|
||||
image: ${MOLECULE_PLATFORM_IMAGE:-ubuntu:22.04}
|
||||
image: ${MOLECULE_PLATFORM_IMAGE:-ubuntu:26.04}
|
||||
command: ${MOLECULE_PLATFORM_COMMAND:-sleep infinity}
|
||||
volumes:
|
||||
- /sys/fs/cgroup:/sys/fs/cgroup:rw
|
||||
|
||||
@@ -89,6 +89,39 @@
|
||||
- not prune_timer_stat.stat.exists
|
||||
fail_msg: "docker-prune timer unit still exists after removal"
|
||||
|
||||
- name: Check healthcheck service unit is absent
|
||||
ansible.builtin.stat:
|
||||
path: "{{ gitea_runner_home }}/.config/systemd/user/runner-healthcheck.service"
|
||||
register: healthcheck_service_stat
|
||||
|
||||
- name: Assert healthcheck service unit is absent
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- not healthcheck_service_stat.stat.exists
|
||||
fail_msg: "runner-healthcheck service unit still exists after removal"
|
||||
|
||||
- name: Check healthcheck timer unit is absent
|
||||
ansible.builtin.stat:
|
||||
path: "{{ gitea_runner_home }}/.config/systemd/user/runner-healthcheck.timer"
|
||||
register: healthcheck_timer_stat
|
||||
|
||||
- name: Assert healthcheck timer unit is absent
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- not healthcheck_timer_stat.stat.exists
|
||||
fail_msg: "runner-healthcheck timer unit still exists after removal"
|
||||
|
||||
- name: Check healthcheck script is absent
|
||||
ansible.builtin.stat:
|
||||
path: "{{ gitea_runner_config_dir }}/healthcheck.sh"
|
||||
register: healthcheck_script_stat
|
||||
|
||||
- name: Assert healthcheck script is absent
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- not healthcheck_script_stat.stat.exists
|
||||
fail_msg: "healthcheck script still exists after removal"
|
||||
|
||||
- name: Check subuid entry is absent
|
||||
ansible.builtin.command: "grep -c '^{{ gitea_runner_service_user }}:' /etc/subuid"
|
||||
register: subuid_check
|
||||
|
||||
@@ -4,7 +4,7 @@ driver:
|
||||
|
||||
platforms:
|
||||
- name: ${MOLECULE_PLATFORM_NAME:-ubuntu-2204}
|
||||
image: ${MOLECULE_PLATFORM_IMAGE:-ubuntu:22.04}
|
||||
image: ${MOLECULE_PLATFORM_IMAGE:-ubuntu:26.04}
|
||||
command: ${MOLECULE_PLATFORM_COMMAND:-sleep infinity}
|
||||
volumes:
|
||||
- /sys/fs/cgroup:/sys/fs/cgroup:rw
|
||||
|
||||
@@ -62,3 +62,45 @@
|
||||
- "'OnCalendar={{ gitea_runner_prune_schedule }}' in prune_timer.content | b64decode"
|
||||
- "'Persistent=true' in prune_timer.content | b64decode"
|
||||
fail_msg: "Prune timer template is missing expected directives"
|
||||
|
||||
- name: Read rendered healthcheck service template
|
||||
ansible.builtin.slurp:
|
||||
src: "{{ gitea_runner_home }}/.config/systemd/user/runner-healthcheck.service"
|
||||
register: healthcheck_service
|
||||
|
||||
- name: Assert healthcheck service contains expected directives
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- "'Type=oneshot' in healthcheck_service.content | b64decode"
|
||||
- "'ExecStart={{ gitea_runner_healthcheck_script_path }}' in healthcheck_service.content | b64decode"
|
||||
- "'DOCKER_HOST=unix:///run/user/' in healthcheck_service.content | b64decode"
|
||||
- "'XDG_RUNTIME_DIR=/run/user/' in healthcheck_service.content | b64decode"
|
||||
fail_msg: "Healthcheck service template is missing expected directives"
|
||||
|
||||
- name: Read rendered healthcheck timer template
|
||||
ansible.builtin.slurp:
|
||||
src: "{{ gitea_runner_home }}/.config/systemd/user/runner-healthcheck.timer"
|
||||
register: healthcheck_timer
|
||||
|
||||
- name: Assert healthcheck timer contains expected directives
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- "'OnBootSec={{ gitea_runner_healthcheck_boot_delay }}' in healthcheck_timer.content | b64decode"
|
||||
- "'OnUnitActiveSec={{ gitea_runner_healthcheck_interval }}' in healthcheck_timer.content | b64decode"
|
||||
- "'Persistent=true' in healthcheck_timer.content | b64decode"
|
||||
fail_msg: "Healthcheck timer template is missing expected directives"
|
||||
|
||||
- name: Read rendered healthcheck script
|
||||
ansible.builtin.slurp:
|
||||
src: "{{ gitea_runner_healthcheck_script_path }}"
|
||||
register: healthcheck_script
|
||||
|
||||
- name: Assert healthcheck script contains expected content
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- "'docker info' in healthcheck_script.content | b64decode"
|
||||
- "'systemctl --user restart docker.service' in healthcheck_script.content | b64decode"
|
||||
- "'systemctl --user restart gitea-runner.service' in healthcheck_script.content | b64decode"
|
||||
- "'docker system prune' in healthcheck_script.content | b64decode"
|
||||
- "gitea_runner_healthcheck_disk_threshold | string in healthcheck_script.content | b64decode"
|
||||
fail_msg: "Healthcheck script template is missing expected content"
|
||||
|
||||
@@ -4,7 +4,7 @@ driver:
|
||||
|
||||
platforms:
|
||||
- name: ${MOLECULE_PLATFORM_NAME:-ubuntu-2204}
|
||||
image: ${MOLECULE_PLATFORM_IMAGE:-ubuntu:22.04}
|
||||
image: ${MOLECULE_PLATFORM_IMAGE:-ubuntu:26.04}
|
||||
command: ${MOLECULE_PLATFORM_COMMAND:-sleep infinity}
|
||||
volumes:
|
||||
- /sys/fs/cgroup:/sys/fs/cgroup:rw
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
- name: Create healthcheck script
|
||||
ansible.builtin.template:
|
||||
src: runner-healthcheck.sh.j2
|
||||
dest: "{{ gitea_runner_healthcheck_script_path }}"
|
||||
owner: "{{ gitea_runner_service_user }}"
|
||||
group: "{{ gitea_runner_service_user }}"
|
||||
mode: "0755"
|
||||
|
||||
- name: Create healthcheck user service file
|
||||
ansible.builtin.template:
|
||||
src: runner-healthcheck.service.j2
|
||||
dest: "{{ gitea_runner_home }}/.config/systemd/user/runner-healthcheck.service"
|
||||
owner: "{{ gitea_runner_service_user }}"
|
||||
group: "{{ gitea_runner_service_user }}"
|
||||
mode: "0644"
|
||||
|
||||
- name: Create healthcheck user timer file
|
||||
ansible.builtin.template:
|
||||
src: runner-healthcheck.timer.j2
|
||||
dest: "{{ gitea_runner_home }}/.config/systemd/user/runner-healthcheck.timer"
|
||||
owner: "{{ gitea_runner_service_user }}"
|
||||
group: "{{ gitea_runner_service_user }}"
|
||||
mode: "0644"
|
||||
|
||||
- name: Reload systemd user daemon for healthcheck timer
|
||||
ansible.builtin.command: systemctl --user daemon-reload
|
||||
become: true
|
||||
become_user: "{{ gitea_runner_service_user }}"
|
||||
environment:
|
||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||
changed_when: true
|
||||
when:
|
||||
- systemd_available.stat.exists
|
||||
- docker_rootless_setup
|
||||
|
||||
- name: Enable and start healthcheck user timer
|
||||
ansible.builtin.command: systemctl --user enable --now runner-healthcheck.timer
|
||||
become: true
|
||||
become_user: "{{ gitea_runner_service_user }}"
|
||||
environment:
|
||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||
changed_when: true
|
||||
when:
|
||||
- systemd_available.stat.exists
|
||||
- docker_rootless_setup
|
||||
@@ -14,6 +14,9 @@
|
||||
- name: Include prune setup
|
||||
ansible.builtin.include_tasks: prune.yml
|
||||
|
||||
- name: Include healthcheck setup
|
||||
ansible.builtin.include_tasks: healthcheck.yml
|
||||
|
||||
- name: Include integration test
|
||||
ansible.builtin.include_tasks: integration_test.yml
|
||||
when: not skip_runner_registration
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Gitea Runner health check (Docker + service + disk)
|
||||
After=docker.service gitea-runner.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
Environment=DOCKER_HOST=unix:///run/user/{{ gitea_runner_uid }}/docker.sock
|
||||
Environment=XDG_RUNTIME_DIR=/run/user/{{ gitea_runner_uid }}
|
||||
ExecStart={{ gitea_runner_healthcheck_script_path }}
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/bin/bash
|
||||
# Health check for gitea-runner: verifies Docker daemon and runner service.
|
||||
# Exits 0 if healthy, 1 if Docker is down (triggers restart), 2 if runner is down.
|
||||
set -euo pipefail
|
||||
|
||||
DOCKER_HOST="unix:///run/user/{{ gitea_runner_uid }}/docker.sock"
|
||||
XDG_RUNTIME_DIR="/run/user/{{ gitea_runner_uid }}"
|
||||
export DOCKER_HOST XDG_RUNTIME_DIR
|
||||
|
||||
# 1. Check Docker daemon responsiveness
|
||||
if ! docker info >/dev/null 2>&1; then
|
||||
echo "ERROR: Docker daemon not responding at ${DOCKER_HOST}"
|
||||
systemctl --user restart docker.service
|
||||
sleep 3
|
||||
if ! docker info >/dev/null 2>&1; then
|
||||
echo "CRITICAL: Docker daemon still down after restart"
|
||||
exit 1
|
||||
fi
|
||||
echo "RECOVERED: Docker daemon restarted successfully"
|
||||
fi
|
||||
|
||||
# 2. Check gitea-runner service is active
|
||||
runner_state=$(systemctl --user is-active gitea-runner.service 2>/dev/null || true)
|
||||
if [[ "$runner_state" != "active" ]]; then
|
||||
echo "ERROR: gitea-runner service is ${runner_state}, restarting"
|
||||
systemctl --user restart gitea-runner.service
|
||||
sleep 2
|
||||
runner_state=$(systemctl --user is-active gitea-runner.service 2>/dev/null || true)
|
||||
if [[ "$runner_state" != "active" ]]; then
|
||||
echo "CRITICAL: gitea-runner service still down after restart"
|
||||
exit 2
|
||||
fi
|
||||
echo "RECOVERED: gitea-runner service restarted successfully"
|
||||
fi
|
||||
|
||||
# 3. Check disk space — prune aggressively if below threshold
|
||||
disk_pct=$(df -P / | awk 'NR==2 {gsub(/%/, "", $5); print $5}')
|
||||
if [[ "$disk_pct" -ge {{ gitea_runner_healthcheck_disk_threshold }} ]]; then
|
||||
echo "WARN: Disk usage at ${disk_pct}%, pruning all runner resources"
|
||||
docker system prune -af --filter "label={{ gitea_runner_prune_label }}" --filter "until=1h" || true
|
||||
docker volume prune -af --filter "label={{ gitea_runner_prune_label }}" || true
|
||||
# Also prune dangling images (no label)
|
||||
docker image prune -af || true
|
||||
disk_pct=$(df -P / | awk 'NR==2 {gsub(/%/, "", $5); print $5}')
|
||||
echo "INFO: Disk usage after prune: ${disk_pct}%"
|
||||
fi
|
||||
|
||||
echo "OK: runner healthy, disk at ${disk_pct}%"
|
||||
exit 0
|
||||
@@ -0,0 +1,10 @@
|
||||
[Unit]
|
||||
Description=Periodic Gitea Runner health check
|
||||
|
||||
[Timer]
|
||||
OnBootSec={{ gitea_runner_healthcheck_boot_delay }}
|
||||
OnUnitActiveSec={{ gitea_runner_healthcheck_interval }}
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
+6
-6
@@ -8,12 +8,12 @@ Each runner runs in an isolated **rootless Docker** environment under a dedicate
|
||||
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/src/branch/master/LICENSE)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/releases)
|
||||
[](https://www.python.org/downloads/)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/releases)
|
||||
[](https://www.python.org/downloads/)
|
||||
|
||||
## Overview
|
||||
|
||||
|
||||
@@ -31,13 +31,14 @@ grm install <host>
|
||||
├── rootless_docker.yml (rootless Docker setup under runner user)
|
||||
├── install_runner.yml (download binary, config, register, service)
|
||||
├── prune.yml (Docker prune timer)
|
||||
├── healthcheck.yml (health check script + systemd timer)
|
||||
└── integration_test.yml (validate service is active)
|
||||
```
|
||||
|
||||
The Ansible role task execution order (from `AGENTS.md`):
|
||||
|
||||
```
|
||||
main.yml → systemd_check → user_setup → rootless_docker → install_runner → prune → integration_test
|
||||
main.yml → systemd_check → user_setup → rootless_docker → install_runner → prune → healthcheck → integration_test
|
||||
```
|
||||
|
||||
- `install_runner.yml` handles: download, config, validate, register, service
|
||||
@@ -59,6 +60,7 @@ main.yml → systemd_check → user_setup → rootless_docker → install_runner
|
||||
| `register.yml` | Registers the runner with Gitea using the registration token |
|
||||
| `service.yml` | Creates the systemd user service file and starts/enables the service |
|
||||
| `prune.yml` | Creates a systemd user timer for daily Docker image and volume pruning |
|
||||
| `healthcheck.yml` | Installs a health check script and systemd timer that monitors Docker daemon, runner service, and disk space; restarts unhealthy services automatically |
|
||||
| `integration_test.yml` | Verifies the `.runner` file exists and the systemd service is active; optionally queries the Gitea API |
|
||||
| `deregister.yml` | Deregisters the runner from Gitea and removes the `.runner` file |
|
||||
| `update_runner.yml` | Downloads a new version of the gitea_runner binary |
|
||||
@@ -71,6 +73,9 @@ main.yml → systemd_check → user_setup → rootless_docker → install_runner
|
||||
| `gitea-runner-config.yaml.j2` | Runner configuration file (labels, capacity, log level) |
|
||||
| `docker-prune.service.j2` | Systemd user service for Docker pruning (oneshot) |
|
||||
| `docker-prune.timer.j2` | Systemd user timer triggering daily Docker prune |
|
||||
| `runner-healthcheck.sh.j2` | Health check script (checks Docker, runner service, disk space; restarts if down) |
|
||||
| `runner-healthcheck.service.j2` | Systemd user service for the health check (oneshot) |
|
||||
| `runner-healthcheck.timer.j2` | Systemd user timer triggering periodic health checks |
|
||||
|
||||
## Per-Runner Isolation
|
||||
|
||||
@@ -139,6 +144,7 @@ flowchart TD
|
||||
- Registers the runner with Gitea
|
||||
- Creates and starts the systemd user service
|
||||
- Sets up the Docker prune timer
|
||||
- Installs the health check script and systemd timer
|
||||
- Runs the integration test (verifies `.runner` file and service state)
|
||||
7. Ansible output is streamed to a timestamped log file at `~/.local/state/grm/logs/ansible-<timestamp>.log`
|
||||
8. On success, the runner is added to the local registry at `~/.local/share/grm/runners.json`
|
||||
|
||||
@@ -83,7 +83,7 @@ The platform list is defined in `devx.molecule.platforms` (single source of trut
|
||||
|
||||
### CI Test Distribution
|
||||
|
||||
CI runs all 6 scenarios x 4 platforms (24 test pairs) distributed across available Gitea Actions runners.
|
||||
CI runs all 7 scenarios x 4 platforms (28 test pairs) distributed across available Gitea Actions runners.
|
||||
|
||||
The `discover-runners` job runs `devx.molecule.discover_runners` which queries the Gitea API for registered runners at three levels (repo, org, instance) and generates a dynamic matrix. If the API query fails (e.g., no admin access for instance-level runners), it falls back to the `MOLECULE_RUNNERS` repo variable, then to a default of 3.
|
||||
|
||||
|
||||
@@ -10,11 +10,14 @@ GRM provides the following CLI commands for managing Gitea Actions runners. The
|
||||
| `grm update` | `<host>` | Update the gitea_runner binary on a remote host |
|
||||
| `grm start` | `<runner_name>` | Start a registered runner |
|
||||
| `grm stop` | `<runner_name>` | Stop a registered runner |
|
||||
| `grm restart` | `<runner_name>` | Restart a runner (stop, prune Docker images, start) |
|
||||
| `grm enable` | `<runner_name>` | Enable a runner to start on boot |
|
||||
| `grm disable` | `<runner_name>` | Disable and deregister a runner |
|
||||
| `grm status` | `<runner_name>` | Check the status of a registered runner |
|
||||
| `grm remove` | `<runner_name>` | Remove a runner completely |
|
||||
| `grm list` | — | List all registered runners with live status |
|
||||
| `grm health` | `[runner_name]` | Run health check (Docker, runner service, disk) on one or all runners |
|
||||
| `grm trigger-workflow` | `<workflow_id>` | Trigger a Gitea Actions workflow via the API |
|
||||
| `grm --version` | — | Show the installed version |
|
||||
|
||||
### Common lifecycle options
|
||||
@@ -139,6 +142,29 @@ grm stop <runner_name> [options]
|
||||
| `--key` | `-k` | Override SSH key from registry |
|
||||
| `--ask-become-pass/--no-ask-become-pass` | — | Prompt for sudo password (default) or skip it |
|
||||
|
||||
## restart
|
||||
|
||||
Restart a registered Gitea Runner (stop, prune Docker images, start).
|
||||
|
||||
```bash
|
||||
grm restart <runner_name> [options]
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
|
||||
| Argument | Description |
|
||||
|----------|-------------|
|
||||
| `runner_name` | Name of the registered runner |
|
||||
|
||||
**Options (common lifecycle options):**
|
||||
|
||||
| Option | Short | Description |
|
||||
|--------|-------|-------------|
|
||||
| `--host` | — | Override host from registry |
|
||||
| `--user` | `-u` | Override user from registry |
|
||||
| `--key` | `-k` | Override SSH key from registry |
|
||||
| `--ask-become-pass/--no-ask-become-pass` | — | Prompt for sudo password (default) or skip it |
|
||||
|
||||
## enable
|
||||
|
||||
Enable a registered Gitea Runner to start on boot.
|
||||
@@ -276,6 +302,70 @@ If no runners are registered:
|
||||
No runners registered. Use 'grm install' to add one.
|
||||
```
|
||||
|
||||
## health
|
||||
|
||||
Run a health check on one or all registered runners. Checks Docker daemon status, Gitea runner service status, and disk space usage. Unhealthy services are automatically restarted by the healthcheck script.
|
||||
|
||||
```bash
|
||||
grm health [runner_name] [options]
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
|
||||
| Argument | Description |
|
||||
|----------|-------------|
|
||||
| `runner_name` | (optional) Name of the runner to check. If omitted, checks all registered runners. |
|
||||
|
||||
**Options (common lifecycle options):**
|
||||
|
||||
| Option | Short | Description |
|
||||
|--------|-------|-------------|
|
||||
| `--host` | — | Override host from registry |
|
||||
| `--user` | `-u` | Override user from registry |
|
||||
| `--key` | `-k` | Override SSH key from registry |
|
||||
| `--ask-become-pass/--no-ask-become-pass` | — | Prompt for sudo password (default) or skip it |
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
grm health
|
||||
# Check a specific runner:
|
||||
grm health prod-runner
|
||||
```
|
||||
|
||||
Output shows NAME, HOST, HEALTHY (yes/no), and MESSAGE columns. The command exits with code 1 if any runner is unhealthy.
|
||||
|
||||
The health check is also run automatically via a systemd timer installed by the Ansible role. See `ansible/roles/gitea-runner/templates/runner-healthcheck.sh.j2` for the script and `runner-healthcheck.timer.j2` for the timer.
|
||||
|
||||
## trigger-workflow
|
||||
|
||||
Trigger a Gitea Actions workflow via the API.
|
||||
|
||||
```bash
|
||||
grm trigger-workflow <workflow_id> [options]
|
||||
grm trigger-workflow --list
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
|
||||
| Argument | Description |
|
||||
|----------|-------------|
|
||||
| `workflow_id` | Workflow filename (e.g., `ci.yml`) or ID |
|
||||
|
||||
**Options:**
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--list` | List available workflows in the repository |
|
||||
| `--ref` | Branch or tag to trigger on (default: repository default branch) |
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
grm trigger-workflow --list
|
||||
grm trigger-workflow ci.yml --ref master
|
||||
```
|
||||
|
||||
## --version
|
||||
|
||||
Show the installed GRM version.
|
||||
|
||||
+3
-3
@@ -34,7 +34,7 @@ ci = [
|
||||
"build==1.5.0",
|
||||
"twine==6.2.0",
|
||||
# Reusable CI/CD and dev tools (auto-merge, pr-review, pre-push checks, etc.)
|
||||
"devx==0.26.3",
|
||||
"devx==0.30.0",
|
||||
]
|
||||
# Lint and type-checking tools (quality job)
|
||||
lint = [
|
||||
@@ -47,14 +47,14 @@ lint = [
|
||||
]
|
||||
# Molecule testing (molecule-tests job)
|
||||
molecule = [
|
||||
"molecule==26.4.0",
|
||||
"molecule==26.6.0",
|
||||
"molecule-docker==2.1.0",
|
||||
]
|
||||
# Full dev environment (local development, includes everything)
|
||||
dev = [
|
||||
"gitea-runner-manager[ci,lint,molecule]",
|
||||
# Reusable CI/CD and dev tools (pre-push hooks, create-task, create-pr)
|
||||
"devx==0.26.3",
|
||||
"devx==0.30.0",
|
||||
# Non-Python dev dependency: checkmake (Makefile linter)
|
||||
# Install via: go install github.com/checkmake/checkmake/cmd/checkmake@latest
|
||||
]
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""Gitea Runner Manager — lean CLI for managing Gitea Actions runners."""
|
||||
|
||||
__version__ = "0.12.3"
|
||||
__version__ = "0.14.0"
|
||||
|
||||
@@ -442,6 +442,44 @@ def _collect_become_pass(ask_become_pass: bool) -> str | None:
|
||||
return sys.stdin.readline().strip() or None
|
||||
|
||||
|
||||
@cli.command(name="health", help=_("Run health check on one or all registered runners."))
|
||||
@click.argument("runner_name", required=False)
|
||||
@_runner_options
|
||||
@_handle_errors("Health check failed: {error}")
|
||||
def health(
|
||||
runner_name: str | None,
|
||||
host: str | None,
|
||||
user: str | None,
|
||||
key: str | None,
|
||||
ask_become_pass: bool,
|
||||
) -> None:
|
||||
"""Check Docker, runner service, and disk health on remote hosts."""
|
||||
become_pass = _collect_become_pass(ask_become_pass)
|
||||
manager = RunnerManager()
|
||||
results = manager.health(
|
||||
name=runner_name,
|
||||
host=host,
|
||||
user=user,
|
||||
key=key,
|
||||
ask_become_pass=ask_become_pass,
|
||||
become_pass=become_pass,
|
||||
become_password_file=_get_become_password_file(),
|
||||
verbose=_get_verbose(),
|
||||
)
|
||||
if not results:
|
||||
click.echo(_("No runners registered. Use 'grm install' to add one."))
|
||||
return
|
||||
click.echo(f"{_('NAME'):<18} {_('HOST'):<16} {_('HEALTHY'):<10} {_('MESSAGE')}")
|
||||
click.echo("-" * 80)
|
||||
all_healthy = True
|
||||
for r in results:
|
||||
if r["healthy"] != "yes":
|
||||
all_healthy = False
|
||||
click.echo(f"{r['name']:<18} {r['host']:<16} {r['healthy']:<10} {r['message']}")
|
||||
if not all_healthy:
|
||||
raise click.ClickException(_("One or more runners are unhealthy"))
|
||||
|
||||
|
||||
@cli.command(name="list", help=_("List all registered runners with live status."))
|
||||
@click.option(
|
||||
"--ask-become-pass/--no-ask-become-pass",
|
||||
|
||||
@@ -487,6 +487,68 @@ class RunnerManager:
|
||||
)
|
||||
return result
|
||||
|
||||
def health(
|
||||
self,
|
||||
name: str | None = None,
|
||||
host: str | None = None,
|
||||
user: str | None = None,
|
||||
key: str | None = None,
|
||||
ask_become_pass: bool = False,
|
||||
become_pass: str | None = None,
|
||||
become_password_file: str | None = None,
|
||||
verbose: bool = False,
|
||||
) -> list[dict[str, str]]:
|
||||
"""Run health check on one or all registered runners.
|
||||
|
||||
When *name* is provided, checks only that runner. Otherwise,
|
||||
checks all registered runners. Returns a list of dicts with
|
||||
``name``, ``host``, ``healthy`` (``"yes"``/``"no"``), and
|
||||
``message`` keys.
|
||||
"""
|
||||
if name:
|
||||
actual_host, actual_user, actual_key, _gitea_url = self._resolve_runner(name, host, user, key)
|
||||
entries = [(name, actual_host, actual_user, actual_key)]
|
||||
else:
|
||||
entries = [(n, info["host"], info["user"], info.get("key")) for n, info in self._registry.list().items()]
|
||||
|
||||
results: list[dict[str, str]] = []
|
||||
for runner_name, r_host, r_user, r_key in entries:
|
||||
say(_("Checking health of {name} on {host}", name=runner_name, host=r_host))
|
||||
healthy = "no"
|
||||
message = "unknown"
|
||||
try:
|
||||
stdout = self._executor.run_ad_hoc(
|
||||
r_host,
|
||||
r_user,
|
||||
r_key,
|
||||
"shell",
|
||||
f"sudo -u grm-{runner_name} "
|
||||
f"XDG_RUNTIME_DIR=/run/user/$(id -u grm-{runner_name}) "
|
||||
f"systemctl --user start runner-healthcheck.service && "
|
||||
f"journalctl --user -u runner-healthcheck.service --no-pager -n 1",
|
||||
become=True,
|
||||
ask_become_pass=ask_become_pass or become_password_file is not None,
|
||||
check=False,
|
||||
become_pass=become_pass,
|
||||
)
|
||||
if "OK:" in stdout:
|
||||
healthy = "yes"
|
||||
# Extract the OK line
|
||||
for line in stdout.splitlines():
|
||||
if "OK:" in line:
|
||||
message = line.split("OK:", 1)[1].strip()
|
||||
break
|
||||
else:
|
||||
for line in stdout.splitlines():
|
||||
stripped = line.strip()
|
||||
if stripped and "CHANGED" not in stripped and "WARNING" not in stripped:
|
||||
message = stripped
|
||||
break
|
||||
except AnsibleError as e:
|
||||
message = str(e)
|
||||
results.append({"name": runner_name, "host": r_host, "healthy": healthy, "message": message})
|
||||
return results
|
||||
|
||||
@staticmethod
|
||||
def _parse_status(stdout: str) -> str:
|
||||
ansible_noise = (" | CHANGED | ", " | FAILED | ", " | UNREACHABLE | ", "[WARNING]", "ssh:", ">>")
|
||||
|
||||
@@ -31,6 +31,14 @@
|
||||
"ru": "Проверить состояние зарегистрированного Gitea Runner.",
|
||||
"zh": "检查已注册的 Gitea Runner 状态。"
|
||||
},
|
||||
"Checking health of {name} on {host}": {
|
||||
"bg": "Проверка на здравословното състояние на {name} на {host}",
|
||||
"de": "Gesundheitsprüfung von {name} auf {host}",
|
||||
"en": "Checking health of {name} on {host}",
|
||||
"pl": "Sprawdzanie zdrowia {name} na {host}",
|
||||
"ru": "Проверка здоровья {name} на {host}",
|
||||
"zh": "正在检查 {host} 上 {name} 的健康状态"
|
||||
},
|
||||
"Checking status of Gitea Runner {name} on {host}": {
|
||||
"bg": "Проверка на състоянието на Gitea Runner {name} на {host}",
|
||||
"de": "Prüfe Status von Gitea Runner {name} auf {host}",
|
||||
@@ -175,6 +183,22 @@
|
||||
"ru": "Токен админ API Gitea для интеграционного теста (env: CI_GITEA_TOKEN)",
|
||||
"zh": "Gitea 管理员 API 令牌,用于集成测试(环境变量: CI_GITEA_TOKEN)"
|
||||
},
|
||||
"HEALTHY": {
|
||||
"bg": "ЗДРАВ",
|
||||
"de": "GESUND",
|
||||
"en": "HEALTHY",
|
||||
"pl": "ZDROWY",
|
||||
"ru": "ЗДОРОВ",
|
||||
"zh": "健康"
|
||||
},
|
||||
"Health check failed: {error}": {
|
||||
"bg": "Проверката на здравословното състояние неуспешна: {error}",
|
||||
"de": "Gesundheitsprüfung fehlgeschlagen: {error}",
|
||||
"en": "Health check failed: {error}",
|
||||
"pl": "Sprawdzanie zdrowia nie powiodło się: {error}",
|
||||
"ru": "Проверка здоровья не удалась: {error}",
|
||||
"zh": "健康检查失败: {error}"
|
||||
},
|
||||
"HOST": {
|
||||
"bg": "ХОСТ",
|
||||
"de": "HOST",
|
||||
@@ -239,6 +263,14 @@
|
||||
"ru": "Ошибка списка: {error}",
|
||||
"zh": "列表失败: {error}"
|
||||
},
|
||||
"MESSAGE": {
|
||||
"bg": "СЪОБЩЕНИЕ",
|
||||
"de": "MELDUNG",
|
||||
"en": "MESSAGE",
|
||||
"pl": "WIADOMOŚĆ",
|
||||
"ru": "СООБЩЕНИЕ",
|
||||
"zh": "消息"
|
||||
},
|
||||
"NAME": {
|
||||
"bg": "ИМЕ",
|
||||
"de": "NAME",
|
||||
@@ -263,6 +295,14 @@
|
||||
"ru": "Нет зарегистрированных runners. Используйте 'grm install' чтобы добавить.",
|
||||
"zh": "没有已注册的 runners。使用 'grm install' 添加一个。"
|
||||
},
|
||||
"One or more runners are unhealthy": {
|
||||
"bg": "Един или повече runners са нездравословни",
|
||||
"de": "Ein oder mehrere Runner sind fehlerhaft",
|
||||
"en": "One or more runners are unhealthy",
|
||||
"pl": "Jeden lub więcej runnerów jest w złym stanie",
|
||||
"ru": "Один или несколько runners нездоровы",
|
||||
"zh": "一个或多个 runners 不健康"
|
||||
},
|
||||
"Override SSH key from registry": {
|
||||
"bg": "Замяна на SSH ключа от регистъра",
|
||||
"de": "SSH-Schlüssel aus Registrierung überschreiben",
|
||||
@@ -423,6 +463,14 @@
|
||||
"ru": "Выполнение Ansible playbook",
|
||||
"zh": "正在运行 Ansible playbook"
|
||||
},
|
||||
"Run health check on one or all registered runners.": {
|
||||
"bg": "Проверка на здравословното състояние на един или всички регистрирани runners.",
|
||||
"de": "Gesundheitsprüfung für einen oder alle registrierten Runner ausführen.",
|
||||
"en": "Run health check on one or all registered runners.",
|
||||
"pl": "Uruchom sprawdzanie zdrowia jednego lub wszystkich zarejestrowanych runnerów.",
|
||||
"ru": "Проверить здоровье одного или всех зарегистрированных runners.",
|
||||
"zh": "对一个或所有已注册 runners 运行健康检查。"
|
||||
},
|
||||
"SSH user (env: GITEA_RUNNER_USER)": {
|
||||
"bg": "SSH потребител (env: GITEA_RUNNER_USER)",
|
||||
"de": "SSH-Benutzer (env: GITEA_RUNNER_USER)",
|
||||
|
||||
@@ -852,6 +852,75 @@ class TestCLI:
|
||||
assert result.exit_code != 0
|
||||
assert "fail" in result.output
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
def test_health_all_healthy(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
mock_manager.health.return_value = [
|
||||
{"name": "r1", "host": "10.0.0.1", "healthy": "yes", "message": "runner healthy, disk at 42%"},
|
||||
{"name": "r2", "host": "10.0.0.2", "healthy": "yes", "message": "runner healthy, disk at 50%"},
|
||||
]
|
||||
mock_manager_class.return_value = mock_manager
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ["health"], input="secret\n")
|
||||
assert result.exit_code == 0
|
||||
assert "r1" in result.output
|
||||
assert "r2" in result.output
|
||||
assert "yes" in result.output
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
def test_health_with_unhealthy(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
mock_manager.health.return_value = [
|
||||
{"name": "r1", "host": "10.0.0.1", "healthy": "yes", "message": "runner healthy, disk at 42%"},
|
||||
{"name": "r2", "host": "10.0.0.2", "healthy": "no", "message": "Docker daemon down"},
|
||||
]
|
||||
mock_manager_class.return_value = mock_manager
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ["health"], input="secret\n")
|
||||
assert result.exit_code != 0
|
||||
assert "unhealthy" in result.output.lower()
|
||||
assert "r2" in result.output
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
def test_health_single_runner(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
mock_manager.health.return_value = [
|
||||
{"name": "r1", "host": "10.0.0.1", "healthy": "yes", "message": "runner healthy, disk at 42%"},
|
||||
]
|
||||
mock_manager_class.return_value = mock_manager
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ["health", "r1"], input="secret\n")
|
||||
assert result.exit_code == 0
|
||||
assert "r1" in result.output
|
||||
mock_manager.health.assert_called_once()
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
def test_health_empty(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
mock_manager.health.return_value = []
|
||||
mock_manager_class.return_value = mock_manager
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ["health"], input="secret\n")
|
||||
assert result.exit_code == 0
|
||||
assert "No runners registered" in result.output
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
def test_health_error(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
from gitea_runner_manager.exceptions import AnsibleError
|
||||
|
||||
mock_manager.health.side_effect = AnsibleError("fail")
|
||||
mock_manager_class.return_value = mock_manager
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ["health"], input="secret\n")
|
||||
assert result.exit_code != 0
|
||||
assert "fail" in result.output
|
||||
|
||||
@patch("gitea_runner_manager.cli.os.getlogin", side_effect=OSError("no tty"))
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
def test_default_user_fallback_on_getlogin_error(
|
||||
|
||||
@@ -625,6 +625,112 @@ class TestRunnerManager:
|
||||
assert manager.list_runners(no_status=True) == []
|
||||
|
||||
|
||||
class TestHealth:
|
||||
"""Tests for the ``health`` method."""
|
||||
|
||||
def test_health_single_runner_healthy(self) -> None:
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.get.return_value = {"host": "10.0.0.1", "user": "ubuntu", "key": "/key"}
|
||||
manager = RunnerManager(registry=mock_registry)
|
||||
mock_executor = MagicMock()
|
||||
mock_executor.run_ad_hoc.return_value = "OK: runner healthy, disk at 42%"
|
||||
manager._executor = mock_executor
|
||||
|
||||
results = manager.health(name="r1")
|
||||
assert len(results) == 1
|
||||
assert results[0]["name"] == "r1"
|
||||
assert results[0]["healthy"] == "yes"
|
||||
assert "runner healthy" in results[0]["message"]
|
||||
mock_executor.run_ad_hoc.assert_called_once()
|
||||
|
||||
def test_health_single_runner_unhealthy(self) -> None:
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.get.return_value = {"host": "10.0.0.1", "user": "ubuntu", "key": None}
|
||||
manager = RunnerManager(registry=mock_registry)
|
||||
mock_executor = MagicMock()
|
||||
mock_executor.run_ad_hoc.return_value = "CRITICAL: Docker daemon still down after restart"
|
||||
manager._executor = mock_executor
|
||||
|
||||
results = manager.health(name="r1")
|
||||
assert len(results) == 1
|
||||
assert results[0]["healthy"] == "no"
|
||||
assert "Docker daemon still down" in results[0]["message"]
|
||||
|
||||
def test_health_all_runners(self) -> None:
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.list.return_value = {
|
||||
"r1": {"host": "10.0.0.1", "user": "ubuntu", "key": None},
|
||||
"r2": {"host": "10.0.0.2", "user": "ubuntu", "key": None},
|
||||
}
|
||||
manager = RunnerManager(registry=mock_registry)
|
||||
mock_executor = MagicMock()
|
||||
mock_executor.run_ad_hoc.side_effect = [
|
||||
"OK: runner healthy, disk at 42%",
|
||||
"ERROR: gitea-runner service is inactive, restarting",
|
||||
]
|
||||
manager._executor = mock_executor
|
||||
|
||||
results = manager.health()
|
||||
assert len(results) == 2
|
||||
assert results[0]["healthy"] == "yes"
|
||||
assert results[1]["healthy"] == "no"
|
||||
|
||||
def test_health_empty_registry(self) -> None:
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.list.return_value = {}
|
||||
manager = RunnerManager(registry=mock_registry)
|
||||
results = manager.health()
|
||||
assert results == []
|
||||
|
||||
def test_health_ansible_error(self) -> None:
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.get.return_value = {"host": "10.0.0.1", "user": "ubuntu", "key": None}
|
||||
manager = RunnerManager(registry=mock_registry)
|
||||
mock_executor = MagicMock()
|
||||
mock_executor.run_ad_hoc.side_effect = AnsibleError("ssh unreachable")
|
||||
manager._executor = mock_executor
|
||||
|
||||
results = manager.health(name="r1")
|
||||
assert len(results) == 1
|
||||
assert results[0]["healthy"] == "no"
|
||||
assert "ssh unreachable" in results[0]["message"]
|
||||
|
||||
def test_health_with_host_override(self) -> None:
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.get.return_value = {"host": "10.0.0.1", "user": "ubuntu", "key": None}
|
||||
manager = RunnerManager(registry=mock_registry)
|
||||
mock_executor = MagicMock()
|
||||
mock_executor.run_ad_hoc.return_value = "OK: runner healthy, disk at 50%"
|
||||
manager._executor = mock_executor
|
||||
|
||||
results = manager.health(name="r1", host="10.0.0.99", user="root")
|
||||
assert len(results) == 1
|
||||
assert results[0]["host"] == "10.0.0.99"
|
||||
call_args = mock_executor.run_ad_hoc.call_args.args
|
||||
assert call_args[0] == "10.0.0.99"
|
||||
assert call_args[1] == "root"
|
||||
|
||||
def test_health_runner_not_found(self) -> None:
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.get.return_value = None
|
||||
manager = RunnerManager(registry=mock_registry)
|
||||
with pytest.raises(AnsibleError, match="not found in registry"):
|
||||
manager.health(name="nonexistent")
|
||||
|
||||
def test_health_passes_become_pass(self) -> None:
|
||||
"""become_pass is forwarded to run_ad_hoc for sudo authentication."""
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.get.return_value = {"host": "10.0.0.1", "user": "ubuntu", "key": None}
|
||||
manager = RunnerManager(registry=mock_registry)
|
||||
mock_executor = MagicMock()
|
||||
mock_executor.run_ad_hoc.return_value = "OK: runner healthy, disk at 42%"
|
||||
manager._executor = mock_executor
|
||||
|
||||
manager.health(name="r1", become_pass="s3cr3t")
|
||||
call_kwargs = mock_executor.run_ad_hoc.call_args.kwargs
|
||||
assert call_kwargs["become_pass"] == "s3cr3t"
|
||||
|
||||
|
||||
class TestExtraVarsFile:
|
||||
"""Tests for the ``_extra_vars_file`` context manager."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user