Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -99,7 +99,7 @@ Focus on the FIRST error.
|
||||
**Release failures:**
|
||||
- **git-cliff errors**: version calculation, no unreleased changes
|
||||
- **Lint/test during release**: release runs `make lint-ruff` and `make pytest-cov`
|
||||
- **Tag/commit misalignment**: check `src/gitea_runner_manager/__init__.py` version
|
||||
- **Tag/commit misalignment**: check `src/grm/__init__.py` version
|
||||
|
||||
**Publish failures:**
|
||||
- **PyPI publish**: registry auth, package build errors
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
name: doc-syncer
|
||||
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:
|
||||
@@ -69,3 +69,24 @@ 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 quality 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.
|
||||
|
||||
+14
-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.
|
||||
# Required scopes: read:user, read:repository, read:admin (or just "admin")
|
||||
# Generate token at: Settings → Applications → Generate New Token
|
||||
# CI_GITEA_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).
|
||||
# Number of times to retry API checks waiting for runner to appear.
|
||||
@@ -48,7 +49,17 @@ GITEA_REGISTRATION_TOKEN=your-registration-token
|
||||
|
||||
# Gitea PyPI registry username (for private package access)
|
||||
# Used by PIP_INSTALL to configure PIP_EXTRA_INDEX_URL
|
||||
CI_GITEA_USERNAME=emil
|
||||
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
|
||||
@@ -60,4 +71,4 @@ DEVX_TASK_PREFIX=GRM
|
||||
# Vikunja project ID for GRM
|
||||
DEVX_VIKUNJA_PROJECT_ID=6
|
||||
# Version file path (relative to repo root)
|
||||
DEVX_VERSION_FILE=src/gitea_runner_manager/__init__.py
|
||||
DEVX_VERSION_FILE=src/grm/__init__.py
|
||||
|
||||
+27
-26
@@ -6,7 +6,7 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
||||
|
||||
jobs:
|
||||
@@ -18,9 +18,9 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
env:
|
||||
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
||||
run: make setup-image EXTRAS=lint
|
||||
run: make setup-image EXTRAS=ci,lint
|
||||
- name: Lint all
|
||||
run: |
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
@@ -30,21 +30,20 @@ jobs:
|
||||
run: |
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
make pytest-cov
|
||||
- name: Documentation lint check
|
||||
- name: Documentation gate (coverage + stale refs + lint + version refs + prose)
|
||||
env:
|
||||
PYTHONPATH: src
|
||||
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
||||
DEVX_DOC_COVERAGE_STRICT: "1"
|
||||
DEVX_DOC_VERSIONS_PKG: grm
|
||||
DEVX_VALE_LEVEL: warning
|
||||
run: |
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
pip install --upgrade devx \
|
||||
--index-url "https://${CI_GITEA_USERNAME}:${CI_GITEA_TOKEN}@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/" \
|
||||
--no-deps
|
||||
python3 -m devx.ci.lint_docs --root .
|
||||
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/gitea_runner_manager/translations.json
|
||||
python3 -m devx.ci.check_translations --translations src/grm/translations.json
|
||||
- name: Check unit test speed
|
||||
env:
|
||||
PYTHONPATH: src
|
||||
@@ -81,13 +80,13 @@ jobs:
|
||||
fetch-depth: 0
|
||||
- name: Set up environment
|
||||
env:
|
||||
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
||||
run: make setup-image EXTRAS=ci,lint
|
||||
- name: Release dry-run validation
|
||||
env:
|
||||
PYTHONPATH: src
|
||||
DEVX_VERSION_FILE: src/gitea_runner_manager/__init__.py
|
||||
DEVX_VERSION_FILE: src/grm/__init__.py
|
||||
DEVX_TASK_PREFIX: GRM
|
||||
run: |
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
@@ -107,7 +106,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
- name: Set up environment
|
||||
env:
|
||||
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
||||
run: make setup-image EXTRAS=ci
|
||||
- name: Detect changed paths
|
||||
@@ -136,7 +135,7 @@ jobs:
|
||||
run: make setup-image EXTRAS=ci
|
||||
- name: Validate auto-merge preconditions
|
||||
env:
|
||||
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }}
|
||||
DEVX_TASK_PREFIX: GRM
|
||||
DEVX_VIKUNJA_PROJECT_ID: 6
|
||||
@@ -166,13 +165,13 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
env:
|
||||
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
||||
run: make setup-image EXTRAS=ci
|
||||
- name: Discover available runners
|
||||
id: discover
|
||||
env:
|
||||
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
MOLECULE_RUNNERS: ${{ vars.MOLECULE_RUNNERS }}
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
@@ -197,7 +196,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
env:
|
||||
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
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
|
||||
@@ -224,12 +223,14 @@ jobs:
|
||||
echo "Docker not available in CI container — skipping molecule tests"
|
||||
exit 0
|
||||
fi
|
||||
echo "$CI_GITEA_TOKEN" | docker login git.oblachno.oblachno.fyi -u "$CI_GITEA_USERNAME" --password-stdin
|
||||
_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
|
||||
# shellcheck disable=SC2086 # intentional word splitting for argument expansion
|
||||
python3 -m devx.molecule.molecule_ci_guard $TEST_PAIRS
|
||||
env:
|
||||
GITEA_URL: ${{ github.server_url }}
|
||||
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
||||
RUN_ID: ${{ github.run_id }}
|
||||
ANSIBLE_INJECT_INVOCATION: "1"
|
||||
@@ -251,12 +252,12 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
env:
|
||||
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
||||
run: make setup-image EXTRAS=ci
|
||||
- name: Run automated PR review
|
||||
env:
|
||||
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -289,15 +290,15 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
token: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
- name: Set up environment
|
||||
env:
|
||||
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
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:
|
||||
CI_GITEA_TOKEN: ${{ secrets.REVIEW_GITEA_TOKEN }}
|
||||
REVIEWER_GITEA_API_TOKEN: ${{ secrets.REVIEWER_GITEA_API_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
PYTHONPATH: src
|
||||
@@ -312,7 +313,7 @@ jobs:
|
||||
--body "Auto-approved: all CI checks passed (quality, molecule, pr-review, pre-merge-check)."
|
||||
- name: Squash merge with task ID
|
||||
env:
|
||||
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }}
|
||||
PYTHONPATH: src
|
||||
DEVX_TASK_PREFIX: GRM
|
||||
|
||||
@@ -32,7 +32,7 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
||||
|
||||
jobs:
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
fetch-depth: 1
|
||||
- name: Set up environment
|
||||
env:
|
||||
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
||||
run: make setup-image EXTRAS=ci
|
||||
- name: Check if this is a release commit
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
fetch-depth: 1
|
||||
- name: Set up environment
|
||||
env:
|
||||
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
||||
run: make setup-image EXTRAS=ci
|
||||
- name: Validate latest commit message
|
||||
@@ -96,10 +96,10 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
token: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
- name: Set up environment
|
||||
env:
|
||||
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
||||
run: make setup-image EXTRAS=ci,lint
|
||||
- name: Configure git
|
||||
@@ -110,7 +110,7 @@ jobs:
|
||||
id: release-tag
|
||||
env:
|
||||
PYTHONPATH: src
|
||||
DEVX_VERSION_FILE: src/gitea_runner_manager/__init__.py
|
||||
DEVX_VERSION_FILE: src/grm/__init__.py
|
||||
DEVX_TASK_PREFIX: GRM
|
||||
DEVX_VIKUNJA_PROJECT_ID: 6
|
||||
run: |
|
||||
@@ -120,7 +120,7 @@ jobs:
|
||||
- name: Notify on failure
|
||||
if: failure()
|
||||
env:
|
||||
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
@@ -144,12 +144,12 @@ jobs:
|
||||
ref: ${{ needs.release.outputs.tag }}
|
||||
- name: Set up environment
|
||||
env:
|
||||
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
||||
run: make setup-image EXTRAS=ci,lint
|
||||
- name: Build and publish release
|
||||
env:
|
||||
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
@@ -160,7 +160,7 @@ jobs:
|
||||
- name: Notify on failure
|
||||
if: failure()
|
||||
env:
|
||||
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
@@ -176,27 +176,30 @@ jobs:
|
||||
if: needs.detect-type.outputs.is-release == 'false'
|
||||
runs-on: docker
|
||||
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
|
||||
timeout-minutes: 10
|
||||
timeout-minutes: 15
|
||||
concurrency:
|
||||
group: sync-wiki-${{ github.repository }}
|
||||
cancel-in-progress: false
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up environment
|
||||
env:
|
||||
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
||||
run: make setup-image EXTRAS=ci
|
||||
- name: Sync documentation to wiki
|
||||
env:
|
||||
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
python3 -m devx.ci.sync_wiki --repo "${{ github.repository }}" --strict
|
||||
python3 -m devx.ci.sync_wiki --repo "${{ github.repository }}" --verify
|
||||
- name: Notify on failure
|
||||
if: failure()
|
||||
env:
|
||||
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
@@ -217,14 +220,14 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: master
|
||||
token: ${{ secrets.CI_GITEA_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
|
||||
env:
|
||||
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
||||
run: make setup-image EXTRAS=lint
|
||||
- name: Generate and push badges
|
||||
@@ -236,7 +239,7 @@ jobs:
|
||||
- name: Notify on failure
|
||||
if: failure()
|
||||
env:
|
||||
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
@@ -258,7 +261,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
- name: Set up environment
|
||||
env:
|
||||
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
|
||||
run: make setup-image EXTRAS=ci
|
||||
- name: Update Vikunja task
|
||||
@@ -273,7 +276,7 @@ jobs:
|
||||
- name: Notify on failure
|
||||
if: failure()
|
||||
env:
|
||||
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
@@ -293,12 +296,12 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
env:
|
||||
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
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:
|
||||
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
PYTHONPATH: src
|
||||
DEVX_REPO_NAME: grm
|
||||
DEVX_REPO_OWNER: oblachno-oss
|
||||
@@ -309,7 +312,7 @@ jobs:
|
||||
- name: Notify on failure
|
||||
if: failure()
|
||||
env:
|
||||
CI_GITEA_TOKEN: ${{ secrets.CI_GITEA_TOKEN }}
|
||||
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
|
||||
@@ -57,6 +57,38 @@ repos:
|
||||
pass_filenames: false
|
||||
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
|
||||
name: pytest with 100% coverage
|
||||
entry: make pytest-cov
|
||||
|
||||
@@ -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,8 @@
|
||||
extends: existence
|
||||
message: "'%s' should be in lowercase."
|
||||
link: 'https://developers.google.com/style/colons'
|
||||
nonword: true
|
||||
level: warning
|
||||
scope: sentence
|
||||
tokens:
|
||||
- '(?<!:[^ ]+?):\s[A-Z]'
|
||||
@@ -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,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,13 @@
|
||||
extends: existence
|
||||
message: "Avoid first-person pronouns such as '%s'."
|
||||
link: 'https://developers.google.com/style/pronouns#personal-pronouns'
|
||||
ignorecase: true
|
||||
level: warning
|
||||
nonword: true
|
||||
tokens:
|
||||
- (?:^|\s)I\s
|
||||
- (?:^|\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,29 @@
|
||||
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
|
||||
indicators:
|
||||
- ":"
|
||||
exceptions:
|
||||
- Azure
|
||||
- CLI
|
||||
- Cosmos
|
||||
- Docker
|
||||
- Emmet
|
||||
- gRPC
|
||||
- I
|
||||
- Kubernetes
|
||||
- Linux
|
||||
- macOS
|
||||
- Marketplace
|
||||
- MongoDB
|
||||
- REPL
|
||||
- Studio
|
||||
- TypeScript
|
||||
- URLs
|
||||
- Visual
|
||||
- VS
|
||||
- Windows
|
||||
- JSON
|
||||
@@ -0,0 +1,11 @@
|
||||
extends: substitution
|
||||
message: "Use '%s' instead of '%s'."
|
||||
link: 'https://developers.google.com/style/abbreviations'
|
||||
ignorecase: true
|
||||
level: error
|
||||
nonword: true
|
||||
action:
|
||||
name: replace
|
||||
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,7 @@
|
||||
extends: existence
|
||||
message: "Use the Oxford comma in '%s'."
|
||||
link: 'https://developers.google.com/style/commas'
|
||||
scope: sentence
|
||||
level: warning
|
||||
tokens:
|
||||
- '(?:[^,]+,){1,}\s\w+\s(?:and|or)'
|
||||
@@ -0,0 +1,7 @@
|
||||
extends: existence
|
||||
message: "Use parentheses judiciously."
|
||||
link: 'https://developers.google.com/style/parentheses'
|
||||
nonword: true
|
||||
level: suggestion
|
||||
tokens:
|
||||
- '\(.+\)'
|
||||
@@ -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,8 @@
|
||||
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\d+(?:ns|ms|s|min|h|d)
|
||||
@@ -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,80 @@
|
||||
extends: substitution
|
||||
message: "Use '%s' instead of '%s'."
|
||||
link: "https://developers.google.com/style/word-list"
|
||||
level: warning
|
||||
ignorecase: false
|
||||
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
|
||||
"(?:OAuth ?2|Oauth)": 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
|
||||
Ajax: AJAX
|
||||
a\.k\.a|aka: or|also known as
|
||||
Android device: Android-powered device
|
||||
android: Android
|
||||
API explorer: APIs Explorer
|
||||
application: app
|
||||
approx\.: approximately
|
||||
authN: authentication
|
||||
authZ: authorization
|
||||
autoupdate: automatically update
|
||||
cellular data: mobile data
|
||||
cellular network: mobile network
|
||||
chapter: documents|pages|sections
|
||||
check box: checkbox
|
||||
CLI: command-line tool
|
||||
click on: click|click in
|
||||
Cloud: Google Cloud Platform|GCP
|
||||
Container Engine: Kubernetes Engine
|
||||
content type: media type
|
||||
curated roles: predefined roles
|
||||
data are: data is
|
||||
Developers Console: Google API Console|API Console
|
||||
disabled?: turn off|off
|
||||
ephemeral IP address: ephemeral external IP address
|
||||
fewer data: less data
|
||||
file name: filename
|
||||
firewalls: firewall rules
|
||||
functionality: capability|feature
|
||||
Google account: Google Account
|
||||
Google accounts: Google Accounts
|
||||
Googling: search with Google
|
||||
grayed-out: unavailable
|
||||
HTTPs: HTTPS
|
||||
in order to: to
|
||||
ingest: import|load
|
||||
k8s: Kubernetes
|
||||
long press: touch & hold
|
||||
network IP address: internal IP address
|
||||
omnibox: address bar
|
||||
open-source: open source
|
||||
overview screen: recents screen
|
||||
regex: regular expression
|
||||
SHA1: SHA-1|HAS-SHA1
|
||||
sign into: sign in to
|
||||
sign-?on: single sign-on
|
||||
static IP address: static external IP address
|
||||
stylesheet: style sheet
|
||||
synch: sync
|
||||
tablename: table name
|
||||
tablet: device
|
||||
touch: tap
|
||||
url: URL
|
||||
vs\.: versus
|
||||
World Wide Web: web
|
||||
@@ -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"
|
||||
}
|
||||
@@ -56,8 +56,8 @@ CI also runs a best-effort `make workflow-dryrun` step (skipped if act_runner is
|
||||
|
||||
## Architecture
|
||||
|
||||
- **Python CLI** (`src/gitea_runner_manager/`) — Click-based CLI that delegates to Ansible
|
||||
- **Ansible Role** (`ansible/roles/gitea-runner/`) — Idempotent role for rootless Docker runner setup
|
||||
- **Python CLI** (`src/grm/`) — Click-based CLI that delegates to Ansible
|
||||
- **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
|
||||
- **Versioning** (`cliff.toml`) — git-cliff configuration for automated semver versioning from conventional commits
|
||||
|
||||
@@ -84,6 +84,12 @@ as a defense-in-depth measure, but branch protection is the primary gate.
|
||||
### 1. Create Vikunja Task
|
||||
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
|
||||
```bash
|
||||
git checkout master && git pull
|
||||
@@ -97,7 +103,7 @@ git checkout -b GRM-N-short-description
|
||||
|
||||
### 4. Commit (Conventional Commits)
|
||||
Branch commits use conventional commit format (no `GRM-N:` prefix):
|
||||
```
|
||||
```text
|
||||
feat: add new feature
|
||||
fix: resolve bug
|
||||
docs: update README
|
||||
@@ -166,7 +172,7 @@ it attests that the reviewer has gone through every checklist category.
|
||||
The `--checklist-categories` flag is also **required** — it must list at
|
||||
least 8 of the 13 category numbers, ensuring the reviewer actually
|
||||
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.
|
||||
|
||||
Then add the `ready-to-merge` label. The auto-merge workflow will:
|
||||
@@ -232,7 +238,7 @@ Vikunja task updates:
|
||||
`AGENTS.md`, `Makefile`, etc.), the release is **skipped entirely** — no version
|
||||
bump, no tag, no publish. This prevents unnecessary releases for CI/docs-only changes.
|
||||
- Uses **git-cliff** to calculate the next semver version from conventional commits
|
||||
- Updates `__version__` in `src/gitea_runner_manager/__init__.py` (single source of truth)
|
||||
- Updates `__version__` in `src/grm/__init__.py` (single source of truth)
|
||||
- Updates `CHANGELOG.md` with the new version section
|
||||
- **Runs `make lint-ruff` and `make pytest-cov`** to verify the release is healthy
|
||||
- If lint or tests fail, **aborts immediately** — no commit, no tag
|
||||
@@ -244,7 +250,7 @@ Vikunja task updates:
|
||||
- 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. Runs for ALL
|
||||
non-release commits (not just when release succeeds), so docs-only
|
||||
non-release commits (not only when release succeeds), so docs-only
|
||||
changes still update the wiki.
|
||||
|
||||
4. **badges** — Generates and pushes quality badge SVGs to the `badges` branch.
|
||||
@@ -253,7 +259,7 @@ Vikunja task updates:
|
||||
any release commits.
|
||||
|
||||
5. **vikunja** — Marks the corresponding Vikunja task as done. Runs for ALL
|
||||
non-release commits (not just when release succeeds), so infrastructure-only
|
||||
non-release commits (not only when release succeeds), so infrastructure-only
|
||||
changes still update the task tracker.
|
||||
|
||||
6. **publish** — Runs after release succeeds (needs: release). Builds and
|
||||
@@ -284,7 +290,7 @@ via `[tool.devx.classify]` in `pyproject.toml`.
|
||||
- `activate.sh`, `activate.fish`, `activate.zsh` — Generated venv scripts
|
||||
|
||||
**User-facing paths** (tool changes → release needed) — everything else:
|
||||
- `src/gitea_runner_manager/**` — Python CLI source (except `__init__.py`)
|
||||
- `src/grm/**` — Python CLI source (except `__init__.py`)
|
||||
- `ansible/**` — Ansible role
|
||||
- `pyproject.toml` — Package metadata
|
||||
- Any new file type not in the allowlist
|
||||
@@ -318,14 +324,14 @@ The codebase enforces strict separation between the GRM tool and the devx packag
|
||||
|
||||
| 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) |
|
||||
| `ansible/` | Ansible role for runner setup | Changes trigger release |
|
||||
|
||||
### Import Rules
|
||||
|
||||
1. **`src/gitea_runner_manager/` 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)
|
||||
1. **`src/grm/` NEVER imports from devx** — the GRM tool is self-contained
|
||||
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
|
||||
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)
|
||||
|
||||
@@ -355,11 +361,11 @@ The `tea` Gitea CLI tool is used for Gitea API interactions in devx. It is insta
|
||||
|
||||
### 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 |
|
||||
|------------|-------------|-----------------|
|
||||
| `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` |
|
||||
|
||||
**In workflows**, always use `env:` blocks (not inline `PYTHONPATH=value`):
|
||||
@@ -370,7 +376,7 @@ Since devx is installed as a package (via `pip install` from git), it is importa
|
||||
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
|
||||
|
||||
@@ -403,7 +409,7 @@ ensures all merged work appears in the changelog.
|
||||
| `feat!:` or `BREAKING CHANGE` | minor (pre-1.0: major would be 1.0.0) |
|
||||
| `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
|
||||
|
||||
@@ -419,7 +425,7 @@ The version source is `__version__` in `src/gitea_runner_manager/__init__.py`, r
|
||||
The devx package is configured via `DEVX_*` environment variables:
|
||||
- `DEVX_TASK_PREFIX=GRM` — Prefix for Vikunja task identifiers
|
||||
- `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 infrastructure and user-facing path patterns.
|
||||
|
||||
@@ -433,6 +439,23 @@ 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)
|
||||
- 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,
|
||||
@@ -462,14 +485,14 @@ report `ok`.
|
||||
|
||||
## Ansible Role Structure
|
||||
|
||||
```
|
||||
```text
|
||||
main.yml → systemd_check → user_setup → rootless_docker → install_runner → prune → integration_test
|
||||
```
|
||||
|
||||
- `install_runner.yml` handles: download, config, validate, register, service
|
||||
- `main.yml` handles: prune, integration_test (NOT install_runner — avoids duplicates)
|
||||
- `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)
|
||||
|
||||
## Molecule Scenarios
|
||||
|
||||
@@ -490,7 +513,7 @@ All documentation lives in `/docs/` and is synced to the Gitea wiki automaticall
|
||||
|
||||
### Structure
|
||||
|
||||
```
|
||||
```text
|
||||
docs/
|
||||
├── index.md # Wiki homepage
|
||||
├── mapping.json # File-to-wiki-page title mapping
|
||||
@@ -538,7 +561,7 @@ the user should not need to specify which profile to use.
|
||||
|
||||
### Available Profiles
|
||||
|
||||
**Global** (shared with infra and devx):
|
||||
**Global** (shared across all projects):
|
||||
|
||||
| Profile | Location | Purpose |
|
||||
|---------|----------|---------|
|
||||
@@ -552,7 +575,7 @@ the user should not need to specify which profile to use.
|
||||
| `ci-investigator` | Investigate CI failures (quality, molecule, release, publish, wiki sync) |
|
||||
| `molecule-runner` | Run 7 molecule scenarios across 4 platforms, report pass/fail |
|
||||
| `dep-upgrader` | Python + Ansible dependency upgrades with molecule verification |
|
||||
| `doc-syncer` | Doc coverage, doc linting, wiki sync for grm docs |
|
||||
| `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
|
||||
@@ -563,7 +586,7 @@ the user should not need to specify which profile to use.
|
||||
| PR ready for review | `pr-reviewer` | Foreground |
|
||||
| Molecule tests need to run | `molecule-runner` | Background |
|
||||
| Dependency upgrade requested | `dep-upgrader` | Background |
|
||||
| Doc coverage failure or wiki sync issue | `doc-syncer` | Background |
|
||||
| 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 |
|
||||
|
||||
@@ -573,7 +596,7 @@ the user should not need to specify which profile to use.
|
||||
2. **Background by default, foreground when blocking.**
|
||||
3. **Provide full context in the prompt** — subagents don't inherit conversation history.
|
||||
4. **One subagent per concern.** Chain: investigate → fix in main session → review.
|
||||
5. **Don't delegate trivial work** (<30s, <50 lines of context).
|
||||
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.
|
||||
|
||||
|
||||
@@ -2,6 +2,64 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [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
|
||||
|
||||
+2
-2
@@ -19,7 +19,7 @@ The `GRM-N` prefix is mandatory — CI extracts it for merge messages and Vikunj
|
||||
### Feature branches
|
||||
Use **conventional commits** on feature branches:
|
||||
|
||||
```
|
||||
```text
|
||||
feat: add new command
|
||||
fix: resolve timeout issue
|
||||
chore: update dependencies
|
||||
@@ -33,7 +33,7 @@ Allowed types: `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `perf`, `tes
|
||||
### Master branch (squash merges)
|
||||
Squash commits on `master` must follow:
|
||||
|
||||
```
|
||||
```text
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
grm Copyright (C) 2026 emil
|
||||
grm Copyright (C) 2026 oblachno-oss
|
||||
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.
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
.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
|
||||
.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
|
||||
VENV := .venv
|
||||
@@ -14,7 +15,7 @@ all: setup
|
||||
DEVX_PYTHON := $(BIN)/python
|
||||
DEVX_VENV := $(VENV)
|
||||
DEVX_BIN := $(BIN)
|
||||
DEVX_COV_PKG := src/gitea_runner_manager
|
||||
DEVX_COV_PKG := src/grm
|
||||
DEVX_TEST_PATHS := tests/ scripts/tests/
|
||||
DEVX_LINT_PATHS := src/ scripts/ tests/
|
||||
|
||||
@@ -85,7 +86,8 @@ setup-release: $(VENV)/bin/activate .env configure-gitea-pypi
|
||||
# the venv symlink first, then installs the project.
|
||||
setup-image:
|
||||
@if [ -d /opt/venv ]; then ln -sf /opt/venv .venv; . .venv/bin/activate; \
|
||||
if [ -n "$$CI_GITEA_TOKEN" ]; then export PIP_EXTRA_INDEX_URL="https://$$CI_GITEA_USERNAME:$$CI_GITEA_TOKEN@git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple/"; fi; \
|
||||
_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/"; fi; \
|
||||
pip install -e .$(if $(EXTRAS),[$(EXTRAS)],); \
|
||||
else echo "[setup-image] /opt/venv not found — falling back to setup-ci"; $(MAKE) setup-ci; fi
|
||||
|
||||
@@ -152,16 +154,15 @@ test-unit: devx-test-unit
|
||||
|
||||
# Override devx-pytest-cov to cover both src/ and scripts/
|
||||
pytest-cov:
|
||||
@$(BIN)/pytest $(DEVX_TEST_PATHS) -v --cov=src/gitea_runner_manager --cov=scripts --cov-report=term-missing --cov-fail-under=100
|
||||
@$(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
|
||||
|
||||
configure-gitea-pypi:
|
||||
@if [ -z "$$CI_GITEA_TOKEN" ]; then . ./.env 2>/dev/null; fi; \
|
||||
CI_GITEA_TOKEN="$$CI_GITEA_TOKEN"; \
|
||||
if [ -z "$$CI_GITEA_TOKEN" ]; then echo "[configure-gitea-pypi] CI_GITEA_TOKEN not set — skipping (devx must be on public PyPI)"; exit 0; fi; \
|
||||
echo "[configure-gitea-pypi] Gitea PyPI registry configured (CI_GITEA_TOKEN present)."
|
||||
_TOKEN="$$CI_GITEA_API_TOKEN"; [ -z "$$_TOKEN" ] && _TOKEN="$$DEVELOPER_GITEA_API_TOKEN"; [ -z "$$_TOKEN" ] && _TOKEN="$$CI_GITEA_TOKEN"; \
|
||||
if [ -z "$$_TOKEN" ]; then echo "[configure-gitea-pypi] Gitea API token not set — skipping (devx must be on public PyPI)"; exit 0; fi; \
|
||||
echo "[configure-gitea-pypi] Gitea PyPI registry configured (token present)."
|
||||
|
||||
ansible-lint:
|
||||
PATH="$(PWD)/$(BIN):$$PATH" $(BIN)/ansible-lint ansible/
|
||||
@@ -173,7 +174,10 @@ makefile-lint:
|
||||
echo "checkmake not found, skipping Makefile lint"; \
|
||||
fi
|
||||
|
||||
lint-all: lint ansible-lint makefile-lint workflow-lint
|
||||
lint-all: lint ansible-lint makefile-lint workflow-lint check-api-identity-checks
|
||||
|
||||
check-api-identity-checks:
|
||||
@$(BIN)/python -m devx.tools.check_api_identity_checks
|
||||
|
||||
test-integration:
|
||||
$(BIN)/pytest tests/integration/ -v --no-cov
|
||||
@@ -199,3 +203,7 @@ create-task: devx-create-task
|
||||
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/src/branch/master/LICENSE)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/releases)
|
||||
[](https://www.python.org/downloads/)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/releases)
|
||||
[](https://www.python.org/downloads/)
|
||||
|
||||
## Why GRM?
|
||||
|
||||
@@ -99,7 +99,7 @@ The registry is publicly readable — no authentication required to install.
|
||||
**Quick install (one-off):**
|
||||
|
||||
```bash
|
||||
pip install gitea-runner-manager --index-url https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple
|
||||
pip install grm --index-url https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple
|
||||
```
|
||||
|
||||
**Persistent configuration (recommended):**
|
||||
@@ -115,7 +115,7 @@ extra-index-url = https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/py
|
||||
Then install normally:
|
||||
|
||||
```bash
|
||||
pip install gitea-runner-manager
|
||||
pip install grm
|
||||
```
|
||||
|
||||
This installs the `grm` CLI and its Python dependencies. The Ansible playbooks
|
||||
@@ -147,7 +147,7 @@ GRM provides a single `grm` command with subcommands for the full runner lifecyc
|
||||
| `grm enable <name>` | Enable a runner to start on boot |
|
||||
| `grm disable <name>` | Disable and deregister a 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 --no-status` | List registered runners without SSH status checks |
|
||||
@@ -187,7 +187,7 @@ GRM reads configuration from a `.env` file in the current directory (loaded auto
|
||||
|
||||
### Sudo Password Handling
|
||||
|
||||
GRM delegates remote operations to Ansible, which uses `sudo` (become) on the target host. There are several ways to provide the sudo password, in priority order:
|
||||
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.
|
||||
@@ -339,11 +339,11 @@ See the [Development Setup](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/w
|
||||
|
||||
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.
|
||||
|
||||
```
|
||||
```text
|
||||
grm install <host>
|
||||
└── RunnerManager.install()
|
||||
└── ansible-playbook ansible/install-runner.yml
|
||||
|
||||
@@ -53,3 +53,10 @@ docker_apt_source_line: >-
|
||||
{{ ansible_facts['distribution_release'] }} stable
|
||||
# Set to false in CI/molecule to skip rootless daemon startup (needs kernel userns)
|
||||
docker_rootless_setup: true
|
||||
|
||||
# Rootless Docker network driver: "pasta" (IPv6 support) or "slirp4netns" (IPv4 only)
|
||||
# pasta has proper outgoing IPv6 support; slirp4netns does not (known limitation).
|
||||
docker_rootless_net_driver: "pasta"
|
||||
|
||||
# IPv6 subnet for rootless Docker containers (ULA range, not routable on internet)
|
||||
docker_ipv6_cidr: "fd00:dead:beef::/48"
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
name:
|
||||
- uidmap
|
||||
- slirp4netns
|
||||
- passt
|
||||
- fuse-overlayfs
|
||||
- docker-ce
|
||||
- docker-ce-cli
|
||||
@@ -57,6 +58,7 @@
|
||||
- docker
|
||||
- docker-compose
|
||||
- slirp4netns
|
||||
- passt
|
||||
- fuse-overlayfs
|
||||
- rsync
|
||||
state: present
|
||||
@@ -97,6 +99,75 @@
|
||||
changed_when: true
|
||||
when: docker_rootless_setup
|
||||
|
||||
- name: Ensure Docker config directory exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ gitea_runner_home }}/.config/docker"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
owner: "{{ gitea_runner_service_user }}"
|
||||
group: "{{ gitea_runner_service_user }}"
|
||||
when: docker_rootless_setup
|
||||
|
||||
- name: Ensure systemd user override directory exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ gitea_runner_home }}/.config/systemd/user/docker.service.d"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
owner: "{{ gitea_runner_service_user }}"
|
||||
group: "{{ gitea_runner_service_user }}"
|
||||
when: docker_rootless_setup
|
||||
|
||||
- name: Configure rootless Docker to use pasta with IPv6
|
||||
ansible.builtin.copy:
|
||||
dest: "{{ gitea_runner_home }}/.config/systemd/user/docker.service.d/override.conf"
|
||||
content: |
|
||||
[Service]
|
||||
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_NET={{ docker_rootless_net_driver }}"
|
||||
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=implicit"
|
||||
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_FLAGS=--ipv6"
|
||||
mode: "0644"
|
||||
owner: "{{ gitea_runner_service_user }}"
|
||||
group: "{{ gitea_runner_service_user }}"
|
||||
register: docker_network_override
|
||||
when: docker_rootless_setup
|
||||
|
||||
- name: Reload systemd user daemon if network config changed
|
||||
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:
|
||||
- docker_rootless_setup
|
||||
- docker_network_override is changed
|
||||
|
||||
- name: Configure rootless Docker daemon with IPv6 enabled
|
||||
ansible.builtin.copy:
|
||||
dest: "{{ gitea_runner_home }}/.config/docker/daemon.json"
|
||||
content: |
|
||||
{
|
||||
"ipv6": true,
|
||||
"ip6tables": true,
|
||||
"fixed-cidr-v6": "{{ docker_ipv6_cidr }}"
|
||||
}
|
||||
mode: "0644"
|
||||
owner: "{{ gitea_runner_service_user }}"
|
||||
group: "{{ gitea_runner_service_user }}"
|
||||
register: docker_ipv6_config
|
||||
when: docker_rootless_setup
|
||||
|
||||
- name: Restart rootless Docker if config changed
|
||||
ansible.builtin.command: systemctl --user restart 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
|
||||
- docker_ipv6_config is changed or docker_network_override is changed
|
||||
|
||||
- name: Wait for rootless Docker daemon to be ready
|
||||
ansible.builtin.command: docker version
|
||||
become: true
|
||||
|
||||
+6
-6
@@ -8,12 +8,12 @@ Each runner runs in an isolated **rootless Docker** environment under a dedicate
|
||||
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/src/branch/master/LICENSE)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/releases)
|
||||
[](https://www.python.org/downloads/)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/wiki)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/releases)
|
||||
[](https://www.python.org/downloads/)
|
||||
|
||||
## Overview
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
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.
|
||||
1. **Python CLI** (`src/grm/`) — built with Click, handles argument parsing, environment loading, i18n translations, and delegates to Ansible via the `ansible-playbook` subprocess.
|
||||
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, and registers the runner with Gitea.
|
||||
|
||||
## High-Level Design
|
||||
@@ -22,7 +22,7 @@ The Ansible role handles all remote state: user creation, package installation,
|
||||
|
||||
## Component Tree
|
||||
|
||||
```
|
||||
```text
|
||||
grm install <host>
|
||||
└── RunnerManager.install()
|
||||
└── ansible-playbook ansible/install-runner.yml
|
||||
@@ -37,14 +37,14 @@ grm install <host>
|
||||
|
||||
The Ansible role task execution order (from `AGENTS.md`):
|
||||
|
||||
```
|
||||
```text
|
||||
main.yml → systemd_check → user_setup → rootless_docker → install_runner → prune → healthcheck → integration_test
|
||||
```
|
||||
|
||||
- `install_runner.yml` handles: download, config, validate, register, service
|
||||
- `main.yml` handles: prune, integration_test (NOT install_runner — avoids duplicates)
|
||||
- `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)
|
||||
|
||||
### Ansible task files
|
||||
|
||||
@@ -95,7 +95,7 @@ Lingering is enabled via `loginctl enable-linger` so the user's systemd services
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
CLI["Python CLI<br/>src/gitea_runner_manager/<br/>(Click)"]
|
||||
CLI["Python CLI<br/>src/grm/<br/>(Click)"]
|
||||
RM["RunnerManager<br/>runner_manager.py"]
|
||||
EXEC["Executor<br/>executor.py"]
|
||||
REG["Registry<br/>registry.py<br/>~/.local/share/grm/runners.json"]
|
||||
@@ -173,9 +173,21 @@ Each runner operates under a dedicated unprivileged system user. The Docker daem
|
||||
|
||||
- User namespace mapping via `/etc/subuid` and `/etc/subgid` (range: 100000-165535)
|
||||
- Rootless Docker socket at `/run/user/<UID>/docker.sock`
|
||||
- `slirp4netns` for user-mode networking
|
||||
- `pasta` for user-mode networking with IPv6 support (replaces `slirp4netns`, which lacks outgoing IPv6)
|
||||
- `fuse-overlayfs` for rootless container storage
|
||||
|
||||
The rootless Docker daemon is configured via a systemd user override
|
||||
(`docker.service.d/override.conf`) that sets:
|
||||
|
||||
- `DOCKERD_ROOTLESS_ROOTLESSKIT_NET=pasta` — use pasta as the network driver
|
||||
- `DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=implicit` — pasta's native port forwarding
|
||||
- `DOCKERD_ROOTLESS_ROOTLESSKIT_FLAGS=--ipv6` — enable IPv6 routing
|
||||
|
||||
The daemon.json enables IPv6 with a ULA subnet (`fd00:dead:beef::/48`)
|
||||
for container addressing. This ensures runner containers can reach
|
||||
both IPv4 and IPv6 services (e.g., the Gitea registry) without
|
||||
per-workaround DNS hacks.
|
||||
|
||||
Containers launched by the runner never have root access to the host. The rootless Docker daemon is started as a systemd user service and persists via lingering.
|
||||
|
||||
### Secret handling
|
||||
@@ -207,7 +219,7 @@ From `AGENTS.md`, the project also includes:
|
||||
|
||||
## Python Modules
|
||||
|
||||
The Python CLI layer (`src/gitea_runner_manager/`) consists of the following modules:
|
||||
The Python CLI layer (`src/grm/`) consists of the following modules:
|
||||
|
||||
| Module | Description |
|
||||
|--------|-------------|
|
||||
|
||||
@@ -36,7 +36,7 @@ git checkout -b GRM-N-short-description
|
||||
|
||||
Branch commits use conventional commit format (no `GRM-N:` prefix):
|
||||
|
||||
```
|
||||
```text
|
||||
feat: add new feature
|
||||
fix: resolve bug
|
||||
docs: update README
|
||||
@@ -158,7 +158,7 @@ After a PR is merged to master, the release pipeline runs automatically.
|
||||
- Runs `devx.ci.release` which uses **git-cliff** to:
|
||||
- **Checks for user-facing changes** via `devx.ci.classify_changes` — if only workflow/infrastructure files changed, the release is **skipped entirely** — no version bump, no tag, no publish
|
||||
- Calculate the next semver version from conventional commits since the last tag
|
||||
- Update `__version__` in `src/gitea_runner_manager/__init__.py` (single source of truth)
|
||||
- Update `__version__` in `src/grm/__init__.py` (single source of truth)
|
||||
- Update `CHANGELOG.md` with the new version section
|
||||
- **Run `make lint-ruff` and `make pytest-cov`** to verify the release is healthy
|
||||
- If lint or tests fail, **abort immediately** — no commit, no tag
|
||||
@@ -208,7 +208,7 @@ from accidentally skipping releases. Classification is config-driven via
|
||||
`[tool.devx.classify]` in `pyproject.toml`.
|
||||
|
||||
**User-facing paths** (tool changes → release needed):
|
||||
- `src/gitea_runner_manager/**` — Python CLI source
|
||||
- `src/grm/**` — Python CLI source
|
||||
- `ansible/**` — Ansible role
|
||||
- `pyproject.toml` — Package metadata
|
||||
|
||||
@@ -322,7 +322,7 @@ From `cliff.toml` `[bump]` section:
|
||||
- `breaking_always_bump_major = false`
|
||||
- `initial_tag = "0.1.0"`
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -27,15 +27,15 @@
|
||||
- **Secrets handling**: Secrets are passed via temp JSON files with `0600` permissions, never on the command line (CWE-214). Extra-vars are written to a temporary JSON file and passed via `--extra-vars @tempfile`, which is deleted after execution. This prevents secrets from being visible in the process list (`ps aux`).
|
||||
- **Linting**: `make lint-all` runs ruff + pyright + bandit + ansible-lint + checkmake + actionlint
|
||||
- **Formatting**: `ruff format` with double quotes and space indentation
|
||||
- **Type checking**: `pyright` in strict mode for `src/gitea_runner_manager/`
|
||||
- **Type checking**: `pyright` in strict mode for `src/grm/`
|
||||
- **Security scanning**: `bandit -r src/` on every PR
|
||||
- **Import rules**: `src/gitea_runner_manager/` NEVER imports from devx — the GRM tool is self-contained
|
||||
- **Import rules**: `src/grm/` NEVER imports from devx — the GRM tool is self-contained
|
||||
|
||||
## Commit Rules
|
||||
|
||||
Branch commits use conventional commit format (no `GRM-N:` prefix):
|
||||
|
||||
```
|
||||
```text
|
||||
feat: add new feature
|
||||
fix: resolve bug
|
||||
docs: update README
|
||||
@@ -99,7 +99,7 @@ git checkout -b GRM-N-short-description
|
||||
|
||||
Branch commits use conventional commit format (no `GRM-N:` prefix):
|
||||
|
||||
```
|
||||
```text
|
||||
feat: add new feature
|
||||
fix: resolve bug
|
||||
docs: update README
|
||||
@@ -195,14 +195,14 @@ make workflow-check # Static lint + dry-run of workflow YAML
|
||||
|
||||
## Ansible Role Conventions
|
||||
|
||||
```
|
||||
```text
|
||||
main.yml → systemd_check → user_setup → rootless_docker → install_runner → prune → integration_test
|
||||
```
|
||||
|
||||
- `install_runner.yml` handles: download, config, validate, register, service
|
||||
- `main.yml` handles: prune, integration_test (NOT install_runner — avoids duplicates)
|
||||
- `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)
|
||||
- `apt` tasks use `cache_valid_time: 3600` to avoid unnecessary cache updates
|
||||
- `remove-runner.yml` runs `loginctl disable-linger` and removes subuid/subgid entries
|
||||
|
||||
@@ -216,7 +216,7 @@ Not all changes require a new release. The project classifies changes using `dev
|
||||
- Lint config files, `.env.example`, `.gitignore`
|
||||
|
||||
**User-facing paths** (release needed):
|
||||
- `src/gitea_runner_manager/**` (except `__init__.py`)
|
||||
- `src/grm/**` (except `__init__.py`)
|
||||
- `ansible/**`
|
||||
- `pyproject.toml`
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ Key technical decisions for the GRM project, extracted from `CHANGELOG.md` and `
|
||||
|
||||
**Date:** 2026-06-21 (v0.2.0 unreleased)
|
||||
|
||||
**Decision:** Use `dynamic = ["version"]` in `pyproject.toml` with setuptools `attr` to source the version from `__version__` in `src/gitea_runner_manager/__init__.py`.
|
||||
**Decision:** Use `dynamic = ["version"]` in `pyproject.toml` with setuptools `attr` to source the version from `__version__` in `src/grm/__init__.py`.
|
||||
|
||||
**Rationale:** `__init__.py` is the single source of truth for the version. The release script (`devx.ci.release`) only updates `__init__.py` — there is no need to touch `pyproject.toml`. `grm --version` reports this version directly. This eliminates version duplication across files and ensures the runtime version always matches the tagged release.
|
||||
|
||||
@@ -84,7 +84,7 @@ Key technical decisions for the GRM project, extracted from `CHANGELOG.md` and `
|
||||
|
||||
**Rationale:** Passing secrets as command-line arguments (e.g., `--extra-vars '{"token": "..."}'`) makes them visible in the process list (`ps aux`), which is a known security weakness (CWE-214). The `RunnerManager._extra_vars_file()` context manager writes extra-vars to a temporary file via `tempfile.mkstemp()`, sets permissions to `0600`, passes the file to Ansible via `--extra-vars @tempfile`, and deletes the file in a `finally` block — even if an exception occurs. This ensures secrets are never visible in the process list.
|
||||
|
||||
**Source:** `AGENTS.md` (Key Conventions), `src/gitea_runner_manager/runner_manager.py` (`_extra_vars_file` method)
|
||||
**Source:** `AGENTS.md` (Key Conventions), `src/grm/runner_manager.py` (`_extra_vars_file` method)
|
||||
|
||||
---
|
||||
|
||||
@@ -94,7 +94,7 @@ Key technical decisions for the GRM project, extracted from `CHANGELOG.md` and `
|
||||
|
||||
**Decision:** Classify changed files into user-facing and workflow-only categories using `devx.ci.classify_changes`. Only user-facing changes trigger a release; workflow-only changes (CI, docs, tests, lint config) do not.
|
||||
|
||||
**Rationale:** Not all changes require a new release. CI workflow updates, documentation improvements, and test additions should not produce a new version tag. The classification is config-driven via `[tool.devx.classify]` in `pyproject.toml`. The strategy is safe-by-default: any file NOT in the explicit workflow-only allowlist is treated as user-facing, preventing new file types from accidentally skipping releases. User-facing paths include `src/gitea_runner_manager/**` (except `__init__.py`) and `ansible/**`. Workflow-only paths include `.gitea/**`, `docs/**`, `tests/**`, `scripts/**`, and various config files.
|
||||
**Rationale:** Not all changes require a new release. CI workflow updates, documentation improvements, and test additions should not produce a new version tag. The classification is config-driven via `[tool.devx.classify]` in `pyproject.toml`. The strategy is safe-by-default: any file NOT in the explicit workflow-only allowlist is treated as user-facing, preventing new file types from accidentally skipping releases. User-facing paths include `src/grm/**` (except `__init__.py`) and `ansible/**`. Workflow-only paths include `.gitea/**`, `docs/**`, `tests/**`, `scripts/**`, and other config files.
|
||||
|
||||
**Source:** `AGENTS.md` (Smart CI: User-Facing vs Workflow-Only Changes), `pyproject.toml` (`[tool.devx.classify]`)
|
||||
|
||||
@@ -104,9 +104,9 @@ Key technical decisions for the GRM project, extracted from `CHANGELOG.md` and `
|
||||
|
||||
**Date:** 2026-06-21 (v0.6.2)
|
||||
|
||||
**Decision:** Separate CI/CD and development tooling into the `devx` package (installed from git), keeping the GRM tool itself self-contained in `src/gitea_runner_manager/`.
|
||||
**Decision:** Separate CI/CD and development tooling into the `devx` package (installed from git), keeping the GRM tool itself self-contained in `src/grm/`.
|
||||
|
||||
**Rationale:** The GRM CLI tool must be self-contained — it never imports from devx. This ensures the installed package has no dependency on CI infrastructure. devx MAY import from `gitea_runner_manager` (one-way dependency), as it uses the tool's API clients, config, and i18n for CI automation. Cross-module imports within devx are allowed. This separation was formalised when scripts were migrated from the `scripts/` directory to the devx package in GRM-64.
|
||||
**Rationale:** The GRM CLI tool must be self-contained — it never imports from devx. This ensures the installed package has no dependency on CI infrastructure. devx MAY import from `grm` (one-way dependency), as it uses the tool's API clients, config, and i18n for CI automation. Cross-module imports within devx are allowed. This separation was formalised when scripts were migrated from the `scripts/` directory to the devx package in GRM-64.
|
||||
|
||||
**Source:** `AGENTS.md` (Source Code Separation and devx Integration), `CHANGELOG.md` (0.6.2 — Refactor: "Migrate from scripts/ to devx package")
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
```text
|
||||
.
|
||||
├── src/gitea_runner_manager/ # Python CLI source
|
||||
├── src/grm/ # Python CLI source
|
||||
│ ├── cli.py # Click commands
|
||||
│ ├── runner_manager.py # Ansible orchestration + registry integration
|
||||
│ ├── executor.py # Ansible subprocess execution
|
||||
@@ -172,7 +172,7 @@ make test-unit # Without coverage
|
||||
make pytest-cov # With 100% coverage enforcement
|
||||
```
|
||||
|
||||
The coverage requirement is `--cov-fail-under=100` — 100% test coverage is required for all code in `src/gitea_runner_manager/`.
|
||||
The coverage requirement is `--cov-fail-under=100` — 100% test coverage is required for all code in `src/grm/`.
|
||||
|
||||
### Integration tests
|
||||
|
||||
|
||||
@@ -14,9 +14,9 @@ Runs pytest with 100% coverage requirement.
|
||||
From the `Makefile`:
|
||||
|
||||
- `test-unit` — `pytest tests/unit/ -v --no-cov` (unit tests without coverage)
|
||||
- `pytest-cov` — `pytest tests/ -v --cov=src/gitea_runner_manager --cov-report=term-missing --cov-fail-under=100` (unit tests with 100% coverage enforcement)
|
||||
- `pytest-cov` — `pytest tests/ -v --cov=src/grm --cov-report=term-missing --cov-fail-under=100` (unit tests with 100% coverage enforcement)
|
||||
|
||||
The coverage requirement is `--cov-fail-under=100` — 100% test coverage is required for all code in `src/gitea_runner_manager/`. The CI quality job runs `make pytest-cov` on every PR, and the release workflow runs it again before tagging a release.
|
||||
The coverage requirement is `--cov-fail-under=100` — 100% test coverage is required for all code in `src/grm/`. The CI quality job runs `make pytest-cov` on every PR, and the release workflow runs it again before tagging a release.
|
||||
|
||||
### Test speed verification
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ GRM provides the following CLI commands for managing Gitea Actions runners. The
|
||||
| `grm enable` | `<runner_name>` | Enable a runner to start on boot |
|
||||
| `grm disable` | `<runner_name>` | Disable and deregister a runner |
|
||||
| `grm status` | `<runner_name>` | Check the status of a registered runner |
|
||||
| `grm remove` | `<runner_name>` | Remove a runner completely |
|
||||
| `grm remove` | `<runner_name>` | Remove a runner entirely |
|
||||
| `grm list` | — | List all registered runners with live status |
|
||||
| `grm health` | `[runner_name]` | Run health check (Docker, runner service, disk) on one or all runners |
|
||||
| `grm trigger-workflow` | `<workflow_id>` | Trigger a Gitea Actions workflow via the API |
|
||||
@@ -244,7 +244,7 @@ grm status <runner_name> [options]
|
||||
|
||||
## remove
|
||||
|
||||
Remove a registered Gitea Runner completely.
|
||||
Remove a registered Gitea Runner entirely.
|
||||
|
||||
```bash
|
||||
grm remove <runner_name> [options]
|
||||
@@ -288,7 +288,7 @@ The status is checked live by running an Ansible ad-hoc command on each remote h
|
||||
|
||||
**Example output:**
|
||||
|
||||
```
|
||||
```text
|
||||
NAME HOST USER LABELS STATUS
|
||||
------------------------------------------------------------------------------------------
|
||||
prod-runner 192.168.1.10 ubuntu docker:docker://gitea/... active
|
||||
@@ -298,7 +298,7 @@ test-runner 192.168.1.20 ubuntu inac
|
||||
|
||||
If no runners are registered:
|
||||
|
||||
```
|
||||
```text
|
||||
No runners registered. Use 'grm install' to add one.
|
||||
```
|
||||
|
||||
@@ -374,7 +374,7 @@ Show the installed GRM version.
|
||||
grm --version
|
||||
```
|
||||
|
||||
This reports the version from `__version__` in `src/gitea_runner_manager/__init__.py`, which is the single source of truth set by the automated release pipeline.
|
||||
This reports the version from `__version__` in `src/grm/__init__.py`, which is the single source of truth set by the automated release pipeline.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
|
||||
+14
-14
@@ -1,6 +1,6 @@
|
||||
# FAQ
|
||||
|
||||
## How do I obtain the Gitea registration token?
|
||||
## How to obtain the Gitea registration token
|
||||
|
||||
There are three levels of registration tokens, depending on which repositories the runner should serve:
|
||||
|
||||
@@ -10,15 +10,15 @@ There are three levels of registration tokens, depending on which repositories t
|
||||
|
||||
Set the token as `GITEA_REGISTRATION_TOKEN` in your `.env` file or pass it via `--token` on the command line.
|
||||
|
||||
## What is the CI_GITEA_TOKEN and do I need it?
|
||||
## What is the CI_GITEA_TOKEN and is it needed?
|
||||
|
||||
`CI_GITEA_TOKEN` is a Gitea admin API token used for optional post-install verification. When set, GRM queries the Gitea API after installation to confirm the runner appears in the runner list. This is purely informational — the integration test passes/fails based on the `.runner` file and systemd service, not the API check.
|
||||
|
||||
To generate one: Settings → Applications → Generate New Token, with the `admin` scope (or at minimum `read:user`, `read:repository`, `read:admin`).
|
||||
|
||||
If you skip it, GRM will still verify the runner correctly — it just won't show the extra API confirmation.
|
||||
If you skip it, GRM will still verify the runner correctly — it won't show the extra API confirmation.
|
||||
|
||||
## How do I skip the sudo password prompt for automation?
|
||||
## How to skip the sudo password prompt for automation
|
||||
|
||||
Configure passwordless sudo on the remote host and pass `--no-ask-become-pass` to the CLI command. This is recommended for CI/CD pipelines.
|
||||
|
||||
@@ -34,7 +34,7 @@ Then use:
|
||||
grm install 192.168.1.10 --user ubuntu --key ~/.ssh/id_ed25519 --name prod-runner --no-ask-become-pass
|
||||
```
|
||||
|
||||
## Can I run multiple runners on the same host?
|
||||
## Can multiple runners run on the same host?
|
||||
|
||||
Yes. Each runner instance is fully isolated with its own system user (`grm-<name>`), rootless Docker daemon, data directory, and systemd user service. Install additional runners with different `--name` values and manage them independently by name.
|
||||
|
||||
@@ -46,7 +46,7 @@ grm list
|
||||
|
||||
Runners on the same host never interfere with each other or with the host's Docker installation.
|
||||
|
||||
## Why does my runner appear offline after installation?
|
||||
## Why does a runner appear offline after installation?
|
||||
|
||||
Check that `GITEA_URL` and `GITEA_REGISTRATION_TOKEN` are correct, verify the runner service is running with `sudo -u grm-<name> systemctl --user status gitea-runner`, and check the logs for registration errors. You can also confirm the runner appears as **Online** in the Gitea UI under **Actions → Runners**.
|
||||
|
||||
@@ -64,7 +64,7 @@ This is a harmless cleanup traceback from Molecule's Docker driver when the test
|
||||
|
||||
GRM stores each runner's connection details (host, user, SSH key, Gitea URL, labels) in a local JSON registry at `~/.local/share/grm/runners.json`. After installation, lifecycle commands work by runner name only — you can override any stored value by passing the corresponding flag.
|
||||
|
||||
## How do I update the gitea_runner binary?
|
||||
## How to update the gitea_runner binary
|
||||
|
||||
Use the `grm update` command:
|
||||
|
||||
@@ -80,7 +80,7 @@ grm update 192.168.1.10 --user ubuntu --version 1.0.8
|
||||
|
||||
The update command downloads the new binary and replaces the existing one at `/usr/local/bin/gitea_runner`. The runner service is restarted automatically.
|
||||
|
||||
## How do I completely remove a runner?
|
||||
## How to remove a runner entirely
|
||||
|
||||
Use the `grm remove` command:
|
||||
|
||||
@@ -99,13 +99,13 @@ grm remove prod-runner --force
|
||||
## What is the difference between disable and remove?
|
||||
|
||||
- **`grm disable <name>`** — Deregisters the runner from Gitea and stops the service, but leaves the user, directories, and service files in place. The runner can be re-enabled later with `grm enable` and re-registered with a new token.
|
||||
- **`grm remove <name>`** — Completely removes the runner: deregisters from Gitea, stops and disables the service, removes the system user, deletes all directories, and removes the local registry entry. This is irreversible.
|
||||
- **`grm remove <name>`** — Removes the runner entirely: deregisters from Gitea, stops and disables the service, removes the system user, deletes all directories, and removes the local registry entry. This is irreversible.
|
||||
|
||||
## What operating systems are supported?
|
||||
|
||||
GRM supports Arch Linux (rolling), Ubuntu 22.04/24.04, and Debian 12. All supported OSes are tested in CI via Molecule scenarios on every PR that changes Ansible files.
|
||||
|
||||
## How do I change the UI language?
|
||||
## How to change the UI language
|
||||
|
||||
Set the `GRM_LANG` environment variable to one of the supported languages: `en` (English, default), `bg` (Bulgarian), `de` (German), `ru` (Russian), `zh` (Chinese), `pl` (Polish).
|
||||
|
||||
@@ -119,7 +119,7 @@ Or set it in your `.env` file:
|
||||
GRM_LANG=bg
|
||||
```
|
||||
|
||||
## How do I enable debug logging?
|
||||
## How to enable debug logging
|
||||
|
||||
Set the `GRM_LOG_LEVEL` environment variable to `DEBUG`:
|
||||
|
||||
@@ -129,7 +129,7 @@ GRM_LOG_LEVEL=DEBUG grm install 192.168.1.10 --user ubuntu --name prod-runner
|
||||
|
||||
The log file at `~/.local/state/grm/logs/grm.log` always captures DEBUG level regardless of this setting. Ansible execution logs are stored in timestamped files at `~/.local/state/grm/logs/ansible-<timestamp>.log`.
|
||||
|
||||
## What runner labels should I use?
|
||||
## What runner labels should be used?
|
||||
|
||||
By default, runners are registered with `docker,ubuntu-latest:docker://runner-images:ubuntu-22.04`. You can override this with `--labels` or the `GITEA_RUNNER_LABELS` environment variable.
|
||||
|
||||
@@ -151,12 +151,12 @@ Yes. GRM is designed with security as a first-class concern:
|
||||
- **No shell injection**: The CLI never uses `shell=True` with subprocess.
|
||||
- **Bandit security scan**: The CI pipeline runs Bandit on every PR.
|
||||
|
||||
## Can I install GRM via pip?
|
||||
## Can GRM be installed via pip?
|
||||
|
||||
Yes:
|
||||
|
||||
```bash
|
||||
pip install gitea-runner-manager
|
||||
pip install grm
|
||||
```
|
||||
|
||||
This installs the `grm` CLI and its Python dependencies. The Ansible playbooks and role are bundled with the package. For development or access to Make targets, clone the repository instead.
|
||||
|
||||
@@ -58,7 +58,7 @@ The registry is publicly readable — no authentication required to install.
|
||||
**Quick install (one-off):**
|
||||
|
||||
```bash
|
||||
pip install gitea-runner-manager --index-url https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple
|
||||
pip install grm --index-url https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple
|
||||
```
|
||||
|
||||
**Persistent configuration (recommended):**
|
||||
@@ -73,7 +73,7 @@ extra-index-url = https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/py
|
||||
Then install normally:
|
||||
|
||||
```bash
|
||||
pip install gitea-runner-manager
|
||||
pip install grm
|
||||
```
|
||||
|
||||
This installs the `grm` CLI and its Python dependencies. The Ansible playbooks
|
||||
@@ -223,7 +223,7 @@ make update HOST=192.168.1.10 USER=ubuntu VERSION=1.0.8
|
||||
|
||||
## Removing Runners
|
||||
|
||||
To remove a runner completely (deregisters from Gitea, removes user, directories, and service files):
|
||||
To remove a runner entirely (deregisters from Gitea, removes user, directories, and service files):
|
||||
|
||||
```bash
|
||||
grm remove prod-runner --token <registration-token>
|
||||
|
||||
@@ -145,12 +145,12 @@ This is a harmless cleanup traceback from Molecule's Docker driver when the test
|
||||
The pre-commit hook validates that commit messages follow conventional commit format (`feat:`, `fix:`, `docs:`, etc.). The `GRM-N:` prefix is not allowed on branch commits — use it only in PR titles.
|
||||
|
||||
**Correct:**
|
||||
```
|
||||
```text
|
||||
feat: add new runner label option
|
||||
```
|
||||
|
||||
**Incorrect:**
|
||||
```
|
||||
```text
|
||||
GRM-33: add new runner label option
|
||||
update README
|
||||
```
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ elif [ -x "${HOME}/.pyenv/bin/pyenv" ]; then
|
||||
export PATH="${PYENV_ROOT}/bin:${PYENV_ROOT}/shims:${PATH}"
|
||||
eval "$("${PYENV_ROOT}/bin/pyenv" init -)" 2>/dev/null || true
|
||||
eval "$("${PYENV_ROOT}/bin/pyenv" virtualenv-init -)" 2>/dev/null || true
|
||||
pyenv activate gitea-runner-manager 2>/dev/null || true
|
||||
pyenv activate grm 2>/dev/null || true
|
||||
PY=python3
|
||||
else
|
||||
PY=python3
|
||||
|
||||
+15
-10
@@ -3,7 +3,7 @@ requires = ["setuptools>=61.0", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "gitea-runner-manager"
|
||||
name = "grm"
|
||||
dynamic = ["version"]
|
||||
description = "Lean CLI to manage Gitea Actions runners"
|
||||
readme = "README.md"
|
||||
@@ -20,10 +20,10 @@ dependencies = [
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
grm = "gitea_runner_manager.cli:cli"
|
||||
grm = "grm.cli:cli"
|
||||
|
||||
[tool.setuptools.dynamic]
|
||||
version = {attr = "gitea_runner_manager.__version__"}
|
||||
version = {attr = "grm.__version__"}
|
||||
|
||||
[project.optional-dependencies]
|
||||
# Minimal deps for CI scripts that only need click/dotenv
|
||||
@@ -34,7 +34,7 @@ ci = [
|
||||
"build==1.5.0",
|
||||
"twine==6.2.0",
|
||||
# Reusable CI/CD and dev tools (auto-merge, pr-review, pre-push checks, etc.)
|
||||
"devx==0.32.0",
|
||||
"devx==0.40.0",
|
||||
]
|
||||
# Lint and type-checking tools (quality job)
|
||||
lint = [
|
||||
@@ -52,9 +52,9 @@ molecule = [
|
||||
]
|
||||
# Full dev environment (local development, includes everything)
|
||||
dev = [
|
||||
"gitea-runner-manager[ci,lint,molecule]",
|
||||
"grm[ci,lint,molecule]",
|
||||
# Reusable CI/CD and dev tools (pre-push hooks, create-task, create-pr)
|
||||
"devx==0.32.0",
|
||||
"devx==0.40.0",
|
||||
# Non-Python dev dependency: checkmake (Makefile linter)
|
||||
# Install via: go install github.com/checkmake/checkmake/cmd/checkmake@latest
|
||||
]
|
||||
@@ -63,12 +63,12 @@ dev = [
|
||||
where = ["src"]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
gitea_runner_manager = ["translations.json"]
|
||||
grm = ["translations.json"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests", "scripts/tests"]
|
||||
pythonpath = ["src", "scripts"]
|
||||
addopts = "--cov=src/gitea_runner_manager --cov=scripts/prune_runner_images.py --cov-report=term-missing --cov-fail-under=100"
|
||||
addopts = "--cov=src/grm --cov=scripts/prune_runner_images.py --cov-report=term-missing --cov-fail-under=100"
|
||||
markers = [
|
||||
"integration: marks tests as integration tests (not counted in coverage)",
|
||||
]
|
||||
@@ -88,7 +88,7 @@ indent-style = "space"
|
||||
[tool.pyright]
|
||||
include = ["src"]
|
||||
pythonVersion = "3.12"
|
||||
strict = ["src/gitea_runner_manager"]
|
||||
strict = ["src/grm"]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Change classification — determines which changes trigger a release
|
||||
@@ -101,6 +101,11 @@ vikunja_project_id = 6
|
||||
repo_owner = "oblachno-oss"
|
||||
repo_name = "grm"
|
||||
|
||||
# grm doesn't have its own CI scripts — it uses devx's CI modules.
|
||||
# Skip CI script checks (doc_coverage would otherwise look for src/ci/).
|
||||
[tool.devx.doc_coverage]
|
||||
ci_scripts_dir = ""
|
||||
|
||||
# Molecule test weights for LPT scheduling.
|
||||
# GRM has a single role (gitea-runner) with 7 scenarios.
|
||||
# Weights are estimates — recalibrate from CI logs after next run.
|
||||
@@ -128,7 +133,7 @@ infrastructure = ["scripts/**"]
|
||||
# but are actually infrastructure:
|
||||
# - __init__.py: only contains __version__ (set by release.py, not user code)
|
||||
infrastructure_overrides = [
|
||||
"src/gitea_runner_manager/__init__.py",
|
||||
"src/grm/__init__.py",
|
||||
]
|
||||
|
||||
# User-facing overrides — safety override for broad infrastructure patterns
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""Gitea Runner Manager — lean CLI for managing Gitea Actions runners."""
|
||||
|
||||
__version__ = "0.14.1"
|
||||
__version__ = "0.18.0"
|
||||
@@ -125,8 +125,8 @@ def cli(ctx: click.Context, become_password_file: str | None, verbose: bool) ->
|
||||
@click.option(
|
||||
"--admin-token",
|
||||
"-a",
|
||||
default=lambda: os.getenv("CI_GITEA_TOKEN"),
|
||||
help=_("Gitea admin API token for integration test (env: CI_GITEA_TOKEN)"),
|
||||
default=lambda: os.getenv("CI_GITEA_API_TOKEN") or os.getenv("CI_GITEA_TOKEN"),
|
||||
help=_("Gitea admin API token for integration test (env: CI_GITEA_API_TOKEN or CI_GITEA_TOKEN)"),
|
||||
)
|
||||
@click.option(
|
||||
"--integration-retries",
|
||||
@@ -528,8 +528,10 @@ def list_runners(ask_become_pass: bool, no_status: bool) -> None:
|
||||
)
|
||||
@click.option(
|
||||
"--token",
|
||||
default=lambda: os.getenv("CI_GITEA_TOKEN"),
|
||||
help=_("Gitea API token (env: CI_GITEA_TOKEN)"),
|
||||
default=lambda: (
|
||||
os.getenv("DEVELOPER_GITEA_API_TOKEN") or os.getenv("CI_GITEA_API_TOKEN") or os.getenv("CI_GITEA_TOKEN")
|
||||
),
|
||||
help=_("Gitea API token (env: DEVELOPER_GITEA_API_TOKEN, CI_GITEA_API_TOKEN, or CI_GITEA_TOKEN)"),
|
||||
)
|
||||
@click.option(
|
||||
"--list",
|
||||
@@ -175,13 +175,13 @@
|
||||
"ru": "URL Gitea (env: GITEA_URL)",
|
||||
"zh": "Gitea URL(环境变量: GITEA_URL)"
|
||||
},
|
||||
"Gitea admin API token for integration test (env: CI_GITEA_TOKEN)": {
|
||||
"bg": "Gitea admin API токен за интеграционен тест (env: CI_GITEA_TOKEN)",
|
||||
"de": "Gitea-Admin-API-Token für Integrationstest (env: CI_GITEA_TOKEN)",
|
||||
"en": "Gitea admin API token for integration test (env: CI_GITEA_TOKEN)",
|
||||
"pl": "Token API administratora Gitea do testów integracyjnych (env: CI_GITEA_TOKEN)",
|
||||
"ru": "Токен админ API Gitea для интеграционного теста (env: CI_GITEA_TOKEN)",
|
||||
"zh": "Gitea 管理员 API 令牌,用于集成测试(环境变量: CI_GITEA_TOKEN)"
|
||||
"Gitea admin API token for integration test (env: CI_GITEA_API_TOKEN or CI_GITEA_TOKEN)": {
|
||||
"bg": "Gitea admin API token for integration test (env: CI_GITEA_API_TOKEN or CI_GITEA_TOKEN)",
|
||||
"de": "Gitea admin API token for integration test (env: CI_GITEA_API_TOKEN or CI_GITEA_TOKEN)",
|
||||
"en": "Gitea admin API token for integration test (env: CI_GITEA_API_TOKEN or CI_GITEA_TOKEN)",
|
||||
"pl": "Gitea admin API token for integration test (env: CI_GITEA_API_TOKEN or CI_GITEA_TOKEN)",
|
||||
"ru": "Gitea admin API token for integration test (env: CI_GITEA_API_TOKEN or CI_GITEA_TOKEN)",
|
||||
"zh": "Gitea admin API token for integration test(环境变量: CI_GITEA_API_TOKEN or CI_GITEA_TOKEN)"
|
||||
},
|
||||
"HEALTHY": {
|
||||
"bg": "ЗДРАВ",
|
||||
@@ -703,13 +703,13 @@
|
||||
"ru": "Git ref для запуска workflow (по умолчанию: master)",
|
||||
"zh": "运行工作流的 Git ref(默认:master)"
|
||||
},
|
||||
"Gitea API token (env: CI_GITEA_TOKEN)": {
|
||||
"bg": "Gitea API токен (env: CI_GITEA_TOKEN)",
|
||||
"de": "Gitea API-Token (env: CI_GITEA_TOKEN)",
|
||||
"en": "Gitea API token (env: CI_GITEA_TOKEN)",
|
||||
"pl": "Token API Gitea (env: CI_GITEA_TOKEN)",
|
||||
"ru": "Токен API Gitea (env: CI_GITEA_TOKEN)",
|
||||
"zh": "Gitea API 令牌(环境变量:CI_GITEA_TOKEN)"
|
||||
"Gitea API token (env: DEVELOPER_GITEA_API_TOKEN, CI_GITEA_API_TOKEN, or CI_GITEA_TOKEN)": {
|
||||
"bg": "Gitea API token (env: DEVELOPER_GITEA_API_TOKEN, CI_GITEA_API_TOKEN, or CI_GITEA_TOKEN)",
|
||||
"de": "Gitea API token (env: DEVELOPER_GITEA_API_TOKEN, CI_GITEA_API_TOKEN, or CI_GITEA_TOKEN)",
|
||||
"en": "Gitea API token (env: DEVELOPER_GITEA_API_TOKEN, CI_GITEA_API_TOKEN, or CI_GITEA_TOKEN)",
|
||||
"pl": "Gitea API token (env: DEVELOPER_GITEA_API_TOKEN, CI_GITEA_API_TOKEN, or CI_GITEA_TOKEN)",
|
||||
"ru": "Gitea API token (env: DEVELOPER_GITEA_API_TOKEN, CI_GITEA_API_TOKEN, or CI_GITEA_TOKEN)",
|
||||
"zh": "Gitea API token(环境变量:DEVELOPER_GITEA_API_TOKEN, CI_GITEA_API_TOKEN, or CI_GITEA_TOKEN)"
|
||||
},
|
||||
"List available workflows instead of triggering one": {
|
||||
"bg": "Списък на наличните работни процеси вместо изпълнение",
|
||||
@@ -5,14 +5,14 @@ from unittest.mock import MagicMock, patch
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
|
||||
from gitea_runner_manager.cli import cli
|
||||
from grm.cli import cli
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestLifecycleCLI:
|
||||
"""Test the full lifecycle CLI commands end-to-end."""
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_install_start_status_stop_disable_remove(self, mock_manager_class: MagicMock) -> None:
|
||||
"""Exercise the full lifecycle via CLI."""
|
||||
mock_manager = MagicMock()
|
||||
|
||||
@@ -5,14 +5,14 @@ from unittest.mock import MagicMock, patch
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
|
||||
from gitea_runner_manager.cli import cli
|
||||
from grm.cli import cli
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestMultiInstanceCLI:
|
||||
"""Test that multiple runner instances can be managed independently."""
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_install_two_instances(self, mock_manager_class: MagicMock) -> None:
|
||||
"""Install two named instances on the same host."""
|
||||
mock_manager = MagicMock()
|
||||
@@ -34,7 +34,7 @@ class TestMultiInstanceCLI:
|
||||
assert calls[0].kwargs["name"] == "runner-a"
|
||||
assert calls[1].kwargs["name"] == "runner-b"
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_start_stop_one_instance(self, mock_manager_class: MagicMock) -> None:
|
||||
"""Start one instance and stop another independently."""
|
||||
mock_manager = MagicMock()
|
||||
|
||||
+88
-83
@@ -6,10 +6,15 @@ from unittest.mock import MagicMock, patch
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
|
||||
from gitea_runner_manager import __version__
|
||||
from gitea_runner_manager.cli import cli
|
||||
from grm import __version__
|
||||
from grm.cli import cli
|
||||
|
||||
_TEST_ENV = {"GITEA_URL": "https://git.example.com", "CI_GITEA_TOKEN": ""}
|
||||
_TEST_ENV = {
|
||||
"GITEA_URL": "https://git.example.com",
|
||||
"DEVELOPER_GITEA_API_TOKEN": "",
|
||||
"CI_GITEA_API_TOKEN": "",
|
||||
"CI_GITEA_TOKEN": "",
|
||||
}
|
||||
|
||||
|
||||
class TestCLI:
|
||||
@@ -27,7 +32,7 @@ class TestCLI:
|
||||
assert result.exit_code == 0
|
||||
assert __version__ in result.output
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_install(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
mock_manager_class.return_value = mock_manager
|
||||
@@ -50,7 +55,7 @@ class TestCLI:
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_install_no_ask_become_pass(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
mock_manager_class.return_value = mock_manager
|
||||
@@ -73,10 +78,10 @@ class TestCLI:
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_install_missing_url(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
from gitea_runner_manager.exceptions import AnsibleError
|
||||
from grm.exceptions import AnsibleError
|
||||
|
||||
mock_manager.install.side_effect = AnsibleError("GITEA_URL must be set (or pass --url)")
|
||||
mock_manager_class.return_value = mock_manager
|
||||
@@ -87,7 +92,7 @@ class TestCLI:
|
||||
assert result.exit_code != 0
|
||||
assert "GITEA_URL must be set" in result.output
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_install_with_url_flag(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
mock_manager_class.return_value = mock_manager
|
||||
@@ -114,10 +119,10 @@ class TestCLI:
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_install_missing_token(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
from gitea_runner_manager.exceptions import AnsibleError
|
||||
from grm.exceptions import AnsibleError
|
||||
|
||||
mock_manager.install.side_effect = AnsibleError("GITEA_REGISTRATION_TOKEN must be set (or pass --token)")
|
||||
mock_manager_class.return_value = mock_manager
|
||||
@@ -128,7 +133,7 @@ class TestCLI:
|
||||
assert result.exit_code != 0
|
||||
assert "GITEA_REGISTRATION_TOKEN must be set" in result.output
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_install_with_options(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
mock_manager_class.return_value = mock_manager
|
||||
@@ -165,7 +170,7 @@ class TestCLI:
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_install_ask_become_pass(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
mock_manager_class.return_value = mock_manager
|
||||
@@ -188,10 +193,10 @@ class TestCLI:
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_install_error(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
from gitea_runner_manager.exceptions import AnsibleError
|
||||
from grm.exceptions import AnsibleError
|
||||
|
||||
mock_manager.install.side_effect = AnsibleError("fail")
|
||||
mock_manager_class.return_value = mock_manager
|
||||
@@ -201,7 +206,7 @@ class TestCLI:
|
||||
assert result.exit_code != 0
|
||||
assert "fail" in result.output
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_install_with_labels(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
mock_manager_class.return_value = mock_manager
|
||||
@@ -226,7 +231,7 @@ class TestCLI:
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_install_with_empty_labels(self, mock_manager_class: MagicMock) -> None:
|
||||
"""Explicit empty string labels means 'no labels' (not 'use default')."""
|
||||
mock_manager = MagicMock()
|
||||
@@ -250,7 +255,7 @@ class TestCLI:
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_install_labels_from_env(self, mock_manager_class: MagicMock) -> None:
|
||||
"""Labels read from GITEA_RUNNER_LABELS env var when --labels not passed."""
|
||||
mock_manager = MagicMock()
|
||||
@@ -274,7 +279,7 @@ class TestCLI:
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_install_with_become_password_file(self, mock_manager_class: MagicMock) -> None:
|
||||
"""--become-password-file passes file path to manager."""
|
||||
import tempfile
|
||||
@@ -311,7 +316,7 @@ class TestCLI:
|
||||
|
||||
os.unlink(pw_file)
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_install_verbose(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
mock_manager_class.return_value = mock_manager
|
||||
@@ -334,7 +339,7 @@ class TestCLI:
|
||||
verbose=True,
|
||||
)
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_update(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
mock_manager_class.return_value = mock_manager
|
||||
@@ -364,7 +369,7 @@ class TestCLI:
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_update_ask_become_pass(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
mock_manager_class.return_value = mock_manager
|
||||
@@ -382,10 +387,10 @@ class TestCLI:
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_update_error(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
from gitea_runner_manager.exceptions import AnsibleError
|
||||
from grm.exceptions import AnsibleError
|
||||
|
||||
mock_manager.update.side_effect = AnsibleError("fail")
|
||||
mock_manager_class.return_value = mock_manager
|
||||
@@ -395,7 +400,7 @@ class TestCLI:
|
||||
assert result.exit_code != 0
|
||||
assert "fail" in result.output
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_start(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
mock_manager_class.return_value = mock_manager
|
||||
@@ -413,7 +418,7 @@ class TestCLI:
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_start_with_override(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
mock_manager_class.return_value = mock_manager
|
||||
@@ -431,7 +436,7 @@ class TestCLI:
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_stop(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
mock_manager_class.return_value = mock_manager
|
||||
@@ -449,7 +454,7 @@ class TestCLI:
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_restart(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
mock_manager_class.return_value = mock_manager
|
||||
@@ -467,7 +472,7 @@ class TestCLI:
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_enable(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
mock_manager_class.return_value = mock_manager
|
||||
@@ -485,7 +490,7 @@ class TestCLI:
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_disable(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
mock_manager_class.return_value = mock_manager
|
||||
@@ -505,10 +510,10 @@ class TestCLI:
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_disable_missing_url(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
from gitea_runner_manager.exceptions import AnsibleError
|
||||
from grm.exceptions import AnsibleError
|
||||
|
||||
mock_manager.disable.side_effect = AnsibleError("GITEA_URL must be set (or pass --url)")
|
||||
mock_manager_class.return_value = mock_manager
|
||||
@@ -519,7 +524,7 @@ class TestCLI:
|
||||
assert result.exit_code != 0
|
||||
assert "GITEA_URL must be set" in result.output
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_disable_with_url_flag(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
mock_manager_class.return_value = mock_manager
|
||||
@@ -540,10 +545,10 @@ class TestCLI:
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_disable_error(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
from gitea_runner_manager.exceptions import AnsibleError
|
||||
from grm.exceptions import AnsibleError
|
||||
|
||||
mock_manager.disable.side_effect = AnsibleError("fail")
|
||||
mock_manager_class.return_value = mock_manager
|
||||
@@ -553,7 +558,7 @@ class TestCLI:
|
||||
assert result.exit_code != 0
|
||||
assert "fail" in result.output
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_status(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
mock_manager_class.return_value = mock_manager
|
||||
@@ -571,7 +576,7 @@ class TestCLI:
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_remove(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
mock_manager_class.return_value = mock_manager
|
||||
@@ -592,7 +597,7 @@ class TestCLI:
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_remove_force_flag(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
mock_manager_class.return_value = mock_manager
|
||||
@@ -613,10 +618,10 @@ class TestCLI:
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_start_error(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
from gitea_runner_manager.exceptions import AnsibleError
|
||||
from grm.exceptions import AnsibleError
|
||||
|
||||
mock_manager.start.side_effect = AnsibleError("fail")
|
||||
mock_manager_class.return_value = mock_manager
|
||||
@@ -626,10 +631,10 @@ class TestCLI:
|
||||
assert result.exit_code != 0
|
||||
assert "fail" in result.output
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_stop_error(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
from gitea_runner_manager.exceptions import AnsibleError
|
||||
from grm.exceptions import AnsibleError
|
||||
|
||||
mock_manager.stop.side_effect = AnsibleError("fail")
|
||||
mock_manager_class.return_value = mock_manager
|
||||
@@ -639,10 +644,10 @@ class TestCLI:
|
||||
assert result.exit_code != 0
|
||||
assert "fail" in result.output
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_enable_error(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
from gitea_runner_manager.exceptions import AnsibleError
|
||||
from grm.exceptions import AnsibleError
|
||||
|
||||
mock_manager.enable.side_effect = AnsibleError("fail")
|
||||
mock_manager_class.return_value = mock_manager
|
||||
@@ -652,10 +657,10 @@ class TestCLI:
|
||||
assert result.exit_code != 0
|
||||
assert "fail" in result.output
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_status_error(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
from gitea_runner_manager.exceptions import AnsibleError
|
||||
from grm.exceptions import AnsibleError
|
||||
|
||||
mock_manager.status.side_effect = AnsibleError("fail")
|
||||
mock_manager_class.return_value = mock_manager
|
||||
@@ -665,10 +670,10 @@ class TestCLI:
|
||||
assert result.exit_code != 0
|
||||
assert "fail" in result.output
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_remove_missing_url(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
from gitea_runner_manager.exceptions import AnsibleError
|
||||
from grm.exceptions import AnsibleError
|
||||
|
||||
mock_manager.remove.side_effect = AnsibleError("GITEA_URL must be set (or pass --url)")
|
||||
mock_manager_class.return_value = mock_manager
|
||||
@@ -679,7 +684,7 @@ class TestCLI:
|
||||
assert result.exit_code != 0
|
||||
assert "GITEA_URL must be set" in result.output
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_remove_with_url_flag(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
mock_manager_class.return_value = mock_manager
|
||||
@@ -701,10 +706,10 @@ class TestCLI:
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_remove_error(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
from gitea_runner_manager.exceptions import AnsibleError
|
||||
from grm.exceptions import AnsibleError
|
||||
|
||||
mock_manager.remove.side_effect = AnsibleError("fail")
|
||||
mock_manager_class.return_value = mock_manager
|
||||
@@ -714,7 +719,7 @@ class TestCLI:
|
||||
assert result.exit_code != 0
|
||||
assert "fail" in result.output
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_list(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
mock_manager.list_runners.return_value = [
|
||||
@@ -736,7 +741,7 @@ class TestCLI:
|
||||
assert "active" in result.output
|
||||
mock_manager.list_runners.assert_called_once_with(become_pass=None, no_status=False)
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_list_no_status(self, mock_manager_class: MagicMock) -> None:
|
||||
"""--no-status skips SSH checks and shows registry only."""
|
||||
mock_manager = MagicMock()
|
||||
@@ -758,22 +763,22 @@ class TestCLI:
|
||||
assert "n/a" in result.output
|
||||
mock_manager.list_runners.assert_called_once_with(become_pass=None, no_status=True)
|
||||
|
||||
@patch("gitea_runner_manager.cli.click.prompt", return_value="secret")
|
||||
@patch("gitea_runner_manager.cli.sys.stdin")
|
||||
@patch("grm.cli.click.prompt", return_value="secret")
|
||||
@patch("grm.cli.sys.stdin")
|
||||
def test_collect_become_pass_tty(self, mock_stdin: MagicMock, mock_prompt: MagicMock) -> None:
|
||||
from gitea_runner_manager.cli import _collect_become_pass
|
||||
from grm.cli import _collect_become_pass
|
||||
|
||||
mock_stdin.isatty.return_value = True
|
||||
assert _collect_become_pass(ask_become_pass=True) == "secret"
|
||||
|
||||
@patch("gitea_runner_manager.cli.sys.stdin")
|
||||
@patch("grm.cli.sys.stdin")
|
||||
def test_collect_become_pass_no_ask(self, mock_stdin: MagicMock) -> None:
|
||||
from gitea_runner_manager.cli import _collect_become_pass
|
||||
from grm.cli import _collect_become_pass
|
||||
|
||||
mock_stdin.isatty.return_value = True
|
||||
assert _collect_become_pass(ask_become_pass=False) is None
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_list_with_piped_become_pass(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
mock_manager.list_runners.return_value = []
|
||||
@@ -784,7 +789,7 @@ class TestCLI:
|
||||
assert result.exit_code == 0
|
||||
mock_manager.list_runners.assert_called_once_with(become_pass="secret", no_status=False)
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_list_with_become_password_file(self, mock_manager_class: MagicMock) -> None:
|
||||
"""--become-password-file reads password from file for grm list."""
|
||||
import os
|
||||
@@ -806,7 +811,7 @@ class TestCLI:
|
||||
finally:
|
||||
os.unlink(pw_file)
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_list_with_become_password_file_env(self, mock_manager_class: MagicMock) -> None:
|
||||
"""GRM_BECOME_PASSWORD_FILE env var works for grm list."""
|
||||
import os
|
||||
@@ -828,7 +833,7 @@ class TestCLI:
|
||||
finally:
|
||||
os.unlink(pw_file)
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_list_empty(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
mock_manager.list_runners.return_value = []
|
||||
@@ -839,10 +844,10 @@ class TestCLI:
|
||||
assert result.exit_code == 0
|
||||
assert "No runners registered" in result.output
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_list_error(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
from gitea_runner_manager.exceptions import AnsibleError
|
||||
from grm.exceptions import AnsibleError
|
||||
|
||||
mock_manager.list_runners.side_effect = AnsibleError("fail")
|
||||
mock_manager_class.return_value = mock_manager
|
||||
@@ -852,7 +857,7 @@ class TestCLI:
|
||||
assert result.exit_code != 0
|
||||
assert "fail" in result.output
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_health_all_healthy(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
mock_manager.health.return_value = [
|
||||
@@ -868,7 +873,7 @@ class TestCLI:
|
||||
assert "r2" in result.output
|
||||
assert "yes" in result.output
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_health_with_unhealthy(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
mock_manager.health.return_value = [
|
||||
@@ -883,7 +888,7 @@ class TestCLI:
|
||||
assert "unhealthy" in result.output.lower()
|
||||
assert "r2" in result.output
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_health_single_runner(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
mock_manager.health.return_value = [
|
||||
@@ -897,7 +902,7 @@ class TestCLI:
|
||||
assert "r1" in result.output
|
||||
mock_manager.health.assert_called_once()
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_health_empty(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
mock_manager.health.return_value = []
|
||||
@@ -908,10 +913,10 @@ class TestCLI:
|
||||
assert result.exit_code == 0
|
||||
assert "No runners registered" in result.output
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_health_error(self, mock_manager_class: MagicMock) -> None:
|
||||
mock_manager = MagicMock()
|
||||
from gitea_runner_manager.exceptions import AnsibleError
|
||||
from grm.exceptions import AnsibleError
|
||||
|
||||
mock_manager.health.side_effect = AnsibleError("fail")
|
||||
mock_manager_class.return_value = mock_manager
|
||||
@@ -921,8 +926,8 @@ class TestCLI:
|
||||
assert result.exit_code != 0
|
||||
assert "fail" in result.output
|
||||
|
||||
@patch("gitea_runner_manager.cli.os.getlogin", side_effect=OSError("no tty"))
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.os.getlogin", side_effect=OSError("no tty"))
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_default_user_fallback_on_getlogin_error(
|
||||
self, mock_manager_class: MagicMock, mock_getlogin: MagicMock
|
||||
) -> None:
|
||||
@@ -937,8 +942,8 @@ class TestCLI:
|
||||
call_kwargs = mock_manager.install.call_args.kwargs
|
||||
assert call_kwargs["user"] == "testuser"
|
||||
|
||||
@patch("gitea_runner_manager.cli.os.getlogin", side_effect=OSError("no tty"))
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.os.getlogin", side_effect=OSError("no tty"))
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_default_user_fallback_to_root(self, mock_manager_class: MagicMock, mock_getlogin: MagicMock) -> None:
|
||||
"""os.getlogin() failure with no USER env falls back to 'root'."""
|
||||
mock_manager = MagicMock()
|
||||
@@ -951,7 +956,7 @@ class TestCLI:
|
||||
call_kwargs = mock_manager.install.call_args.kwargs
|
||||
assert call_kwargs["user"] == "root"
|
||||
|
||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||
@patch("grm.cli.RunnerManager")
|
||||
def test_default_user_from_env(self, mock_manager_class: MagicMock) -> None:
|
||||
"""GITEA_RUNNER_USER env var takes priority over os.getlogin()."""
|
||||
mock_manager = MagicMock()
|
||||
@@ -970,20 +975,20 @@ class TestCLI:
|
||||
|
||||
def test_get_verbose_no_context(self) -> None:
|
||||
"""_get_verbose returns False when called outside Click context."""
|
||||
from gitea_runner_manager.cli import _get_verbose
|
||||
from grm.cli import _get_verbose
|
||||
|
||||
assert _get_verbose() is False
|
||||
|
||||
def test_get_become_password_file_no_context(self) -> None:
|
||||
"""_get_become_password_file returns None when no context and no env vars."""
|
||||
from gitea_runner_manager.cli import _get_become_password_file
|
||||
from grm.cli import _get_become_password_file
|
||||
|
||||
with patch.dict("os.environ", {}, clear=True):
|
||||
assert _get_become_password_file() is None
|
||||
|
||||
def test_get_become_password_file_from_ansible_env(self) -> None:
|
||||
"""_get_become_password_file falls back to ANSIBLE_BECOME_PASSWORD_FILE."""
|
||||
from gitea_runner_manager.cli import _get_become_password_file
|
||||
from grm.cli import _get_become_password_file
|
||||
|
||||
with patch.dict("os.environ", {"ANSIBLE_BECOME_PASSWORD_FILE": "/tmp/ansible.txt"}, clear=True):
|
||||
assert _get_become_password_file() == "/tmp/ansible.txt"
|
||||
@@ -994,7 +999,7 @@ class TestTriggerWorkflow:
|
||||
|
||||
def test_trigger_workflow_success(self) -> None:
|
||||
runner = CliRunner(env=_TEST_ENV)
|
||||
with patch("gitea_runner_manager.cli.GiteaWorkflowClient") as mock_client_cls:
|
||||
with patch("grm.cli.GiteaWorkflowClient") as mock_client_cls:
|
||||
mock_client = MagicMock()
|
||||
mock_client_cls.return_value = mock_client
|
||||
mock_client.dispatch_workflow.return_value = {
|
||||
@@ -1022,7 +1027,7 @@ class TestTriggerWorkflow:
|
||||
|
||||
def test_trigger_workflow_list(self) -> None:
|
||||
runner = CliRunner(env=_TEST_ENV)
|
||||
with patch("gitea_runner_manager.cli.GiteaWorkflowClient") as mock_client_cls:
|
||||
with patch("grm.cli.GiteaWorkflowClient") as mock_client_cls:
|
||||
mock_client = MagicMock()
|
||||
mock_client_cls.return_value = mock_client
|
||||
mock_client.list_workflows.return_value = [
|
||||
@@ -1037,7 +1042,7 @@ class TestTriggerWorkflow:
|
||||
|
||||
def test_trigger_workflow_list_empty(self) -> None:
|
||||
runner = CliRunner(env=_TEST_ENV)
|
||||
with patch("gitea_runner_manager.cli.GiteaWorkflowClient") as mock_client_cls:
|
||||
with patch("grm.cli.GiteaWorkflowClient") as mock_client_cls:
|
||||
mock_client = MagicMock()
|
||||
mock_client_cls.return_value = mock_client
|
||||
mock_client.list_workflows.return_value = []
|
||||
@@ -1052,10 +1057,10 @@ class TestTriggerWorkflow:
|
||||
assert "WORKFLOW_ID" in result.output
|
||||
|
||||
def test_trigger_workflow_api_error(self) -> None:
|
||||
from gitea_runner_manager.gitea_client import GiteaAPIError
|
||||
from grm.gitea_client import GiteaAPIError
|
||||
|
||||
runner = CliRunner(env=_TEST_ENV)
|
||||
with patch("gitea_runner_manager.cli.GiteaWorkflowClient") as mock_client_cls:
|
||||
with patch("grm.cli.GiteaWorkflowClient") as mock_client_cls:
|
||||
mock_client = MagicMock()
|
||||
mock_client_cls.return_value = mock_client
|
||||
mock_client.dispatch_workflow.side_effect = GiteaAPIError(404, "workflow not found")
|
||||
@@ -1065,7 +1070,7 @@ class TestTriggerWorkflow:
|
||||
|
||||
def test_trigger_workflow_custom_repo_and_ref(self) -> None:
|
||||
runner = CliRunner(env=_TEST_ENV)
|
||||
with patch("gitea_runner_manager.cli.GiteaWorkflowClient") as mock_client_cls:
|
||||
with patch("grm.cli.GiteaWorkflowClient") as mock_client_cls:
|
||||
mock_client = MagicMock()
|
||||
mock_client_cls.return_value = mock_client
|
||||
mock_client.dispatch_workflow.return_value = None
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user