Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9609ef2505 | ||
|
|
97e7687e25 | ||
|
|
f672da1753 | ||
|
|
d6549de2e0 | ||
|
|
dc4bb0936d | ||
|
|
52477e558a | ||
|
|
28214de583 | ||
|
|
7fd023d192 | ||
|
|
2ffedaa793 | ||
|
|
e7b2d4e6af | ||
|
|
bda2af91bc | ||
|
|
c72dc97f63 | ||
|
|
db9fb6f162 | ||
|
|
d102453960 | ||
|
|
42409d9e47 | ||
|
|
91e880f05c | ||
|
|
2d2eaa3291 | ||
|
|
8176a62885 | ||
|
|
443756a508 | ||
|
|
340222e041 | ||
|
|
179e47bbb2 | ||
|
|
68d16577b3 | ||
|
|
38607d9f29 | ||
|
|
90139b306b | ||
|
|
dd475bec0d | ||
|
|
0f0ada3576 | ||
|
|
185e41c49e | ||
|
|
103741b3ab | ||
|
|
b770d1debf | ||
|
|
2f11489be0 | ||
|
|
8e9e58fedb | ||
|
|
70d985ebaa | ||
|
|
66f071b676 | ||
|
|
7e18d285ab | ||
|
|
f6ba60bda6 | ||
|
|
0545388b34 | ||
|
|
eb0a56350b | ||
|
|
f712a4493e | ||
|
|
9289b19162 | ||
|
|
185251090f | ||
|
|
fff930920f | ||
|
|
88eca1f6b8 | ||
|
|
1718a415c8 | ||
|
|
3f27ee1423 | ||
|
|
2cf267bace | ||
|
|
87513f9e8f | ||
|
|
8b1a959bc9 | ||
|
|
f6a4f1fe43 | ||
|
|
9ea7ae656b | ||
|
|
70240a13cf | ||
|
|
fd7b786db4 | ||
|
|
c4fe70979c | ||
|
|
dd9fc601fb | ||
|
|
62b37d045e | ||
|
|
390fcb9d4b | ||
|
|
9f8adf14bc | ||
|
|
92e4d2fd2b | ||
|
|
f30764d60f | ||
|
|
a0e3fc09c9 | ||
|
|
bb9e8e6c4b | ||
|
|
d8312ff62c | ||
|
|
39a8f20df2 | ||
|
|
240f08fff4 | ||
|
|
d38a64b0e1 | ||
|
|
89bd40c4a8 | ||
|
|
ab2101983f | ||
|
|
12f4aa4c92 | ||
|
|
60ad1ad30d | ||
|
|
0e64353b0d | ||
|
|
5956bb4fac | ||
|
|
9879ac5935 | ||
|
|
89a765eca6 | ||
|
|
8625f66151 | ||
|
|
7449a05506 | ||
|
|
c61933c4a2 | ||
|
|
dde8445ed4 | ||
|
|
621cc87664 | ||
|
|
262fd57771 | ||
|
|
8a41b1237d | ||
|
|
34742bab40 | ||
|
|
d3dbb17cc2 | ||
|
|
751f594ce5 | ||
|
|
d8caebee2d | ||
|
|
bb4cc80a98 | ||
|
|
e0b2e64b8e | ||
|
|
00404cb484 | ||
|
|
1a30b595dc | ||
|
|
3538eb0803 | ||
|
|
5a93559b79 | ||
|
|
e30acbe213 | ||
|
|
386f3a88c6 | ||
|
|
e99e9d0ac8 | ||
|
|
ca1d8e5cc0 | ||
|
|
83e800c900 | ||
|
|
be17dc278c | ||
|
|
58b8d5b5de | ||
|
|
48f23b554f | ||
|
|
d32bb40cbd | ||
|
|
34954aa396 | ||
|
|
b9d728334f | ||
|
|
02d7c02a19 | ||
|
|
f861d14f32 | ||
|
|
4359dbdc26 | ||
|
|
fc494f5cc0 | ||
|
|
461ec207ad | ||
|
|
dca82753b2 | ||
|
|
3b952b09b5 | ||
|
|
833792d0ad | ||
|
|
d8a90eaea1 | ||
|
|
358620401d | ||
|
|
32f0ad5cb3 | ||
|
|
4e9d033a40 | ||
|
|
63ef5cdbcf | ||
|
|
8fbe2d3f51 | ||
|
|
a9178714af | ||
|
|
6ffcc38181 | ||
|
|
e5b0e17ec3 | ||
|
|
dc5e1431b5 | ||
|
|
dfa8d77bfa | ||
|
|
a178b1b5d2 | ||
|
|
99529a57af | ||
|
|
0eb033419f | ||
|
|
df4b7f2a19 | ||
|
|
312a706d39 | ||
|
|
ea2f0cc600 | ||
|
|
bd8e13ee66 | ||
|
|
41fc36ff4a | ||
|
|
e585543e9d | ||
|
|
c62c35f5b6 | ||
|
|
4b900ce673 | ||
|
|
cae66e0743 | ||
|
|
0b3a76c550 | ||
|
|
a4d5ba6b70 | ||
|
|
d9ce4e240f | ||
|
|
c1f68f115a | ||
|
|
fdf1293c85 | ||
|
|
01b3f594f7 | ||
|
|
9ffa7a3671 | ||
|
|
f04be9c39b | ||
|
|
f67dff8458 | ||
|
|
763f7640af | ||
|
|
f8eeea611f | ||
|
|
774bf479ab | ||
|
|
844171ee93 | ||
|
|
9a5be879a2 | ||
|
|
f24ed4c963 | ||
|
|
d1e1d05be5 | ||
|
|
dbb9bd7108 | ||
|
|
f905550aba | ||
|
|
cae4e2a860 | ||
|
|
efbd24daec | ||
|
|
9066ef9724 | ||
|
|
96770a770e | ||
|
|
e753b34788 | ||
|
|
48422b18e5 | ||
|
|
190157cce6 | ||
|
|
1d0a082044 | ||
|
|
799d36f254 | ||
|
|
e0d43b0ed8 | ||
|
|
3189161f61 | ||
|
|
c339698603 | ||
|
|
cbf082c78f | ||
|
|
4070135fda | ||
|
|
ace0176e3a | ||
|
|
fda1d99d86 | ||
|
|
465f45d939 | ||
|
|
103beaa0e8 | ||
|
|
2e97578269 | ||
|
|
c31ec312ac | ||
|
|
c67b810e58 | ||
|
|
ef16b07cdf | ||
|
|
2c849c7324 | ||
|
|
5804a18974 | ||
|
|
9dbe20ba73 | ||
|
|
b92c87ba68 | ||
|
|
dc4430d160 | ||
|
|
7aa0ebaefe | ||
|
|
e93da43219 | ||
|
|
b131a2872d | ||
|
|
e4dd8f308f | ||
|
|
467e0d66e6 | ||
|
|
6ee5b74bb5 | ||
|
|
21cc89899f | ||
|
|
0382e155a6 | ||
|
|
d87c0d7e9a | ||
|
|
4be480a18e | ||
|
|
de92f675ed | ||
|
|
b5803a8611 | ||
|
|
ec22d20a15 | ||
|
|
e96012af7f | ||
|
|
addef7500c | ||
|
|
8a0d2c428b | ||
|
|
d68fb6d272 | ||
|
|
6721864b87 | ||
|
|
06471d1403 | ||
|
|
1a04971622 | ||
|
|
91440c1b0a | ||
|
|
3bb8d75748 | ||
|
|
d4a8172a25 | ||
|
|
2199ec0bfe | ||
|
|
9ae9a76b11 | ||
|
|
31d0eeae98 | ||
|
|
acc768eaea | ||
|
|
cf2314c20f | ||
|
|
aac47e3472 | ||
|
|
811e7a2309 | ||
|
|
cb68c85bb5 | ||
|
|
0ba30e09ea | ||
|
|
6a02581687 |
@@ -0,0 +1,185 @@
|
|||||||
|
---
|
||||||
|
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 & Virtual Environment
|
||||||
|
|
||||||
|
The grm repo is at `/home/emo/dev/ideas/oblachno/grm`. Always `cd` there first.
|
||||||
|
|
||||||
|
All Python tools run inside `.venv`. `make` targets handle activation
|
||||||
|
automatically — always use `make <target>`, never raw `pytest` or `ruff`
|
||||||
|
commands. If `.venv` doesn't exist, run `make setup` first.
|
||||||
|
|
||||||
|
## 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/grm/__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,155 @@
|
|||||||
|
---
|
||||||
|
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 & Virtual Environment
|
||||||
|
|
||||||
|
The grm repo is at `/home/emo/dev/ideas/oblachno/grm`. Always `cd` there first.
|
||||||
|
|
||||||
|
All Python tools run inside `.venv`. `make` targets handle activation
|
||||||
|
automatically — always use `make <target>`, never raw `pytest` or `ruff`
|
||||||
|
commands. If `.venv` doesn't exist, run `make setup` 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
|
||||||
|
.venv/bin/python -m devx.tools.check_pyproject_deps
|
||||||
|
.venv/bin/python -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,138 @@
|
|||||||
|
---
|
||||||
|
name: doc-sync-specialist
|
||||||
|
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 & Virtual Environment
|
||||||
|
|
||||||
|
The grm repo is at `/home/emo/dev/ideas/oblachno/grm`. Always `cd` there first.
|
||||||
|
|
||||||
|
All Python tools run inside `.venv`. `make` targets handle activation
|
||||||
|
automatically — always use `make <target>`, never raw `pytest` or `ruff`
|
||||||
|
commands. If `.venv` doesn't exist, run `make setup` 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
|
||||||
|
.venv/bin/python -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
|
||||||
|
.venv/bin/python -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
|
||||||
|
.venv/bin/python -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,158 @@
|
|||||||
|
---
|
||||||
|
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 & Virtual Environment
|
||||||
|
|
||||||
|
The grm repo is at `/home/emo/dev/ideas/oblachno/grm`. Always `cd` there first.
|
||||||
|
|
||||||
|
All Python tools run inside `.venv`. `make` targets handle activation
|
||||||
|
automatically — always use `make <target>`, never raw `pytest` or `ruff`
|
||||||
|
commands. If `.venv` doesn't exist, run `make setup` 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,154 @@
|
|||||||
|
---
|
||||||
|
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 & Virtual Environment
|
||||||
|
|
||||||
|
The grm repo is at `/home/emo/dev/ideas/oblachno/grm`. Always `cd` there first.
|
||||||
|
|
||||||
|
All Python tools run inside `.venv`. `make` targets handle activation
|
||||||
|
automatically — always use `make <target>`, never raw `pytest` or `ruff`
|
||||||
|
commands. If `.venv` doesn't exist, run `make setup` 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`)
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# testing-and-debugging
|
||||||
|
|
||||||
|
Make targets for testing, debugging, and CI investigation. **Use these
|
||||||
|
instead of raw `pytest`, `ruff`, or `molecule` commands.**
|
||||||
|
|
||||||
|
## Why Make Targets
|
||||||
|
|
||||||
|
Make targets encapsulate the correct venv activation, PYTHONPATH, env
|
||||||
|
vars, and flags. Running raw commands bypasses venv activation and
|
||||||
|
produces false failures (missing dependencies, wrong Python version).
|
||||||
|
|
||||||
|
## Unit Tests
|
||||||
|
|
||||||
|
| Task | Command | Notes |
|
||||||
|
|------|---------|-------|
|
||||||
|
| Run all unit tests | `make test-unit` | Fast, no coverage |
|
||||||
|
| Run with coverage | `make pytest-cov` | **Required before push** — enforces 100% |
|
||||||
|
| Run single test | `make pytest-cov TEST=tests/test_foo.py::test_bar` | |
|
||||||
|
|
||||||
|
## Linting
|
||||||
|
|
||||||
|
| Task | Command | Notes |
|
||||||
|
|------|---------|-------|
|
||||||
|
| Full lint | `make lint-all` | ruff + pyright + bandit + ansible-lint + checkmake + actionlint |
|
||||||
|
| Ruff only | `make lint-ruff` | |
|
||||||
|
| Type check | `make typecheck` | pyright |
|
||||||
|
| Bandit | `make lint-bandit` | Security linter |
|
||||||
|
| Workflow lint | `make workflow-check` | actionlint + act_runner dry-run |
|
||||||
|
|
||||||
|
## Molecule Tests
|
||||||
|
|
||||||
|
| Task | Command | Notes |
|
||||||
|
|------|---------|-------|
|
||||||
|
| All scenarios | `make molecule` | All 6 scenarios on Ubuntu 22.04 |
|
||||||
|
| All platforms | `make molecule-all` | All 6 scenarios on all 4 OSes |
|
||||||
|
| Parallel | `make molecule-all-parallel` | MOLECULE_JOBS=4 |
|
||||||
|
|
||||||
|
## Pre-Push Verification
|
||||||
|
|
||||||
|
**Before pushing any branch:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make pre-push
|
||||||
|
```
|
||||||
|
|
||||||
|
This runs `lint-all` + `pytest-cov`. The pre-push git hook only
|
||||||
|
validates the Vikunja task exists — it does NOT run tests. You must
|
||||||
|
run `make pre-push` manually.
|
||||||
|
|
||||||
|
## CI Failure Investigation
|
||||||
|
|
||||||
|
When investigating a CI failure:
|
||||||
|
|
||||||
|
1. **Fetch logs via MCP** — use `mcp_call_tool` with gitea server,
|
||||||
|
`actions_run_read` method, `download_job_log` tool
|
||||||
|
2. **Reproduce locally** — use `make pytest-cov` or `make lint-ci`
|
||||||
|
depending on which CI job failed
|
||||||
|
3. **Never run raw pytest** — always use the make target
|
||||||
|
|
||||||
|
## Virtual Environment
|
||||||
|
|
||||||
|
All commands run inside `.venv`. `make` targets handle activation
|
||||||
|
automatically. For raw commands (rare), activate first:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source activate.sh # bash/zsh
|
||||||
|
source activate.fish # fish
|
||||||
|
source activate.zsh # zsh
|
||||||
|
```
|
||||||
|
|
||||||
|
If `.venv` doesn't exist, run `make setup` first.
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
### Coverage Verification Before Push
|
||||||
|
|
||||||
|
**Always run `make pytest-cov` before pushing** — CI enforces 100%
|
||||||
|
coverage and will fail the PR if any lines are uncovered. This is the
|
||||||
|
most common cause of CI validate job failures after code changes. The
|
||||||
|
pre-push git hook only validates Vikunja task existence, not tests.
|
||||||
|
|
||||||
|
### API Response Type Checking
|
||||||
|
|
||||||
|
Never use `is True`/`is False` identity checks on API response values.
|
||||||
|
Many APIs return boolean values as strings (`"true"`/`"false"`). Use
|
||||||
|
string comparison or truthy/falsy helpers instead.
|
||||||
|
|
||||||
|
### Time Mocking in Tests
|
||||||
|
|
||||||
|
Always mock `time.sleep` and `time.monotonic` in unit tests using
|
||||||
|
`@patch` decorators. Real sleep calls make tests slow and exceed test
|
||||||
|
speed limits.
|
||||||
+31
-3
@@ -15,7 +15,8 @@ GITEA_REGISTRATION_TOKEN=your-registration-token
|
|||||||
# If set, API checks are performed as a bonus but do NOT affect pass/fail.
|
# If set, API checks are performed as a bonus but do NOT affect pass/fail.
|
||||||
# Required scopes: read:user, read:repository, read:admin (or just "admin")
|
# Required scopes: read:user, read:repository, read:admin (or just "admin")
|
||||||
# Generate token at: Settings → Applications → Generate New Token
|
# Generate token at: Settings → Applications → Generate New Token
|
||||||
# REPO_TOKEN=your-admin-api-token
|
# CI_GITEA_API_TOKEN=your-admin-api-token
|
||||||
|
# Legacy CI_GITEA_TOKEN is also accepted.
|
||||||
|
|
||||||
# Integration test API retries (optional, default: 3).
|
# Integration test API retries (optional, default: 3).
|
||||||
# Number of times to retry API checks waiting for runner to appear.
|
# Number of times to retry API checks waiting for runner to appear.
|
||||||
@@ -24,6 +25,9 @@ GITEA_REGISTRATION_TOKEN=your-registration-token
|
|||||||
# Default SSH user for remote hosts (optional, overrides --user)
|
# Default SSH user for remote hosts (optional, overrides --user)
|
||||||
# GITEA_RUNNER_USER=ubuntu
|
# GITEA_RUNNER_USER=ubuntu
|
||||||
|
|
||||||
|
# Repository for grm trigger-workflow (optional, default: oblachno-oss/grm)
|
||||||
|
# GRM_REPO=oblachno-oss/grm
|
||||||
|
|
||||||
# Default SSH private key path (optional, overrides --key)
|
# Default SSH private key path (optional, overrides --key)
|
||||||
# GITEA_RUNNER_KEY=~/.ssh/id_ed25519
|
# GITEA_RUNNER_KEY=~/.ssh/id_ed25519
|
||||||
|
|
||||||
@@ -34,13 +38,37 @@ GITEA_REGISTRATION_TOKEN=your-registration-token
|
|||||||
# GITEA_RUNNER_LABELS=docker:docker://gitea/runner-images:ubuntu-latest
|
# GITEA_RUNNER_LABELS=docker:docker://gitea/runner-images:ubuntu-latest
|
||||||
|
|
||||||
# UI language for GRM console messages (optional, default: en)
|
# UI language for GRM console messages (optional, default: en)
|
||||||
# Supported: en, bg, de, ru, zh
|
# Supported: en, bg, de, ru, zh, pl
|
||||||
# GRM_LANG=en
|
# GRM_LANG=en
|
||||||
|
|
||||||
|
# Sudo password file for Ansible become operations (optional)
|
||||||
|
# When set, GRM reads the sudo password from this file instead of prompting.
|
||||||
|
# Priority: --become-password-file CLI flag > GRM_BECOME_PASSWORD_FILE > ANSIBLE_BECOME_PASSWORD_FILE
|
||||||
|
# GRM_BECOME_PASSWORD_FILE=~/.grm-sudo-pass
|
||||||
|
# ANSIBLE_BECOME_PASSWORD_FILE=~/.grm-sudo-pass
|
||||||
|
|
||||||
|
# Gitea PyPI registry username (for private package access)
|
||||||
|
# Used by PIP_INSTALL to configure PIP_EXTRA_INDEX_URL
|
||||||
|
CI_GITEA_USERNAME=your-gitea-username
|
||||||
|
|
||||||
|
# Role-based Gitea API tokens (devx 0.40.0+)
|
||||||
|
# DEVELOPER_GITEA_API_TOKEN is used by local `grm trigger-workflow` and `make create-pr`.
|
||||||
|
# CI_GITEA_API_TOKEN is used by CI workflows (and accepted as a fallback for local tools).
|
||||||
|
# REVIEWER_GITEA_API_TOKEN is used by CI to post APPROVE reviews; it must belong to a
|
||||||
|
# different user than the PR author.
|
||||||
|
# Legacy CI_GITEA_TOKEN and REVIEW_GITEA_TOKEN are accepted as fallbacks.
|
||||||
|
# DEVELOPER_GITEA_API_TOKEN=your-developer-token
|
||||||
|
# CI_GITEA_API_TOKEN=your-ci-token
|
||||||
|
# REVIEWER_GITEA_API_TOKEN=your-reviewer-token
|
||||||
|
|
||||||
|
# Vikunja API token (required for `make create-task` dev workflow)
|
||||||
|
# Generate at: Vikunja → Settings → API Tokens
|
||||||
|
# VIKUNJA_TOKEN=your-vikunja-api-token
|
||||||
|
|
||||||
# devx configuration (GRM-specific overrides)
|
# devx configuration (GRM-specific overrides)
|
||||||
# Task prefix for Vikunja task IDs
|
# Task prefix for Vikunja task IDs
|
||||||
DEVX_TASK_PREFIX=GRM
|
DEVX_TASK_PREFIX=GRM
|
||||||
# Vikunja project ID for GRM
|
# Vikunja project ID for GRM
|
||||||
DEVX_VIKUNJA_PROJECT_ID=6
|
DEVX_VIKUNJA_PROJECT_ID=6
|
||||||
# Version file path (relative to repo root)
|
# Version file path (relative to repo root)
|
||||||
DEVX_VERSION_FILE=src/gitea_runner_manager/__init__.py
|
DEVX_VERSION_FILE=src/grm/__init__.py
|
||||||
|
|||||||
+296
-171
@@ -5,41 +5,75 @@ on:
|
|||||||
types: [opened, synchronize]
|
types: [opened, synchronize]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
PIP_BREAK_SYSTEM_PACKAGES: "1"
|
||||||
|
PYTHONPATH: src
|
||||||
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
|
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
quality:
|
# Single validation job that merges: quality, detect-changes,
|
||||||
|
# release-dry-run, pre-merge-check, pr-review, and discover-runners.
|
||||||
|
# Uses ci-full image (has git-cliff for release-dry-run).
|
||||||
|
# Saves ~5x checkout+setup overhead vs 6 separate jobs.
|
||||||
|
validate:
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
timeout-minutes: 10
|
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
outputs:
|
||||||
|
ansible-changed: ${{ steps.detect.outputs.ansible-changed }}
|
||||||
|
user-facing-changed: ${{ steps.detect.outputs.user-facing-changed }}
|
||||||
|
runner-count: ${{ steps.discover-runners.outputs.runner-count }}
|
||||||
|
runner-indices: ${{ steps.discover-runners.outputs.runner-indices }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
- name: Set up environment
|
- name: Set up environment
|
||||||
env:
|
env:
|
||||||
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
run: make setup-quality
|
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
||||||
|
run: make setup-image EXTRAS=ci,lint
|
||||||
|
# --- quality steps ---
|
||||||
- name: Lint all
|
- name: Lint all
|
||||||
run: |
|
run: |
|
||||||
. .venv/bin/activate
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
make lint-all
|
make lint-all
|
||||||
- name: Unit tests with 100% coverage
|
- name: Unit tests with 100% coverage
|
||||||
run: |
|
run: |
|
||||||
. .venv/bin/activate
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
make pytest-cov
|
make pytest-cov
|
||||||
- name: Check unit test speed
|
- name: Documentation gate (coverage + stale refs + lint + version refs + prose)
|
||||||
env:
|
env:
|
||||||
PYTHONPATH: src
|
DEVX_DOC_COVERAGE_STRICT: "1"
|
||||||
|
DEVX_DOC_VERSIONS_PKG: grm
|
||||||
|
DEVX_VALE_LEVEL: warning
|
||||||
run: |
|
run: |
|
||||||
. .venv/bin/activate
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
make devx-docs-check
|
||||||
|
- name: Translation completeness check
|
||||||
|
run: |
|
||||||
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
|
python3 -m devx.ci.check_translations --translations src/grm/translations.json
|
||||||
|
- name: Check unit test speed
|
||||||
|
run: |
|
||||||
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
python3 -m devx.tools.check_test_speed --max-seconds 4 --max-single-seconds 0.5
|
python3 -m devx.tools.check_test_speed --max-seconds 4 --max-single-seconds 0.5
|
||||||
- name: Dependency security scan
|
- name: Dependency security scan
|
||||||
run: |
|
run: |
|
||||||
. .venv/bin/activate
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
# Install pip in venv if missing (needed by pip-audit)
|
# Install pip in venv if missing (needed by pip-audit)
|
||||||
.venv/bin/python -m ensurepip 2>/dev/null || true
|
.venv/bin/python -m ensurepip 2>/dev/null || true
|
||||||
PIPAPI_PYTHON_LOCATION=$PWD/.venv/bin/python \
|
PIPAPI_PYTHON_LOCATION=$PWD/.venv/bin/python \
|
||||||
pip-audit --desc --skip-editable 2>&1 || true
|
pip-audit --desc --skip-editable 2>&1 || true
|
||||||
- name: Workflow dry-run validation
|
- name: Workflow dry-run validation
|
||||||
run: |
|
run: |
|
||||||
. .venv/bin/activate
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
# Best-effort: only runs if act_runner is installed
|
# Best-effort: only runs if act_runner is installed
|
||||||
if command -v act_runner >/dev/null 2>&1; then
|
if command -v act_runner >/dev/null 2>&1; then
|
||||||
@@ -47,177 +81,22 @@ jobs:
|
|||||||
else
|
else
|
||||||
echo "act_runner not found — skipping workflow dry-run (static lint still passed)"
|
echo "act_runner not found — skipping workflow dry-run (static lint still passed)"
|
||||||
fi
|
fi
|
||||||
|
# --- detect-changes step ---
|
||||||
release-dry-run:
|
|
||||||
needs: [quality, detect-changes]
|
|
||||||
if: needs.detect-changes.outputs.user-facing-changed == 'true'
|
|
||||||
runs-on: docker
|
|
||||||
timeout-minutes: 10
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Set up environment
|
|
||||||
env:
|
|
||||||
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
|
||||||
run: make setup-release
|
|
||||||
- name: Release dry-run validation
|
|
||||||
env:
|
|
||||||
PYTHONPATH: src
|
|
||||||
DEVX_VERSION_FILE: src/gitea_runner_manager/__init__.py
|
|
||||||
DEVX_TASK_PREFIX: GRM
|
|
||||||
run: |
|
|
||||||
. .venv/bin/activate
|
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
|
||||||
python3 -m devx.ci.release --dry-run || true
|
|
||||||
|
|
||||||
detect-changes:
|
|
||||||
runs-on: docker
|
|
||||||
timeout-minutes: 10
|
|
||||||
outputs:
|
|
||||||
ansible-changed: ${{ steps.detect.outputs.ansible-changed }}
|
|
||||||
user-facing-changed: ${{ steps.detect.outputs.user-facing-changed }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Set up environment
|
|
||||||
env:
|
|
||||||
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
|
||||||
run: make setup-ci
|
|
||||||
- name: Detect changed paths
|
- name: Detect changed paths
|
||||||
id: detect
|
id: detect
|
||||||
env:
|
env:
|
||||||
PYTHONPATH: src
|
|
||||||
DEVX_TASK_PREFIX: GRM
|
DEVX_TASK_PREFIX: GRM
|
||||||
run: |
|
run: |
|
||||||
. .venv/bin/activate
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
python3 -m devx.ci.classify_changes \
|
python3 -m devx.ci.classify_changes \
|
||||||
--base "origin/master" \
|
--base "origin/master" \
|
||||||
--head "${{ github.event.pull_request.head.sha || github.sha }}" \
|
--head "${{ github.event.pull_request.head.sha || github.sha }}" \
|
||||||
--github-output
|
--github-output
|
||||||
|
# --- validate-pr + pr-review steps (PR only) ---
|
||||||
discover-runners:
|
- name: Validate auto-merge preconditions
|
||||||
needs: [detect-changes]
|
if: github.event_name == 'pull_request'
|
||||||
if: needs.detect-changes.outputs.ansible-changed == 'true'
|
|
||||||
runs-on: docker
|
|
||||||
timeout-minutes: 10
|
|
||||||
outputs:
|
|
||||||
runner-count: ${{ steps.discover.outputs.runner-count }}
|
|
||||||
runner-indices: ${{ steps.discover.outputs.runner-indices }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Set up environment
|
|
||||||
env:
|
env:
|
||||||
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
|
||||||
run: make setup-ci
|
|
||||||
- name: Discover available runners
|
|
||||||
id: discover
|
|
||||||
env:
|
|
||||||
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
|
||||||
MOLECULE_RUNNERS: ${{ vars.MOLECULE_RUNNERS }}
|
|
||||||
PYTHONPATH: src
|
|
||||||
run: |
|
|
||||||
. .venv/bin/activate
|
|
||||||
python3 -m devx.molecule.discover_runners \
|
|
||||||
--owner "${{ github.repository_owner }}" \
|
|
||||||
--repo "${{ github.event.repository.name }}" \
|
|
||||||
--github-output
|
|
||||||
|
|
||||||
molecule-tests:
|
|
||||||
needs: [quality, detect-changes, discover-runners]
|
|
||||||
if: needs.detect-changes.outputs.ansible-changed == 'true'
|
|
||||||
runs-on: docker
|
|
||||||
timeout-minutes: 10
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
runner-index: [1, 2, 3]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Set up environment
|
|
||||||
env:
|
|
||||||
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
|
||||||
run: make setup-molecule
|
|
||||||
- name: Discover assigned test pairs
|
|
||||||
env:
|
|
||||||
RUNNER_INDEX: ${{ matrix.runner-index }}
|
|
||||||
MAX_RUNNERS: ${{ needs.discover-runners.outputs.runner-count }}
|
|
||||||
PYTHONPATH: src
|
|
||||||
run: |
|
|
||||||
. .venv/bin/activate
|
|
||||||
python3 -m devx.molecule.distribute_molecule \
|
|
||||||
--runner-index "$RUNNER_INDEX" \
|
|
||||||
--max-runners "$MAX_RUNNERS" \
|
|
||||||
--github-env --skip-if-excess
|
|
||||||
- name: Run molecule tests
|
|
||||||
if: env.SKIP != 'true'
|
|
||||||
run: |
|
|
||||||
. .venv/bin/activate
|
|
||||||
if [ -z "$TEST_PAIRS" ]; then exit 0; fi
|
|
||||||
# shellcheck disable=SC2086 # intentional word splitting for argument expansion
|
|
||||||
python3 -m devx.molecule.molecule_ci_guard $TEST_PAIRS
|
|
||||||
env:
|
|
||||||
GITEA_URL: ${{ github.server_url }}
|
|
||||||
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
|
||||||
RUN_ID: ${{ github.run_id }}
|
|
||||||
JOB_NAME: ${{ github.job }}
|
|
||||||
MATRIX_INDEX: ${{ matrix.runner-index }}
|
|
||||||
GITEA_REPOSITORY: ${{ github.repository }}
|
|
||||||
PYTHONPATH: src
|
|
||||||
|
|
||||||
pr-review:
|
|
||||||
if: github.event_name == 'pull_request'
|
|
||||||
runs-on: docker
|
|
||||||
timeout-minutes: 10
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
. .env 2>/dev/null || true
|
|
||||||
python3 -m pip install --break-system-packages --target=src "devx==0.10.1" --extra-index-url "https://emil:${{ secrets.REPO_TOKEN }}@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/"
|
|
||||||
python3 -m pip install --break-system-packages --target=src "devx==0.10.1" --extra-index-url "https://emil:${{ secrets.REPO_TOKEN }}@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/"
|
|
||||||
python3 -m pip install --break-system-packages --target=src "devx==0.10.1" --extra-index-url "https://emil:${{ secrets.REPO_TOKEN }}@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/"
|
|
||||||
- name: Run automated PR review
|
|
||||||
env:
|
|
||||||
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
|
||||||
PYTHONPATH: src
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
python3 -m devx.ci.pr_review \
|
|
||||||
"${{ github.event.number }}" \
|
|
||||||
"${{ github.repository }}"
|
|
||||||
|
|
||||||
auto-merge:
|
|
||||||
# Auto-merge runs after all CI checks pass. It reads the task ID
|
|
||||||
# from the branch name (falling back to .taskid file), 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]
|
|
||||||
if: >-
|
|
||||||
always() &&
|
|
||||||
github.event_name == 'pull_request' &&
|
|
||||||
needs.quality.result == 'success' &&
|
|
||||||
needs.pr-review.result == 'success' &&
|
|
||||||
(needs.molecule-tests.result == 'success' || needs.molecule-tests.result == 'skipped')
|
|
||||||
runs-on: docker
|
|
||||||
timeout-minutes: 10
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
token: ${{ secrets.REPO_TOKEN }}
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
. .env 2>/dev/null || true
|
|
||||||
python3 -m pip install --break-system-packages --target=src "devx==0.10.1" --extra-index-url "https://emil:${{ secrets.REPO_TOKEN }}@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/"
|
|
||||||
python3 -m pip install --break-system-packages --target=src "devx==0.10.1" --extra-index-url "https://emil:${{ secrets.REPO_TOKEN }}@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/"
|
|
||||||
python3 -m pip install --break-system-packages --target=src "devx==0.10.1" --extra-index-url "https://emil:${{ secrets.REPO_TOKEN }}@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/"
|
|
||||||
- name: Squash merge with task ID
|
|
||||||
env:
|
|
||||||
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
|
||||||
VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }}
|
VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }}
|
||||||
PYTHONPATH: src
|
|
||||||
DEVX_TASK_PREFIX: GRM
|
DEVX_TASK_PREFIX: GRM
|
||||||
DEVX_VIKUNJA_PROJECT_ID: 6
|
DEVX_VIKUNJA_PROJECT_ID: 6
|
||||||
HEAD_REF: ${{ github.head_ref }}
|
HEAD_REF: ${{ github.head_ref }}
|
||||||
@@ -225,6 +104,252 @@ jobs:
|
|||||||
REPOSITORY: ${{ github.repository }}
|
REPOSITORY: ${{ github.repository }}
|
||||||
PR_NUMBER: ${{ github.event.number }}
|
PR_NUMBER: ${{ github.event.number }}
|
||||||
run: |
|
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"
|
||||||
|
- name: Run automated PR review
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
run: |
|
||||||
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
|
set -euo pipefail
|
||||||
|
python3 -m devx.ci.pr_review \
|
||||||
|
"${{ github.event.number }}" \
|
||||||
|
"${{ github.repository }}"
|
||||||
|
# --- release-dry-run step (conditional) ---
|
||||||
|
- name: Release dry-run validation
|
||||||
|
if: steps.detect.outputs.user-facing-changed == 'true'
|
||||||
|
env:
|
||||||
|
DEVX_VERSION_FILE: src/grm/__init__.py
|
||||||
|
DEVX_TASK_PREFIX: GRM
|
||||||
|
run: |
|
||||||
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
python3 -m devx.ci.release --dry-run
|
||||||
|
# --- discover-runners step (conditional on ansible-changed) ---
|
||||||
|
- name: Discover available molecule runners
|
||||||
|
id: discover-runners
|
||||||
|
if: steps.detect.outputs.ansible-changed == 'true'
|
||||||
|
env:
|
||||||
|
MOLECULE_RUNNERS: ${{ vars.MOLECULE_RUNNERS }}
|
||||||
|
run: |
|
||||||
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
|
python3 -m devx.molecule.discover_runners \
|
||||||
|
--owner "${{ github.repository_owner }}" \
|
||||||
|
--repo "${{ github.event.repository.name }}" \
|
||||||
|
--github-output
|
||||||
|
- name: Notify on failure
|
||||||
|
if: failure()
|
||||||
|
env:
|
||||||
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
|
run: |
|
||||||
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
python3 -m devx.ci.notify_failure --auto-login \
|
||||||
|
--repo "${{ github.repository }}" \
|
||||||
|
--run-id "${{ github.run_id }}" \
|
||||||
|
--workflow "ci/validate" \
|
||||||
|
--commit "${{ github.sha }}"
|
||||||
|
|
||||||
|
molecule-tests:
|
||||||
|
needs: [validate]
|
||||||
|
if: needs.validate.outputs.ansible-changed == 'true'
|
||||||
|
runs-on: docker
|
||||||
|
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
max-parallel: 4
|
||||||
|
matrix:
|
||||||
|
runner-index: [1, 2, 3, 4]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Set up environment
|
||||||
|
env:
|
||||||
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
|
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
||||||
|
run: make setup-image EXTRAS=ci,molecule
|
||||||
|
- name: Install Ansible collections
|
||||||
|
run: |
|
||||||
|
. .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:
|
||||||
|
RUNNER_INDEX: ${{ matrix.runner-index }}
|
||||||
|
MAX_RUNNERS: 4
|
||||||
|
run: |
|
||||||
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
|
python3 -m devx.molecule.distribute_molecule \
|
||||||
|
--runner-index "$RUNNER_INDEX" \
|
||||||
|
--max-runners "$MAX_RUNNERS" \
|
||||||
|
--github-env
|
||||||
|
- name: Prune stale Docker data
|
||||||
|
id: prune
|
||||||
|
if: env.SKIP != 'true'
|
||||||
|
run: |
|
||||||
|
docker system prune -af --volumes 2>/dev/null || true
|
||||||
|
disk_pct=$(df -P / | awk 'NR==2 {gsub(/%/, "", $5); print $5}')
|
||||||
|
echo "Disk usage after prune: ${disk_pct}%"
|
||||||
|
if [ "$disk_pct" -ge 85 ]; then
|
||||||
|
echo "should-run=false" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "::warning::Disk usage at ${disk_pct}% after prune — skipping molecule tests to avoid ENOSPC failures"
|
||||||
|
else
|
||||||
|
echo "should-run=true" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
- name: Run molecule tests
|
||||||
|
if: env.SKIP != 'true' && steps.prune.outputs.should-run != 'false'
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
|
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
||||||
|
DOCKER_HOST: unix:///var/run/docker.sock
|
||||||
|
ANSIBLE_INJECT_INVOCATION: "1"
|
||||||
|
run: |
|
||||||
|
. .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"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
_TOKEN="$CI_GITEA_API_TOKEN"; [ -z "$_TOKEN" ] && _TOKEN="$CI_GITEA_TOKEN"
|
||||||
|
[ -z "$_TOKEN" ] && { echo "Gitea API token not set — skipping Docker login"; exit 0; }
|
||||||
|
echo "$_TOKEN" | docker login git.oblachno.oblachno.fyi -u "$CI_GITEA_USERNAME" --password-stdin
|
||||||
|
# Run each molecule test pair sequentially.
|
||||||
|
# Pairs are 4-part: scenario|platform_name|platform_image|platform_command
|
||||||
|
# Spaces in platform_command are encoded as __SPACE__.
|
||||||
|
role_dir="ansible/roles/gitea_runner"
|
||||||
|
# shellcheck disable=SC2086 # intentional word splitting for pair list
|
||||||
|
for pair in $TEST_PAIRS; do
|
||||||
|
IFS='|' read -r scenario platform_name platform_image platform_command <<< "$pair"
|
||||||
|
platform_command="${platform_command//__SPACE__/ }"
|
||||||
|
export MOLECULE_PLATFORM_NAME="$platform_name"
|
||||||
|
export MOLECULE_PLATFORM_IMAGE="$platform_image"
|
||||||
|
if [ -n "$platform_command" ]; then
|
||||||
|
export MOLECULE_PLATFORM_COMMAND="$platform_command"
|
||||||
|
else
|
||||||
|
unset MOLECULE_PLATFORM_COMMAND
|
||||||
|
fi
|
||||||
|
export ANSIBLE_ALLOW_BROKEN_CONDITIONALS=true
|
||||||
|
echo "--- Running: $scenario on $platform_name ---"
|
||||||
|
pushd "$role_dir" >/dev/null
|
||||||
|
if [ "$scenario" = "default" ]; then
|
||||||
|
molecule test || {
|
||||||
|
echo "FAILED: $pair — running molecule destroy"
|
||||||
|
molecule destroy 2>/dev/null || true
|
||||||
|
popd >/dev/null
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
else
|
||||||
|
molecule test -s "$scenario" || {
|
||||||
|
echo "FAILED: $pair — running molecule destroy"
|
||||||
|
molecule destroy -s "$scenario" 2>/dev/null || true
|
||||||
|
popd >/dev/null
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
popd >/dev/null
|
||||||
|
echo "PASSED: $pair"
|
||||||
|
docker system prune -af --volumes 2>/dev/null || true
|
||||||
|
done
|
||||||
|
echo "All molecule tests passed."
|
||||||
|
|
||||||
|
auto-merge:
|
||||||
|
# Auto-merge runs after validate passes. molecule-tests is NOT in needs
|
||||||
|
# because Gitea Actions skips dependent jobs of skipped jobs without
|
||||||
|
# evaluating if: conditions — having molecule-tests in needs would
|
||||||
|
# cascade the skip to auto-merge when ansible-changed=false.
|
||||||
|
needs: [validate]
|
||||||
|
if: >-
|
||||||
|
always() &&
|
||||||
|
github.event_name == 'pull_request' &&
|
||||||
|
needs.validate.result == 'success'
|
||||||
|
runs-on: docker
|
||||||
|
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
|
||||||
|
timeout-minutes: 10
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
|
- name: Set up environment
|
||||||
|
env:
|
||||||
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
|
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
||||||
|
run: make setup-image EXTRAS=ci
|
||||||
|
- name: Post approval review
|
||||||
|
env:
|
||||||
|
REVIEWER_GITEA_API_TOKEN: ${{ secrets.REVIEWER_GITEA_API_TOKEN }}
|
||||||
|
PR_NUMBER: ${{ github.event.number }}
|
||||||
|
REPOSITORY: ${{ github.repository }}
|
||||||
|
run: |
|
||||||
|
. .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 (validate, molecule-tests)."
|
||||||
|
- name: Wait for molecule tests to complete
|
||||||
|
env:
|
||||||
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
|
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||||
|
run: |
|
||||||
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
|
# Poll commit status until all required checks pass or fail
|
||||||
|
MAX_WAIT=600 # 10 minutes
|
||||||
|
ELAPSED=0
|
||||||
|
while [ $ELAPSED -lt $MAX_WAIT ]; do
|
||||||
|
STATUS=$(curl -s -H "Authorization: token $CI_GITEA_API_TOKEN" \
|
||||||
|
"https://git.oblachno.oblachno.fyi/api/v1/repos/${{ github.repository }}/commits/$HEAD_SHA/status" \
|
||||||
|
| python3 -c "
|
||||||
|
import sys,json
|
||||||
|
d=json.load(sys.stdin)
|
||||||
|
statuses={s['context']:s['status'] for s in d.get('statuses',[])}
|
||||||
|
# Check if all molecule-tests contexts are terminal (success/failure)
|
||||||
|
mol_contexts=[k for k in statuses if 'molecule-tests' in k]
|
||||||
|
if not mol_contexts:
|
||||||
|
print('pending')
|
||||||
|
elif all(statuses[k] in ('success','failure') for k in mol_contexts):
|
||||||
|
if any(statuses[k]=='failure' for k in mol_contexts):
|
||||||
|
print('failure')
|
||||||
|
else:
|
||||||
|
print('success')
|
||||||
|
else:
|
||||||
|
print('pending')
|
||||||
|
")
|
||||||
|
echo "Molecule tests status: $STATUS (elapsed: ${ELAPSED}s)"
|
||||||
|
if [ "$STATUS" = "success" ]; then
|
||||||
|
echo "All molecule tests passed."
|
||||||
|
break
|
||||||
|
elif [ "$STATUS" = "failure" ]; then
|
||||||
|
echo "ERROR: Molecule tests failed. Aborting auto-merge."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sleep 30
|
||||||
|
ELAPSED=$((ELAPSED + 30))
|
||||||
|
done
|
||||||
|
if [ $ELAPSED -ge $MAX_WAIT ]; then
|
||||||
|
echo "ERROR: Timed out waiting for molecule tests."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
- name: Squash merge with task ID
|
||||||
|
env:
|
||||||
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_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 }}
|
||||||
|
run: |
|
||||||
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
python3 -m devx.ci.auto_merge \
|
python3 -m devx.ci.auto_merge \
|
||||||
"$HEAD_REF" \
|
"$HEAD_REF" \
|
||||||
"$PR_TITLE" \
|
"$PR_TITLE" \
|
||||||
|
|||||||
+127
-199
@@ -1,263 +1,191 @@
|
|||||||
name: Post-merge
|
name: Post-merge
|
||||||
|
|
||||||
# Runs on every push to master. A single workflow with conditional jobs
|
# Runs on every push to master (after CI workflow merges a PR).
|
||||||
# replaces the previous 4 separate workflows (release.yml, post-merge.yml,
|
# Consolidated into 2 jobs (from 7) to reduce runner overhead:
|
||||||
# sync-wiki.yml, and the badges job from ci.yml).
|
# detect-and-configure ──→ release-and-maintain
|
||||||
#
|
#
|
||||||
# Job dependency graph:
|
# Job 1: detect release commit, validate commit msg, configure repo
|
||||||
|
# (branch protection, labels).
|
||||||
|
# Job 2: release + publish + sync-wiki + vikunja + badges.
|
||||||
|
# Individual steps are conditional on job 1 outputs.
|
||||||
#
|
#
|
||||||
# detect-type ──┬── release (skip if release commit)
|
# The badges step always runs (even on release commits) so version
|
||||||
# ├── badges (ALWAYS runs — even on release commits)
|
# badge picks up the new __version__. It runs last so it sees the
|
||||||
# ├── configure-repo (independent — skip if release commit)
|
# new version if release created one.
|
||||||
# ├── sync-wiki (needs release — skip if release commit/fails)
|
|
||||||
# └── vikunja (needs release — skip if release commit/fails)
|
|
||||||
#
|
#
|
||||||
# sync-wiki and vikunja depend on release succeeding so that the wiki
|
# When release creates a "release: vX.Y.Z" commit and tag, the publish
|
||||||
# and task tracker are only updated when the code is actually released.
|
# step builds and publishes the package to the Gitea PyPI registry.
|
||||||
# If release fails, they are skipped to avoid leaving the wiki or
|
# The release commit's post-merge run still updates badges. Other
|
||||||
# Vikunja in an inconsistent state with the codebase on master.
|
# steps (sync-wiki, vikunja) skip on release commits.
|
||||||
#
|
|
||||||
# The badges job depends on release so it picks up the latest version
|
|
||||||
# number. It uses `if: always()` with no is-release condition so it
|
|
||||||
# runs on every push to master, including release commits. This
|
|
||||||
# ensures badges (tests, coverage, version, etc.) are always current.
|
|
||||||
#
|
|
||||||
# When release.py creates a "release: vX.Y.Z" commit, the release
|
|
||||||
# commit's post-merge run still updates badges (version badge picks
|
|
||||||
# up the new version). Other jobs skip. The tag push triggers publish.yml.
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [master]
|
branches: [master]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: post-merge-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
PIP_BREAK_SYSTEM_PACKAGES: "1"
|
||||||
|
PYTHONPATH: src
|
||||||
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
|
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
detect-type:
|
detect-and-configure:
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
|
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
outputs:
|
outputs:
|
||||||
is-release: ${{ steps.check.outputs.is-release }}
|
is-release: ${{ steps.check.outputs.is-release }}
|
||||||
|
is-automated: ${{ steps.check.outputs.is-automated }}
|
||||||
|
user-facing-changed: ${{ steps.detect.outputs.user-facing-changed }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 0
|
||||||
- name: Install dependencies
|
- name: Set up environment
|
||||||
|
env:
|
||||||
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
|
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
||||||
|
run: make setup-image EXTRAS=ci
|
||||||
|
- name: Ensure branch protection and labels
|
||||||
|
env:
|
||||||
|
DEVX_REPO_NAME: grm
|
||||||
|
DEVX_REPO_OWNER: oblachno-oss
|
||||||
|
DEVX_STATUS_CHECKS: "CI / validate (pull_request),CI / molecule-tests (1) (pull_request),CI / molecule-tests (2) (pull_request),CI / molecule-tests (3) (pull_request)"
|
||||||
run: |
|
run: |
|
||||||
. .env 2>/dev/null || true
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
python3 -m pip install --break-system-packages --target=src "devx==0.10.1" --extra-index-url "https://emil:${{ secrets.REPO_TOKEN }}@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/"
|
python3 -m devx.tools.configure_repo
|
||||||
python3 -m pip install --break-system-packages --target=src "devx==0.10.1" --extra-index-url "https://emil:${{ secrets.REPO_TOKEN }}@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/"
|
|
||||||
python3 -m pip install --break-system-packages --target=src "devx==0.10.1" --extra-index-url "https://emil:${{ secrets.REPO_TOKEN }}@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/"
|
|
||||||
- name: Check if this is a release commit
|
- name: Check if this is a release commit
|
||||||
id: check
|
id: check
|
||||||
env:
|
|
||||||
PYTHONPATH: src
|
|
||||||
run: python3 -m devx.ci.detect_release_commit
|
|
||||||
|
|
||||||
validate-commit-msg:
|
|
||||||
needs: [detect-type]
|
|
||||||
if: needs.detect-type.outputs.is-release == 'false'
|
|
||||||
runs-on: docker
|
|
||||||
timeout-minutes: 5
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 1
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
run: |
|
||||||
. .env 2>/dev/null || true
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
python3 -m pip install --break-system-packages --target=src "devx==0.10.1" --extra-index-url "https://emil:${{ secrets.REPO_TOKEN }}@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/"
|
python3 -m devx.ci.detect_release_commit
|
||||||
python3 -m pip install --break-system-packages --target=src "devx==0.10.1" --extra-index-url "https://emil:${{ secrets.REPO_TOKEN }}@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/"
|
|
||||||
python3 -m pip install --break-system-packages --target=src "devx==0.10.1" --extra-index-url "https://emil:${{ secrets.REPO_TOKEN }}@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/"
|
|
||||||
- name: Validate latest commit message
|
- name: Validate latest commit message
|
||||||
|
if: steps.check.outputs.is-automated == 'false'
|
||||||
env:
|
env:
|
||||||
PYTHONPATH: src
|
|
||||||
DEVX_TASK_PREFIX: GRM
|
DEVX_TASK_PREFIX: GRM
|
||||||
run: |
|
run: |
|
||||||
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
git log -1 --format=%B > commit-msg.txt
|
git log -1 --format=%B > commit-msg.txt
|
||||||
python3 -m devx.ci.validate_commit_msg commit-msg.txt --branch master
|
python3 -m devx.ci.validate_commit_msg commit-msg.txt --branch master
|
||||||
rm -f commit-msg.txt
|
rm -f commit-msg.txt
|
||||||
|
- name: Detect changed paths
|
||||||
release:
|
id: detect
|
||||||
needs: [detect-type]
|
if: steps.check.outputs.is-release == 'false'
|
||||||
if: needs.detect-type.outputs.is-release == 'false'
|
|
||||||
runs-on: docker
|
|
||||||
timeout-minutes: 10
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
token: ${{ secrets.REPO_TOKEN }}
|
|
||||||
- name: Set up environment
|
|
||||||
env:
|
env:
|
||||||
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
|
||||||
run: make setup-release
|
|
||||||
- name: Configure git
|
|
||||||
run: |
|
|
||||||
git config user.name "grm-ci-bot"
|
|
||||||
git config user.email "grm-ci-bot@oblachno.fyi"
|
|
||||||
- name: Run release
|
|
||||||
env:
|
|
||||||
PYTHONPATH: src
|
|
||||||
DEVX_VERSION_FILE: src/gitea_runner_manager/__init__.py
|
|
||||||
DEVX_TASK_PREFIX: GRM
|
DEVX_TASK_PREFIX: GRM
|
||||||
DEVX_VIKUNJA_PROJECT_ID: 6
|
|
||||||
run: |
|
run: |
|
||||||
. .venv/bin/activate
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
python3 -m devx.ci.classify_changes \
|
||||||
python3 -m devx.ci.release
|
--base "HEAD~1" \
|
||||||
|
--head "HEAD" \
|
||||||
|
--github-output
|
||||||
- name: Notify on failure
|
- name: Notify on failure
|
||||||
if: failure()
|
if: failure()
|
||||||
env:
|
env:
|
||||||
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
PYTHONPATH: src
|
|
||||||
run: |
|
run: |
|
||||||
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
python3 -m devx.tools.install_tools --tool tea
|
|
||||||
python3 -m devx.ci.notify_failure --auto-login \
|
python3 -m devx.ci.notify_failure --auto-login \
|
||||||
--repo "${{ github.repository }}" \
|
--repo "${{ github.repository }}" \
|
||||||
--run-id "${{ github.run_id }}" \
|
--run-id "${{ github.run_id }}" \
|
||||||
--workflow "post-merge/release" \
|
--workflow "post-merge/detect-and-configure" \
|
||||||
--commit "${{ github.sha }}"
|
--commit "${{ github.sha }}"
|
||||||
|
|
||||||
sync-wiki:
|
release-and-maintain:
|
||||||
needs: [detect-type, release]
|
needs: [detect-and-configure]
|
||||||
if: needs.detect-type.outputs.is-release == 'false'
|
if: always() && needs.detect-and-configure.result == 'success'
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
timeout-minutes: 10
|
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest
|
||||||
steps:
|
timeout-minutes: 15
|
||||||
- uses: actions/checkout@v4
|
outputs:
|
||||||
with:
|
tag: ${{ steps.release-tag.outputs.tag }}
|
||||||
fetch-depth: 0
|
defaults:
|
||||||
- name: Set up environment
|
run:
|
||||||
env:
|
shell: bash
|
||||||
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
|
||||||
run: make setup-ci
|
|
||||||
- name: Sync documentation to wiki
|
|
||||||
env:
|
|
||||||
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
|
||||||
PYTHONPATH: src
|
|
||||||
run: |
|
|
||||||
. .venv/bin/activate
|
|
||||||
python3 -m devx.ci.sync_wiki --repo "${{ github.repository }}" --strict
|
|
||||||
- name: Notify on failure
|
|
||||||
if: failure()
|
|
||||||
env:
|
|
||||||
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
|
||||||
PYTHONPATH: src
|
|
||||||
run: |
|
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
|
||||||
python3 -m devx.tools.install_tools --tool tea
|
|
||||||
python3 -m devx.ci.notify_failure --auto-login \
|
|
||||||
--repo "${{ github.repository }}" \
|
|
||||||
--run-id "${{ github.run_id }}" \
|
|
||||||
--workflow "post-merge/sync-wiki" \
|
|
||||||
--commit "${{ github.sha }}"
|
|
||||||
|
|
||||||
badges:
|
|
||||||
needs: [detect-type, release]
|
|
||||||
if: always()
|
|
||||||
runs-on: docker
|
|
||||||
timeout-minutes: 10
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
ref: master
|
ref: master
|
||||||
token: ${{ secrets.REPO_TOKEN }}
|
token: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
- name: Fetch latest master
|
|
||||||
run: |
|
|
||||||
git fetch origin master
|
|
||||||
git reset --hard origin/master
|
|
||||||
- name: Set up environment
|
- name: Set up environment
|
||||||
env:
|
env:
|
||||||
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
run: make setup-ci
|
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
||||||
|
run: make setup-image EXTRAS=ci,lint
|
||||||
|
- name: Configure git
|
||||||
|
run: |
|
||||||
|
git config user.name "grm-ci-bot"
|
||||||
|
git config user.email "grm-ci-bot@oblachno.fyi"
|
||||||
|
# --- release + publish (only if not a release commit) ---
|
||||||
|
- name: Run release
|
||||||
|
id: release-tag
|
||||||
|
if: needs.detect-and-configure.outputs.is-release == 'false' && needs.detect-and-configure.outputs.user-facing-changed == 'true'
|
||||||
|
env:
|
||||||
|
DEVX_VERSION_FILE: src/grm/__init__.py
|
||||||
|
DEVX_TASK_PREFIX: GRM
|
||||||
|
DEVX_VIKUNJA_PROJECT_ID: 6
|
||||||
|
run: |
|
||||||
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
python3 -m devx.ci.release
|
||||||
|
- name: Build and publish release
|
||||||
|
if: steps.release-tag.outputs.tag != ''
|
||||||
|
env:
|
||||||
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
|
run: |
|
||||||
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
git fetch --tags
|
||||||
|
git checkout "${{ steps.release-tag.outputs.tag }}"
|
||||||
|
python3 -m devx.ci.publish "${{ steps.release-tag.outputs.tag }}" "${{ github.repository }}" --auto-login
|
||||||
|
# --- sync-wiki + vikunja (skip on automated/release commits) ---
|
||||||
|
- name: Sync documentation to wiki
|
||||||
|
if: needs.detect-and-configure.outputs.is-automated == 'false'
|
||||||
|
env:
|
||||||
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
|
run: |
|
||||||
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
|
python3 -m devx.ci.sync_wiki --repo "${{ github.repository }}" --verify
|
||||||
|
- name: Update Vikunja task
|
||||||
|
if: needs.detect-and-configure.outputs.is-automated == 'false'
|
||||||
|
env:
|
||||||
|
VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }}
|
||||||
|
DEVX_TASK_PREFIX: GRM
|
||||||
|
DEVX_VIKUNJA_PROJECT_ID: 6
|
||||||
|
run: |
|
||||||
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
|
python3 -m devx.ci.post_merge --git-sha "${{ github.sha }}"
|
||||||
|
# --- badges (always run — even on release commits) ---
|
||||||
- name: Generate and push badges
|
- name: Generate and push badges
|
||||||
env:
|
env:
|
||||||
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
PRE_COMMIT_ALLOW_NO_CONFIG: "1"
|
PRE_COMMIT_ALLOW_NO_CONFIG: "1"
|
||||||
run: |
|
run: |
|
||||||
. .venv/bin/activate
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
# Fetch latest master to pick up any release commit that was pushed
|
||||||
|
git fetch origin master
|
||||||
|
git reset --hard origin/master
|
||||||
python3 -m devx.ci.push_badges
|
python3 -m devx.ci.push_badges
|
||||||
- name: Notify on failure
|
- name: Notify on failure
|
||||||
if: failure()
|
if: failure()
|
||||||
env:
|
env:
|
||||||
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||||
PYTHONPATH: src
|
|
||||||
run: |
|
run: |
|
||||||
|
. .venv/bin/activate 2>/dev/null || true
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
python3 -m devx.tools.install_tools --tool tea
|
|
||||||
python3 -m devx.ci.notify_failure --auto-login \
|
python3 -m devx.ci.notify_failure --auto-login \
|
||||||
--repo "${{ github.repository }}" \
|
--repo "${{ github.repository }}" \
|
||||||
--run-id "${{ github.run_id }}" \
|
--run-id "${{ github.run_id }}" \
|
||||||
--workflow "post-merge/badges" \
|
--workflow "post-merge/release-and-maintain" \
|
||||||
--commit "${{ github.sha }}"
|
|
||||||
|
|
||||||
vikunja:
|
|
||||||
needs: [detect-type, release]
|
|
||||||
if: needs.detect-type.outputs.is-release == 'false'
|
|
||||||
runs-on: docker
|
|
||||||
timeout-minutes: 10
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
. .env 2>/dev/null || true
|
|
||||||
python3 -m pip install --break-system-packages --target=src "devx==0.10.1" --extra-index-url "https://emil:${{ secrets.REPO_TOKEN }}@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/"
|
|
||||||
python3 -m pip install --break-system-packages --target=src "devx==0.10.1" --extra-index-url "https://emil:${{ secrets.REPO_TOKEN }}@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/"
|
|
||||||
python3 -m pip install --break-system-packages --target=src "devx==0.10.1" --extra-index-url "https://emil:${{ secrets.REPO_TOKEN }}@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/"
|
|
||||||
- name: Update Vikunja task
|
|
||||||
env:
|
|
||||||
VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }}
|
|
||||||
PYTHONPATH: src
|
|
||||||
DEVX_TASK_PREFIX: GRM
|
|
||||||
DEVX_VIKUNJA_PROJECT_ID: 6
|
|
||||||
run: python3 -m devx.ci.post_merge --git-sha "${{ github.sha }}"
|
|
||||||
- name: Notify on failure
|
|
||||||
if: failure()
|
|
||||||
env:
|
|
||||||
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
|
||||||
PYTHONPATH: src
|
|
||||||
run: |
|
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
|
||||||
python3 -m devx.tools.install_tools --tool tea
|
|
||||||
python3 -m devx.ci.notify_failure --auto-login \
|
|
||||||
--repo "${{ github.repository }}" \
|
|
||||||
--run-id "${{ github.run_id }}" \
|
|
||||||
--workflow "post-merge/vikunja" \
|
|
||||||
--commit "${{ github.sha }}"
|
|
||||||
|
|
||||||
configure-repo:
|
|
||||||
needs: [detect-type]
|
|
||||||
if: needs.detect-type.outputs.is-release == 'false'
|
|
||||||
runs-on: docker
|
|
||||||
timeout-minutes: 10
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
. .env 2>/dev/null || true
|
|
||||||
python3 -m pip install --break-system-packages --target=src "devx==0.10.1" --extra-index-url "https://emil:${{ secrets.REPO_TOKEN }}@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/"
|
|
||||||
python3 -m pip install --break-system-packages --target=src "devx==0.10.1" --extra-index-url "https://emil:${{ secrets.REPO_TOKEN }}@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/"
|
|
||||||
python3 -m pip install --break-system-packages --target=src "devx==0.10.1" --extra-index-url "https://emil:${{ secrets.REPO_TOKEN }}@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/"
|
|
||||||
- name: Ensure branch protection and labels
|
|
||||||
env:
|
|
||||||
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
|
||||||
PYTHONPATH: src
|
|
||||||
DEVX_REPO_NAME: grm
|
|
||||||
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: python3 -m devx.tools.configure_repo
|
|
||||||
- name: Notify on failure
|
|
||||||
if: failure()
|
|
||||||
env:
|
|
||||||
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
|
||||||
PYTHONPATH: src
|
|
||||||
run: |
|
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
|
||||||
python3 -m devx.tools.install_tools --tool tea
|
|
||||||
python3 -m devx.ci.notify_failure --auto-login \
|
|
||||||
--repo "${{ github.repository }}" \
|
|
||||||
--run-id "${{ github.run_id }}" \
|
|
||||||
--workflow "post-merge/configure-repo" \
|
|
||||||
--commit "${{ github.sha }}"
|
--commit "${{ github.sha }}"
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
name: Publish Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- 'v*'
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
tag:
|
|
||||||
description: 'Tag to publish (e.g. v0.7.0)'
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
publish:
|
|
||||||
runs-on: docker
|
|
||||||
timeout-minutes: 10
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Install CI tools
|
|
||||||
run: |
|
|
||||||
. .env 2>/dev/null || true
|
|
||||||
python3 -m pip install --break-system-packages --target=src "devx==0.9.12" --extra-index-url "https://emil:${{ secrets.REPO_TOKEN }}@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/"
|
|
||||||
python3 -m pip install --break-system-packages --target=src "devx==0.9.12" --extra-index-url "https://emil:${{ secrets.REPO_TOKEN }}@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/"
|
|
||||||
python3 -m pip install --break-system-packages --target=src "devx==0.9.12" --extra-index-url "https://emil:${{ secrets.REPO_TOKEN }}@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/"
|
|
||||||
python3 -m devx.tools.install_tools --tool git-cliff --tool tea
|
|
||||||
- name: Install build tools
|
|
||||||
run: python3 -m pip install --break-system-packages build twine
|
|
||||||
- name: Configure tea login
|
|
||||||
env:
|
|
||||||
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
|
||||||
run: |
|
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
|
||||||
- name: Build and publish release
|
|
||||||
env:
|
|
||||||
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
|
||||||
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
|
|
||||||
PYTHONPATH: src
|
|
||||||
run: |
|
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
|
||||||
python3 -m devx.ci.publish \
|
|
||||||
"${{ github.event.inputs.tag || github.ref_name }}" \
|
|
||||||
"${{ github.repository }}"
|
|
||||||
- name: Notify on failure
|
|
||||||
if: failure()
|
|
||||||
env:
|
|
||||||
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
|
||||||
PYTHONPATH: src
|
|
||||||
run: |
|
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
|
||||||
python3 -m devx.ci.notify_failure --auto-login \
|
|
||||||
--repo "${{ github.repository }}" \
|
|
||||||
--run-id "${{ github.run_id }}" \
|
|
||||||
--workflow "publish" \
|
|
||||||
--commit "${{ github.sha }}"
|
|
||||||
+70
-5
@@ -57,6 +57,38 @@ repos:
|
|||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
stages: [pre-commit]
|
stages: [pre-commit]
|
||||||
|
|
||||||
|
- id: checkmake
|
||||||
|
name: checkmake Makefile linter
|
||||||
|
entry: make checkmake
|
||||||
|
language: system
|
||||||
|
files: ^Makefile$
|
||||||
|
pass_filenames: false
|
||||||
|
stages: [pre-commit]
|
||||||
|
|
||||||
|
- id: check-test-speed
|
||||||
|
name: unit test speed check
|
||||||
|
entry: .venv/bin/python -m devx.tools.check_test_speed --max-seconds 4 --max-single-seconds 0.5
|
||||||
|
language: system
|
||||||
|
types: [python]
|
||||||
|
pass_filenames: false
|
||||||
|
stages: [pre-commit]
|
||||||
|
|
||||||
|
- id: check-translations
|
||||||
|
name: translation completeness check
|
||||||
|
entry: env PYTHONPATH=src .venv/bin/python -m devx.ci.check_translations --translations src/grm/translations.json
|
||||||
|
language: system
|
||||||
|
files: ^src/grm/translations\.json$
|
||||||
|
pass_filenames: false
|
||||||
|
stages: [pre-commit]
|
||||||
|
|
||||||
|
- id: docs-check
|
||||||
|
name: documentation gate (coverage + stale refs + lint + version refs + prose)
|
||||||
|
entry: bash -c 'PYTHONPATH=src DEVX_DOC_COVERAGE_STRICT=1 DEVX_DOC_VERSIONS_PKG=grm DEVX_VALE_LEVEL=warning make devx-docs-check'
|
||||||
|
language: system
|
||||||
|
pass_filenames: false
|
||||||
|
always_run: true
|
||||||
|
stages: [pre-commit]
|
||||||
|
|
||||||
- id: pytest-cov
|
- id: pytest-cov
|
||||||
name: pytest with 100% coverage
|
name: pytest with 100% coverage
|
||||||
entry: make pytest-cov
|
entry: make pytest-cov
|
||||||
@@ -65,9 +97,42 @@ repos:
|
|||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
stages: [pre-push]
|
stages: [pre-push]
|
||||||
|
|
||||||
- id: commit-msg
|
- id: check-ansible-no-log
|
||||||
name: validate commit message
|
name: ansible no_log on secret tasks
|
||||||
entry: env PYTHONPATH=src .venv/bin/python -m devx.ci.validate_commit_msg
|
entry: make check-ansible-no-log
|
||||||
language: system
|
language: system
|
||||||
stages: [commit-msg]
|
files: ^ansible/.*\.(yml|yaml)$
|
||||||
pass_filenames: true
|
pass_filenames: false
|
||||||
|
stages: [pre-commit]
|
||||||
|
|
||||||
|
- id: check-ansible-no-state-absent-on-db
|
||||||
|
name: no state absent on DB paths
|
||||||
|
entry: make check-ansible-no-state-absent-on-db
|
||||||
|
language: system
|
||||||
|
files: ^ansible/.*\.(yml|yaml)$
|
||||||
|
pass_filenames: false
|
||||||
|
stages: [pre-commit]
|
||||||
|
|
||||||
|
- id: check-ansible-patterns
|
||||||
|
name: ansible failure-masking patterns
|
||||||
|
entry: make check-ansible-patterns
|
||||||
|
language: system
|
||||||
|
files: ^ansible/.*\.(yml|yaml)$
|
||||||
|
pass_filenames: false
|
||||||
|
stages: [pre-commit]
|
||||||
|
|
||||||
|
- id: check-jinja-expr
|
||||||
|
name: jinja2 expression validation
|
||||||
|
entry: make check-jinja-expr
|
||||||
|
language: system
|
||||||
|
files: ^ansible/.*\.(yml|yaml|j2)$
|
||||||
|
pass_filenames: false
|
||||||
|
stages: [pre-commit]
|
||||||
|
|
||||||
|
- id: check-ansible-set-fact-to-json
|
||||||
|
name: set_fact to_json misuse check
|
||||||
|
entry: make check-ansible-set-fact-to-json
|
||||||
|
language: system
|
||||||
|
files: ^ansible/.*\.(yml|yaml)$
|
||||||
|
pass_filenames: false
|
||||||
|
stages: [pre-commit]
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# Vale configuration for grm documentation
|
||||||
|
# https://vale.sh/docs/
|
||||||
|
|
||||||
|
StylesPath = .vale/styles
|
||||||
|
|
||||||
|
# Packages are downloaded via `vale sync`
|
||||||
|
Packages = write-good, Google, Readability
|
||||||
|
|
||||||
|
# Minimum alert level to display (suggestion, warning, error)
|
||||||
|
MinAlertLevel = warning
|
||||||
|
|
||||||
|
# Project vocabulary — terms not flagged as spelling errors
|
||||||
|
Vocab = grm
|
||||||
|
|
||||||
|
[*.{md}]
|
||||||
|
# Enable style guides
|
||||||
|
BasedOnStyles = Vale, write-good, Google, Readability, devx
|
||||||
|
|
||||||
|
# Google style — relax rules too strict for technical docs
|
||||||
|
Google.Contractions = NO
|
||||||
|
Google.WordList = NO
|
||||||
|
Google.Acronyms = NO
|
||||||
|
Google.We = NO
|
||||||
|
Google.Will = NO
|
||||||
|
Google.Colons = NO
|
||||||
|
Google.Headings = NO
|
||||||
|
Google.EmDash = NO
|
||||||
|
Google.Units = NO
|
||||||
|
Google.Latin = NO
|
||||||
|
Google.OptionalPlurals = NO
|
||||||
|
|
||||||
|
# write-good — relax rules too strict for technical writing
|
||||||
|
write-good.E-Prime = NO
|
||||||
|
write-good.So = NO
|
||||||
|
write-good.ThereIs = NO
|
||||||
|
write-good.TooWordy = NO
|
||||||
|
write-good.Passive = NO
|
||||||
|
|
||||||
|
# Vale defaults — spelling catches too many technical terms
|
||||||
|
Vale.Terms = NO
|
||||||
|
Vale.Repetition = NO
|
||||||
|
Vale.Spelling = NO
|
||||||
|
|
||||||
|
# Readability — technical docs are naturally complex, downgrade to suggestions
|
||||||
|
Readability.FleschReadingEase = suggestion
|
||||||
|
Readability.FleschKincaid = suggestion
|
||||||
|
Readability.AutomatedReadability = suggestion
|
||||||
|
Readability.ColemanLiau = suggestion
|
||||||
|
Readability.LIX = suggestion
|
||||||
|
Readability.GunningFog = suggestion
|
||||||
|
Readability.SMOG = suggestion
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
extends: existence
|
||||||
|
message: "Use 'AM' or 'PM' (preceded by a space)."
|
||||||
|
link: "https://developers.google.com/style/word-list"
|
||||||
|
level: error
|
||||||
|
nonword: true
|
||||||
|
tokens:
|
||||||
|
- '\d{1,2}[AP]M\b'
|
||||||
|
- '\d{1,2} ?[ap]m\b'
|
||||||
|
- '\d{1,2} ?[aApP]\.[mM]\.'
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
extends: conditional
|
||||||
|
message: "Spell out '%s', if it's unfamiliar to the audience."
|
||||||
|
link: 'https://developers.google.com/style/abbreviations'
|
||||||
|
level: suggestion
|
||||||
|
ignorecase: false
|
||||||
|
# Ensures that the existence of 'first' implies the existence of 'second'.
|
||||||
|
first: '\b([A-Z]{3,5})\b'
|
||||||
|
second: '(?:\b[A-Z][a-z]+ )+\(([A-Z]{3,5})\)'
|
||||||
|
# ... with the exception of these:
|
||||||
|
exceptions:
|
||||||
|
- API
|
||||||
|
- ASP
|
||||||
|
- CLI
|
||||||
|
- CPU
|
||||||
|
- CSS
|
||||||
|
- CSV
|
||||||
|
- DEBUG
|
||||||
|
- DOM
|
||||||
|
- DPI
|
||||||
|
- FAQ
|
||||||
|
- GCC
|
||||||
|
- GDB
|
||||||
|
- GET
|
||||||
|
- GPU
|
||||||
|
- GTK
|
||||||
|
- GUI
|
||||||
|
- HTML
|
||||||
|
- HTTP
|
||||||
|
- HTTPS
|
||||||
|
- IDE
|
||||||
|
- JAR
|
||||||
|
- JSON
|
||||||
|
- JSX
|
||||||
|
- LESS
|
||||||
|
- LLDB
|
||||||
|
- NET
|
||||||
|
- NOTE
|
||||||
|
- NVDA
|
||||||
|
- OSS
|
||||||
|
- PATH
|
||||||
|
- PDF
|
||||||
|
- PHP
|
||||||
|
- POST
|
||||||
|
- RAM
|
||||||
|
- REPL
|
||||||
|
- RSA
|
||||||
|
- SCM
|
||||||
|
- SCSS
|
||||||
|
- SDK
|
||||||
|
- SQL
|
||||||
|
- SSH
|
||||||
|
- SSL
|
||||||
|
- SVG
|
||||||
|
- TBD
|
||||||
|
- TCP
|
||||||
|
- TODO
|
||||||
|
- URI
|
||||||
|
- URL
|
||||||
|
- USB
|
||||||
|
- UTF
|
||||||
|
- XML
|
||||||
|
- XSS
|
||||||
|
- YAML
|
||||||
|
- ZIP
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
extends: existence
|
||||||
|
message: "Don't attribute human qualities to software or hardware ('%s')."
|
||||||
|
link: https://developers.google.com/style/anthropomorphism
|
||||||
|
level: suggestion
|
||||||
|
ignorecase: true
|
||||||
|
# Limited to the two verbs the guide itself names. Broader lists (wants, knows,
|
||||||
|
# thinks) can't tell a software subject from a human one: on a 950-file corpus
|
||||||
|
# they produced 8 false positives ('the customer wants', 'your audience knows')
|
||||||
|
# for every 2 real ones.
|
||||||
|
tokens:
|
||||||
|
- sees
|
||||||
|
- tells
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
extends: existence
|
||||||
|
message: "'%s' should be in lowercase."
|
||||||
|
link: 'https://developers.google.com/style/colons'
|
||||||
|
level: warning
|
||||||
|
scope: sentence
|
||||||
|
# The match is the word itself, not ': X', and `nonword` is off. Both are
|
||||||
|
# required for a project Vocab to work: Vale compares accept.txt entries
|
||||||
|
# against the matched text, and `nonword: true` opts out of that entirely.
|
||||||
|
# So a proper noun after a colon can be exempted by adding it to accept.txt.
|
||||||
|
# The guide's other exemption, notice labels, is handled by the lookbehinds;
|
||||||
|
# headings are already excluded by `scope: sentence`. See issue #20.
|
||||||
|
tokens:
|
||||||
|
- '(?<!Note: )(?<!Caution: )(?<!Warning: )(?<!Success: )(?<=:\s)[A-Z]\w+'
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
extends: substitution
|
||||||
|
message: "Use '%s' instead of '%s'."
|
||||||
|
link: 'https://developers.google.com/style/contractions'
|
||||||
|
level: suggestion
|
||||||
|
ignorecase: true
|
||||||
|
action:
|
||||||
|
name: replace
|
||||||
|
swap:
|
||||||
|
are not: aren't
|
||||||
|
cannot: can't
|
||||||
|
could not: couldn't
|
||||||
|
did not: didn't
|
||||||
|
do not: don't
|
||||||
|
does not: doesn't
|
||||||
|
has not: hasn't
|
||||||
|
have not: haven't
|
||||||
|
how is: how's
|
||||||
|
is not: isn't
|
||||||
|
it is: it's
|
||||||
|
should not: shouldn't
|
||||||
|
that is: that's
|
||||||
|
they are: they're
|
||||||
|
was not: wasn't
|
||||||
|
we are: we're
|
||||||
|
we have: we've
|
||||||
|
were not: weren't
|
||||||
|
what is: what's
|
||||||
|
when is: when's
|
||||||
|
where is: where's
|
||||||
|
will not: won't
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
extends: existence
|
||||||
|
message: "Use 'July 31, 2016' format, not '%s'."
|
||||||
|
link: 'https://developers.google.com/style/dates-times'
|
||||||
|
ignorecase: true
|
||||||
|
level: error
|
||||||
|
nonword: true
|
||||||
|
tokens:
|
||||||
|
- '\d{1,2}(?:\.|/)\d{1,2}(?:\.|/)\d{4}'
|
||||||
|
- '\d{1,2} (?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|Jul(?:y)?|Aug(?:ust)?|Sep(?:tember)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?) \d{4}'
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
extends: existence
|
||||||
|
message: "In general, don't use an ellipsis."
|
||||||
|
link: 'https://developers.google.com/style/ellipses'
|
||||||
|
nonword: true
|
||||||
|
level: warning
|
||||||
|
action:
|
||||||
|
name: remove
|
||||||
|
tokens:
|
||||||
|
- '\.\.\.'
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
extends: existence
|
||||||
|
message: "Don't put a space before or after a dash."
|
||||||
|
link: "https://developers.google.com/style/dashes"
|
||||||
|
nonword: true
|
||||||
|
level: error
|
||||||
|
action:
|
||||||
|
name: edit
|
||||||
|
params:
|
||||||
|
- trim
|
||||||
|
- " "
|
||||||
|
tokens:
|
||||||
|
- '\s[—–]\s'
|
||||||
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
extends: existence
|
||||||
|
message: "Avoid the unverifiable claim '%s'."
|
||||||
|
link: https://developers.google.com/style/excessive-claims
|
||||||
|
level: suggestion
|
||||||
|
ignorecase: true
|
||||||
|
# The guide also names 'never', 'always', and 'ensure', but in technical writing
|
||||||
|
# those are usually legitimate instructions ('never commit secrets') rather than
|
||||||
|
# product claims: they accounted for 125 of 142 hits on a 950-file corpus.
|
||||||
|
# 'best practices' is a fixed term, not a superlative.
|
||||||
|
tokens:
|
||||||
|
- 'best(?! practices?)'
|
||||||
|
- simplest
|
||||||
|
- fastest
|
||||||
|
- guarantees?
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
extends: existence
|
||||||
|
message: "Don't use exclamation points in text."
|
||||||
|
link: "https://developers.google.com/style/exclamation-points"
|
||||||
|
nonword: true
|
||||||
|
level: error
|
||||||
|
action:
|
||||||
|
name: edit
|
||||||
|
params:
|
||||||
|
- trim_right
|
||||||
|
- "!"
|
||||||
|
tokens:
|
||||||
|
- '\w+!(?:\s|$)'
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
extends: existence
|
||||||
|
message: "Avoid first-person pronouns such as '%s'."
|
||||||
|
link: 'https://developers.google.com/style/pronouns#personal-pronouns'
|
||||||
|
ignorecase: true
|
||||||
|
level: warning
|
||||||
|
# The 'I' tokens use lookaround rather than consuming the surrounding
|
||||||
|
# whitespace. Matching ' I ' made the alert span cover both spaces, which shows
|
||||||
|
# up as a too-wide underline in editors, and read as "such as ' I '". Dropping
|
||||||
|
# `nonword` also lets a project Vocab apply, which it can't when set. See PR #50.
|
||||||
|
tokens:
|
||||||
|
- '(?<=^|\s)I(?=[\s,])'
|
||||||
|
- "\\bI'm\\b"
|
||||||
|
- \bme\b
|
||||||
|
- \bmy\b
|
||||||
|
- \bmine\b
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
extends: existence
|
||||||
|
message: "Don't use '%s' as a gender-neutral pronoun."
|
||||||
|
link: 'https://developers.google.com/style/pronouns#gender-neutral-pronouns'
|
||||||
|
level: error
|
||||||
|
ignorecase: true
|
||||||
|
tokens:
|
||||||
|
- he/she
|
||||||
|
- s/he
|
||||||
|
- \(s\)he
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
extends: substitution
|
||||||
|
message: "Consider using '%s' instead of '%s'."
|
||||||
|
ignorecase: true
|
||||||
|
link: "https://developers.google.com/style/inclusive-documentation"
|
||||||
|
level: error
|
||||||
|
action:
|
||||||
|
name: replace
|
||||||
|
swap:
|
||||||
|
(?:alumna|alumnus): graduate
|
||||||
|
(?:alumnae|alumni): graduates
|
||||||
|
air(?:m[ae]n|wom[ae]n): pilot(s)
|
||||||
|
anchor(?:m[ae]n|wom[ae]n): anchor(s)
|
||||||
|
authoress: author
|
||||||
|
camera(?:m[ae]n|wom[ae]n): camera operator(s)
|
||||||
|
door(?:m[ae]|wom[ae]n): concierge(s)
|
||||||
|
draft(?:m[ae]n|wom[ae]n): drafter(s)
|
||||||
|
fire(?:m[ae]n|wom[ae]n): firefighter(s)
|
||||||
|
fisher(?:m[ae]n|wom[ae]n): fisher(s)
|
||||||
|
fresh(?:m[ae]n|wom[ae]n): first-year student(s)
|
||||||
|
garbage(?:m[ae]n|wom[ae]n): waste collector(s)
|
||||||
|
lady lawyer: lawyer
|
||||||
|
ladylike: courteous
|
||||||
|
mail(?:m[ae]n|wom[ae]n): mail carriers
|
||||||
|
man and wife: husband and wife
|
||||||
|
man enough: strong enough
|
||||||
|
mankind: human kind|humanity
|
||||||
|
manmade: manufactured
|
||||||
|
manpower: personnel
|
||||||
|
middle(?:m[ae]n|wom[ae]n): intermediary
|
||||||
|
news(?:m[ae]n|wom[ae]n): journalist(s)
|
||||||
|
ombuds(?:man|woman): ombuds
|
||||||
|
oneupmanship: upstaging
|
||||||
|
poetess: poet
|
||||||
|
police(?:m[ae]n|wom[ae]n): police officer(s)
|
||||||
|
repair(?:m[ae]n|wom[ae]n): technician(s)
|
||||||
|
sales(?:m[ae]n|wom[ae]n): salesperson or sales people
|
||||||
|
service(?:m[ae]n|wom[ae]n): soldier(s)
|
||||||
|
steward(?:ess)?: flight attendant
|
||||||
|
tribes(?:m[ae]n|wom[ae]n): tribe member(s)
|
||||||
|
waitress: waiter
|
||||||
|
woman doctor: doctor
|
||||||
|
woman scientist[s]?: scientist(s)
|
||||||
|
work(?:m[ae]n|wom[ae]n): worker(s)
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
extends: existence
|
||||||
|
message: "Don't put a period at the end of a heading."
|
||||||
|
link: "https://developers.google.com/style/capitalization#capitalization-in-titles-and-headings"
|
||||||
|
nonword: true
|
||||||
|
level: warning
|
||||||
|
scope: heading
|
||||||
|
action:
|
||||||
|
name: edit
|
||||||
|
params:
|
||||||
|
- trim_right
|
||||||
|
- "."
|
||||||
|
tokens:
|
||||||
|
- '[a-z0-9][.]\s*$'
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
extends: capitalization
|
||||||
|
message: "'%s' should use sentence-style capitalization."
|
||||||
|
link: "https://developers.google.com/style/capitalization#capitalization-in-titles-and-headings"
|
||||||
|
level: warning
|
||||||
|
scope: heading
|
||||||
|
match: $sentence
|
||||||
|
# No `indicators: [":"]` here. That makes Vale require a capital after a colon,
|
||||||
|
# which is the Microsoft convention this rule was originally copied from. This
|
||||||
|
# guide says the opposite: "the first word after a colon is generally
|
||||||
|
# lowercase" (developers.google.com/style/colons), and Colons.yml enforces
|
||||||
|
# exactly that. See issue #58.
|
||||||
|
exceptions:
|
||||||
|
- Azure
|
||||||
|
- CLI
|
||||||
|
- Cosmos
|
||||||
|
- Docker
|
||||||
|
- Emmet
|
||||||
|
- gRPC
|
||||||
|
- I
|
||||||
|
- Kubernetes
|
||||||
|
- Linux
|
||||||
|
- macOS
|
||||||
|
- Marketplace
|
||||||
|
- MongoDB
|
||||||
|
- REPL
|
||||||
|
- Studio
|
||||||
|
- TypeScript
|
||||||
|
- URLs
|
||||||
|
- Visual
|
||||||
|
- VS
|
||||||
|
- Windows
|
||||||
|
- JSON
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
extends: existence
|
||||||
|
message: "Avoid the jargon '%s'."
|
||||||
|
link: https://developers.google.com/style/jargon
|
||||||
|
level: suggestion
|
||||||
|
ignorecase: true
|
||||||
|
# The guide also cites 'solution', 'support', and 'workload' as overloaded
|
||||||
|
# terms, but those have ordinary technical meanings and accounted for every hit
|
||||||
|
# on a 950-file corpus, so only the unambiguous figurative terms are listed.
|
||||||
|
tokens:
|
||||||
|
- break-glass
|
||||||
|
- camel ?case
|
||||||
|
- out-of-the-box
|
||||||
|
- swim ?lane
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
extends: substitution
|
||||||
|
message: "Use '%s' instead of '%s'."
|
||||||
|
link: 'https://developers.google.com/style/abbreviations'
|
||||||
|
ignorecase: true
|
||||||
|
level: error
|
||||||
|
nonword: true
|
||||||
|
action:
|
||||||
|
name: replace
|
||||||
|
# The delimiter is a lookahead so the replacement doesn't swallow the comma or
|
||||||
|
# space that follows (issue #18). `$` is included so the abbreviation is still
|
||||||
|
# caught at the end of a heading, table cell, or block, which accounted for 8
|
||||||
|
# of 10 occurrences on a 950-file corpus.
|
||||||
|
swap:
|
||||||
|
'\b(?:eg|e\.g\.)(?=[\s,;]|$)': for example
|
||||||
|
'\b(?:ie|i\.e\.)(?=[\s,;]|$)': that is
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
extends: existence
|
||||||
|
message: "'%s' doesn't need a hyphen."
|
||||||
|
link: "https://developers.google.com/style/hyphens"
|
||||||
|
level: error
|
||||||
|
ignorecase: false
|
||||||
|
nonword: true
|
||||||
|
action:
|
||||||
|
name: edit
|
||||||
|
params:
|
||||||
|
- regex
|
||||||
|
- "-"
|
||||||
|
- " "
|
||||||
|
tokens:
|
||||||
|
- '\b[^\s-]+ly-\w+\b'
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
extends: existence
|
||||||
|
message: "Don't use plurals in parentheses such as in '%s'."
|
||||||
|
link: "https://developers.google.com/style/plurals-parentheses"
|
||||||
|
level: error
|
||||||
|
nonword: true
|
||||||
|
action:
|
||||||
|
name: edit
|
||||||
|
params:
|
||||||
|
- trim_right
|
||||||
|
- "(s)"
|
||||||
|
tokens:
|
||||||
|
- '\b\w+\(s\)'
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
extends: existence
|
||||||
|
message: "Spell out all ordinal numbers ('%s') in text."
|
||||||
|
link: 'https://developers.google.com/style/numbers'
|
||||||
|
level: error
|
||||||
|
nonword: true
|
||||||
|
tokens:
|
||||||
|
- \d+(?:st|nd|rd|th)
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
extends: existence
|
||||||
|
message: "Use the Oxford comma in '%s'."
|
||||||
|
link: 'https://developers.google.com/style/commas'
|
||||||
|
scope: sentence
|
||||||
|
level: warning
|
||||||
|
nonword: true
|
||||||
|
# List items may be several words long, not just one. Four guards keep the
|
||||||
|
# false-positive rate down:
|
||||||
|
#
|
||||||
|
# 1. The comma can't be the one closing a fronted subordinate clause
|
||||||
|
# ('When your alarm rings, you turn it off and tumble out of bed.') --
|
||||||
|
# that comma separates clauses, not list items. Only the first comma of
|
||||||
|
# such a sentence is exempt, so 'When it rains, apples, pears or bananas
|
||||||
|
# get wet.' is still caught.
|
||||||
|
# 2. The item can't open with a clause-introducer (', which ...',
|
||||||
|
# ', specifically ...').
|
||||||
|
# 3. The item can't open with a subject pronoun followed by a verb, which
|
||||||
|
# marks a compound predicate rather than a list ('..., you walk to the
|
||||||
|
# fridge and get a snack.'). A pronoun directly followed by 'and'/'or'
|
||||||
|
# is a real list item, so ', you and me.' still matches.
|
||||||
|
# 4. Neither item may contain an auxiliary verb, which is another compound
|
||||||
|
# predicate signal (', it has some downsides and is officially
|
||||||
|
# discouraged.').
|
||||||
|
#
|
||||||
|
# The trailing anchor allows end-of-scope so list fragments ('Apples, pears
|
||||||
|
# or bananas') are still caught.
|
||||||
|
tokens:
|
||||||
|
- '(?<!^(?i:when|whenever|while|if|unless|until|although|though|because|since|after|before|once|whereas|whether|as)\b[^,]{0,80}),\s(?!(?:which|who|whom|whose|that|where|when|while|because|since|although|though|if|unless|so|but|and|or|however|therefore|thus|specifically|especially|namely|then|take|see|note|consider|make|use|either|neither)\b)(?!(?i:i|you|we|they|he|she|it)\s+(?!(?:and|or)\b))(?:(?!\b(?:is|are|was|were|has|have|had|be|been|being|will|would|can|could|should|may|might|must|do|does|did)\b)\w+ ){0,4}\w+ (?:and|or) (?:(?!\b(?:is|are|was|were|has|have|had|be|been|being|will|would|can|could|should|may|might|must|do|does|did)\b)\w+ ){0,4}\w+(?:[.?!]|$)'
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
extends: existence
|
||||||
|
message: "Use parentheses judiciously."
|
||||||
|
link: 'https://developers.google.com/style/parentheses'
|
||||||
|
nonword: true
|
||||||
|
level: suggestion
|
||||||
|
# `[^)]` rather than `.+`: a greedy match ran from the first '(' on a line to
|
||||||
|
# the last ')', so 'Text (one) and more (two).' produced a single alert
|
||||||
|
# covering everything between them. See issue #30.
|
||||||
|
# A bare 3-5 letter acronym is skipped: Acronyms.yml requires acronyms to be
|
||||||
|
# defined as 'Spelled Out Term (ACRONYM)', so flagging those parentheses would
|
||||||
|
# put the two rules in direct conflict. The acronym has to be the whole
|
||||||
|
# parenthetical — '(NASA rocket program)' is an ordinary aside and still
|
||||||
|
# flags. Length matches the {3,5} in Acronyms.yml. See PR #59.
|
||||||
|
tokens:
|
||||||
|
- '\((?![A-Z]{3,5}\))[^)]+\)'
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
extends: existence
|
||||||
|
link: 'https://developers.google.com/style/voice'
|
||||||
|
message: "In general, use active voice instead of passive voice ('%s')."
|
||||||
|
ignorecase: true
|
||||||
|
level: suggestion
|
||||||
|
raw:
|
||||||
|
- \b(am|are|were|being|is|been|was|be)\b\s*
|
||||||
|
tokens:
|
||||||
|
- '[\w]+ed'
|
||||||
|
- awoken
|
||||||
|
- beat
|
||||||
|
- become
|
||||||
|
- been
|
||||||
|
- begun
|
||||||
|
- bent
|
||||||
|
- beset
|
||||||
|
- bet
|
||||||
|
- bid
|
||||||
|
- bidden
|
||||||
|
- bitten
|
||||||
|
- bled
|
||||||
|
- blown
|
||||||
|
- born
|
||||||
|
- bought
|
||||||
|
- bound
|
||||||
|
- bred
|
||||||
|
- broadcast
|
||||||
|
- broken
|
||||||
|
- brought
|
||||||
|
- built
|
||||||
|
- burnt
|
||||||
|
- burst
|
||||||
|
- cast
|
||||||
|
- caught
|
||||||
|
- chosen
|
||||||
|
- clung
|
||||||
|
- come
|
||||||
|
- cost
|
||||||
|
- crept
|
||||||
|
- cut
|
||||||
|
- dealt
|
||||||
|
- dived
|
||||||
|
- done
|
||||||
|
- drawn
|
||||||
|
- dreamt
|
||||||
|
- driven
|
||||||
|
- drunk
|
||||||
|
- dug
|
||||||
|
- eaten
|
||||||
|
- fallen
|
||||||
|
- fed
|
||||||
|
- felt
|
||||||
|
- fit
|
||||||
|
- fled
|
||||||
|
- flown
|
||||||
|
- flung
|
||||||
|
- forbidden
|
||||||
|
- foregone
|
||||||
|
- forgiven
|
||||||
|
- forgotten
|
||||||
|
- forsaken
|
||||||
|
- fought
|
||||||
|
- found
|
||||||
|
- frozen
|
||||||
|
- given
|
||||||
|
- gone
|
||||||
|
- gotten
|
||||||
|
- ground
|
||||||
|
- grown
|
||||||
|
- heard
|
||||||
|
- held
|
||||||
|
- hidden
|
||||||
|
- hit
|
||||||
|
- hung
|
||||||
|
- hurt
|
||||||
|
- kept
|
||||||
|
- knelt
|
||||||
|
- knit
|
||||||
|
- known
|
||||||
|
- laid
|
||||||
|
- lain
|
||||||
|
- leapt
|
||||||
|
- learnt
|
||||||
|
- led
|
||||||
|
- left
|
||||||
|
- lent
|
||||||
|
- let
|
||||||
|
- lighted
|
||||||
|
- lost
|
||||||
|
- made
|
||||||
|
- meant
|
||||||
|
- met
|
||||||
|
- misspelt
|
||||||
|
- mistaken
|
||||||
|
- mown
|
||||||
|
- overcome
|
||||||
|
- overdone
|
||||||
|
- overtaken
|
||||||
|
- overthrown
|
||||||
|
- paid
|
||||||
|
- pled
|
||||||
|
- proven
|
||||||
|
- put
|
||||||
|
- quit
|
||||||
|
- read
|
||||||
|
- rid
|
||||||
|
- ridden
|
||||||
|
- risen
|
||||||
|
- run
|
||||||
|
- rung
|
||||||
|
- said
|
||||||
|
- sat
|
||||||
|
- sawn
|
||||||
|
- seen
|
||||||
|
- sent
|
||||||
|
- set
|
||||||
|
- sewn
|
||||||
|
- shaken
|
||||||
|
- shaven
|
||||||
|
- shed
|
||||||
|
- shod
|
||||||
|
- shone
|
||||||
|
- shorn
|
||||||
|
- shot
|
||||||
|
- shown
|
||||||
|
- shrunk
|
||||||
|
- shut
|
||||||
|
- slain
|
||||||
|
- slept
|
||||||
|
- slid
|
||||||
|
- slit
|
||||||
|
- slung
|
||||||
|
- smitten
|
||||||
|
- sold
|
||||||
|
- sought
|
||||||
|
- sown
|
||||||
|
- sped
|
||||||
|
- spent
|
||||||
|
- spilt
|
||||||
|
- spit
|
||||||
|
- split
|
||||||
|
- spoken
|
||||||
|
- spread
|
||||||
|
- sprung
|
||||||
|
- spun
|
||||||
|
- stolen
|
||||||
|
- stood
|
||||||
|
- stridden
|
||||||
|
- striven
|
||||||
|
- struck
|
||||||
|
- strung
|
||||||
|
- stuck
|
||||||
|
- stung
|
||||||
|
- stunk
|
||||||
|
- sung
|
||||||
|
- sunk
|
||||||
|
- swept
|
||||||
|
- swollen
|
||||||
|
- sworn
|
||||||
|
- swum
|
||||||
|
- swung
|
||||||
|
- taken
|
||||||
|
- taught
|
||||||
|
- thought
|
||||||
|
- thrived
|
||||||
|
- thrown
|
||||||
|
- thrust
|
||||||
|
- told
|
||||||
|
- torn
|
||||||
|
- trodden
|
||||||
|
- understood
|
||||||
|
- upheld
|
||||||
|
- upset
|
||||||
|
- wed
|
||||||
|
- wept
|
||||||
|
- withheld
|
||||||
|
- withstood
|
||||||
|
- woken
|
||||||
|
- won
|
||||||
|
- worn
|
||||||
|
- wound
|
||||||
|
- woven
|
||||||
|
- written
|
||||||
|
- wrung
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
extends: existence
|
||||||
|
message: "Don't use periods with acronyms or initialisms such as '%s'."
|
||||||
|
link: 'https://developers.google.com/style/abbreviations'
|
||||||
|
level: error
|
||||||
|
nonword: true
|
||||||
|
tokens:
|
||||||
|
- '\b(?:[A-Z]\.){3,}'
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
extends: existence
|
||||||
|
message: "Commas and periods go inside quotation marks."
|
||||||
|
link: 'https://developers.google.com/style/quotation-marks'
|
||||||
|
level: error
|
||||||
|
nonword: true
|
||||||
|
tokens:
|
||||||
|
- '"[^"]+"[.,?]'
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
extends: existence
|
||||||
|
message: "Don't add words such as 'from' or 'between' to describe a range of numbers."
|
||||||
|
link: 'https://developers.google.com/style/hyphens'
|
||||||
|
nonword: true
|
||||||
|
level: warning
|
||||||
|
tokens:
|
||||||
|
- '(?:from|between)\s\d+\s?-\s?\d+'
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
extends: existence
|
||||||
|
message: "Use semicolons judiciously."
|
||||||
|
link: 'https://developers.google.com/style/semicolons'
|
||||||
|
nonword: true
|
||||||
|
scope: sentence
|
||||||
|
level: suggestion
|
||||||
|
tokens:
|
||||||
|
- ';'
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
extends: existence
|
||||||
|
message: "Don't use internet slang abbreviations such as '%s'."
|
||||||
|
link: 'https://developers.google.com/style/abbreviations'
|
||||||
|
ignorecase: true
|
||||||
|
level: error
|
||||||
|
tokens:
|
||||||
|
- 'tl;dr'
|
||||||
|
- ymmv
|
||||||
|
- rtfm
|
||||||
|
- imo
|
||||||
|
- fwiw
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
extends: existence
|
||||||
|
message: "'%s' should have one space."
|
||||||
|
link: 'https://developers.google.com/style/sentence-spacing'
|
||||||
|
level: error
|
||||||
|
nonword: true
|
||||||
|
action:
|
||||||
|
name: remove
|
||||||
|
tokens:
|
||||||
|
- '[a-z][.?!] {2,}[A-Z]'
|
||||||
|
- '[a-z][.?!][A-Z]'
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
extends: existence
|
||||||
|
message: "In general, use American spelling instead of '%s'."
|
||||||
|
link: 'https://developers.google.com/style/spelling'
|
||||||
|
ignorecase: true
|
||||||
|
level: warning
|
||||||
|
tokens:
|
||||||
|
- '(?:\w+)nised?'
|
||||||
|
- 'colour'
|
||||||
|
- 'labour'
|
||||||
|
- 'centre'
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
extends: existence
|
||||||
|
message: "Avoid time-based words like '%s' in product documentation."
|
||||||
|
link: https://developers.google.com/style/timeless-documentation
|
||||||
|
level: suggestion
|
||||||
|
ignorecase: true
|
||||||
|
# The guide also names 'now' and 'new', but both have common senses that aren't
|
||||||
|
# time-anchored ('create a new project'): adding them took a 950-file corpus of
|
||||||
|
# technical documentation from 14 hits to 117. 'recently' is left out too — every
|
||||||
|
# hit in that corpus was the UI idiom 'recently used'.
|
||||||
|
tokens:
|
||||||
|
- currently
|
||||||
|
- latest
|
||||||
|
- soon
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
extends: existence
|
||||||
|
message: "Put a nonbreaking space between the number and the unit in '%s'."
|
||||||
|
link: "https://developers.google.com/style/units-of-measure"
|
||||||
|
nonword: true
|
||||||
|
level: error
|
||||||
|
tokens:
|
||||||
|
- '\b\d+(?:B|kB|MB|GB|TB)\b'
|
||||||
|
- '\b\d+(?:ns|ms|min|h|d)\b'
|
||||||
|
# Seconds are split out so a decade ('1990s') isn't read as a unit.
|
||||||
|
- '\b\d+s\b(?<!\b(?:19|20)\d\ds\b)'
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
extends: existence
|
||||||
|
message: "Try to avoid using first-person plural like '%s'."
|
||||||
|
link: 'https://developers.google.com/style/pronouns#personal-pronouns'
|
||||||
|
level: warning
|
||||||
|
ignorecase: true
|
||||||
|
tokens:
|
||||||
|
- we
|
||||||
|
- we'(?:ve|re)
|
||||||
|
- ours?
|
||||||
|
- us
|
||||||
|
- let's
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
extends: existence
|
||||||
|
message: "Avoid using '%s'."
|
||||||
|
link: 'https://developers.google.com/style/tense'
|
||||||
|
ignorecase: true
|
||||||
|
level: warning
|
||||||
|
tokens:
|
||||||
|
- will
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
extends: substitution
|
||||||
|
message: "Use '%s' instead of '%s'."
|
||||||
|
link: "https://developers.google.com/style/word-list"
|
||||||
|
level: warning
|
||||||
|
# Case matters here: each key's own capitalization is what's being corrected,
|
||||||
|
# so ignorecase would make these match their own replacements. The rest of the
|
||||||
|
# word list lives in WordListCase.yml.
|
||||||
|
ignorecase: false
|
||||||
|
action:
|
||||||
|
name: replace
|
||||||
|
swap:
|
||||||
|
Ajax: AJAX
|
||||||
|
Android device: Android-powered device
|
||||||
|
android: Android
|
||||||
|
API explorer: APIs Explorer
|
||||||
|
authN: authentication
|
||||||
|
authZ: authorization
|
||||||
|
CLI: command-line tool
|
||||||
|
Cloud: Google Cloud Platform|GCP
|
||||||
|
Container Engine: Kubernetes Engine
|
||||||
|
Developers Console: Google API Console|API Console
|
||||||
|
Google account: Google Account
|
||||||
|
Google accounts: Google Accounts
|
||||||
|
Googling: search with Google
|
||||||
|
HTTPs: HTTPS
|
||||||
|
k8s: Kubernetes
|
||||||
|
SHA1: SHA-1|HAS-SHA1
|
||||||
|
url: URL
|
||||||
|
World Wide Web: web
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
extends: substitution
|
||||||
|
message: "Use '%s' instead of '%s'."
|
||||||
|
link: "https://developers.google.com/style/word-list"
|
||||||
|
level: warning
|
||||||
|
# The case-insensitive half of the word list, so sentence-initial use is caught
|
||||||
|
# ('Touch the screen', not only 'touch the screen'). Entries that must stay
|
||||||
|
# case-sensitive are in WordList.yml.
|
||||||
|
ignorecase: true
|
||||||
|
action:
|
||||||
|
name: replace
|
||||||
|
swap:
|
||||||
|
"(?:API Console|dev|developer) key": API key
|
||||||
|
"(?:cell ?phone|smart ?phone)": phone|mobile phone
|
||||||
|
"(?:dev|developer|APIs) console": API console
|
||||||
|
"(?:e-mail|Email|E-mail)": email
|
||||||
|
"(?:file ?path|path ?name)": path
|
||||||
|
"(?:kill|terminate|abort)": stop|exit|cancel|end
|
||||||
|
# Longest form first: with the shortest alternative leading, 'OAuth 2' matched
|
||||||
|
# only 'OAuth', so applying the suggestion produced 'OAuth 2.0 2'. The rule is
|
||||||
|
# already case-insensitive, so the inline (?i) is redundant. See issue #41.
|
||||||
|
'\bOauth2\.0\b|\bOAuth ?2\b(?!\.0)|\bOauth\b(?! ?2)': OAuth 2.0
|
||||||
|
"(?:ok|Okay)": OK|okay
|
||||||
|
"(?:WiFi|wifi)": Wi-Fi
|
||||||
|
'[\.]+apk': APK
|
||||||
|
'3\-D': 3D
|
||||||
|
'Google (?:I\-O|IO)': Google I/O
|
||||||
|
"tap (?:&|and) hold": touch & hold
|
||||||
|
"un(?:check|select)": clear
|
||||||
|
above: preceding
|
||||||
|
account name: username
|
||||||
|
action bar: app bar
|
||||||
|
admin: administrator
|
||||||
|
a\.k\.a|aka: or|also known as
|
||||||
|
application: app
|
||||||
|
approx\.: approximately
|
||||||
|
autoupdate: automatically update
|
||||||
|
cellular data: mobile data
|
||||||
|
cellular network: mobile network
|
||||||
|
chapter: documents|pages|sections
|
||||||
|
check box: checkbox
|
||||||
|
click on: click|click in
|
||||||
|
content type: media type
|
||||||
|
curated roles: predefined roles
|
||||||
|
data are: data is
|
||||||
|
disabled?: turn off|off
|
||||||
|
ephemeral IP address: ephemeral external IP address
|
||||||
|
fewer data: less data
|
||||||
|
file name: filename
|
||||||
|
firewalls: firewall rules
|
||||||
|
functionality: capability|feature
|
||||||
|
grayed-out: unavailable
|
||||||
|
in order to: to
|
||||||
|
ingest: import|load
|
||||||
|
long press: touch & hold
|
||||||
|
network IP address: internal IP address
|
||||||
|
omnibox: address bar
|
||||||
|
open-source: open source
|
||||||
|
overview screen: recents screen
|
||||||
|
regex: regular expression
|
||||||
|
sign into: sign in to
|
||||||
|
'(?<!single )sign-?on': single sign-on
|
||||||
|
static IP address: static external IP address
|
||||||
|
stylesheet: style sheet
|
||||||
|
synch: sync
|
||||||
|
tablename: table name
|
||||||
|
tablet: device
|
||||||
|
'touch(?! ?(?:&|and) hold)': tap
|
||||||
|
vs\.: versus
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"feed": "https://github.com/errata-ai/Google/releases.atom",
|
||||||
|
"vale_version": ">=1.0.0"
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
extends: metric
|
||||||
|
message: "Try to keep the Automated Readability Index (%s) below 8."
|
||||||
|
link: https://en.wikipedia.org/wiki/Automated_readability_index
|
||||||
|
|
||||||
|
formula: |
|
||||||
|
(4.71 * (characters / words)) + (0.5 * (words / sentences)) - 21.43
|
||||||
|
|
||||||
|
condition: "> 8"
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
extends: metric
|
||||||
|
message: "Try to keep the Coleman–Liau Index grade (%s) below 9."
|
||||||
|
link: https://en.wikipedia.org/wiki/Coleman%E2%80%93Liau_index
|
||||||
|
|
||||||
|
formula: |
|
||||||
|
(0.0588 * (characters / words) * 100) - (0.296 * (sentences / words) * 100) - 15.8
|
||||||
|
|
||||||
|
condition: "> 9"
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
extends: metric
|
||||||
|
message: "Try to keep the Flesch–Kincaid grade level (%s) below 8."
|
||||||
|
link: https://en.wikipedia.org/wiki/Flesch%E2%80%93Kincaid_readability_tests
|
||||||
|
|
||||||
|
formula: |
|
||||||
|
(0.39 * (words / sentences)) + (11.8 * (syllables / words)) - 15.59
|
||||||
|
|
||||||
|
condition: "> 8"
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
extends: metric
|
||||||
|
message: "Try to keep the Flesch reading ease score (%s) above 70."
|
||||||
|
link: https://en.wikipedia.org/wiki/Flesch%E2%80%93Kincaid_readability_tests
|
||||||
|
|
||||||
|
formula: |
|
||||||
|
206.835 - (1.015 * (words / sentences)) - (84.6 * (syllables / words))
|
||||||
|
|
||||||
|
condition: "< 70"
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
extends: metric
|
||||||
|
message: "Try to keep the Gunning-Fog index (%s) below 10."
|
||||||
|
link: https://en.wikipedia.org/wiki/Gunning_fog_index
|
||||||
|
|
||||||
|
formula: |
|
||||||
|
0.4 * ((words / sentences) + 100 * (complex_words / words))
|
||||||
|
|
||||||
|
condition: "> 10"
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
extends: metric
|
||||||
|
message: "Try to keep the LIX score (%s) below 35."
|
||||||
|
|
||||||
|
link: https://en.wikipedia.org/wiki/Lix_(readability_test)
|
||||||
|
# Very Easy: 20 - 25
|
||||||
|
#
|
||||||
|
# Easy: 30 - 35
|
||||||
|
#
|
||||||
|
# Medium: 40 - 45
|
||||||
|
#
|
||||||
|
# Difficult: 50 - 55
|
||||||
|
#
|
||||||
|
# Very Difficult: 60+
|
||||||
|
formula: |
|
||||||
|
(words / sentences) + ((long_words * 100) / words)
|
||||||
|
|
||||||
|
condition: "> 35"
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
extends: metric
|
||||||
|
message: "Try to keep the SMOG grade (%s) below 10."
|
||||||
|
link: https://en.wikipedia.org/wiki/SMOG
|
||||||
|
|
||||||
|
formula: |
|
||||||
|
1.0430 * math.sqrt((polysyllabic_words * 30.0) / sentences) + 3.1291
|
||||||
|
|
||||||
|
condition: "> 10"
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"feed": "https://github.com/errata-ai/Readability/releases.atom",
|
||||||
|
"vale_version": ">=2.13.0"
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
devx
|
||||||
|
grm
|
||||||
|
GRM
|
||||||
|
Gitea
|
||||||
|
ZITADEL
|
||||||
|
OpenTofu
|
||||||
|
Ansible
|
||||||
|
Vaultwarden
|
||||||
|
Nextcloud
|
||||||
|
Vikunja
|
||||||
|
Mattermost
|
||||||
|
Prometheus
|
||||||
|
Grafana
|
||||||
|
Loki
|
||||||
|
Alertmanager
|
||||||
|
Promtail
|
||||||
|
pyproject
|
||||||
|
tofu
|
||||||
|
act_runner
|
||||||
|
actionlint
|
||||||
|
hadolint
|
||||||
|
git-cliff
|
||||||
|
pre-commit
|
||||||
|
semver
|
||||||
|
changelog
|
||||||
|
idempotent
|
||||||
|
rootless
|
||||||
|
OIDC
|
||||||
|
SSO
|
||||||
|
SAML
|
||||||
|
LDAP
|
||||||
|
pytest
|
||||||
|
molecule
|
||||||
|
ruff
|
||||||
|
pyright
|
||||||
|
bandit
|
||||||
|
Vale
|
||||||
|
oblachno
|
||||||
|
Oblachno
|
||||||
|
Bulgarian
|
||||||
|
gitea-runner
|
||||||
|
runner
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
extends: existence
|
||||||
|
message: "Unlabeled code block — add a language tag (```bash, ```yaml, etc.)"
|
||||||
|
level: warning
|
||||||
|
scope: raw
|
||||||
|
raw:
|
||||||
|
- '(?ms)^\n```\n.*?^```\s*$'
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
extends: existence
|
||||||
|
message: "Avoid '%s' — it's condescending in technical documentation"
|
||||||
|
level: warning
|
||||||
|
ignorecase: true
|
||||||
|
tokens:
|
||||||
|
- '\bsimply\b'
|
||||||
|
- '\bjust\b'
|
||||||
|
- '\bobviously\b'
|
||||||
|
- '\bof course\b'
|
||||||
|
- '\bas you (can )?see\b'
|
||||||
|
- '\beasily\b'
|
||||||
|
- '\btrivial\b'
|
||||||
|
- '\bstraightforward\b'
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Custom Vale style for devx documentation
|
||||||
|
|
||||||
|
Project-specific terminology and style rules
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
extends: substitution
|
||||||
|
message: "Use '%s' instead of '%s' (terminology consistency)"
|
||||||
|
level: error
|
||||||
|
ignorecase: false
|
||||||
|
swap:
|
||||||
|
'\b(?i)gitea\b': Gitea
|
||||||
|
'\b(?i)zitadel\b': ZITADEL
|
||||||
|
'\b(?i)opentofu\b': OpenTofu
|
||||||
|
'\b(?i)vaultwarden\b': Vaultwarden
|
||||||
|
'\b(?i)nextcloud\b': Nextcloud
|
||||||
|
'\b(?i)mattermost\b': Mattermost
|
||||||
@@ -0,0 +1,702 @@
|
|||||||
|
extends: existence
|
||||||
|
message: "Try to avoid using clichés like '%s'."
|
||||||
|
ignorecase: true
|
||||||
|
level: warning
|
||||||
|
tokens:
|
||||||
|
- a chip off the old block
|
||||||
|
- a clean slate
|
||||||
|
- a dark and stormy night
|
||||||
|
- a far cry
|
||||||
|
- a fine kettle of fish
|
||||||
|
- a loose cannon
|
||||||
|
- a penny saved is a penny earned
|
||||||
|
- a tough row to hoe
|
||||||
|
- a word to the wise
|
||||||
|
- ace in the hole
|
||||||
|
- acid test
|
||||||
|
- add insult to injury
|
||||||
|
- against all odds
|
||||||
|
- air your dirty laundry
|
||||||
|
- all fun and games
|
||||||
|
- all in a day's work
|
||||||
|
- all talk, no action
|
||||||
|
- all thumbs
|
||||||
|
- all your eggs in one basket
|
||||||
|
- all's fair in love and war
|
||||||
|
- all's well that ends well
|
||||||
|
- almighty dollar
|
||||||
|
- American as apple pie
|
||||||
|
- an axe to grind
|
||||||
|
- another day, another dollar
|
||||||
|
- armed to the teeth
|
||||||
|
- as luck would have it
|
||||||
|
- as old as time
|
||||||
|
- as the crow flies
|
||||||
|
- at loose ends
|
||||||
|
- at my wits end
|
||||||
|
- avoid like the plague
|
||||||
|
- babe in the woods
|
||||||
|
- back against the wall
|
||||||
|
- back in the saddle
|
||||||
|
- back to square one
|
||||||
|
- back to the drawing board
|
||||||
|
- bad to the bone
|
||||||
|
- badge of honor
|
||||||
|
- bald faced liar
|
||||||
|
- ballpark figure
|
||||||
|
- banging your head against a brick wall
|
||||||
|
- baptism by fire
|
||||||
|
- barking up the wrong tree
|
||||||
|
- bat out of hell
|
||||||
|
- be all and end all
|
||||||
|
- beat a dead horse
|
||||||
|
- beat around the bush
|
||||||
|
- been there, done that
|
||||||
|
- beggars can't be choosers
|
||||||
|
- behind the eight ball
|
||||||
|
- bend over backwards
|
||||||
|
- benefit of the doubt
|
||||||
|
- bent out of shape
|
||||||
|
- best thing since sliced bread
|
||||||
|
- bet your bottom dollar
|
||||||
|
- better half
|
||||||
|
- better late than never
|
||||||
|
- better mousetrap
|
||||||
|
- better safe than sorry
|
||||||
|
- between a rock and a hard place
|
||||||
|
- beyond the pale
|
||||||
|
- bide your time
|
||||||
|
- big as life
|
||||||
|
- big cheese
|
||||||
|
- big fish in a small pond
|
||||||
|
- big man on campus
|
||||||
|
- bigger they are the harder they fall
|
||||||
|
- bird in the hand
|
||||||
|
- bird's eye view
|
||||||
|
- birds and the bees
|
||||||
|
- birds of a feather flock together
|
||||||
|
- bit the hand that feeds you
|
||||||
|
- bite the bullet
|
||||||
|
- bite the dust
|
||||||
|
- bitten off more than he can chew
|
||||||
|
- black as coal
|
||||||
|
- black as pitch
|
||||||
|
- black as the ace of spades
|
||||||
|
- blast from the past
|
||||||
|
- bleeding heart
|
||||||
|
- blessing in disguise
|
||||||
|
- blind ambition
|
||||||
|
- blind as a bat
|
||||||
|
- blind leading the blind
|
||||||
|
- blood is thicker than water
|
||||||
|
- blood sweat and tears
|
||||||
|
- blow off steam
|
||||||
|
- blow your own horn
|
||||||
|
- blushing bride
|
||||||
|
- boils down to
|
||||||
|
- bolt from the blue
|
||||||
|
- bone to pick
|
||||||
|
- bored stiff
|
||||||
|
- bored to tears
|
||||||
|
- bottomless pit
|
||||||
|
- boys will be boys
|
||||||
|
- bright and early
|
||||||
|
- brings home the bacon
|
||||||
|
- broad across the beam
|
||||||
|
- broken record
|
||||||
|
- brought back to reality
|
||||||
|
- bull by the horns
|
||||||
|
- bull in a china shop
|
||||||
|
- burn the midnight oil
|
||||||
|
- burning question
|
||||||
|
- burning the candle at both ends
|
||||||
|
- burst your bubble
|
||||||
|
- bury the hatchet
|
||||||
|
- busy as a bee
|
||||||
|
- by hook or by crook
|
||||||
|
- call a spade a spade
|
||||||
|
- called onto the carpet
|
||||||
|
- calm before the storm
|
||||||
|
- can of worms
|
||||||
|
- can't cut the mustard
|
||||||
|
- can't hold a candle to
|
||||||
|
- case of mistaken identity
|
||||||
|
- cat got your tongue
|
||||||
|
- cat's meow
|
||||||
|
- caught in the crossfire
|
||||||
|
- caught red-handed
|
||||||
|
- checkered past
|
||||||
|
- chomping at the bit
|
||||||
|
- cleanliness is next to godliness
|
||||||
|
- clear as a bell
|
||||||
|
- clear as mud
|
||||||
|
- close to the vest
|
||||||
|
- cock and bull story
|
||||||
|
- cold shoulder
|
||||||
|
- come hell or high water
|
||||||
|
- cool as a cucumber
|
||||||
|
- cool, calm, and collected
|
||||||
|
- cost a king's ransom
|
||||||
|
- count your blessings
|
||||||
|
- crack of dawn
|
||||||
|
- crash course
|
||||||
|
- creature comforts
|
||||||
|
- cross that bridge when you come to it
|
||||||
|
- crushing blow
|
||||||
|
- cry like a baby
|
||||||
|
- cry me a river
|
||||||
|
- cry over spilt milk
|
||||||
|
- crystal clear
|
||||||
|
- curiosity killed the cat
|
||||||
|
- cut and dried
|
||||||
|
- cut through the red tape
|
||||||
|
- cut to the chase
|
||||||
|
- cute as a bugs ear
|
||||||
|
- cute as a button
|
||||||
|
- cute as a puppy
|
||||||
|
- cuts to the quick
|
||||||
|
- dark before the dawn
|
||||||
|
- day in, day out
|
||||||
|
- dead as a doornail
|
||||||
|
- devil is in the details
|
||||||
|
- dime a dozen
|
||||||
|
- divide and conquer
|
||||||
|
- dog and pony show
|
||||||
|
- dog days
|
||||||
|
- dog eat dog
|
||||||
|
- dog tired
|
||||||
|
- don't burn your bridges
|
||||||
|
- don't count your chickens
|
||||||
|
- don't look a gift horse in the mouth
|
||||||
|
- don't rock the boat
|
||||||
|
- don't step on anyone's toes
|
||||||
|
- don't take any wooden nickels
|
||||||
|
- down and out
|
||||||
|
- down at the heels
|
||||||
|
- down in the dumps
|
||||||
|
- down the hatch
|
||||||
|
- down to earth
|
||||||
|
- draw the line
|
||||||
|
- dressed to kill
|
||||||
|
- dressed to the nines
|
||||||
|
- drives me up the wall
|
||||||
|
- dull as dishwater
|
||||||
|
- dyed in the wool
|
||||||
|
- eagle eye
|
||||||
|
- ear to the ground
|
||||||
|
- early bird catches the worm
|
||||||
|
- easier said than done
|
||||||
|
- easy as pie
|
||||||
|
- eat your heart out
|
||||||
|
- eat your words
|
||||||
|
- eleventh hour
|
||||||
|
- even the playing field
|
||||||
|
- every dog has its day
|
||||||
|
- every fiber of my being
|
||||||
|
- everything but the kitchen sink
|
||||||
|
- eye for an eye
|
||||||
|
- face the music
|
||||||
|
- facts of life
|
||||||
|
- fair weather friend
|
||||||
|
- fall by the wayside
|
||||||
|
- fan the flames
|
||||||
|
- feast or famine
|
||||||
|
- feather your nest
|
||||||
|
- feathered friends
|
||||||
|
- few and far between
|
||||||
|
- fifteen minutes of fame
|
||||||
|
- filthy vermin
|
||||||
|
- fine kettle of fish
|
||||||
|
- fish out of water
|
||||||
|
- fishing for a compliment
|
||||||
|
- fit as a fiddle
|
||||||
|
- fit the bill
|
||||||
|
- fit to be tied
|
||||||
|
- flash in the pan
|
||||||
|
- flat as a pancake
|
||||||
|
- flip your lid
|
||||||
|
- flog a dead horse
|
||||||
|
- fly by night
|
||||||
|
- fly the coop
|
||||||
|
- follow your heart
|
||||||
|
- for all intents and purposes
|
||||||
|
- for the birds
|
||||||
|
- for what it's worth
|
||||||
|
- force of nature
|
||||||
|
- force to be reckoned with
|
||||||
|
- forgive and forget
|
||||||
|
- fox in the henhouse
|
||||||
|
- free and easy
|
||||||
|
- free as a bird
|
||||||
|
- fresh as a daisy
|
||||||
|
- full steam ahead
|
||||||
|
- fun in the sun
|
||||||
|
- garbage in, garbage out
|
||||||
|
- gentle as a lamb
|
||||||
|
- get a kick out of
|
||||||
|
- get a leg up
|
||||||
|
- get down and dirty
|
||||||
|
- get the lead out
|
||||||
|
- get to the bottom of
|
||||||
|
- get your feet wet
|
||||||
|
- gets my goat
|
||||||
|
- gilding the lily
|
||||||
|
- give and take
|
||||||
|
- go against the grain
|
||||||
|
- go at it tooth and nail
|
||||||
|
- go for broke
|
||||||
|
- go him one better
|
||||||
|
- go the extra mile
|
||||||
|
- go with the flow
|
||||||
|
- goes without saying
|
||||||
|
- good as gold
|
||||||
|
- good deed for the day
|
||||||
|
- good things come to those who wait
|
||||||
|
- good time was had by all
|
||||||
|
- good times were had by all
|
||||||
|
- greased lightning
|
||||||
|
- greek to me
|
||||||
|
- green thumb
|
||||||
|
- green-eyed monster
|
||||||
|
- grist for the mill
|
||||||
|
- growing like a weed
|
||||||
|
- hair of the dog
|
||||||
|
- hand to mouth
|
||||||
|
- happy as a clam
|
||||||
|
- happy as a lark
|
||||||
|
- hasn't a clue
|
||||||
|
- have a nice day
|
||||||
|
- have high hopes
|
||||||
|
- have the last laugh
|
||||||
|
- haven't got a row to hoe
|
||||||
|
- head honcho
|
||||||
|
- head over heels
|
||||||
|
- hear a pin drop
|
||||||
|
- heard it through the grapevine
|
||||||
|
- heart's content
|
||||||
|
- heavy as lead
|
||||||
|
- hem and haw
|
||||||
|
- high and dry
|
||||||
|
- high and mighty
|
||||||
|
- high as a kite
|
||||||
|
- hit paydirt
|
||||||
|
- hold your head up high
|
||||||
|
- hold your horses
|
||||||
|
- hold your own
|
||||||
|
- hold your tongue
|
||||||
|
- honest as the day is long
|
||||||
|
- horns of a dilemma
|
||||||
|
- horse of a different color
|
||||||
|
- hot under the collar
|
||||||
|
- hour of need
|
||||||
|
- I beg to differ
|
||||||
|
- icing on the cake
|
||||||
|
- if the shoe fits
|
||||||
|
- if the shoe were on the other foot
|
||||||
|
- in a jam
|
||||||
|
- in a jiffy
|
||||||
|
- in a nutshell
|
||||||
|
- in a pig's eye
|
||||||
|
- in a pinch
|
||||||
|
- in a word
|
||||||
|
- in hot water
|
||||||
|
- in the gutter
|
||||||
|
- in the nick of time
|
||||||
|
- in the thick of it
|
||||||
|
- in your dreams
|
||||||
|
- it ain't over till the fat lady sings
|
||||||
|
- it goes without saying
|
||||||
|
- it takes all kinds
|
||||||
|
- it takes one to know one
|
||||||
|
- it's a small world
|
||||||
|
- it's only a matter of time
|
||||||
|
- ivory tower
|
||||||
|
- Jack of all trades
|
||||||
|
- jockey for position
|
||||||
|
- jog your memory
|
||||||
|
- joined at the hip
|
||||||
|
- judge a book by its cover
|
||||||
|
- jump down your throat
|
||||||
|
- jump in with both feet
|
||||||
|
- jump on the bandwagon
|
||||||
|
- jump the gun
|
||||||
|
- jump to conclusions
|
||||||
|
- just a hop, skip, and a jump
|
||||||
|
- just the ticket
|
||||||
|
- justice is blind
|
||||||
|
- keep a stiff upper lip
|
||||||
|
- keep an eye on
|
||||||
|
- keep it simple, stupid
|
||||||
|
- keep the home fires burning
|
||||||
|
- keep up with the Joneses
|
||||||
|
- keep your chin up
|
||||||
|
- keep your fingers crossed
|
||||||
|
- kick the bucket
|
||||||
|
- kick up your heels
|
||||||
|
- kick your feet up
|
||||||
|
- kid in a candy store
|
||||||
|
- kill two birds with one stone
|
||||||
|
- kiss of death
|
||||||
|
- knock it out of the park
|
||||||
|
- knock on wood
|
||||||
|
- knock your socks off
|
||||||
|
- know him from Adam
|
||||||
|
- know the ropes
|
||||||
|
- know the score
|
||||||
|
- knuckle down
|
||||||
|
- knuckle sandwich
|
||||||
|
- knuckle under
|
||||||
|
- labor of love
|
||||||
|
- ladder of success
|
||||||
|
- land on your feet
|
||||||
|
- lap of luxury
|
||||||
|
- last but not least
|
||||||
|
- last hurrah
|
||||||
|
- last-ditch effort
|
||||||
|
- law of the jungle
|
||||||
|
- law of the land
|
||||||
|
- lay down the law
|
||||||
|
- leaps and bounds
|
||||||
|
- let sleeping dogs lie
|
||||||
|
- let the cat out of the bag
|
||||||
|
- let the good times roll
|
||||||
|
- let your hair down
|
||||||
|
- let's talk turkey
|
||||||
|
- letter perfect
|
||||||
|
- lick your wounds
|
||||||
|
- lies like a rug
|
||||||
|
- life's a bitch
|
||||||
|
- life's a grind
|
||||||
|
- light at the end of the tunnel
|
||||||
|
- lighter than a feather
|
||||||
|
- lighter than air
|
||||||
|
- like clockwork
|
||||||
|
- like father like son
|
||||||
|
- like taking candy from a baby
|
||||||
|
- like there's no tomorrow
|
||||||
|
- lion's share
|
||||||
|
- live and learn
|
||||||
|
- live and let live
|
||||||
|
- long and short of it
|
||||||
|
- long lost love
|
||||||
|
- look before you leap
|
||||||
|
- look down your nose
|
||||||
|
- look what the cat dragged in
|
||||||
|
- looking a gift horse in the mouth
|
||||||
|
- looks like death warmed over
|
||||||
|
- loose cannon
|
||||||
|
- lose your head
|
||||||
|
- lose your temper
|
||||||
|
- loud as a horn
|
||||||
|
- lounge lizard
|
||||||
|
- loved and lost
|
||||||
|
- low man on the totem pole
|
||||||
|
- luck of the draw
|
||||||
|
- luck of the Irish
|
||||||
|
- make hay while the sun shines
|
||||||
|
- make money hand over fist
|
||||||
|
- make my day
|
||||||
|
- make the best of a bad situation
|
||||||
|
- make the best of it
|
||||||
|
- make your blood boil
|
||||||
|
- man of few words
|
||||||
|
- man's best friend
|
||||||
|
- mark my words
|
||||||
|
- meaningful dialogue
|
||||||
|
- missed the boat on that one
|
||||||
|
- moment in the sun
|
||||||
|
- moment of glory
|
||||||
|
- moment of truth
|
||||||
|
- money to burn
|
||||||
|
- more power to you
|
||||||
|
- more than one way to skin a cat
|
||||||
|
- movers and shakers
|
||||||
|
- moving experience
|
||||||
|
- naked as a jaybird
|
||||||
|
- naked truth
|
||||||
|
- neat as a pin
|
||||||
|
- needle in a haystack
|
||||||
|
- needless to say
|
||||||
|
- neither here nor there
|
||||||
|
- never look back
|
||||||
|
- never say never
|
||||||
|
- nip and tuck
|
||||||
|
- nip it in the bud
|
||||||
|
- no guts, no glory
|
||||||
|
- no love lost
|
||||||
|
- no pain, no gain
|
||||||
|
- no skin off my back
|
||||||
|
- no stone unturned
|
||||||
|
- no time like the present
|
||||||
|
- no use crying over spilled milk
|
||||||
|
- nose to the grindstone
|
||||||
|
- not a hope in hell
|
||||||
|
- not a minute's peace
|
||||||
|
- not in my backyard
|
||||||
|
- not playing with a full deck
|
||||||
|
- not the end of the world
|
||||||
|
- not written in stone
|
||||||
|
- nothing to sneeze at
|
||||||
|
- nothing ventured nothing gained
|
||||||
|
- now we're cooking
|
||||||
|
- off the top of my head
|
||||||
|
- off the wagon
|
||||||
|
- off the wall
|
||||||
|
- old hat
|
||||||
|
- older and wiser
|
||||||
|
- older than dirt
|
||||||
|
- older than Methuselah
|
||||||
|
- on a roll
|
||||||
|
- on cloud nine
|
||||||
|
- on pins and needles
|
||||||
|
- on the bandwagon
|
||||||
|
- on the money
|
||||||
|
- on the nose
|
||||||
|
- on the rocks
|
||||||
|
- on the spot
|
||||||
|
- on the tip of my tongue
|
||||||
|
- on the wagon
|
||||||
|
- on thin ice
|
||||||
|
- once bitten, twice shy
|
||||||
|
- one bad apple doesn't spoil the bushel
|
||||||
|
- one born every minute
|
||||||
|
- one brick short
|
||||||
|
- one foot in the grave
|
||||||
|
- one in a million
|
||||||
|
- one red cent
|
||||||
|
- only game in town
|
||||||
|
- open a can of worms
|
||||||
|
- open and shut case
|
||||||
|
- open the flood gates
|
||||||
|
- opportunity doesn't knock twice
|
||||||
|
- out of pocket
|
||||||
|
- out of sight, out of mind
|
||||||
|
- out of the frying pan into the fire
|
||||||
|
- out of the woods
|
||||||
|
- out on a limb
|
||||||
|
- over a barrel
|
||||||
|
- over the hump
|
||||||
|
- pain and suffering
|
||||||
|
- pain in the
|
||||||
|
- panic button
|
||||||
|
- par for the course
|
||||||
|
- part and parcel
|
||||||
|
- party pooper
|
||||||
|
- pass the buck
|
||||||
|
- patience is a virtue
|
||||||
|
- pay through the nose
|
||||||
|
- penny pincher
|
||||||
|
- perfect storm
|
||||||
|
- pig in a poke
|
||||||
|
- pile it on
|
||||||
|
- pillar of the community
|
||||||
|
- pin your hopes on
|
||||||
|
- pitter patter of little feet
|
||||||
|
- plain as day
|
||||||
|
- plain as the nose on your face
|
||||||
|
- play by the rules
|
||||||
|
- play your cards right
|
||||||
|
- playing the field
|
||||||
|
- playing with fire
|
||||||
|
- pleased as punch
|
||||||
|
- plenty of fish in the sea
|
||||||
|
- point with pride
|
||||||
|
- poor as a church mouse
|
||||||
|
- pot calling the kettle black
|
||||||
|
- pretty as a picture
|
||||||
|
- pull a fast one
|
||||||
|
- pull your punches
|
||||||
|
- pulling your leg
|
||||||
|
- pure as the driven snow
|
||||||
|
- put it in a nutshell
|
||||||
|
- put one over on you
|
||||||
|
- put the cart before the horse
|
||||||
|
- put the pedal to the metal
|
||||||
|
- put your best foot forward
|
||||||
|
- put your foot down
|
||||||
|
- quick as a bunny
|
||||||
|
- quick as a lick
|
||||||
|
- quick as a wink
|
||||||
|
- quick as lightning
|
||||||
|
- quiet as a dormouse
|
||||||
|
- rags to riches
|
||||||
|
- raining buckets
|
||||||
|
- raining cats and dogs
|
||||||
|
- rank and file
|
||||||
|
- rat race
|
||||||
|
- reap what you sow
|
||||||
|
- red as a beet
|
||||||
|
- red herring
|
||||||
|
- reinvent the wheel
|
||||||
|
- rich and famous
|
||||||
|
- rings a bell
|
||||||
|
- ripe old age
|
||||||
|
- ripped me off
|
||||||
|
- rise and shine
|
||||||
|
- road to hell is paved with good intentions
|
||||||
|
- rob Peter to pay Paul
|
||||||
|
- roll over in the grave
|
||||||
|
- rub the wrong way
|
||||||
|
- ruled the roost
|
||||||
|
- running in circles
|
||||||
|
- sad but true
|
||||||
|
- sadder but wiser
|
||||||
|
- salt of the earth
|
||||||
|
- scared stiff
|
||||||
|
- scared to death
|
||||||
|
- sealed with a kiss
|
||||||
|
- second to none
|
||||||
|
- see eye to eye
|
||||||
|
- seen the light
|
||||||
|
- seize the day
|
||||||
|
- set the record straight
|
||||||
|
- set the world on fire
|
||||||
|
- set your teeth on edge
|
||||||
|
- sharp as a tack
|
||||||
|
- shoot for the moon
|
||||||
|
- shoot the breeze
|
||||||
|
- shot in the dark
|
||||||
|
- shoulder to the wheel
|
||||||
|
- sick as a dog
|
||||||
|
- sigh of relief
|
||||||
|
- signed, sealed, and delivered
|
||||||
|
- sink or swim
|
||||||
|
- six of one, half a dozen of another
|
||||||
|
- skating on thin ice
|
||||||
|
- slept like a log
|
||||||
|
- slinging mud
|
||||||
|
- slippery as an eel
|
||||||
|
- slow as molasses
|
||||||
|
- smart as a whip
|
||||||
|
- smooth as a baby's bottom
|
||||||
|
- sneaking suspicion
|
||||||
|
- snug as a bug in a rug
|
||||||
|
- sow wild oats
|
||||||
|
- spare the rod, spoil the child
|
||||||
|
- speak of the devil
|
||||||
|
- spilled the beans
|
||||||
|
- spinning your wheels
|
||||||
|
- spitting image of
|
||||||
|
- spoke with relish
|
||||||
|
- spread like wildfire
|
||||||
|
- spring to life
|
||||||
|
- squeaky wheel gets the grease
|
||||||
|
- stands out like a sore thumb
|
||||||
|
- start from scratch
|
||||||
|
- stick in the mud
|
||||||
|
- still waters run deep
|
||||||
|
- stitch in time
|
||||||
|
- stop and smell the roses
|
||||||
|
- straight as an arrow
|
||||||
|
- straw that broke the camel's back
|
||||||
|
- strong as an ox
|
||||||
|
- stubborn as a mule
|
||||||
|
- stuff that dreams are made of
|
||||||
|
- stuffed shirt
|
||||||
|
- sweating blood
|
||||||
|
- sweating bullets
|
||||||
|
- take a load off
|
||||||
|
- take one for the team
|
||||||
|
- take the bait
|
||||||
|
- take the bull by the horns
|
||||||
|
- take the plunge
|
||||||
|
- takes one to know one
|
||||||
|
- takes two to tango
|
||||||
|
- the more the merrier
|
||||||
|
- the real deal
|
||||||
|
- the real McCoy
|
||||||
|
- the red carpet treatment
|
||||||
|
- the same old story
|
||||||
|
- there is no accounting for taste
|
||||||
|
- thick as a brick
|
||||||
|
- thick as thieves
|
||||||
|
- thin as a rail
|
||||||
|
- think outside of the box
|
||||||
|
- third time's the charm
|
||||||
|
- this day and age
|
||||||
|
- this hurts me worse than it hurts you
|
||||||
|
- this point in time
|
||||||
|
- three sheets to the wind
|
||||||
|
- through thick and thin
|
||||||
|
- throw in the towel
|
||||||
|
- tie one on
|
||||||
|
- tighter than a drum
|
||||||
|
- time and time again
|
||||||
|
- time is of the essence
|
||||||
|
- tip of the iceberg
|
||||||
|
- tired but happy
|
||||||
|
- to coin a phrase
|
||||||
|
- to each his own
|
||||||
|
- to make a long story short
|
||||||
|
- to the best of my knowledge
|
||||||
|
- toe the line
|
||||||
|
- tongue in cheek
|
||||||
|
- too good to be true
|
||||||
|
- too hot to handle
|
||||||
|
- too numerous to mention
|
||||||
|
- touch with a ten foot pole
|
||||||
|
- tough as nails
|
||||||
|
- trial and error
|
||||||
|
- trials and tribulations
|
||||||
|
- tried and true
|
||||||
|
- trip down memory lane
|
||||||
|
- twist of fate
|
||||||
|
- two cents worth
|
||||||
|
- two peas in a pod
|
||||||
|
- ugly as sin
|
||||||
|
- under the counter
|
||||||
|
- under the gun
|
||||||
|
- under the same roof
|
||||||
|
- under the weather
|
||||||
|
- until the cows come home
|
||||||
|
- unvarnished truth
|
||||||
|
- up the creek
|
||||||
|
- uphill battle
|
||||||
|
- upper crust
|
||||||
|
- upset the applecart
|
||||||
|
- vain attempt
|
||||||
|
- vain effort
|
||||||
|
- vanquish the enemy
|
||||||
|
- vested interest
|
||||||
|
- waiting for the other shoe to drop
|
||||||
|
- wakeup call
|
||||||
|
- warm welcome
|
||||||
|
- watch your p's and q's
|
||||||
|
- watch your tongue
|
||||||
|
- watching the clock
|
||||||
|
- water under the bridge
|
||||||
|
- weather the storm
|
||||||
|
- weed them out
|
||||||
|
- week of Sundays
|
||||||
|
- went belly up
|
||||||
|
- wet behind the ears
|
||||||
|
- what goes around comes around
|
||||||
|
- what you see is what you get
|
||||||
|
- when it rains, it pours
|
||||||
|
- when push comes to shove
|
||||||
|
- when the cat's away
|
||||||
|
- when the going gets tough, the tough get going
|
||||||
|
- white as a sheet
|
||||||
|
- whole ball of wax
|
||||||
|
- whole hog
|
||||||
|
- whole nine yards
|
||||||
|
- wild goose chase
|
||||||
|
- will wonders never cease?
|
||||||
|
- wisdom of the ages
|
||||||
|
- wise as an owl
|
||||||
|
- wolf at the door
|
||||||
|
- words fail me
|
||||||
|
- work like a dog
|
||||||
|
- world weary
|
||||||
|
- worst nightmare
|
||||||
|
- worth its weight in gold
|
||||||
|
- wrong side of the bed
|
||||||
|
- yanking your chain
|
||||||
|
- yappy as a dog
|
||||||
|
- years young
|
||||||
|
- you are what you eat
|
||||||
|
- you can run but you can't hide
|
||||||
|
- you only live once
|
||||||
|
- you're the boss
|
||||||
|
- young and foolish
|
||||||
|
- young and vibrant
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
extends: existence
|
||||||
|
message: "Try to avoid using '%s'."
|
||||||
|
ignorecase: true
|
||||||
|
level: suggestion
|
||||||
|
tokens:
|
||||||
|
- am
|
||||||
|
- are
|
||||||
|
- aren't
|
||||||
|
- be
|
||||||
|
- been
|
||||||
|
- being
|
||||||
|
- he's
|
||||||
|
- here's
|
||||||
|
- here's
|
||||||
|
- how's
|
||||||
|
- i'm
|
||||||
|
- is
|
||||||
|
- isn't
|
||||||
|
- it's
|
||||||
|
- she's
|
||||||
|
- that's
|
||||||
|
- there's
|
||||||
|
- they're
|
||||||
|
- was
|
||||||
|
- wasn't
|
||||||
|
- we're
|
||||||
|
- were
|
||||||
|
- weren't
|
||||||
|
- what's
|
||||||
|
- where's
|
||||||
|
- who's
|
||||||
|
- you're
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
extends: repetition
|
||||||
|
message: "'%s' is repeated!"
|
||||||
|
level: warning
|
||||||
|
alpha: true
|
||||||
|
action:
|
||||||
|
name: edit
|
||||||
|
params:
|
||||||
|
- truncate
|
||||||
|
- " "
|
||||||
|
tokens:
|
||||||
|
- '[^\s]+'
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
extends: existence
|
||||||
|
message: "'%s' may be passive voice. Use active voice if you can."
|
||||||
|
ignorecase: true
|
||||||
|
level: warning
|
||||||
|
raw:
|
||||||
|
- \b(am|are|were|being|is|been|was|be)\b\s*
|
||||||
|
tokens:
|
||||||
|
- '[\w]+ed'
|
||||||
|
- awoken
|
||||||
|
- beat
|
||||||
|
- become
|
||||||
|
- been
|
||||||
|
- begun
|
||||||
|
- bent
|
||||||
|
- beset
|
||||||
|
- bet
|
||||||
|
- bid
|
||||||
|
- bidden
|
||||||
|
- bitten
|
||||||
|
- bled
|
||||||
|
- blown
|
||||||
|
- born
|
||||||
|
- bought
|
||||||
|
- bound
|
||||||
|
- bred
|
||||||
|
- broadcast
|
||||||
|
- broken
|
||||||
|
- brought
|
||||||
|
- built
|
||||||
|
- burnt
|
||||||
|
- burst
|
||||||
|
- cast
|
||||||
|
- caught
|
||||||
|
- chosen
|
||||||
|
- clung
|
||||||
|
- come
|
||||||
|
- cost
|
||||||
|
- crept
|
||||||
|
- cut
|
||||||
|
- dealt
|
||||||
|
- dived
|
||||||
|
- done
|
||||||
|
- drawn
|
||||||
|
- dreamt
|
||||||
|
- driven
|
||||||
|
- drunk
|
||||||
|
- dug
|
||||||
|
- eaten
|
||||||
|
- fallen
|
||||||
|
- fed
|
||||||
|
- felt
|
||||||
|
- fit
|
||||||
|
- fled
|
||||||
|
- flown
|
||||||
|
- flung
|
||||||
|
- forbidden
|
||||||
|
- foregone
|
||||||
|
- forgiven
|
||||||
|
- forgotten
|
||||||
|
- forsaken
|
||||||
|
- fought
|
||||||
|
- found
|
||||||
|
- frozen
|
||||||
|
- given
|
||||||
|
- gone
|
||||||
|
- gotten
|
||||||
|
- ground
|
||||||
|
- grown
|
||||||
|
- heard
|
||||||
|
- held
|
||||||
|
- hidden
|
||||||
|
- hit
|
||||||
|
- hung
|
||||||
|
- hurt
|
||||||
|
- kept
|
||||||
|
- knelt
|
||||||
|
- knit
|
||||||
|
- known
|
||||||
|
- laid
|
||||||
|
- lain
|
||||||
|
- leapt
|
||||||
|
- learnt
|
||||||
|
- led
|
||||||
|
- left
|
||||||
|
- lent
|
||||||
|
- let
|
||||||
|
- lighted
|
||||||
|
- lost
|
||||||
|
- made
|
||||||
|
- meant
|
||||||
|
- met
|
||||||
|
- misspelt
|
||||||
|
- mistaken
|
||||||
|
- mown
|
||||||
|
- overcome
|
||||||
|
- overdone
|
||||||
|
- overtaken
|
||||||
|
- overthrown
|
||||||
|
- paid
|
||||||
|
- pled
|
||||||
|
- proven
|
||||||
|
- put
|
||||||
|
- quit
|
||||||
|
- read
|
||||||
|
- rid
|
||||||
|
- ridden
|
||||||
|
- risen
|
||||||
|
- run
|
||||||
|
- rung
|
||||||
|
- said
|
||||||
|
- sat
|
||||||
|
- sawn
|
||||||
|
- seen
|
||||||
|
- sent
|
||||||
|
- set
|
||||||
|
- sewn
|
||||||
|
- shaken
|
||||||
|
- shaven
|
||||||
|
- shed
|
||||||
|
- shod
|
||||||
|
- shone
|
||||||
|
- shorn
|
||||||
|
- shot
|
||||||
|
- shown
|
||||||
|
- shrunk
|
||||||
|
- shut
|
||||||
|
- slain
|
||||||
|
- slept
|
||||||
|
- slid
|
||||||
|
- slit
|
||||||
|
- slung
|
||||||
|
- smitten
|
||||||
|
- sold
|
||||||
|
- sought
|
||||||
|
- sown
|
||||||
|
- sped
|
||||||
|
- spent
|
||||||
|
- spilt
|
||||||
|
- spit
|
||||||
|
- split
|
||||||
|
- spoken
|
||||||
|
- spread
|
||||||
|
- sprung
|
||||||
|
- spun
|
||||||
|
- stolen
|
||||||
|
- stood
|
||||||
|
- stridden
|
||||||
|
- striven
|
||||||
|
- struck
|
||||||
|
- strung
|
||||||
|
- stuck
|
||||||
|
- stung
|
||||||
|
- stunk
|
||||||
|
- sung
|
||||||
|
- sunk
|
||||||
|
- swept
|
||||||
|
- swollen
|
||||||
|
- sworn
|
||||||
|
- swum
|
||||||
|
- swung
|
||||||
|
- taken
|
||||||
|
- taught
|
||||||
|
- thought
|
||||||
|
- thrived
|
||||||
|
- thrown
|
||||||
|
- thrust
|
||||||
|
- told
|
||||||
|
- torn
|
||||||
|
- trodden
|
||||||
|
- understood
|
||||||
|
- upheld
|
||||||
|
- upset
|
||||||
|
- wed
|
||||||
|
- wept
|
||||||
|
- withheld
|
||||||
|
- withstood
|
||||||
|
- woken
|
||||||
|
- won
|
||||||
|
- worn
|
||||||
|
- wound
|
||||||
|
- woven
|
||||||
|
- written
|
||||||
|
- wrung
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
Based on [write-good](https://github.com/btford/write-good).
|
||||||
|
|
||||||
|
> Naive linter for English prose for developers who can't write good and wanna learn to do other stuff good too.
|
||||||
|
|
||||||
|
```
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2014 Brian Ford
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
```
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
extends: existence
|
||||||
|
message: "Don't start a sentence with '%s'."
|
||||||
|
level: error
|
||||||
|
raw:
|
||||||
|
- '(?:[;-]\s)so[\s,]|\bSo[\s,]'
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
extends: existence
|
||||||
|
message: "Don't start a sentence with '%s'."
|
||||||
|
ignorecase: false
|
||||||
|
level: error
|
||||||
|
raw:
|
||||||
|
- '(?:[;-]\s)There\s(is|are)|\bThere\s(is|are)\b'
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
extends: existence
|
||||||
|
message: "'%s' is too wordy."
|
||||||
|
ignorecase: true
|
||||||
|
level: warning
|
||||||
|
tokens:
|
||||||
|
- a number of
|
||||||
|
- abundance
|
||||||
|
- accede to
|
||||||
|
- accelerate
|
||||||
|
- accentuate
|
||||||
|
- accompany
|
||||||
|
- accomplish
|
||||||
|
- accorded
|
||||||
|
- accrue
|
||||||
|
- acquiesce
|
||||||
|
- acquire
|
||||||
|
- additional
|
||||||
|
- adjacent to
|
||||||
|
- adjustment
|
||||||
|
- admissible
|
||||||
|
- advantageous
|
||||||
|
- adversely impact
|
||||||
|
- advise
|
||||||
|
- aforementioned
|
||||||
|
- aggregate
|
||||||
|
- aircraft
|
||||||
|
- all of
|
||||||
|
- all things considered
|
||||||
|
- alleviate
|
||||||
|
- allocate
|
||||||
|
- along the lines of
|
||||||
|
- already existing
|
||||||
|
- alternatively
|
||||||
|
- amazing
|
||||||
|
- ameliorate
|
||||||
|
- anticipate
|
||||||
|
- apparent
|
||||||
|
- appreciable
|
||||||
|
- as a matter of fact
|
||||||
|
- as a means of
|
||||||
|
- as far as I'm concerned
|
||||||
|
- as of yet
|
||||||
|
- as to
|
||||||
|
- as yet
|
||||||
|
- ascertain
|
||||||
|
- assistance
|
||||||
|
- at the present time
|
||||||
|
- at this time
|
||||||
|
- attain
|
||||||
|
- attributable to
|
||||||
|
- authorize
|
||||||
|
- because of the fact that
|
||||||
|
- belated
|
||||||
|
- benefit from
|
||||||
|
- bestow
|
||||||
|
- by means of
|
||||||
|
- by virtue of
|
||||||
|
- by virtue of the fact that
|
||||||
|
- cease
|
||||||
|
- close proximity
|
||||||
|
- commence
|
||||||
|
- comply with
|
||||||
|
- concerning
|
||||||
|
- consequently
|
||||||
|
- consolidate
|
||||||
|
- constitutes
|
||||||
|
- demonstrate
|
||||||
|
- depart
|
||||||
|
- designate
|
||||||
|
- discontinue
|
||||||
|
- due to the fact that
|
||||||
|
- each and every
|
||||||
|
- economical
|
||||||
|
- eliminate
|
||||||
|
- elucidate
|
||||||
|
- employ
|
||||||
|
- endeavor
|
||||||
|
- enumerate
|
||||||
|
- equitable
|
||||||
|
- equivalent
|
||||||
|
- evaluate
|
||||||
|
- evidenced
|
||||||
|
- exclusively
|
||||||
|
- expedite
|
||||||
|
- expend
|
||||||
|
- expiration
|
||||||
|
- facilitate
|
||||||
|
- factual evidence
|
||||||
|
- feasible
|
||||||
|
- finalize
|
||||||
|
- first and foremost
|
||||||
|
- for all intents and purposes
|
||||||
|
- for the most part
|
||||||
|
- for the purpose of
|
||||||
|
- forfeit
|
||||||
|
- formulate
|
||||||
|
- have a tendency to
|
||||||
|
- honest truth
|
||||||
|
- however
|
||||||
|
- if and when
|
||||||
|
- impacted
|
||||||
|
- implement
|
||||||
|
- in a manner of speaking
|
||||||
|
- in a timely manner
|
||||||
|
- in a very real sense
|
||||||
|
- in accordance with
|
||||||
|
- in addition
|
||||||
|
- in all likelihood
|
||||||
|
- in an effort to
|
||||||
|
- in between
|
||||||
|
- in excess of
|
||||||
|
- in lieu of
|
||||||
|
- in light of the fact that
|
||||||
|
- in many cases
|
||||||
|
- in my opinion
|
||||||
|
- in order to
|
||||||
|
- in regard to
|
||||||
|
- in some instances
|
||||||
|
- in terms of
|
||||||
|
- in the case of
|
||||||
|
- in the event that
|
||||||
|
- in the final analysis
|
||||||
|
- in the nature of
|
||||||
|
- in the near future
|
||||||
|
- in the process of
|
||||||
|
- inception
|
||||||
|
- incumbent upon
|
||||||
|
- indicate
|
||||||
|
- indication
|
||||||
|
- initiate
|
||||||
|
- irregardless
|
||||||
|
- is applicable to
|
||||||
|
- is authorized to
|
||||||
|
- is responsible for
|
||||||
|
- it is
|
||||||
|
- it is essential
|
||||||
|
- it seems that
|
||||||
|
- it was
|
||||||
|
- magnitude
|
||||||
|
- maximum
|
||||||
|
- methodology
|
||||||
|
- minimize
|
||||||
|
- minimum
|
||||||
|
- modify
|
||||||
|
- monitor
|
||||||
|
- multiple
|
||||||
|
- necessitate
|
||||||
|
- nevertheless
|
||||||
|
- not certain
|
||||||
|
- not many
|
||||||
|
- not often
|
||||||
|
- not unless
|
||||||
|
- not unlike
|
||||||
|
- notwithstanding
|
||||||
|
- null and void
|
||||||
|
- numerous
|
||||||
|
- objective
|
||||||
|
- obligate
|
||||||
|
- obtain
|
||||||
|
- on the contrary
|
||||||
|
- on the other hand
|
||||||
|
- one particular
|
||||||
|
- optimum
|
||||||
|
- overall
|
||||||
|
- owing to the fact that
|
||||||
|
- participate
|
||||||
|
- particulars
|
||||||
|
- pass away
|
||||||
|
- pertaining to
|
||||||
|
- point in time
|
||||||
|
- portion
|
||||||
|
- possess
|
||||||
|
- preclude
|
||||||
|
- previously
|
||||||
|
- prior to
|
||||||
|
- prioritize
|
||||||
|
- procure
|
||||||
|
- proficiency
|
||||||
|
- provided that
|
||||||
|
- purchase
|
||||||
|
- put simply
|
||||||
|
- readily apparent
|
||||||
|
- refer back
|
||||||
|
- regarding
|
||||||
|
- relocate
|
||||||
|
- remainder
|
||||||
|
- remuneration
|
||||||
|
- requirement
|
||||||
|
- reside
|
||||||
|
- residence
|
||||||
|
- retain
|
||||||
|
- satisfy
|
||||||
|
- shall
|
||||||
|
- should you wish
|
||||||
|
- similar to
|
||||||
|
- solicit
|
||||||
|
- span across
|
||||||
|
- strategize
|
||||||
|
- subsequent
|
||||||
|
- substantial
|
||||||
|
- successfully complete
|
||||||
|
- sufficient
|
||||||
|
- terminate
|
||||||
|
- the month of
|
||||||
|
- the point I am trying to make
|
||||||
|
- therefore
|
||||||
|
- time period
|
||||||
|
- took advantage of
|
||||||
|
- transmit
|
||||||
|
- transpire
|
||||||
|
- type of
|
||||||
|
- until such time as
|
||||||
|
- utilization
|
||||||
|
- utilize
|
||||||
|
- validate
|
||||||
|
- various different
|
||||||
|
- what I mean to say is
|
||||||
|
- whether or not
|
||||||
|
- with respect to
|
||||||
|
- with the exception of
|
||||||
|
- witnessed
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
extends: existence
|
||||||
|
message: "'%s' is a weasel word!"
|
||||||
|
ignorecase: true
|
||||||
|
level: warning
|
||||||
|
tokens:
|
||||||
|
- clearly
|
||||||
|
- completely
|
||||||
|
- exceedingly
|
||||||
|
- excellent
|
||||||
|
- extremely
|
||||||
|
- fairly
|
||||||
|
- huge
|
||||||
|
- interestingly
|
||||||
|
- is a number
|
||||||
|
- largely
|
||||||
|
- mostly
|
||||||
|
- obviously
|
||||||
|
- quite
|
||||||
|
- relatively
|
||||||
|
- remarkably
|
||||||
|
- several
|
||||||
|
- significantly
|
||||||
|
- substantially
|
||||||
|
- surprisingly
|
||||||
|
- tiny
|
||||||
|
- usually
|
||||||
|
- various
|
||||||
|
- vast
|
||||||
|
- very
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"feed": "https://github.com/errata-ai/write-good/releases.atom",
|
||||||
|
"vale_version": ">=1.0.0"
|
||||||
|
}
|
||||||
@@ -1,5 +1,19 @@
|
|||||||
# AGENTS.md — Project Conventions for GRM
|
# AGENTS.md — Project Conventions for GRM
|
||||||
|
|
||||||
|
## Virtual Environment
|
||||||
|
|
||||||
|
All Python tools, tests, and scripts run inside a standard `.venv` directory.
|
||||||
|
Activate it before running any non-`make` command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source activate.sh # bash/zsh
|
||||||
|
source activate.fish # fish
|
||||||
|
source activate.zsh # zsh
|
||||||
|
```
|
||||||
|
|
||||||
|
If `.venv` doesn't exist, run `make setup` first. The `make` targets handle
|
||||||
|
venv activation automatically — always prefer `make <target>` over raw commands.
|
||||||
|
|
||||||
## Build & Test Commands
|
## Build & Test Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -17,11 +31,10 @@ make workflow-check # workflow-lint + workflow-dryrun
|
|||||||
```
|
```
|
||||||
|
|
||||||
`make setup` automatically installs all development tools:
|
`make setup` automatically installs all development tools:
|
||||||
- **Python deps** via `devx.tools.setup` (pip install -e .[dev], ansible-galaxy, pre-commit hooks)
|
- **Python deps** via `pip install -e .[dev]` (includes devx from Gitea PyPI registry, configured by `make configure-gitea-pypi`)
|
||||||
- **devx package** via `make install-devx` (installs the devx package from git, providing all CI/CD tools)
|
- **Post-install setup** via `devx.tools.setup --skip-install` (ansible-galaxy, pre-commit hooks, tea CLI login)
|
||||||
- **checkmake** via `devx.tools.install_checkmake` (Makefile linter)
|
- **checkmake** via `devx.tools.install_checkmake` (Makefile linter)
|
||||||
- **actionlint, git-cliff, act_runner, tea** via `devx.tools.install_tools` (CI/CD tools to ~/.local/bin)
|
- **actionlint, git-cliff, act_runner, tea** via `devx.tools.install_tools` (CI/CD tools to ~/.local/bin)
|
||||||
- **tea CLI login** via `devx.tools.setup` (configures `tea login` from `.env` `REPO_TOKEN`)
|
|
||||||
|
|
||||||
## Workflow Verification (Before Push)
|
## Workflow Verification (Before Push)
|
||||||
|
|
||||||
@@ -38,13 +51,13 @@ Workflow YAML files (`.gitea/workflows/*.yml`) are verified with two tools:
|
|||||||
|
|
||||||
Both run via `make workflow-check` and are part of `make lint-all`.
|
Both run via `make workflow-check` and are part of `make lint-all`.
|
||||||
The pre-commit hook runs actionlint automatically when workflow files change.
|
The pre-commit hook runs actionlint automatically when workflow files change.
|
||||||
The CI `quality` job runs `make setup` (which installs all tools) then `make lint-all`.
|
The CI `validate` job runs `make setup-image` (which installs all tools) then `make lint-all`.
|
||||||
CI also runs a best-effort `make workflow-dryrun` step (skipped if act_runner is not installed in the CI Docker image).
|
CI also runs a best-effort `make workflow-dryrun` step (skipped if act_runner is not installed in the CI Docker image).
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
- **Python CLI** (`src/gitea_runner_manager/`) — Click-based CLI that delegates to Ansible
|
- **Python CLI** (`src/grm/`) — Click-based CLI that delegates to Ansible
|
||||||
- **Ansible Role** (`ansible/roles/gitea-runner/`) — Idempotent role for rootless Docker runner setup
|
- **Ansible Role** (`ansible/roles/gitea_runner/`) — Idempotent role for rootless Docker runner setup with pasta networking (IPv6 support)
|
||||||
- **devx package** (installed from git) — Reusable CI/CD tools: auto-merge, post-merge, release, publishing, molecule distribution, PR reviews, failure notifications
|
- **devx package** (installed from git) — Reusable CI/CD tools: auto-merge, post-merge, release, publishing, molecule distribution, PR reviews, failure notifications
|
||||||
- **Versioning** (`cliff.toml`) — git-cliff configuration for automated semver versioning from conventional commits
|
- **Versioning** (`cliff.toml`) — git-cliff configuration for automated semver versioning from conventional commits
|
||||||
|
|
||||||
@@ -56,20 +69,26 @@ Every change to master goes through this workflow. No exceptions.
|
|||||||
|
|
||||||
Branch protection and labels are automatically configured by
|
Branch protection and labels are automatically configured by
|
||||||
`devx.tools.configure_repo` (run as `python -m devx.tools.configure_repo`),
|
`devx.tools.configure_repo` (run as `python -m devx.tools.configure_repo`),
|
||||||
which runs as a `configure-repo` job in
|
which runs as a step in the `detect-and-configure` job in
|
||||||
the post-merge workflow on every push to master.
|
the post-merge workflow on every push to master.
|
||||||
|
|
||||||
The following rules are enforced for `master`:
|
The following rules are enforced for `master`:
|
||||||
- **Require pull request**: No direct pushes to master
|
- **Require pull request**: No direct pushes to master
|
||||||
- **Require approval review**: At least 1 `APPROVE` review before merge
|
- **Require approval review**: At least 1 `APPROVE` review before merge
|
||||||
- **Require status checks**: CI quality + molecule tests must pass
|
- **Require status checks**: CI validate + molecule tests must pass
|
||||||
- **Block force pushes**: No history rewriting on master
|
- **Block force pushes**: No history rewriting on master
|
||||||
|
|
||||||
The auto-merge workflow enforces the APPROVE review check programmatically
|
The auto-merge workflow enforces the APPROVE review check programmatically
|
||||||
as a defense-in-depth measure, but branch protection is the primary gate.
|
as a defense-in-depth measure, but branch protection is the primary gate.
|
||||||
|
|
||||||
### 1. Create Vikunja Task
|
### 1. Create Vikunja Task
|
||||||
Create a task in Vikunja project 6 to get a `GRM-N` identifier.
|
Create a task in Vikunja project 6 via `make create-task -- --title "Task title" --description "<h2>...</h2>"` (requires `VIKUNJA_TOKEN` in `.env`). This prints the `GRM-N` identifier and next-step instructions.
|
||||||
|
|
||||||
|
**IMPORTANT:** The task title must NOT include the `GRM-N:` prefix.
|
||||||
|
The `make create-pr` and `check_auto_merge_ready` commands automatically
|
||||||
|
prepend `GRM-N: ` to the Vikunja task title when forming the PR title.
|
||||||
|
If the Vikunja task title already includes the prefix, the PR title will
|
||||||
|
have a double prefix and auto-merge validation will fail.
|
||||||
|
|
||||||
### 2. Create Branch
|
### 2. Create Branch
|
||||||
```bash
|
```bash
|
||||||
@@ -84,27 +103,29 @@ git checkout -b GRM-N-short-description
|
|||||||
|
|
||||||
### 4. Commit (Conventional Commits)
|
### 4. Commit (Conventional Commits)
|
||||||
Branch commits use conventional commit format (no `GRM-N:` prefix):
|
Branch commits use conventional commit format (no `GRM-N:` prefix):
|
||||||
```
|
```text
|
||||||
feat: add new feature
|
feat: add new feature
|
||||||
fix: resolve bug
|
fix: resolve bug
|
||||||
docs: update README
|
docs: update README
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5. Push and Create PR
|
### 5. Push and Create PR
|
||||||
- **PR title format**: `GRM-N: <vikunja task title>` (must match the Vikunja task title exactly)
|
- Push: `git push -u origin HEAD` (pre-push hook validates Vikunja task existence via `devx.tools.pre_push_check`)
|
||||||
|
- Create PR: `make create-pr` (creates a PR with title `GRM-N: <vikunja task title>`, auto-derived from the branch name and Vikunja task)
|
||||||
|
- Or both in one step: `make push-with-pr`
|
||||||
- PR body: summary of changes, `Closes GRM-N`
|
- PR body: summary of changes, `Closes GRM-N`
|
||||||
- Add `ready-to-merge` label **only after review is complete**
|
- Add `ready-to-merge` label **only after review is complete**
|
||||||
|
|
||||||
### 6. Review the PR (Mandatory — Before Adding ready-to-merge Label)
|
### 6. Review the PR (Mandatory — Before Adding ready-to-merge Label)
|
||||||
|
|
||||||
**Review checklist:** Every PR is reviewed against
|
**Review checklist:** Every PR is reviewed against 13 categories covering
|
||||||
[REVIEW_CHECKLIST.md](REVIEW_CHECKLIST.md) — 13 categories covering
|
|
||||||
architecture, code quality, security, i18n, testing, performance,
|
architecture, code quality, security, i18n, testing, performance,
|
||||||
UX, documentation, workflow compliance, maintainability, resource
|
UX, documentation, workflow compliance, maintainability, resource
|
||||||
management, backwards compatibility, and logging.
|
management, backwards compatibility, and logging.
|
||||||
|
|
||||||
**Automated review (CI `pr-review` job):** Every PR triggers an automated
|
**Automated review (CI `validate` job):** Every PR triggers an automated
|
||||||
review via `devx.ci.pr_review` (run as `python -m devx.ci.pr_review`). This job posts a review with
|
review via `python -m devx.ci.pr_review` as a step in the `validate` job.
|
||||||
|
This posts a review with
|
||||||
`COMMENT` (no issues) or `REQUEST_CHANGES` (issues found) based on
|
`COMMENT` (no issues) or `REQUEST_CHANGES` (issues found) based on
|
||||||
the **[auto]** items in the checklist:
|
the **[auto]** items in the checklist:
|
||||||
|
|
||||||
@@ -119,20 +140,19 @@ the **[auto]** items in the checklist:
|
|||||||
- Commit conventions (conventional commit format on PR commits)
|
- Commit conventions (conventional commit format on PR commits)
|
||||||
|
|
||||||
The automated review posts inline comments on specific lines and
|
The automated review posts inline comments on specific lines and
|
||||||
includes a link to the full checklist. The agent **must** address all
|
includes a summary of the checklist categories. The agent **must** address all
|
||||||
`REQUEST_CHANGES` issues before proceeding.
|
`REQUEST_CHANGES` issues before proceeding.
|
||||||
|
|
||||||
**Manual review (agent):** After the automated review passes, the agent
|
**Manual review (agent):** After the automated review passes, the agent
|
||||||
must go through **every category** in `REVIEW_CHECKLIST.md` and verify
|
must go through **every category** listed above and verify
|
||||||
the **[manual]** items by reviewing the full diff
|
the **[manual]** items by reviewing the full diff
|
||||||
(`git diff master...HEAD`).
|
(`git diff master...HEAD`).
|
||||||
|
|
||||||
Post review comments using `devx.ci.pr_review` (run as `python -m devx.ci.pr_review`):
|
Post review comments using `devx.ci.pr_review` (run as `python -m devx.ci.pr_review`):
|
||||||
```bash
|
```bash
|
||||||
REPO_TOKEN=<token> python -m devx.ci.pr_review <pr_number> <owner/repo> \
|
CI_GITEA_TOKEN=<token> python -m devx.ci.pr_review <pr_number> <owner/repo> \
|
||||||
--event REQUEST_CHANGES \
|
--event REQUEST_CHANGES \
|
||||||
--body "Review summary" \
|
--body "Review summary"
|
||||||
--comments-json comments.json
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 7. Address Review Comments
|
### 7. Address Review Comments
|
||||||
@@ -142,10 +162,10 @@ Fix each comment one by one, commit, and push. Re-review until satisfied.
|
|||||||
Once all checklist items are verified and comments are addressed, post
|
Once all checklist items are verified and comments are addressed, post
|
||||||
an approval review with `--checklist-confirmed` and `--checklist-categories`:
|
an approval review with `--checklist-confirmed` and `--checklist-categories`:
|
||||||
```bash
|
```bash
|
||||||
REPO_TOKEN=<token> python -m devx.ci.pr_review <pr_number> <owner/repo> \
|
CI_GITEA_TOKEN=<token> python -m devx.ci.pr_review <pr_number> <owner/repo> \
|
||||||
--event APPROVE --checklist-confirmed \
|
--event APPROVE --checklist-confirmed \
|
||||||
--checklist-categories 1,2,3,4,5,6,7,8,9,10,11,12,13 \
|
--checklist-categories 1,2,3,4,5,6,7,8,9,10,11,12,13 \
|
||||||
--body "All 13 REVIEW_CHECKLIST.md categories verified. Architecture: <summary>. Security: <summary>. Tests: <summary>. Docs: <summary>."
|
--body "All 13 checklist categories verified. Architecture: <summary>. Security: <summary>. Tests: <summary>. Docs: <summary>."
|
||||||
```
|
```
|
||||||
|
|
||||||
The `--checklist-confirmed` flag is **required** for APPROVE events —
|
The `--checklist-confirmed` flag is **required** for APPROVE events —
|
||||||
@@ -153,36 +173,47 @@ it attests that the reviewer has gone through every checklist category.
|
|||||||
The `--checklist-categories` flag is also **required** — it must list at
|
The `--checklist-categories` flag is also **required** — it must list at
|
||||||
least 8 of the 13 category numbers, ensuring the reviewer actually
|
least 8 of the 13 category numbers, ensuring the reviewer actually
|
||||||
checked each category rather than rubber-stamping. The review body must
|
checked each category rather than rubber-stamping. The review body must
|
||||||
be substantive (> 50 characters) — trivial approvals like "LGTM" are
|
be substantive (> 50 characters) — perfunctory approvals like "LGTM" are
|
||||||
rejected.
|
rejected.
|
||||||
|
|
||||||
Then add the `ready-to-merge` label. The auto-merge workflow will:
|
Then add the `ready-to-merge` label. The auto-merge workflow will:
|
||||||
1. **Validate** PR title format (`GRM-N: <vikunja task title>`) and match against Vikunja task title
|
1. **Validate** PR title format (`GRM-N: <vikunja task title>`) and match against Vikunja task title
|
||||||
2. **Check** that at least one substantive APPROVE review exists (body > 20 chars or has inline comments)
|
2. **Check** that at least one substantive APPROVE review exists (body > 20 chars or has inline comments)
|
||||||
3. Wait for all CI checks to pass (including the `pr-review` job)
|
3. Wait for all CI checks to pass (including the `validate` job)
|
||||||
4. Squash-merge with title: `GRM-N <conventional commit message>` (space-separated, no colon after GRM-N)
|
4. Squash-merge with title: `GRM-N: <conventional commit message>`
|
||||||
5. The post-merge workflow marks the Vikunja task as done
|
5. The post-merge workflow marks the Vikunja task as done
|
||||||
6. The release workflow automatically versions, tags, and publishes (see below)
|
6. The release-and-maintain job 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
|
> **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
|
> workflow by adding the `ready-to-merge` label. Manual merges bypass the
|
||||||
> `GRM-N <conventional>` format enforcement, producing incorrectly named commits.
|
> `GRM-N: <conventional>` format enforcement, producing incorrectly named commits.
|
||||||
> The auto-merge script validates the PR title matches the Vikunja task ID
|
> The auto-merge script validates the PR title matches the Vikunja task ID
|
||||||
> and conventional commit format before merging.
|
> and conventional commit format before merging.
|
||||||
|
|
||||||
### CI Path Filtering
|
### CI Path Filtering
|
||||||
|
|
||||||
The CI workflow includes a `detect-changes` job that checks whether any files
|
The CI workflow's `validate` job includes a pre-merge validation step
|
||||||
under `ansible/` or `.ansible-lint` have changed. If no Ansible files are
|
that validates branch format, PR title, and Vikunja task match. This
|
||||||
changed, molecule tests are skipped — this prevents non-Ansible changes
|
fails fast before expensive molecule tests run.
|
||||||
(e.g., Python scripts, workflow YAML, docs) from being blocked by molecule
|
|
||||||
test infrastructure flakiness.
|
The `validate` job also includes a `detect-changes` step that checks
|
||||||
|
whether any files under `ansible/` or `.ansible-lint` have changed. If
|
||||||
|
no Ansible files are changed, molecule tests are skipped — this prevents
|
||||||
|
non-Ansible changes (e.g., Python scripts, workflow YAML, docs) from
|
||||||
|
being blocked by molecule test infrastructure flakiness.
|
||||||
|
|
||||||
### Dynamic Runner Discovery
|
### Dynamic Runner Discovery
|
||||||
|
|
||||||
Molecule tests are distributed across available Gitea Actions runners
|
Molecule tests are distributed across available Gitea Actions runners
|
||||||
dynamically via `devx.molecule.discover_runners`. The `discover-runners`
|
dynamically via `devx.molecule.discover_runners`. The `validate` job
|
||||||
job queries the Gitea API for runners at all levels (repo, org, instance)
|
includes a `discover-runners` step (conditional on ansible-changed) that
|
||||||
|
queries the Gitea API for runners at all levels (repo, org, instance)
|
||||||
and generates a dynamic matrix. If the API can't see instance-level runners
|
and generates a dynamic matrix. If the API can't see instance-level runners
|
||||||
(no admin scope), it falls back to the `MOLECULE_RUNNERS` repo variable,
|
(no admin scope), it falls back to the `MOLECULE_RUNNERS` repo variable,
|
||||||
then to a default of 3.
|
then to a default of 3.
|
||||||
@@ -195,42 +226,27 @@ then to a default of 3.
|
|||||||
### Automated Release Pipeline
|
### Automated Release Pipeline
|
||||||
|
|
||||||
After a PR is merged to master, the **post-merge workflow**
|
After a PR is merged to master, the **post-merge workflow**
|
||||||
(`.gitea/workflows/post-merge.yml`) runs automatically. This single
|
(`.gitea/workflows/post-merge.yml`) runs automatically. Consolidated
|
||||||
workflow consolidates release, wiki sync, badge generation, and
|
into 2 jobs (from 7) to reduce runner overhead:
|
||||||
Vikunja task updates:
|
|
||||||
|
|
||||||
1. **detect-type** — Checks if the commit is a regular merge or a
|
1. **detect-and-configure** — Configures repo (branch protection, labels),
|
||||||
release commit (`release: vX.Y.Z`). All subsequent jobs skip for
|
detects release commit, validates commit message. Outputs `is-release`
|
||||||
release commits (the `[skip ci]` tag also prevents re-triggering).
|
and `is-automated` for the next job.
|
||||||
|
|
||||||
2. **release** — Runs `devx.ci.release` which:
|
2. **release-and-maintain** — Runs all post-merge maintenance as
|
||||||
- **Checks for user-facing changes** via `devx.ci.classify_changes` — if only
|
conditional steps:
|
||||||
workflow/infrastructure files changed (`.gitea/`, `docs/`, `tests/`,
|
- **release** (if not a release commit) — Runs `devx.ci.release` which
|
||||||
`AGENTS.md`, `Makefile`, etc.), the release is **skipped entirely** — no version
|
checks for user-facing changes via `classify_changes` (skips if only
|
||||||
bump, no tag, no publish. This prevents unnecessary releases for CI/docs-only changes.
|
workflow/infrastructure files changed), uses git-cliff for semver,
|
||||||
- Uses **git-cliff** to calculate the next semver version from conventional commits
|
updates `__version__`, updates `CHANGELOG.md`, runs lint+tests, commits
|
||||||
- Updates `__version__` in `src/gitea_runner_manager/__init__.py` (single source of truth)
|
with `release: vX.Y.Z [skip ci]`, creates annotated tag, pushes to master.
|
||||||
- Updates `CHANGELOG.md` with the new version section
|
- **publish** (if release created a tag) — Builds and publishes the
|
||||||
- **Runs `make lint-ruff` and `make pytest-cov`** to verify the release is healthy
|
package to the Gitea PyPI registry. Checks out the release tag
|
||||||
- If lint or tests fail, **aborts immediately** — no commit, no tag
|
within the same job.
|
||||||
- Commits with `release: vX.Y.Z [skip ci]` prefix (the `[skip ci]` prevents
|
- **sync-wiki** (if not automated) — Syncs documentation to the Gitea wiki.
|
||||||
re-triggering post-merge on the release commit)
|
- **vikunja** (if not automated) — Marks the corresponding Vikunja task as done.
|
||||||
- Creates an annotated tag `vX.Y.Z` on the release commit
|
- **badges** (always) — Generates and pushes quality badge SVGs to the
|
||||||
- Pushes both the commit and tag to master
|
`badges` branch. Fetches latest master first to pick up release commits.
|
||||||
- `--skip-tests` flag bypasses test verification (emergency use only, not recommended)
|
|
||||||
- Loops are prevented by `has_unreleased_changes` — after a release commit is tagged, the next run finds no unreleased changes and exits
|
|
||||||
|
|
||||||
3. **sync-wiki** — Syncs documentation to the Gitea wiki.
|
|
||||||
|
|
||||||
4. **badges** — Generates and pushes quality badge SVGs to the `badges` branch.
|
|
||||||
Runs **after** the release job (even if release fails or is skipped) so the
|
|
||||||
version badge always reflects the latest state. The script fetches the
|
|
||||||
latest master before generating badges to pick up any release commits.
|
|
||||||
|
|
||||||
5. **vikunja** — Marks the corresponding Vikunja task as done.
|
|
||||||
|
|
||||||
The tag push triggers the **publish workflow** (`.gitea/workflows/publish.yml`)
|
|
||||||
which builds and publishes the package to PyPI.
|
|
||||||
|
|
||||||
### Smart CI: User-Facing vs Workflow-Only Changes
|
### Smart CI: User-Facing vs Workflow-Only Changes
|
||||||
|
|
||||||
@@ -238,33 +254,33 @@ Not all changes require the full CI pipeline or a new release. The project
|
|||||||
classifies changes into two categories using `devx.ci.classify_changes`:
|
classifies changes into two categories using `devx.ci.classify_changes`:
|
||||||
|
|
||||||
**Classification strategy (safe-by-default):** Any file NOT in the explicit
|
**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
|
types from accidentally skipping releases. Classification is config-driven
|
||||||
via `[tool.devx.classify]` in `pyproject.toml`.
|
via `[tool.devx.classify]` in `pyproject.toml`.
|
||||||
|
|
||||||
**Workflow-only paths** (infrastructure → no release needed):
|
**Infrastructure paths** (no release needed):
|
||||||
- `.gitea/**` — Gitea Actions workflows
|
- `.gitea/**` — Gitea Actions workflows
|
||||||
- `scripts/**` — Dev tools and CI/CD automation (not part of installed package)
|
- `scripts/**` — Dev tools and CI/CD automation (not part of installed package)
|
||||||
- `docs/**` — Documentation
|
- `docs/**` — Documentation
|
||||||
- `tests/**` — Test files
|
- `tests/**` — Test files
|
||||||
- `AGENTS.md`, `README.md`, `CHANGELOG.md`, `TROUBLESHOOTING.md`, `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, `REVIEW_CHECKLIST.md` — Project docs
|
- `AGENTS.md`, `README.md`, `CHANGELOG.md`, `TROUBLESHOOTING.md`, `CONTRIBUTING.md` — Project docs
|
||||||
- `Makefile`, `cliff.toml`, `uv.lock` — Build tooling
|
- `Makefile`, `cliff.toml`, `uv.lock` — Build tooling
|
||||||
- `.pre-commit-config.yaml`, `.ruff.toml`, `.ansible-lint`, `.checkmake.ini`, `.editorconfig` — Lint config
|
- `.pre-commit-config.yaml`, `.ansible-lint`, `.checkmake.ini` — Lint config (ruff config is in `pyproject.toml`)
|
||||||
- `.env.example`, `.gitignore`, `.gitattributes` — Config
|
- `.env.example`, `.gitignore` — Config
|
||||||
- `.devin/**` — Agent/CI tooling config
|
- `.devin/**` — Agent/CI tooling config
|
||||||
- `hooks/**` — Git hooks
|
- `hooks/**` — Git hooks
|
||||||
- `activate.sh`, `activate.fish`, `activate.zsh` — Generated venv scripts
|
- `activate.sh`, `activate.fish`, `activate.zsh` — Generated venv scripts
|
||||||
|
|
||||||
**User-facing paths** (tool changes → release needed) — everything else:
|
**User-facing paths** (tool changes → release needed) — everything else:
|
||||||
- `src/gitea_runner_manager/**` — Python CLI source (except `__init__.py` and `api_clients.py`)
|
- `src/grm/**` — Python CLI source (except `__init__.py`)
|
||||||
- `ansible/**` — Ansible role
|
- `ansible/**` — Ansible role
|
||||||
- `pyproject.toml` — Package metadata
|
- `pyproject.toml` — Package metadata
|
||||||
- Any new file type not in the allowlist
|
- Any new file type not in the allowlist
|
||||||
|
|
||||||
**devx module structure** (installed from git, not in this repo):
|
**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.ci.*` — CI/CD automation (run by workflows): release, publish, auto_merge, classify_changes, detect_release_commit, push_badges, doc_coverage, sync_wiki, distribute_molecule, 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.molecule.*` — Molecule helpers: molecule_all, platforms, discover_runners, distribute_molecule
|
||||||
- `devx.gitea_cli` — Tea CLI wrapper
|
- `devx.gitea_cli` — Tea CLI wrapper
|
||||||
- `devx.i18n` — i18n translation system
|
- `devx.i18n` — i18n translation system
|
||||||
- `devx.config` — Shared configuration (DEVX_* env vars)
|
- `devx.config` — Shared configuration (DEVX_* env vars)
|
||||||
@@ -279,7 +295,7 @@ via `[tool.devx.classify]` in `pyproject.toml`.
|
|||||||
|
|
||||||
**AI agents must follow these rules:**
|
**AI agents must follow these rules:**
|
||||||
- When working on workflow/CI/docs-only changes, use `ci:` or `docs:` commit prefixes
|
- 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
|
- The `classify_changes` module enforces this automatically — no manual intervention needed
|
||||||
|
|
||||||
## Source Code Separation and devx Integration
|
## Source Code Separation and devx Integration
|
||||||
@@ -290,20 +306,20 @@ The codebase enforces strict separation between the GRM tool and the devx packag
|
|||||||
|
|
||||||
| Directory | Purpose | Release impact |
|
| Directory | Purpose | Release impact |
|
||||||
|-----------|---------|----------------|
|
|-----------|---------|----------------|
|
||||||
| `src/gitea_runner_manager/` | User-facing GRM CLI tool | Changes trigger release |
|
| `src/grm/` | User-facing GRM CLI tool | Changes trigger release |
|
||||||
| `devx` package (installed from git) | Reusable CI/CD and dev tools | Not in this repo (no release impact) |
|
| `devx` package (installed from git) | Reusable CI/CD and dev tools | Not in this repo (no release impact) |
|
||||||
| `ansible/` | Ansible role for runner setup | Changes trigger release |
|
| `ansible/` | Ansible role for runner setup | Changes trigger release |
|
||||||
|
|
||||||
### Import Rules
|
### Import Rules
|
||||||
|
|
||||||
1. **`src/gitea_runner_manager/` NEVER imports from devx** — the GRM tool is self-contained
|
1. **`src/grm/` NEVER imports from devx** — the GRM tool is self-contained
|
||||||
2. **devx MAY import from `gitea_runner_manager`** — one-way dependency (devx uses the tool's API clients, config, i18n)
|
2. **devx MAY import from `grm`** — one-way dependency (devx uses the tool's API clients, config, i18n)
|
||||||
3. **Cross-module imports within devx** are allowed (devx modules importing from other devx modules) and must be documented
|
3. **Cross-module imports within devx** are allowed (devx modules importing from other devx modules) and must be documented
|
||||||
4. **`devx.gitea_cli`** is a shared wrapper around the `tea` CLI — devx modules import from it for Gitea API operations (issues, labels, PRs, releases, reviews)
|
4. **`devx.gitea_cli`** is a shared wrapper around the `tea` CLI — devx modules import from it for Gitea API operations (issues, labels, PRs, releases, reviews)
|
||||||
|
|
||||||
### tea CLI Integration
|
### tea CLI Integration
|
||||||
|
|
||||||
The `tea` Gitea CLI tool is used for Gitea API interactions in devx. It is installed by `devx.tools.install_tools` and configured by `devx.tools.setup` (login profile from `.env` `REPO_TOKEN`).
|
The `tea` Gitea CLI tool is used for Gitea API interactions in devx. It is installed by `devx.tools.install_tools` and configured by `devx.tools.setup` (login profile from `.env` `CI_GITEA_TOKEN`).
|
||||||
|
|
||||||
**`devx.gitea_cli`** — Python wrapper around `tea` CLI with JSON output parsing:
|
**`devx.gitea_cli`** — Python wrapper around `tea` CLI with JSON output parsing:
|
||||||
- `TeaCLI.create_issue()` — Create issues with labels
|
- `TeaCLI.create_issue()` — Create issues with labels
|
||||||
@@ -327,12 +343,12 @@ The `tea` Gitea CLI tool is used for Gitea API interactions in devx. It is insta
|
|||||||
|
|
||||||
### PYTHONPATH Configuration
|
### PYTHONPATH Configuration
|
||||||
|
|
||||||
Since devx is installed as a package (via `pip install` from git), it is importable directly. Workflows only need `PYTHONPATH=src` when a devx module imports from `gitea_runner_manager`:
|
Since devx is installed as a package (via `pip install` from git), it is importable directly. Workflows only need `PYTHONPATH=src` when a devx module imports from `grm`:
|
||||||
|
|
||||||
| PYTHONPATH | When to use | Example modules |
|
| PYTHONPATH | When to use | Example modules |
|
||||||
|------------|-------------|-----------------|
|
|------------|-------------|-----------------|
|
||||||
| `src` | Module imports from `gitea_runner_manager` | `devx.ci.auto_merge`, `devx.ci.pr_review`, `devx.ci.pr_review`, `devx.ci.sync_wiki`, `devx.ci.post_merge`, `devx.ci.classify_changes`, `devx.molecule.discover_runners`, `devx.ci.doc_coverage` |
|
| `src` | Module imports from `grm` | `devx.ci.auto_merge`, `devx.ci.pr_review`, `devx.ci.pr_review`, `devx.ci.sync_wiki`, `devx.ci.post_merge`, `devx.ci.classify_changes`, `devx.molecule.discover_runners`, `devx.ci.doc_coverage` |
|
||||||
| (none) | Module has no GRM imports | `devx.ci.detect_release_commit`, `devx.molecule.distribute_molecule`, `devx.molecule.molecule_ci_guard`, `devx.ci.push_badges`, `devx.ci.validate_commit_msg` |
|
| (none) | Module has no GRM imports | `devx.ci.detect_release_commit`, `devx.molecule.distribute_molecule`, `devx.ci.push_badges`, `devx.ci.validate_commit_msg` |
|
||||||
|
|
||||||
**In workflows**, always use `env:` blocks (not inline `PYTHONPATH=value`):
|
**In workflows**, always use `env:` blocks (not inline `PYTHONPATH=value`):
|
||||||
```yaml
|
```yaml
|
||||||
@@ -342,7 +358,7 @@ Since devx is installed as a package (via `pip install` from git), it is importa
|
|||||||
run: python -m devx.ci.example
|
run: python -m devx.ci.example
|
||||||
```
|
```
|
||||||
|
|
||||||
**Locally**, devx is installed as a package, so only `PYTHONPATH=src` is needed if importing from `gitea_runner_manager`.
|
**Locally**, devx is installed as a package, so only `PYTHONPATH=src` is needed if importing from `grm`.
|
||||||
|
|
||||||
### Shared Constants
|
### Shared Constants
|
||||||
|
|
||||||
@@ -351,18 +367,18 @@ platform matrix. Both `devx.molecule.distribute_molecule` (CI) and
|
|||||||
`devx.molecule.molecule_all` (dev tool) import `PLATFORMS` from it — this
|
`devx.molecule.molecule_all` (dev tool) import `PLATFORMS` from it — this
|
||||||
avoids dev tools importing directly from CI modules.
|
avoids dev tools importing directly from CI modules.
|
||||||
|
|
||||||
2. **Publish workflow** (`.gitea/workflows/publish.yml`):
|
2. **Publish step** (in the `release-and-maintain` job, runs after the release step creates a tag):
|
||||||
- Triggers on tag push (`v*`)
|
- Runs after the release step creates a tag
|
||||||
- Validates `PYPI_TOKEN` is set (warns if missing)
|
- Gets the tag from the release step's output
|
||||||
- Builds the Python package
|
- Builds the Python package
|
||||||
- Optionally publishes to PyPI (if `PYPI_TOKEN` is set)
|
- Publishes to the Gitea PyPI registry
|
||||||
- Creates a Gitea release with git-cliff-generated release notes
|
- Creates a Gitea release with git-cliff-generated release notes
|
||||||
- On failure, creates a Gitea issue via `devx.ci.notify_failure`
|
- On failure, creates a Gitea issue via `devx.ci.notify_failure`
|
||||||
|
|
||||||
### git-cliff Commit Preprocessing
|
### git-cliff Commit Preprocessing
|
||||||
|
|
||||||
Merge commits on master have the format `GRM-N <conventional commit>`. The
|
Merge commits on master have the format `GRM-N: <conventional commit>`. The
|
||||||
`GRM-N ` prefix is not a valid conventional commit prefix, so `cliff.toml`
|
`GRM-N: ` prefix is not a valid conventional commit prefix, so `cliff.toml`
|
||||||
includes a `commit_preprocessors` entry that strips it before parsing. This
|
includes a `commit_preprocessors` entry that strips it before parsing. This
|
||||||
ensures all merged work appears in the changelog.
|
ensures all merged work appears in the changelog.
|
||||||
|
|
||||||
@@ -375,7 +391,7 @@ ensures all merged work appears in the changelog.
|
|||||||
| `feat!:` or `BREAKING CHANGE` | minor (pre-1.0: major would be 1.0.0) |
|
| `feat!:` or `BREAKING CHANGE` | minor (pre-1.0: major would be 1.0.0) |
|
||||||
| `chore:`, `ci:`, `docs:` | no bump (excluded by cliff.toml) |
|
| `chore:`, `ci:`, `docs:` | no bump (excluded by cliff.toml) |
|
||||||
|
|
||||||
The version source is `__version__` in `src/gitea_runner_manager/__init__.py`, read by setuptools via `dynamic = ["version"]` in `pyproject.toml`. The release script only updates `__init__.py` — no need to touch `pyproject.toml`. `grm --version` reports this version.
|
The version source is `__version__` in `src/grm/__init__.py`, read by setuptools via `dynamic = ["version"]` in `pyproject.toml`. The release script only updates `__init__.py` — no need to touch `pyproject.toml`. `grm --version` reports this version.
|
||||||
|
|
||||||
### Title Format Summary
|
### Title Format Summary
|
||||||
|
|
||||||
@@ -384,16 +400,16 @@ The version source is `__version__` in `src/gitea_runner_manager/__init__.py`, r
|
|||||||
| Branch name | `GRM-N-short-description` | `GRM-33-add-pr-review-step` |
|
| Branch name | `GRM-N-short-description` | `GRM-33-add-pr-review-step` |
|
||||||
| Branch commits | `<conventional commit>` | `feat: add review script` |
|
| Branch commits | `<conventional commit>` | `feat: add review script` |
|
||||||
| PR title | `GRM-N: <vikunja task title>` | `GRM-33: Add mandatory PR review step` |
|
| PR title | `GRM-N: <vikunja task title>` | `GRM-33: Add mandatory PR review step` |
|
||||||
| Merge commit | `GRM-N <conventional commit>` | `GRM-33 feat: add review script` |
|
| Merge commit | `GRM-N: <conventional commit>` | `GRM-33: feat: add review script` |
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
The devx package is configured via `DEVX_*` environment variables:
|
The devx package is configured via `DEVX_*` environment variables:
|
||||||
- `DEVX_TASK_PREFIX=GRM` — Prefix for Vikunja task identifiers
|
- `DEVX_TASK_PREFIX=GRM` — Prefix for Vikunja task identifiers
|
||||||
- `DEVX_VIKUNJA_PROJECT_ID=6` — Vikunja project ID for task tracking
|
- `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
|
- `DEVX_VERSION_FILE=src/grm/__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
|
## Key Conventions
|
||||||
|
|
||||||
@@ -405,23 +421,75 @@ Change classification is config-driven via `[tool.devx.classify]` in `pyproject.
|
|||||||
- Secrets are passed via temp JSON files, never on the command line (CWE-214)
|
- Secrets are passed via temp JSON files, never on the command line (CWE-214)
|
||||||
- CI triggers only on `opened` and `synchronize` PR events (not `labeled`)
|
- CI triggers only on `opened` and `synchronize` PR events (not `labeled`)
|
||||||
|
|
||||||
|
### Testing Conventions
|
||||||
|
|
||||||
|
- **Always run `make pytest-cov` before pushing** — CI enforces 100%
|
||||||
|
coverage and will fail the PR if any lines are uncovered. The pre-push
|
||||||
|
hook only validates Vikunja task existence, not tests.
|
||||||
|
- **Never use `is True`/`is False` identity checks on API response
|
||||||
|
values** — many APIs return boolean values as strings (`"true"`/
|
||||||
|
`"false"`). Use string comparison or truthy/falsy helpers instead.
|
||||||
|
- **Always mock `time.sleep` and `time.monotonic` in unit tests** — real
|
||||||
|
sleep calls make tests slow and exceed test speed limits. Use
|
||||||
|
`@patch("time.sleep")` and `@patch("time.monotonic")` decorators.
|
||||||
|
- **Extract complex inline shell from workflows to tested Python tools**
|
||||||
|
— SSH loops, curl polling, docker exec chains, and multi-line
|
||||||
|
if/then/else shell blocks should be Python scripts in `scripts/`
|
||||||
|
with unit tests. Simple variable checks and venv activation are fine
|
||||||
|
as inline shell.
|
||||||
|
|
||||||
|
### Container-Level Fix Verification (Mandatory)
|
||||||
|
|
||||||
|
**Rule:** Before pushing any fix that modifies container state (CA certs,
|
||||||
|
config files, installed packages, daemon restarts), reproduce the exact
|
||||||
|
sequence locally with the actual Docker image. Do not push to CI as the
|
||||||
|
first test.
|
||||||
|
|
||||||
|
This is a hard rule, not a suggestion. CI cycles take 20+ minutes and
|
||||||
|
ephemeral staging VMs are destroyed after each run, making interactive
|
||||||
|
debugging impossible. A local reproduction takes 30 seconds and catches
|
||||||
|
silent failures immediately.
|
||||||
|
|
||||||
|
**Procedure:**
|
||||||
|
1. `docker pull <actual_image>`
|
||||||
|
2. `docker run -d --name <test> ...` and wait for it to start
|
||||||
|
3. Run the exact commands from the Ansible task or script
|
||||||
|
4. Verify the state change took effect
|
||||||
|
5. Clean up: `docker rm -f <test>`
|
||||||
|
|
||||||
|
### Verified State Modification (Mandatory)
|
||||||
|
|
||||||
|
Ansible tasks that modify container state with `changed_when: false`
|
||||||
|
MUST include a post-task verification step that confirms the state
|
||||||
|
change took effect. `changed_when: false` suppresses both change
|
||||||
|
detection AND failure visibility — a task can silently do nothing and
|
||||||
|
report `ok`.
|
||||||
|
|
||||||
## Ansible Role Structure
|
## Ansible Role Structure
|
||||||
|
|
||||||
```
|
```text
|
||||||
main.yml → systemd_check → user_setup → rootless_docker → install_runner → prune → integration_test
|
main.yml → systemd_check → user_setup → rootless_docker → install_runner → prune → integration_test
|
||||||
```
|
```
|
||||||
|
|
||||||
- `install_runner.yml` handles: download, config, validate, register, service
|
- `install_runner.yml` handles: download, config, validate, register, service
|
||||||
- `main.yml` handles: prune, integration_test (NOT install_runner — avoids duplicates)
|
- `main.yml` handles: prune, integration_test (NOT install_runner — avoids duplicates)
|
||||||
- `systemctl --user` tasks must be guarded by `docker_rootless_setup`
|
- `systemctl --user` tasks must be guarded by `docker_rootless_setup`
|
||||||
- Template creation tasks are NOT guarded by `docker_rootless_setup` (they just create files)
|
- Template creation tasks are NOT guarded by `docker_rootless_setup` (they only create files)
|
||||||
|
- On Arch Linux, `rootless_docker.yml` fetches the rootless setup scripts
|
||||||
|
(`dockerd-rootless-setuptool.sh`, `dockerd-rootless.sh`) from `moby/moby` `contrib/`
|
||||||
|
at a pinned ref (`gitea_runner_rootless_scripts_ref`) into `/usr/bin` and installs
|
||||||
|
`rootlesskit` — Arch's `docker` package ships neither. These fetch tasks run
|
||||||
|
regardless of `docker_rootless_setup` so CI exercises them on the archlinux platform.
|
||||||
|
See ADR-011 in the decision log.
|
||||||
|
|
||||||
## Molecule Scenarios
|
## 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`
|
4 platforms: `ubuntu-2204`, `ubuntu-2404`, `debian-12`, `archlinux`
|
||||||
Platform list is defined in `devx.molecule.platforms` (single source of truth)
|
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
|
## 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`
|
- `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`
|
||||||
@@ -433,7 +501,7 @@ All documentation lives in `/docs/` and is synced to the Gitea wiki automaticall
|
|||||||
|
|
||||||
### Structure
|
### Structure
|
||||||
|
|
||||||
```
|
```text
|
||||||
docs/
|
docs/
|
||||||
├── index.md # Wiki homepage
|
├── index.md # Wiki homepage
|
||||||
├── mapping.json # File-to-wiki-page title mapping
|
├── mapping.json # File-to-wiki-page title mapping
|
||||||
@@ -462,7 +530,7 @@ docs/
|
|||||||
### Documentation Coverage
|
### Documentation Coverage
|
||||||
|
|
||||||
- `devx.ci.doc_coverage` checks that all CLI commands, Python modules, and CI scripts are documented
|
- `devx.ci.doc_coverage` checks that all CLI commands, Python modules, and CI scripts are documented
|
||||||
- Runs as a CI step in the quality job with `--fail-on-missing` (blocks CI if docs are missing)
|
- Runs as a CI step in the validate job with `--fail-on-missing` (blocks CI if docs are missing)
|
||||||
- Enforced: 100% coverage for public CLI commands and major architectural components
|
- Enforced: 100% coverage for public CLI commands and major architectural components
|
||||||
|
|
||||||
### Updating Documentation
|
### Updating Documentation
|
||||||
@@ -471,3 +539,87 @@ docs/
|
|||||||
2. If adding a new page, add it to `docs/mapping.json`
|
2. If adding a new page, add it to `docs/mapping.json`
|
||||||
3. Commit and create a PR (standard PR workflow)
|
3. Commit and create a PR (standard PR workflow)
|
||||||
4. On merge, wiki is automatically synced
|
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 across all projects):
|
||||||
|
|
||||||
|
| 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 (validate, molecule-tests, release-and-maintain) |
|
||||||
|
| `molecule-runner` | Run 7 molecule scenarios across 4 platforms, report pass/fail |
|
||||||
|
| `dep-upgrader` | Python + Ansible dependency upgrades with molecule verification |
|
||||||
|
| `doc-sync-specialist` | 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 (validate, molecule-tests, release-and-maintain) | `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-sync-specialist` | 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 minor 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 as
|
||||||
|
a step in `detect-and-configure` 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"`.
|
||||||
|
|
||||||
|
|||||||
+259
@@ -2,6 +2,265 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [0.21.1] - 2026-08-24
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Use runuser for systemctl --user tasks in gitea_runner role
|
||||||
|
|
||||||
|
## [0.21.0] - 2026-08-09
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- *(healthcheck)* Add two-tier disk prune with critical threshold
|
||||||
|
|
||||||
|
## [0.20.0] - 2026-08-09
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- *(healthcheck)* Add two-tier disk prune with critical threshold
|
||||||
|
|
||||||
|
## [0.19.0] - 2026-08-08
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Use Gitea mirror for Ansible collection installs
|
||||||
|
|
||||||
|
## [0.18.8] - 2026-08-06
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Pin containerd.io to compatible version for Docker 28.x
|
||||||
|
|
||||||
|
|
||||||
|
## [0.18.7] - 2026-08-06
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Move StartLimit to [Unit] and make prune timer reload conditional
|
||||||
|
|
||||||
|
## [0.18.6] - 2026-08-05
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Pre-configure daemon.json before rootless setuptool + add DBUS_SESSION_BUS_ADDRESS
|
||||||
|
|
||||||
|
## [0.18.5] - 2026-08-05
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Pin Docker 28.x + disable containerd snapshotter + tune prune/disk
|
||||||
|
|
||||||
|
## [0.18.4] - 2026-08-05
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Harden rootless Docker daemon resilience on CI runners
|
||||||
|
|
||||||
|
## [0.18.3] - 2026-08-04
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Switch default network driver to slirp4netns (pasta TCP RST bug)
|
||||||
|
|
||||||
|
## [0.18.2] - 2026-07-16
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Load tun module and pre-configure systemd override for Arch rootless Docker
|
||||||
|
|
||||||
|
## [0.18.1] - 2026-07-16
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fetch rootless Docker scripts on Arch Linux
|
||||||
|
|
||||||
|
## [0.18.0] - 2026-07-12
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- *(runner)* Enable IPv6 in rootless Docker via pasta network driver
|
||||||
|
|
||||||
|
## [0.17.2] - 2026-07-11
|
||||||
|
|
||||||
|
### Refactor
|
||||||
|
|
||||||
|
- Adopt devx v0.40.0
|
||||||
|
|
||||||
|
## [0.17.1] - 2026-07-09
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Disable IPv6 in rootless Docker daemon on runners
|
||||||
|
|
||||||
|
## [0.17.0] - 2026-07-08
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Bump devx to 0.38.0 and migrate to role-based Gitea tokens
|
||||||
|
|
||||||
|
## [0.16.0] - 2026-07-07
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Consolidate docs checks into devx-docs-check target
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Replace --strict with --verify for sync_wiki
|
||||||
|
|
||||||
|
## [0.15.0] - 2026-07-06
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Adopt documentation-as-code enhancements from devx
|
||||||
|
|
||||||
|
## [0.14.4] - 2026-07-06
|
||||||
|
|
||||||
|
### Refactor
|
||||||
|
|
||||||
|
- Remove project-specific references from grm
|
||||||
|
|
||||||
|
## [0.14.3] - 2026-07-06
|
||||||
|
|
||||||
|
### Refactor
|
||||||
|
|
||||||
|
- Rename PyPI package from gitea-runner-manager to grm
|
||||||
|
|
||||||
|
## [0.14.2] - 2026-07-05
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Add pre-commit hooks for quality gates matching CI
|
||||||
|
|
||||||
|
## [0.14.1] - 2026-07-01
|
||||||
|
|
||||||
|
### Refactor
|
||||||
|
|
||||||
|
- Align venv management to devx.mak targets
|
||||||
|
|
||||||
|
## [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
|
||||||
|
|
||||||
|
- Fix wiki link URLs, heading hierarchy, quote pip install vars
|
||||||
|
- Improve runner service stability and deregistration
|
||||||
|
|
||||||
|
## [0.12.2] - 2026-06-28
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Bump devx to 0.26.3 (latest with pinned deps)
|
||||||
|
|
||||||
|
## [0.12.1] - 2026-06-28
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Add approval step to auto-merge workflow using REVIEW_GITEA_TOKEN
|
||||||
|
|
||||||
|
## [0.12.0] - 2026-06-28
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Upgrade all dependencies, add trigger-workflow command
|
||||||
|
|
||||||
|
## [0.11.1] - 2026-06-28
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Makefile HOST/NAME requirement errors, add restart and list targets
|
||||||
|
|
||||||
|
## [0.11.0] - 2026-06-28
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Unified --become-password-file, --verbose, --no-status, labels fix
|
||||||
|
|
||||||
|
## [0.10.3] - 2026-06-27
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Install hadolint on-the-fly in setup-image
|
||||||
|
- Revert EXTRAS=ci default in setup-image
|
||||||
|
- Add EXTRAS=ci to all setup-image calls, workflow-level CI_GITEA_TOKEN
|
||||||
|
|
||||||
|
### Refactor
|
||||||
|
|
||||||
|
- Remove hadolint on-the-fly install workaround
|
||||||
|
- Use devx Makefile aliases, bump devx>=0.23.0
|
||||||
|
|
||||||
|
## [0.10.2] - 2026-06-27
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Setup-image configures Gitea PyPI registry and shows pip errors
|
||||||
|
- Gate auto-merge on release-dry-run and unmask failures
|
||||||
|
- Bump devx>=0.22.0 and remove REPO_TOKEN alias
|
||||||
|
|
||||||
|
### Refactor
|
||||||
|
|
||||||
|
- Rename REPO_TOKEN to CI_GITEA_TOKEN, consolidate env vars
|
||||||
|
|
||||||
|
## [0.10.1] - 2026-06-27
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Set PYTHONPATH=src in publish Install CI tools step
|
||||||
|
|
||||||
|
### Refactor
|
||||||
|
|
||||||
|
- Replace duplicated Makefile targets with devx.mak aliases
|
||||||
|
- Consolidate publish.yml into post-merge.yml
|
||||||
|
|
||||||
|
## [0.10.0] - 2026-06-26
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Adopt devx tools, devx.mak fragment, ci extra, remove legacy install-devx
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Always run publish in post-merge (idempotent)
|
||||||
|
|
||||||
|
## [0.9.0] - 2026-06-24
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Adopt devx v0.11.1 across Makefile and workflows
|
||||||
|
- Add Polish as officially supported language
|
||||||
|
|
||||||
|
## [0.8.1] - 2026-06-24
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Repair publish workflow and add publish step to post-merge
|
||||||
|
- Add build/twine to ci deps, activate venv in notify_failure
|
||||||
|
|
||||||
## [0.8.0] - 2026-06-24
|
## [0.8.0] - 2026-06-24
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
+4
-2
@@ -1,5 +1,7 @@
|
|||||||
# Contributing to GRM
|
# Contributing to GRM
|
||||||
|
|
||||||
|
For the full contributing guide, see the [Contributing wiki page](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki/Contributing).
|
||||||
|
|
||||||
Thank you for contributing to Gitea Runner Manager (GRM)!
|
Thank you for contributing to Gitea Runner Manager (GRM)!
|
||||||
|
|
||||||
## Branch Naming
|
## Branch Naming
|
||||||
@@ -17,7 +19,7 @@ The `GRM-N` prefix is mandatory — CI extracts it for merge messages and Vikunj
|
|||||||
### Feature branches
|
### Feature branches
|
||||||
Use **conventional commits** on feature branches:
|
Use **conventional commits** on feature branches:
|
||||||
|
|
||||||
```
|
```text
|
||||||
feat: add new command
|
feat: add new command
|
||||||
fix: resolve timeout issue
|
fix: resolve timeout issue
|
||||||
chore: update dependencies
|
chore: update dependencies
|
||||||
@@ -31,7 +33,7 @@ Allowed types: `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `perf`, `tes
|
|||||||
### Master branch (squash merges)
|
### Master branch (squash merges)
|
||||||
Squash commits on `master` must follow:
|
Squash commits on `master` must follow:
|
||||||
|
|
||||||
```
|
```text
|
||||||
GRM-N: <conventional commit message>
|
GRM-N: <conventional commit message>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ If you develop a new program, and you want it to be of the greatest possible use
|
|||||||
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found.
|
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
grm
|
grm
|
||||||
Copyright (C) 2026 emil
|
Copyright (C) 2026 oblachno-oss
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
@@ -221,7 +221,7 @@ Also add information on how to contact you by electronic and paper mail.
|
|||||||
|
|
||||||
If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:
|
If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
grm Copyright (C) 2026 emil
|
grm Copyright (C) 2026 oblachno-oss
|
||||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.
|
This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
.PHONY: all setup setup-ci setup-quality setup-molecule setup-release install-devx install update lint ansible-lint makefile-lint lint-all test test-unit pytest-cov molecule molecule-all test-all clean workflow-lint workflow-dryrun workflow-check install-tools
|
.PHONY: all setup setup-ci setup-quality setup-molecule setup-release setup-image install update lint ansible-lint makefile-lint lint-all lint-ruff lint-format lint-bandit lint-deps typecheck checkmake install-hooks test test-unit pytest-cov molecule molecule-all test-all clean workflow-lint workflow-dryrun workflow-check install-tools check-api-identity-checks
|
||||||
|
.PHONY: configure-gitea-pypi
|
||||||
|
.PHONY: create-task create-pr push-with-pr git-push
|
||||||
|
.PHONY: check-docs docs-check
|
||||||
|
|
||||||
PYTHON := python3
|
PYTHON := python3
|
||||||
VENV := .venv
|
VENV := .venv
|
||||||
@@ -7,72 +10,96 @@ CHECKMAKE := $(shell command -v checkmake 2>/dev/null || echo $(HOME)/go/bin/che
|
|||||||
|
|
||||||
all: setup
|
all: setup
|
||||||
|
|
||||||
# Pinned devx version — update this when upgrading devx.
|
# --- devx.mak include (shared Makefile targets) -------------------------------
|
||||||
# All workflow files (.gitea/workflows/*.yml) must be updated to match.
|
# Set DEVX_PYTHON before including devx.mak so it uses the venv Python.
|
||||||
DEVX_VERSION := v0.10.1
|
DEVX_PYTHON := $(BIN)/python
|
||||||
|
DEVX_VENV := $(VENV)
|
||||||
|
DEVX_BIN := $(BIN)
|
||||||
|
DEVX_COV_PKG := src/grm
|
||||||
|
DEVX_TEST_PATHS := tests/ scripts/tests/
|
||||||
|
DEVX_LINT_PATHS := src/ scripts/ tests/
|
||||||
|
|
||||||
install-devx: $(VENV)/bin/activate
|
# Include shared targets from devx package (create-task, create-pr, push-with-pr,
|
||||||
@# REPO_TOKEN may come from .env (local) or environment (CI secrets)
|
# check-config, workflow-lint, lint-ruff, clean, venv, .env, activate-scripts,
|
||||||
@if [ -z "$$REPO_TOKEN" ]; then . ./.env 2>/dev/null; fi; \
|
# install-hooks, install-tools, configure-gitea-pypi, checkmake, etc.)
|
||||||
if [ -z "$$REPO_TOKEN" ]; then echo "REPO_TOKEN not set (check .env or environment)"; exit 1; fi; \
|
# Silent if devx not installed yet — run 'make setup' first.
|
||||||
$(BIN)/pip install "devx==$(shell echo $(DEVX_VERSION) | sed 's/^v//')" \
|
DEVX_MAK := $(shell $(BIN)/python -c \
|
||||||
--extra-index-url "https://emil:$$REPO_TOKEN@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/"
|
"from pathlib import Path; import devx; print(Path(devx.__file__).parent / 'make' / 'devx.mak')" \
|
||||||
|
2>/dev/null)
|
||||||
|
# Fallback: when the venv doesn't exist yet, try system python3 or /opt/venv.
|
||||||
|
# In CI, /opt/venv has devx pre-installed; locally, devx may be in system python.
|
||||||
|
ifeq ($(strip $(DEVX_MAK)),)
|
||||||
|
DEVX_MAK := $(shell python3 -c \
|
||||||
|
"from pathlib import Path; import devx; print(Path(devx.__file__).parent / 'make' / 'devx.mak')" \
|
||||||
|
2>/dev/null)
|
||||||
|
endif
|
||||||
|
ifeq ($(strip $(DEVX_MAK)),)
|
||||||
|
DEVX_MAK := $(shell /opt/venv/bin/python -c \
|
||||||
|
"from pathlib import Path; import devx; print(Path(devx.__file__).parent / 'make' / 'devx.mak')" \
|
||||||
|
2>/dev/null)
|
||||||
|
endif
|
||||||
|
-include $(DEVX_MAK)
|
||||||
|
|
||||||
# Full setup for local development (all deps, tools, collections, hooks)
|
# Full setup for local development (all deps, tools, collections, hooks)
|
||||||
# install-devx must run before install-tools (which uses devx modules)
|
# devx is installed via pip install -e .[dev] (devx is in dev extra)
|
||||||
setup: $(VENV)/bin/activate .env activate-scripts install-devx checkmake install-tools
|
setup: $(VENV)/bin/activate .env activate-scripts configure-gitea-pypi
|
||||||
|
@$(PIP_INSTALL) install -e '.[dev]'
|
||||||
|
@$(BIN)/python -m devx.tools.install_checkmake
|
||||||
|
@$(BIN)/python -m devx.tools.install_tools
|
||||||
@export PATH="$(HOME)/.local/bin:$$PATH"; \
|
@export PATH="$(HOME)/.local/bin:$$PATH"; \
|
||||||
$(BIN)/python -m devx.tools.setup --bin "$(BIN)"
|
$(BIN)/python -m devx.tools.setup --bin "$(BIN)" --skip-install
|
||||||
|
|
||||||
# Lean setup for CI jobs that need pytest + lint tools + runtime deps
|
# Lean setup for CI jobs that need pytest + lint tools + runtime deps
|
||||||
# (detect-changes, discover-runners, pr-review, sync-wiki, badges)
|
# (validate job steps: detect-changes, discover-runners, pr-review;
|
||||||
# badges job runs generate_badges.py which needs ruff, pyright, bandit
|
# release-and-maintain job steps: sync-wiki, badges)
|
||||||
setup-ci: $(VENV)/bin/activate .env install-devx
|
# badges step runs generate_badges.py which needs ruff, pyright, bandit
|
||||||
@$(BIN)/python -m devx.tools.setup --bin "$(BIN)" --extras "ci,lint" --no-ansible-collections --no-pre-commit --no-tea-login
|
setup-ci: $(VENV)/bin/activate .env configure-gitea-pypi
|
||||||
|
@$(PIP_INSTALL) install -e '.[ci,lint]'
|
||||||
|
@$(BIN)/python -m devx.tools.setup --bin "$(BIN)" --skip-install --no-ansible-collections --no-pre-commit --no-tea-login
|
||||||
|
|
||||||
# Setup for the quality job (lint + test deps, actionlint tool)
|
# Setup for the validate CI job (lint + test deps, actionlint tool)
|
||||||
# install-devx must run before install-tools (which uses devx modules)
|
setup-quality: $(VENV)/bin/activate .env configure-gitea-pypi
|
||||||
setup-quality: $(VENV)/bin/activate .env install-devx install-tools
|
@$(PIP_INSTALL) install -e '.[ci,lint]'
|
||||||
|
@$(BIN)/python -m devx.tools.install_tools
|
||||||
@export PATH="$(HOME)/.local/bin:$$PATH"; \
|
@export PATH="$(HOME)/.local/bin:$$PATH"; \
|
||||||
$(BIN)/python -m devx.tools.setup --bin "$(BIN)" --extras "ci,lint" --no-ansible-collections --no-pre-commit --no-tea-login
|
$(BIN)/python -m devx.tools.setup --bin "$(BIN)" --skip-install --no-ansible-collections --no-pre-commit --no-tea-login
|
||||||
|
|
||||||
# Full setup for molecule testing (needs ansible, molecule, collections)
|
# Full setup for molecule testing (needs ansible, molecule, collections)
|
||||||
setup-molecule: $(VENV)/bin/activate .env install-devx install-tools
|
setup-molecule: $(VENV)/bin/activate .env configure-gitea-pypi
|
||||||
|
@$(PIP_INSTALL) install -e '.[ci,molecule]'
|
||||||
|
@$(BIN)/python -m devx.tools.install_tools
|
||||||
@export PATH="$(HOME)/.local/bin:$$PATH"; \
|
@export PATH="$(HOME)/.local/bin:$$PATH"; \
|
||||||
$(BIN)/python -m devx.tools.setup --bin "$(BIN)" --extras "ci,molecule" --no-pre-commit --no-tea-login
|
$(BIN)/python -m devx.tools.setup --bin "$(BIN)" --skip-install --no-pre-commit --no-tea-login
|
||||||
|
|
||||||
# Setup for release jobs (needs git-cliff, tea, and lint tools for release.py)
|
# Setup for release jobs (needs git-cliff, tea, and lint tools for release.py)
|
||||||
setup-release: $(VENV)/bin/activate .env install-devx
|
setup-release: $(VENV)/bin/activate .env configure-gitea-pypi
|
||||||
|
@$(PIP_INSTALL) install -e '.[ci,lint]'
|
||||||
@$(BIN)/python -m devx.tools.install_tools --tool git-cliff --tool tea
|
@$(BIN)/python -m devx.tools.install_tools --tool git-cliff --tool tea
|
||||||
@export PATH="$(HOME)/.local/bin:$$PATH"; \
|
@export PATH="$(HOME)/.local/bin:$$PATH"; \
|
||||||
$(BIN)/python -m devx.tools.setup --bin "$(BIN)" --extras "ci,lint" --no-ansible-collections --no-pre-commit
|
$(BIN)/python -m devx.tools.setup --bin "$(BIN)" --skip-install --no-ansible-collections --no-pre-commit
|
||||||
|
|
||||||
.env:
|
# Setup for pre-built image jobs (deps already in image, just link venv + install project)
|
||||||
@if [ ! -f .env ]; then \
|
# Usage: make setup-image (runtime deps only, devx from image)
|
||||||
cp .env.example .env; \
|
# make setup-image EXTRAS=lint (runtime + lint deps, e.g. ansible-lint)
|
||||||
echo "Created .env from .env.example — please edit it with your credentials."; \
|
# make setup-image EXTRAS=ci,lint (runtime + ci + lint deps, upgrades devx)
|
||||||
fi
|
# NOTE: Cannot alias to devx-setup-image because the venv must exist before
|
||||||
|
# devx.mak can be included (chicken-and-egg). This standalone target creates
|
||||||
|
# the venv symlink first, then installs the project.
|
||||||
|
setup-image:
|
||||||
|
@if [ -d /opt/venv ]; then ln -sf /opt/venv .venv; . .venv/bin/activate; \
|
||||||
|
_TOKEN="$$CI_GITEA_API_TOKEN"; [ -z "$$_TOKEN" ] && _TOKEN="$$DEVELOPER_GITEA_API_TOKEN"; [ -z "$$_TOKEN" ] && _TOKEN="$$CI_GITEA_TOKEN"; \
|
||||||
|
if [ -n "$$_TOKEN" ]; then export PIP_EXTRA_INDEX_URL="https://$$CI_GITEA_USERNAME:$${_TOKEN}@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/"; git config --global url."https://$$CI_GITEA_USERNAME:$${_TOKEN}@git.oblachno.oblachno.fyi/".insteadOf "https://git.oblachno.oblachno.fyi/"; fi; \
|
||||||
|
pip install -e .$(if $(EXTRAS),[$(EXTRAS)],); \
|
||||||
|
else echo "[setup-image] /opt/venv not found — falling back to setup-ci"; $(MAKE) setup-ci; fi
|
||||||
|
|
||||||
$(VENV)/bin/activate:
|
# venv, .env, activate-scripts, and PIP_INSTALL are provided by devx.mak
|
||||||
@python3 -c "import sys; v=sys.version_info; assert v >= (3, 12), f'Python 3.12+ required, found {v.major}.{v.minor}'; print(f'Python {v.major}.{v.minor}.{v.micro} OK')"
|
# (devx-venv, devx-env, devx-activate-scripts, DEVX_PIP_INSTALL)
|
||||||
$(PYTHON) -m venv $(VENV)
|
# Aliases for convenience and backward compatibility:
|
||||||
$(BIN)/pip install --upgrade pip setuptools wheel
|
.PHONY: venv activate-scripts
|
||||||
|
PIP_INSTALL := $(DEVX_PIP_INSTALL)
|
||||||
activate-scripts: $(VENV)/bin/activate
|
venv: devx-venv
|
||||||
@test -f activate.sh || (echo '#!/usr/bin/env bash' > activate.sh && echo 'source "$$(cd "$$(dirname "$${BASH_SOURCE[0]}")" && pwd)/.venv/bin/activate"' >> activate.sh && chmod +x activate.sh)
|
.env: devx-env
|
||||||
@test -f activate.fish || (echo '#!/usr/bin/env fish' > activate.fish && echo 'set -l script_dir (dirname (status --current-filename))' >> activate.fish && echo 'source "$$script_dir/.venv/bin/activate.fish"' >> activate.fish && chmod +x activate.fish)
|
activate-scripts: devx-activate-scripts
|
||||||
@test -f activate.zsh || (echo '#!/usr/bin/env zsh' > activate.zsh && echo '0="$${ZERO:-$${0:#$$ZSH_ARGZERO}}"' >> activate.zsh && echo '0="$${$${(M)0:#/*}:-$$PWD/$$0}"' >> activate.zsh && echo 'source "$${0:A:h}/.venv/bin/activate"' >> activate.zsh && chmod +x activate.zsh)
|
|
||||||
|
|
||||||
install-hooks:
|
|
||||||
@cp hooks/pre-commit .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit
|
|
||||||
@cp hooks/pre-push .git/hooks/pre-push && chmod +x .git/hooks/pre-push
|
|
||||||
@echo "Git hooks installed."
|
|
||||||
|
|
||||||
checkmake: install-devx
|
|
||||||
@$(BIN)/python -m devx.tools.install_checkmake
|
|
||||||
|
|
||||||
install-tools: install-devx
|
|
||||||
@$(BIN)/python -m devx.tools.install_tools
|
|
||||||
|
|
||||||
install:
|
install:
|
||||||
@if [ -z "$(HOST)" ]; then echo "HOST is required. Example: make install HOST=192.168.1.10"; exit 1; fi
|
@if [ -z "$(HOST)" ]; then echo "HOST is required. Example: make install HOST=192.168.1.10"; exit 1; fi
|
||||||
@@ -83,48 +110,60 @@ update:
|
|||||||
$(BIN)/grm update $(HOST) $(if $(USER),--user $(USER),) $(if $(KEY),--key $(KEY),) $(if $(VERSION),--version $(VERSION),) $(if $(ASK_BECOME_PASS),--ask-become-pass,)
|
$(BIN)/grm update $(HOST) $(if $(USER),--user $(USER),) $(if $(KEY),--key $(KEY),) $(if $(VERSION),--version $(VERSION),) $(if $(ASK_BECOME_PASS),--ask-become-pass,)
|
||||||
|
|
||||||
start:
|
start:
|
||||||
@if [ -z "$(HOST)" ]; then echo "HOST is required. Example: make start HOST=192.168.1.10"; exit 1; fi
|
@if [ -z "$(NAME)" ]; then echo "NAME is required. Example: make start NAME=runner1"; exit 1; fi
|
||||||
$(BIN)/grm start $(NAME) $(if $(HOST),--host $(HOST),) $(if $(USER),--user $(USER),) $(if $(ASK_BECOME_PASS),--ask-become-pass,)
|
$(BIN)/grm start $(NAME) $(if $(HOST),--host $(HOST),) $(if $(USER),--user $(USER),) $(if $(ASK_BECOME_PASS),--ask-become-pass,)
|
||||||
|
|
||||||
stop:
|
stop:
|
||||||
@if [ -z "$(HOST)" ]; then echo "HOST is required. Example: make stop HOST=192.168.1.10"; exit 1; fi
|
@if [ -z "$(NAME)" ]; then echo "NAME is required. Example: make stop NAME=runner1"; exit 1; fi
|
||||||
$(BIN)/grm stop $(NAME) $(if $(HOST),--host $(HOST),) $(if $(USER),--user $(USER),) $(if $(ASK_BECOME_PASS),--ask-become-pass,)
|
$(BIN)/grm stop $(NAME) $(if $(HOST),--host $(HOST),) $(if $(USER),--user $(USER),) $(if $(ASK_BECOME_PASS),--ask-become-pass,)
|
||||||
|
|
||||||
|
restart:
|
||||||
|
@if [ -z "$(NAME)" ]; then echo "NAME is required. Example: make restart NAME=runner1"; exit 1; fi
|
||||||
|
$(BIN)/grm restart $(NAME) $(if $(HOST),--host $(HOST),) $(if $(USER),--user $(USER),) $(if $(ASK_BECOME_PASS),--ask-become-pass,)
|
||||||
|
|
||||||
enable:
|
enable:
|
||||||
@if [ -z "$(HOST)" ]; then echo "HOST is required. Example: make enable HOST=192.168.1.10"; exit 1; fi
|
@if [ -z "$(NAME)" ]; then echo "NAME is required. Example: make enable NAME=runner1"; exit 1; fi
|
||||||
$(BIN)/grm enable $(NAME) $(if $(HOST),--host $(HOST),) $(if $(USER),--user $(USER),) $(if $(ASK_BECOME_PASS),--ask-become-pass,)
|
$(BIN)/grm enable $(NAME) $(if $(HOST),--host $(HOST),) $(if $(USER),--user $(USER),) $(if $(ASK_BECOME_PASS),--ask-become-pass,)
|
||||||
|
|
||||||
disable:
|
disable:
|
||||||
@if [ -z "$(HOST)" ]; then echo "HOST is required. Example: make disable HOST=192.168.1.10"; exit 1; fi
|
@if [ -z "$(NAME)" ]; then echo "NAME is required. Example: make disable NAME=runner1"; exit 1; fi
|
||||||
$(BIN)/grm disable $(NAME) $(if $(HOST),--host $(HOST),) $(if $(USER),--user $(USER),) $(if $(TOKEN),--token $(TOKEN),) $(if $(ASK_BECOME_PASS),--ask-become-pass,)
|
$(BIN)/grm disable $(NAME) $(if $(HOST),--host $(HOST),) $(if $(USER),--user $(USER),) $(if $(TOKEN),--token $(TOKEN),) $(if $(ASK_BECOME_PASS),--ask-become-pass,)
|
||||||
|
|
||||||
status:
|
status:
|
||||||
@if [ -z "$(HOST)" ]; then echo "HOST is required. Example: make status HOST=192.168.1.10"; exit 1; fi
|
@if [ -z "$(NAME)" ]; then echo "NAME is required. Example: make status NAME=runner1"; exit 1; fi
|
||||||
$(BIN)/grm status $(NAME) $(if $(HOST),--host $(HOST),) $(if $(USER),--user $(USER),) $(if $(ASK_BECOME_PASS),--ask-become-pass,)
|
$(BIN)/grm status $(NAME) $(if $(HOST),--host $(HOST),) $(if $(USER),--user $(USER),) $(if $(ASK_BECOME_PASS),--ask-become-pass,)
|
||||||
|
|
||||||
remove:
|
remove:
|
||||||
@if [ -z "$(HOST)" ]; then echo "HOST is required. Example: make remove HOST=192.168.1.10"; exit 1; fi
|
@if [ -z "$(NAME)" ]; then echo "NAME is required. Example: make remove NAME=runner1"; exit 1; fi
|
||||||
$(BIN)/grm remove $(NAME) $(if $(HOST),--host $(HOST),) $(if $(USER),--user $(USER),) $(if $(TOKEN),--token $(TOKEN),) $(if $(ASK_BECOME_PASS),--ask-become-pass,)
|
$(BIN)/grm remove $(NAME) $(if $(HOST),--host $(HOST),) $(if $(USER),--user $(USER),) $(if $(TOKEN),--token $(TOKEN),) $(if $(FORCE),--force,) $(if $(ASK_BECOME_PASS),--ask-become-pass,)
|
||||||
|
|
||||||
lint-ruff:
|
list:
|
||||||
$(BIN)/ruff check src/ tests/
|
$(BIN)/grm list $(if $(NO_STATUS),--no-status,) $(if $(ASK_BECOME_PASS),--ask-become-pass,)
|
||||||
|
|
||||||
lint-format:
|
# --- Aliases to devx.mak targets ----------------------------------------------
|
||||||
$(BIN)/ruff format --check src/ tests/
|
lint-ruff: devx-lint-ruff
|
||||||
|
lint-format: devx-lint-format
|
||||||
|
typecheck: devx-typecheck
|
||||||
|
lint-bandit: devx-lint-bandit
|
||||||
|
lint-deps: devx-lint-deps
|
||||||
|
lint: devx-lint
|
||||||
|
checkmake: devx-checkmake
|
||||||
|
install-tools: devx-install-tools
|
||||||
|
install-hooks: devx-install-hooks
|
||||||
|
clean: devx-clean
|
||||||
|
test-unit: devx-test-unit
|
||||||
|
|
||||||
typecheck:
|
# Override devx-pytest-cov to cover both src/ and scripts/
|
||||||
$(BIN)/pyright
|
pytest-cov:
|
||||||
|
@$(BIN)/pytest $(DEVX_TEST_PATHS) -v --cov=src/grm --cov=scripts --cov-report=term-missing --cov-fail-under=100
|
||||||
|
workflow-lint: devx-workflow-lint
|
||||||
|
workflow-dryrun: devx-workflow-dryrun
|
||||||
|
workflow-check: devx-workflow-check
|
||||||
|
|
||||||
lint: lint-ruff lint-format typecheck lint-bandit
|
configure-gitea-pypi:
|
||||||
|
_TOKEN="$$CI_GITEA_API_TOKEN"; [ -z "$$_TOKEN" ] && _TOKEN="$$DEVELOPER_GITEA_API_TOKEN"; [ -z "$$_TOKEN" ] && _TOKEN="$$CI_GITEA_TOKEN"; \
|
||||||
lint-bandit:
|
if [ -z "$$_TOKEN" ]; then echo "[configure-gitea-pypi] Gitea API token not set — skipping (devx must be on public PyPI)"; exit 0; fi; \
|
||||||
$(BIN)/bandit -r src/
|
echo "[configure-gitea-pypi] Gitea PyPI registry configured (token present)."
|
||||||
|
|
||||||
lint-deps:
|
|
||||||
@echo "Checking dependencies for known vulnerabilities..."
|
|
||||||
@.venv/bin/python -m ensurepip 2>/dev/null || true
|
|
||||||
@PIPAPI_PYTHON_LOCATION=$$(pwd)/.venv/bin/python \
|
|
||||||
.venv/bin/pip-audit --desc --skip-editable 2>&1 || true
|
|
||||||
|
|
||||||
ansible-lint:
|
ansible-lint:
|
||||||
PATH="$(PWD)/$(BIN):$$PATH" $(BIN)/ansible-lint ansible/
|
PATH="$(PWD)/$(BIN):$$PATH" $(BIN)/ansible-lint ansible/
|
||||||
@@ -136,34 +175,41 @@ makefile-lint:
|
|||||||
echo "checkmake not found, skipping Makefile lint"; \
|
echo "checkmake not found, skipping Makefile lint"; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
lint-all: lint ansible-lint makefile-lint workflow-lint
|
lint-all: lint ansible-lint makefile-lint workflow-lint check-api-identity-checks check-ansible-no-log check-ansible-no-state-absent-on-db check-ansible-patterns check-jinja-expr check-ansible-set-fact-to-json
|
||||||
|
|
||||||
workflow-lint:
|
check-api-identity-checks:
|
||||||
@command -v actionlint >/dev/null 2>&1 || { \
|
@$(BIN)/python -m devx.tools.check_api_identity_checks
|
||||||
echo "actionlint not found. Install: bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)"; \
|
|
||||||
exit 1; \
|
|
||||||
}
|
|
||||||
actionlint -config-file .gitea/actionlint.yaml .gitea/workflows/*.yml
|
|
||||||
|
|
||||||
workflow-dryrun:
|
check-ansible-no-log:
|
||||||
@command -v act_runner >/dev/null 2>&1 || { echo "act_runner not found. Install: https://gitea.com/gitea/act_runner/releases"; exit 1; }
|
@echo "[check-ansible-no-log] Checking Ansible tasks for missing no_log on secret-handling tasks..."
|
||||||
@echo "Dry-running all workflows (no Docker containers started)..."
|
@$(BIN)/python -m devx.tools.check_ansible_no_log
|
||||||
act_runner exec --dryrun -W .gitea/workflows/ 2>&1 | grep -E 'DRYRUN|ERROR|FAIL|Job'
|
@echo "[check-ansible-no-log] Passed."
|
||||||
|
|
||||||
workflow-check: workflow-lint workflow-dryrun
|
check-ansible-no-state-absent-on-db:
|
||||||
@echo "Workflow checks passed (static lint + dry-run)."
|
@echo "[check-ansible-no-state-absent-on-db] Checking for state: absent on DB data directories..."
|
||||||
|
@$(BIN)/python -m devx.tools.check_ansible_no_state_absent_on_db
|
||||||
|
@echo "[check-ansible-no-state-absent-on-db] Passed."
|
||||||
|
|
||||||
test-unit:
|
check-ansible-patterns:
|
||||||
$(BIN)/pytest tests/unit/ -v --no-cov
|
@echo "[check-ansible-patterns] Checking for dangerous failure-masking patterns..."
|
||||||
|
@$(BIN)/python -m devx.tools.check_ansible_patterns
|
||||||
|
@echo "[check-ansible-patterns] Passed."
|
||||||
|
|
||||||
|
check-jinja-expr:
|
||||||
|
@echo "[check-jinja-expr] Validating Jinja2 expressions in Ansible files..."
|
||||||
|
@$(BIN)/python -m devx.tools.check_jinja_expr
|
||||||
|
@echo "[check-jinja-expr] Passed."
|
||||||
|
|
||||||
|
check-ansible-set-fact-to-json:
|
||||||
|
@echo "[check-ansible-set-fact-to-json] Checking set_fact tasks for to_json misuse..."
|
||||||
|
@$(BIN)/python -m devx.tools.check_ansible_set_fact_to_json
|
||||||
|
@echo "[check-ansible-set-fact-to-json] Passed."
|
||||||
|
|
||||||
test-integration:
|
test-integration:
|
||||||
$(BIN)/pytest tests/integration/ -v --no-cov
|
$(BIN)/pytest tests/integration/ -v --no-cov
|
||||||
|
|
||||||
pytest-cov:
|
|
||||||
$(BIN)/pytest tests/ -v --cov=src/gitea_runner_manager --cov-report=term-missing --cov-fail-under=100
|
|
||||||
|
|
||||||
MOLECULE := $(realpath $(BIN))/molecule
|
MOLECULE := $(realpath $(BIN))/molecule
|
||||||
MOLECULE_BASE := cd $(CURDIR)/ansible/roles/gitea-runner && ANSIBLE_ALLOW_BROKEN_CONDITIONALS=true ANSIBLE_INJECT_INVOCATION=1 $(MOLECULE)
|
MOLECULE_BASE := cd $(CURDIR)/ansible/roles/gitea_runner && ANSIBLE_ALLOW_BROKEN_CONDITIONALS=true ANSIBLE_INJECT_INVOCATION=1 $(MOLECULE)
|
||||||
|
|
||||||
# Quick local test: Ubuntu 22.04 only, all scenarios
|
# Quick local test: Ubuntu 22.04 only, all scenarios
|
||||||
molecule:
|
molecule:
|
||||||
@@ -177,7 +223,13 @@ test: test-all
|
|||||||
|
|
||||||
test-all: pytest-cov molecule
|
test-all: pytest-cov molecule
|
||||||
|
|
||||||
clean:
|
# --- Vikunja task and PR management (via devx.mak fragment) -------------------
|
||||||
find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
|
# Aliases for project-specific target names
|
||||||
find . -type f -name "*.pyc" -delete 2>/dev/null || true
|
create-task: devx-create-task
|
||||||
rm -rf .coverage htmlcov/ .molecule/
|
create-pr: devx-create-pr
|
||||||
|
push-with-pr: devx-push-with-pr
|
||||||
|
git-push: devx-push
|
||||||
|
|
||||||
|
# --- Documentation checks (via devx.mak fragment) -----------------------------
|
||||||
|
check-docs: devx-check-docs
|
||||||
|
docs-check: devx-docs-check
|
||||||
|
|||||||
@@ -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/actions)
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/src/branch/master/LICENSE)
|
[](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/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/wiki)
|
||||||
[](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/releases)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/releases)
|
||||||
[](https://www.python.org/downloads/)
|
[](https://www.python.org/downloads/)
|
||||||
|
|
||||||
## Why GRM?
|
## Why GRM?
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ Key problems GRM solves:
|
|||||||
- **Automatic integration testing** — Every installation runs an integration test that verifies the `.runner` registration file and systemd service state.
|
- **Automatic integration testing** — Every installation runs an integration test that verifies the `.runner` registration file and systemd service state.
|
||||||
- **Docker prune automation** — A systemd user timer automatically prunes old Docker images and volumes on a daily schedule.
|
- **Docker prune automation** — A systemd user timer automatically prunes old Docker images and volumes on a daily schedule.
|
||||||
- **Local runner registry** — Connection details are stored in `~/.local/share/grm/runners.json`, so lifecycle commands work by runner name alone.
|
- **Local runner registry** — Connection details are stored in `~/.local/share/grm/runners.json`, so lifecycle commands work by runner name alone.
|
||||||
- **Internationalisation** — Console messages support English, Bulgarian, German, Russian, and Chinese via the `GRM_LANG` environment variable.
|
- **Internationalisation** — Console messages support English, Bulgarian, German, Russian, Chinese, and Polish via the `GRM_LANG` environment variable.
|
||||||
- **Security-conscious** — Secrets (registration tokens) are passed via temporary JSON files with `0600` permissions, never on the command line (CWE-214).
|
- **Security-conscious** — Secrets (registration tokens) are passed via temporary JSON files with `0600` permissions, never on the command line (CWE-214).
|
||||||
- **Comprehensive CI/CD** — 100% test coverage, automated releases via conventional commits and git-cliff, Molecule tests across 4 OS platforms.
|
- **Comprehensive CI/CD** — 100% test coverage, automated releases via conventional commits and git-cliff, Molecule tests across 4 OS platforms.
|
||||||
|
|
||||||
@@ -92,11 +92,35 @@ source .venv/bin/activate
|
|||||||
|
|
||||||
### Option 2: Via pip (for using GRM without the full repo)
|
### Option 2: Via pip (for using GRM without the full repo)
|
||||||
|
|
||||||
|
GRM is published to the Gitea PyPI registry at
|
||||||
|
`https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple`.
|
||||||
|
The registry is publicly readable — no authentication required to install.
|
||||||
|
|
||||||
|
**Quick install (one-off):**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install gitea-runner-manager
|
pip install grm --index-url https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple
|
||||||
```
|
```
|
||||||
|
|
||||||
This installs the `grm` CLI and its Python dependencies. Note that the Ansible playbooks and role are bundled with the package, so `grm install` will work out of the box. However, for development or access to Make targets, clone the repository.
|
**Persistent configuration (recommended):**
|
||||||
|
|
||||||
|
Add the registry to `~/.pip/pip.conf` so future `pip install` commands find
|
||||||
|
GRM automatically:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[global]
|
||||||
|
extra-index-url = https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple
|
||||||
|
```
|
||||||
|
|
||||||
|
Then install normally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install grm
|
||||||
|
```
|
||||||
|
|
||||||
|
This installs the `grm` CLI and its Python dependencies. The Ansible playbooks
|
||||||
|
and role are bundled with the package, so `grm install` works out of the box.
|
||||||
|
For development or access to Make targets, clone the repository (Option 1).
|
||||||
|
|
||||||
### Post-install configuration
|
### Post-install configuration
|
||||||
|
|
||||||
@@ -119,14 +143,20 @@ GRM provides a single `grm` command with subcommands for the full runner lifecyc
|
|||||||
| `grm update <host>` | Update the Gitea Runner binary on a remote host |
|
| `grm update <host>` | Update the Gitea Runner binary on a remote host |
|
||||||
| `grm start <name>` | Start a registered runner |
|
| `grm start <name>` | Start a registered runner |
|
||||||
| `grm stop <name>` | Stop a registered runner |
|
| `grm stop <name>` | Stop a registered runner |
|
||||||
|
| `grm restart <name>` | Restart a runner (stop, prune Docker images, start) |
|
||||||
| `grm enable <name>` | Enable a runner to start on boot |
|
| `grm enable <name>` | Enable a runner to start on boot |
|
||||||
| `grm disable <name>` | Disable and deregister a runner |
|
| `grm disable <name>` | Disable and deregister a runner |
|
||||||
| `grm status <name>` | Check the status of a registered runner |
|
| `grm status <name>` | Check the status of a registered runner |
|
||||||
| `grm remove <name>` | Remove a runner completely (with remote cleanup) |
|
| `grm remove <name>` | Remove a runner entirely (with remote cleanup) |
|
||||||
|
| `grm remove <name> --force` | Remove only the local registry entry (skip remote cleanup) |
|
||||||
| `grm list` | List all registered runners with live status |
|
| `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 |
|
| `grm --version` | Show the installed version |
|
||||||
|
|
||||||
All lifecycle commands (`start`, `stop`, `enable`, `disable`, `status`, `remove`) work by runner name and pull connection details from the local registry. You can override any stored value with `--host`, `--user`, or `--key`.
|
All lifecycle commands (`start`, `stop`, `restart`, `enable`, `disable`, `status`, `remove`) work by runner name and pull connection details from the local registry. You can override any stored value with `--host`, `--user`, or `--key`.
|
||||||
|
|
||||||
See the [CLI Commands](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki/CLI-Commands.-) wiki page for full argument and option reference.
|
See the [CLI Commands](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki/CLI-Commands.-) wiki page for full argument and option reference.
|
||||||
|
|
||||||
@@ -145,13 +175,78 @@ GRM reads configuration from a `.env` file in the current directory (loaded auto
|
|||||||
|
|
||||||
| Variable | Default | Description |
|
| Variable | Default | Description |
|
||||||
|----------|---------|-------------|
|
|----------|---------|-------------|
|
||||||
| `REPO_TOKEN` | — | Gitea admin API token for optional post-install API verification |
|
| `CI_GITEA_TOKEN` | — | Gitea admin API token for optional post-install API verification |
|
||||||
| `GITEA_INTEGRATION_RETRIES` | `3` | Number of API check retries during integration test |
|
| `GITEA_INTEGRATION_RETRIES` | `3` | Number of API check retries during integration test |
|
||||||
| `GITEA_RUNNER_USER` | current login | Default SSH user (overrides `--user`) |
|
| `GITEA_RUNNER_USER` | current login | Default SSH user (overrides `--user`) |
|
||||||
| `GITEA_RUNNER_KEY` | — | Default SSH key path (overrides `--key`) |
|
| `GITEA_RUNNER_KEY` | — | Default SSH key path (overrides `--key`) |
|
||||||
| `GITEA_RUNNER_LABELS` | — | Default runner labels (overrides `--labels`) |
|
| `GITEA_RUNNER_LABELS` | — | Default runner labels (overrides `--labels`) |
|
||||||
| `GRM_LANG` | `en` | UI language: `en`, `bg`, `de`, `ru`, `zh` |
|
| `GRM_LANG` | `en` | UI language: `en`, `bg`, `de`, `ru`, `zh`, `pl` |
|
||||||
| `GRM_LOG_LEVEL` | `INFO` | Console log level: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` |
|
| `GRM_LOG_LEVEL` | `INFO` | Console log level: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` |
|
||||||
|
| `GRM_BECOME_PASSWORD_FILE` | — | Path to file containing sudo password (see [Sudo Password Handling](#sudo-password-handling)) |
|
||||||
|
| `ANSIBLE_BECOME_PASSWORD_FILE` | — | Fallback sudo password file path (Ansible-native env var) |
|
||||||
|
|
||||||
|
### Sudo Password Handling
|
||||||
|
|
||||||
|
GRM delegates remote operations to Ansible, which uses `sudo` (become) on the target host. There are multiple ways to provide the sudo password, in priority order:
|
||||||
|
|
||||||
|
1. **`--become-password-file <path>`** (CLI flag, global) — Read sudo password from a file. Works for all commands including `grm list`.
|
||||||
|
2. **`GRM_BECOME_PASSWORD_FILE`** (env var) — Same as above, set in `.env` or environment.
|
||||||
|
3. **`ANSIBLE_BECOME_PASSWORD_FILE`** (env var) — Fallback, Ansible-native env var.
|
||||||
|
4. **Interactive prompt** — If none of the above are set, GRM prompts for the sudo password (hidden input).
|
||||||
|
5. **Piped stdin** — When stdin is not a TTY, reads the first line: `echo 'password' | grm list`.
|
||||||
|
6. **`--no-ask-become-pass`** — Skip sudo password entirely (use when the target user has passwordless sudo).
|
||||||
|
|
||||||
|
For `grm list` specifically, the password is collected once and reused for all runner status checks via `--become-password-file`, avoiding stdin consumption issues when checking multiple runners.
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Interactive prompt (default)
|
||||||
|
grm install 192.168.1.10 --user ubuntu
|
||||||
|
|
||||||
|
# Password file (recommended for automation)
|
||||||
|
echo 'my-sudo-pass' > ~/.grm-sudo-pass
|
||||||
|
chmod 600 ~/.grm-sudo-pass
|
||||||
|
grm --become-password-file ~/.grm-sudo-pass install 192.168.1.10 --user ubuntu
|
||||||
|
|
||||||
|
# Env var (set in .env)
|
||||||
|
GRM_BECOME_PASSWORD_FILE=~/.grm-sudo-pass
|
||||||
|
grm list # uses the file automatically
|
||||||
|
|
||||||
|
# Piped stdin (for scripts)
|
||||||
|
echo 'my-sudo-pass' | grm list
|
||||||
|
|
||||||
|
# Passwordless sudo on target
|
||||||
|
grm install 192.168.1.10 --user ubuntu --no-ask-become-pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verbose Output
|
||||||
|
|
||||||
|
Pass `-v` / `--verbose` (global flag, before the subcommand) to enable Ansible verbose mode (`-v`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grm --verbose install 192.168.1.10 --user ubuntu
|
||||||
|
grm -v status prod-runner
|
||||||
|
```
|
||||||
|
|
||||||
|
### Runner Labels
|
||||||
|
|
||||||
|
Runner labels control which jobs a runner accepts. They are set at installation time:
|
||||||
|
|
||||||
|
- **`--labels "docker:docker://alpine:latest"`** — Set specific labels.
|
||||||
|
- **`--labels ""`** — Explicitly set **no labels** (overrides `GITEA_RUNNER_LABELS` env var).
|
||||||
|
- **No `--labels` flag** — Uses `GITEA_RUNNER_LABELS` env var if set, otherwise the Ansible role default.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Custom labels
|
||||||
|
grm install 192.168.1.10 --user ubuntu --labels "docker:docker://alpine:latest,ubuntu-22.04:docker://ubuntu:22.04"
|
||||||
|
|
||||||
|
# Explicitly no labels (overrides GITEA_RUNNER_LABELS env var)
|
||||||
|
grm install 192.168.1.10 --user ubuntu --labels ""
|
||||||
|
|
||||||
|
# Use GITEA_RUNNER_LABELS from .env (or role default if unset)
|
||||||
|
grm install 192.168.1.10 --user ubuntu
|
||||||
|
```
|
||||||
|
|
||||||
### Getting tokens
|
### Getting tokens
|
||||||
|
|
||||||
@@ -171,8 +266,8 @@ One of GRM's core features is the ability to run multiple isolated runners on th
|
|||||||
|
|
||||||
- **Dedicated system user**: `grm-<name>` with its own home directory at `/home/grm-<name>/`
|
- **Dedicated system user**: `grm-<name>` with its own home directory at `/home/grm-<name>/`
|
||||||
- **Rootless Docker daemon**: Isolated Docker socket at `/run/user/<UID>/docker.sock`
|
- **Rootless Docker daemon**: Isolated Docker socket at `/run/user/<UID>/docker.sock`
|
||||||
- **Data directory**: `/var/lib/gitea-runner/<name>/`
|
- **Data directory**: `/var/lib/gitea_runner/<name>/`
|
||||||
- **Config directory**: `/etc/gitea-runner/<name>/`
|
- **Config directory**: `/etc/gitea_runner/<name>/`
|
||||||
- **Systemd user service**: `gitea-runner.service` (independent start/stop/enable)
|
- **Systemd user service**: `gitea-runner.service` (independent start/stop/enable)
|
||||||
- **Docker prune timer**: Per-instance daily cleanup
|
- **Docker prune timer**: Per-instance daily cleanup
|
||||||
|
|
||||||
@@ -244,15 +339,15 @@ See the [Development Setup](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/w
|
|||||||
|
|
||||||
GRM consists of two layers:
|
GRM consists of two layers:
|
||||||
|
|
||||||
1. **Python CLI** (`src/gitea_runner_manager/`) — Built with Click, handles argument parsing, environment loading, i18n translations, and delegates to Ansible via the `ansible-playbook` subprocess. Secrets are passed via temporary JSON files to avoid exposure in the process list.
|
1. **Python CLI** (`src/grm/`) — Built with Click, handles argument parsing, environment loading, i18n translations, and delegates to Ansible via the `ansible-playbook` subprocess. Secrets are passed via temporary JSON files to avoid exposure in the process list.
|
||||||
|
|
||||||
2. **Ansible Role** (`ansible/roles/gitea-runner/`) — Idempotent role that creates a dedicated system user, sets up rootless Docker, installs the runner binary, creates a systemd user service, registers the runner with Gitea, and sets up a Docker prune timer.
|
2. **Ansible Role** (`ansible/roles/gitea_runner/`) — Idempotent role that creates a dedicated system user, sets up rootless Docker, installs the runner binary, creates a systemd user service, registers the runner with Gitea, and sets up a Docker prune timer.
|
||||||
|
|
||||||
```
|
```text
|
||||||
grm install <host>
|
grm install <host>
|
||||||
└── RunnerManager.install()
|
└── RunnerManager.install()
|
||||||
└── ansible-playbook ansible/install-runner.yml
|
└── ansible-playbook ansible/install-runner.yml
|
||||||
└── role: gitea-runner
|
└── role: gitea_runner
|
||||||
├── user_setup.yml (create per-runner system user + lingering)
|
├── user_setup.yml (create per-runner system user + lingering)
|
||||||
├── rootless_docker.yml (rootless Docker setup under runner user)
|
├── rootless_docker.yml (rootless Docker setup under runner user)
|
||||||
├── install_runner.yml (download binary, config, register, service)
|
├── install_runner.yml (download binary, config, register, service)
|
||||||
@@ -268,9 +363,8 @@ grm install <host>
|
|||||||
| `runner_manager.py` | Ansible orchestration + registry integration |
|
| `runner_manager.py` | Ansible orchestration + registry integration |
|
||||||
| `executor.py` | Ansible subprocess execution with log capture |
|
| `executor.py` | Ansible subprocess execution with log capture |
|
||||||
| `registry.py` | Local JSON runner registry at `~/.local/share/grm/runners.json` |
|
| `registry.py` | Local JSON runner registry at `~/.local/share/grm/runners.json` |
|
||||||
| `i18n.py` | Internationalisation (en, bg, de, ru, zh) |
|
| `i18n.py` | Internationalisation (en, bg, de, ru, zh, pl) |
|
||||||
| `exceptions.py` | Custom exceptions (`GRMError`, `AnsibleError`, `APIError`) |
|
| `exceptions.py` | Custom exceptions (`GRMError`, `AnsibleError`) |
|
||||||
| `config.py` | Configuration constants (API URLs, repo owner/name) |
|
|
||||||
| `logging_config.py` | Logging to `~/.local/state/grm/logs/grm.log` |
|
| `logging_config.py` | Logging to `~/.local/state/grm/logs/grm.log` |
|
||||||
| `report.py` | Operation report tracking with step status |
|
| `report.py` | Operation report tracking with step status |
|
||||||
| `ui.py` | Colorised console output via Click |
|
| `ui.py` | Colorised console output via Click |
|
||||||
|
|||||||
+1
-15
@@ -1,17 +1,3 @@
|
|||||||
# Troubleshooting
|
# Troubleshooting
|
||||||
|
|
||||||
| Symptom | Likely Cause | Solution |
|
See the [Troubleshooting guide](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki/Troubleshooting) in the wiki.
|
||||||
|---------|-------------|----------|
|
|
||||||
| Pre-commit rejects commit message | Missing conventional format or GRM-N prefix present | Use `feat: description` format without `GRM-N:` |
|
|
||||||
| `make molecule` fails with `runner_name is undefined` | Verify playbook missing variable | Fixed in Phase 1.1; ensure you're on latest master |
|
|
||||||
| CI molecule job fails | Docker not available on runner host | Ensure Gitea runner host has Docker installed and running |
|
|
||||||
| Auto-merge doesn't trigger | Label not exactly `ready-to-merge` or CI checks not all green | Verify label spelling; check CI status |
|
|
||||||
| Vikunja task not updated after merge | VIKUNJA_TOKEN expired or task ID missing from commit | Regenerate token; verify merge commit has `GRM-N:` prefix |
|
|
||||||
| Post-merge can't find Vikunja task | Task not in project 6 or identifier mismatch | Verify task exists in Vikunja project 6 with correct identifier |
|
|
||||||
| `make pytest-cov` fails | Coverage below 100% | Add tests for new code paths |
|
|
||||||
| `devx.tools.configure_repo` fails | REPO_TOKEN missing or invalid | Set token with repo admin scope and re-run |
|
|
||||||
| `configure_repo` sets wrong status checks | Stale `BRANCH_PROTECTION_CONFIG` | Updated to include `(pull_request)` suffix; re-run `configure_repo` |
|
|
||||||
| Token visible in `ps aux` during install | Old version passed tokens via command line | Fixed: tokens now passed via temp file with `0600` permissions |
|
|
||||||
| `remove-runner.yml` leaves lingering enabled | Old version didn't disable lingering | Fixed: now runs `loginctl disable-linger` and removes subuid/subgid |
|
|
||||||
| apt cache update always reports `changed` | `cache_valid_time: 0` forced update every run | Fixed: changed to `cache_valid_time: 3600` |
|
|
||||||
| Prune/service templates created even when `docker_rootless_setup: false` | Template tasks not guarded | Fixed: template creation now guarded by `docker_rootless_setup` |
|
|
||||||
|
|||||||
@@ -6,28 +6,38 @@
|
|||||||
tasks:
|
tasks:
|
||||||
- name: Include systemd availability check
|
- name: Include systemd availability check
|
||||||
ansible.builtin.include_role:
|
ansible.builtin.include_role:
|
||||||
name: gitea-runner
|
name: gitea_runner
|
||||||
tasks_from: systemd_check.yml
|
tasks_from: systemd_check.yml
|
||||||
|
|
||||||
- name: Stop gitea-runner user service
|
- name: Stop gitea-runner user service
|
||||||
ansible.builtin.command: systemctl --user stop gitea-runner
|
ansible.builtin.command: systemctl --user stop gitea-runner
|
||||||
become: true
|
become: true
|
||||||
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
become_user: "{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
|
||||||
environment:
|
environment:
|
||||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
||||||
when: systemd_available.stat.exists
|
when: systemd_available.stat.exists
|
||||||
changed_when: true
|
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-' ~ gitea_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
|
- name: Include deregistration
|
||||||
ansible.builtin.include_role:
|
ansible.builtin.include_role:
|
||||||
name: gitea-runner
|
name: gitea_runner
|
||||||
tasks_from: deregister.yml
|
tasks_from: deregister.yml
|
||||||
when: not skip_runner_registration | default(false)
|
when: not gitea_runner_skip_registration | default(false)
|
||||||
|
|
||||||
- name: Disable gitea-runner user service
|
- name: Disable gitea-runner user service
|
||||||
ansible.builtin.command: systemctl --user disable gitea-runner
|
ansible.builtin.command: systemctl --user disable gitea-runner
|
||||||
become: true
|
become: true
|
||||||
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
become_user: "{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
|
||||||
environment:
|
environment:
|
||||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
||||||
when: systemd_available.stat.exists
|
when: systemd_available.stat.exists
|
||||||
|
|||||||
@@ -6,13 +6,13 @@
|
|||||||
tasks:
|
tasks:
|
||||||
- name: Include systemd availability check
|
- name: Include systemd availability check
|
||||||
ansible.builtin.include_role:
|
ansible.builtin.include_role:
|
||||||
name: gitea-runner
|
name: gitea_runner
|
||||||
tasks_from: systemd_check.yml
|
tasks_from: systemd_check.yml
|
||||||
|
|
||||||
- name: Enable gitea-runner user service
|
- name: Enable gitea-runner user service
|
||||||
ansible.builtin.command: systemctl --user enable gitea-runner
|
ansible.builtin.command: systemctl --user enable gitea-runner
|
||||||
become: true
|
become: true
|
||||||
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
become_user: "{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
|
||||||
environment:
|
environment:
|
||||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
||||||
when: systemd_available.stat.exists
|
when: systemd_available.stat.exists
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
- name: Start gitea-runner user service
|
- name: Start gitea-runner user service
|
||||||
ansible.builtin.command: systemctl --user start gitea-runner
|
ansible.builtin.command: systemctl --user start gitea-runner
|
||||||
become: true
|
become: true
|
||||||
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
become_user: "{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
|
||||||
environment:
|
environment:
|
||||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
||||||
when: systemd_available.stat.exists
|
when: systemd_available.stat.exists
|
||||||
|
|||||||
@@ -3,4 +3,4 @@
|
|||||||
hosts: all
|
hosts: all
|
||||||
become: true
|
become: true
|
||||||
roles:
|
roles:
|
||||||
- role: gitea-runner
|
- role: gitea_runner
|
||||||
|
|||||||
+57
-29
@@ -6,11 +6,11 @@
|
|||||||
tasks:
|
tasks:
|
||||||
- name: Include systemd availability check
|
- name: Include systemd availability check
|
||||||
ansible.builtin.include_role:
|
ansible.builtin.include_role:
|
||||||
name: gitea-runner
|
name: gitea_runner
|
||||||
tasks_from: systemd_check.yml
|
tasks_from: systemd_check.yml
|
||||||
|
|
||||||
- name: Get runner user UID
|
- name: Get runner user UID
|
||||||
ansible.builtin.command: id -u "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
ansible.builtin.command: id -u "{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
|
||||||
register: runner_uid_result
|
register: runner_uid_result
|
||||||
changed_when: false
|
changed_when: false
|
||||||
failed_when: false
|
failed_when: false
|
||||||
@@ -23,20 +23,20 @@
|
|||||||
- name: Stop gitea-runner user service
|
- name: Stop gitea-runner user service
|
||||||
ansible.builtin.command: systemctl --user stop gitea-runner
|
ansible.builtin.command: systemctl --user stop gitea-runner
|
||||||
become: true
|
become: true
|
||||||
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
become_user: "{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
|
||||||
environment:
|
environment:
|
||||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
||||||
when: systemd_available.stat.exists
|
when: gitea_runner_systemd_available.stat.exists
|
||||||
changed_when: true
|
changed_when: true
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|
||||||
- name: Disable gitea-runner user service
|
- name: Disable gitea-runner user service
|
||||||
ansible.builtin.command: systemctl --user disable gitea-runner
|
ansible.builtin.command: systemctl --user disable gitea-runner
|
||||||
become: true
|
become: true
|
||||||
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
become_user: "{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
|
||||||
environment:
|
environment:
|
||||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
||||||
when: systemd_available.stat.exists
|
when: gitea_runner_systemd_available.stat.exists
|
||||||
changed_when: true
|
changed_when: true
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
args:
|
args:
|
||||||
executable: /bin/bash
|
executable: /bin/bash
|
||||||
become: true
|
become: true
|
||||||
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
become_user: "{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
|
||||||
environment:
|
environment:
|
||||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
||||||
DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid | default('') }}/docker.sock"
|
DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid | default('') }}/docker.sock"
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
- name: Prune all Docker images, volumes, and build cache (rootless)
|
- name: Prune all Docker images, volumes, and build cache (rootless)
|
||||||
ansible.builtin.command: docker system prune -af --volumes
|
ansible.builtin.command: docker system prune -af --volumes
|
||||||
become: true
|
become: true
|
||||||
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
become_user: "{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
|
||||||
environment:
|
environment:
|
||||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
||||||
DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid | default('') }}/docker.sock"
|
DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid | default('') }}/docker.sock"
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
- name: Stop rootless Docker daemon
|
- name: Stop rootless Docker daemon
|
||||||
ansible.builtin.command: systemctl --user stop docker
|
ansible.builtin.command: systemctl --user stop docker
|
||||||
become: true
|
become: true
|
||||||
become_user: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
become_user: "{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
|
||||||
environment:
|
environment:
|
||||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
||||||
changed_when: true
|
changed_when: true
|
||||||
@@ -75,92 +75,120 @@
|
|||||||
|
|
||||||
- name: Include deregistration
|
- name: Include deregistration
|
||||||
ansible.builtin.include_role:
|
ansible.builtin.include_role:
|
||||||
name: gitea-runner
|
name: gitea_runner
|
||||||
tasks_from: deregister.yml
|
tasks_from: deregister.yml
|
||||||
when: not skip_runner_registration | default(false)
|
when: not gitea_runner_skip_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-' ~ gitea_runner_name) }}"
|
||||||
|
environment:
|
||||||
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default('') }}"
|
||||||
|
when: gitea_runner_systemd_available.stat.exists
|
||||||
|
changed_when: true
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
- name: Remove docker-prune user service file
|
- name: Remove docker-prune user service file
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: "{{ gitea_runner_home | default('/home/grm-' ~ runner_name) }}/.config/systemd/user/docker-prune.service"
|
path: "{{ gitea_runner_home | default('/home/grm-' ~ gitea_runner_name) }}/.config/systemd/user/docker-prune.service"
|
||||||
state: absent
|
state: absent
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|
||||||
- name: Remove docker-prune user timer file
|
- name: Remove docker-prune user timer file
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: "{{ gitea_runner_home | default('/home/grm-' ~ runner_name) }}/.config/systemd/user/docker-prune.timer"
|
path: "{{ gitea_runner_home | default('/home/grm-' ~ gitea_runner_name) }}/.config/systemd/user/docker-prune.timer"
|
||||||
|
state: absent
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Remove healthcheck user service file
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ gitea_runner_home | default('/home/grm-' ~ gitea_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-' ~ gitea_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/' ~ gitea_runner_name) }}/healthcheck.sh"
|
||||||
state: absent
|
state: absent
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|
||||||
- name: Remove systemd user unit file
|
- name: Remove systemd user unit file
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: "{{ gitea_runner_home | default('/home/grm-' ~ runner_name) }}/.config/systemd/user/gitea-runner.service"
|
path: "{{ gitea_runner_home | default('/home/grm-' ~ gitea_runner_name) }}/.config/systemd/user/gitea-runner.service"
|
||||||
state: absent
|
state: absent
|
||||||
when: remove_systemd_template | default(true)
|
when: remove_systemd_template | default(true)
|
||||||
|
|
||||||
- name: Kill remaining processes of runner user
|
- name: Kill remaining processes of runner user
|
||||||
ansible.builtin.command: loginctl terminate-user "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
ansible.builtin.command: loginctl terminate-user "{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
|
||||||
failed_when: false
|
failed_when: false
|
||||||
changed_when: true
|
changed_when: true
|
||||||
|
|
||||||
- name: Wait for processes to terminate
|
- name: Wait for processes to terminate
|
||||||
ansible.builtin.command: "pkill -u {{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
ansible.builtin.command: "pkill -u {{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
|
||||||
failed_when: false
|
failed_when: false
|
||||||
changed_when: false
|
changed_when: false
|
||||||
|
|
||||||
- name: Disable lingering for runner user
|
- name: Disable lingering for runner user
|
||||||
ansible.builtin.command: loginctl disable-linger "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
ansible.builtin.command: loginctl disable-linger "{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
|
||||||
failed_when: false
|
failed_when: false
|
||||||
changed_when: true
|
changed_when: true
|
||||||
|
|
||||||
- name: Remove runner user and home directory
|
- name: Remove runner user and home directory
|
||||||
ansible.builtin.user:
|
ansible.builtin.user:
|
||||||
name: "{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}"
|
name: "{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}"
|
||||||
state: absent
|
state: absent
|
||||||
remove: true
|
remove: true
|
||||||
when: remove_runner_user | default(true)
|
when: gitea_runner_remove_user | default(true)
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|
||||||
- name: Remove Docker data root when user is kept
|
- name: Remove Docker data root when user is kept
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: "{{ gitea_runner_home | default('/home/grm-' ~ runner_name) }}/.local/share/docker"
|
path: "{{ gitea_runner_home | default('/home/grm-' ~ gitea_runner_name) }}/.local/share/docker"
|
||||||
state: absent
|
state: absent
|
||||||
when: not (remove_runner_user | default(true))
|
when: not (gitea_runner_remove_user | default(true))
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|
||||||
- name: Remove act cache when user is kept
|
- name: Remove act cache when user is kept
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: "{{ gitea_runner_home | default('/home/grm-' ~ runner_name) }}/.cache/act"
|
path: "{{ gitea_runner_home | default('/home/grm-' ~ gitea_runner_name) }}/.cache/act"
|
||||||
state: absent
|
state: absent
|
||||||
when: not (remove_runner_user | default(true))
|
when: not (gitea_runner_remove_user | default(true))
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|
||||||
- name: Remove systemd user config dir when user is kept
|
- name: Remove systemd user config dir when user is kept
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: "{{ gitea_runner_home | default('/home/grm-' ~ runner_name) }}/.config/systemd/user"
|
path: "{{ gitea_runner_home | default('/home/grm-' ~ gitea_runner_name) }}/.config/systemd/user"
|
||||||
state: absent
|
state: absent
|
||||||
when: not (remove_runner_user | default(true))
|
when: not (gitea_runner_remove_user | default(true))
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|
||||||
- name: Remove subuid entry for runner user
|
- name: Remove subuid entry for runner user
|
||||||
ansible.builtin.lineinfile:
|
ansible.builtin.lineinfile:
|
||||||
path: /etc/subuid
|
path: /etc/subuid
|
||||||
regexp: "^{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}:"
|
regexp: "^{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}:"
|
||||||
state: absent
|
state: absent
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|
||||||
- name: Remove subgid entry for runner user
|
- name: Remove subgid entry for runner user
|
||||||
ansible.builtin.lineinfile:
|
ansible.builtin.lineinfile:
|
||||||
path: /etc/subgid
|
path: /etc/subgid
|
||||||
regexp: "^{{ gitea_runner_service_user | default('grm-' ~ runner_name) }}:"
|
regexp: "^{{ gitea_runner_service_user | default('grm-' ~ gitea_runner_name) }}:"
|
||||||
state: absent
|
state: absent
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|
||||||
- name: Remove runner data directory
|
- name: Remove runner data directory
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: "{{ gitea_runner_data_dir | default('/var/lib/gitea-runner/' ~ runner_name) }}"
|
path: "{{ gitea_runner_data_dir | default('/var/lib/gitea-runner/' ~ gitea_runner_name) }}"
|
||||||
state: absent
|
state: absent
|
||||||
|
|
||||||
- name: Remove runner config directory
|
- name: Remove runner config directory
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: "{{ gitea_runner_config_dir | default('/etc/gitea-runner/' ~ runner_name) }}"
|
path: "{{ gitea_runner_config_dir | default('/etc/gitea-runner/' ~ gitea_runner_name) }}"
|
||||||
state: absent
|
state: absent
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
|
---
|
||||||
collections:
|
collections:
|
||||||
- name: community.general
|
- name: community.general
|
||||||
version: ">=13.0.1"
|
type: url
|
||||||
|
source: https://git.oblachno.oblachno.fyi/api/packages/emil/generic/ansible-collections/13.1.0/community-general-13.1.0.tar.gz
|
||||||
- name: ansible.posix
|
- name: ansible.posix
|
||||||
version: ">=1.5.4"
|
type: url
|
||||||
|
source: https://git.oblachno.oblachno.fyi/api/packages/emil/generic/ansible-collections/2.2.1/ansible-posix-2.2.1.tar.gz
|
||||||
|
- name: community.docker
|
||||||
|
type: url
|
||||||
|
source: https://git.oblachno.oblachno.fyi/api/packages/emil/generic/ansible-collections/5.2.1/community-docker-5.2.1.tar.gz
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
- name: Restart Gitea Actions runner (stop, prune images, start)
|
||||||
|
hosts: all
|
||||||
|
become: true
|
||||||
|
vars:
|
||||||
|
prune_images: true
|
||||||
|
tasks:
|
||||||
|
- name: Include systemd availability check
|
||||||
|
ansible.builtin.include_role:
|
||||||
|
name: gitea_runner
|
||||||
|
tasks_from: systemd_check.yml
|
||||||
|
|
||||||
|
- name: Resolve runner UID
|
||||||
|
ansible.builtin.include_role:
|
||||||
|
name: gitea_runner
|
||||||
|
tasks_from: resolve_uid.yml
|
||||||
|
|
||||||
|
- name: Stop gitea-runner user service
|
||||||
|
ansible.builtin.command: systemctl --user stop gitea-runner
|
||||||
|
become: true
|
||||||
|
become_user: "{{ gitea_runner_service_user }}"
|
||||||
|
environment:
|
||||||
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||||
|
when: systemd_available.stat.exists
|
||||||
|
changed_when: true
|
||||||
|
|
||||||
|
- name: Prune stale runner images from rootless Docker
|
||||||
|
ansible.builtin.command:
|
||||||
|
cmd: python3 {{ playbook_dir }}/../scripts/prune_runner_images.py
|
||||||
|
become: true
|
||||||
|
become_user: "{{ gitea_runner_service_user }}"
|
||||||
|
environment:
|
||||||
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||||
|
DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid }}/docker.sock"
|
||||||
|
when:
|
||||||
|
- systemd_available.stat.exists
|
||||||
|
- prune_images | default(true)
|
||||||
|
changed_when: true
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Start gitea-runner user service
|
||||||
|
ansible.builtin.command: systemctl --user start gitea-runner
|
||||||
|
become: true
|
||||||
|
become_user: "{{ gitea_runner_service_user }}"
|
||||||
|
environment:
|
||||||
|
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
||||||
|
when: systemd_available.stat.exists
|
||||||
|
changed_when: true
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
---
|
|
||||||
gitea_runner_version: "1.0.8"
|
|
||||||
runner_labels: "docker,ubuntu-latest:docker://runner-images:ubuntu-22.04"
|
|
||||||
skip_runner_registration: false
|
|
||||||
|
|
||||||
# Per-runner user (rootless isolation)
|
|
||||||
gitea_runner_user_prefix: "grm-"
|
|
||||||
gitea_runner_base_home: "/home"
|
|
||||||
gitea_runner_service_user: "{{ gitea_runner_user_prefix }}{{ runner_name }}"
|
|
||||||
gitea_runner_home: "{{ gitea_runner_base_home }}/{{ gitea_runner_service_user }}"
|
|
||||||
|
|
||||||
# Base paths (instance-scoped via runner_name)
|
|
||||||
gitea_runner_base_data_dir: "/var/lib/gitea-runner"
|
|
||||||
gitea_runner_base_config_dir: "/etc/gitea-runner"
|
|
||||||
gitea_runner_data_dir: "{{ gitea_runner_base_data_dir }}/{{ runner_name }}"
|
|
||||||
gitea_runner_config_dir: "{{ gitea_runner_base_config_dir }}/{{ runner_name }}"
|
|
||||||
gitea_runner_binary_path: "/usr/local/bin/gitea_runner"
|
|
||||||
|
|
||||||
# Prune configuration
|
|
||||||
gitea_runner_prune_until: "24h"
|
|
||||||
gitea_runner_prune_schedule: "daily"
|
|
||||||
gitea_runner_prune_label: "gitea-runner=true"
|
|
||||||
|
|
||||||
# Service configuration
|
|
||||||
gitea_runner_service_restart_sec: "5"
|
|
||||||
|
|
||||||
# Removal defaults
|
|
||||||
remove_systemd_template: true
|
|
||||||
remove_runner_user: true
|
|
||||||
|
|
||||||
# Runner configuration
|
|
||||||
gitea_runner_log_level: "info"
|
|
||||||
gitea_runner_container_label: "gitea-runner=true"
|
|
||||||
gitea_runner_file: ".runner"
|
|
||||||
|
|
||||||
# Docker installation (for rootless dependencies)
|
|
||||||
docker_gpg_key_path: "/etc/apt/keyrings/docker.gpg"
|
|
||||||
docker_apt_arch: "{{ 'amd64' if ansible_facts['architecture'] == 'x86_64' else ansible_facts['architecture'] }}"
|
|
||||||
docker_apt_source_line: >-
|
|
||||||
deb [arch={{ docker_apt_arch }} signed-by={{ docker_gpg_key_path }}]
|
|
||||||
https://download.docker.com/linux/{{ ansible_facts['distribution'] | lower }}
|
|
||||||
{{ ansible_facts['distribution_release'] }} stable
|
|
||||||
# Set to false in CI/molecule to skip rootless daemon startup (needs kernel userns)
|
|
||||||
docker_rootless_setup: true
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Converge
|
|
||||||
hosts: all
|
|
||||||
become: true
|
|
||||||
vars:
|
|
||||||
gitea_url: "http://localhost:3000"
|
|
||||||
registration_token: "fake-token-for-testing"
|
|
||||||
runner_name: "molecule-test-runner"
|
|
||||||
skip_runner_registration: true
|
|
||||||
docker_rootless_setup: false
|
|
||||||
roles:
|
|
||||||
- role: gitea-runner
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Converge
|
|
||||||
hosts: all
|
|
||||||
become: true
|
|
||||||
vars:
|
|
||||||
gitea_url: "http://localhost:3000"
|
|
||||||
registration_token: "fake-token-for-testing"
|
|
||||||
runner_name: "deregister-test-runner"
|
|
||||||
skip_runner_registration: true
|
|
||||||
docker_rootless_setup: false
|
|
||||||
roles:
|
|
||||||
- role: gitea-runner
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Converge
|
|
||||||
hosts: all
|
|
||||||
become: true
|
|
||||||
vars:
|
|
||||||
gitea_url: "http://localhost:3000"
|
|
||||||
registration_token: "fake-token-for-testing"
|
|
||||||
runner_name: "lifecycle-test-runner"
|
|
||||||
skip_runner_registration: true
|
|
||||||
docker_rootless_setup: false
|
|
||||||
roles:
|
|
||||||
- role: gitea-runner
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Converge
|
|
||||||
hosts: all
|
|
||||||
become: true
|
|
||||||
vars:
|
|
||||||
gitea_url: "http://localhost:3000"
|
|
||||||
registration_token: "fake-token-for-testing"
|
|
||||||
runner_name: "remove-test-runner"
|
|
||||||
skip_runner_registration: true
|
|
||||||
docker_rootless_setup: false
|
|
||||||
roles:
|
|
||||||
- role: gitea-runner
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Converge
|
|
||||||
hosts: all
|
|
||||||
become: true
|
|
||||||
vars:
|
|
||||||
gitea_url: "http://localhost:3000"
|
|
||||||
registration_token: "fake-token-for-testing"
|
|
||||||
runner_name: "template-test-runner"
|
|
||||||
skip_runner_registration: true
|
|
||||||
docker_rootless_setup: false
|
|
||||||
roles:
|
|
||||||
- role: gitea-runner
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Verify
|
|
||||||
hosts: all
|
|
||||||
become: true
|
|
||||||
vars:
|
|
||||||
runner_name: "template-test-runner"
|
|
||||||
pre_tasks:
|
|
||||||
- name: Load role defaults
|
|
||||||
ansible.builtin.include_vars:
|
|
||||||
dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults"
|
|
||||||
tasks:
|
|
||||||
- name: Check systemd user service exists
|
|
||||||
ansible.builtin.stat:
|
|
||||||
path: "{{ gitea_runner_home }}/.config/systemd/user/gitea-runner.service"
|
|
||||||
register: service_stat
|
|
||||||
|
|
||||||
- name: Assert user service exists
|
|
||||||
ansible.builtin.assert:
|
|
||||||
that:
|
|
||||||
- service_stat.stat.exists
|
|
||||||
fail_msg: "Systemd user service is missing"
|
|
||||||
|
|
||||||
- name: Read rendered user service template
|
|
||||||
ansible.builtin.slurp:
|
|
||||||
src: "{{ gitea_runner_home }}/.config/systemd/user/gitea-runner.service"
|
|
||||||
register: service_template
|
|
||||||
|
|
||||||
- name: Assert user service template contains expected directives
|
|
||||||
ansible.builtin.assert:
|
|
||||||
that:
|
|
||||||
- "'Type=simple' in service_template.content | b64decode"
|
|
||||||
- "'ExecStart={{ gitea_runner_binary_path }}' in service_template.content | b64decode"
|
|
||||||
- "'Restart=on-failure' in service_template.content | b64decode"
|
|
||||||
- "'DOCKER_HOST=unix:///run/user' in service_template.content | b64decode"
|
|
||||||
- "'XDG_RUNTIME_DIR=/run/user' in service_template.content | b64decode"
|
|
||||||
fail_msg: "User service template is missing expected directives"
|
|
||||||
|
|
||||||
- name: Read rendered prune service template
|
|
||||||
ansible.builtin.slurp:
|
|
||||||
src: "{{ gitea_runner_home }}/.config/systemd/user/docker-prune.service"
|
|
||||||
register: prune_service
|
|
||||||
|
|
||||||
- name: Assert prune service contains expected directives
|
|
||||||
ansible.builtin.assert:
|
|
||||||
that:
|
|
||||||
- "'Type=oneshot' in prune_service.content | b64decode"
|
|
||||||
- "'docker system prune' in prune_service.content | b64decode"
|
|
||||||
- "'docker volume prune' in prune_service.content | b64decode"
|
|
||||||
fail_msg: "Prune service template is missing expected directives"
|
|
||||||
|
|
||||||
- name: Read rendered prune timer template
|
|
||||||
ansible.builtin.slurp:
|
|
||||||
src: "{{ gitea_runner_home }}/.config/systemd/user/docker-prune.timer"
|
|
||||||
register: prune_timer
|
|
||||||
|
|
||||||
- name: Assert prune timer contains expected directives
|
|
||||||
ansible.builtin.assert:
|
|
||||||
that:
|
|
||||||
- "'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"
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Converge
|
|
||||||
hosts: all
|
|
||||||
become: true
|
|
||||||
vars:
|
|
||||||
gitea_url: "http://localhost:3000"
|
|
||||||
registration_token: "fake-token-for-testing"
|
|
||||||
runner_name: "update-test-runner"
|
|
||||||
skip_runner_registration: true
|
|
||||||
docker_rootless_setup: false
|
|
||||||
roles:
|
|
||||||
- role: gitea-runner
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Check if runner registration file exists
|
|
||||||
ansible.builtin.stat:
|
|
||||||
path: "{{ gitea_runner_data_dir }}/.runner"
|
|
||||||
register: runner_file_stat
|
|
||||||
|
|
||||||
- name: Read runner registration file
|
|
||||||
ansible.builtin.slurp:
|
|
||||||
src: "{{ gitea_runner_data_dir }}/.runner"
|
|
||||||
register: runner_file_content
|
|
||||||
when: runner_file_stat.stat.exists | default(false) | bool
|
|
||||||
|
|
||||||
- name: Parse runner registration data
|
|
||||||
ansible.builtin.set_fact:
|
|
||||||
runner_reg: >
|
|
||||||
{{ (runner_file_content.content | b64decode | from_json)
|
|
||||||
if (runner_file_content is defined and runner_file_content.content is defined)
|
|
||||||
else {} }}
|
|
||||||
when: runner_file_stat.stat.exists | default(false) | bool
|
|
||||||
|
|
||||||
- name: Deregister runner with Gitea via CLI
|
|
||||||
ansible.builtin.command: >
|
|
||||||
{{ gitea_runner_binary_path }} delete
|
|
||||||
--token {{ registration_token }}
|
|
||||||
--name {{ runner_name }}
|
|
||||||
--instance {{ gitea_url }}
|
|
||||||
--no-interactive
|
|
||||||
args:
|
|
||||||
chdir: "{{ gitea_runner_data_dir }}"
|
|
||||||
become: true
|
|
||||||
become_user: "{{ gitea_runner_service_user }}"
|
|
||||||
environment:
|
|
||||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default(0) }}"
|
|
||||||
DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid | default(0) }}/docker.sock"
|
|
||||||
when:
|
|
||||||
- runner_file_stat.stat.exists | default(false) | bool
|
|
||||||
- not skip_runner_registration
|
|
||||||
register: deregister_output
|
|
||||||
changed_when: deregister_output.rc == 0
|
|
||||||
failed_when: false
|
|
||||||
|
|
||||||
- name: Remove runner registration file
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: "{{ gitea_runner_data_dir }}/.runner"
|
|
||||||
state: absent
|
|
||||||
when: runner_file_stat.stat.exists | default(false) | bool
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Check runner registration file exists
|
|
||||||
ansible.builtin.stat:
|
|
||||||
path: "{{ gitea_runner_data_dir }}/.runner"
|
|
||||||
register: runner_file_stat
|
|
||||||
|
|
||||||
- name: Read runner registration file
|
|
||||||
ansible.builtin.slurp:
|
|
||||||
src: "{{ gitea_runner_data_dir }}/.runner"
|
|
||||||
register: runner_file_content
|
|
||||||
when: runner_file_stat.stat.exists | default(false) | bool
|
|
||||||
|
|
||||||
- name: Parse runner registration data
|
|
||||||
ansible.builtin.set_fact:
|
|
||||||
runner_reg: >
|
|
||||||
{{ (runner_file_content.content | b64decode | from_json)
|
|
||||||
if (runner_file_content is defined and runner_file_content.content is defined)
|
|
||||||
else {} }}
|
|
||||||
when: runner_file_stat.stat.exists | default(false) | bool
|
|
||||||
|
|
||||||
- name: Verify runner user service active
|
|
||||||
ansible.builtin.command: systemctl --user is-active gitea-runner
|
|
||||||
become: true
|
|
||||||
become_user: "{{ gitea_runner_service_user }}"
|
|
||||||
environment:
|
|
||||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
|
||||||
register: service_check
|
|
||||||
changed_when: false
|
|
||||||
when:
|
|
||||||
- systemd_available.stat.exists
|
|
||||||
- docker_rootless_setup
|
|
||||||
|
|
||||||
- name: Validate runner installation
|
|
||||||
ansible.builtin.fail:
|
|
||||||
msg: >
|
|
||||||
Runner '{{ runner_name }}' is not properly installed:
|
|
||||||
{% if not (runner_file_stat.stat.exists | default(false)) %}
|
|
||||||
- Registration file (.runner) is missing. Registration may have failed.
|
|
||||||
{% endif %}
|
|
||||||
{% if docker_rootless_setup and not (service_check.stdout | default('') | trim) == 'active' %}
|
|
||||||
- Systemd user service is not active.
|
|
||||||
{% endif %}
|
|
||||||
when: >
|
|
||||||
not (runner_file_stat.stat.exists | default(false))
|
|
||||||
or (docker_rootless_setup and not (service_check.stdout | default('') | trim) == 'active')
|
|
||||||
|
|
||||||
- name: Report runner status
|
|
||||||
ansible.builtin.debug:
|
|
||||||
msg: >
|
|
||||||
Runner '{{ runner_name }}' is installed and running.
|
|
||||||
Registered: {{ runner_file_stat.stat.exists | default(false) }}
|
|
||||||
{% if runner_reg.id is defined %}Runner ID: {{ runner_reg.id }}{% endif %}
|
|
||||||
{% if runner_reg.uuid is defined %}UUID: {{ runner_reg.uuid }}{% endif %}
|
|
||||||
{% if runner_reg.address is defined %}Gitea: {{ runner_reg.address }}{% endif %}
|
|
||||||
Service: {{ service_check.stdout | default('unknown') | trim }}
|
|
||||||
|
|
||||||
- name: Optional Gitea API verification
|
|
||||||
when:
|
|
||||||
- gitea_url is defined
|
|
||||||
- gitea_admin_token is defined
|
|
||||||
- gitea_admin_token | length > 0
|
|
||||||
block:
|
|
||||||
- name: Check admin runners API
|
|
||||||
ansible.builtin.uri:
|
|
||||||
url: "{{ gitea_url }}/api/v1/admin/runners"
|
|
||||||
headers:
|
|
||||||
Authorization: "token {{ gitea_admin_token }}"
|
|
||||||
method: GET
|
|
||||||
status_code: [200, 401, 403, 404]
|
|
||||||
return_content: true
|
|
||||||
body_format: json
|
|
||||||
register: admin_api_response
|
|
||||||
ignore_errors: true
|
|
||||||
|
|
||||||
- name: Check repo runners API
|
|
||||||
ansible.builtin.uri:
|
|
||||||
url: "{{ gitea_url }}/api/v1/repos/{{ gitea_runner_test_repo | default('oblachno-oss/grm') }}/actions/runners"
|
|
||||||
headers:
|
|
||||||
Authorization: "token {{ gitea_admin_token }}"
|
|
||||||
method: GET
|
|
||||||
status_code: [200, 401, 403, 404]
|
|
||||||
return_content: true
|
|
||||||
body_format: json
|
|
||||||
register: repo_api_response
|
|
||||||
ignore_errors: true
|
|
||||||
|
|
||||||
- name: Report API status (informational only)
|
|
||||||
ansible.builtin.debug:
|
|
||||||
msg: >
|
|
||||||
API checks (informational only — not used for pass/fail):
|
|
||||||
Admin API: {{ admin_api_response.status | default('no response') }}.
|
|
||||||
Repo API: {{ repo_api_response.status | default('no response') }}.
|
|
||||||
{% if admin_api_response.json.runners | default([]) | selectattr('name', 'equalto', runner_name) | list | length > 0 %}
|
|
||||||
Runner found in admin API.
|
|
||||||
{% endif %}
|
|
||||||
{% if repo_api_response.json.runners | default([]) | selectattr('name', 'equalto', runner_name) | list | length > 0 %}
|
|
||||||
Runner found in repo API.
|
|
||||||
{% endif %}
|
|
||||||
rescue:
|
|
||||||
- name: API check failed
|
|
||||||
ansible.builtin.debug:
|
|
||||||
msg: "API verification skipped due to connection or permission error."
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Create docker-prune user service file
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: docker-prune.service.j2
|
|
||||||
dest: "{{ gitea_runner_home }}/.config/systemd/user/docker-prune.service"
|
|
||||||
owner: "{{ gitea_runner_service_user }}"
|
|
||||||
group: "{{ gitea_runner_service_user }}"
|
|
||||||
mode: "0644"
|
|
||||||
|
|
||||||
- name: Create docker-prune user timer file
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: docker-prune.timer.j2
|
|
||||||
dest: "{{ gitea_runner_home }}/.config/systemd/user/docker-prune.timer"
|
|
||||||
owner: "{{ gitea_runner_service_user }}"
|
|
||||||
group: "{{ gitea_runner_service_user }}"
|
|
||||||
mode: "0644"
|
|
||||||
|
|
||||||
- name: Reload systemd user daemon for prune 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 docker-prune user timer
|
|
||||||
ansible.builtin.command: systemctl --user enable --now docker-prune.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
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Ensure work directory exists
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: "{{ gitea_runner_data_dir }}"
|
|
||||||
state: directory
|
|
||||||
owner: "{{ gitea_runner_service_user }}"
|
|
||||||
group: "{{ gitea_runner_service_user }}"
|
|
||||||
mode: "0755"
|
|
||||||
|
|
||||||
- name: Check if runner is already registered
|
|
||||||
ansible.builtin.stat:
|
|
||||||
path: "{{ gitea_runner_data_dir }}/.runner"
|
|
||||||
register: runner_registered
|
|
||||||
|
|
||||||
- name: Register runner with Gitea
|
|
||||||
ansible.builtin.command: >
|
|
||||||
{{ gitea_runner_binary_path }} register
|
|
||||||
--token {{ registration_token }}
|
|
||||||
--name {{ runner_name }}
|
|
||||||
--instance {{ gitea_url }}
|
|
||||||
--labels {{ runner_labels }}
|
|
||||||
--no-interactive
|
|
||||||
args:
|
|
||||||
chdir: "{{ gitea_runner_data_dir }}"
|
|
||||||
become: true
|
|
||||||
become_user: "{{ gitea_runner_service_user }}"
|
|
||||||
environment:
|
|
||||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default(0) }}"
|
|
||||||
DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid | default(0) }}/docker.sock"
|
|
||||||
when: not runner_registered.stat.exists
|
|
||||||
register: register_output
|
|
||||||
changed_when: "'already exists' not in register_output.stdout | default('')"
|
|
||||||
timeout: 60
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Ensure keyrings directory exists (Debian/Ubuntu)
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: "/etc/apt/keyrings"
|
|
||||||
state: directory
|
|
||||||
mode: "0755"
|
|
||||||
when: ansible_facts['os_family'] == 'Debian'
|
|
||||||
|
|
||||||
- name: Download and dearmor Docker GPG key (Debian/Ubuntu)
|
|
||||||
ansible.builtin.shell: |
|
|
||||||
set -o pipefail
|
|
||||||
curl -fsSL "https://download.docker.com/linux/{{ ansible_facts['distribution'] | lower }}/gpg" | gpg --dearmor --yes -o {{ docker_gpg_key_path }}
|
|
||||||
args:
|
|
||||||
creates: "{{ docker_gpg_key_path }}"
|
|
||||||
executable: /bin/bash
|
|
||||||
when: ansible_facts['os_family'] == 'Debian'
|
|
||||||
|
|
||||||
- name: Add Docker APT repository (Debian/Ubuntu)
|
|
||||||
ansible.builtin.copy:
|
|
||||||
dest: /etc/apt/sources.list.d/docker.list
|
|
||||||
content: "{{ docker_apt_source_line }}\n"
|
|
||||||
mode: "0644"
|
|
||||||
register: docker_apt_repo
|
|
||||||
when: ansible_facts['os_family'] == 'Debian'
|
|
||||||
|
|
||||||
- name: Update apt cache after adding Docker repo (Debian/Ubuntu)
|
|
||||||
ansible.builtin.apt:
|
|
||||||
update_cache: true
|
|
||||||
when:
|
|
||||||
- ansible_facts['os_family'] == 'Debian'
|
|
||||||
- docker_apt_repo is changed
|
|
||||||
|
|
||||||
- name: Install rootless Docker dependencies (Debian/Ubuntu)
|
|
||||||
ansible.builtin.apt:
|
|
||||||
name:
|
|
||||||
- uidmap
|
|
||||||
- slirp4netns
|
|
||||||
- fuse-overlayfs
|
|
||||||
- docker-ce
|
|
||||||
- docker-ce-cli
|
|
||||||
- docker-ce-rootless-extras
|
|
||||||
- containerd.io
|
|
||||||
- docker-compose-plugin
|
|
||||||
- rsync
|
|
||||||
state: present
|
|
||||||
when: ansible_facts['os_family'] == 'Debian'
|
|
||||||
|
|
||||||
- name: Update pacman cache (Arch Linux)
|
|
||||||
community.general.pacman:
|
|
||||||
update_cache: true
|
|
||||||
when: ansible_facts['os_family'] == 'Archlinux'
|
|
||||||
changed_when: false
|
|
||||||
|
|
||||||
- name: Install rootless Docker dependencies (Arch Linux)
|
|
||||||
community.general.pacman:
|
|
||||||
name:
|
|
||||||
- docker
|
|
||||||
- docker-compose
|
|
||||||
- slirp4netns
|
|
||||||
- fuse-overlayfs
|
|
||||||
- rsync
|
|
||||||
state: present
|
|
||||||
when: ansible_facts['os_family'] == 'Archlinux'
|
|
||||||
|
|
||||||
- name: Check if rootless Docker is already set up
|
|
||||||
ansible.builtin.stat:
|
|
||||||
path: "{{ gitea_runner_home }}/.config/systemd/user/docker.service"
|
|
||||||
register: rootless_docker_check
|
|
||||||
|
|
||||||
- name: Set up rootless Docker for runner user
|
|
||||||
ansible.builtin.command: dockerd-rootless-setuptool.sh install
|
|
||||||
args:
|
|
||||||
creates: "{{ gitea_runner_home }}/.config/systemd/user/docker.service"
|
|
||||||
become: true
|
|
||||||
become_user: "{{ gitea_runner_service_user }}"
|
|
||||||
environment:
|
|
||||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
|
||||||
when:
|
|
||||||
- docker_rootless_setup
|
|
||||||
- not rootless_docker_check.stat.exists
|
|
||||||
|
|
||||||
- name: Start rootless Docker daemon (systemd user service)
|
|
||||||
ansible.builtin.command: systemctl --user start docker
|
|
||||||
become: true
|
|
||||||
become_user: "{{ gitea_runner_service_user }}"
|
|
||||||
environment:
|
|
||||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
|
||||||
changed_when: true
|
|
||||||
when: docker_rootless_setup
|
|
||||||
|
|
||||||
- name: Enable rootless Docker daemon (systemd user service)
|
|
||||||
ansible.builtin.command: systemctl --user enable docker
|
|
||||||
become: true
|
|
||||||
become_user: "{{ gitea_runner_service_user }}"
|
|
||||||
environment:
|
|
||||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
|
||||||
changed_when: true
|
|
||||||
when: docker_rootless_setup
|
|
||||||
|
|
||||||
- name: Wait for rootless Docker daemon to be ready
|
|
||||||
ansible.builtin.command: docker version
|
|
||||||
become: true
|
|
||||||
become_user: "{{ gitea_runner_service_user }}"
|
|
||||||
environment:
|
|
||||||
DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid }}/docker.sock"
|
|
||||||
XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}"
|
|
||||||
register: docker_ready
|
|
||||||
until: docker_ready.rc == 0
|
|
||||||
retries: 10
|
|
||||||
delay: 2
|
|
||||||
changed_when: false
|
|
||||||
when: docker_rootless_setup
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user