Compare commits

...
272 Commits
Author SHA1 Message Date
devx-ci-bot 79830b52e7 release: v0.44.0 [skip ci] 2026-07-13 23:55:52 +00:00
emil ddfbdec956 DEVX-136: feat: add fix_pr_title module and update_pr API method
Post-merge / detect-and-configure (push) Successful in 12s
Post-merge / release-and-maintain (push) Successful in 1m0s
2026-07-13 23:55:11 +00:00
gitea-actions-bot 68f0872134 chore: update badge URLs to commit 08f1c46f [skip ci] 2026-07-13 05:03:21 +00:00
devx-ci-bot 888cc4e3b2 release: v0.43.0 [skip ci] 2026-07-13 05:02:47 +00:00
emil f08ff0e7a3 DEVX-135: feat: add get_customer_vm_ip and get_observability_vm_ip to I/O check
Post-merge / detect-and-configure (push) Successful in 13s
Post-merge / release-and-maintain (push) Successful in 1m1s
2026-07-13 05:02:03 +00:00
gitea-actions-bot 772e1b1c6d chore: update badge URLs to commit 29e3ef9c [skip ci] 2026-07-13 02:59:23 +00:00
devx-ci-bot bdfe2c561b release: v0.42.0 [skip ci] 2026-07-13 02:58:46 +00:00
emil 02b27dd343 DEVX-134: feat: add I/O function isolation check and skip integration tests
Post-merge / detect-and-configure (push) Successful in 16s
Post-merge / release-and-maintain (push) Successful in 1m7s
2026-07-13 02:57:54 +00:00
gitea-actions-bot e5488fcfbd chore: update badge URLs to commit ae591a0c [skip ci] 2026-07-13 02:27:15 +00:00
devx-ci-bot 1a60739b5a release: v0.41.2 [skip ci] 2026-07-13 02:26:42 +00:00
emil 50dcb67083 DEVX-133: fix: auto-discover molecule root instead of hardcoding gitea-runner
Post-merge / detect-and-configure (push) Successful in 13s
Post-merge / release-and-maintain (push) Successful in 59s
2026-07-13 02:25:59 +00:00
gitea-actions-bot 7b624b0525 chore: update badge URLs to commit 9175bdc9 [skip ci] 2026-07-13 01:39:18 +00:00
devx-ci-bot 570de94575 release: v0.41.1 [skip ci] 2026-07-13 01:38:46 +00:00
emil 55583fe399 DEVX-132: fix: check_test_isolation accepts multiple --test-path values
Post-merge / detect-and-configure (push) Successful in 13s
Post-merge / release-and-maintain (push) Successful in 1m0s
2026-07-13 01:38:04 +00:00
gitea-actions-bot 35f4fb7172 chore: update badge URLs to commit 691cdd2c [skip ci] 2026-07-13 01:20:22 +00:00
emil b3d47753a8 DEVX-131: ci: fix build-images skipping on release commits via workflow_dispatch
Post-merge / detect-and-configure (push) Successful in 12s
Post-merge / release-and-maintain (push) Successful in 42s
2026-07-13 01:19:24 +00:00
emil 945b45b641 release: v0.41.0 [skip ci] 2026-07-13 03:10:59 +02:00
gitea-actions-bot 9e59acd485 chore: update badge URLs to commit 6b281bd3 [skip ci] 2026-07-13 01:06:16 +00:00
emil f44b321f37 DEVX-129: test: cover crypto.py line 37 (retry on leading dash)
Post-merge / detect-and-configure (push) Successful in 13s
Post-merge / release-and-maintain (push) Successful in 39s
2026-07-13 01:05:20 +00:00
emil 77c2f7e043 DEVX-129: feat: test isolation pytest plugin, shift-left quality gates, dep upgrades
Post-merge / detect-and-configure (push) Successful in 11s
Post-merge / release-and-maintain (push) Failing after 27s
2026-07-13 00:57:28 +00:00
gitea-actions-bot b923e47d81 chore: update badge URLs to commit f13acf06 [skip ci] 2026-07-12 20:02:08 +00:00
emil 63204c7cb0 DEVX-128: docs: add retrospective for self-approval fallback and CI consolidation
Post-merge / detect-and-configure (push) Successful in 29s
Post-merge / release-and-maintain (push) Successful in 1m10s
2026-07-12 20:00:30 +00:00
gitea-actions-bot 0c7837fb0e chore: update badge URLs to commit 51c7146d [skip ci] 2026-07-12 16:35:39 +00:00
devx-ci-bot 59d6fa1833 release: v0.40.1 [skip ci] 2026-07-12 16:34:45 +00:00
emil d035b620e0 DEVX-127: fix: fall back to CI token when reviewer self-approval is rejected
Post-merge / detect-and-configure (push) Successful in 17s
Post-merge / release-and-maintain (push) Successful in 1m25s
2026-07-12 16:33:53 +00:00
gitea-actions-bot 5987adee64 chore: update badge URLs to commit a22225af [skip ci] 2026-07-12 01:53:50 +00:00
emil cb84dae050 DEVX-126: ci: consolidate CI and post-merge workflows
Post-merge / detect-and-configure (push) Successful in 20s
Post-merge / release-and-maintain (push) Successful in 46s
2026-07-12 01:52:40 +00:00
gitea-actions-bot ed0dfce98b chore: update badge URLs to commit 2747061d [skip ci] 2026-07-11 23:09:11 +00:00
devx-ci-bot c244881f22 release: v0.40.0 [skip ci] 2026-07-11 23:08:23 +00:00
emil 4cde7de696 DEVX-125: feat: detect double-prefix in Vikunja task title during pre-merge validation
Post-merge / detect-type (push) Successful in 8s
Post-merge / validate-commit-msg (push) Successful in 8s
Post-merge / sync-wiki (push) Successful in 21s
Post-merge / release (push) Successful in 30s
Post-merge / vikunja (push) Successful in 13s
Post-merge / configure-repo (push) Successful in 10s
Post-merge / publish (push) Successful in 19s
Post-merge / badges (push) Successful in 41s
2026-07-11 23:07:45 +00:00
gitea-actions-bot d675889604 chore: update badge URLs to commit c9f25c13 [skip ci] 2026-07-09 11:54:42 +00:00
devx-ci-bot e23138e731 release: v0.39.0 [skip ci] 2026-07-09 11:53:12 +00:00
emil ef3b882e5b DEVX-124: feat: extract shared utilities from infra and grm into devx
Post-merge / detect-type (push) Successful in 13s
Post-merge / validate-commit-msg (push) Successful in 10s
Post-merge / configure-repo (push) Successful in 28s
Post-merge / vikunja (push) Successful in 44s
Post-merge / sync-wiki (push) Successful in 58s
Post-merge / release (push) Successful in 1m7s
Post-merge / publish (push) Successful in 44s
Post-merge / badges (push) Successful in 1m6s
2026-07-09 11:51:50 +00:00
gitea-actions-bot 8d9ee1ea26 chore: update badge URLs to commit 931a4a37 [skip ci] 2026-07-08 20:20:51 +00:00
emil 1497b29487 DEVX-123: ci: retrigger workflow after configuring secrets
Post-merge / detect-type (push) Successful in 9s
Post-merge / validate-commit-msg (push) Successful in 12s
Post-merge / release (push) Successful in 19s
Post-merge / configure-repo (push) Successful in 15s
Post-merge / vikunja (push) Successful in 17s
Post-merge / publish (push) Has been skipped
Post-merge / sync-wiki (push) Successful in 26s
Post-merge / badges (push) Successful in 31s
2026-07-08 20:19:44 +00:00
gitea-actions-bot cb126e83da chore: update badge URLs to commit 37543185 [skip ci] 2026-07-08 19:31:39 +00:00
devx-ci-bot 281193c741 release: v0.38.0 [skip ci] 2026-07-08 19:30:58 +00:00
emil 0228fce5b9 DEVX-123: feat: introduce role-based Gitea API token environment variables
Post-merge / detect-type (push) Successful in 10s
Post-merge / validate-commit-msg (push) Successful in 10s
Post-merge / configure-repo (push) Successful in 11s
Post-merge / sync-wiki (push) Successful in 17s
Post-merge / vikunja (push) Successful in 18s
Post-merge / release (push) Successful in 36s
Post-merge / publish (push) Successful in 20s
Post-merge / badges (push) Successful in 35s
2026-07-08 19:30:10 +00:00
gitea-actions-bot 981d3e41cc chore: update badge URLs to commit fe187115 [skip ci] 2026-07-07 22:02:05 +00:00
devx-ci-bot 3cd2459eef release: v0.37.0 [skip ci] 2026-07-07 22:01:14 +00:00
emil ef08513bcf DEVX-122: feat: consolidate docs checks into devx-docs-check target
Post-merge / detect-type (push) Successful in 19s
Post-merge / validate-commit-msg (push) Successful in 10s
Post-merge / vikunja (push) Successful in 24s
Post-merge / configure-repo (push) Successful in 25s
Post-merge / sync-wiki (push) Successful in 32s
Post-merge / release (push) Successful in 43s
Post-merge / publish (push) Successful in 21s
Post-merge / badges (push) Successful in 38s
2026-07-07 22:00:07 +00:00
gitea-actions-bot 05922eca2f chore: update badge URLs to commit bff19e05 [skip ci] 2026-07-07 15:53:21 +00:00
devx-ci-bot 05de2b0aa9 release: v0.36.2 [skip ci] 2026-07-07 15:52:35 +00:00
emil 6a463a93d2 DEVX-121: fix: GiteaClient.set_repo_variable uses PUT instead of PATCH
Post-merge / detect-type (push) Successful in 9s
Post-merge / validate-commit-msg (push) Successful in 9s
Post-merge / vikunja (push) Successful in 22s
Post-merge / configure-repo (push) Successful in 17s
Post-merge / sync-wiki (push) Successful in 31s
Post-merge / release (push) Successful in 38s
Post-merge / publish (push) Successful in 23s
Post-merge / badges (push) Successful in 39s
2026-07-07 15:51:45 +00:00
gitea-actions-bot ad7b52c368 chore: update badge URLs to commit d9423d85 [skip ci] 2026-07-07 12:05:28 +00:00
devx-ci-bot 6b81e1a50a release: v0.36.1 [skip ci] 2026-07-07 12:04:41 +00:00
emil 443dc01b4e DEVX-120: fix: preserve .badges/ dir during git clean in push_badges
Post-merge / detect-type (push) Successful in 9s
Post-merge / validate-commit-msg (push) Successful in 10s
Post-merge / configure-repo (push) Successful in 16s
Post-merge / vikunja (push) Successful in 21s
Post-merge / sync-wiki (push) Successful in 30s
Post-merge / release (push) Successful in 40s
Post-merge / publish (push) Successful in 25s
Post-merge / badges (push) Successful in 41s
2026-07-07 12:03:51 +00:00
devx-ci-bot 1d7bf7118a release: v0.36.0 [skip ci] 2026-07-07 11:58:00 +00:00
emil f98534ebe2 DEVX-119: feat: add GiteaClient repo variable methods and parallelize pytest-cov
Post-merge / detect-type (push) Successful in 12s
Post-merge / validate-commit-msg (push) Successful in 11s
Post-merge / configure-repo (push) Successful in 17s
Post-merge / vikunja (push) Successful in 23s
Post-merge / sync-wiki (push) Successful in 29s
Post-merge / release (push) Successful in 43s
Post-merge / publish (push) Successful in 23s
Post-merge / badges (push) Failing after 31s
2026-07-07 11:57:04 +00:00
emilandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> c62b168b25 DEVX-116: chore: update grm package name references
Post-merge / detect-type (push) Successful in 10s
Post-merge / validate-commit-msg (push) Successful in 10s
Post-merge / configure-repo (push) Successful in 15s
Post-merge / vikunja (push) Successful in 22s
Post-merge / release (push) Successful in 23s
Post-merge / publish (push) Has been skipped
Post-merge / sync-wiki (push) Successful in 30s
Post-merge / badges (push) Failing after 30s
Update hardcoded path and docstring examples from
`gitea_runner_manager` to `grm` after the package rename in grm PR #203.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-06 15:51:10 +02:00
devx-ci-bot 40a94df029 release: v0.35.7 [skip ci] 2026-07-06 13:22:27 +00:00
emilandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> f50c4c1e00 DEVX-118: fix: use Gitea wiki dash-marker filename convention
Post-merge / detect-type (push) Successful in 10s
Post-merge / validate-commit-msg (push) Successful in 11s
Post-merge / vikunja (push) Successful in 24s
Post-merge / configure-repo (push) Successful in 16s
Post-merge / sync-wiki (push) Successful in 35s
Post-merge / release (push) Successful in 44s
Post-merge / publish (push) Successful in 22s
Post-merge / badges (push) Failing after 30s
Gitea appends a ".-" suffix before ".md" for wiki page titles that
contain dashes, to distinguish literal dashes from space-to-dash
conversions. For example, "Getting-Started" becomes
"Getting-Started.-.md", while "Architecture" becomes "Architecture.md".

Previously the code wrote "Getting-Started.md" which Gitea couldn't
recognize as a valid wiki page, causing verification to fail with
"page not found" for 15 of 21 pages.

Also force-push to handle concurrent CI runs that may have pushed to
the wiki repo between our clone and push.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-06 15:21:22 +02:00
devx-ci-bot bbb264efc9 release: v0.35.6 [skip ci] 2026-07-06 13:00:59 +00:00
emilandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> c97b249935 DEVX-118: fix: add delay before wiki verification to avoid race condition
Post-merge / detect-type (push) Successful in 10s
Post-merge / validate-commit-msg (push) Successful in 9s
Post-merge / configure-repo (push) Successful in 15s
Post-merge / vikunja (push) Successful in 20s
Post-merge / sync-wiki (push) Successful in 29s
Post-merge / release (push) Successful in 39s
Post-merge / publish (push) Successful in 25s
Post-merge / badges (push) Failing after 38s
Gitea needs a few seconds to process pushed wiki commits before a
re-clone will see them. Add a 5s sleep after a successful push before
verification re-clones the wiki.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-06 15:00:02 +02:00
devx-ci-bot 32b9a53151 release: v0.35.5 [skip ci] 2026-07-06 09:46:57 +00:00
emilandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> ae68df63f1 DEVX-118: fix: embed token in wiki clone URL for push auth
Post-merge / detect-type (push) Successful in 9s
Post-merge / validate-commit-msg (push) Successful in 9s
Post-merge / vikunja (push) Successful in 19s
Post-merge / sync-wiki (push) Successful in 24s
Post-merge / configure-repo (push) Successful in 14s
Post-merge / release (push) Successful in 34s
Post-merge / publish (push) Successful in 19s
Post-merge / badges (push) Failing after 29s
The wiki Git push failed with "could not read Username" because the
clone URL didn't include credentials. Use token@host URL format so
both clone and push authenticate properly.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-06 11:46:05 +02:00
devx-ci-bot 6402f31345 release: v0.35.4 [skip ci] 2026-07-06 09:42:49 +00:00
emilandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> f017fec8f5 DEVX-118: fix: configure git identity before commit in sync_wiki
Post-merge / detect-type (push) Successful in 16s
Post-merge / validate-commit-msg (push) Successful in 11s
Post-merge / configure-repo (push) Successful in 17s
Post-merge / vikunja (push) Successful in 21s
Post-merge / sync-wiki (push) Failing after 24s
Post-merge / release (push) Successful in 37s
Post-merge / publish (push) Successful in 21s
Post-merge / badges (push) Failing after 31s
CI environments may lack git user.email/user.name config, causing
git commit to fail with exit code 128. Set identity explicitly before
committing wiki changes.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-06 11:41:36 +02:00
devx-ci-bot add02273b6 release: v0.35.3 [skip ci] 2026-07-06 09:40:19 +00:00
emilandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> e489fdb206 DEVX-118: fix: replace --strict with --verify for sync_wiki
Post-merge / detect-type (push) Successful in 12s
Post-merge / validate-commit-msg (push) Successful in 25s
Post-merge / vikunja (push) Successful in 33s
Post-merge / configure-repo (push) Successful in 28s
Post-merge / sync-wiki (push) Failing after 37s
Post-merge / release (push) Successful in 52s
Post-merge / publish (push) Successful in 29s
Post-merge / badges (push) Failing after 38s
The rewritten sync_wiki.py removed the --strict flag. The new git-based
approach is strict by default; --verify adds post-sync page verification.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-06 11:36:30 +02:00
devx-ci-bot 45a9c7d431 release: v0.35.2 [skip ci] 2026-07-06 08:45:42 +00:00
emilandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 8e1c7d03a4 DEVX-118: fix: exclude .vale directory from lint_docs scanning
Post-merge / detect-type (push) Successful in 10s
Post-merge / validate-commit-msg (push) Successful in 10s
Post-merge / configure-repo (push) Successful in 21s
Post-merge / vikunja (push) Successful in 23s
Post-merge / sync-wiki (push) Failing after 29s
Post-merge / release (push) Successful in 41s
Post-merge / publish (push) Successful in 39s
Post-merge / badges (push) Failing after 45s
Third-party Vale style packages contain README.md files with code blocks
that don't specify a language, causing false positives in lint_docs.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-06 10:44:25 +02:00
emilandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 2de3ab4d84 DEVX-118: docs: update AGENTS.md with new tools and make targets
Post-merge / detect-type (push) Successful in 11s
Post-merge / validate-commit-msg (push) Successful in 11s
Post-merge / release (push) Successful in 16s
Post-merge / publish (push) Has been skipped
Post-merge / vikunja (push) Successful in 17s
Post-merge / sync-wiki (push) Failing after 22s
Post-merge / configure-repo (push) Successful in 15s
Post-merge / badges (push) Failing after 31s
Document check_doc_versions.py, Vale, and new make targets in AGENTS.md.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-06 10:29:47 +02:00
devx-ci-bot fa501adfbc release: v0.35.1 [skip ci] 2026-07-06 08:27:00 +00:00
emilandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 0a5625b70b DEVX-118: refactor: rewrite sync_wiki.py to use git-based approach
Post-merge / detect-type (push) Successful in 10s
Post-merge / validate-commit-msg (push) Successful in 9s
Post-merge / vikunja (push) Successful in 21s
Post-merge / configure-repo (push) Successful in 23s
Post-merge / sync-wiki (push) Failing after 28s
Post-merge / release (push) Successful in 42s
Post-merge / publish (push) Successful in 22s
Post-merge / badges (push) Failing after 31s
Replace the unreliable Gitea wiki API with direct Git operations:
- Clone {repo}.wiki.git, copy docs with link transformation, push
- Faster: single git push vs N API calls
- More reliable: no API timeouts or rate limits
- Atomic: all pages sync in one commit
- Auto-pruning: stale wiki pages removed automatically
- Link transformation: [text](file.md) → [text](file) for wiki format
- 36 new tests covering transform_links, clone, sync_files, commit, verify

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-06 10:26:00 +02:00
devx-ci-bot f28ba432ce release: v0.35.0 [skip ci] 2026-07-06 08:16:54 +00:00
emilandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> fb342e7b9d DEVX-118: feat: enrich lint_docs.py with single H1, max depth, line length, code block lang, orphan checks
Post-merge / detect-type (push) Successful in 10s
Post-merge / validate-commit-msg (push) Successful in 9s
Post-merge / vikunja (push) Successful in 19s
Post-merge / configure-repo (push) Successful in 16s
Post-merge / release (push) Successful in 40s
Post-merge / sync-wiki (push) Successful in 43s
Post-merge / publish (push) Successful in 28s
Post-merge / badges (push) Failing after 36s
- Add check_single_h1: each markdown file should have at most one H1
- Add check_max_heading_depth: headings should not exceed H4 (configurable)
- Add check_line_length: warn on lines >120 chars (non-blocking — badge URLs)
- Add check_code_block_languages: fenced code blocks must specify a language
- Add check_orphan_docs: warn on docs not linked from index.md or mapping.json
- Fix all code blocks in docs to specify language (text for plain blocks)
- Fix duplicate H1 in .vale/styles/devx/README.md
- Add 18 new tests for full coverage of new checks

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-06 10:15:54 +02:00
devx-ci-bot bb700ab969 release: v0.34.0 [skip ci] 2026-07-06 08:05:29 +00:00
emilandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> bbf0c81c32 DEVX-118: feat: enhance documentation-as-code with badges, version refs, Vale
Post-merge / detect-type (push) Successful in 11s
Post-merge / validate-commit-msg (push) Successful in 13s
Post-merge / vikunja (push) Successful in 19s
Post-merge / configure-repo (push) Successful in 15s
Post-merge / release (push) Successful in 45s
Post-merge / sync-wiki (push) Successful in 50s
Post-merge / publish (push) Successful in 32s
Post-merge / badges (push) Failing after 36s
- Fix badge system: clean .badges dir from orphan branch, add version
  verification, make badges job depend on release (avoids stale version
  badge race condition)
- Add check_doc_versions.py: lint tool that verifies docs version
  references match current __version__, with --fix for auto-update
- Integrate check_doc_versions into release process (auto-updates docs
  on every release commit)
- Add Vale prose linter integration: .vale.ini, custom styles for
  terminology and code block language, CI step, make target
- Fix stale version references in docs (0.27.0 → 0.33.4)
- Fix e.g. → for example in docs (Google.Latin Vale rule)
- Add CI steps for check_doc_versions and Vale to quality workflow
- Add make targets: devx-check-doc-versions, devx-vale

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-06 10:03:47 +02:00
gitea-actions-bot 3e12cf222f chore: update badge URLs to commit 40fbd801 [skip ci] 2026-07-06 06:18:52 +00:00
devx-ci-bot 951ba7de7a release: v0.33.4 [skip ci] 2026-07-06 06:18:39 +00:00
emil e796b06a91 DEVX-117: refactor: remove project-specific references from devx
Post-merge / detect-type (push) Successful in 9s
Post-merge / validate-commit-msg (push) Successful in 9s
Post-merge / vikunja (push) Successful in 15s
Post-merge / configure-repo (push) Successful in 19s
Post-merge / release (push) Successful in 45s
Post-merge / sync-wiki (push) Successful in 46s
Post-merge / badges (push) Successful in 46s
Post-merge / publish (push) Successful in 17s
2026-07-06 06:17:52 +00:00
gitea-actions-bot 990f2fa612 chore: update badge URLs to commit 7802ce60 [skip ci] 2026-07-06 04:56:16 +00:00
devx-ci-bot a7f5f47564 release: v0.33.3 [skip ci] 2026-07-06 04:56:04 +00:00
emil d623a64344 DEVX-115: fix: make wiki sync resilient to API timeouts and stale page lists
Post-merge / detect-type (push) Successful in 12s
Post-merge / validate-commit-msg (push) Successful in 9s
Post-merge / vikunja (push) Successful in 16s
Post-merge / release (push) Successful in 39s
Post-merge / configure-repo (push) Successful in 17s
Post-merge / sync-wiki (push) Successful in 45s
Post-merge / badges (push) Successful in 47s
Post-merge / publish (push) Successful in 18s
2026-07-06 04:55:06 +00:00
gitea-actions-bot 268a4e7988 chore: update badge URLs to commit b07bea6f [skip ci] 2026-07-05 20:47:45 +00:00
emil 7daaf9e4a9 DEVX-114: ci: add testing-and-debugging skill for devx repo
Post-merge / detect-type (push) Successful in 13s
Post-merge / validate-commit-msg (push) Successful in 9s
Post-merge / release (push) Successful in 17s
Post-merge / publish (push) Has been skipped
Post-merge / vikunja (push) Successful in 17s
Post-merge / configure-repo (push) Successful in 20s
Post-merge / sync-wiki (push) Successful in 43s
Post-merge / badges (push) Successful in 54s
2026-07-05 20:46:21 +00:00
gitea-actions-bot b7c9334881 chore: update badge URLs to commit 83595808 [skip ci] 2026-07-05 19:18:21 +00:00
devx-ci-bot 3406639f13 release: v0.33.2 [skip ci] 2026-07-05 19:18:03 +00:00
emil 9f02ccb40d DEVX-113: fix: abort sync_wiki when list_wiki_pages fails
Post-merge / detect-type (push) Successful in 11s
Post-merge / validate-commit-msg (push) Successful in 10s
Post-merge / vikunja (push) Successful in 22s
Post-merge / configure-repo (push) Successful in 18s
Post-merge / release (push) Successful in 47s
Post-merge / badges (push) Successful in 54s
Post-merge / sync-wiki (push) Successful in 55s
Post-merge / publish (push) Successful in 31s
2026-07-05 19:17:10 +00:00
gitea-actions-bot 5206158603 chore: update badge URLs to commit 66fec9ab [skip ci] 2026-07-05 14:47:30 +00:00
devx-ci-bot 489cc8343a release: v0.33.1 [skip ci] 2026-07-05 14:47:16 +00:00
emil 20ea80135c DEVX-112: fix: build images after post-merge publish, not on push
Post-merge / detect-type (push) Successful in 10s
Post-merge / validate-commit-msg (push) Successful in 10s
Post-merge / sync-wiki (push) Successful in 28s
Post-merge / release (push) Successful in 32s
Post-merge / vikunja (push) Successful in 13s
Post-merge / badges (push) Successful in 36s
Post-merge / configure-repo (push) Successful in 14s
Post-merge / publish (push) Successful in 20s
2026-07-05 14:46:33 +00:00
gitea-actions-bot 53b1d300aa chore: update badge URLs to commit 546910d3 [skip ci] 2026-07-05 14:13:08 +00:00
devx-ci-bot 5b9e92f324 release: v0.33.0 [skip ci] 2026-07-05 14:12:51 +00:00
emil 2c0118111d DEVX-111: feat: add check_api_identity_checks, setup_ssh_key, and api utils
Post-merge / detect-type (push) Successful in 9s
Post-merge / validate-commit-msg (push) Successful in 9s
Build Images / detect-type (push) Successful in 42s
Post-merge / vikunja (push) Successful in 17s
Post-merge / release (push) Successful in 52s
Post-merge / configure-repo (push) Successful in 23s
Post-merge / badges (push) Successful in 55s
Post-merge / sync-wiki (push) Successful in 58s
Post-merge / publish (push) Successful in 21s
Build Images / build-and-push (push) Successful in 3m15s
Build Images / cleanup (push) Successful in 3m38s
2026-07-05 14:11:58 +00:00
gitea-actions-bot 333641f862 chore: update badge URLs to commit e8088b8e [skip ci] 2026-07-01 23:37:55 +00:00
devx-ci-bot f21b01dce2 release: v0.32.1 [skip ci] 2026-07-01 23:37:11 +00:00
emil ff80745eea DEVX-110: fix: add missing i18n translations for new tools
Post-merge / detect-type (push) Successful in 15s
Post-merge / validate-commit-msg (push) Successful in 14s
Build Images / detect-type (push) Successful in 56s
Post-merge / release (push) Successful in 1m2s
Post-merge / configure-repo (push) Successful in 28s
Post-merge / sync-wiki (push) Successful in 1m11s
Post-merge / badges (push) Successful in 1m19s
Post-merge / vikunja (push) Successful in 1m15s
Post-merge / publish (push) Successful in 33s
Build Images / build-and-push (push) Successful in 4m22s
Build Images / cleanup (push) Successful in 2m50s
2026-07-01 23:35:56 +00:00
gitea-actions-bot 319807f41c chore: update badge URLs to commit ce9bf024 [skip ci] 2026-07-01 23:01:36 +00:00
devx-ci-bot e652d3bb75 release: v0.32.0 [skip ci] 2026-07-01 23:01:19 +00:00
emil d59de06652 DEVX-110: feat: extract docker-login, tofu-ops, check-deps, install-tofu to Python tools
Build Images / cleanup (push) Successful in 3m20s
Post-merge / detect-type (push) Successful in 9s
Post-merge / validate-commit-msg (push) Successful in 9s
Post-merge / vikunja (push) Successful in 21s
Build Images / detect-type (push) Successful in 41s
Post-merge / configure-repo (push) Successful in 17s
Post-merge / release (push) Successful in 43s
Post-merge / sync-wiki (push) Successful in 47s
Post-merge / badges (push) Successful in 51s
Post-merge / publish (push) Successful in 22s
Build Images / build-and-push (push) Successful in 3m26s
2026-07-01 23:00:28 +00:00
gitea-actions-bot ae37a8e3e4 chore: update badge URLs to commit de35661c [skip ci] 2026-07-01 22:35:46 +00:00
devx-ci-bot c63e85923a release: v0.31.0 [skip ci] 2026-07-01 22:35:34 +00:00
emil 77c1af8ed3 DEVX-110: feat: centralize venv management in devx.mak
Post-merge / detect-type (push) Successful in 9s
Build Images / detect-type (push) Failing after 13s
Build Images / build-and-push (push) Has been skipped
Post-merge / validate-commit-msg (push) Successful in 10s
Build Images / cleanup (push) Has been skipped
Post-merge / vikunja (push) Successful in 15s
Post-merge / configure-repo (push) Successful in 18s
Post-merge / sync-wiki (push) Successful in 29s
Post-merge / release (push) Successful in 32s
Post-merge / badges (push) Successful in 39s
Post-merge / publish (push) Successful in 17s
2026-07-01 22:34:49 +00:00
gitea-actions-bot a48fb46c52 chore: update badge URLs to commit 1755d7a2 [skip ci] 2026-07-01 20:54:47 +00:00
emil 2392a13afc DEVX-109: docs: add container-level fix verification and verified state modification rules
Post-merge / detect-type (push) Successful in 12s
Post-merge / validate-commit-msg (push) Successful in 11s
Post-merge / release (push) Successful in 21s
Post-merge / publish (push) Has been skipped
Post-merge / vikunja (push) Successful in 17s
Post-merge / configure-repo (push) Successful in 15s
Post-merge / sync-wiki (push) Successful in 45s
Post-merge / badges (push) Successful in 46s
2026-07-01 20:53:32 +00:00
gitea-actions-bot 32315b1d5d chore: update badge URLs to commit 753a5f9e [skip ci] 2026-07-01 14:05:12 +00:00
devx-ci-bot f70f468630 release: v0.30.0 [skip ci] 2026-07-01 14:04:57 +00:00
emil 85b5ec1485 DEVX-108: feat: add standard label creation to configure_repo
Post-merge / detect-type (push) Successful in 16s
Post-merge / validate-commit-msg (push) Successful in 21s
Post-merge / vikunja (push) Successful in 24s
Post-merge / configure-repo (push) Successful in 17s
Post-merge / sync-wiki (push) Successful in 49s
Post-merge / release (push) Successful in 50s
Post-merge / badges (push) Successful in 58s
Build Images / detect-type (push) Successful in 1m28s
Post-merge / publish (push) Successful in 20s
Build Images / build-and-push (push) Successful in 3m2s
Build Images / cleanup (push) Successful in 3m9s
2026-07-01 14:03:48 +00:00
gitea-actions-bot 091b951adc chore: update badge URLs to commit d23c6b86 [skip ci] 2026-07-01 09:36:05 +00:00
emil 19eb57445d DEVX-103: docs: fix outdated version refs, language list, config key, and missing modules
Post-merge / detect-type (push) Successful in 11s
Post-merge / validate-commit-msg (push) Successful in 9s
Post-merge / release (push) Successful in 17s
Post-merge / publish (push) Has been skipped
Post-merge / vikunja (push) Successful in 19s
Post-merge / configure-repo (push) Successful in 15s
Post-merge / sync-wiki (push) Successful in 47s
Post-merge / badges (push) Successful in 42s
2026-07-01 09:34:53 +00:00
gitea-actions-bot bdd0e05869 chore: update badge URLs to commit d5dfe563 [skip ci] 2026-07-01 09:29:34 +00:00
devx-ci-bot 27fd99a091 release: v0.29.1 [skip ci] 2026-07-01 09:29:17 +00:00
emil e3fa9b7c95 DEVX-107: fix: strip task ID prefix from commit messages in extract_conventional_msg
Post-merge / detect-type (push) Successful in 10s
Post-merge / validate-commit-msg (push) Successful in 13s
Build Images / detect-type (push) Successful in 43s
Post-merge / vikunja (push) Successful in 16s
Post-merge / sync-wiki (push) Successful in 33s
Post-merge / release (push) Successful in 37s
Post-merge / configure-repo (push) Successful in 13s
Post-merge / badges (push) Successful in 48s
Post-merge / publish (push) Successful in 20s
Build Images / build-and-push (push) Successful in 3m7s
Build Images / cleanup (push) Successful in 3m18s
2026-07-01 09:28:23 +00:00
gitea-actions-bot ce60356542 chore: update badge URLs to commit 82fb419c [skip ci] 2026-07-01 06:20:37 +00:00
devx-ci-bot 35c72ef595 release: v0.29.0 [skip ci] 2026-07-01 06:20:24 +00:00
emil c0fcaef25f DEVX-106: feat: detect badge commits as automated CI commits
Build Images / build-and-push (push) Successful in 3m1s
Build Images / cleanup (push) Successful in 2m47s
Post-merge / detect-type (push) Successful in 8s
Post-merge / validate-commit-msg (push) Successful in 9s
Post-merge / vikunja (push) Successful in 21s
Post-merge / configure-repo (push) Successful in 13s
Build Images / detect-type (push) Successful in 40s
Post-merge / sync-wiki (push) Successful in 36s
Post-merge / release (push) Successful in 39s
Post-merge / badges (push) Successful in 45s
Post-merge / publish (push) Successful in 18s
2026-07-01 06:19:34 +00:00
gitea-actions-bot 3dd5b452c0 chore: update badge URLs to commit d5cf4c7f [skip ci] 2026-07-01 01:11:39 +00:00
emil b2515bbf37 DEVX-105: docs: add devx-workflow skill for agent guidance
Post-merge / detect-type (push) Successful in 13s
Post-merge / validate-commit-msg (push) Successful in 11s
Post-merge / release (push) Successful in 18s
Post-merge / vikunja (push) Successful in 17s
Post-merge / publish (push) Has been skipped
Post-merge / configure-repo (push) Successful in 15s
Post-merge / sync-wiki (push) Successful in 55s
Post-merge / badges (push) Successful in 54s
2026-07-01 01:10:23 +00:00
gitea-actions-bot ad2e59980f chore: update badge URLs to commit b041147a [skip ci] 2026-07-01 00:51:32 +00:00
devx-ci-bot 68a01d1bda release: v0.28.0 [skip ci] 2026-07-01 00:51:20 +00:00
emil 621b051793 DEVX-104: feat: auto-rebase in auto-merge, new rebase tools, CLI registration
Build Images / build-and-push (push) Successful in 5m35s
Build Images / cleanup (push) Successful in 3m32s
Post-merge / detect-type (push) Successful in 9s
Post-merge / validate-commit-msg (push) Successful in 10s
Post-merge / vikunja (push) Successful in 24s
Post-merge / configure-repo (push) Successful in 21s
Post-merge / sync-wiki (push) Successful in 38s
Post-merge / release (push) Successful in 44s
Post-merge / badges (push) Successful in 50s
Post-merge / publish (push) Successful in 20s
Build Images / detect-type (push) Successful in 1m26s
2026-07-01 00:50:23 +00:00
gitea-actions-bot 66554657f2 chore: update badge URLs to commit 44123e77 [skip ci] 2026-06-30 05:34:13 +00:00
devx-ci-bot 587d3a6ca4 release: v0.27.3 [skip ci] 2026-06-30 05:33:54 +00:00
emil 9d75e408ae DEVX-14: fix: retry wiki integrity check on transient API timeout
Post-merge / detect-type (push) Successful in 10s
Post-merge / validate-commit-msg (push) Successful in 8s
Build Images / detect-type (push) Successful in 42s
Post-merge / sync-wiki (push) Successful in 30s
Post-merge / vikunja (push) Successful in 16s
Post-merge / release (push) Successful in 44s
Post-merge / configure-repo (push) Successful in 17s
Post-merge / badges (push) Successful in 43s
Post-merge / publish (push) Successful in 20s
Build Images / build-and-push (push) Successful in 3m12s
Build Images / cleanup (push) Successful in 2m37s
2026-06-30 05:33:00 +00:00
gitea-actions-bot 412bbea01d chore: update badge URLs to commit 8d35f5dd [skip ci] 2026-06-29 11:30:18 +00:00
devx-ci-bot ce5ce33a12 release: v0.27.2 [skip ci] 2026-06-29 11:30:13 +00:00
emil 0fae419584 DEVX-100: fix: retry release push on non-fast-forward with rebase loop
Post-merge / detect-type (push) Successful in 10s
Post-merge / validate-commit-msg (push) Successful in 9s
Post-merge / vikunja (push) Successful in 12s
Post-merge / configure-repo (push) Successful in 11s
Post-merge / sync-wiki (push) Successful in 23s
Post-merge / release (push) Successful in 29s
Post-merge / badges (push) Successful in 30s
Build Images / detect-type (push) Successful in 1m0s
Post-merge / publish (push) Successful in 16s
Build Images / build-and-push (push) Successful in 3m57s
Build Images / cleanup (push) Successful in 2m56s
2026-06-29 11:29:31 +00:00
gitea-actions-bot 70b011d4a6 chore: update badge URLs to commit 53e15be6 [skip ci] 2026-06-28 17:02:18 +00:00
devx-ci-bot a5c16a92df release: v0.27.1 [skip ci] 2026-06-28 17:02:12 +00:00
emil e6f022ae96 DEVX-99: fix: exclude .devin/.terraform dirs from lint_docs, add duplicate heading excludes
Post-merge / detect-type (push) Successful in 9s
Post-merge / validate-commit-msg (push) Successful in 8s
Post-merge / vikunja (push) Successful in 12s
Post-merge / configure-repo (push) Successful in 10s
Post-merge / sync-wiki (push) Successful in 19s
Build Images / detect-type (push) Successful in 35s
Post-merge / release (push) Successful in 25s
Post-merge / badges (push) Successful in 28s
Post-merge / publish (push) Successful in 15s
Build Images / build-and-push (push) Successful in 2m51s
Build Images / cleanup (push) Successful in 2m18s
2026-06-28 17:01:37 +00:00
gitea-actions-bot 1a27983750 chore: update badge URLs to commit 8f2186a4 [skip ci] 2026-06-28 16:36:47 +00:00
devx-ci-bot 5d7ed62b34 release: v0.27.0 [skip ci] 2026-06-28 16:36:35 +00:00
emil ee80c27631 DEVX-98: feat: add lint_docs tool, fix doc_coverage/check_translations for any repo
Post-merge / detect-type (push) Successful in 10s
Post-merge / validate-commit-msg (push) Successful in 8s
Post-merge / vikunja (push) Successful in 13s
Post-merge / configure-repo (push) Successful in 11s
Post-merge / sync-wiki (push) Successful in 21s
Post-merge / release (push) Successful in 25s
Build Images / detect-type (push) Successful in 38s
Post-merge / badges (push) Successful in 34s
Post-merge / publish (push) Successful in 33s
Build Images / build-and-push (push) Successful in 3m20s
Build Images / cleanup (push) Successful in 1m57s
2026-06-28 16:35:59 +00:00
gitea-actions-bot 98b1659579 chore: update badge URLs to commit f308b9f8 [skip ci] 2026-06-28 15:02:09 +00:00
devx-ci-bot c12d9abc6d release: v0.26.4 [skip ci] 2026-06-28 15:01:52 +00:00
emil 40a65cb0a6 DEVX-97: fix: wrap all user-facing strings with _() for i18n completeness
Post-merge / vikunja (push) Successful in 13s
Post-merge / detect-type (push) Successful in 12s
Post-merge / validate-commit-msg (push) Successful in 11s
Post-merge / sync-wiki (push) Successful in 21s
Post-merge / release (push) Successful in 29s
Post-merge / configure-repo (push) Successful in 11s
Post-merge / badges (push) Successful in 34s
Post-merge / publish (push) Successful in 22s
Build Images / detect-type (push) Successful in 48s
Build Images / build-and-push (push) Successful in 4m3s
Build Images / cleanup (push) Successful in 2m6s
2026-06-28 15:01:07 +00:00
gitea-actions-bot 1a89738dd4 chore: update badge URLs to commit 36f474db [skip ci] 2026-06-28 14:56:08 +00:00
devx-ci-bot 9e604ea2c7 release: v0.26.3 [skip ci] 2026-06-28 14:55:48 +00:00
emil 4bb50bed58 DEVX-97: fix: pin all dependencies to exact versions for reproducibility
Post-merge / detect-type (push) Successful in 10s
Post-merge / validate-commit-msg (push) Successful in 12s
Post-merge / sync-wiki (push) Successful in 24s
Post-merge / vikunja (push) Successful in 15s
Post-merge / release (push) Successful in 32s
Post-merge / configure-repo (push) Successful in 13s
Post-merge / badges (push) Successful in 49s
Post-merge / publish (push) Successful in 24s
Build Images / detect-type (push) Waiting to run
Build Images / build-and-push (push) Blocked by required conditions
Build Images / cleanup (push) Blocked by required conditions
2026-06-28 14:55:03 +00:00
gitea-actions-bot 5bb9dce530 chore: update badge URLs to commit c04f826e [skip ci] 2026-06-28 14:42:36 +00:00
emil 73662a3bf0 DEVX-95: chore: pin all dependency versions to concrete releases
Post-merge / detect-type (push) Successful in 18s
Post-merge / validate-commit-msg (push) Successful in 10s
Post-merge / release (push) Successful in 15s
Post-merge / sync-wiki (push) Successful in 24s
Post-merge / vikunja (push) Successful in 14s
Build Images / detect-type (push) Successful in 45s
Post-merge / publish (push) Has been skipped
Post-merge / configure-repo (push) Successful in 12s
Post-merge / badges (push) Successful in 34s
Build Images / build-and-push (push) Successful in 3m47s
Build Images / cleanup (push) Successful in 2m11s
2026-06-28 14:41:27 +00:00
gitea-actions-bot a1e87b1905 chore: update badge URLs to commit 991d923a [skip ci] 2026-06-28 14:38:20 +00:00
devx-ci-bot d8d0ad04a2 release: v0.26.2 [skip ci] 2026-06-28 14:38:13 +00:00
emil e836c09088 DEVX-96: fix: block admin merge override and auto-approve with review token
Post-merge / detect-type (push) Successful in 9s
Post-merge / validate-commit-msg (push) Successful in 8s
Post-merge / configure-repo (push) Successful in 10s
Post-merge / sync-wiki (push) Successful in 18s
Post-merge / vikunja (push) Successful in 20s
Post-merge / release (push) Successful in 25s
Post-merge / badges (push) Successful in 29s
Post-merge / publish (push) Successful in 16s
Build Images / build-and-push (push) Waiting to run
Build Images / cleanup (push) Blocked by required conditions
Build Images / detect-type (push) Successful in 45s
2026-06-28 14:37:36 +00:00
gitea-actions-bot 507436b134 chore: update badge URLs to commit 8dbdd563 [skip ci] 2026-06-28 12:15:13 +00:00
devx-ci-bot 32cec2c5ad release: v0.26.1 [skip ci] 2026-06-28 12:15:06 +00:00
emil 55c530eb00 DEVX-93: fix: force pip upgrade in setup-image to install new dependencies
Post-merge / detect-type (push) Successful in 8s
Post-merge / validate-commit-msg (push) Successful in 9s
Post-merge / vikunja (push) Successful in 11s
Post-merge / release (push) Successful in 25s
Post-merge / configure-repo (push) Successful in 9s
Post-merge / sync-wiki (push) Successful in 18s
Post-merge / badges (push) Successful in 28s
Build Images / detect-type (push) Successful in 41s
Post-merge / publish (push) Successful in 15s
Build Images / build-and-push (push) Successful in 3m1s
Build Images / cleanup (push) Successful in 2m25s
2026-06-28 12:14:31 +00:00
gitea-actions-bot 3928de4507 chore: update badge URLs to commit 7dc6d2ce [skip ci] 2026-06-28 01:59:24 +00:00
devx-ci-bot 8bb1813715 release: v0.26.0 [skip ci] 2026-06-28 01:59:17 +00:00
emil f7f53941a1 DEVX-92: feat: add distribute_items CI tool for parallel VM deployment
Post-merge / detect-type (push) Successful in 9s
Post-merge / validate-commit-msg (push) Successful in 8s
Post-merge / vikunja (push) Successful in 12s
Post-merge / sync-wiki (push) Successful in 17s
Post-merge / configure-repo (push) Successful in 9s
Post-merge / release (push) Successful in 25s
Build Images / detect-type (push) Successful in 41s
Post-merge / badges (push) Successful in 28s
Post-merge / publish (push) Successful in 15s
Build Images / build-and-push (push) Successful in 2m53s
Build Images / cleanup (push) Successful in 7m33s
2026-06-28 01:58:42 +00:00
gitea-actions-bot 5edfdaa7aa chore: update badge URLs to commit 3a6bff69 [skip ci] 2026-06-28 00:18:22 +00:00
devx-ci-bot 893da8ba34 release: v0.25.0 [skip ci] 2026-06-28 00:18:14 +00:00
emil 64a58874b6 DEVX-91: feat: add manual review support to pr_review (--event, --body, --checklist-confirmed)
Post-merge / detect-type (push) Successful in 8s
Post-merge / validate-commit-msg (push) Successful in 9s
Post-merge / vikunja (push) Successful in 13s
Post-merge / configure-repo (push) Successful in 10s
Post-merge / sync-wiki (push) Successful in 19s
Build Images / detect-type (push) Successful in 34s
Post-merge / release (push) Successful in 27s
Post-merge / badges (push) Successful in 32s
Post-merge / publish (push) Successful in 16s
Build Images / build-and-push (push) Successful in 3m38s
Build Images / cleanup (push) Successful in 7m17s
2026-06-28 00:17:33 +00:00
gitea-actions-bot c1c2041ca4 chore: update badge URLs to commit 0fa6360d [skip ci] 2026-06-28 00:07:16 +00:00
devx-ci-bot 49ff8870b1 release: v0.24.1 [skip ci] 2026-06-28 00:07:07 +00:00
emil 4c1ecbf4fa DEVX-91: refactor: add find_task_by_identifier, config fallbacks for tools
Post-merge / release (push) Successful in 27s
Post-merge / detect-type (push) Successful in 8s
Post-merge / validate-commit-msg (push) Successful in 10s
Post-merge / vikunja (push) Successful in 11s
Post-merge / sync-wiki (push) Successful in 18s
Post-merge / configure-repo (push) Successful in 10s
Build Images / detect-type (push) Successful in 42s
Post-merge / badges (push) Successful in 30s
Post-merge / publish (push) Successful in 15s
Build Images / build-and-push (push) Successful in 2m51s
Build Images / cleanup (push) Successful in 1m44s
2026-06-28 00:06:29 +00:00
gitea-actions-bot 93a1cb9945 chore: update badge URLs to commit 6c0ce9c6 [skip ci] 2026-06-27 23:39:13 +00:00
devx-ci-bot 507bc86b92 release: v0.24.0 [skip ci] 2026-06-27 23:38:58 +00:00
emil 81dc30ecff DEVX-91: feat: add pr_status, pr_logs, pr_label tools
Post-merge / detect-type (push) Successful in 9s
Post-merge / validate-commit-msg (push) Successful in 11s
Post-merge / vikunja (push) Successful in 13s
Post-merge / sync-wiki (push) Successful in 17s
Post-merge / configure-repo (push) Successful in 10s
Post-merge / release (push) Successful in 30s
Build Images / detect-type (push) Successful in 48s
Post-merge / badges (push) Successful in 41s
Post-merge / publish (push) Successful in 16s
Build Images / build-and-push (push) Successful in 4m41s
Build Images / cleanup (push) Successful in 2m23s
2026-06-27 23:38:16 +00:00
gitea-actions-bot 3c421dd1ad chore: update badge URLs to commit d312f7b7 [skip ci] 2026-06-27 22:14:47 +00:00
devx-ci-bot 11ce99756c release: v0.23.4 [skip ci] 2026-06-27 22:14:38 +00:00
emil 6149167ba2 DEVX-90: fix: classify .gitea/** as user-facing for devx, support glob in user_facing_overrides
Post-merge / detect-type (push) Successful in 8s
Post-merge / validate-commit-msg (push) Successful in 8s
Post-merge / vikunja (push) Successful in 11s
Post-merge / sync-wiki (push) Successful in 16s
Post-merge / configure-repo (push) Successful in 10s
Build Images / detect-type (push) Successful in 31s
Post-merge / release (push) Successful in 23s
Post-merge / badges (push) Successful in 28s
Post-merge / publish (push) Successful in 15s
Build Images / build-and-push (push) Successful in 2m46s
Build Images / cleanup (push) Successful in 1m29s
2026-06-27 22:14:05 +00:00
gitea-actions-bot 014ab0b63f chore: update badge URLs to commit 7430be68 [skip ci] 2026-06-27 22:07:28 +00:00
emil 2a3ee1ec96 DEVX-89: fix: add --auto-login to all notify_failure calls in workflows
Post-merge / detect-type (push) Successful in 9s
Post-merge / validate-commit-msg (push) Successful in 10s
Post-merge / configure-repo (push) Successful in 10s
Post-merge / release (push) Successful in 12s
Post-merge / vikunja (push) Successful in 12s
Post-merge / publish (push) Has been skipped
Post-merge / sync-wiki (push) Successful in 18s
Post-merge / badges (push) Successful in 28s
2026-06-27 22:06:47 +00:00
gitea-actions-bot 5d4968eb21 chore: update badge URLs to commit 48324375 [skip ci] 2026-06-27 21:41:36 +00:00
devx-ci-bot 33cfbb0f41 release: v0.23.3 [skip ci] 2026-06-27 21:41:26 +00:00
emil 598238e4d6 DEVX-88: fix: correct clean_images delete URL and add retry with error handling
Post-merge / detect-type (push) Successful in 9s
Post-merge / validate-commit-msg (push) Successful in 10s
Post-merge / vikunja (push) Successful in 10s
Post-merge / sync-wiki (push) Successful in 18s
Build Images / detect-type (push) Successful in 36s
Post-merge / release (push) Successful in 25s
Post-merge / configure-repo (push) Successful in 9s
Post-merge / badges (push) Successful in 29s
Post-merge / publish (push) Successful in 19s
Build Images / build-and-push (push) Successful in 2m55s
Build Images / cleanup (push) Successful in 5m47s
2026-06-27 21:40:49 +00:00
gitea-actions-bot 925b99b7db chore: update badge URLs to commit dd2ca2f7 [skip ci] 2026-06-27 21:20:07 +00:00
devx-ci-bot 16ed48bd26 release: v0.23.2 [skip ci] 2026-06-27 21:19:53 +00:00
emil 3b0500164b DEVX-87: fix: add skip-ci flag to release commits and concurrency to build-images
Post-merge / detect-type (push) Successful in 8s
Post-merge / validate-commit-msg (push) Successful in 8s
Post-merge / sync-wiki (push) Successful in 15s
Post-merge / vikunja (push) Successful in 13s
Build Images / detect-type (push) Successful in 31s
Post-merge / configure-repo (push) Successful in 10s
Post-merge / release (push) Successful in 25s
Post-merge / badges (push) Successful in 36s
Post-merge / publish (push) Successful in 15s
Build Images / build-and-push (push) Successful in 2m50s
Build Images / cleanup (push) Successful in 5m24s
2026-06-27 21:19:17 +00:00
gitea-actions-bot 00a44ec5dc chore: update badge URLs to commit b62d5c3d [skip ci] 2026-06-27 19:42:25 +00:00
gitea-actions-bot b385c57621 chore: update badge URLs to commit 3e6359f8 [skip ci] 2026-06-27 19:41:36 +00:00
devx-ci-bot 3181b24f5e release: v0.23.1
Post-merge / detect-type (push) Successful in 11s
Post-merge / validate-commit-msg (push) Has been skipped
Post-merge / release (push) Has been skipped
Post-merge / sync-wiki (push) Has been skipped
Post-merge / vikunja (push) Has been skipped
Post-merge / configure-repo (push) Has been skipped
Post-merge / publish (push) Has been skipped
Build Images / detect-type (push) Successful in 44s
Build Images / build-and-push (push) Has been skipped
Build Images / cleanup (push) Has been skipped
Post-merge / badges (push) Successful in 32s
2026-06-27 19:40:59 +00:00
emil bc8478220c DEVX-86: fix: add rsync to ci-full image for molecule_docker
Post-merge / detect-type (push) Successful in 10s
Post-merge / validate-commit-msg (push) Successful in 10s
Build Images / detect-type (push) Successful in 34s
Post-merge / release (push) Successful in 28s
Post-merge / sync-wiki (push) Successful in 23s
Post-merge / vikunja (push) Successful in 18s
Post-merge / configure-repo (push) Successful in 13s
Post-merge / badges (push) Successful in 44s
Post-merge / publish (push) Successful in 18s
Build Images / build-and-push (push) Successful in 3m45s
Build Images / cleanup (push) Successful in 2m17s
2026-06-27 19:40:20 +00:00
gitea-actions-bot a568c0899f chore: update badge URLs to commit 0f22063f [skip ci] 2026-06-27 18:06:46 +00:00
gitea-actions-bot 4216698ca8 chore: update badge URLs to commit 6823dfce [skip ci] 2026-06-27 18:06:11 +00:00
devx-ci-bot f3685b9029 release: v0.23.0
Post-merge / detect-type (push) Successful in 10s
Post-merge / validate-commit-msg (push) Has been skipped
Post-merge / release (push) Has been skipped
Post-merge / sync-wiki (push) Has been skipped
Post-merge / vikunja (push) Has been skipped
Post-merge / configure-repo (push) Has been skipped
Post-merge / publish (push) Has been skipped
Build Images / detect-type (push) Successful in 38s
Build Images / build-and-push (push) Has been skipped
Build Images / cleanup (push) Has been skipped
Post-merge / badges (push) Successful in 28s
2026-06-27 18:06:01 +00:00
emil 2e5470236e DEVX-85: feat: add devx-lint-dockerfiles to devx.mak, alias setup-image
Post-merge / detect-type (push) Successful in 8s
Post-merge / validate-commit-msg (push) Successful in 9s
Post-merge / vikunja (push) Successful in 12s
Post-merge / sync-wiki (push) Successful in 17s
Post-merge / configure-repo (push) Successful in 11s
Build Images / detect-type (push) Successful in 36s
Post-merge / release (push) Successful in 26s
Post-merge / badges (push) Successful in 31s
Post-merge / publish (push) Successful in 17s
Build Images / build-and-push (push) Successful in 2m58s
Build Images / cleanup (push) Successful in 1m25s
2026-06-27 18:05:25 +00:00
gitea-actions-bot d2dcf8f7c4 chore: update badge URLs to commit 1d6085cf [skip ci] 2026-06-27 17:25:13 +00:00
emil 357e07a9a6 DEVX-84: refactor: remove hadolint on-the-fly install from setup-image
Post-merge / detect-type (push) Successful in 9s
Post-merge / validate-commit-msg (push) Successful in 9s
Post-merge / release (push) Successful in 11s
Post-merge / vikunja (push) Successful in 12s
Post-merge / publish (push) Has been skipped
Post-merge / configure-repo (push) Successful in 10s
Post-merge / sync-wiki (push) Successful in 21s
Post-merge / badges (push) Successful in 27s
2026-06-27 17:24:34 +00:00
gitea-actions-bot ffb3976224 chore: update badge URLs to commit 22ed0d7b [skip ci] 2026-06-27 16:44:47 +00:00
gitea-actions-bot ad75b22f2a chore: update badge URLs to commit b800f158 [skip ci] 2026-06-27 16:44:13 +00:00
devx-ci-bot 37f867f6d8 release: v0.22.1
Post-merge / detect-type (push) Successful in 11s
Post-merge / validate-commit-msg (push) Has been skipped
Post-merge / release (push) Has been skipped
Post-merge / sync-wiki (push) Has been skipped
Post-merge / vikunja (push) Has been skipped
Post-merge / configure-repo (push) Has been skipped
Post-merge / publish (push) Has been skipped
Build Images / detect-type (push) Successful in 39s
Build Images / build-and-push (push) Has been skipped
Post-merge / badges (push) Successful in 26s
Build Images / cleanup (push) Has been skipped
2026-06-27 16:44:03 +00:00
emil 233a0bc055 DEVX-83: fix: fail lint-dockerfiles when hadolint is missing
Post-merge / detect-type (push) Successful in 9s
Post-merge / validate-commit-msg (push) Successful in 11s
Post-merge / vikunja (push) Successful in 15s
Post-merge / sync-wiki (push) Successful in 17s
Build Images / detect-type (push) Successful in 34s
Post-merge / configure-repo (push) Successful in 11s
Post-merge / release (push) Successful in 29s
Post-merge / badges (push) Successful in 37s
Post-merge / publish (push) Successful in 18s
Build Images / build-and-push (push) Successful in 3m1s
Build Images / cleanup (push) Successful in 4m21s
2026-06-27 16:43:22 +00:00
gitea-actions-bot 1a28f5dcc5 chore: update badge URLs to commit 7471d490 [skip ci] 2026-06-27 16:17:12 +00:00
emil 3af60af439 DEVX-82: fix: checkout release tag in publish job
Post-merge / detect-type (push) Successful in 9s
Post-merge / validate-commit-msg (push) Successful in 11s
Post-merge / release (push) Successful in 13s
Post-merge / vikunja (push) Successful in 12s
Post-merge / publish (push) Has been skipped
Post-merge / sync-wiki (push) Successful in 20s
Post-merge / configure-repo (push) Successful in 10s
Post-merge / badges (push) Successful in 32s
2026-06-27 16:16:25 +00:00
gitea-actions-bot e181104e1c chore: update badge URLs to commit 6a571fb4 [skip ci] 2026-06-27 15:06:45 +00:00
gitea-actions-bot ee1826fb34 chore: update badge URLs to commit 477726f7 [skip ci] 2026-06-27 15:06:17 +00:00
devx-ci-bot 9170546a31 release: v0.22.0
Post-merge / detect-type (push) Successful in 8s
Post-merge / validate-commit-msg (push) Has been skipped
Post-merge / release (push) Has been skipped
Post-merge / sync-wiki (push) Has been skipped
Post-merge / vikunja (push) Has been skipped
Post-merge / configure-repo (push) Has been skipped
Post-merge / publish (push) Has been skipped
Build Images / detect-type (push) Successful in 33s
Post-merge / badges (push) Successful in 24s
Build Images / build-and-push (push) Has been skipped
Build Images / cleanup (push) Has been skipped
2026-06-27 15:06:08 +00:00
emil 91f59076a1 DEVX-81: feat: document CI_GITEA_TOKEN scopes and add CI_GITEA_USERNAME to env var table
Post-merge / detect-type (push) Successful in 10s
Post-merge / validate-commit-msg (push) Successful in 8s
Post-merge / vikunja (push) Successful in 11s
Post-merge / configure-repo (push) Successful in 11s
Post-merge / sync-wiki (push) Successful in 17s
Post-merge / release (push) Successful in 20s
Post-merge / badges (push) Successful in 26s
Post-merge / publish (push) Successful in 14s
2026-06-27 15:05:37 +00:00
gitea-actions-bot ca310fe442 chore: update badge URLs to commit c8ba6417 [skip ci] 2026-06-27 15:00:55 +00:00
emil decba5ec77 DEVX-80: chore: update badge URLs to commit 20bcfe96 [skip ci] 2026-06-27 14:56:57 +00:00
gitea-actions-bot 082df1d893 chore: update badge URLs to commit 20bcfe96 [skip ci] 2026-06-27 13:47:51 +00:00
gitea-actions-bot 08f58e551b chore: update badge URLs to commit 8203528c [skip ci] 2026-06-27 13:47:06 +00:00
devx-ci-bot 66bace57d9 release: v0.21.2
Post-merge / detect-type (push) Successful in 12s
Post-merge / validate-commit-msg (push) Has been skipped
Post-merge / release (push) Has been skipped
Post-merge / sync-wiki (push) Has been skipped
Post-merge / vikunja (push) Has been skipped
Post-merge / configure-repo (push) Has been skipped
Post-merge / publish (push) Has been skipped
Build Images / detect-type (push) Successful in 32s
Build Images / build-and-push (push) Has been skipped
Build Images / cleanup (push) Has been skipped
Post-merge / badges (push) Successful in 31s
2026-06-27 13:46:57 +00:00
emil 5c8d74015e DEVX-79: fix: gate auto-merge on release-dry-run and unmask failures
Post-merge / detect-type (push) Successful in 10s
Post-merge / validate-commit-msg (push) Successful in 10s
Post-merge / sync-wiki (push) Successful in 17s
Post-merge / vikunja (push) Successful in 12s
Post-merge / configure-repo (push) Successful in 11s
Post-merge / release (push) Successful in 30s
Post-merge / badges (push) Successful in 35s
Post-merge / publish (push) Successful in 18s
2026-06-27 13:46:16 +00:00
gitea-actions-bot 48eec986f6 chore: update badge URLs to commit 3a45f8e1 [skip ci] 2026-06-27 13:28:35 +00:00
emil 1fc1cfb23f DEVX-78: style: compact devx-setup-image to pass checkmake maxbodylength (5 lines)
Post-merge / detect-type (push) Successful in 10s
Post-merge / validate-commit-msg (push) Successful in 12s
Post-merge / release (push) Successful in 16s
Build Images / detect-type (push) Successful in 35s
Post-merge / vikunja (push) Successful in 13s
Post-merge / sync-wiki (push) Successful in 24s
Post-merge / publish (push) Has been skipped
Post-merge / configure-repo (push) Successful in 12s
Post-merge / badges (push) Successful in 36s
Build Images / build-and-push (push) Successful in 3m23s
Build Images / cleanup (push) Successful in 1m0s
2026-06-27 13:27:41 +00:00
gitea-actions-bot f24b6914f6 chore: update badge URLs to commit b687262f [skip ci] 2026-06-27 13:24:52 +00:00
gitea-actions-bot 84d02ca221 chore: update badge URLs to commit 6043e937 [skip ci] 2026-06-27 13:24:13 +00:00
devx-ci-bot 18ac8f4ba9 release: v0.21.1
Post-merge / detect-type (push) Successful in 9s
Post-merge / validate-commit-msg (push) Has been skipped
Post-merge / release (push) Has been skipped
Post-merge / sync-wiki (push) Has been skipped
Post-merge / vikunja (push) Has been skipped
Post-merge / configure-repo (push) Has been skipped
Build Images / detect-type (push) Successful in 36s
Post-merge / publish (push) Has been skipped
Build Images / build-and-push (push) Has been skipped
Build Images / cleanup (push) Has been skipped
Post-merge / badges (push) Successful in 32s
2026-06-27 13:24:07 +00:00
emil 9fb9be9c35 DEVX-77: fix: devx-setup-image configures Gitea PyPI registry and shows pip errors
Post-merge / detect-type (push) Successful in 9s
Post-merge / validate-commit-msg (push) Successful in 10s
Post-merge / sync-wiki (push) Successful in 19s
Post-merge / configure-repo (push) Successful in 10s
Post-merge / vikunja (push) Successful in 11s
Post-merge / release (push) Successful in 27s
Build Images / detect-type (push) Successful in 39s
Post-merge / badges (push) Successful in 30s
Post-merge / publish (push) Successful in 16s
Build Images / build-and-push (push) Successful in 3m17s
Build Images / cleanup (push) Successful in 1m15s
2026-06-27 13:23:29 +00:00
gitea-actions-bot 9a46723391 chore: update badge URLs to commit f7a68467 [skip ci] 2026-06-27 13:05:31 +00:00
devx-ci-bot 5828d3f07b release: v0.21.0
Post-merge / sync-wiki (push) Has been skipped
Post-merge / vikunja (push) Has been skipped
Post-merge / publish (push) Has been skipped
Post-merge / configure-repo (push) Has been skipped
Build Images / detect-type (push) Successful in 38s
Build Images / build-and-push (push) Has been skipped
Post-merge / badges (push) Successful in 28s
Build Images / cleanup (push) Has been skipped
Post-merge / detect-type (push) Successful in 8s
Post-merge / validate-commit-msg (push) Has been skipped
Post-merge / release (push) Has been skipped
2026-06-27 13:04:50 +00:00
gitea-actions-bot 4232f4baee chore: update badge URLs to commit 88ae1247 [skip ci] 2026-06-27 13:04:46 +00:00
emil a9fd1a47af DEVX-76: feat: add --auto-login to publish, extract configure_tea_login to gitea_cli
Post-merge / publish (push) Successful in 17s
Post-merge / detect-type (push) Successful in 10s
Post-merge / validate-commit-msg (push) Successful in 10s
Build Images / build-and-push (push) Successful in 3m6s
Post-merge / vikunja (push) Successful in 11s
Build Images / cleanup (push) Successful in 54s
Post-merge / sync-wiki (push) Successful in 18s
Build Images / detect-type (push) Successful in 33s
Post-merge / configure-repo (push) Successful in 10s
Post-merge / badges (push) Successful in 27s
Post-merge / release (push) Successful in 32s
2026-06-27 13:04:06 +00:00
gitea-actions-bot ecd10241fb chore: update badge URLs to commit 022fdc32 [skip ci] 2026-06-27 08:02:52 +00:00
emil 5fb497d108 DEVX-75: fix: remove tag fallback step from release workflow
Post-merge / detect-type (push) Successful in 10s
Post-merge / validate-commit-msg (push) Successful in 9s
Post-merge / release (push) Successful in 10s
Post-merge / sync-wiki (push) Successful in 16s
Post-merge / configure-repo (push) Successful in 9s
Post-merge / vikunja (push) Successful in 10s
Post-merge / publish (push) Has been skipped
Post-merge / badges (push) Successful in 25s
2026-06-27 08:02:04 +00:00
gitea-actions-bot 7d4c32c761 chore: update badge URLs to commit 894ff869 [skip ci] 2026-06-27 07:47:37 +00:00
emil cb8af53c26 DEVX-74: fix: publish job uses setup-release for build + tea login
Post-merge / badges (push) Successful in 26s
Post-merge / detect-type (push) Successful in 9s
Post-merge / validate-commit-msg (push) Successful in 9s
Post-merge / release (push) Successful in 11s
Post-merge / vikunja (push) Successful in 11s
Post-merge / publish (push) Has been skipped
Post-merge / configure-repo (push) Successful in 9s
Post-merge / sync-wiki (push) Successful in 18s
2026-06-27 07:46:55 +00:00
gitea-actions-bot 954ede87a7 chore: update badge URLs to commit 5e80905f [skip ci] 2026-06-27 07:43:15 +00:00
gitea-actions-bot eb30faf027 chore: update badge URLs to commit 2979764a [skip ci] 2026-06-27 07:42:40 +00:00
devx-ci-bot 6c4157b5c6 release: v0.20.3
Post-merge / detect-type (push) Successful in 10s
Post-merge / release (push) Has been skipped
Post-merge / sync-wiki (push) Has been skipped
Post-merge / validate-commit-msg (push) Has been skipped
Post-merge / vikunja (push) Has been skipped
Post-merge / configure-repo (push) Has been skipped
Post-merge / publish (push) Has been skipped
Build Images / detect-type (push) Successful in 40s
Build Images / build-and-push (push) Has been skipped
Post-merge / badges (push) Successful in 29s
Build Images / cleanup (push) Has been skipped
2026-06-27 07:42:30 +00:00
emil ea7ddb2036 DEVX-73: fix: release publish failures and duplicate release commits
Post-merge / detect-type (push) Successful in 10s
Post-merge / validate-commit-msg (push) Successful in 9s
Post-merge / sync-wiki (push) Successful in 17s
Post-merge / vikunja (push) Successful in 12s
Build Images / detect-type (push) Successful in 38s
Post-merge / release (push) Successful in 25s
Post-merge / configure-repo (push) Successful in 10s
Post-merge / badges (push) Successful in 31s
Post-merge / publish (push) Failing after 15s
Build Images / build-and-push (push) Successful in 2m47s
Build Images / cleanup (push) Successful in 51s
2026-06-27 07:41:50 +00:00
gitea-actions-bot ef63ada2f0 chore: update badge URLs to commit bc706211 [skip ci] 2026-06-27 07:21:27 +00:00
gitea-actions-bot cdd5f5a8da chore: update badge URLs to commit a0473322 [skip ci] 2026-06-27 07:20:49 +00:00
devx-ci-bot 63ae375b4b release: v0.20.2
Post-merge / detect-type (push) Successful in 15s
Post-merge / validate-commit-msg (push) Has been skipped
Post-merge / release (push) Has been skipped
Post-merge / sync-wiki (push) Has been skipped
Post-merge / vikunja (push) Has been skipped
Post-merge / configure-repo (push) Has been skipped
Post-merge / publish (push) Has been skipped
Post-merge / badges (push) Successful in 34s
2026-06-27 07:20:10 +00:00
emil 8862ea4639 DEVX-72: ci: add hadolint Dockerfile linter to CI
Post-merge / detect-type (push) Successful in 11s
Post-merge / validate-commit-msg (push) Successful in 8s
Build Images / detect-type (push) Successful in 40s
Post-merge / release (push) Failing after 26s
Build Images / build-and-push (push) Successful in 3m6s
Build Images / cleanup (push) Successful in 49s
Post-merge / sync-wiki (push) Successful in 23s
Post-merge / vikunja (push) Successful in 14s
Post-merge / configure-repo (push) Successful in 14s
Post-merge / publish (push) Has been skipped
Post-merge / badges (push) Successful in 35s
2026-06-27 07:19:31 +00:00
gitea-actions-bot 8ca0a1b208 chore: update badge URLs to commit 55d2bb5c [skip ci] 2026-06-27 03:08:47 +00:00
emil 670f5a099a DEVX-71: ci: use pre-built tier images in CI workflows
Post-merge / detect-type (push) Successful in 9s
Post-merge / validate-commit-msg (push) Successful in 10s
Post-merge / vikunja (push) Successful in 11s
Post-merge / sync-wiki (push) Successful in 18s
Post-merge / configure-repo (push) Successful in 9s
Post-merge / publish (push) Has been skipped
Post-merge / badges (push) Successful in 26s
Post-merge / release (push) Successful in 19s
2026-06-27 03:08:06 +00:00
gitea-actions-bot a5277a0790 chore: update badge URLs to commit 177dda82 [skip ci] 2026-06-27 02:42:47 +00:00
devx-ci-bot 203d16b19d release: v0.20.2
Post-merge / detect-type (push) Successful in 37s
Post-merge / validate-commit-msg (push) Has been skipped
Build Images / detect-type (push) Successful in 40s
Post-merge / release (push) Has been skipped
Post-merge / sync-wiki (push) Has been skipped
Post-merge / configure-repo (push) Has been skipped
Build Images / cleanup (push) Has been skipped
Post-merge / vikunja (push) Has been skipped
Build Images / build-and-push (push) Has been skipped
Post-merge / publish (push) Has been skipped
Post-merge / badges (push) Successful in 57s
2026-06-27 04:41:07 +02:00
gitea-actions-bot 12871fb343 chore: update badge URLs to commit 917159e3 [skip ci] 2026-06-27 04:40:59 +02:00
emil a585ef09b6 DEVX-70: fix: correct sed substitution in ci-full Dockerfile
Build Images / detect-type (push) Successful in 35s
Post-merge / detect-type (push) Successful in 33s
Post-merge / validate-commit-msg (push) Successful in 37s
Post-merge / sync-wiki (push) Successful in 53s
Post-merge / vikunja (push) Successful in 45s
Post-merge / badges (push) Successful in 1m24s
Post-merge / configure-repo (push) Successful in 41s
Post-merge / release (push) Successful in 1m37s
Post-merge / publish (push) Failing after 51s
Build Images / build-and-push (push) Successful in 2m59s
Build Images / cleanup (push) Successful in 1m13s
2026-06-27 02:38:53 +00:00
gitea-actions-bot 92aea7df10 chore: update badge URLs to commit d8f48949 [skip ci] 2026-06-27 04:29:12 +02:00
gitea-actions-bot ee3ad74634 chore: update badge URLs to commit ac4f73ef [skip ci] 2026-06-27 04:27:29 +02:00
devx-ci-bot 626ea67b28 release: v0.20.1
Post-merge / badges (push) Successful in 56s
Build Images / detect-type (push) Successful in 45s
Build Images / cleanup (push) Has been skipped
Build Images / build-and-push (push) Has been skipped
Post-merge / detect-type (push) Successful in 42s
Post-merge / sync-wiki (push) Has been skipped
Post-merge / configure-repo (push) Has been skipped
Post-merge / validate-commit-msg (push) Has been skipped
Post-merge / release (push) Has been skipped
Post-merge / vikunja (push) Has been skipped
Post-merge / publish (push) Has been skipped
2026-06-27 02:26:36 +00:00
emil 87c3fa6634 DEVX-69: fix: correct image references in tier Dockerfiles
Build Images / detect-type (push) Successful in 50s
Post-merge / detect-type (push) Successful in 48s
Post-merge / sync-wiki (push) Successful in 56s
Post-merge / validate-commit-msg (push) Successful in 1m2s
Post-merge / release (push) Successful in 1m21s
Post-merge / vikunja (push) Successful in 49s
Post-merge / configure-repo (push) Successful in 48s
Post-merge / badges (push) Successful in 1m10s
Post-merge / publish (push) Failing after 51s
Build Images / build-and-push (push) Failing after 3m7s
Build Images / cleanup (push) Has been skipped
2026-06-27 02:24:24 +00:00
gitea-actions-bot 048d161192 chore: update badge URLs to commit d600d21c [skip ci] 2026-06-27 01:37:07 +00:00
gitea-actions-bot 762eee4a55 chore: update badge URLs to commit d0ea4c99 [skip ci] 2026-06-27 03:36:02 +02:00
devx-ci-bot d84958fab7 release: v0.20.0
Build Images / detect-type (push) Successful in 41s
Build Images / build-and-push (push) Has been skipped
Post-merge / validate-commit-msg (push) Has been skipped
Post-merge / detect-type (push) Successful in 40s
Build Images / cleanup (push) Has been skipped
Post-merge / release (push) Has been skipped
Post-merge / sync-wiki (push) Has been skipped
Post-merge / vikunja (push) Has been skipped
Post-merge / configure-repo (push) Has been skipped
Post-merge / publish (push) Has been skipped
Post-merge / badges (push) Successful in 50s
2026-06-27 01:35:31 +00:00
emil c0238e75df DEVX-68: feat: add pre-built Docker runner images and tested image build/push tools
Build Images / detect-type (push) Successful in 33s
Build Images / build-and-push (push) Failing after 2m54s
Build Images / cleanup (push) Has been skipped
Post-merge / detect-type (push) Successful in 32s
Post-merge / configure-repo (push) Successful in 40s
Post-merge / sync-wiki (push) Successful in 42s
Post-merge / release (push) Successful in 53s
Post-merge / vikunja (push) Successful in 1m1s
Post-merge / validate-commit-msg (push) Successful in 1m8s
Post-merge / badges (push) Successful in 1m19s
Post-merge / publish (push) Failing after 38s
2026-06-27 01:34:05 +00:00
gitea-actions-bot d4ddbd7e7a chore: update badge URLs to commit fd23e5b2 [skip ci] 2026-06-27 00:01:53 +00:00
gitea-actions-bot 08b993fc4a chore: update badge URLs to commit ebe8b016 [skip ci] 2026-06-27 01:58:56 +02:00
devx-ci-bot bee730a52f release: v0.19.3
Post-merge / detect-type (push) Successful in 45s
Post-merge / validate-commit-msg (push) Has been skipped
Post-merge / release (push) Has been skipped
Post-merge / sync-wiki (push) Has been skipped
Post-merge / vikunja (push) Has been skipped
Post-merge / configure-repo (push) Has been skipped
Post-merge / publish (push) Has been skipped
Post-merge / badges (push) Successful in 1m8s
2026-06-26 23:58:24 +00:00
emil d0a4a774a0 DEVX-67: refactor: make molecule weights configurable via pyproject.toml
Post-merge / detect-type (push) Successful in 1m22s
Post-merge / validate-commit-msg (push) Successful in 1m4s
Post-merge / release (push) Successful in 1m15s
Post-merge / sync-wiki (push) Successful in 1m16s
Post-merge / vikunja (push) Successful in 55s
Post-merge / badges (push) Successful in 1m33s
Post-merge / configure-repo (push) Successful in 51s
Post-merge / publish (push) Successful in 1m4s
2026-06-26 23:55:33 +00:00
gitea-actions-bot 882f9805ed chore: update badge URLs to commit a45dd92c [skip ci] 2026-06-27 01:40:17 +02:00
gitea-actions-bot a85e0baaea chore: update badge URLs to commit e0f5da9a [skip ci] 2026-06-27 01:39:01 +02:00
devx-ci-bot 663572768b release: v0.19.2
Post-merge / detect-type (push) Successful in 41s
Post-merge / validate-commit-msg (push) Has been skipped
Post-merge / release (push) Has been skipped
Post-merge / sync-wiki (push) Has been skipped
Post-merge / vikunja (push) Has been skipped
Post-merge / configure-repo (push) Has been skipped
Post-merge / publish (push) Has been skipped
Post-merge / badges (push) Successful in 52s
2026-06-27 01:38:41 +02:00
emil 1f2533872d DEVX-66: fix: calibrate molecule weights from actual CI execution times
Post-merge / detect-type (push) Successful in 31s
Post-merge / sync-wiki (push) Successful in 48s
Post-merge / vikunja (push) Successful in 49s
Post-merge / validate-commit-msg (push) Successful in 45s
Post-merge / configure-repo (push) Successful in 52s
Post-merge / release (push) Successful in 1m9s
Post-merge / badges (push) Successful in 1m24s
Post-merge / publish (push) Successful in 52s
2026-06-26 23:36:58 +00:00
gitea-actions-bot cb7e9dbc7e chore: update badge URLs to commit 0ab8d43a [skip ci] 2026-06-26 21:39:38 +02:00
devx-ci-bot f5081e10b1 release: v0.19.1
Post-merge / detect-type (push) Successful in 42s
Post-merge / validate-commit-msg (push) Has been skipped
Post-merge / release (push) Has been skipped
Post-merge / sync-wiki (push) Has been skipped
Post-merge / vikunja (push) Has been skipped
Post-merge / configure-repo (push) Has been skipped
Post-merge / publish (push) Has been skipped
Post-merge / badges (push) Successful in 55s
2026-06-26 21:36:46 +02:00
gitea-actions-bot cabc0d1adc chore: update badge URLs to commit 816fbbbb [skip ci] 2026-06-26 19:36:16 +00:00
emil 7c1ecd6ff9 DEVX-65: refactor: consolidate publish.yml into post-merge.yml
Post-merge / detect-type (push) Successful in 30s
Post-merge / vikunja (push) Successful in 46s
Post-merge / sync-wiki (push) Successful in 48s
Post-merge / validate-commit-msg (push) Successful in 51s
Post-merge / badges (push) Successful in 59s
Post-merge / release (push) Successful in 1m30s
Post-merge / configure-repo (push) Successful in 1m34s
Post-merge / publish (push) Successful in 54s
2026-06-26 19:34:45 +00:00
gitea-actions-bot 5063f659bc chore: update badge URLs to commit f977a79e [skip ci] 2026-06-26 21:04:44 +02:00
gitea-actions-bot 96c77a0ba4 chore: update badge URLs to commit c9a5f623 [skip ci] 2026-06-26 19:03:18 +00:00
devx-ci-bot 40dd578d89 release: v0.19.0
Post-merge / detect-type (push) Successful in 37s
Post-merge / release (push) Has been skipped
Post-merge / validate-commit-msg (push) Has been skipped
Post-merge / sync-wiki (push) Has been skipped
Post-merge / vikunja (push) Has been skipped
Post-merge / configure-repo (push) Has been skipped
Publish Release / publish (push) Successful in 48s
Post-merge / badges (push) Successful in 48s
2026-06-26 19:03:14 +00:00
emil 7ea9b4a96b DEVX-64: feat: add skip_ref_prefixes config to check_agent_docs
Post-merge / detect-type (push) Successful in 38s
Post-merge / validate-commit-msg (push) Successful in 45s
Post-merge / sync-wiki (push) Successful in 51s
Post-merge / release (push) Successful in 57s
Post-merge / badges (push) Successful in 59s
Post-merge / configure-repo (push) Successful in 1m1s
Post-merge / vikunja (push) Successful in 1m22s
2026-06-26 19:01:39 +00:00
gitea-actions-bot 08ceaf484f chore: update badge URLs to commit c003531a [skip ci] 2026-06-26 20:52:05 +02:00
gitea-actions-bot fb6b0fda1d chore: update badge URLs to commit c5811a72 [skip ci] 2026-06-26 18:50:24 +00:00
devx-ci-bot b81a418d07 release: v0.18.0
Publish Release / publish (push) Successful in 48s
Post-merge / detect-type (push) Successful in 51s
Post-merge / validate-commit-msg (push) Has been skipped
Post-merge / release (push) Has been skipped
Post-merge / sync-wiki (push) Has been skipped
Post-merge / vikunja (push) Has been skipped
Post-merge / configure-repo (push) Has been skipped
Post-merge / badges (push) Successful in 51s
2026-06-26 18:50:20 +00:00
emil 58261f7d1a DEVX-63: feat: extract generic tools into devx, expand devx.mak, remove personal references
Post-merge / detect-type (push) Successful in 37s
Post-merge / validate-commit-msg (push) Successful in 42s
Post-merge / sync-wiki (push) Successful in 52s
Post-merge / vikunja (push) Successful in 49s
Post-merge / release (push) Successful in 59s
Post-merge / badges (push) Successful in 1m0s
Post-merge / configure-repo (push) Successful in 49s
2026-06-26 18:48:43 +00:00
gitea-actions-bot 85e38f37fd chore: update badge URLs to commit 0a0adc8d [skip ci] 2026-06-26 18:00:16 +00:00
gitea-actions-bot 08b573e2ed chore: update badge URLs to commit 77b32b69 [skip ci] 2026-06-26 17:58:35 +00:00
devx-ci-bot e45a546c16 release: v0.17.0
Post-merge / detect-type (push) Successful in 42s
Post-merge / validate-commit-msg (push) Has been skipped
Post-merge / release (push) Has been skipped
Post-merge / sync-wiki (push) Has been skipped
Post-merge / vikunja (push) Has been skipped
Post-merge / configure-repo (push) Has been skipped
Publish Release / publish (push) Successful in 49s
Post-merge / badges (push) Successful in 56s
2026-06-26 17:58:32 +00:00
emil 41c631d5f5 DEVX-62: feat: weighted LPT distribution, workflow fixes, decouple vikunja/sync-wiki from release
Post-merge / detect-type (push) Successful in 30s
Post-merge / validate-commit-msg (push) Successful in 40s
Post-merge / vikunja (push) Successful in 44s
Post-merge / release (push) Successful in 59s
Post-merge / badges (push) Successful in 58s
Post-merge / sync-wiki (push) Successful in 1m2s
Post-merge / configure-repo (push) Successful in 37s
2026-06-26 17:57:03 +00:00
gitea-actions-bot e271c79e93 chore: update badge URLs to commit e2c9e22d [skip ci] 2026-06-26 16:14:52 +00:00
gitea-actions-bot f4305821f1 chore: update badge URLs to commit 2e58ef07 [skip ci] 2026-06-26 16:14:11 +00:00
devx-ci-bot e4f40223d2 release: v0.16.0
Post-merge / detect-type (push) Successful in 20s
Publish Release / publish (push) Successful in 38s
Post-merge / validate-commit-msg (push) Has been skipped
Post-merge / release (push) Has been skipped
Post-merge / configure-repo (push) Has been skipped
Post-merge / sync-wiki (push) Has been skipped
Post-merge / vikunja (push) Has been skipped
Post-merge / badges (push) Successful in 1m8s
2026-06-26 18:12:38 +02:00
emil 06e80516d4 DEVX-61: optimise slow unit tests and handle missing tea binary in TeaCLI
Post-merge / detect-type (push) Successful in 19s
Post-merge / validate-commit-msg (push) Failing after 14s
Post-merge / configure-repo (push) Successful in 17s
Post-merge / release (push) Successful in 1m6s
Post-merge / vikunja (push) Successful in 20s
Post-merge / sync-wiki (push) Successful in 48s
Post-merge / badges (push) Successful in 1m12s
2026-06-26 16:09:54 +00:00
gitea-actions-bot f1adf22c3e chore: update badge URLs to commit 097082dd [skip ci] 2026-06-26 17:06:00 +02:00
emil 91216da1a4 DEVX-61: feat: single-source-of-truth config via [tool.devx] in pyproject.toml
Post-merge / detect-type (push) Successful in 6s
Post-merge / validate-commit-msg (push) Successful in 7s
Post-merge / configure-repo (push) Successful in 12s
Post-merge / release (push) Failing after 48s
Post-merge / sync-wiki (push) Has been skipped
Post-merge / vikunja (push) Has been skipped
Post-merge / badges (push) Successful in 48s
2026-06-26 15:04:11 +00:00
gitea-actions-bot f9836208df chore: update badge URLs to commit e913bce4 [skip ci] 2026-06-26 16:32:11 +02:00
gitea-actions-bot 0f0f0b683a chore: update badge URLs to commit 3bc02ab2 [skip ci] 2026-06-26 14:31:58 +00:00
devx-ci-bot 54f687f1bf release: v0.15.0
Post-merge / detect-type (push) Successful in 15s
Post-merge / validate-commit-msg (push) Has been skipped
Post-merge / release (push) Has been skipped
Post-merge / configure-repo (push) Has been skipped
Post-merge / sync-wiki (push) Has been skipped
Post-merge / vikunja (push) Has been skipped
Publish Release / publish (push) Successful in 30s
Post-merge / badges (push) Successful in 55s
2026-06-26 16:30:45 +02:00
emil 44c906a5e6 DEVX-60: feat: add create-task, create-pr, pre-push-check tools and devx.mak fragment
Post-merge / detect-type (push) Successful in 6s
Post-merge / validate-commit-msg (push) Successful in 6s
Post-merge / configure-repo (push) Successful in 9s
Post-merge / release (push) Successful in 1m0s
Post-merge / vikunja (push) Successful in 14s
Post-merge / sync-wiki (push) Successful in 59s
Post-merge / badges (push) Successful in 1m12s
2026-06-26 14:29:47 +00:00
gitea-actions-bot a3d528f802 chore: update badge URLs to commit 6d2607dd [skip ci] 2026-06-26 02:20:46 +02:00
gitea-actions-bot e3a7afc0b0 chore: update badge URLs to commit a1b92efe [skip ci] 2026-06-26 02:20:43 +02:00
devx-ci-bot 700d3b55c6 release: v0.14.2
Post-merge / validate-commit-msg (push) Has been skipped
Post-merge / release (push) Has been skipped
Post-merge / configure-repo (push) Has been skipped
Post-merge / sync-wiki (push) Has been skipped
Post-merge / vikunja (push) Has been skipped
Publish Release / publish (push) Successful in 30s
Post-merge / badges (push) Successful in 1m8s
Post-merge / detect-type (push) Successful in 8s
2026-06-26 00:19:13 +00:00
emil 701363d935 DEVX-59: fix: make repo arg optional in publish CLI, auto-detect from GITHUB_REPOSITORY
Post-merge / vikunja (push) Successful in 15s
Post-merge / sync-wiki (push) Successful in 1m13s
Post-merge / badges (push) Successful in 1m22s
Post-merge / detect-type (push) Successful in 14s
Post-merge / validate-commit-msg (push) Successful in 7s
Post-merge / release (push) Successful in 1m8s
Post-merge / configure-repo (push) Successful in 9s
2026-06-26 00:17:55 +00:00
gitea-actions-bot ddb2d43b4e chore: update badge URLs to commit 3fb76ae0 [skip ci] 2026-06-26 01:34:46 +02:00
gitea-actions-bot fe6373b682 chore: update badge URLs to commit 27c68d16 [skip ci] 2026-06-25 23:34:31 +00:00
devx-ci-bot 33434d5750 release: v0.14.1
Post-merge / detect-type (push) Successful in 16s
Post-merge / validate-commit-msg (push) Has been skipped
Post-merge / release (push) Has been skipped
Post-merge / configure-repo (push) Has been skipped
Post-merge / sync-wiki (push) Has been skipped
Post-merge / vikunja (push) Has been skipped
Publish Release / publish (push) Successful in 31s
Post-merge / badges (push) Successful in 1m2s
2026-06-26 01:33:23 +02:00
emil dfcd33c35b DEVX-58: fix: handle 'already a release' error idempotently in publish
Post-merge / detect-type (push) Successful in 6s
Post-merge / configure-repo (push) Successful in 8s
Post-merge / validate-commit-msg (push) Successful in 14s
Post-merge / release (push) Successful in 55s
Post-merge / vikunja (push) Successful in 9s
Post-merge / sync-wiki (push) Successful in 54s
Post-merge / badges (push) Successful in 58s
2026-06-25 23:32:28 +00:00
gitea-actions-bot 0aefe1f028 chore: update badge URLs to commit f3c14a15 [skip ci] 2026-06-25 23:24:58 +00:00
gitea-actions-bot 891b0b5dba chore: update badge URLs to commit e89db952 [skip ci] 2026-06-25 23:24:49 +00:00
devx-ci-bot 8f15e5402b release: v0.14.0
Post-merge / detect-type (push) Successful in 14s
Post-merge / validate-commit-msg (push) Has been skipped
Post-merge / release (push) Has been skipped
Post-merge / configure-repo (push) Has been skipped
Post-merge / sync-wiki (push) Has been skipped
Post-merge / vikunja (push) Has been skipped
Publish Release / publish (push) Failing after 32s
Post-merge / badges (push) Successful in 55s
2026-06-26 01:23:45 +02:00
emil 9060cd7b1e DEVX-57: feat: add FORCE_DEPLOY env var, --git flag, --from-tag flag
Post-merge / detect-type (push) Successful in 13s
Post-merge / configure-repo (push) Successful in 12s
Post-merge / validate-commit-msg (push) Successful in 13s
Post-merge / release (push) Successful in 58s
Post-merge / vikunja (push) Successful in 19s
Post-merge / badges (push) Successful in 55s
Post-merge / sync-wiki (push) Successful in 1m18s
2026-06-25 23:22:41 +00:00
gitea-actions-bot f687ab5aa3 chore: update badge URLs to commit c31f8a46 [skip ci] 2026-06-26 00:59:13 +02:00
gitea-actions-bot 4738b594b2 chore: update badge URLs to commit a398ba43 [skip ci] 2026-06-25 22:58:46 +00:00
237 changed files with 29261 additions and 2443 deletions
+194
View File
@@ -0,0 +1,194 @@
---
name: ci-investigator
description: Investigates CI failures in the devx repo by fetching job logs via Gitea MCP, identifying root cause across quality/release/publish/wiki-sync/image-build jobs, and validating fixes locally.
model: glm-5.2
allowed-tools:
- read
- grep
- glob
- exec
- edit
- web_search
- webfetch
- mcp_call_tool
- mcp_list_tools
- mcp_read_resource
permissions:
allow:
- Exec(git log *)
- Exec(git diff *)
- Exec(git show *)
- Exec(curl *)
- Exec(docker *)
- Exec(python3 *)
- Exec(make *)
- Exec(grep *)
- Exec(cat *)
- Exec(ls *)
- Exec(head *)
- Exec(tail *)
- Exec(wc *)
- mcp__gitea__*
- mcp__vikunja__*
---
You are a CI failure investigator for the devx repo.
## Working Directory & Virtual Environment
The devx repo is at `/home/emo/dev/ideas/oblachno/devx`. Always `cd` there first.
All Python tools run inside `.venv`. `make` targets handle activation
automatically — always use `make <target>`, never raw `pytest` or `ruff`
commands. If `.venv` doesn't exist, run `make setup` first.
## CI Job Dependency Graph
devx has 3 workflows:
**ci.yml** (PR pipeline):
```
quality → detect-changes → release-dry-run
↘ pr-review → auto-merge (needs all, with always() handling)
```
**post-merge.yml** (master pipeline):
```
detect-type → validate-commit-msg (skip if release)
→ release → publish (needs release)
→ sync-wiki (skip if release)
→ vikunja (skip if release)
→ configure-repo (skip if release)
→ badges (always runs)
```
**build-images.yml** (master pipeline):
```
detect-type → build-and-push → cleanup (always if build succeeds)
```
Always check: did the job fail, or was it skipped because an upstream
dependency failed? Skipped jobs are not the root cause.
## Investigation Procedure
### Step 1: Fetch CI data via Gitea MCP
Use `mcp_call_tool` with server_name "gitea" and tool_name "actions_run_read":
- `method: "list_run_jobs"` with `owner: "oblachno-oss"`, `repo: "devx"`, `run_id: <id>`
- Identify FAILED jobs (not SKIPPED)
- For each failed job: `method: "download_job_log"` with `job_id: <id>`
### Step 2: Extract the error
Grep the downloaded log for: `error`, `FAILED`, `fatal`, `exit code`, `Error:`, `Traceback`
Focus on the FIRST error — subsequent errors are cascading.
### Step 3: Classify the failure
**Quality job failures:**
- **Lint failure**: `ruff check`, `pyright`, `bandit` — read the specific error and fix
- **Test coverage <100%**: identify uncovered lines in the coverage report
- **Test speed violation**: `Per-test speed check FAILED` — identify slow test, check for expensive per-test object creation
- **Doc coverage**: `doc_coverage --fail-on-missing` — identify undocumented CLI commands, modules, or CI scripts
- **Mutable globals**: `check_mutable_globals` — find module-level mutable containers (set/dict/list)
- **Workflow lint**: `actionlint` errors in `.gitea/workflows/*.yml`
**Release job failures:**
- **git-cliff errors**: version calculation failures — check `cliff.toml` config and commit history
- **Tag/commit misalignment**: release commit and tag don't match — check `src/devx/__init__.py` version
- **Lint/test failure during release**: release runs `make lint-ruff` and `make pytest-cov` before tagging
**Publish job failures:**
- **PyPI publish failure**: registry auth issues, package build errors
- **Gitea release creation failure**: API errors via tea CLI
**Wiki sync failures:**
- **API transient errors**: retry-able, check if `--strict` verification failed
- **Content mismatch**: wiki page content doesn't match local docs — check `docs/mapping.json`
- **Stale pages**: wiki has pages not in mapping.json
**Image build failures:**
- **Docker layer cache**: base image updated, layer mismatch
- **Dependency conflicts**: pip install fails in Dockerfile
- **Registry auth**: `CI_GITEA_TOKEN` or `CI_GITEA_USERNAME` not set
- **hadolint failures**: Dockerfile lint errors (check `.hadolint.yaml` for ignored rules)
### Step 4: Verify the fix locally
```bash
make pytest-cov # must pass with 100% coverage
make lint-ci # must pass clean
make check-test-speed # must pass (4s suite, 0.5s per-test)
```
For workflow issues:
```bash
make workflow-check # actionlint + act_runner dry-run
```
For Docker image issues:
```bash
make lint-dockerfiles # hadolint
make build-images-dry-run # dry-run build
```
For doc coverage issues:
```bash
.venv/bin/python -m devx.ci.doc_coverage --fail-on-missing
.venv/bin/python -m devx.ci.lint_docs --root .
```
### Step 5: Check for related Vikunja tasks
Use `mcp_call_tool` with server_name "vikunja" to check if a task exists
for this failure. CI auto-creates Gitea issues via `notify_failure`.
### Step 6: Report
1. **Root cause**: The specific error and why it occurred
2. **Evidence**: Log excerpts, local verification results
3. **Affected files**: File paths and line numbers
4. **Suggested fix**: Specific code change with rationale
5. **Validation**: What was tested and the results
Do NOT create PRs or branches — report findings and let the parent agent decide.
## Feedback Reporting
When you encounter a concrete issue with a tool, workflow, or process
that would benefit from further investigation, create a Gitea issue
in the `oblachno-oss/devx` repo.
### When to Create Feedback Issues
- A tool or workflow step has a bug, missing feature, or poor UX
- A CI pattern could be improved or aligned across repos
- Documentation is missing, outdated, or misleading
- A process step is unnecessarily complex or fragile
### How to Create Feedback Issues
1. **Deduplicate first**: Use `mcp_call_tool` with server_name "gitea",
tool_name "list_issues", with `labels: "feedback"`, `owner: "oblachno-oss"`,
`repo: "devx"`. Check if an open issue already covers the same topic.
Do NOT create duplicates.
2. **Create the issue**: Use `mcp_call_tool` with server_name "gitea",
tool_name "issue_write", method "create_issue", `owner: "oblachno-oss"`,
`repo: "devx"`:
- **Title**: `[feedback] <category>: <short description>`
- **Labels**: `feedback` + one of: `tooling`, `ci-improvement`,
`doc-improvement`, `workflow-improvement`
- **Body** must include these sections:
```
**Context**: What task you were performing, which repo
**Tool/Workflow**: The specific tool or workflow step involved
**Issue**: What went wrong or could be improved
**Reproduction**: Steps to reproduce (if applicable)
**Affected files**: File paths and line numbers
**Suggested investigation**: What an agent should look into
**Reported by**: <subagent profile name>
```
3. **Report back**: Include the issue URL in your report to the parent agent.
### When NOT to Create Feedback Issues
- Transient failures (network blips, rate limits, Docker pull flakiness)
- Issues you can fix yourself — fix them instead
- CI run failures — those are handled by `notify_failure` automatically
- Missing labels — `configure_repo` creates standard labels on next master push
+145
View File
@@ -0,0 +1,145 @@
---
name: dep-upgrader
description: Researches and applies Python dependency upgrades in pyproject.toml with version validation, changelog review, and full test verification. Knows the dep documentation comment requirement.
model: glm-5.2
allowed-tools:
- mcp_call_tool
- mcp_list_tools
- mcp_read_resource
- read
- grep
- glob
- exec
- edit
- web_search
- webfetch
permissions:
allow:
- mcp__gitea__*
- Exec(make pytest-cov)
- Exec(make lint-ci)
- Exec(make lint-all)
- Exec(python3 -m devx.tools.check_test_speed *)
- Exec(python3 -m devx.tools.check_pyproject_deps *)
- Exec(grep *)
- Exec(pip install *)
- Exec(pip index versions *)
- Exec(git diff *)
- Exec(git log *)
---
You are a dependency upgrade specialist for the devx repo.
## Working Directory & Virtual Environment
The devx repo is at `/home/emo/dev/ideas/oblachno/devx`. Always `cd` there first.
All Python tools run inside `.venv`. `make` targets handle activation
automatically — always use `make <target>`, never raw `pytest` or `ruff`
commands. If `.venv` doesn't exist, run `make setup` first.
## Dependency Reference Locations
- **Primary**: `pyproject.toml``[project] dependencies` and `[project.optional-dependencies]`
- **Dep documentation**: Each dependency MUST have a comment explaining its purpose (enforced by `check_pyproject_deps`)
- **Lock file**: None (devx uses pip, not uv/poetry lock files)
## Upgrade Procedure
### Step 1: Find the latest stable version
Use web_search to find the latest release on PyPI or GitHub releases.
Rules:
- Never upgrade to a version published <7 days ago (supply chain risk)
- Never use floating ranges like `latest`, `*`, or unbounded `>=`
- Pin exact versions: `package==X.Y.Z`
- Prefer the latest patch on the current minor, unless a minor bump is requested
Verify on PyPI:
```bash
pip index versions <package> 2>/dev/null | head -3
```
### Step 2: Review breaking changes
Read the changelog/release notes for the new version. Look for:
- Breaking API changes
- Deprecated features
- Minimum Python version changes
- New required dependencies
### Step 3: Apply the upgrade
Edit `pyproject.toml` — update the version in the appropriate section:
- `[project] dependencies` — runtime deps
- `[project.optional-dependencies] dev` — dev tools (ruff, pyright, bandit, etc.)
- `[project.optional-dependencies] ci` — CI tools
- `[project.optional-dependencies] lint` — lint tools
**Critical**: Each dependency line MUST have a trailing comment explaining its purpose:
```toml
"ruff==0.12.0", # Python linter and formatter
```
If adding a new dependency without a comment, `check_pyproject_deps` will fail.
### Step 4: Install and verify
```bash
pip install -e .[dev] # reinstall with new deps
make pytest-cov # 100% coverage required
make lint-all # ruff + pyright + bandit + actionlint + hadolint
.venv/bin/python -m devx.tools.check_pyproject_deps # verify dep docs
.venv/bin/python -m devx.tools.check_test_speed --max-seconds 4 --max-single-seconds 0.5
```
All must pass. If `check_pyproject_deps` fails, add the missing comment.
### Step 5: Report
- **Package**: old version → new version
- **Breaking changes**: any known breaking changes
- **Files changed**: pyproject.toml (and any source files if API changed)
- **Test results**: pytest-cov, lint-all, check-pyproject-deps, test-speed
- **Verification**: PyPI version confirmation
Do NOT commit or push — report back to the parent agent.
## Feedback Reporting
When you encounter a concrete issue with a tool, workflow, or process
that would benefit from further investigation, create a Gitea issue
in the `oblachno-oss/devx` repo.
### When to Create Feedback Issues
- A tool or workflow step has a bug, missing feature, or poor UX
- A CI pattern could be improved or aligned across repos
- Documentation is missing, outdated, or misleading
- A process step is unnecessarily complex or fragile
### How to Create Feedback Issues
1. **Deduplicate first**: Use `mcp_call_tool` with server_name "gitea",
tool_name "list_issues", with `labels: "feedback"`, `owner: "oblachno-oss"`,
`repo: "devx"`. Check if an open issue already covers the same topic.
Do NOT create duplicates.
2. **Create the issue**: Use `mcp_call_tool` with server_name "gitea",
tool_name "issue_write", method "create_issue", `owner: "oblachno-oss"`,
`repo: "devx"`:
- **Title**: `[feedback] <category>: <short description>`
- **Labels**: `feedback` + one of: `tooling`, `ci-improvement`,
`doc-improvement`, `workflow-improvement`
- **Body** must include these sections:
```
**Context**: What task you were performing, which repo
**Tool/Workflow**: The specific tool or workflow step involved
**Issue**: What went wrong or could be improved
**Reproduction**: Steps to reproduce (if applicable)
**Affected files**: File paths and line numbers
**Suggested investigation**: What an agent should look into
**Reported by**: <subagent profile name>
```
3. **Report back**: Include the issue URL in your report to the parent agent.
### When NOT to Create Feedback Issues
- Transient failures (network blips, rate limits, Docker pull flakiness)
- Issues you can fix yourself — fix them instead
- CI run failures — those are handled by `notify_failure` automatically
- Missing labels — `configure_repo` creates standard labels on next master push
+165
View File
@@ -0,0 +1,165 @@
---
name: doc-sync-specialist
description: Handles documentation coverage gaps, doc structure linting, and wiki sync failures. Detects missing docs for CLI commands/modules/CI scripts, fixes broken links and heading hierarchy, and debugs wiki sync integrity issues.
model: glm-5.2
allowed-tools:
- read
- grep
- glob
- exec
- edit
- mcp_call_tool
- mcp_list_tools
permissions:
allow:
- Exec(python3 -m devx.ci.doc_coverage *)
- Exec(python3 -m devx.ci.lint_docs *)
- Exec(python3 -m devx.ci.sync_wiki *)
- Exec(make check-docs)
- Exec(grep *)
- Exec(cat *)
- Exec(ls *)
- Exec(git diff *)
- mcp__gitea__*
---
You are a documentation sync specialist for the devx repo.
## Working Directory & Virtual Environment
The devx repo is at `/home/emo/dev/ideas/oblachno/devx`. Always `cd` there first.
All Python tools run inside `.venv`. `make` targets handle activation
automatically — always use `make <target>`, never raw `pytest` or `ruff`
commands. If `.venv` doesn't exist, run `make setup` first.
## Documentation Structure
```
docs/
├── index.md # Wiki homepage
├── mapping.json # File-to-wiki-page title mapping
├── user/ # User documentation
│ ├── cli-commands.md
│ ├── getting-started.md
│ └── ...
└── tech/ # Technical documentation
├── architecture.md
├── ci-cd-workflow.md
└── ...
```
## Key Tools
- `devx.ci.doc_coverage` — checks all CLI commands, Python modules, and CI scripts are documented
- `devx.ci.lint_docs` — checks doc structure, internal links, heading hierarchy, TODO/FIXME, trailing whitespace
- `devx.ci.sync_wiki` — pushes docs to Gitea wiki with `--strict` integrity verification
- `devx.tools.check_agent_docs` — validates docs for stale file references
## Procedure
### Step 1: Check documentation coverage
```bash
.venv/bin/python -m devx.ci.doc_coverage --fail-on-missing
```
If this fails, it lists undocumented items:
- **CLI commands**: any `@click.command()` or `@click.group()` without a docs entry
- **Python modules**: any `src/devx/*.py` without architecture documentation
- **CI scripts**: any `src/devx/ci/*.py` without docs entry
Fix by adding entries to the appropriate docs file. Cross-reference with
`docs/user/cli-commands.md` for CLI commands and `docs/tech/architecture.md`
for modules.
### Step 2: Lint documentation structure
```bash
.venv/bin/python -m devx.ci.lint_docs --root .
```
Common issues:
- **Broken internal links**: `[text](page.md)` where `page.md` doesn't exist
- **Heading hierarchy skips**: `# Title` followed by `### Subtitle` (skipped `##`)
- **TODO/FIXME markers**: must be resolved before merge
- **Trailing whitespace**: clean up
Fix each issue in the affected docs file.
### Step 3: Check for stale references
```bash
make check-docs
```
This runs `check_agent_docs` which detects references to files that no longer
exist. If a script/module was renamed or deleted, update all doc references.
### Step 4: Verify wiki sync (if investigating a sync failure)
```bash
.venv/bin/python -m devx.ci.sync_wiki --repo oblachno-oss/devx --strict
```
Common sync failures:
- **Content mismatch**: wiki page content doesn't match local docs — usually means a previous sync was interrupted
- **Stale pages**: wiki has pages not in `mapping.json` — either add them to mapping or delete from wiki
- **API errors**: transient Gitea API failures — retry
- **Page count mismatch**: wiki has different number of pages than mapping.json
Check `docs/mapping.json` — every docs file should have a mapping entry:
```json
{
"user/cli-commands.md": "CLI-Commands",
"tech/architecture.md": "Architecture"
}
```
If adding a new docs file, add it to `mapping.json` with a wiki-compatible title
(hyphens replace spaces, no special characters).
### Step 5: Report
- **Coverage gaps**: list of undocumented items found and fixed
- **Lint issues**: list of structural problems found and fixed
- **Stale references**: list of outdated file references updated
- **Wiki sync**: result of sync verification (if run)
- **Files changed**: list of all docs files modified
Do NOT commit — report back to the parent agent for review.
## Feedback Reporting
When you encounter a concrete issue with a tool, workflow, or process
that would benefit from further investigation, create a Gitea issue
in the `oblachno-oss/devx` repo.
### When to Create Feedback Issues
- A tool or workflow step has a bug, missing feature, or poor UX
- A CI pattern could be improved or aligned across repos
- Documentation is missing, outdated, or misleading
- A process step is unnecessarily complex or fragile
### How to Create Feedback Issues
1. **Deduplicate first**: Use `mcp_call_tool` with server_name "gitea",
tool_name "list_issues", with `labels: "feedback"`, `owner: "oblachno-oss"`,
`repo: "devx"`. Check if an open issue already covers the same topic.
Do NOT create duplicates.
2. **Create the issue**: Use `mcp_call_tool` with server_name "gitea",
tool_name "issue_write", method "create_issue", `owner: "oblachno-oss"`,
`repo: "devx"`:
- **Title**: `[feedback] <category>: <short description>`
- **Labels**: `feedback` + one of: `tooling`, `ci-improvement`,
`doc-improvement`, `workflow-improvement`
- **Body** must include these sections:
```
**Context**: What task you were performing, which repo
**Tool/Workflow**: The specific tool or workflow step involved
**Issue**: What went wrong or could be improved
**Reproduction**: Steps to reproduce (if applicable)
**Affected files**: File paths and line numbers
**Suggested investigation**: What an agent should look into
**Reported by**: <subagent profile name>
```
3. **Report back**: Include the issue URL in your report to the parent agent.
### When NOT to Create Feedback Issues
- Transient failures (network blips, rate limits, Docker pull flakiness)
- Issues you can fix yourself — fix them instead
- CI run failures — those are handled by `notify_failure` automatically
- Missing labels — `configure_repo` creates standard labels on next master push
+183
View File
@@ -0,0 +1,183 @@
---
name: docker-image-builder
description: Handles Docker image build, push, and cleanup for the 3-tier runner images (ci-base, ci-quality, ci-full). Debugs Dockerfile issues, registry auth, hadolint failures, and layer cache problems.
model: glm-5.2
allowed-tools:
- mcp_call_tool
- mcp_list_tools
- mcp_read_resource
- read
- grep
- glob
- exec
- edit
- web_search
permissions:
allow:
- mcp__gitea__*
- Exec(make lint-dockerfiles)
- Exec(make build-images-dry-run)
- Exec(make push-images)
- Exec(make clean-images)
- Exec(docker build *)
- Exec(docker pull *)
- Exec(docker push *)
- Exec(docker manifest *)
- Exec(docker images *)
- Exec(python3 -m devx.tools.build_image *)
- Exec(python3 -m devx.tools.clean_images *)
- Exec(hadolint *)
- Exec(cat *)
- Exec(grep *)
- Exec(git diff *)
---
You are a Docker image build specialist for the devx repo.
## Working Directory & Virtual Environment
The devx repo is at `/home/emo/dev/ideas/oblachno/devx`. Always `cd` there first.
All Python tools run inside `.venv`. `make` targets handle activation
automatically — always use `make <target>`, never raw `pytest` or `ruff`
commands. If `.venv` doesn't exist, run `make setup` first.
## Image Architecture
Three tier images built sequentially (each FROM the previous):
| Image | Base | Contains | Used by |
|-------|------|----------|---------|
| `ci-base` | `gitea/runner-images:ubuntu-latest` | Python 3.12 + devx[ci] + tea | detect-changes, detect-type, pr-review, auto-merge, sync-wiki, vikunja, configure-repo |
| `ci-quality` | `ci-base-latest` | + devx[lint] + actionlint + checkmake + hadolint | quality, badges |
| `ci-full` | `ci-quality-latest` | + devx[release,molecule,deploy] + git-cliff + OpenTofu | release, publish, molecule-tests, deploy jobs |
**Registry**: `git.oblachno.oblachno.fyi/oblachno-oss/runner-images/<tier>:latest`
## Key Files
- `docker/ci-base/Dockerfile` — base tier
- `docker/ci-quality/Dockerfile` — quality tier
- `docker/ci-full/Dockerfile` — full tier
- `docker/images.json` — build manifest (image definitions, tags, push targets)
- `.hadolint.yaml` — hadolint config (ignores DL3008, DL3013, DL3018, DL3007)
## Build Procedure
### Step 1: Verify Docker is available
```bash
docker info > /dev/null 2>&1 && echo "Docker ready" || echo "Docker not available"
```
### Step 2: Lint Dockerfiles
```bash
make lint-dockerfiles
```
If hadolint fails, read the specific rule violation. Check `.hadolint.yaml`
for already-ignored rules before adding new ignores.
### Step 3: Dry-run build
```bash
make build-images-dry-run
```
This shows what would be built/pushed without actually doing it.
Verify the image names, tags, and registry paths are correct.
### Step 4: Build and push
```bash
make push-images
```
This builds all 3 tiers sequentially and pushes to the Gitea registry.
If only one tier needs rebuilding:
```bash
.venv/bin/python -m devx.tools.build_image \
--dockerfile docker/ci-quality/Dockerfile \
--name oblachno-oss/runner-images/ci-quality \
--tag latest \
--registry git.oblachno.oblachno.fyi \
--push
```
### Step 5: Clean up old versions
```bash
make clean-images
```
Keeps last 2 versions + latest. Uses Gitea API via `clean_images.py`.
## Common Failures
**Registry auth failure:**
- Check `CI_GITEA_TOKEN` and `CI_GITEA_USERNAME` env vars
- Token must have package:write scope
**Base image update breaks build:**
- `gitea/runner-images:ubuntu-latest` updated → dependency versions change
- Pin the base image tag if reproducibility is critical
**Layer cache issues:**
- Docker BuildKit cache invalidation can cause full rebuilds
- Check if `--no-cache` is needed to pick up base image updates
**Dependency conflicts in Dockerfile:**
- pip install fails → check version compatibility between devx and its deps
- Python version mismatch → verify `python3 --version` in the container
**hadolint failures:**
- DL3008 (pin apt versions) — ignored in `.hadolint.yaml`
- DL3013 (pin pip versions) — ignored (we use `==` in pyproject.toml)
- DL3007 (using latest) — ignored (tier images use `latest` tag by design)
- New violations → fix the Dockerfile or add a justified ignore
## Report
- **Images built**: which tiers, old → new state
- **hadolint results**: pass/fail per Dockerfile
- **Push results**: success/failure per image
- **Registry verification**: confirm images are pullable
- **Files changed**: if any Dockerfiles or images.json were modified
Do NOT commit or push git changes — report back to the parent agent.
## Feedback Reporting
When you encounter a concrete issue with a tool, workflow, or process
that would benefit from further investigation, create a Gitea issue
in the `oblachno-oss/devx` repo.
### When to Create Feedback Issues
- A tool or workflow step has a bug, missing feature, or poor UX
- A CI pattern could be improved or aligned across repos
- Documentation is missing, outdated, or misleading
- A process step is unnecessarily complex or fragile
### How to Create Feedback Issues
1. **Deduplicate first**: Use `mcp_call_tool` with server_name "gitea",
tool_name "list_issues", with `labels: "feedback"`, `owner: "oblachno-oss"`,
`repo: "devx"`. Check if an open issue already covers the same topic.
Do NOT create duplicates.
2. **Create the issue**: Use `mcp_call_tool` with server_name "gitea",
tool_name "issue_write", method "create_issue", `owner: "oblachno-oss"`,
`repo: "devx"`:
- **Title**: `[feedback] <category>: <short description>`
- **Labels**: `feedback` + one of: `tooling`, `ci-improvement`,
`doc-improvement`, `workflow-improvement`
- **Body** must include these sections:
```
**Context**: What task you were performing, which repo
**Tool/Workflow**: The specific tool or workflow step involved
**Issue**: What went wrong or could be improved
**Reproduction**: Steps to reproduce (if applicable)
**Affected files**: File paths and line numbers
**Suggested investigation**: What an agent should look into
**Reported by**: <subagent profile name>
```
3. **Report back**: Include the issue URL in your report to the parent agent.
### When NOT to Create Feedback Issues
- Transient failures (network blips, rate limits, Docker pull flakiness)
- Issues you can fix yourself — fix them instead
- CI run failures — those are handled by `notify_failure` automatically
- Missing labels — `configure_repo` creates standard labels on next master push
+167
View File
@@ -0,0 +1,167 @@
---
name: workflow-validator
description: Validates Gitea Actions workflow YAML files using actionlint and act_runner dry-run. Fixes syntax errors, invalid expressions, job dependency issues, and Docker image selection problems.
model: glm-5.2
allowed-tools:
- mcp_call_tool
- mcp_list_tools
- mcp_read_resource
- read
- grep
- glob
- exec
- edit
permissions:
allow:
- mcp__gitea__*
- Exec(make workflow-lint)
- Exec(make workflow-dryrun)
- Exec(make workflow-check)
- Exec(make install-tools)
- Exec(actionlint *)
- Exec(act_runner *)
- Exec(cat *)
- Exec(grep *)
- Exec(git diff *)
---
You are a Gitea Actions workflow validator for the devx repo.
## Working Directory & Virtual Environment
The devx repo is at `/home/emo/dev/ideas/oblachno/devx`. Always `cd` there first.
All Python tools run inside `.venv`. `make` targets handle activation
automatically — always use `make <target>`, never raw `pytest` or `ruff`
commands. If `.venv` doesn't exist, run `make setup` first.
## Key Files
- `.gitea/workflows/ci.yml` — PR pipeline (quality, detect-changes, release-dry-run, pr-review, auto-merge)
- `.gitea/workflows/post-merge.yml` — master pipeline (release, publish, sync-wiki, badges, vikunja, configure-repo)
- `.gitea/workflows/build-images.yml` — Docker image build pipeline
- `.gitea/actionlint.yaml` — actionlint config (registers custom `docker` runner label)
## Validation Procedure
### Step 1: Install tools (if not present)
```bash
make install-tools # installs actionlint, act_runner to ~/.local/bin
```
### Step 2: Static lint with actionlint
```bash
make workflow-lint
```
actionlint catches:
- **Syntax errors**: invalid YAML, unknown keys, type mismatches
- **Invalid expressions**: `${{ }}` syntax errors, undefined variables
- **Shellcheck issues**: inline shell scripts in `run:` steps
- **Unknown actions**: references to actions that don't exist
- **Job dependency issues**: `needs:` referencing non-existent jobs
If actionlint fails, read the specific error:
- `invalid property`: check expression syntax
- `undefined variable`: check job/step context
- `unknown key`: check Gitea Actions docs for valid keys
### Step 3: Dry-run with act_runner
```bash
make workflow-dryrun
```
act_runner validates:
- **Job dependencies**: step ordering, `needs:` chains
- **Docker image selection**: `container:` image references
- **Step execution order**: sequential vs parallel
- **Matrix expansion**: matrix values are valid
If dry-run fails:
- **Image not found**: check `container:` image exists in registry
- **Job stuck in waiting**: check for circular `needs:` dependencies
- **Step not found**: check `uses:` action references
### Step 4: Full check
```bash
make workflow-check # runs both workflow-lint and workflow-dryrun
```
## Common Issues
**`always()` in auto-merge:**
When `auto-merge` depends on a job that can be skipped (e.g. `molecule-tests`),
the `if:` condition MUST include `always() &&` at the start. Without it,
Gitea Actions skips `auto-merge` when any dependency is skipped, even if
the condition explicitly allows `result == 'skipped'`.
```yaml
auto-merge:
needs: [quality, detect-changes, pr-review, molecule-tests]
if: >-
always() &&
github.event_name == 'pull_request' &&
needs.quality.result == 'success' &&
(needs.molecule-tests.result == 'success' || needs.molecule-tests.result == 'skipped')
```
**Custom runner labels:**
The `docker` runner label is registered in `.gitea/actionlint.yaml`.
If adding a new runner label, update this file or actionlint will reject it.
**Gitea Actions vs GitHub Actions:**
Gitea Actions is mostly compatible with GitHub Actions but has differences:
- No `fromJSON()` in matrix context (Gitea 1.26.x)
- `concurrency` blocks can cause jobs to get stuck (Gitea 1.26.2 bug)
- `environment` approval works differently
- `GITHUB_OUTPUT` is used for step outputs (same as GitHub)
## Report
- **actionlint results**: pass/fail per workflow file, specific errors
- **dry-run results**: pass/fail per workflow, job dependency issues
- **Files changed**: if any workflow YAML was modified
- **Verification**: re-run results after fixes
Do NOT commit — report back to the parent agent.
## Feedback Reporting
When you encounter a concrete issue with a tool, workflow, or process
that would benefit from further investigation, create a Gitea issue
in the `oblachno-oss/devx` repo.
### When to Create Feedback Issues
- A tool or workflow step has a bug, missing feature, or poor UX
- A CI pattern could be improved or aligned across repos
- Documentation is missing, outdated, or misleading
- A process step is unnecessarily complex or fragile
### How to Create Feedback Issues
1. **Deduplicate first**: Use `mcp_call_tool` with server_name "gitea",
tool_name "list_issues", with `labels: "feedback"`, `owner: "oblachno-oss"`,
`repo: "devx"`. Check if an open issue already covers the same topic.
Do NOT create duplicates.
2. **Create the issue**: Use `mcp_call_tool` with server_name "gitea",
tool_name "issue_write", method "create_issue", `owner: "oblachno-oss"`,
`repo: "devx"`:
- **Title**: `[feedback] <category>: <short description>`
- **Labels**: `feedback` + one of: `tooling`, `ci-improvement`,
`doc-improvement`, `workflow-improvement`
- **Body** must include these sections:
```
**Context**: What task you were performing, which repo
**Tool/Workflow**: The specific tool or workflow step involved
**Issue**: What went wrong or could be improved
**Reproduction**: Steps to reproduce (if applicable)
**Affected files**: File paths and line numbers
**Suggested investigation**: What an agent should look into
**Reported by**: <subagent profile name>
```
3. **Report back**: Include the issue URL in your report to the parent agent.
### When NOT to Create Feedback Issues
- Transient failures (network blips, rate limits, Docker pull flakiness)
- Issues you can fix yourself — fix them instead
- CI run failures — those are handled by `notify_failure` automatically
- Missing labels — `configure_repo` creates standard labels on next master push
+37
View File
@@ -0,0 +1,37 @@
# devx-workflow
Quick reference for devx tools when working on the devx repo itself.
## PR Workflow (use these, not raw git/tea/MCP)
| Task | Command |
|------|---------|
| Create Vikunja task | `make create-task -- --title "..." --description "..."` |
| Create PR | `make create-pr` |
| Push + create PR | `make push-with-pr` |
| Check CI status | `make devx-pr-status` or `make devx-pr-status PR=42 WAIT=1` |
| Fetch CI failure logs | `make devx-pr-logs` or `make devx-pr-logs PR=42 JOB=quality TAIL=50` |
| Add ready-to-merge label | `make devx-pr-label` or `make devx-pr-label PR=42` |
| Post PR review | `make devx-pr-review PR=42 EVENT=APPROVE BODY="..." CHECKLIST=1,2,3,4,5,6,7,8,9,10,11,12,13` |
| Rebase current branch | `make rebase` |
| Rebase PR via API | `make pr-rebase` or `make pr-rebase PR=42` |
## Auto-merge Behavior
When the `ready-to-merge` label is added and all CI checks pass:
1. Auto-merge validates PR title format (`DEVX-N: <vikunja task title>`)
2. If branch is behind master, auto-merge **rebases via Gitea API** automatically
3. The rebase triggers a new CI run; the next auto-merge attempt merges
4. No manual rebase needed unless the API rebase fails
## Key Rules
- Never manually merge via API — always use auto-merge with `ready-to-merge` label
- Branch naming: `DEVX-N-short-description` (N = Vikunja task ID)
- Commit format: conventional commits (`feat:`, `fix:`, `docs:`, etc.)
- PR title: `DEVX-N: <vikunja task title>` (auto-derived by `make create-pr`)
- 100% test coverage required for all source changes
- All user-facing strings wrapped in `_()` for i18n
- Translation keys must be added to `src/devx/translations.json`
- New CLI commands must be documented in `docs/user/cli-commands.md`
- New tools must be registered in `src/devx/cli.py` and added to Make targets
@@ -0,0 +1,98 @@
# testing-and-debugging
Make targets for testing, debugging, and CI investigation. **Use these
instead of raw `pytest`, `ruff`, or `actionlint` commands.**
## Why Make Targets
Make targets encapsulate the correct venv activation, PYTHONPATH, env
vars, and flags. Running raw commands bypasses venv activation and
produces false failures (missing dependencies, wrong Python version).
## Unit Tests
| Task | Command | Notes |
|------|---------|-------|
| Run all unit tests | `make test-unit` | Fast, no coverage |
| Run with coverage | `make pytest-cov` | **Required before push** — enforces 100% |
| Run single test | `make pytest-cov TEST=tests/test_foo.py::test_bar` | |
| Check test speed | `make check-test-speed` | Fails if tests > 10s total or > 0.5s each |
| Check test coverage | `make check-test-coverage` | Fails if source changed but tests didn't |
## Linting
| Task | Command | Notes |
|------|---------|-------|
| Full lint | `make lint-all` | ruff + workflow-lint + lint-dockerfiles |
| Ruff only | `make lint-ruff` | |
| Format check | `make lint-format` | |
| Type check | `make typecheck` | pyright |
| Bandit | `make lint-bandit` | Security linter |
| Workflow lint | `make workflow-check` | actionlint + act_runner dry-run |
| Dockerfile lint | `make lint-dockerfiles` | hadolint on all Dockerfiles |
| Check mutable globals | `make check-mutable-globals` | Detects module-level mutable state |
| Check dep docs | `make check-dep-docs` | Verifies pyproject.toml deps have comments |
## Pre-Push Verification
**Before pushing any branch:**
```bash
make pre-push
```
This runs `lint-all` + `pytest-cov`. The pre-push git hook only
validates the Vikunja task exists — it does NOT run tests. You must
run `make pre-push` manually.
## CI Failure Investigation
When investigating a CI failure:
1. **Fetch logs via MCP** — use `mcp_call_tool` with gitea server,
`actions_run_read` method, `download_job_log` tool
2. **Reproduce locally** — use `make pytest-cov` or `make lint-all`
depending on which CI job failed
3. **Never run raw pytest** — always use the make target
## Virtual Environment
All commands run inside `.venv`. `make` targets handle activation
automatically. For raw commands (rare), activate first:
```bash
source activate.sh # bash/zsh
source activate.fish # fish
source activate.zsh # zsh
```
If `.venv` doesn't exist, run `make setup` first.
## Common Pitfalls
### Coverage Verification Before Push
**Always run `make pytest-cov` before pushing** — CI enforces 100%
coverage and will fail the PR if any lines are uncovered. This is the
most common cause of CI 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
the `is_truthy()`/`is_falsy()` helpers from `devx.utils.api` or compare
against string values.
### 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 (10s total, 0.5s per test).
### Mutable Global State
The `check-mutable-globals` tool detects module-level mutable state
(lists, dicts, sets) that can cause test pollution. Avoid module-level
mutable defaults — use factory functions or `None` with initialization
inside functions.
+20
View File
@@ -0,0 +1,20 @@
.venv/
.git/
.gitea/
tests/
docs/
*.egg-info/
__pycache__/
htmlcov/
.coverage
dist/
build/
*.md
!README.md
.env
.env.example
activate.sh
activate.fish
activate.zsh
hooks/
.devin/
+15 -2
View File
@@ -1,6 +1,19 @@
# Gitea API token (required for CI scripts that interact with Gitea)
# Role-based Gitea API tokens.
# Each token serves a specific role. For small teams the developer and CI
# tokens may belong to the same user, but the reviewer token MUST belong to a
# different Gitea user than the PR author so Gitea accepts approval reviews.
# Create at: https://git.oblachno.oblachno.fyi/user/settings/applications
REPO_TOKEN=
# Developer token — used by local tooling: create-task, create-pr, setup, etc.
DEVELOPER_GITEA_API_TOKEN=
# CI token — used by CI workflows and scripts that do not post approvals.
# Legacy CI_GITEA_TOKEN is also accepted.
CI_GITEA_API_TOKEN=
# Reviewer token — used by the auto-merge workflow to post APPROVE reviews.
# This must be a different Gitea user from the developer/CI user.
REVIEWER_GITEA_API_TOKEN=
# Vikunja API token (required for post-merge task updates)
# Create at: https://work.oblachno.oblachno.fyi/settings/tokens
+138
View File
@@ -0,0 +1,138 @@
name: Build Images
# Builds and pushes pre-built Docker runner images to the Gitea registry.
# These images eliminate the 40-120s setup tax on every CI job by baking
# devx and all dependencies into the image.
#
# Triggers:
# - After post-merge workflow completes successfully (workflow_run)
# This ensures images are only rebuilt AFTER the release is published
# to PyPI, so the image always has the latest released version.
# - Manually via workflow_dispatch
#
# Consolidated into 2 jobs (from 3):
# build-and-push (includes release-commit detection) ──→ cleanup
#
# The workflow builds 3 tier images in sequence:
# ci-base → ci-quality → ci-full
# Each tier builds FROM the previous one, so they must be built in order.
# After pushing, a cleanup job removes old versions (keeps last 2 + latest).
on:
workflow_run:
workflows: ["Post-merge"]
types: [completed]
branches: [master]
workflow_dispatch:
concurrency:
group: build-images
cancel-in-progress: false
jobs:
build-and-push:
runs-on: docker
timeout-minutes: 30
outputs:
is-release: ${{ steps.check.outputs.is-release }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Set up environment
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
run: make setup-release
- name: Check if this is a release commit
id: check
env:
PYTHONPATH: src
run: |
. .venv/bin/activate
python3 -m devx.ci.detect_release_commit
- name: Docker registry login
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' && steps.check.outputs.is-release == 'false')
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
run: |
. .venv/bin/activate
_TOKEN="$CI_GITEA_API_TOKEN"
[ -z "$_TOKEN" ] && _TOKEN="$DEVELOPER_GITEA_API_TOKEN"
[ -z "$_TOKEN" ] && _TOKEN="$CI_GITEA_TOKEN"
if [ -z "$_TOKEN" ]; then echo "Gitea API token not set — skipping Docker login"; exit 1; fi
echo "$_TOKEN" | docker login git.oblachno.oblachno.fyi -u "$CI_GITEA_USERNAME" --password-stdin
- name: Build and push tier images
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' && steps.check.outputs.is-release == 'false')
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
PYTHONPATH: src
run: |
. .venv/bin/activate
export PATH="$HOME/.local/bin:$PATH"
# Build ci-base first (it's the base for ci-quality and ci-full)
python3 -m devx.tools.build_image \
--dockerfile docker/ci-base/Dockerfile \
--name oblachno-oss/runner-images/ci-base \
--tag latest \
--registry git.oblachno.oblachno.fyi \
--push --pull
# Build ci-quality (FROM ci-base-latest)
python3 -m devx.tools.build_image \
--dockerfile docker/ci-quality/Dockerfile \
--name oblachno-oss/runner-images/ci-quality \
--tag latest \
--registry git.oblachno.oblachno.fyi \
--push
# Build ci-full (FROM ci-quality-latest)
python3 -m devx.tools.build_image \
--dockerfile docker/ci-full/Dockerfile \
--name oblachno-oss/runner-images/ci-full \
--tag latest \
--registry git.oblachno.oblachno.fyi \
--push
- name: Notify on failure
if: failure()
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
PYTHONPATH: src
run: |
. .venv/bin/activate 2>/dev/null || true
export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.ci.notify_failure \
--repo "${{ github.repository }}" \
--run-id "${{ github.run_id }}" \
--workflow "build-images/build-and-push" \
--commit "${{ github.sha }}" \
--auto-login
cleanup:
needs: [build-and-push]
if: always() && needs.build-and-push.result == 'success'
runs-on: docker
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Set up environment
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
run: make setup-ci
- name: Clean up old image versions
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
PYTHONPATH: src
run: |
. .venv/bin/activate
python3 -m devx.tools.clean_images \
--owner oblachno-oss \
--name oblachno-oss/runner-images/ci-base \
--name oblachno-oss/runner-images/ci-quality \
--name oblachno-oss/runner-images/ci-full \
--keep 2
+151 -116
View File
@@ -5,62 +5,24 @@ on:
types: [opened, synchronize]
workflow_dispatch:
jobs:
quality:
runs-on: docker
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- name: Set up environment
run: make setup-quality
- name: Lint all
run: |
. .venv/bin/activate
export PATH="$HOME/.local/bin:$PATH"
make lint-all
- name: Unit tests with 100% coverage
run: |
. .venv/bin/activate
make pytest-cov
- name: Check unit test speed
env:
PYTHONPATH: src
run: |
. .venv/bin/activate
python3 -m devx.tools.check_test_speed --max-seconds 4 --max-single-seconds 0.5
- name: Documentation coverage check
env:
PYTHONPATH: src
run: |
. .venv/bin/activate
python3 -m devx.ci.doc_coverage --fail-on-missing
- name: Translation completeness check
env:
PYTHONPATH: src
run: |
. .venv/bin/activate
python3 -m devx.ci.check_translations
- name: Dependency security scan
run: |
. .venv/bin/activate
# Install pip in venv if missing (needed by pip-audit)
.venv/bin/python -m ensurepip 2>/dev/null || true
PIPAPI_PYTHON_LOCATION=$PWD/.venv/bin/python \
pip-audit --desc --skip-editable 2>&1 || true
- name: Workflow dry-run validation
run: |
. .venv/bin/activate
export PATH="$HOME/.local/bin:$PATH"
# Best-effort: only runs if act_runner is installed
if command -v act_runner >/dev/null 2>&1; then
make workflow-dryrun
else
echo "act_runner not found — skipping workflow dry-run (static lint still passed)"
fi
env:
PIP_BREAK_SYSTEM_PACKAGES: "1"
PYTHONPATH: src
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
detect-changes:
jobs:
# Single validation job that merges: quality, detect-changes,
# release-dry-run, pr-review, and pre-merge-check.
# Uses ci-full image (has git-cliff for release-dry-run).
# Saves ~4x checkout+setup overhead vs 5 separate jobs.
validate:
runs-on: docker
timeout-minutes: 10
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest
timeout-minutes: 15
defaults:
run:
shell: bash
outputs:
user-facing-changed: ${{ steps.detect.outputs.user-facing-changed }}
steps:
@@ -68,83 +30,156 @@ jobs:
with:
fetch-depth: 0
- name: Set up environment
run: make setup-ci
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
run: make setup-image
# --- quality steps ---
- name: Lint all
run: |
. .venv/bin/activate 2>/dev/null || true
export PATH="$HOME/.local/bin:$PATH"
make lint-all
- name: Unit tests with 100% coverage
run: |
. .venv/bin/activate 2>/dev/null || true
make pytest-cov
- name: Check unit test speed
run: |
. .venv/bin/activate 2>/dev/null || true
python3 -m devx.tools.check_test_speed --max-seconds 6 --max-single-seconds 0.5
- name: Documentation gate (coverage + stale refs + lint + version refs + prose)
env:
DEVX_DOC_COVERAGE_STRICT: "1"
DEVX_VALE_LEVEL: warning
run: |
. .venv/bin/activate 2>/dev/null || true
export PATH="$HOME/.local/bin:$PATH"
make devx-docs-check
- name: Translation completeness check
run: |
. .venv/bin/activate 2>/dev/null || true
python3 -m devx.ci.check_translations
- name: Dependency security scan
run: |
. .venv/bin/activate 2>/dev/null || true
# Install pip in venv if missing (needed by pip-audit)
.venv/bin/python -m ensurepip 2>/dev/null || true
PIPAPI_PYTHON_LOCATION=$PWD/.venv/bin/python \
pip-audit --desc --skip-editable 2>&1 || true
- name: Workflow dry-run validation
run: |
. .venv/bin/activate 2>/dev/null || true
export PATH="$HOME/.local/bin:$PATH"
# Best-effort: only runs if act_runner is installed
if command -v act_runner >/dev/null 2>&1; then
make workflow-dryrun
else
echo "act_runner not found — skipping workflow dry-run (static lint still passed)"
fi
# --- detect-changes step ---
- name: Detect changed paths
id: detect
env:
PYTHONPATH: src
run: |
. .venv/bin/activate
. .venv/bin/activate 2>/dev/null || true
python3 -m devx.ci.classify_changes \
--base "origin/master" \
--head "${{ github.event.pull_request.head.sha || github.sha }}" \
--github-output
release-dry-run:
needs: [quality, detect-changes]
if: needs.detect-changes.outputs.user-facing-changed == 'true'
runs-on: docker
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up environment
run: make setup-release
- name: Release dry-run validation
# --- validate-pr + pr-review steps (PR only) ---
- name: Validate auto-merge preconditions
if: github.event_name == 'pull_request'
env:
PYTHONPATH: src
run: |
. .venv/bin/activate
export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.ci.release --dry-run || true
pr-review:
if: github.event_name == 'pull_request'
runs-on: docker
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- name: Set up environment
run: make setup-ci
- name: Run automated PR review
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
PYTHONPATH: src
run: |
set -euo pipefail
. .venv/bin/activate
python3 -m devx.ci.pr_review \
"${{ github.event.number }}" \
"${{ github.repository }}"
auto-merge:
# Auto-merge runs after all CI checks pass. It reads the task ID
# from the branch name, validates the PR title, and squash-merges.
needs: [quality, detect-changes, pr-review]
if: github.event_name == 'pull_request'
runs-on: docker
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.REPO_TOKEN }}
- name: Install dependencies
run: |
python3 -m pip install --break-system-packages requests python-dotenv click
python3 -m pip install --break-system-packages -e .
- name: Squash merge with task ID
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }}
DEVX_VIKUNJA_PROJECT_ID: "8"
PYTHONPATH: src
HEAD_REF: ${{ github.head_ref }}
PR_TITLE: ${{ github.event.pull_request.title }}
REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.number }}
run: |
. .venv/bin/activate 2>/dev/null || true
python3 -m devx.ci.check_auto_merge_ready \
--branch "$HEAD_REF" \
--pr-title "$PR_TITLE" \
--repo "$REPOSITORY" \
--pr-number "$PR_NUMBER"
- name: Run automated PR review
if: github.event_name == 'pull_request'
run: |
. .venv/bin/activate 2>/dev/null || true
set -euo pipefail
python3 -m devx.ci.pr_review \
"${{ github.event.number }}" \
"${{ github.repository }}"
# --- release-dry-run step (conditional) ---
- name: Release dry-run validation
if: steps.detect.outputs.user-facing-changed == 'true'
run: |
. .venv/bin/activate 2>/dev/null || true
export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.ci.release --dry-run
- name: Notify on failure
if: failure()
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
run: |
. .venv/bin/activate 2>/dev/null || true
export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.ci.notify_failure \
--repo "${{ github.repository }}" \
--run-id "${{ github.run_id }}" \
--workflow "ci/validate" \
--commit "${{ github.sha }}" \
--auto-login
auto-merge:
# Auto-merge runs after validate passes. It reads the task ID
# from the branch name, validates the PR title, and squash-merges.
needs: [validate]
if: >-
always() &&
github.event_name == 'pull_request' &&
needs.validate.result == 'success'
runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
timeout-minutes: 10
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.CI_GITEA_API_TOKEN }}
- name: Set up environment
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
run: make setup-image
- name: Post approval review
env:
REVIEWER_GITEA_API_TOKEN: ${{ secrets.REVIEWER_GITEA_API_TOKEN }}
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
PR_NUMBER: ${{ github.event.number }}
REPOSITORY: ${{ github.repository }}
run: |
. .venv/bin/activate 2>/dev/null || true
python3 -m devx.ci.pr_review \
"$PR_NUMBER" \
"$REPOSITORY" \
--event APPROVE \
--checklist-confirmed \
--checklist-categories 1,2,3,4,5,6,7,8,9,10,11,12,13 \
--body "Auto-approved: all CI checks passed (validate job)."
- name: Squash merge with task ID
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }}
DEVX_VIKUNJA_PROJECT_ID: "8"
HEAD_REF: ${{ github.head_ref }}
PR_TITLE: ${{ github.event.pull_request.title }}
REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.number }}
run: |
. .venv/bin/activate 2>/dev/null || true
python3 -m devx.ci.auto_merge \
"$HEAD_REF" \
"$PR_TITLE" \
+125 -204
View File
@@ -1,262 +1,183 @@
name: Post-merge
# Runs on every push to master. A single workflow with conditional jobs
# replaces separate workflows for release, wiki sync, badges, and
# Vikunja task updates.
# Runs on every push to master (after CI workflow merges a PR).
# Consolidated into 2 jobs (from 7) to reduce runner overhead:
# detect-and-configure ──→ release-and-maintain
#
# Job dependency graph:
# Job 1: detect release commit, validate commit msg, configure repo
# (branch protection, labels).
# Job 2: release + publish + sync-wiki + vikunja + badges.
# Individual steps are conditional on job 1 outputs.
#
# detect-type ──┬── release (skip if release commit)
# ├── badges (ALWAYS runs — even on release commits)
# ├── configure-repo (independent — skip if release commit)
# ├── sync-wiki (needs release — skip if release commit/fails)
# └── vikunja (needs release — skip if release commit/fails)
# The badges step always runs (even on release commits) so version
# badge picks up the new __version__. It runs last so it sees the
# new version if release created one.
#
# sync-wiki and vikunja depend on release succeeding so that the wiki
# and task tracker are only updated when the code is actually released.
# If release fails, they are skipped to avoid leaving the wiki or
# Vikunja in an inconsistent state with the codebase on master.
#
# The badges job depends on release so it picks up the latest version
# number. It uses `if: always()` with no is-release condition so it
# runs on every push to master, including release commits. This
# ensures badges (tests, coverage, version, etc.) are always current.
#
# When release creates a "release: vX.Y.Z" commit, the release
# commit's post-merge run still updates badges (version badge picks
# up the new version). Other jobs skip. The tag push triggers publish.yml.
# When release creates a "release: vX.Y.Z" commit and tag, the publish
# step builds and publishes the package to the Gitea PyPI registry.
# The release commit's post-merge run still updates badges. Other
# steps (sync-wiki, vikunja) skip on release commits.
on:
push:
branches: [master]
concurrency:
group: post-merge-${{ github.ref }}
cancel-in-progress: true
env:
PIP_BREAK_SYSTEM_PACKAGES: "1"
PYTHONPATH: src
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
CI_GITEA_USERNAME: ${{ vars.CI_GITEA_USERNAME }}
jobs:
detect-type:
detect-and-configure:
runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
timeout-minutes: 10
defaults:
run:
shell: bash
outputs:
is-release: ${{ steps.check.outputs.is-release }}
is-automated: ${{ steps.check.outputs.is-automated }}
user-facing-changed: ${{ steps.detect.outputs.user-facing-changed }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Install dependencies
fetch-depth: 0
- name: Set up environment
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
run: make setup-image
- name: Ensure branch protection and labels
env:
DEVX_REPO_NAME: devx
DEVX_REPO_OWNER: oblachno-oss
DEVX_STATUS_CHECKS: "CI / validate (pull_request)"
run: |
python3 -m pip install --break-system-packages requests python-dotenv click
python3 -m pip install --break-system-packages -e .
. .venv/bin/activate 2>/dev/null || true
python3 -m devx.tools.configure_repo
- name: Check if this is a release commit
id: check
env:
PYTHONPATH: src
run: python3 -m devx.ci.detect_release_commit
validate-commit-msg:
needs: [detect-type]
if: needs.detect-type.outputs.is-release == 'false'
runs-on: docker
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Install dependencies
run: |
python3 -m pip install --break-system-packages click python-dotenv
python3 -m pip install --break-system-packages -e .
. .venv/bin/activate 2>/dev/null || true
python3 -m devx.ci.detect_release_commit
- name: Validate latest commit message
env:
PYTHONPATH: src
if: steps.check.outputs.is-automated == 'false'
run: |
. .venv/bin/activate 2>/dev/null || true
git log -1 --format=%B > commit-msg.txt
python3 -m devx.ci.validate_commit_msg commit-msg.txt --branch master
rm -f commit-msg.txt
release:
needs: [detect-type]
if: needs.detect-type.outputs.is-release == 'false'
runs-on: docker
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.REPO_TOKEN }}
- name: Set up environment
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
run: make setup-release
- name: Configure git
- name: Detect changed paths
id: detect
if: steps.check.outputs.is-release == 'false'
run: |
git config user.name "devx-ci-bot"
git config user.email "devx-ci-bot@oblachno.fyi"
- name: Run release
env:
PYTHONPATH: src
run: |
. .venv/bin/activate
export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.ci.release
- name: Publish release
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
PYTHONPATH: src
run: |
. .venv/bin/activate
export PATH="$HOME/.local/bin:$PATH"
TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
if [ -z "$TAG" ]; then
echo "No tag found — skipping publish"
exit 0
fi
echo "Publishing release $TAG (idempotent — skips if already published)..."
python3 -m devx.ci.publish "$TAG" "${{ github.repository }}"
. .venv/bin/activate 2>/dev/null || true
python3 -m devx.ci.classify_changes \
--base "HEAD~1" \
--head "HEAD" \
--github-output
- name: Notify on failure
if: failure()
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
PYTHONPATH: src
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
run: |
. .venv/bin/activate 2>/dev/null || true
export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.tools.install_tools --tool tea
tea login add --name devx --url "${{ github.server_url }}" --token "$REPO_TOKEN" || true
tea login default devx || true
python3 -m devx.ci.notify_failure \
--repo "${{ github.repository }}" \
--run-id "${{ github.run_id }}" \
--workflow "post-merge/release" \
--commit "${{ github.sha }}"
--workflow "post-merge/detect-and-configure" \
--commit "${{ github.sha }}" \
--auto-login
sync-wiki:
needs: [detect-type, release]
if: needs.detect-type.outputs.is-release == 'false'
release-and-maintain:
needs: [detect-and-configure]
if: always() && needs.detect-and-configure.result == 'success'
runs-on: docker
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up environment
run: make setup-ci
- name: Sync documentation to wiki
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
PYTHONPATH: src
run: |
. .venv/bin/activate
python3 -m devx.ci.sync_wiki --repo "${{ github.repository }}" --strict
- name: Notify on failure
if: failure()
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
PYTHONPATH: src
run: |
export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.ci.notify_failure \
--repo "${{ github.repository }}" \
--run-id "${{ github.run_id }}" \
--workflow "post-merge/sync-wiki" \
--commit "${{ github.sha }}"
badges:
needs: [detect-type, release]
if: always()
runs-on: docker
timeout-minutes: 10
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest
timeout-minutes: 15
outputs:
tag: ${{ steps.release-tag.outputs.tag }}
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: master
token: ${{ secrets.REPO_TOKEN }}
- name: Fetch latest master
run: |
git fetch origin master
git reset --hard origin/master
token: ${{ secrets.CI_GITEA_API_TOKEN }}
- name: Set up environment
run: make setup-ci
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
run: make setup-image EXTRAS=release
- name: Configure git
run: |
git config user.name "devx-ci-bot"
git config user.email "devx-ci-bot@oblachno.fyi"
# --- release + publish (only if user-facing changes, not a release commit) ---
- name: Run release
id: release-tag
if: needs.detect-and-configure.outputs.is-release == 'false' && needs.detect-and-configure.outputs.user-facing-changed == 'true'
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
run: |
. .venv/bin/activate 2>/dev/null || true
export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.ci.release
- name: Build and publish release
if: steps.release-tag.outputs.tag != ''
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
run: |
. .venv/bin/activate 2>/dev/null || true
export PATH="$HOME/.local/bin:$PATH"
git fetch --tags
git checkout "${{ steps.release-tag.outputs.tag }}"
python3 -m devx.ci.publish "${{ steps.release-tag.outputs.tag }}" "${{ github.repository }}" --auto-login
# --- sync-wiki + vikunja (skip on automated/release commits) ---
- name: Sync documentation to wiki
if: needs.detect-and-configure.outputs.is-automated == 'false'
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
run: |
. .venv/bin/activate 2>/dev/null || true
python3 -m devx.ci.sync_wiki --repo "${{ github.repository }}" --verify
- name: Update Vikunja task
if: needs.detect-and-configure.outputs.is-automated == 'false'
env:
VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }}
DEVX_VIKUNJA_PROJECT_ID: "8"
run: |
. .venv/bin/activate 2>/dev/null || true
python3 -m devx.ci.post_merge --git-sha "${{ github.sha }}"
# --- badges (always run — even on release commits) ---
- name: Generate and push badges
env:
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
PRE_COMMIT_ALLOW_NO_CONFIG: "1"
run: |
. .venv/bin/activate
. .venv/bin/activate 2>/dev/null || true
export PATH="$HOME/.local/bin:$PATH"
# Fetch latest master to pick up any release commit that was pushed
git fetch origin master
git reset --hard origin/master
python3 -m devx.ci.push_badges
- name: Notify on failure
if: failure()
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
PYTHONPATH: src
CI_GITEA_API_TOKEN: ${{ secrets.CI_GITEA_API_TOKEN }}
run: |
. .venv/bin/activate 2>/dev/null || true
export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.ci.notify_failure \
--repo "${{ github.repository }}" \
--run-id "${{ github.run_id }}" \
--workflow "post-merge/badges" \
--commit "${{ github.sha }}"
vikunja:
needs: [detect-type, release]
if: needs.detect-type.outputs.is-release == 'false'
runs-on: docker
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install dependencies
run: |
python3 -m pip install --break-system-packages requests python-dotenv click
python3 -m pip install --break-system-packages -e .
- name: Update Vikunja task
env:
VIKUNJA_TOKEN: ${{ secrets.VIKUNJA_TOKEN }}
DEVX_VIKUNJA_PROJECT_ID: "8"
PYTHONPATH: src
run: python3 -m devx.ci.post_merge --git-sha "${{ github.sha }}"
- name: Notify on failure
if: failure()
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
PYTHONPATH: src
run: |
export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.tools.install_tools --tool tea
tea login add --name devx --url "${{ github.server_url }}" --token "$REPO_TOKEN" || true
tea login default devx || true
python3 -m devx.ci.notify_failure \
--repo "${{ github.repository }}" \
--run-id "${{ github.run_id }}" \
--workflow "post-merge/vikunja" \
--commit "${{ github.sha }}"
configure-repo:
needs: [detect-type]
if: needs.detect-type.outputs.is-release == 'false'
runs-on: docker
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: |
python3 -m pip install --break-system-packages requests python-dotenv click
python3 -m pip install --break-system-packages -e .
- name: Ensure branch protection and labels
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
PYTHONPATH: src
run: python3 -m devx.tools.configure_repo --repo devx --owner oblachno-oss
- name: Notify on failure
if: failure()
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
PYTHONPATH: src
run: |
export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.tools.install_tools --tool tea
tea login add --name devx --url "${{ github.server_url }}" --token "$REPO_TOKEN" || true
tea login default devx || true
python3 -m devx.ci.notify_failure \
--repo "${{ github.repository }}" \
--run-id "${{ github.run_id }}" \
--workflow "post-merge/configure-repo" \
--commit "${{ github.sha }}"
--workflow "post-merge/release-and-maintain" \
--commit "${{ github.sha }}" \
--auto-login
-55
View File
@@ -1,55 +0,0 @@
name: Publish Release
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
tag:
description: 'Tag to publish (e.g. v0.9.11)'
required: true
type: string
jobs:
publish:
runs-on: docker
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install dependencies
run: |
python3 -m pip install --break-system-packages build twine requests python-dotenv click
python3 -m pip install --break-system-packages -e .
- name: Install CI tools
run: |
export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.tools.install_tools --tool git-cliff --tool tea
- name: Configure tea login
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
run: |
export PATH="$HOME/.local/bin:$PATH"
tea login add --name devx --url "${{ github.server_url }}" --token "$REPO_TOKEN" || true
tea login default devx || true
- name: Build and publish release
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
PYTHONPATH: src
run: |
export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.ci.publish "${{ github.event.inputs.tag || github.ref_name }}" "${{ github.repository }}"
- name: Notify on failure
if: failure()
env:
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
PYTHONPATH: src
run: |
export PATH="$HOME/.local/bin:$PATH"
python3 -m devx.ci.notify_failure \
--repo "${{ github.repository }}" \
--run-id "${{ github.run_id }}" \
--workflow "publish" \
--commit "${{ github.sha }}"
+14
View File
@@ -0,0 +1,14 @@
# Hadolint configuration for devx Dockerfiles
# https://github.com/hadolint/hadolint#configure
ignored:
- DL3008 # Don't require pinning apt package versions
- DL3013 # Don't require pinning pip package versions
- DL3018 # Don't require pinning apk package versions
- DL3007 # Using latest is intentional for tier images (rebuilt on every merge)
- SC2102 # False positive: pip extras [release,molecule,deploy] look like shell ranges
trustedRegistries:
- git.oblachno.oblachno.fyi
- docker.io
- gitea/runner-images
+32
View File
@@ -49,6 +49,38 @@ repos:
pass_filenames: false
stages: [pre-commit]
- id: checkmake
name: checkmake Makefile linter
entry: make checkmake
language: system
files: (Makefile|\.mak)$
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 6 --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
language: system
files: ^src/devx/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_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
+49
View File
@@ -0,0 +1,49 @@
# Vale configuration for devx 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 = devx
[*.{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
# 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
+9
View File
@@ -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]\.'
+64
View File
@@ -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
+8
View File
@@ -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]'
+30
View File
@@ -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
+9
View File
@@ -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}'
+9
View File
@@ -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:
- '\.\.\.'
+13
View File
@@ -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'
+12
View File
@@ -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|$)'
+13
View File
@@ -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
+9
View File
@@ -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
+43
View File
@@ -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*$'
+29
View File
@@ -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
+11
View File
@@ -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
+14
View File
@@ -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'
+12
View File
@@ -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\)'
+7
View File
@@ -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)
+7
View File
@@ -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)'
+7
View File
@@ -0,0 +1,7 @@
extends: existence
message: "Use parentheses judiciously."
link: 'https://developers.google.com/style/parentheses'
nonword: true
level: suggestion
tokens:
- '\(.+\)'
+184
View File
@@ -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
+7
View File
@@ -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,}'
+7
View File
@@ -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:
- '"[^"]+"[.,?]'
+7
View File
@@ -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+'
+8
View File
@@ -0,0 +1,8 @@
extends: existence
message: "Use semicolons judiciously."
link: 'https://developers.google.com/style/semicolons'
nonword: true
scope: sentence
level: suggestion
tokens:
- ';'
+11
View File
@@ -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
+10
View File
@@ -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]'
+10
View File
@@ -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'
+8
View File
@@ -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)
+11
View File
@@ -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
+7
View File
@@ -0,0 +1,7 @@
extends: existence
message: "Avoid using '%s'."
link: 'https://developers.google.com/style/tense'
ignorecase: true
level: warning
tokens:
- will
+80
View File
@@ -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
+4
View File
@@ -0,0 +1,4 @@
{
"feed": "https://github.com/errata-ai/Google/releases.atom",
"vale_version": ">=1.0.0"
}
View File
@@ -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"
+8
View File
@@ -0,0 +1,8 @@
extends: metric
message: "Try to keep the ColemanLiau 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 FleschKincaid 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"
+8
View File
@@ -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"
+17
View File
@@ -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"
+8
View File
@@ -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"
+4
View File
@@ -0,0 +1,4 @@
{
"feed": "https://github.com/errata-ai/Readability/releases.atom",
"vale_version": ">=2.13.0"
}
@@ -0,0 +1,38 @@
devx
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
Vikunja
oblachno
Oblachno
Bulgarian
+6
View File
@@ -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*$'
+13
View File
@@ -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'
+3
View File
@@ -0,0 +1,3 @@
# Custom Vale style for devx documentation
Project-specific terminology and style rules
+11
View File
@@ -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
+702
View File
@@ -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
+32
View File
@@ -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
+11
View File
@@ -0,0 +1,11 @@
extends: repetition
message: "'%s' is repeated!"
level: warning
alpha: true
action:
name: edit
params:
- truncate
- " "
tokens:
- '[^\s]+'
+183
View File
@@ -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
+27
View File
@@ -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.
```
+5
View File
@@ -0,0 +1,5 @@
extends: existence
message: "Don't start a sentence with '%s'."
level: error
raw:
- '(?:[;-]\s)so[\s,]|\bSo[\s,]'
+6
View File
@@ -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'
+221
View File
@@ -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
+29
View File
@@ -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
+4
View File
@@ -0,0 +1,4 @@
{
"feed": "https://github.com/errata-ai/write-good/releases.atom",
"vale_version": ">=1.0.0"
}
+365 -51
View File
@@ -1,23 +1,39 @@
# AGENTS.md — Project Conventions for devx
## Virtual Environment
All Python tools, tests, and scripts run inside a standard `.venv` directory.
Activate it before running any non-`make` command:
```bash
source activate.sh # bash/zsh
source activate.fish # fish
source activate.zsh # zsh
```
If `.venv` doesn't exist, run `make setup` first. The `make` targets handle
venv activation automatically — always prefer `make <target>` over raw commands.
## Build & Test Commands
```bash
make setup # Create venv, install deps, set up hooks, install CI tools
make install-tools # Install actionlint, git-cliff, act_runner to ~/.local/bin
make lint-all # ruff + pyright + bandit + actionlint
make install-tools # Install actionlint, git-cliff, act_runner, tea, hadolint, vale to ~/.local/bin
make lint-all # ruff + pyright + bandit + actionlint + lint-dockerfiles
make pytest-cov # Unit tests with 100% coverage enforcement
make test-unit # Unit tests without coverage
make workflow-lint # Static lint of .gitea/workflows/*.yml (actionlint)
make workflow-dryrun # Dry-run all workflows in Docker (act_runner exec --dryrun)
make workflow-check # workflow-lint + workflow-dryrun
make devx-check-doc-versions # Verify docs version refs match __version__
make devx-vale # Run Vale prose linter on docs and README
make clean # Remove caches, build artifacts, coverage data
```
`make setup` automatically installs all development tools:
- **Python deps** via `python -m devx.tools.setup` (pip install -e .[dev], pre-commit hooks)
- **actionlint, git-cliff, act_runner, tea** via `python -m devx.tools.install_tools` (CI/CD tools to ~/.local/bin)
- **tea CLI login** via `python -m devx.tools.setup` (configures `tea login` from `.env` `REPO_TOKEN`)
- **actionlint, git-cliff, act_runner, tea, hadolint, vale** via `python -m devx.tools.install_tools` (CI/CD tools to ~/.local/bin)
- **tea CLI login** via `python -m devx.tools.setup` (configures `tea login` from `.env` `CI_GITEA_TOKEN`)
## Workflow Verification (Before Push)
@@ -34,7 +50,7 @@ Workflow YAML files (`.gitea/workflows/*.yml`) are verified with two tools:
Both run via `make workflow-check` and are part of `make lint-all`.
The pre-commit hook runs actionlint automatically when workflow files change.
The CI `quality` job runs `make setup-quality` then `make lint-all`.
The CI `validate` job runs `make setup-image` then `make lint-all`.
CI also runs a best-effort `make workflow-dryrun` step (skipped if act_runner is not installed in the CI Docker image).
## Architecture
@@ -43,7 +59,7 @@ devx is a reusable Python package providing development and CI/CD tools for obla
### Package Structure
```
```text
src/devx/
├── __init__.py # Version (single source of truth, read by setuptools)
├── cli.py # Click-based CLI entry point (devx command)
@@ -52,36 +68,69 @@ src/devx/
├── gitea_cli.py # TeaCLI — wrapper around tea CLI with JSON parsing
├── i18n.py # Translation system (gettext-based, translations.json)
├── exceptions.py # Custom exception types
├── translations.json # Translation strings (en, bg)
├── translations.json # Translation strings (en, bg, de, pl, ru, zh)
├── ci/ # CI/CD automation modules (run by workflows)
│ ├── release.py # Automated versioning, tagging, changelog
│ ├── publish.py # Build and publish to Gitea PyPI registry (--skip-build for non-Python repos)
│ ├── auto_merge.py # Squash-merge PRs with task ID validation
│ ├── check_auto_merge_ready.py # Pre-merge validation gate (branch, PR title, Vikunja, behind-master)
│ ├── _shared.py # Shared utilities (get_latest_tag)
│ ├── classify_changes.py # User-facing vs workflow-only change detection
│ ├── classify_changes.py # User-facing vs infrastructure change detection
│ ├── detect_release_commit.py # Detect release commits on master
│ ├── validate_commit_msg.py # Conventional commit validation
│ ├── pr_review.py # Automated PR review
│ ├── pr_review.py # Automated PR review + manual reviews (--event, --body, --checklist-confirmed)
│ ├── post_merge.py # Vikunja task updates after merge
│ ├── sync_wiki.py # Sync documentation to Gitea wiki
│ ├── push_badges.py # Generate and push quality badges (--retries for retry on git push failures)
│ ├── notify_failure.py # Create Gitea issues on CI failures (--auto-login)
│ ├── distribute_files.py # Distribute files across parallel runners
│ ├── distribute_files.py # Distribute files across parallel runners (LPT scheduling)
│ ├── distribute_items.py # Distribute generic items (VMs, hosts) across parallel runners (LPT)
│ ├── integration_guard.py # Run pytest with cross-runner fail-fast
│ ├── check_translations.py # Translation completeness check
── doc_coverage.py # Documentation coverage check
── doc_coverage.py # Documentation coverage check
│ ├── lint_docs.py # Documentation linter (structure, links, headings, code blocks, orphans)
│ ├── validate_deploy_ref.py # Validate git tag for deployments (--github-output)
│ └── record_deployed_tag.py # Record deployed tag to Gitea repo variable
├── tools/ # Developer tooling modules (run locally or by CI)
│ ├── setup.py # Environment setup (venv, deps, hooks)
│ ├── install_tools.py # Install actionlint, git-cliff, act_runner, tea
│ ├── install_tools.py # Install actionlint, git-cliff, act_runner, tea, hadolint, vale
│ ├── install_checkmake.py # Install checkmake (Makefile linter)
│ ├── check_doc_versions.py # Verify docs version refs match __version__
│ ├── build_image.py # Build and push Docker images to Gitea registry
│ ├── clean_images.py # Clean up old Docker image versions from Gitea registry
│ ├── check_test_speed.py # Measure unit test execution time
│ ├── check_mutable_globals.py # Detect module-level mutable globals (test isolation bugs)
│ ├── check_pyproject_deps.py # Validate pyproject.toml deps have documentation comments
│ ├── check_test_coverage.py # Ensure changed files have corresponding tests (configurable rules)
│ ├── check_agent_docs.py # Validate docs for stale file references (configurable patterns)
│ ├── check_config.py # Validate pyproject.toml [tool.devx] config
│ ├── configure_repo.py # Branch protection and label setup
── generate_badges.py # Badge SVG generation
── generate_badges.py # Badge SVG generation
│ ├── generate_cliff_config.py # Generate git-cliff config (cliff.toml)
│ ├── create_task.py # Create Vikunja tasks
│ ├── create_pr.py # Create PRs with auto-derived title from Vikunja
│ ├── pr_status.py # Check CI status for a PR/commit (--wait polls)
│ ├── pr_logs.py # Fetch logs for failed CI jobs
│ ├── pr_label.py # Add labels to PRs (idempotent)
│ ├── pre_push_check.py # Validate Vikunja task existence before push
│ └── _shared.py # Shared tool utilities
├── opentofu.py # OpenTofu output helpers (get_tofu_output, get_tofu_vm_ip, get_tofu_vm_field)
├── utils/ # Shared utilities (reusable across projects)
│ ├── api.py # API response helpers (is_truthy, is_falsy)
│ ├── ssh.py # SSH exec + wait_for_ssh (pure-Python socket check)
│ ├── crypto.py # Secret generation (shell-safe passwords)
│ ├── vault.py # Ansible vault encrypt/decrypt helpers
│ ├── network.py # HTTP connectivity check + wait_for_ssh
│ ├── confirm.py # Typed confirmation validation for destructive ops
│ ├── json_registry.py # File-locked JSON registry for local state
│ ├── step_tracker.py # Multi-step operation tracking with reports
│ └── logging.py # XDG-compliant logging configuration
└── molecule/ # Optional molecule testing helpers (for Ansible projects)
├── discover_runners.py # Dynamic Gitea runner discovery
├── distribute_molecule.py # Distribute molecule scenarios across runners (--roles-root for multi-role)
├── distribute_molecule.py # Distribute molecule scenarios across runners (LPT scheduling, --roles-root for multi-role)
├── molecule_ci_guard.py # Run molecule with cross-runner fail-fast (--roles-root)
├── molecule_all.py # Run all molecule scenarios locally
├── start_docker.py # Ensure Docker daemon is running for molecule tests
└── platforms.py # Supported molecule platforms
```
@@ -99,18 +148,24 @@ Every change to master goes through this workflow. No exceptions.
### Branch Protection (Required Gitea Settings)
Branch protection and labels are automatically configured by
`python -m devx.tools.configure_repo`, which runs as a `configure-repo` job in
the post-merge workflow on every push to master.
`python -m devx.tools.configure_repo`, which runs as a step in the
`detect-and-configure` job in the post-merge workflow on every push to master.
The following rules are enforced for `master`:
- **Require pull request**: No direct pushes to master
- **Require approval review**: At least 1 `APPROVE` review before merge
- **Require status checks**: CI quality must pass
- **Require status checks**: CI validate must pass
- **Block force pushes**: No history rewriting on master
### 1. Create Vikunja Task
Create a task in Vikunja to get a `DEVX-N` identifier.
**IMPORTANT:** The task title must NOT include the `DEVX-N:` prefix.
The `make create-pr` and `check_auto_merge_ready` commands automatically
prepend `DEVX-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
@@ -124,7 +179,7 @@ git checkout -b DEVX-N-short-description
### 4. Commit (Conventional Commits)
Branch commits use conventional commit format (no `DEVX-N:` prefix):
```
```text
feat: add new feature
fix: resolve bug
docs: update README
@@ -137,8 +192,9 @@ docs: update README
### 6. Review the PR
**Automated review (CI `pr-review` job):** Every PR triggers an automated
review via `python -m devx.ci.pr_review`. This job posts a review with
**Automated review (CI `validate` job):** Every PR triggers an automated
review via `python -m devx.ci.pr_review` as a step in the `validate` job.
This posts a review with
`COMMENT` (no issues) or `REQUEST_CHANGES` (issues found):
- Architecture compliance (no subprocess in CLI, no hardcoded URLs)
@@ -161,7 +217,7 @@ Once all checklist items are verified and comments are addressed, approve
the PR. Then add the `ready-to-merge` label. The auto-merge workflow will:
1. **Validate** PR title format (`DEVX-N: <vikunja task title>`) and match against Vikunja task title
2. **Check** that at least one substantive APPROVE review exists
3. Wait for all CI checks to pass (including the `pr-review` job)
3. Wait for all CI checks to pass (including the `validate` job)
4. Squash-merge with title: `DEVX-N: <conventional commit message>`
5. The post-merge workflow marks the Vikunja task as done
6. The release workflow automatically versions, tags, and publishes
@@ -172,30 +228,27 @@ the PR. Then add the `ready-to-merge` label. The auto-merge workflow will:
### Automated Release Pipeline
After a PR is merged to master, the **post-merge workflow**
(`.gitea/workflows/post-merge.yml`) runs automatically:
(`.gitea/workflows/post-merge.yml`) runs automatically. Consolidated
into 2 jobs (from 7) to reduce runner overhead:
1. **detect-type** — Checks if the commit is a regular merge or a
release commit (`release: vX.Y.Z`). All subsequent jobs skip for
release commits.
1. **detect-and-configure** — Configures repo (branch protection, labels),
detects release commit, validates commit message. Outputs `is-release`
and `is-automated` for the next job.
2. **release** — Runs `python -m devx.ci.release` which:
- Checks for user-facing changes via `python -m devx.ci.classify_changes`
- Uses **git-cliff** to calculate the next semver version from conventional commits
- Updates `__version__` in `src/devx/__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
- Commits with `release: vX.Y.Z [skip ci]` prefix
- Creates an annotated tag `vX.Y.Z` on the release commit
- Pushes both the commit and tag to master
3. **sync-wiki** — Syncs documentation to the Gitea wiki.
4. **badges** — Generates and pushes quality badge SVGs to the `badges` branch.
5. **vikunja** — Marks the corresponding Vikunja task as done.
The tag push triggers the **publish workflow** (`.gitea/workflows/publish.yml`)
which builds and publishes the package to the Gitea PyPI registry.
2. **release-and-maintain** — Runs all post-merge maintenance as
conditional steps:
- **release** (if not a release commit) — Runs `python -m devx.ci.release`
which checks for user-facing changes via `classify_changes`, uses
git-cliff for semver, updates `__version__`, updates `CHANGELOG.md`,
runs lint+tests, commits with `release: vX.Y.Z [skip ci]`, creates
annotated tag, pushes to master.
- **publish** (if release created a tag) — Builds and publishes the
package to the Gitea PyPI registry. Checks out the release tag
within the same job.
- **sync-wiki** (if not automated) — Syncs documentation to the Gitea wiki.
- **vikunja** (if not automated) — Marks the corresponding Vikunja task as done.
- **badges** (always) — Generates and pushes quality badge SVGs to the
`badges` branch. Fetches latest master first to pick up release commits.
### Smart CI: User-Facing vs Workflow-Only Changes
@@ -249,7 +302,7 @@ so `.:src` is not needed. The `src` directory is the sole import root.
The `tea` Gitea CLI tool is used for Gitea API interactions. It is installed
by `python -m devx.tools.install_tools` and configured by
`python -m devx.tools.setup` (login profile from `.env` `REPO_TOKEN`).
`python -m devx.tools.setup` (login profile from `.env` `CI_GITEA_TOKEN`).
**`devx.gitea_cli.TeaCLI`** — Python wrapper around `tea` CLI with JSON output parsing:
- `create_issue()` — Create issues with labels
@@ -286,14 +339,14 @@ setuptools via `dynamic = ["version"]` in `pyproject.toml`.
### Task ID Resolution
`auto_merge` resolves the task ID solely from the branch name (e.g.
`auto_merge` resolves the task ID solely from the branch name (for example
`DEVX-12-fix-foo``DEVX-12`). Branch names must include the task ID
prefix — there is no `.taskid` file fallback. If a stale `.taskid` file
exists in the repo, a deprecation warning is printed advising its removal.
### Workflow `auto-merge` Job and `always()`
When `auto-merge` depends on a job that can be skipped (e.g.
When `auto-merge` depends on a job that can be skipped (for example
`molecule-tests`), the `if:` condition MUST include `always() &&`
at the start. Without it, Gitea Actions skips `auto-merge` when any
dependency is skipped, even if the condition explicitly allows
@@ -301,15 +354,33 @@ dependency is skipped, even if the condition explicitly allows
```yaml
auto-merge:
needs: [quality, detect-changes, pr-review, molecule-tests]
needs: [validate, molecule-tests]
if: >-
always() &&
github.event_name == 'pull_request' &&
needs.quality.result == 'success' &&
needs.pr-review.result == 'success' &&
needs.validate.result == 'success' &&
(needs.molecule-tests.result == 'success' || needs.molecule-tests.result == 'skipped')
```
### LPT Test Distribution Algorithm
`distribute_molecule` and `distribute_files` use **LPT (Longest Processing
Time first)** scheduling instead of naive round-robin. This produces a more
balanced distribution when test items have varying costs:
1. **Weight estimation**: Each item is assigned a weight:
- Molecule scenarios: heuristic by name (`nextcloud`=10, `gitea`=8,
`binary`=2, default=3). See `_SCENARIO_WEIGHTS` in
`distribute_molecule.py`.
- Integration test files: weight by file size in bytes (as a proxy
for test runtime).
2. **LPT assignment**: Items are sorted by weight (descending), then
each is assigned to the runner with the least total weight.
This ensures heavy scenarios (for example `nextcloud`) are spread across
different runners rather than clustered on one, reducing the
longest-runner time from ~16 min to ~11 min with 6 runners.
## Config System
devx uses environment variables with `.env` file fallback for configuration.
@@ -324,8 +395,8 @@ devx uses environment variables with `.env` file fallback for configuration.
| `DEVX_REPO_NAME` | **(none — must be set)** | Repository name (or `owner/repo`) |
| `DEVX_TASK_PREFIX` | `DEVX` | Task ID prefix (GRM, OBL-INFRA, etc.) |
| `DEVX_VIKUNJA_PROJECT_ID` | `6` | Vikunja project ID |
| `DEVX_LANG` | `en` | Language for i18n (en, bg) |
| `REPO_TOKEN` | (from .env) | Gitea API token |
| `DEVX_LANG` | `en` | Language for i18n (en, bg, de, pl, ru, zh) |
| `CI_GITEA_TOKEN` | (from .env) | Gitea API token |
| `VIKUNJA_TOKEN` | (from .env) | Vikunja API token |
### Per-Project Overrides
@@ -334,6 +405,139 @@ Projects using devx can override the default API URLs and language by setting
`DEVX_*` environment variables or entries in their `.env` file. The config
system loads `.env` automatically via `python-dotenv`.
### pyproject.toml [tool.devx] Configuration
In addition to `DEVX_` env vars, many devx tools read configuration from
the `[tool.devx]` section in `pyproject.toml`. This allows per-project
customization without environment variables.
**Base config** (`[tool.devx]`):
- `task_prefix` — Task ID prefix (for example `"DEVX"`, `"GRM"`, `"OBL-INFRA"`)
- `vikunja_project_id` — Vikunja project ID
- `repo_owner` / `repo_name` — Gitea repository coordinates
- `gitea_api_url` / `vikunja_api_url` — API endpoints
**Tool-specific config**:
- `[tool.devx.check_mutable_globals]``scan_dirs`, `skip_dirs`, `known_safe`
- `[tool.devx.check_test_coverage]``rules` (source_pattern → test_paths mapping), `skip_patterns`
- `[tool.devx.check_agent_docs]``scan_dirs`, `deleted_files`, `deprecated_patterns`, `legitimate_indicators`
## devx.mak — Shared Makefile Fragment
`devx.mak` provides common Makefile targets that projects can include
via `-include $(DEVX_MAK)`. This eliminates Makefile duplication across
projects.
**Available targets** (all prefixed with `devx-`):
| Target | Purpose |
|--------|---------|
| `devx-create-task` | Create a Vikunja task |
| `devx-create-pr` | Create a PR with auto-derived title |
| `devx-push` | Push current branch to origin |
| `devx-push-with-pr` | Push and create PR in one step |
| `devx-pr-status` | Check CI status for a PR (`PR=`, `WAIT=`, `TIMEOUT=`) |
| `devx-pr-logs` | Fetch logs for failed CI jobs (`PR=`, `JOB=`, `TAIL=`) |
| `devx-pr-label` | Add a label to a PR (`PR=`, `LABEL=ready-to-merge`) |
| `devx-pr-review` | Post a review on a PR (`PR=`, `EVENT=`, `BODY=`, `CHECKLIST=`) |
| `devx-check-config` | Validate devx configuration |
| `devx-configure-gitea-pypi` | Configure Gitea private PyPI registry |
| `devx-env` | Create .env from .env.example |
| `devx-venv` | Create Python venv with version check |
| `devx-activate-scripts` | Create shell/fish/zsh activate scripts |
| `devx-install-hooks` | Set git hooks path to hooks/ |
| `devx-install-tools` | Install actionlint, git-cliff, act_runner, tea, hadolint |
| `devx-install-checkmake` | Install checkmake (Makefile linter) |
| `devx-checkmake` | Lint Makefiles with checkmake |
| `devx-workflow-lint` | Static lint of Gitea Actions YAML (actionlint) |
| `devx-workflow-dryrun` | Dry-run all workflows (act_runner) |
| `devx-workflow-dryrun-safe` | Best-effort dry-run (skips if act_runner missing) |
| `devx-workflow-check` | Static lint + dry-run |
| `devx-notify-failure` | Create Gitea issue on CI failure |
| `devx-lint-ruff` | Run ruff check |
| `devx-lint-format` | Run ruff format --check |
| `devx-typecheck` | Run pyright |
| `devx-lint-bandit` | Run bandit security scan |
| `devx-lint-deps` | Check dependencies for vulnerabilities (pip-audit) |
| `devx-lint` | Run all lint targets |
| `devx-test-unit` | Run unit tests without coverage |
| `devx-pytest-cov` | Run pytest with coverage enforcement |
| `devx-check-mutable-globals` | Scan for mutable path globals |
| `devx-check-dep-docs` | Validate pyproject.toml deps are documented |
| `devx-check-test-coverage` | Check changed files have corresponding tests |
| `devx-check-docs` | Validate docs for stale references |
| `devx-check-test-speed` | Verify test suite timing |
| `devx-pre-push` | Run lint + tests before push |
| `devx-clean` | Remove caches, build artifacts, coverage data |
| `devx-setup-image` | Link /opt/venv + install project (for pre-built image CI jobs) |
| `devx-lint-dockerfiles` | Lint Dockerfiles with hadolint (fail-fast, parameterized by `DEVX_DOCKERFILE_PATHS`) |
| `devx-build-images` | Build Docker images from manifest (no push) |
| `devx-push-images` | Build and push Docker images to Gitea registry |
| `devx-build-images-dry-run` | Show what would be built/pushed |
| `devx-clean-images` | Delete old image versions (keep last 2 + latest) |
**Variables** (set BEFORE including devx.mak):
- `DEVX_PYTHON` — Python executable (default: `python3`)
- `DEVX_VENV` — venv directory (default: `.venv`)
- `DEVX_BIN` — venv bin directory (default: `$(DEVX_VENV)/bin`)
- `DEVX_LINT_PATHS` — paths for ruff/bandit (default: `src/ tests/`)
- `DEVX_COV_PKG` — coverage package (default: `src/devx`)
- `DEVX_TEST_PATHS` — pytest paths (default: `tests/`)
- `DEVX_PR_BASE` — PR base branch (default: `master`)
- `DEVX_DOCKERFILE_PATHS` — directory to search for Dockerfiles (default: `docker`)
- `DEVX_GITEA_REGISTRY` — registry URL (default: `git.oblachno.oblachno.fyi`)
- `DEVX_IMAGE_MANIFEST` — path to JSON manifest (default: `docker/images.json`)
- `DEVX_IMAGE_OWNER` — package owner for cleanup (default: `oblachno-oss`)
## Pre-built Docker Runner Images
devx builds and publishes three tier images to the Gitea container registry
to eliminate the 40-120s setup tax on every CI job:
| Image | Contains | Used by jobs |
|-------|----------|-------------|
| `ci-base-latest` | Python 3.12 + devx[ci] + tea | auto-merge, detect-and-configure |
| `ci-quality-latest` | ci-base + devx[lint] + actionlint + checkmake + hadolint | (badges in release-and-maintain uses ci-full) |
| `ci-full-latest` | ci-quality + devx[release,molecule,deploy] + git-cliff + OpenTofu | validate, release-and-maintain, molecule-tests, build-and-push |
**Build process** (in `build-images.yml` workflow):
1. `ci-base` builds FROM `gitea/runner-images:ubuntu-latest`
2. `ci-quality` builds FROM `ci-base-latest`
3. `ci-full` builds FROM `ci-quality-latest`
Each image is tagged `latest` and pushed to
`git.oblachno.oblachno.fyi/oblachno-oss/runner-images:<tier>-latest`.
**Using images in workflows**:
```yaml
jobs:
validate:
runs-on: docker
container: git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-full:latest
steps:
- uses: actions/checkout@v4
- name: Set up environment
run: make setup-image # links /opt/venv, installs project (no-deps)
```
**Image build/push tools** (tested Python modules):
- `devx.tools.build_image` — Build and push Docker images from Dockerfile or manifest
- `devx.tools.clean_images` — Delete old image versions via Gitea API (keep last N + latest)
**Usage in project Makefile**:
```makefile
DEVX_PYTHON := $(BIN)/python
DEVX_MAK := $(shell $(BIN)/python -c \
"from pathlib import Path; import devx; print(Path(devx.__file__).parent / 'make' / 'devx.mak')" \
2>/dev/null)
-include $(DEVX_MAK)
# Aliases for project-specific names
lint-ruff: devx-lint-ruff
workflow-lint: devx-workflow-lint
create-task: devx-create-task
```
## Key Conventions
- Python 3.12+ required (ruff/pyright target `py312`)
@@ -343,3 +547,113 @@ system loads `.env` automatically via `python-dotenv`.
- Line length: 120 chars
- Secrets are passed via environment variables, never on the command line
- All user-facing strings wrapped in `_()` for i18n
### Container-Level Fix Verification (Mandatory)
**Rule:** Before pushing any fix that modifies container state (CA certs,
config files, installed packages, daemon restarts), reproduce the exact
sequence locally with the actual Docker image. Do not push to CI as the
first test.
This is a hard rule, not a suggestion. CI cycles take 20+ minutes and
ephemeral staging VMs are destroyed after each run, making interactive
debugging impossible. A local reproduction takes 30 seconds and catches
silent failures immediately.
**Procedure:**
1. `docker pull <actual_image>`
2. `docker run -d --name <test> ...` and wait for it to start
3. Run the exact commands from the Ansible task or script
4. Verify the state change took effect
5. Clean up: `docker rm -f <test>`
### Verified State Modification (Mandatory)
Ansible tasks that modify container state with `changed_when: false`
MUST include a post-task verification step that confirms the state
change took effect. `changed_when: false` suppresses both change
detection AND failure visibility — a task can silently do nothing and
report `ok`.
## Subagent Delegation Policy
Custom subagent profiles are defined in `.devin/agents/` (project-specific)
and `~/.config/devin/agents/` (global, shared across repos). The agent MUST
automatically delegate to the appropriate subagent based on the task —
the user should not need to specify which profile to use.
### Available Profiles
**Global** (shared across all projects):
| Profile | Location | Purpose |
|---------|----------|---------|
| `pr-reviewer` | `~/.config/devin/agents/` | 13-category PR checklist + quality gates |
| `release-check` | `~/.config/devin/agents/` | Pre-merge readiness validation |
**devx-specific** (in `.devin/agents/`):
| Profile | Purpose |
|---------|---------|
| `ci-investigator` | Investigate CI failures (validate, release-and-maintain, build-images) |
| `dep-upgrader` | Python dependency upgrades in pyproject.toml with dep-doc validation |
| `docker-image-builder` | Build/push/cleanup 3-tier runner images (ci-base, ci-quality, ci-full) |
| `doc-sync-specialist` | Doc coverage, doc linting, wiki sync integrity |
| `workflow-validator` | actionlint + act_runner dry-run validation |
### When to Delegate Automatically
| Trigger | Profile | Mode |
|---------|---------|------|
| CI run failure (validate, release-and-maintain, build-images) | `ci-investigator` | Background |
| PR ready for review | `pr-reviewer` | Foreground |
| Dependency upgrade requested | `dep-upgrader` | Background |
| Docker image build/push needed | `docker-image-builder` | Background |
| Doc coverage failure or wiki sync issue | `doc-sync-specialist` | Background |
| Workflow YAML modified or validation needed | `workflow-validator` | Background |
| Branch ready for merge | `release-check` | Foreground |
### Delegation Rules
1. **Auto-select the profile.** Do not ask the user which profile to use.
2. **Background by default, foreground when blocking.**
3. **Provide full context in the prompt** — subagents don't inherit conversation history.
4. **One subagent per concern.** Chain: investigate → fix in main session → review.
5. **Don't delegate minor work** (<30s, <50 lines of context).
6. **Compact after subagent returns.**
7. **Never skip delegation to save time** — it keeps main context small.
## Feedback Issue Handling
Subagents create Gitea issues in the current repo when they encounter
tool, workflow, or process issues that warrant follow-up. These issues
use the `feedback` label plus a category label (`tooling`,
`ci-improvement`, `doc-improvement`, `workflow-improvement`).
Standard labels are created automatically by `configure_repo` (runs in
post-merge on every master push). If a label does not exist yet, the
subagent's issue creation will still succeed — labels can be added
afterwards.
### When a Subagent Reports a Feedback Issue URL
1. **Acknowledge it** in your response to the user — mention the issue URL
2. **Do NOT close or modify** the issue — it is for follow-up work
3. **Do NOT create a PR** to address it unless the user explicitly asks
4. If the user asks to address feedback, spawn a subagent to investigate
the issue and implement a fix
### Creating Feedback Issues Manually
As the parent agent, you can also create feedback issues directly using
the Gitea MCP (`issue_write` with `create_issue` method). Follow the
same format as subagents:
- Title: `[feedback] <category>: <short description>`
- Labels: `feedback` + category label
- Body: include context, tool/workflow, issue, reproduction, affected
files, suggested investigation, and "Reported by: parent agent"
Always deduplicate first via `list_issues` with `labels: "feedback"`.
+445
View File
@@ -2,6 +2,451 @@
All notable changes to this project will be documented in this file.
## [0.44.0] - 2026-07-13
### Features
- Add fix_pr_title module and update_pr API method
## [0.43.0] - 2026-07-13
### Features
- Add get_customer_vm_ip and get_observability_vm_ip to I/O check
## [0.42.0] - 2026-07-13
### Features
- Add I/O function isolation check and skip integration tests
## [0.41.2] - 2026-07-13
### Bug Fixes
- Auto-discover molecule root instead of hardcoding gitea-runner
## [0.41.1] - 2026-07-13
### Bug Fixes
- Check_test_isolation accepts multiple --test-path values
## [0.41.0] - 2026-07-13
### Features
- Test isolation pytest plugin, shift-left quality gates, dep upgrades
## [0.40.1] - 2026-07-12
### Bug Fixes
- Fall back to CI token when reviewer self-approval is rejected
## [0.40.0] - 2026-07-11
### Features
- Detect double-prefix in Vikunja task title during pre-merge validation
## [0.39.0] - 2026-07-09
### Features
- Extract shared utilities from infra and grm into devx
## [0.38.0] - 2026-07-08
### Features
- Introduce role-based Gitea API token environment variables
## [0.37.0] - 2026-07-07
### Features
- Consolidate docs checks into devx-docs-check target
## [0.36.2] - 2026-07-07
### Bug Fixes
- GiteaClient.set_repo_variable uses PUT instead of PATCH
## [0.36.1] - 2026-07-07
### Bug Fixes
- Preserve .badges/ dir during git clean in push_badges
## [0.36.0] - 2026-07-07
### Features
- Add GiteaClient repo variable methods and parallelize pytest-cov
## [0.35.7] - 2026-07-06
### Bug Fixes
- Use Gitea wiki dash-marker filename convention
## [0.35.6] - 2026-07-06
### Bug Fixes
- Add delay before wiki verification to avoid race condition
## [0.35.5] - 2026-07-06
### Bug Fixes
- Embed token in wiki clone URL for push auth
## [0.35.4] - 2026-07-06
### Bug Fixes
- Configure git identity before commit in sync_wiki
## [0.35.3] - 2026-07-06
### Bug Fixes
- Replace --strict with --verify for sync_wiki
## [0.35.2] - 2026-07-06
### Bug Fixes
- Exclude .vale directory from lint_docs scanning
## [0.35.1] - 2026-07-06
### Refactor
- Rewrite sync_wiki.py to use git-based approach
## [0.35.0] - 2026-07-06
### Features
- Enrich lint_docs.py with single H1, max depth, line length, code block lang, orphan checks
## [0.34.0] - 2026-07-06
### Features
- Enhance documentation-as-code with badges, version refs, Vale
## [0.33.4] - 2026-07-06
### Refactor
- Remove project-specific references from devx
## [0.33.3] - 2026-07-06
### Bug Fixes
- Make wiki sync resilient to API timeouts and stale page lists
## [0.33.2] - 2026-07-05
### Bug Fixes
- Abort sync_wiki when list_wiki_pages fails
## [0.33.1] - 2026-07-05
### Bug Fixes
- Build images after post-merge publish, not on push
## [0.33.0] - 2026-07-05
### Features
- Add check_api_identity_checks, setup_ssh_key, and api utils
## [0.32.1] - 2026-07-01
### Bug Fixes
- Add missing i18n translations for new tools
## [0.32.0] - 2026-07-01
### Features
- Extract docker-login, tofu-ops, check-deps, install-tofu to Python tools
## [0.31.0] - 2026-07-01
### Features
- Centralize venv management in devx.mak
## [0.30.0] - 2026-07-01
### Features
- Add standard label creation to configure_repo
## [0.29.1] - 2026-07-01
### Bug Fixes
- Strip task ID prefix from commit messages in extract_conventional_msg
## [0.29.0] - 2026-07-01
### Features
- Detect badge commits as automated CI commits
## [0.28.0] - 2026-07-01
### Features
- Auto-rebase in auto-merge, new rebase tools, CLI registration
## [0.27.3] - 2026-06-30
### Bug Fixes
- Retry wiki integrity check on transient API timeout
## [0.27.2] - 2026-06-29
### Bug Fixes
- Retry release push on non-fast-forward with rebase loop
## [0.27.1] - 2026-06-28
### Bug Fixes
- Exclude .devin/.terraform dirs from lint_docs, add duplicate heading excludes
## [0.27.0] - 2026-06-28
### Features
- Add lint_docs tool, fix doc_coverage/check_translations for any repo
## [0.26.4] - 2026-06-28
### Bug Fixes
- Wrap all user-facing strings with _() for i18n completeness
## [0.26.3] - 2026-06-28
### Bug Fixes
- Pin all dependencies to exact versions for reproducibility
## [0.26.2] - 2026-06-28
### Bug Fixes
- Block admin merge override and auto-approve with review token
## [0.26.1] - 2026-06-28
### Bug Fixes
- Force pip upgrade in setup-image to install new dependencies
## [0.26.0] - 2026-06-28
### Features
- Add distribute_items CI tool for parallel VM deployment
## [0.25.0] - 2026-06-28
### Features
- Add manual review support to pr_review (--event, --body, --checklist-confirmed)
## [0.24.1] - 2026-06-28
### Refactor
- Add find_task_by_identifier, config fallbacks for tools
## [0.24.0] - 2026-06-27
### Features
- Add pr_status, pr_logs, pr_label tools
## [0.23.4] - 2026-06-27
### Bug Fixes
- Add --auto-login to all notify_failure calls in workflows
- Classify .gitea/** as user-facing for devx, support glob in user_facing_overrides
## [0.23.3] - 2026-06-27
### Bug Fixes
- Correct clean_images delete URL and add retry with error handling
## [0.23.2] - 2026-06-27
### Bug Fixes
- Add skip-ci flag to release commits and concurrency to build-images
## [0.23.1] - 2026-06-27
### Bug Fixes
- Add rsync to ci-full image for molecule_docker
## [0.23.0] - 2026-06-27
### Features
- Add devx-lint-dockerfiles to devx.mak, alias setup-image
### Refactor
- Remove hadolint on-the-fly install from setup-image
## [0.22.1] - 2026-06-27
### Bug Fixes
- Checkout release tag in publish job
- Fail lint-dockerfiles when hadolint is missing
## [0.22.0] - 2026-06-27
### Features
- Document CI_GITEA_TOKEN scopes and add CI_GITEA_USERNAME to env var table
## [0.21.2] - 2026-06-27
### Bug Fixes
- Gate auto-merge on release-dry-run and unmask failures
## [0.21.1] - 2026-06-27
### Bug Fixes
- Devx-setup-image configures Gitea PyPI registry and shows pip errors
## [0.21.0] - 2026-06-27
### Features
- Add --auto-login to publish, extract configure_tea_login to gitea_cli
### Bug Fixes
- Publish job uses setup-release for build + tea login
- Remove tag fallback step from release workflow
## [0.20.3] - 2026-06-27
### Bug Fixes
- Release publish failures and duplicate release commits
## [0.20.2] - 2026-06-27
## [0.20.2] - 2026-06-27
### Bug Fixes
- Correct sed substitution in ci-full Dockerfile
## [0.20.1] - 2026-06-27
### Bug Fixes
- Correct image references in tier Dockerfiles
## [0.20.0] - 2026-06-27
### Features
- Add pre-built Docker runner images and tested image build/push tools
## [0.19.3] - 2026-06-26
### Refactor
- Make molecule weights configurable via pyproject.toml
## [0.19.2] - 2026-06-26
### Bug Fixes
- Calibrate molecule weights from actual CI execution times
## [0.19.1] - 2026-06-26
### Refactor
- Consolidate publish.yml into post-merge.yml
## [0.19.0] - 2026-06-26
### Features
- Add skip_ref_prefixes config to check_agent_docs
## [0.18.0] - 2026-06-26
### Features
- Extract generic tools into devx, expand devx.mak, remove personal references
## [0.17.0] - 2026-06-26
### Features
- Weighted LPT distribution, workflow fixes, decouple vikunja/sync-wiki from release
## [0.16.0] - 2026-06-26
### Features
- Single-source-of-truth config via [tool.devx] in pyproject.toml
## [0.15.0] - 2026-06-26
### Features
- Add create-task, create-pr, pre-push-check tools and devx.mak fragment
## [0.14.2] - 2026-06-26
### Bug Fixes
- Make repo arg optional in publish CLI, auto-detect from GITHUB_REPOSITORY
## [0.14.1] - 2026-06-25
### Bug Fixes
- Handle 'already a release' error idempotently in publish
## [0.14.0] - 2026-06-25
### Features
- Add FORCE_DEPLOY env var, --git flag, --from-tag flag
## [0.13.0] - 2026-06-25
### Features
+3 -3
View File
@@ -208,8 +208,8 @@ 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
devx
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
devx 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.
+102 -53
View File
@@ -1,4 +1,4 @@
.PHONY: all setup setup-ci setup-quality setup-release install update lint lint-ruff lint-format typecheck lint-bandit lint-deps lint-all test test-unit pytest-cov clean workflow-lint workflow-dryrun workflow-check install-tools install-hooks activate-scripts
.PHONY: all setup setup-ci setup-quality setup-release setup-image install update lint lint-all lint-dockerfiles test test-unit pytest-cov clean install-tools install-hooks activate-scripts checkmake check-mutable-globals check-dep-docs check-test-speed build-images push-images build-images-dry-run clean-images
PYTHON := python3
VENV := .venv
@@ -6,6 +6,36 @@ BIN := $(VENV)/bin
all: setup
# --- devx.mak integration ----------------------------------------------------
# Include shared targets from the devx package itself (venv management,
# workflow-lint, notify-failure, checkmake, lint targets, quality checks, etc.)
# Since devx IS the package, we can include its own devx.mak.
DEVX_PYTHON := $(BIN)/python
DEVX_VENV := $(VENV)
DEVX_BIN := $(BIN)
DEVX_LINT_PATHS := src/ tests/
DEVX_COV_PKG := src/devx
DEVX_TEST_PATHS := tests/
DEVX_MAK := $(shell $(BIN)/python -c \
"from pathlib import Path; import devx; print(Path(devx.__file__).parent / 'make' / 'devx.mak')" \
2>/dev/null)
# Fallback: when the venv doesn't exist yet (chicken-and-egg), use the
# source tree copy directly. devx IS the package, so src/devx/make/devx.mak
# is always available in this repo.
ifeq ($(strip $(DEVX_MAK)),)
DEVX_MAK := $(CURDIR)/src/devx/make/devx.mak
endif
-include $(DEVX_MAK)
# venv, .env, and activate-scripts are provided by devx.mak
# (devx-venv, devx-env, devx-activate-scripts, $(DEVX_VENV)/bin/activate rule)
# Aliases for convenience and backward compatibility:
.PHONY: venv activate-scripts
venv: devx-venv
.env: devx-env
activate-scripts: devx-activate-scripts
# Full setup for local development
setup: $(VENV)/bin/activate .env activate-scripts install-tools
@$(BIN)/pip install -e '.[dev]' 2>/dev/null; \
@@ -25,23 +55,18 @@ setup-quality: $(VENV)/bin/activate .env install-tools
# Setup for release jobs (needs git-cliff, tea, lint tools)
setup-release: $(VENV)/bin/activate .env
@$(BIN)/pip install -e '.[ci,lint]' 2>/dev/null; \
@$(BIN)/pip install -e '.[ci,lint,release]' 2>/dev/null; \
$(BIN)/python -m devx.tools.install_tools --tool git-cliff --tool tea; \
export PATH="$(HOME)/.local/bin:$$PATH"; \
$(BIN)/python -m devx.tools.setup --bin "$(BIN)" --extras "ci,lint" --no-pre-commit
$(BIN)/python -m devx.tools.setup --bin "$(BIN)" --extras "ci,lint,release" --no-pre-commit
.env:
@if [ ! -f .env ]; then cp .env.example .env; echo "Created .env from .env.example — please edit it."; fi
$(VENV)/bin/activate:
@python3 -c "import sys; v=sys.version_info; assert v >= (3, 12), f'Python 3.12+ required, found {v.major}.{v.minor}'; print(f'Python {v.major}.{v.minor}.{v.micro} OK')"
$(PYTHON) -m venv $(VENV)
$(BIN)/pip install --upgrade pip setuptools wheel
activate-scripts: $(VENV)/bin/activate
@test -f activate.sh || (echo '#!/usr/bin/env bash' > activate.sh && echo 'source "$$(cd "$$(dirname "$${BASH_SOURCE[0]}")" && pwd)/.venv/bin/activate"' >> activate.sh && chmod +x activate.sh)
@test -f activate.fish || (echo '#!/usr/bin/env fish' > activate.fish && echo 'set -l script_dir (dirname (status --current-filename))' >> activate.fish && echo 'source "$$script_dir/.venv/bin/activate.fish"' >> activate.fish && chmod +x activate.fish)
@test -f activate.zsh || (echo '#!/usr/bin/env zsh' > activate.zsh && echo '0="$${ZERO:-$${0:#$$ZSH_ARGZERO}}"' >> activate.zsh && echo '0="$${$${(M)0:#/*}:-$$PWD/$$0}"' >> activate.zsh && echo 'source "$${0:A:h}/.venv/bin/activate"' >> activate.zsh && chmod +x activate.zsh)
# Setup for pre-built image jobs (deps already in image, just link venv + install project)
# Note: Not aliased to devx-setup-image because devx's own CI images may have
# an older devx.mak that doesn't yet define devx-setup-image. Consumer repos
# (grm, infra) can safely alias to devx-setup-image since they install devx from PyPI.
setup-image:
@if [ -d /opt/venv ]; then ln -sf /opt/venv $(VENV); . $(VENV)/bin/activate && pip install --no-cache-dir -e . 2>/dev/null; \
else echo "[setup-image] /opt/venv not found — falling back to setup-ci"; $(MAKE) setup-ci; fi
install-hooks:
@cp hooks/pre-commit .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit
@@ -52,48 +77,72 @@ install-tools: $(VENV)/bin/activate
@$(BIN)/pip install -e '.' 2>/dev/null; \
$(BIN)/python -m devx.tools.install_tools
lint-ruff:
$(BIN)/ruff check src/ tests/
# Aliases — project-specific names map to devx.mak targets
.PHONY: lint-ruff lint-format typecheck lint-bandit lint-deps lint
.PHONY: workflow-lint workflow-dryrun workflow-dryrun-safe workflow-check
.PHONY: notify-failure checkmake check-mutable-globals check-dep-docs
.PHONY: check-test-speed check-test-coverage check-docs check-test-isolation check-translations
.PHONY: create-task create-pr push-with-pr git-push rebase pr-rebase
.PHONY: lint-all lint-dockerfiles
lint-ruff: devx-lint-ruff
lint-format: devx-lint-format
typecheck: devx-typecheck
lint-bandit: devx-lint-bandit
lint-deps: devx-lint-deps
lint: devx-lint
workflow-lint: devx-workflow-lint
workflow-dryrun: devx-workflow-dryrun
workflow-dryrun-safe: devx-workflow-dryrun-safe
workflow-check: devx-workflow-check
notify-failure: devx-notify-failure
checkmake: devx-checkmake
check-mutable-globals: devx-check-mutable-globals
check-dep-docs: devx-check-dep-docs
check-test-speed: devx-check-test-speed
check-test-isolation: devx-check-test-isolation
check-translations: devx-check-translations
check-test-coverage: devx-check-test-coverage
check-docs: devx-check-docs
create-task: devx-create-task
create-pr: devx-create-pr
push-with-pr: devx-push-with-pr
git-push: devx-push
rebase: devx-rebase
pr-rebase: devx-pr-rebase
lint-format:
$(BIN)/ruff format --check src/ tests/
lint-all: lint workflow-lint lint-dockerfiles
@echo "[lint-all] All linting checks passed."
typecheck:
$(BIN)/pyright
# Note: Not aliased to devx-lint-dockerfiles for the same reason as setup-image —
# devx's own CI images may have an older devx.mak. Consumer repos can safely alias.
lint-dockerfiles:
@echo "[lint-dockerfiles] Linting Dockerfiles with hadolint..."
@command -v hadolint >/dev/null 2>&1 || { echo "hadolint not found" >&2; exit 1; }
@find docker -name 'Dockerfile*' -exec hadolint {} +
@echo "[lint-dockerfiles] All Dockerfiles passed."
lint-bandit:
$(BIN)/bandit -r src/
test-unit: devx-test-unit
lint: lint-ruff lint-format typecheck lint-bandit
lint-deps:
@echo "Checking dependencies for known vulnerabilities..."
@.venv/bin/python -m ensurepip 2>/dev/null || true
@PIPAPI_PYTHON_LOCATION=$$(pwd)/.venv/bin/python .venv/bin/pip-audit --desc --skip-editable 2>&1 || true
lint-all: lint workflow-lint
workflow-lint:
@command -v actionlint >/dev/null 2>&1 || { echo "actionlint not found."; exit 1; }
actionlint -config-file .gitea/actionlint.yaml .gitea/workflows/*.yml
workflow-dryrun:
@command -v act_runner >/dev/null 2>&1 || { echo "act_runner not found."; exit 1; }
@echo "Dry-running all workflows..."
act_runner exec --dryrun -W .gitea/workflows/ 2>&1 | grep -E 'DRYRUN|ERROR|FAIL|Job'
workflow-check: workflow-lint workflow-dryrun
@echo "Workflow checks passed."
test-unit:
$(BIN)/pytest tests/unit/ -v --no-cov
pytest-cov:
$(BIN)/pytest tests/ -v --cov=src/devx --cov-report=term-missing --cov-fail-under=100
pytest-cov: devx-pytest-cov
test: pytest-cov
clean:
find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
find . -type f -name "*.pyc" -delete 2>/dev/null || true
rm -rf .coverage htmlcov/ dist/ build/ *.egg-info/
pre-push: lint-all pytest-cov
@echo "[pre-push] All checks passed. Proceeding with push."
clean: devx-clean
@echo "[clean] Done."
# ── Docker image management ──────────────────────────────────────────────────
build-images: devx-build-images
@echo "[build-images] Done."
push-images: devx-push-images
@echo "[push-images] Done."
build-images-dry-run: devx-build-images-dry-run
@echo "[build-images-dry-run] Done."
clean-images: devx-clean-images
@echo "[clean-images] Done."
+30 -13
View File
@@ -16,12 +16,12 @@ quality badges.
[![CI](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions/workflows/ci.yml/badge.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![License: GPL-3.0](https://img.shields.io/badge/license-GPL--3.0-blue)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/LICENSE)
[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/525d3b703321fc96efce8b2a84ed9eb8475ac829/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/525d3b703321fc96efce8b2a84ed9eb8475ac829/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/525d3b703321fc96efce8b2a84ed9eb8475ac829/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/525d3b703321fc96efce8b2a84ed9eb8475ac829/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/525d3b703321fc96efce8b2a84ed9eb8475ac829/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/525d3b703321fc96efce8b2a84ed9eb8475ac829/python.svg)](https://www.python.org/downloads/)
[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/08f1c46f6446a146ea400eb5dd5537da7a8834a1/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/08f1c46f6446a146ea400eb5dd5537da7a8834a1/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/08f1c46f6446a146ea400eb5dd5537da7a8834a1/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/08f1c46f6446a146ea400eb5dd5537da7a8834a1/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/08f1c46f6446a146ea400eb5dd5537da7a8834a1/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/08f1c46f6446a146ea400eb5dd5537da7a8834a1/python.svg)](https://www.python.org/downloads/)
## Why devx?
@@ -87,7 +87,7 @@ extra index and list devx in your dependencies:
```toml
[project]
dependencies = [
"devx>=0.11.1",
"devx>=0.44.0",
]
[tool.pip]
@@ -101,8 +101,8 @@ pip install -e .
```
> **Note:** If your project requires a specific devx version, pin it in
> `dependencies` (e.g., `"devx==0.11.1"`) or use a version constraint
> (e.g., `"devx>=0.11.1,<0.12"`).
> `dependencies` (for example, `"devx==0.44.0"`) or use a version constraint
> (for example, `"devx>=0.44.0,<0.45"`).
### Optional extras
@@ -158,6 +158,9 @@ python -m devx.ci.check_translations --translations path/to/translations.json
# Documentation coverage check
python -m devx.ci.doc_coverage --fail-on-missing
# Documentation lint (structure, links, headings, TODOs)
python -m devx.ci.lint_docs --root .
# Validate a commit message
python -m devx.ci.validate_commit_msg commit-msg.txt --branch master
@@ -326,10 +329,24 @@ The config system loads `.env` automatically via `python-dotenv`.
| `DEVX_DOCS_DIR` | `docs` | Documentation directory (used by sync_wiki) |
| `DEVX_STATUS_CHECKS` | `CI / quality (pull_request)` | Comma-separated status check contexts |
| `DEVX_PYPI_REGISTRY_URL` | — | Gitea PyPI registry URL (used by publish) |
| `REPO_TOKEN` | — | Gitea API token |
| `CI_GITEA_TOKEN` | — | Gitea API token (see scopes below) |
| `CI_GITEA_USERNAME` | — | Gitea username for registry authentication |
| `VIKUNJA_TOKEN` | — | Vikunja API token |
| `PYPI_TOKEN` | — | Standard PyPI token (takes precedence over Gitea registry) |
#### CI_GITEA_TOKEN scopes
The `CI_GITEA_TOKEN` is a single Gitea Personal Access Token used across all
workflows. It requires these scopes:
| Scope | Purpose |
|-------|---------|
| `read:repository` | Read repos, PRs, issues, branches |
| `write:repository` | Push commits, merge PRs, create tags/releases, create issues, set branch protection, push wiki |
| `read:package` | Pull packages from Gitea PyPI registry, pull Docker images |
| `write:package` | Publish packages to Gitea PyPI registry, push Docker images |
| `read:organization` | Query org-level runners for molecule test distribution |
### Per-project overrides
Projects using devx can override the default API URLs and language by setting
@@ -355,7 +372,7 @@ infrastructure = []
# Files that would default to user-facing but are actually infrastructure
infrastructure_overrides = [
"src/myproject/__init__.py", # only contains __version__
"src/myproject/__init__.py", # example only — only contains __version__
]
# Safety override for broad infrastructure patterns
@@ -403,7 +420,7 @@ make clean # Remove caches, build artifacts, coverage data
| `make lint-deps` | pip-audit dependency vulnerability scan |
| `make test-unit` | Unit tests without coverage |
| `make pytest-cov` | Unit tests with 100% coverage enforcement |
| `make workflow-lint` | actionlint on .gitea/workflows/*.yml |
| `make workflow-lint` | actionlint on `.gitea/workflows/*.yml` |
| `make workflow-dryrun` | act_runner exec --dryrun on all workflows |
| `make workflow-check` | workflow-lint + workflow-dryrun |
| `make clean` | Remove caches, build artifacts, coverage data |
@@ -417,7 +434,7 @@ devx is a self-contained Python package under `src/devx/`. It never imports
from scripts outside the package. All tools are invoked via
`python -m devx.ci.*`, `python -m devx.tools.*`, or `python -m devx.molecule.*`.
```
```text
src/devx/
├── __init__.py # Version (single source of truth, read by setuptools)
├── cli.py # Click-based CLI entry point (devx command)
+26
View File
@@ -0,0 +1,26 @@
# ci-base — lightweight image for CI jobs that only need devx core + tea.
#
# Used by: detect-type, detect-changes, validate-commit-msg, pr-review,
# auto-merge, sync-wiki, vikunja, configure-repo, discover-runners,
# molecule-report, discover-integration-runners
#
# Jobs using this image: setup is instant (ln -s /opt/venv .venv)
# No pip install needed — devx and all deps are pre-installed.
FROM gitea/runner-images:ubuntu-latest
# Create a virtual environment with all deps pre-installed
RUN python3 -m venv /opt/venv
ENV PATH="/opt/venv/bin:/root/.local/bin:$PATH"
# Install devx from local source (build context = devx repo root)
COPY . /tmp/devx
RUN pip install --no-cache-dir --upgrade pip setuptools wheel \
&& pip install --no-cache-dir /tmp/devx[ci] \
&& rm -rf /tmp/devx
# Install tea CLI (for Gitea API operations in CI)
RUN python3 -m devx.tools.install_tools --tool tea
# Workspace directory (actions/checkout mounts repo here)
WORKDIR /workspace
+24
View File
@@ -0,0 +1,24 @@
# ci-full — heaviest image, includes everything for release, molecule, deploy.
#
# Used by: release, publish, release-dry-run, molecule-tests,
# provision-infra, deploy-observability, provision-zitadel,
# deploy-customer, integration-tests
#
# Layers on top of ci-quality: adds release tools, molecule, deploy deps,
# git-cliff, and OpenTofu.
FROM git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-quality:latest
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
# Install rsync (required by molecule_docker for file sync between host and test containers)
RUN apt-get update && apt-get install -y --no-install-recommends rsync \
&& rm -rf /var/lib/apt/lists/*
# Install devx[release,molecule,deploy] from local source
COPY . /tmp/devx
RUN pip install --no-cache-dir /tmp/devx[release,molecule,deploy] \
&& rm -rf /tmp/devx
# Install git-cliff (changelog generator for release job) and OpenTofu (for infra deploy jobs)
RUN python3 -m devx.tools.install_tools --tool git-cliff --tool tofu
+17
View File
@@ -0,0 +1,17 @@
# ci-quality — image for lint, type-checking, badge generation.
#
# Used by: quality (lint-all + pytest-cov + checks), badges (generate_badges
# runs ruff/pyright/bandit to produce quality badge)
#
# Layers on top of ci-base: adds lint tools + actionlint + checkmake.
FROM git.oblachno.oblachno.fyi/oblachno-oss/runner-images/ci-base:latest
# Install devx[lint] from local source (adds ruff, pyright, bandit, etc.)
COPY . /tmp/devx
RUN pip install --no-cache-dir /tmp/devx[lint] \
&& rm -rf /tmp/devx
# Install CI/CD binary tools
RUN python3 -m devx.tools.install_tools --tool actionlint --tool vale --tool hadolint \
&& python3 -m devx.tools.install_checkmake
+20
View File
@@ -0,0 +1,20 @@
[
{
"name": "oblachno-oss/runner-images/ci-base",
"dockerfile": "docker/ci-base/Dockerfile",
"context": ".",
"tags": ["latest"]
},
{
"name": "oblachno-oss/runner-images/ci-quality",
"dockerfile": "docker/ci-quality/Dockerfile",
"context": ".",
"tags": ["latest"]
},
{
"name": "oblachno-oss/runner-images/ci-full",
"dockerfile": "docker/ci-full/Dockerfile",
"context": ".",
"tags": ["latest"]
}
]
@@ -0,0 +1,173 @@
# ADR-0001: Test Isolation Pytest Plugin and Shift-Left Quality Gates
Date: 2026-07-13
Status: Accepted
## Context
Unit tests in devx were slow (10s+) and getting slower. Investigation
revealed two root causes:
1. **Unpatched subprocess calls** — test functions calling
`subprocess.run`, `update_doc_versions`, or `run_cmd` without
`@patch` decorators, causing real subprocess execution during tests.
2. **Excessive iterations** — statistical tests with 1000-iteration
loops that should use property-based testing or smaller samples.
These issues were discovered manually by profiling with
`pytest --durations=0`. There was no automated check to prevent
regressions — new tests could introduce the same patterns and slow
down the suite again.
Additionally, translation completeness checks
(`devx.ci.check_translations`) only ran in CI, not locally. Developers
discovered missing translations at CI time, wasting round-trips.
## Decision
### 1. Test Isolation as a Pytest Plugin (pytest11 entry point)
Implement the test isolation check as a **pytest plugin** registered
via the `pytest11` entry point in `pyproject.toml`:
```toml
[project.entry-points.pytest11]
devx_test_isolation = "devx.tools.check_test_isolation"
```
This makes the check **transparent and always-on** — every `pytest`
invocation in any repo with devx installed automatically runs the
static analysis. No extra Makefile target or CI step needed.
The plugin (`devx.tools.check_test_isolation`) statically analyzes
test files during `pytest_collection_finish` and **fails the test run**
on any hard violation:
- **unpatched-subprocess**: `subprocess.run/call/Popen/check_call/check_output`
called in a test function without `@patch` or `with patch(...)`
- **unpatched-sleep**: `time.sleep` called without `@patch`
- **unpatched-helper**: known subprocess-spawning helpers
(`update_doc_versions`, `run_cmd`, `run_tests`) called without
`@patch` (and without patching their internal dependencies)
- **excessive-iterations**: `for _ in range(N)` where N > 100
- **heavy-module-import**: `httpx`, `ansible`, etc. imported at module
level in test files, slowing collection for all tests
- **reload-without-cleanup**: `importlib.reload()` called an odd number
of times, leaving module state modified
Transitive-subprocess findings (via call-graph analysis) are reported
as **advisories** — the static analysis can't predict early exits or
runtime branch conditions, so the runtime audit is authoritative.
The plugin also wraps `subprocess.run` at runtime to catch real
subprocess calls that leak through transitive call paths (for example
`CliRunner.invoke(main)``main()``update_doc_versions()`
`subprocess.run()`). If a test spawns a real subprocess without
`@patch`, the test fails.
A standalone CLI (`python -m devx.tools.check_test_isolation`) is also
provided for CI gates and pre-commit hooks where pytest isn't run.
### 2. Shift-Left Quality Gates in `make lint`
Add `devx-check-translations` and `devx-check-test-isolation` to the
`devx-lint` target in `devx.mak`. This means `make lint` now runs:
- ruff check + format
- pyright typecheck
- bandit security scan
- **translation completeness** (missing keys, dead keys, missing languages)
- **test isolation** (unpatched subprocess, time.sleep, excessive loops)
These were previously CI-only checks. Running them in `make lint`
catches issues at the developer's machine, not in CI.
### 3. Pre-commit Hook Coverage
Update the pre-commit hook to run all three shift-left checks:
test speed, translation completeness, and test isolation. This
catches issues even earlier than `make lint` — before the commit
is even created.
## Consequences
### Positive
- **Automatic enforcement**: The pytest plugin runs on every `pytest`
invocation across devx, grm, and infra — no per-repo configuration
needed. New tests with unpatched subprocess calls fail immediately.
- **Shift-left**: Translation gaps and test isolation violations are
caught locally (pre-commit / `make lint`) instead of in CI.
- **Fast feedback**: Static analysis adds <0.1s to test runs; runtime
subprocess audit adds negligible overhead (wrapper checks a
thread-local flag).
- **Transitive detection**: The call-graph BFS traces
`CliRunner.invoke(main)``main()``update_doc_versions()`
`subprocess.run()`, catching indirect subprocess leaks that direct
analysis misses. The runtime audit provides authoritative enforcement.
- **No false positives**: The call graph correctly recognizes that
patching `run_cmd` makes `run_tests` (which calls `run_cmd`) safe,
and class methods are excluded to avoid false positives when classes
like `TeaCLI` are patched.
### Negative
- **Coverage instrumentation gap**: The pytest plugin module is loaded
before coverage starts, so module-level code (decorators, class
definitions) appears uncovered. Mitigated by `-p no:devx_test_isolation`
in devx's own `pyproject.toml` `addopts` and `# pragma: no cover` on
plugin hook functions.
- **Static analysis limitations**: The call-graph BFS can't predict
runtime branch conditions or early exits — a test that patches
`shutil.which` to return `None` may skip the subprocess path
entirely, but the static analysis still reports it. Transitive
findings are advisories (exit 0) for this reason; the runtime audit
is authoritative.
- **Translation burden**: Every new `_()` call in source requires
adding 6 language translations. This is by design (all supported
languages must be complete) but adds friction for quick prototypes.
## Implementation Details
### Pytest Plugin Discovery
The `pytest11` entry point is the standard mechanism for pytest
plugins. When devx is installed (via pip), pytest auto-discovers
the plugin. No `conftest.py` or `pytest_plugins` declaration needed
in consumer repos.
### Disabling the Plugin
- `--no-test-isolation` flag: disables static analysis and runtime
subprocess audit for a single run
- `-p no:devx_test_isolation` in `addopts`: disables for a repo
(used in devx's own `pyproject.toml` for coverage reasons)
### Call-Graph Analysis
The `CallGraph` class parses all `.py` files under `src/` and builds
a map of function → called functions. When a test calls
`CliRunner.invoke(target)`, a BFS traces the call graph from `target`
to find all reachable functions. Class methods are excluded from the
call graph to avoid false positives when classes are patched (for example
`@patch("...TeaCLI")` mocks all methods). The BFS respects `@patch`
decorators — if a function is patched, traversal stops at that node.
### Runtime Subprocess Audit
The `_SubprocessAudit` singleton wraps `subprocess.run`, `call`,
`check_call`, `check_output`, and `Popen` with thread-local
recording wrappers. During each non-integration test, the wrapper
records calls; if any are recorded (that is the test didn't `@patch`
subprocess), the test fails. The wrappers check a thread-local flag,
so inactive audits have zero overhead beyond the flag check.
### Known Subprocess Helpers
The `KNOWN_SUBPROCESS_HELPERS` dict maps function names to
descriptions. `HELPER_INTERNAL_CALLS` maps each helper to the
function names it internally calls, enabling transitive safety
checks for direct calls in test functions. The call-graph BFS
handles transitive detection for `CliRunner.invoke` targets. Both
are defined in `check_test_isolation.py` and can be extended as
new subprocess-spawning helpers are added to devx.
+11 -10
View File
@@ -12,12 +12,12 @@ project to be reusable across all oblachno-oss repositories.
[![CI](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions/workflows/ci.yml/badge.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![License: GPL-3.0](https://img.shields.io/badge/license-GPL--3.0-blue)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/LICENSE)
[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/525d3b703321fc96efce8b2a84ed9eb8475ac829/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/525d3b703321fc96efce8b2a84ed9eb8475ac829/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/525d3b703321fc96efce8b2a84ed9eb8475ac829/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/525d3b703321fc96efce8b2a84ed9eb8475ac829/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/525d3b703321fc96efce8b2a84ed9eb8475ac829/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/525d3b703321fc96efce8b2a84ed9eb8475ac829/python.svg)](https://www.python.org/downloads/)
[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/08f1c46f6446a146ea400eb5dd5537da7a8834a1/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/08f1c46f6446a146ea400eb5dd5537da7a8834a1/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/08f1c46f6446a146ea400eb5dd5537da7a8834a1/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/08f1c46f6446a146ea400eb5dd5537da7a8834a1/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/08f1c46f6446a146ea400eb5dd5537da7a8834a1/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/08f1c46f6446a146ea400eb5dd5537da7a8834a1/python.svg)](https://www.python.org/downloads/)
## Overview
@@ -74,14 +74,14 @@ Add devx to your `pyproject.toml` dependencies and configure the registry:
```toml
[project]
dependencies = [
"devx>=0.11.1",
"devx>=0.44.0",
]
[tool.pip]
extra-index-url = "https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple"
```
Pin a specific version if needed: `"devx==0.11.1"` or `"devx>=0.11.1,<0.12"`.
Pin a specific version if needed: `"devx==0.44.0"` or `"devx>=0.44.0,<0.45"`.
### Optional extras
@@ -131,7 +131,7 @@ wiki sync details.
devx provides a `devx` CLI with three command groups:
- `devx ci <command>` — CI/CD automation (17 commands)
- `devx tools <command>` — Developer tools (7 commands)
- `devx tools <command>` — Developer tools (9 commands)
- `devx molecule <command>` — Molecule testing (4 commands, optional)
See [CLI Commands](CLI-Commands) for full command documentation with examples.
@@ -149,7 +149,7 @@ fallback. Key variables:
| `DEVX_REPO_NAME` | **(must be set)** | Repository name |
| `DEVX_TASK_PREFIX` | `DEVX` | Task ID prefix (GRM, OBL-INFRA, etc.) |
| `DEVX_LANG` | `en` | Language for i18n (en, bg, de, ru, zh, pl) |
| `REPO_TOKEN` | — | Gitea API token |
| `CI_GITEA_TOKEN` | — | Gitea API token |
| `VIKUNJA_TOKEN` | — | Vikunja API token |
See [AGENTS.md](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/AGENTS.md)
@@ -158,6 +158,7 @@ for the full configuration reference, PR workflow, and project conventions.
## Wiki pages
- [Home](Home) — This page
- [Getting Started](Getting-Started) — Installation, configuration, and quick start guide
- [CLI Commands](CLI-Commands) — Full CLI command documentation with examples
- [Architecture](Architecture) — Package structure, module descriptions, design principles
- [CI/CD Workflow](CI-CD-Workflow) — Pipeline documentation, workflows, and CI scripts
+1
View File
@@ -1,5 +1,6 @@
{
"index.md": "Home",
"user/getting-started.md": "Getting-Started",
"user/cli-commands.md": "CLI-Commands",
"tech/architecture.md": "Architecture",
"tech/ci-cd-workflow.md": "CI-CD-Workflow"
@@ -0,0 +1,158 @@
# Retrospective: Self-Approval Fallback and CI Consolidation
## Date
2026-07-12
## Context
The devx package (reusable CI/CD tools) underwent two significant
changes during this period: workflow consolidation (DEVX-126) and the
self-approval fallback fix (DEVX-127). The self-approval bug was the
last remaining blocker for end-to-end automated CI/CD across all
oblachno repos. This retrospective covers devx v0.40.0 through v0.40.1.
## Scope
PRs: DEVX-125 (double-prefix detection), DEVX-126 (CI consolidation),
DEVX-127 (self-approval fallback). ~16 commits including release/badge
churn.
## Timeline of Key Failures
| Run | Issue | Fix Commit |
|--------|----------------------------------------------|------------|
| infra #2562 | Self-approval rejected (403) | `d035b62` |
| devx CI | Auto-merge review body too short (< 20 chars) | `fc613d4` |
| devx CI | test_setup flaky due to PIP_BREAK_SYSTEM_PACKAGES | `043f259` |
| devx CI | Missing translations for self-approval messages | `0d8c7f5` |
## What Served Us Well
- **Test-driven fix for pr_review.py.** The self-approval fallback was
implemented with full test coverage before being deployed. Tests
covered both the fallback-available and fallback-unavailable paths,
ensuring the code was correct before it hit CI.
- **i18n enforcement caught missing translations.** The translation
completeness check flagged the new self-approval error messages that
were added without corresponding translation entries. This prevented
untranslated strings from reaching production.
- **Consolidated CI workflow.** DEVX-126 merged 7 separate CI jobs into
a single `validate` job, reducing runner overhead and eliminating
inter-job dependency issues. The consolidation pattern was then
applied to grm and infra.
- **Conventional commit enforcement.** The `validate_commit_msg` check
caught a double-prefix in the Vikunja task title (DEVX-125), which
would have caused auto-merge validation failures downstream.
## What Slowed Us Down
### 1. Self-Approval Bug Not Caught Earlier (1 infra CI failure)
The `pr_review.py` script used the `REVIEWER_GITEA_API_TOKEN` for
APPROVE events. When the token belonged to the PR author, Gitea
rejected the self-approval with 403. This was only discovered when the
infra PR CI run #2562 failed — the devx CI had passed because devx PRs
were reviewed by a different user.
**Root cause:** No test simulated the self-approval rejection scenario.
The tests mocked the Gitea API to always return 200 for review
submissions.
**Time wasted:** ~2 hours (cross-repo investigation + fix + test).
**Fix:** Added fallback to `CI_GITEA_API_TOKEN` when the reviewer token
is rejected with self-approval. The fallback is transparent — the
script logs a warning and retries with the CI token.
**Lesson:** Test API interactions against all HTTP error codes the
external system can return, not only the happy path. For Gitea, this
includes 403 (self-approval), 409 (conflict), and 422 (validation).
### 2. Auto-Merge Review Body Length Check (1 CI failure)
The auto-merge validation requires APPROVE review bodies to be > 20
chars (to prevent perfunctory approvals). The automated review posted
by `pr_review.py` had a body of exactly 17 chars, failing the check.
**Root cause:** The review body was a generic "Automated review passed"
message that was too short. The length check was added to prevent
rubber-stamping by human reviewers, but it also affected automated
reviews.
**Time wasted:** ~1 CI run.
**Fix:** Expanded the automated review body to include a summary of
checked categories, ensuring it exceeds 20 chars.
**Lesson:** Automated reviews need substantive bodies too. The length
check doesn't distinguish between human and automated reviewers.
### 3. test_setup Flaky Due to Environment Variable (1 CI failure)
`test_setup.py` failed intermittently because `PIP_BREAK_SYSTEM_PACKAGES`
was set in the CI environment but not in local tests. The test didn't
isolate itself from the environment variable.
**Root cause:** The test assumed a clean environment but CI sets
`PIP_BREAK_SYSTEM_PACKAGES=1` globally. The test's behavior changed
based on this env var.
**Time wasted:** ~1 CI run.
**Fix:** Isolated the test from the env var using `monkeypatch.delenv`.
**Lesson:** Tests that interact with environment-dependent behavior
should explicitly set or unset the relevant env vars, not assume
defaults.
### 4. Missing Translations for New Messages (1 CI failure)
The self-approval fallback added new user-facing messages (warning
about token fallback) but didn't add translations for all supported
languages. The translation completeness check caught this.
**Root cause:** New `click.echo()` calls were added with `_()` wrappers
but the translation JSON wasn't updated.
**Time wasted:** ~1 CI run.
**Fix:** Added translations for all new messages in `translations.json`.
**Lesson:** When adding new `_()` wrapped strings, update
`translations.json` in the same commit. The i18n check is strict —
100% completeness is required.
## Improvements Implemented
### 1. Self-Approval Fallback (HIGH impact)
`pr_review.py` now falls back to `CI_GITEA_API_TOKEN` for APPROVE
events when the reviewer token is rejected as self-approval. This
unblocked auto-merge across all three repos.
### 2. Double-Prefix Detection (MEDIUM impact)
`check_auto_merge_ready.py` now detects and rejects Vikunja task titles
that include the identifier prefix (for example, "DEVX-127: Fix...").
The validator adds the prefix automatically, so a double prefix would
fail validation.
### 3. CI Workflow Consolidation (MEDIUM impact)
Merged 7 separate CI jobs into a single `validate` job, reducing runner
overhead by ~5 min per CI run and eliminating inter-job dependency
issues.
## Action Items for Future Sessions
1. **Test API interactions against all relevant HTTP error codes.**
Don't only test the happy path. For Gitea: 200, 201, 204, 403, 404,
409, 422.
2. **Update translations in the same commit as new `_()` strings.**
The i18n check will fail otherwise.
3. **Isolate tests from environment variables.** Use `monkeypatch.setenv`
or `monkeypatch.delenv` for any env var the test's behavior depends on.
4. **Ensure automated review bodies are substantive (> 20 chars).**
Include a summary of checked categories.
5. **When adding fallback logic, test both the fallback-available and
fallback-unavailable paths.** Both must be covered for 100% branch
coverage.
+74 -57
View File
@@ -6,7 +6,7 @@ from scripts outside the package.
## Package structure
```
```text
src/devx/
├── __init__.py # Version (single source of truth, read by setuptools)
├── cli.py # Click-based CLI entry point (devx command)
@@ -41,6 +41,7 @@ src/devx/
│ ├── setup.py # Environment setup (venv, deps, hooks, tea login)
│ ├── install_tools.py # Install actionlint, git-cliff, act_runner, tea
│ ├── check_test_speed.py # Measure unit test execution time
│ ├── check_test_isolation.py # Pytest plugin: detect un-hermetic test patterns
│ ├── configure_repo.py # Branch protection and label setup
│ ├── generate_badges.py # Badge SVG generation
│ ├── generate_cliff_config.py # Generate cliff.toml with correct prefix
@@ -86,7 +87,7 @@ overridden via environment variables with the `DEVX_` prefix. Provides:
- `GITEA_API_URL` / `VIKUNJA_API_URL` — API endpoints
- `REPO_OWNER` — repository owner (must be set per-project)
- `TASK_PREFIX` / `TASK_ID_RE` — task ID prefix and regex (e.g., `DEVX-N`)
- `TASK_PREFIX` / `TASK_ID_RE` — task ID prefix and regex (for example, `DEVX-N`)
- `VIKUNJA_PROJECT_ID` — Vikunja project for task tracking
- `DEFAULT_TIMEOUT`, `DEFAULT_PER_PAGE` — HTTP client defaults
- `MAX_RETRIES`, `RETRY_BACKOFF_BASE`, `RETRY_STATUS_CODES` — retry config
@@ -103,7 +104,7 @@ Custom exception hierarchy:
### `i18n.py`
Simple i18n system using a JSON translations file (`translations.json`).
Supports five languages: `en`, `bg`, `de`, `ru`, `zh`. The `_()` function
Supports six languages: `en`, `bg`, `de`, `pl`, `ru`, `zh`. The `_()` function
wraps user-facing strings for translation.
Projects can extend translations by setting `DEVX_TRANSLATIONS_PATH` to a
@@ -122,7 +123,9 @@ exponential backoff (2s, 4s, 8s).
- Labels (list, create, add to issues)
- Issues (create, list)
- Pull requests (get commits, merge, create review)
- Releases (list)
- Releases (list, create idempotent)
- Actions (list runs, list jobs, get job logs)
- Actions variables (get, set idempotent)
- Wiki pages (list, fetch, create, update, delete)
**`VikunjaClient`** — Vikunja REST API wrapper:
@@ -206,7 +209,7 @@ a layered rule system configured in `pyproject.toml` under
4. **Default**: user-facing (safe default — any unknown file triggers release)
Also supports custom tags (orthogonal to release impact) for CI conditional
execution (e.g., `ansible` tag to trigger molecule tests).
execution (for example, `ansible` tag to trigger molecule tests).
### `pr_review.py`
@@ -245,7 +248,7 @@ for retrying on git push failures.
Creates a Gitea issue when a CI workflow fails. Uses the `tea` CLI for issue
creation with failure labels. Supports `--auto-login` to configure the tea
CLI login profile from `REPO_TOKEN` and `DEVX_GITEA_API_URL` before creating
CLI login profile from `CI_GITEA_TOKEN` and `DEVX_GITEA_API_URL` before creating
the issue.
### `post_merge.py`
@@ -309,7 +312,7 @@ from `devx.api_clients`, `devx.config`, and `devx.gitea_cli`.
### `setup.py`
Project setup: installs Python dependencies (editable mode with extras),
Ansible Galaxy collections (if `ansible/requirements.yml` exists), pre-commit
Ansible Galaxy collections (if `ansible/requirements.yml` exists in the target repo), pre-commit
hooks (pre-commit, commit-msg, pre-push), and configures the `tea` CLI login
profile from `.env`. Supports `--extras` to specify dependency groups,
`--no-pre-commit` to skip hook installation, and `--no-tea-login` to skip tea
@@ -329,13 +332,22 @@ total suite time must not exceed `--max-seconds` (default: 10s), and no
individual test may exceed `--max-single-seconds` (default: 0.5s, 0 to
disable). Runs `make test-unit` with `PYTEST_ADDOPTS=--durations=0`.
### `check_test_isolation.py`
Pytest plugin (auto-discovered via `pytest11` entry point) that
statically analyzes test files for un-hermetic patterns causing slow
or flaky tests: unpatched `subprocess.run`/`time.sleep` calls, known
subprocess-spawning helpers called without `@patch`, and excessive
loop iterations (>100). Also available as a standalone CLI for CI
gates and pre-commit hooks. See ADR-0001 for design rationale.
### `configure_repo.py`
Configures repository branch protection and labels via the Gitea REST API.
Sets up master branch protection (required status checks, block on rejected
reviews, block on outdated branch) and creates standard labels. Status check
contexts are read from `DEVX_STATUS_CHECKS` or default to
`CI / quality (pull_request)`.
`CI / validate (pull_request)`.
### `generate_badges.py`
@@ -382,7 +394,7 @@ single-role (4-part) and multi-role (5-part) pair encoding.
Runs all molecule scenarios on all supported OS platforms sequentially.
Intended for local development; CI uses the parallel matrix instead.
### `discover_runners.py`
### `molecule/discover_runners.py`
Discovers available Gitea Actions runners for molecule tests. Same logic as
`devx.ci.discover_runners` but intended for molecule-specific workflows.
@@ -432,14 +444,14 @@ v2 failures. Supports loading custom platforms from a JSON file.
3. **Tool modules** (`devx.tools.*`) may import from `devx.api_clients`,
`devx.config`, `devx.gitea_cli`
4. **Cross-module imports** within `devx.ci.*` or `devx.tools.*` are allowed
but must be documented (e.g., `release.py` imports from
but must be documented (for example, `release.py` imports from
`classify_changes.py`)
## Data flow
### PR lifecycle
```
```text
Developer creates Vikunja task (DEVX-N)
@@ -454,13 +466,14 @@ Developer pushes and creates PR (title: "DEVX-N: <vikunja task title>")
CI workflow (ci.yml) triggers:
├── quality (lint, tests, coverage, test speed, doc coverage,
translation check, dependency scan, workflow dry-run)
├── detect-changes (classify_changes.py → user-facing or workflow-only)
── if user-facing → release-dry-run (release.py --dry-run)
├── pr-review (pr_review.py → posts COMMENT or REQUEST_CHANGES)
├── validate (single job: quality + detect-changes +
release-dry-run + pr-review + pre-merge validation)
├── quality steps (lint, tests, coverage, test speed, doc coverage,
│ │ translation check, dependency scan, workflow dry-run)
── detect-changes (classify_changes.py → user-facing or workflow-only)
│ └── if user-facing → release-dry-run (release.py --dry-run)
├── pre-merge validation (check_auto_merge_ready.py)
│ └── pr-review (pr_review.py → posts COMMENT or REQUEST_CHANGES)
└── auto-merge (auto_merge.py)
├── validate PR title format
@@ -475,56 +488,60 @@ CI workflow (ci.yml) triggers:
### Post-merge flow
```
```text
Push to master (squash-merge commit: "DEVX-N <conventional commit>")
Post-merge workflow (post-merge.yml) triggers:
├── detect-type (detect_release_commit.py)
── is-release? → skip all jobs except badges
├── detect-and-configure (single job)
── configure-repo (configure_repo.py)
│ ├── detect-type (detect_release_commit.py)
│ │ └── is-release? → skip all steps except badges
│ └── validate-commit-msg (validate_commit_msg.py --branch master)
── validate-commit-msg (validate_commit_msg.py --branch master)
├── release (release.py)
│ ├── classify_changes.py → skip if workflow-only
│ ├── git-cliff → calculate next version
│ ├── update __version__ in __init__.py
│ ├── update CHANGELOG.md
│ ├── run make lint-ruff && make pytest-cov
│ ├── commit "release: vX.Y.Z [skip ci]"
── create annotated tag vX.Y.Z
└── push commit + tag to master
│ ▼
Tag push triggers publish workflow (see below)
├── sync-wiki (sync_wiki.py --strict)
└── sync docs/ to Gitea wiki with integrity check
├── badges (push_badges.py) [ALWAYS runs, even on release commits]
├── fetch latest master
── generate_badges.py → SVG files
│ ├── push to orphan badges branch
── update README.md + docs/index.md with cache-busting URLs
├── vikunja (post_merge.py)
├── extract task ID from commit message
│ ├── mark Vikunja task as done
└── post comment with merge SHA
└── configure-repo (configure_repo.py)
└── ensure branch protection and labels
── release-and-maintain (needs detect-and-configure)
├── release (release.py) [skip if release commit or workflow-only]
│ ├── classify_changes.py → skip if workflow-only
│ ├── git-cliff → calculate next version
│ ├── update __version__ in __init__.py
│ ├── update CHANGELOG.md
│ ├── run make lint-ruff && make pytest-cov
│ ├── commit "release: vX.Y.Z [skip ci]"
│ ├── create annotated tag vX.Y.Z
── push commit + tag to master
│ publish (publish.py) [if release created a tag]
├── build package (python -m build)
│ ├── publish to Gitea PyPI registry (twine upload)
│ │ OR publish to standard PyPI (if PYPI_TOKEN set)
│ OR skip publish (if --skip-build)
│ └── create Gitea release with git-cliff notes
├── sync-wiki (sync_wiki.py --strict) [skip if automated]
── sync docs/ to Gitea wiki with integrity check
── vikunja (post_merge.py) [skip if automated]
│ ├── extract task ID from commit message
├── mark Vikunja task as done
│ └── post comment with merge SHA
└── badges (push_badges.py) [ALWAYS runs, even on release commits]
├── fetch latest master
├── generate_badges.py → SVG files
├── push to orphan badges branch
└── update README.md + docs/index.md with cache-busting URLs
```
### Publish flow
```
Tag push (vX.Y.Z) triggers publish workflow (publish.yml):
```text
Within release-and-maintain job (after release step creates a tag):
├── install build, twine, git-cliff, tea
├── configure tea login
├── checkout release tag
└── publish (publish.py)
├── build package (python -m build)
@@ -536,7 +553,7 @@ Tag push (vX.Y.Z) triggers publish workflow (publish.yml):
### Badge generation flow
```
```text
push_badges.py:
├── fetch_latest_master() → git fetch + reset --hard origin/master
@@ -569,7 +586,7 @@ push_badges.py:
The `tea` Gitea CLI tool is used for Gitea API interactions where tea provides
reliable, official support. It is installed by
`python -m devx.tools.install_tools` and configured by
`python -m devx.tools.setup` (login profile from `.env` `REPO_TOKEN`).
`python -m devx.tools.setup` (login profile from `.env` `CI_GITEA_TOKEN`).
`devx.gitea_cli.TeaCLI` wraps tea with JSON output parsing. Operations that
tea does not support (wiki management, commit status, runner discovery,
+154 -116
View File
@@ -1,32 +1,29 @@
# CI/CD Workflow
devx uses Gitea Actions for CI/CD automation. Three workflows implement a
complete pipeline: pull request validation, post-merge release automation, and
tag-triggered publishing.
devx uses Gitea Actions for CI/CD automation. Two workflows implement a
complete pipeline: pull request validation and post-merge release
automation (including publishing).
## Workflow overview
```
```text
PR opened/synchronized ──► CI (ci.yml)
│ ├── quality
├── detect-changes
├── release-dry-run (if user-facing)
│ ├── pr-review
│ ├── validate (quality + detect-changes +
│ release-dry-run + pr-review +
│ pre-merge validation)
│ └── auto-merge ──► squash-merge to master
│ │
▼ ▼
Push to master ──► Post-merge (post-merge.yml)
├── detect-type
├── validate-commit-msg
├── release ──► tag vX.Y.Z
── sync-wiki │
├── badges │
├── vikunja │
└── configure-repo │
Tag push (v*) ──► Publish (publish.yml)
└── publish ──► Gitea PyPI registry + Gitea release
├── detect-and-configure (detect-type +
validate-commit-msg +
│ configure-repo)
── release-and-maintain
├── release ──► tag vX.Y.Z
├── publish ──► Gitea PyPI registry + Gitea release
├── sync-wiki
├── vikunja
└── badges (always runs)
```
## CI workflow (`ci.yml`)
@@ -35,9 +32,15 @@ Runs on pull requests (opened and synchronize) and manual dispatch.
### Jobs
#### `quality`
#### `validate`
The main quality gate. Runs on every PR:
The single validation job. Consolidates the former `quality`,
`detect-changes`, `release-dry-run`, `pr-review`, and `pre-merge-check`
jobs into one job to save checkout+setup overhead. Runs on every PR.
**Quality steps**
The main quality gate:
1. **Lint all** — ruff check, ruff format check, pyright, bandit, actionlint
(via `make lint-all`)
@@ -52,21 +55,21 @@ The main quality gate. Runs on every PR:
7. **Workflow dry-run validation**`make workflow-dryrun` via act_runner
(best-effort, skipped if act_runner is not installed)
#### `detect-changes`
**`detect-changes` step**
Classifies changes between `origin/master` and the PR head as user-facing or
workflow-only using `python -m devx.ci.classify_changes --github-output`.
Writes `user-facing-changed=true|false` to the job output for use by
downstream jobs.
downstream steps.
#### `release-dry-run`
**`release-dry-run` step**
Depends on `quality` and `detect-changes`. Only runs if user-facing changes
are detected. Runs `python -m devx.ci.release --dry-run` to validate that
the release script can calculate the next version and generate the changelog
without making changes. Non-blocking (uses `|| true`).
Only runs if the detect-changes step detected user-facing changes. Runs
`python -m devx.ci.release --dry-run` to validate that the release script
can calculate the next version and generate the changelog without making
changes. Non-blocking (uses `|| true`).
#### `pr-review`
**`pr-review` step**
Runs on every pull request. Executes `python -m devx.ci.pr_review` with the
PR number and repository. Fetches the PR diff via the Gitea API and runs
@@ -87,13 +90,26 @@ Checks performed:
7. Test coverage — source changes must include test updates
8. Commit conventions — conventional commit format on PR commits
**Pre-merge validation step**
Runs on every pull request. Executes
`python -m devx.ci.check_auto_merge_ready` with the branch name, PR title,
repository, and PR number. Validates auto-merge preconditions before the
`auto-merge` job runs:
1. **Branch name** — must contain a valid task ID (for example,
`DEVX-12-fix-foo``DEVX-12`)
2. **PR title format** — must be `{PREFIX}-N: <vikunja task title>`
3. **Vikunja task** — must exist and the title must match the PR title
4. **Branch state** — must not be behind master
#### `auto-merge`
Depends on `quality`, `detect-changes`, and `pr-review`. The final job in the
CI workflow. Runs `python -m devx.ci.auto_merge` with the branch name, PR
title, repository, and PR number:
Depends on `validate`. The final job in the CI workflow. Runs
`python -m devx.ci.auto_merge` with the branch name, PR title, repository,
and PR number:
1. **Read task ID** from branch name (e.g., `DEVX-12-fix-foo``DEVX-12`)
1. **Read task ID** from branch name (for example, `DEVX-12-fix-foo``DEVX-12`)
2. **Validate PR title format** — must be `{PREFIX}-N: <vikunja task title>`
3. **Validate PR title matches Vikunja task** — fetches the Vikunja task and
compares the title
@@ -107,8 +123,9 @@ The merge commit push to master triggers the post-merge workflow.
### Smart CI: user-facing vs workflow-only changes
Not all changes require a new release. The `detect-changes` job classifies
changes using `python -m devx.ci.classify_changes`:
Not all changes require a new release. The `detect-changes` step in the
`validate` job classifies changes using
`python -m devx.ci.classify_changes`:
**Workflow-only paths** (infrastructure — no release needed):
- `.gitea/**` — Gitea Actions workflows
@@ -137,55 +154,90 @@ Rule priority (first match wins):
## Post-merge workflow (`post-merge.yml`)
Runs on every push to master. A single workflow with conditional jobs
replaces separate workflows for release, wiki sync, badges, and Vikunja task
updates.
Runs on every push to master. Consolidated into 2 jobs (from 7) to reduce
runner overhead: `detect-and-configure` (detect-type + validate-commit-msg +
configure-repo) and `release-and-maintain` (release + publish + sync-wiki +
badges + vikunja). Individual steps within `release-and-maintain` are
conditional on the `detect-and-configure` job's outputs.
### Job dependency graph
```
detect-type ──┬── validate-commit-msg (skip if release commit)
├── release (skip if release commit)
│ │
│ ├── sync-wiki (needs release)
│ ├── badges (needs release, ALWAYS runs)
│ └── vikunja (needs release)
└── configure-repo (independent, skip if release commit)
```text
detect-and-configure
├── configure-repo (independent, skip if release commit)
├── detect-type → is-release? is-automated?
└── validate-commit-msg (skip if release commit)
release-and-maintain (needs detect-and-configure)
├── release (skip if release commit or workflow-only)
│ └── publish (if release created a tag)
├── sync-wiki (skip if automated)
├── vikunja (skip if automated)
└── badges (always runs)
```
`sync-wiki` and `vikunja` depend on `release` succeeding so that the wiki and
task tracker are only updated when the code is actually released. If release
fails, they are skipped to avoid leaving the wiki or Vikunja in an
inconsistent state.
`sync-wiki` and `vikunja` run only on non-automated commits (that is, real PR
merges) so that the wiki and task tracker are only updated when a human
change lands. They skip on release commits and automated commits.
The `badges` job uses `if: always()` with no is-release condition so it runs
on every push to master, including release commits. This ensures badges
(tests, coverage, version, etc.) are always current.
The `badges` step always runs (even on release commits) so badges (tests,
coverage, version, etc.) are always current. It runs last so it picks up
any version bump the release step created.
When `release` creates a `release: vX.Y.Z` commit, the release commit's
post-merge run still updates badges (the version badge picks up the new
version). Other jobs skip. The tag push triggers `publish.yml`.
version). Other steps skip. The `publish` step builds and publishes the
package to the Gitea PyPI registry within the same `release-and-maintain`
job (it checks out the release tag).
### Jobs
### Post-merge jobs
#### `detect-type`
#### `detect-and-configure`
The first post-merge job. Consolidates the former `detect-type`,
`validate-commit-msg`, and `configure-repo` jobs. Outputs `is-release`,
`is-automated`, and `user-facing-changed` for the `release-and-maintain`
job.
**`detect-type` step**
Checks if the latest commit is a release commit (`release: vX.Y.Z [skip ci]`)
using `python -m devx.ci.detect_release_commit`. Writes `is-release=true` or
`is-release=false` to the job output. All subsequent jobs use this to
conditionally skip for release commits.
`is-release=false` (and `is-automated`) to the job output. The
`release-and-maintain` job uses these to conditionally skip steps for
release commits.
#### `validate-commit-msg`
**`validate-commit-msg` step**
Depends on `detect-type`. Skips for release commits. Validates the latest
commit message using `python -m devx.ci.validate_commit_msg --branch master`.
On master, commits must follow `{PREFIX}-N: <conventional commit>` format
(added by auto-merge).
Skips for release/automated commits. Validates the latest commit message
using `python -m devx.ci.validate_commit_msg --branch master`. On master,
commits must follow `{PREFIX}-N: <conventional commit>` format (added by
auto-merge).
#### `release`
**`configure-repo` step**
Depends on `detect-type`. Skips for release commits. The core release
automation job. Runs `python -m devx.ci.release`:
Ensures branch protection and labels are configured using
`python -m devx.tools.configure_repo --repo <name> --owner <owner>`:
- Sets up master branch protection (required status checks, block on rejected
reviews, block on outdated branch)
- Creates standard labels
- Status check contexts read from `DEVX_STATUS_CHECKS` or default to
`CI / validate (pull_request)`
On failure, the `notify_failure` step creates a Gitea issue.
#### `release-and-maintain`
Depends on `detect-and-configure`. The second post-merge job. Consolidates
the former `release`, `publish`, `sync-wiki`, `badges`, and `vikunja` jobs.
Individual steps are conditional on the `detect-and-configure` job's outputs.
**`release` step**
Skips for release commits and workflow-only changes. The core release
automation step. Runs `python -m devx.ci.release`:
1. **Classify changes** — calls `classify_changes.py` to check for user-facing
changes. If only infrastructure files changed, exits without releasing.
@@ -205,7 +257,7 @@ automation job. Runs `python -m devx.ci.release`:
8. **Push** — pushes both the commit and tag to master
The script is idempotent: if there are no new conventional commits since the
last tag, it exits without doing anything. If the tag already exists (e.g.,
last tag, it exits without doing anything. If the tag already exists (for example,
from a partial previous run), it skips tag creation and only pushes.
**Tag consistency**: Before releasing, the script fetches remote tags and
@@ -225,11 +277,10 @@ tag/version/commit alignment.
On failure, the `notify_failure` step creates a Gitea issue via
`python -m devx.ci.notify_failure`.
#### `sync-wiki`
**`sync-wiki` step**
Depends on `detect-type` and `release`. Skips for release commits. Syncs
documentation from `docs/` to the Gitea wiki using
`python -m devx.ci.sync_wiki --repo <owner/repo> --strict`:
Skips for automated commits. Syncs documentation from `docs/` to the Gitea
wiki using `python -m devx.ci.sync_wiki --repo <owner/repo> --strict`:
1. Reads `docs/mapping.json` to map file paths to wiki page titles
2. Lists existing wiki pages via the Gitea API
@@ -243,15 +294,14 @@ deleted).
On failure, the `notify_failure` step creates a Gitea issue.
#### `badges`
**`badges` step**
Depends on `detect-type` and `release`. Uses `if: always()` so it runs on
every push to master, including release commits. Generates and pushes quality
badges using `python -m devx.ci.push_badges`:
Always runs (even on release commits). Generates and pushes quality badges
using `python -m devx.ci.push_badges`:
1. **Fetch latest master** — `git fetch origin master && git reset --hard
origin/master` (ensures the version badge reflects the current state,
even if the release job just pushed a new version)
even if the release step recently pushed a new version)
2. **Generate badges** — calls `devx.tools.generate_badges` which runs
pytest-cov, doc-coverage, lint checks, and version extraction, then writes
SVG files: `coverage.svg`, `tests.svg`, `docs.svg`, `quality.svg`,
@@ -268,11 +318,10 @@ and waits 10s between attempts).
On failure, the `notify_failure` step creates a Gitea issue.
#### `vikunja`
**`vikunja` step**
Depends on `detect-type` and `release`. Skips for release commits. Updates
the Vikunja task after a merge using `python -m devx.ci.post_merge --git-sha
<sha>`:
Skips for automated commits. Updates the Vikunja task after a merge using
`python -m devx.ci.post_merge --git-sha <sha>`:
1. Extracts the task ID from the first line of the commit message
2. Marks the corresponding Vikunja task as done
@@ -280,32 +329,17 @@ the Vikunja task after a merge using `python -m devx.ci.post_merge --git-sha
On failure, the `notify_failure` step creates a Gitea issue.
#### `configure-repo`
**`publish` step**
Depends on `detect-type`. Skips for release commits. Ensures branch
protection and labels are configured using
`python -m devx.tools.configure_repo --repo <name> --owner <owner>`:
- Sets up master branch protection (required status checks, block on rejected
reviews, block on outdated branch)
- Creates standard labels
- Status check contexts read from `DEVX_STATUS_CHECKS` or default to
`CI / quality (pull_request)`
On failure, the `notify_failure` step creates a Gitea issue.
## Publish workflow (`publish.yml`)
Runs on tag pushes matching `v*`. Triggered by the `release` job in the
post-merge workflow when it creates and pushes a new version tag.
### Job: `publish`
Only runs if the `release` step created a tag. Builds and publishes the
package within the same `release-and-maintain` job (checks out the release
tag). Runs `python -m devx.ci.publish <tag> <owner/repo>`:
1. **Install dependencies** — build, twine, requests, python-dotenv, click,
and the project itself
2. **Install CI tools** — git-cliff and tea via
`python -m devx.tools.install_tools`
3. **Configure tea login**`tea login add` using `REPO_TOKEN`
3. **Configure tea login**`tea login add` using `CI_GITEA_TOKEN`
4. **Build and publish**`python -m devx.ci.publish <tag> <owner/repo>`:
- Build the package with `python -m build`
- Publish to the Gitea PyPI registry (default) using `twine upload
@@ -329,7 +363,7 @@ On failure, the `notify_failure` step creates a Gitea issue.
### `auto_merge.py`
Auto-merge PR when all CI checks pass. Reads task ID from the branch name
(e.g., `DEVX-12-fix-foo``DEVX-12`). Validates PR title format, checks the
(for example, `DEVX-12-fix-foo``DEVX-12`). Validates PR title format, checks the
Vikunja task exists and the title matches, extracts the conventional commit
message from PR commits, and squash-merges with
`{PREFIX}-N <conventional commit>` title.
@@ -377,7 +411,7 @@ python -m devx.ci.pr_review <pr_number> <owner/repo>
Creates a Gitea issue when a CI workflow fails. Uses the tea CLI for issue
creation with failure labels. Supports `--auto-login` to configure the tea
CLI login profile from `REPO_TOKEN`.
CLI login profile from `CI_GITEA_TOKEN`.
```bash
python -m devx.ci.notify_failure --repo <owner/repo> --run-id <id> \
@@ -518,25 +552,29 @@ The complete release process from PR to published package:
1. **PR merged**`auto-merge` squash-merges the PR to master with
`{PREFIX}-N <conventional commit>` title
2. **Post-merge triggers** — the merge push triggers `post-merge.yml`
3. **detect-type** — confirms the commit is not a release commit
4. **release**`release.py` calculates the next version, updates files,
runs tests, commits `release: vX.Y.Z [skip ci]`, creates tag `vX.Y.Z`,
and pushes to master
5. **Tag push triggers publish** — the tag push triggers `publish.yml`
6. **publish**`publish.py` builds the package, publishes to the Gitea PyPI
registry, and creates a Gitea release with git-cliff notes
7. **sync-wiki** — documentation is synced to the Gitea wiki
8. **badges** — quality badges are regenerated and pushed to the `badges`
branch; README and docs/index.md are updated with cache-busting URLs
9. **vikunja** — the corresponding Vikunja task is marked as done
10. **configure-repo** — branch protection and labels are ensured
3. **detect-and-configure** — detects release commit, validates commit
message, and ensures branch protection/labels
4. **release** (step in `release-and-maintain`) — `release.py` calculates
the next version, updates files, runs tests, commits
`release: vX.Y.Z [skip ci]`, creates tag `vX.Y.Z`, and pushes to master
5. **publish** (step in `release-and-maintain`) `publish.py` builds the
package, publishes to the Gitea PyPI registry, and creates a Gitea
release with git-cliff notes (checks out the release tag within the
same job)
6. **sync-wiki** (step in `release-and-maintain`) — documentation is synced
to the Gitea wiki
7. **vikunja** (step in `release-and-maintain`) — the corresponding Vikunja
task is marked as done
8. **badges** (step in `release-and-maintain`) — quality badges are
regenerated and pushed to the `badges` branch; README and docs/index.md
are updated with cache-busting URLs
The release commit's post-merge run skips all jobs except `badges` (which
The release commit's post-merge run skips all steps except `badges` (which
picks up the new version number). This prevents infinite loops.
## Failure handling
Every job in the post-merge and publish workflows has a `notify_failure` step
Every job in the CI and post-merge workflows has a `notify_failure` step
that runs `if: failure()`. This creates a Gitea issue with the workflow name,
run ID, and commit SHA, ensuring failures that would otherwise go unnoticed
in the Actions tab are surfaced as issues. The issue is created via the tea
+95 -5
View File
@@ -127,14 +127,37 @@ Click commands from `cli.py` and checks if each has documentation in
```bash
devx ci doc-coverage
devx ci doc-coverage --docs-dir docs/ --fail-on-missing
devx ci doc-coverage --docs-dir docs/ --source-dir src/ --fail-on-missing
```
Options:
- `--docs-dir <dir>` — path to the docs directory (default: `docs/`)
- `--source-dir <dir>` — path to the source directory (default: auto-detect)
- `--fail-on-missing` — exit with non-zero status if any documentation is
missing
### `devx ci lint-docs`
Lint documentation files for structure, broken links, heading hierarchy,
duplicate headings, TODO/FIXME markers, and trailing whitespace.
```bash
devx ci lint-docs
devx ci lint-docs --root . --fix
devx ci lint-docs --no-check-links --no-check-stale
```
Options:
- `--root <dir>` — repository root directory (default: `.`)
- `--docs-dir <dir>` — docs directory (default: `<root>/docs`)
- `--check-links/--no-check-links` — check internal links (default: yes)
- `--check-headings/--no-check-headings` — check heading hierarchy (default: yes)
- `--check-todo/--no-check-todo` — check for TODO/FIXME markers (default: yes)
- `--check-stale/--no-check-stale` — check for stale docs (default: no)
- `--check-trailing/--no-check-trailing` — check trailing whitespace (default: yes)
- `--check-duplicates/--no-check-duplicates` — check duplicate headings (default: yes)
- `--fix` — auto-fix trailing whitespace
### `devx ci integration-guard`
Run pytest with cross-runner failure detection. If any
@@ -148,7 +171,7 @@ devx ci integration-guard -- -x -v --tb=short test_a.py
Environment variables:
- `GITEA_URL` — base URL of the Gitea instance
- `REPO_TOKEN` — API token with repo access
- `CI_GITEA_TOKEN` — API token with repo access
- `RUN_ID` — workflow run ID (`GITHUB_RUN_ID`)
- `JOB_NAME` — base job name (`GITHUB_JOB`)
- `MATRIX_INDEX` — current matrix index (runner-index)
@@ -171,7 +194,7 @@ Options:
- `--run-id <id>` — CI run ID (required)
- `--workflow <name>` — workflow name (required)
- `--commit <sha>` — commit SHA (required)
- `--auto-login` — configure tea CLI login from `REPO_TOKEN` before creating
- `--auto-login` — configure tea CLI login from `CI_GITEA_TOKEN` before creating
the issue
### `devx ci post-merge`
@@ -311,6 +334,44 @@ devx tools check-test-speed --max-seconds 10
devx tools check-test-speed --max-seconds 4 --max-single-seconds 0.5
```
### `devx tools check-test-isolation`
Statically analyze test files for un-hermetic patterns that cause slow
or flaky tests. Also available as a **pytest plugin** (auto-discovered
via the `pytest11` entry point when devx is installed — runs
automatically on every `pytest` invocation and **fails on violations**).
Detected patterns (hard errors — exit non-zero):
- **unpatched-subprocess**: `subprocess.run/call/Popen/check_call/check_output`
called in a test function without `@patch` or `with patch(...)`
- **unpatched-sleep**: `time.sleep` called without `@patch`
- **unpatched-helper**: known subprocess-spawning helpers (`update_doc_versions`,
`run_cmd`, `run_tests`) called without `@patch` or patching their internal deps
- **excessive-iterations**: `for _ in range(N)` where N > 100
- **heavy-module-import**: `httpx`, `ansible`, etc. imported at module level
- **reload-without-cleanup**: `importlib.reload()` called an odd number of times
Advisory patterns (exit 0 — runtime audit is authoritative):
- **transitive-subprocess**: `CliRunner.invoke(target)` where `target`
transitively calls `subprocess.run` without being patched. Detected via
static call-graph analysis. The runtime subprocess audit catches actual
leaks — if a real subprocess runs without `@patch`, the test fails.
```bash
devx tools check-test-isolation
devx tools check-test-isolation --test-path tests/
devx tools check-test-isolation --categories unpatched-subprocess,transitive-subprocess
devx tools check-test-isolation --max-loop-iterations 50
devx tools check-test-isolation --src-dir src/
```
Pytest plugin options (automatic when devx is installed):
- `--no-test-isolation` — disable static analysis and runtime subprocess audit
- `--test-isolation-max-loop N` — max iterations per loop (default: 100)
### `devx tools configure-repo`
Configure repository: branch protection and labels via the Gitea REST API.
@@ -382,7 +443,7 @@ devx tools install-tools --list # list status
### `devx tools setup`
Project setup: install Python dependencies (editable mode with extras),
Ansible Galaxy collections (if `ansible/requirements.yml` exists), pre-commit
Ansible Galaxy collections (if `ansible/requirements.yml` exists in the target repo), pre-commit
hooks (pre-commit, commit-msg, pre-push), and configure the tea CLI login
profile from `.env`.
@@ -398,6 +459,35 @@ Options:
- `--no-pre-commit` — skip pre-commit hook installation
- `--no-tea-login` — skip tea CLI login configuration
### `devx tools rebase`
Rebase the current branch onto `origin/master` and force-push with
`--force-with-lease`. Checks if the branch is behind master first —
if up-to-date, exits without doing anything.
```bash
devx tools rebase # rebase + force-push
devx tools rebase -- --no-push # rebase locally only
```
Options (pass after `--`):
- `--no-push` — rebase locally without pushing
### `devx tools pr-rebase`
Rebase a pull request's head branch onto master via the Gitea API
(server-side). This triggers a new `pull_request synchronize` event,
which starts a new CI run. Useful when you don't have the branch
checked out locally.
```bash
devx tools pr-rebase -- --pr 42 # rebase PR #42
devx tools pr-rebase # auto-detect PR from current branch
```
Options (pass after `--`):
- `--pr <N>` — PR number (auto-detected from current branch if omitted)
## Molecule Commands
Molecule commands require the `molecule` extra (`pip install devx[molecule]`).
@@ -462,7 +552,7 @@ Options:
Environment variables:
- `GITEA_URL` — base URL of the Gitea instance
- `REPO_TOKEN` — API token with repo access
- `CI_GITEA_TOKEN` — API token with repo access
- `RUN_ID` — workflow run ID (`GITHUB_RUN_ID`)
- `JOB_NAME` — base job name (`GITHUB_JOB`)
- `MATRIX_INDEX` — current matrix index (runner-index)
+161
View File
@@ -0,0 +1,161 @@
# Getting Started with devx
This guide walks you through installing devx, configuring it for your project,
and setting up a complete CI/CD pipeline.
## Prerequisites
- **Python 3.12+**
- **A Gitea instance** with Actions enabled
- **A Gitea API token** with repo, workflow, and organization scopes
- **(Optional) Vikunja API token** for task tracking integration
## Installation
devx is published to the Gitea PyPI registry. Configure pip to use it:
```bash
# Configure Gitea PyPI registry
pip config set global.extra-index-url https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple
# Install devx
pip install devx
```
Or install from source:
```bash
git clone https://git.oblachno.oblachno.fyi/oblachno-oss/devx.git
cd devx
make setup
```
## Quick Start
### 1. Configure environment variables
Create a `.env` file in your project root:
```bash
CI_GITEA_TOKEN=your_gitea_api_token
VIKUNJA_TOKEN=your_vikunja_api_token # optional
```
### 2. Add devx to your project
Add devx to your `pyproject.toml`:
```toml
[project]
dependencies = [
"devx>=0.44.0",
]
[project.optional-dependencies]
dev = [
"devx>=0.44.0",
]
```
### 3. Set up the Makefile
devx provides a shared Makefile fragment. Add this to your `Makefile`:
```makefile
include devx.mak
```
Run `devx tools setup` to install all development tools (actionlint, git-cliff,
tea CLI, etc.) and configure pre-commit hooks.
### 4. Create the docs structure
devx expects a `docs/` directory with at minimum:
```text
docs/
├── index.md # Documentation home page
├── mapping.json # Wiki page title mappings
├── user/ # User-facing documentation
│ └── cli-commands.md
└── tech/ # Technical documentation
├── architecture.md
└── ci-cd-workflow.md
```
Example `docs/mapping.json`:
```json
{
"index.md": "Home",
"user/cli-commands.md": "CLI-Commands",
"tech/architecture.md": "Architecture",
"tech/ci-cd-workflow.md": "CI-CD-Workflow"
}
```
### 5. Set up CI workflows
Create `.gitea/workflows/ci.yml` and `.gitea/workflows/post-merge.yml` in your
project. See the [CI/CD Workflow guide](../tech/ci-cd-workflow.md) for details.
### 6. Configure release settings
Add a `cliff.toml` for git-cliff-based versioning:
```bash
devx tools generate-cliff-config
```
Add `[tool.devx]` section to `pyproject.toml` for project-specific config:
```toml
[tool.devx]
# Vikunja project ID for task tracking
vikunja_project_id = 6
[tool.devx.classify]
# File patterns that are infrastructure (no release needed)
infrastructure = [
".gitea/**",
"docs/**",
"tests/**",
"AGENTS.md",
"README.md",
"CHANGELOG.md",
]
```
## Available Tools
### CI/CD Automation (`devx.ci.*`)
- `devx.ci.release` — Automated semver versioning and tagging
- `devx.ci.publish` — Package publishing to Gitea PyPI registry
- `devx.ci.auto_merge` — Squash-merge automation with task ID validation
- `devx.ci.pr_review` — Automated PR review with inline comments
- `devx.ci.classify_changes` — User-facing vs workflow-only change detection
- `devx.ci.sync_wiki` — Push docs/ to Gitea wiki
- `devx.ci.doc_coverage` — Documentation coverage checker
- `devx.ci.lint_docs` — Documentation linter (structure, links, headings)
- `devx.ci.check_translations` — i18n translation completeness checker
- `devx.ci.notify_failure` — Create Gitea issues on CI failures
- `devx.ci.distribute_files` — Parallel test file distribution
- `devx.ci.distribute_items` — Parallel item distribution across runners
- `devx.ci.discover_runners` — Dynamic runner discovery via Gitea API
### Development Tools (`devx.tools.*`)
- `devx.tools.setup` — Environment setup (venv, deps, hooks, tools)
- `devx.tools.install_tools` — Install CI/CD tools (actionlint, git-cliff, tea)
- `devx.tools.create_task` — Create Vikunja tasks
- `devx.tools.create_pr` — Create Gitea PRs with task ID in title
- `devx.tools.configure_repo` — Configure branch protection and labels
- `devx.tools.generate_badges` — Generate quality badge SVGs
- `devx.tools.check_test_speed` — Enforce test execution speed limits
## Next Steps
- Read the [CLI Commands reference](cli-commands.md) for all available commands
- Read the [Architecture guide](../tech/architecture.md) to understand internals
- Read the [CI/CD Workflow guide](../tech/ci-cd-workflow.md) for pipeline details
+11 -3
View File
@@ -1,7 +1,15 @@
#!/usr/bin/env bash
# pre-commit hook: fail if unit tests are too slow.
# Checks both total suite time (10s) and per-test time (0.5s).
# Aligned with CI (ci.yml uses same thresholds).
# pre-commit hook: fast local quality gates that shift-left CI checks.
# Runs test speed, translation completeness, and test isolation checks.
# All of these run in CI — failing here saves a round-trip.
set -e
export PYTHONPATH=src
# Test speed: total suite < 4s, individual tests < 0.5s
python3 -m devx.tools.check_test_speed --max-seconds 4 --max-single-seconds 0.5
# Translation completeness: missing keys, dead keys, missing languages
python3 -m devx.ci.check_translations
# Test isolation: unpatched subprocess/time.sleep in test functions
python3 -m devx.tools.check_test_isolation --test-path tests/
+81 -26
View File
@@ -13,62 +13,98 @@ classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
]
# All dependencies are pinned to exact versions for full reproducibility.
# Update pinned versions in a dedicated PR with verification.
dependencies = [
"requests>=2.34.2",
"python-dotenv>=1.2.2",
"click>=8.4.1",
"requests==2.34.2",
"python-dotenv==1.2.2",
"click==8.4.2",
"tenacity==9.1.4", # retry logic for GiteaClient/VikunjaClient
]
[project.scripts]
devx = "devx.cli:cli"
# Pytest plugin — auto-discovered by pytest when devx is installed.
# Runs static analysis on test files during every pytest invocation
# to detect un-hermetic patterns (unpatched subprocess, time.sleep, etc.)
[project.entry-points.pytest11]
devx_test_isolation = "devx.tools.check_test_isolation"
[tool.setuptools.dynamic]
version = {attr = "devx.__version__"}
[project.optional-dependencies]
# Minimal deps for CI scripts that only need click/dotenv/requests
# Test runners (pytest + coverage + parallel execution)
ci = [
"pytest>=9.1.0",
"pytest-cov>=7.1.0",
"build>=1.5.0",
"twine>=6.2.0",
"pytest==9.1.1",
"pytest-cov==7.1.0",
"pytest-xdist==3.8.0",
]
# Lint and type-checking tools (quality job)
# Lint and type-checking tools (quality job, badge generation)
lint = [
"ruff>=0.15.17",
"pyright>=1.1.410",
"bandit>=1.8.2",
"pip-audit>=2.10",
"pre-commit>=4.6.0",
"ruff==0.15.21",
"pyright==1.1.411",
"bandit==1.9.4",
"pip-audit==2.10.1",
"pre-commit==4.6.0",
]
# Molecule testing (optional — for projects with Ansible roles)
# Release tools (build + publish to PyPI/Gitea registry)
release = [
"build==1.5.1",
"twine==6.2.0",
]
# Molecule testing (for projects with Ansible roles)
molecule = [
"molecule>=26.4.0",
"molecule-docker>=2.1.0",
"ansible-lint>=26.4.0",
"ansible>=14.0.0",
"molecule==26.6.0",
"molecule-docker==2.1.0",
"ansible-lint==26.6.0",
"ansible-core==2.21.1",
]
# Deploy tools (for infra staging/production deployments)
deploy = [
"ansible-core==2.21.1",
"boto3==1.43.37",
"docker==7.1.0",
"jinja2==3.1.6",
"pyyaml==6.0.3",
"cryptography==49.0.0",
]
# Full dev environment (local development)
dev = [
"devx[ci,lint]",
"build>=1.3.0",
"twine>=6.2.0",
"devx[ci,lint,release,molecule]",
"build==1.5.1",
"twine==6.2.0",
]
[tool.setuptools.packages.find]
where = ["src"]
[tool.setuptools.package-data]
devx = ["translations.json"]
devx = ["translations.json", "make/*.mak"]
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]
addopts = "--cov=src/devx --cov-report=term-missing --cov-fail-under=100"
addopts = "--cov=src/devx --cov-report=term-missing --cov-fail-under=100 -p no:devx_test_isolation"
markers = [
"integration: marks tests as integration tests (not counted in coverage)",
]
[tool.coverage.run]
# The test isolation pytest plugin (check_test_isolation.py) is loaded
# by pytest before coverage instrumentation starts. Coverage config below
# excludes decorator lines and pragma-marked code from the coverage check.
branch = false
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"if __name__ == .__main__",
# Click decorator lines are executed at import time, before coverage
"@click\\.command|@click\\.option|@click\\.argument",
]
[tool.ruff]
target-version = "py312"
line-length = 120
@@ -84,6 +120,8 @@ indent-style = "space"
[tool.pyright]
include = ["src"]
pythonVersion = "3.12"
venvPath = "."
venv = ".venv"
strict = ["src/devx/config.py", "src/devx/exceptions.py", "src/devx/i18n.py", "src/devx/api_clients.py", "src/devx/gitea_cli.py"]
# ---------------------------------------------------------------------------
@@ -96,6 +134,19 @@ strict = ["src/devx/config.py", "src/devx/exceptions.py", "src/devx/i18n.py", "s
# Rule priority (first match wins):
# 1. user_facing_overrides (safety — highest priority)
# 2. infrastructure_overrides (explicit per-file)
# Project-specific devx configuration (read by devx.config)
[tool.devx]
task_prefix = "DEVX"
vikunja_project_id = 8
repo_owner = "oblachno-oss"
repo_name = "devx"
[tool.devx.check_agent_docs]
skip_ref_prefixes = [
"src/myproject/",
"ansible/requirements.yml",
]
# 3. infrastructure (DEFAULT_INFRASTRUCTURE + project-specific patterns)
# 4. Default: user-facing (safe)
[tool.devx.classify]
@@ -119,8 +170,12 @@ infrastructure_overrides = [
]
# User-facing overrides — safety override for broad infrastructure patterns
# (empty — add when an infrastructure pattern is too broad)
user_facing_overrides = []
# devx workflow files (.gitea/**) are reference implementations that
# downstream repos (grm, infra) copy from. Changes to them affect how
# consumer projects run their CI, so they must trigger a release.
user_facing_overrides = [
".gitea/**",
]
# Tag patterns — additional categories for CI conditional execution
# Orthogonal to release impact (user-facing vs infrastructure)
+1 -1
View File
@@ -1,3 +1,3 @@
"""devx — reusable development and CI/CD tools for oblachno-oss projects."""
__version__ = "0.13.0"
__version__ = "0.44.0"
+213 -93
View File
@@ -4,10 +4,16 @@ from __future__ import annotations
import json
import logging
import time
from typing import Any
import requests
from tenacity import (
before_sleep_log,
retry,
retry_if_exception_type,
stop_after_attempt,
wait_exponential,
)
from devx.config import DEFAULT_TIMEOUT, MAX_RETRIES, RETRY_BACKOFF_BASE, RETRY_STATUS_CODES
from devx.exceptions import APIError
@@ -27,14 +33,69 @@ def _parse_error(e: requests.HTTPError) -> tuple[int, str]:
return status, message
def _is_retryable(e: Exception) -> bool:
"""Check if an exception is a transient error worth retrying."""
if isinstance(e, requests.ConnectionError):
return True
if isinstance(e, requests.HTTPError):
status, _ = _parse_error(e)
return status in RETRY_STATUS_CODES
return isinstance(e, requests.Timeout)
class _TransientHTTPError(requests.HTTPError):
"""HTTP error with a retryable status code (wrapped for tenacity)."""
class _RetryableRequestError(Exception):
"""Connection/timeout error wrapped for tenacity retry."""
def _execute_request(
session: requests.Session,
method: str,
url: str,
**kwargs: Any,
) -> requests.Response:
"""Execute a single HTTP request, wrapping transient errors for tenacity.
Non-retryable HTTP errors (4xx except 429) raise :class:`APIError` directly.
Retryable errors (429, 5xx, connection, timeout) raise exceptions that
tenacity will retry.
"""
try:
response = session.request(method, url, timeout=DEFAULT_TIMEOUT, **kwargs)
response.raise_for_status()
return response
except requests.HTTPError as e:
status, message = _parse_error(e)
if status in RETRY_STATUS_CODES:
# Wrap in _TransientHTTPError so tenacity retries it
raise _TransientHTTPError(message, response=e.response) from e
raise APIError(status, message) from e
except (requests.ConnectionError, requests.Timeout) as e:
raise _RetryableRequestError(str(e)) from e
# Tenacity retry decorator shared by both clients.
# Retries on transient HTTP errors (429, 5xx) and connection/timeout errors.
_retry_decorator = retry(
stop=stop_after_attempt(MAX_RETRIES),
wait=wait_exponential(multiplier=RETRY_BACKOFF_BASE, min=RETRY_BACKOFF_BASE, max=RETRY_BACKOFF_BASE**MAX_RETRIES),
retry=retry_if_exception_type((_TransientHTTPError, _RetryableRequestError)),
before_sleep=before_sleep_log(logger, logging.WARNING),
reraise=True,
)
def _request_with_retry(
session: requests.Session,
url: str,
method: str,
**kwargs: Any,
) -> requests.Response:
"""Execute an HTTP request with tenacity-managed retry logic.
On exhaustion, the last exception is translated to :class:`APIError`.
"""
try:
return _retry_decorator(_execute_request)(session, method, url, **kwargs)
except _TransientHTTPError as e:
response = getattr(e, "response", None)
status = response.status_code if response is not None else 0
raise APIError(status, str(e)) from e
except _RetryableRequestError as e:
raise APIError(0, str(e)) from e
class GiteaClient:
@@ -56,49 +117,7 @@ class GiteaClient:
return f"{self._base_url}/repos/{self._owner}/{self._repo}{path}"
def _request(self, method: str, path: str, **kwargs: Any) -> requests.Response:
url = self._url(path)
last_exc: Exception | None = None
for attempt in range(MAX_RETRIES):
try:
response = self._session.request(method, url, timeout=DEFAULT_TIMEOUT, **kwargs)
response.raise_for_status()
return response
except requests.HTTPError as e:
status, message = _parse_error(e)
if _is_retryable(e) and attempt < MAX_RETRIES - 1:
wait = RETRY_BACKOFF_BASE ** (attempt + 1)
logger.warning(
"Transient HTTP %d on %s %s, retrying in %ds (attempt %d/%d)",
status,
method,
path,
wait,
attempt + 1,
MAX_RETRIES,
)
time.sleep(wait)
last_exc = e
continue
raise APIError(status, message) from e
except (requests.ConnectionError, requests.Timeout) as e:
if attempt < MAX_RETRIES - 1:
wait = RETRY_BACKOFF_BASE ** (attempt + 1)
logger.warning(
"Connection error on %s %s, retrying in %ds (attempt %d/%d)",
method,
path,
wait,
attempt + 1,
MAX_RETRIES,
)
time.sleep(wait)
last_exc = e
continue
raise APIError(0, str(e)) from e
# Should not reach here, but just in case
if last_exc: # pragma: no cover
raise APIError(0, str(last_exc)) from last_exc
raise APIError(0, "Max retries exceeded") # pragma: no cover
return _request_with_retry(self._session, self._url(path), method, **kwargs)
# -- repo settings --
@@ -175,6 +194,19 @@ class GiteaClient:
payload = {"Do": "squash", "MergeTitleField": merge_title}
self._request("POST", f"/pulls/{pr_number}/merge", json=payload)
def update_pr_branch(self, pr_number: str | int, style: str = "rebase") -> None:
"""Update PR head branch by merging/rebasing the base branch into it.
Uses the Gitea API ``POST /pulls/{index}/update?style=rebase`` endpoint.
This rebases the PR's head branch onto the latest base branch server-side,
triggering a ``pull_request synchronize`` event that starts a new CI run.
Args:
pr_number: PR number.
style: Update method ``"rebase"`` (default) or ``"merge"``.
"""
self._request("POST", f"/pulls/{pr_number}/update", params={"style": style})
def get_commit_status(self, sha: str) -> list[dict[str, Any]]:
"""Fetch all status check contexts reported for a commit.
@@ -192,6 +224,31 @@ class GiteaClient:
r = self._request("GET", f"/pulls/{pr_number}")
return r.json()
def update_pr(self, pr_number: str | int, fields: dict[str, Any]) -> dict[str, Any]:
"""Update a pull request (e.g. title, body, state).
Args:
pr_number: PR number.
fields: Dict of fields to update (e.g. {"title": "new title"}).
"""
r = self._request("PATCH", f"/pulls/{pr_number}", json=fields)
return r.json()
def create_pr(self, title: str, head: str, base: str = "master", body: str = "") -> dict[str, Any]:
"""Create a pull request and return the PR dict.
Args:
title: PR title.
head: Head branch name.
base: Base branch name (default: master).
body: PR description (markdown).
"""
payload: dict[str, Any] = {"title": title, "head": head, "base": base}
if body:
payload["body"] = body
r = self._request("POST", "/pulls", json=payload)
return r.json()
def list_prs(self, state: str = "all", **params: Any) -> list[dict[str, Any]]:
"""List pull requests, optionally filtered by state.
@@ -208,6 +265,20 @@ class GiteaClient:
r = self._request("GET", f"/pulls/{pr_number}/files")
return r.json()
def add_pr_label(self, pr_number: str | int, label_names: list[str]) -> None:
"""Attach labels to a PR/issue by name.
Args:
pr_number: PR or issue number.
label_names: List of label names to attach.
"""
self._request("POST", f"/issues/{pr_number}/labels", json={"labels": label_names})
def get_pr_label_names(self, pr_number: str | int) -> list[str]:
"""Return label names currently attached to a PR/issue."""
r = self._request("GET", f"/issues/{pr_number}/labels")
return [label.get("name", "") for label in r.json()]
def get_pr_commits(self, pr_number: str | int) -> list[dict[str, Any]]:
"""Fetch the commits included in a pull request."""
r = self._request("GET", f"/pulls/{pr_number}/commits")
@@ -286,6 +357,61 @@ class GiteaClient:
return existing
return self.create_release(tag=tag, name=name, body=body, draft=draft, prerelease=prerelease)
# -- actions (CI/CD) --
def list_action_runs(self, **params: Any) -> dict[str, Any]:
"""List workflow runs for the repository.
Returns the raw API response dict (includes ``workflow_runs`` and
``total_count`` keys per Gitea API).
"""
r = self._request("GET", "/actions/runs", params=params)
return r.json()
def get_action_run_jobs(self, run_id: str | int) -> list[dict[str, Any]]:
"""List jobs for a specific workflow run."""
r = self._request("GET", f"/actions/runs/{run_id}/jobs")
data = r.json()
return data.get("jobs", [])
def get_action_job_logs(self, job_id: str | int) -> str:
"""Fetch logs for a specific CI job.
Returns the raw log text. Raises APIError if logs are unavailable.
"""
r = self._request("GET", f"/actions/jobs/{job_id}/logs")
return r.text
# -- actions variables (repo-level) --
def get_repo_variable(self, name: str) -> str | None:
"""Read a Gitea Actions repository variable.
Returns the variable value, or ``None`` if the variable is not set.
Raises :class:`APIError` on other HTTP errors.
"""
try:
r = self._request("GET", f"/actions/variables/{name}")
return r.json().get("value")
except APIError as e:
if e.status == 404:
return None
raise
def set_repo_variable(self, name: str, value: str) -> None:
"""Create or update a Gitea Actions repository variable (idempotent).
Tries PUT first (update); if the variable doesn't exist (404),
creates it via POST. Gitea 1.26.x does not support PATCH for
action variables.
"""
try:
self._request("PUT", f"/actions/variables/{name}", json={"value": value})
except APIError as e:
if e.status != 404:
raise
self._request("POST", f"/actions/variables/{name}", json={"value": value})
class VikunjaClient:
"""Low-level Vikunja REST API client with connection pooling."""
@@ -297,47 +423,7 @@ class VikunjaClient:
def _request(self, method: str, path: str, **kwargs: Any) -> requests.Response:
url = f"{self._base_url}{path}"
last_exc: Exception | None = None
for attempt in range(MAX_RETRIES):
try:
response = self._session.request(method, url, timeout=DEFAULT_TIMEOUT, **kwargs)
response.raise_for_status()
return response
except requests.HTTPError as e:
status, message = _parse_error(e)
if _is_retryable(e) and attempt < MAX_RETRIES - 1:
wait = RETRY_BACKOFF_BASE ** (attempt + 1)
logger.warning(
"Transient HTTP %d on %s %s, retrying in %ds (attempt %d/%d)",
status,
method,
path,
wait,
attempt + 1,
MAX_RETRIES,
)
time.sleep(wait)
last_exc = e
continue
raise APIError(status, message) from e
except (requests.ConnectionError, requests.Timeout) as e:
if attempt < MAX_RETRIES - 1:
wait = RETRY_BACKOFF_BASE ** (attempt + 1)
logger.warning(
"Connection error on %s %s, retrying in %ds (attempt %d/%d)",
method,
path,
wait,
attempt + 1,
MAX_RETRIES,
)
time.sleep(wait)
last_exc = e
continue
raise APIError(0, str(e)) from e
if last_exc: # pragma: no cover
raise APIError(0, str(last_exc)) from last_exc
raise APIError(0, "Max retries exceeded") # pragma: no cover
return _request_with_retry(self._session, url, method, **kwargs)
def list_tasks(self, **params: Any) -> list[dict[str, Any]]:
r = self._request("GET", "/tasks", params=params)
@@ -353,6 +439,40 @@ class VikunjaClient:
r = self._request("GET", f"/projects/{project_id}/tasks", params=params)
return r.json()
def find_task_by_identifier(self, project_id: int, identifier: str, per_page: int = 50) -> dict[str, Any] | None:
"""Find a task by its identifier (e.g. ``DEVX-42``) in a project.
Paginates through all tasks in the project. Returns the task dict
or None if not found.
"""
page = 1
while True:
tasks = self.list_project_tasks(project_id, page=page, per_page=per_page)
if not tasks:
break
for t in tasks:
if t.get("identifier") == identifier:
return t
if len(tasks) < per_page:
break
page += 1
return None
def create_task(self, project_id: int, title: str, description: str = "") -> dict[str, Any]:
"""Create a task in a project and return the created task dict.
Args:
project_id: Target Vikunja project ID.
title: Task title (required, non-empty).
description: Task description (HTML supported, optional).
"""
r = self._request(
"PUT",
f"/projects/{project_id}/tasks",
json={"title": title, "description": description},
)
return r.json()
def post_comment(self, task_id: int, comment: str) -> None:
self._request("PUT", f"/tasks/{task_id}/comments", json={"comment": comment})
+100
View File
@@ -2,8 +2,14 @@
from __future__ import annotations
import os
import subprocess # nosec B404
import click
from devx.config import TASK_ID_RE
from devx.i18n import _
def get_latest_tag() -> str:
"""Get the latest git tag, or empty string if none exists."""
@@ -16,3 +22,97 @@ def get_latest_tag() -> str:
if result.returncode != 0:
return ""
return result.stdout.strip()
def run_cmd(
args: list[str],
check: bool = True,
capture: bool = True,
) -> subprocess.CompletedProcess[str]:
"""Run a command and return the completed process.
Args:
args: Command and arguments as a list.
check: If True, raise :class:`click.ClickException` on non-zero exit.
capture: If True, capture stdout/stderr. If False, inherit parent's.
"""
result = subprocess.run( # nosec B603
args,
capture_output=capture,
text=True,
check=False,
)
if check and result.returncode != 0:
raise click.ClickException(
_(
"Command failed ({cmd}): {stderr}",
cmd=" ".join(args),
stderr=result.stderr.strip() if result.stderr else result.stdout.strip(),
)
)
return result
def extract_task_id(text: str) -> str:
"""Extract the ``{PREFIX}-N`` task identifier from *text*.
Returns the matched string (e.g. ``DEVX-42``) or an empty string if
no task ID is found.
"""
match = TASK_ID_RE.search(text)
return match.group(0) if match else ""
def write_github_env(key: str, value: str) -> None:
"""Append a key=value line to the ``$GITHUB_ENV`` file.
Multi-line values use the heredoc syntax required by Gitea Actions.
Raises :class:`click.ClickException` if ``GITHUB_ENV`` is not set.
"""
gh_env = os.environ.get("GITHUB_ENV")
if not gh_env:
raise click.ClickException("GITHUB_ENV environment variable is not set")
with open(gh_env, "a", encoding="utf-8") as f: # noqa: PTH123
if "\n" in value:
delimiter = "EOF"
f.write(f"{key}<<{delimiter}\n{value}\n{delimiter}\n")
else:
f.write(f"{key}={value}\n")
def write_github_output(key: str, value: str) -> None:
"""Append a key=value line to the ``$GITHUB_OUTPUT`` file.
Raises :class:`click.ClickException` if ``GITHUB_OUTPUT`` is not set.
"""
gh_output = os.environ.get("GITHUB_OUTPUT")
if not gh_output:
raise click.ClickException("GITHUB_OUTPUT environment variable is not set")
with open(gh_output, "a", encoding="utf-8") as f: # noqa: PTH123
f.write(f"{key}={value}\n")
def lpt_distribute[T](items: list[T], weights: list[int], max_runners: int) -> list[list[T]]:
"""Distribute *items* across *max_runners* using LPT scheduling.
Sorts items by weight (descending), then assigns each to the runner
with the least total weight. This produces a more balanced distribution
than naive round-robin when items have varying costs.
Args:
items: Items to distribute.
weights: Parallel list of integer weights (higher = heavier).
max_runners: Number of runner groups to create.
Returns:
A list of ``max_runners`` lists, each containing the items assigned
to that runner.
"""
groups: list[list[T]] = [[] for _ in range(max_runners)]
loads = [0] * max_runners
indexed = sorted(enumerate(items), key=lambda x: (-weights[x[0]], x[0]))
for orig_idx, item in indexed:
min_runner = min(range(max_runners), key=lambda r: loads[r])
groups[min_runner].append(item)
loads[min_runner] += weights[orig_idx]
return groups
+66 -43
View File
@@ -17,12 +17,10 @@ This allows the PR title to be a human-friendly Vikunja task title
while the squashed commit follows conventional commits.
Usage:
REPO_TOKEN=<token> python3 -m devx.ci.auto_merge <branch> <pr_title> <repo> <pr_number>
CI_GITEA_API_TOKEN=<token> VIKUNJA_TOKEN=<token> python3 -m devx.ci.auto_merge <branch> <pr_title> <repo> <pr_number>
"""
import os
import re
import subprocess # nosec B404
from pathlib import Path
from typing import Any
@@ -30,17 +28,21 @@ import click
from dotenv import load_dotenv # pyright: ignore[reportMissingImports,reportUnknownVariableType]
from devx.api_clients import GiteaClient, VikunjaClient
from devx.ci._shared import extract_task_id as _extract_task_id
from devx.config import (
CONVENTIONAL_RE,
DEFAULT_PER_PAGE,
GITEA_API_URL,
TASK_ID_RE,
TASK_PREFIX,
VIKUNJA_API_URL,
VIKUNJA_PROJECT_ID,
)
from devx.exceptions import APIError
from devx.i18n import _
from devx.tokens import get_ci_token, get_vikunja_token
# Strip leading task ID prefix (e.g. "DEVX-12: " or "OBL-INFRA-364: ") from commit subjects.
_TASK_ID_PREFIX_RE = re.compile(rf"^{TASK_PREFIX}-\d+:\s*")
TASKID_FILE = ".taskid" # Deprecated, kept for backward-compat warnings
PR_TITLE_RE = re.compile(rf"^{TASK_PREFIX}-\d+:\s+.+")
@@ -48,20 +50,6 @@ PR_TITLE_RE = re.compile(rf"^{TASK_PREFIX}-\d+:\s+.+")
load_dotenv()
def run_cmd(args: list[str], check: bool = True) -> subprocess.CompletedProcess[str]:
"""Run a command and return the completed process."""
result = subprocess.run(args, capture_output=True, text=True, check=False) # nosec B603
if check and result.returncode != 0:
raise click.ClickException(
_(
"Command failed ({cmd}): {stderr}",
cmd=" ".join(args),
stderr=result.stderr.strip() or result.stdout.strip(),
)
)
return result
def read_taskid(branch: str) -> str:
"""Read task ID from branch name.
@@ -92,9 +80,8 @@ def read_taskid(branch: str) -> str:
def extract_task_id(branch: str) -> str:
"""Extract DEVX-N task identifier from branch name (legacy fallback)."""
match = TASK_ID_RE.search(branch)
return match.group(0) if match else ""
"""Extract task identifier from branch name (delegates to shared utility)."""
return _extract_task_id(branch)
def validate_pr_title(pr_title: str, task_id: str) -> None:
@@ -128,9 +115,12 @@ def get_vikunja_task_title(task_id: str) -> str:
Raises ClickException if VIKUNJA_TOKEN is not set or the task is not found.
"""
token = os.environ.get("VIKUNJA_TOKEN", "")
if not token:
raise click.ClickException(_("VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles."))
try:
token = get_vikunja_token()
except click.ClickException:
raise click.ClickException(
_("VIKUNJA_TOKEN is not set. This is required in CI to validate PR titles.")
) from None
client = VikunjaClient(VIKUNJA_API_URL, token)
page = 1
while True:
@@ -174,19 +164,33 @@ def validate_pr_title_matches_vikunja(pr_title: str, task_id: str) -> None:
def extract_conventional_msg(commits: list[dict[str, Any]]) -> str:
"""Extract the conventional commit message from PR commits.
Iterates commits in reverse order (newest first) to find the first
message matching the conventional commit format. Falls back to the
newest commit message if none match.
Picks the highest-priority conventional commit message from the PR.
Priority: feat > fix > refactor > docs > chore > other.
Falls back to the newest commit message if none match.
"""
priority = {"feat": 5, "fix": 4, "refactor": 3, "docs": 2, "chore": 1, "ci": 1, "style": 1, "test": 1}
best_msg = ""
best_score = 0
for commit in reversed(commits):
commit_info = commit.get("commit", {})
message = str(commit_info.get("message", "") if isinstance(commit_info, dict) else "").split("\n")[0]
if CONVENTIONAL_RE.match(message):
return message
# Fallback: use the newest commit's first line
# Strip any leading task ID prefix (e.g. "OBL-INFRA-364: fix: ...") so
# conventional commit matching works on the remainder.
stripped = _TASK_ID_PREFIX_RE.sub("", message)
m = CONVENTIONAL_RE.match(stripped)
if m:
prefix = m.group(1).split("(")[0].strip() # e.g. "feat" from "feat(scope)"
score = priority.get(prefix, 0)
if score > best_score:
best_score = score
best_msg = stripped
if best_msg:
return best_msg
# Fallback: use the newest commit's first line (strip task ID prefix if present)
if commits:
commit_info = commits[-1].get("commit", {})
return str(commit_info.get("message", "") if isinstance(commit_info, dict) else "").split("\n")[0]
raw = str(commit_info.get("message", "") if isinstance(commit_info, dict) else "").split("\n")[0]
return _TASK_ID_PREFIX_RE.sub("", raw)
return ""
@@ -196,9 +200,10 @@ def extract_conventional_msg(commits: list[dict[str, Any]]) -> str:
@click.argument("repo")
@click.argument("pr_number")
def main(branch: str, pr_title: str, repo: str, pr_number: str) -> None:
token = os.environ.get("REPO_TOKEN", "")
if not token:
raise click.ClickException(_("ERROR: REPO_TOKEN is not set."))
try:
token = get_ci_token()
except click.ClickException:
raise click.ClickException(_("ERROR: CI_GITEA_TOKEN is not set.")) from None
# Validate PR number is an integer
try:
@@ -237,17 +242,35 @@ def main(branch: str, pr_title: str, repo: str, pr_number: str) -> None:
client.merge_pr(pr_num, merge_title)
except APIError as e:
if e.status == 405 and "behind" in e.message.lower():
# Head branch is behind master — do NOT auto-rebase.
# Auto-rebasing creates a feedback loop: the force-push triggers
# a new pull_request synchronize event, which starts a new CI run,
# which runs auto-merge again, which rebases again, etc.
raise click.ClickException(
# Head branch is behind master. Auto-rebase via Gitea API.
# This triggers a new pull_request synchronize event → new CI run.
# The next auto-merge attempt will find the branch up-to-date and
# merge successfully. This is NOT an infinite loop: the rebase
# resolves the "behind" condition, so the next run merges.
# If another PR merges in between, the branch may fall behind
# again, but the process converges as PRs stop merging.
click.echo(
_(
"Branch is behind master. Rebase manually:\n"
" git fetch origin master && git rebase origin/master && git push --force-with-lease\n"
"Then re-add the ready-to-merge label.",
"Branch is behind master. Auto-rebasing via Gitea API...\n"
"A new CI run will start automatically after the rebase.\n"
"The next auto-merge attempt will merge this PR.",
)
) from None
)
try:
client.update_pr_branch(pr_num, style="rebase")
except APIError as rebase_err:
raise click.ClickException(
_(
"Auto-rebase failed with HTTP {status}: {message}\n"
"Rebase manually:\n"
" git fetch origin master && git rebase origin/master && git push --force-with-lease\n"
"Then re-add the ready-to-merge label.",
status=rebase_err.status,
message=rebase_err.message,
)
) from None
# Exit cleanly — the rebase triggers a new CI run that will retry.
return
else:
raise click.ClickException(
_(
+314
View File
@@ -0,0 +1,314 @@
#!/usr/bin/env python3
"""Pre-merge validation gate for auto-merge preconditions.
Validates that a PR satisfies auto-merge requirements BEFORE expensive
jobs (molecule tests, staging deploy) run. This catches issues early:
1. Branch name contains a task ID (e.g., ``DEVX-256-fix-foo``).
2. PR title follows ``{PREFIX}-N: <title>`` format.
3. PR title task ID matches the branch task ID.
4. PR title matches the Vikunja task title (requires ``VIKUNJA_TOKEN``).
5. Branch is not behind master (would trigger a rebase retry cycle).
Exit code 0 = ready for auto-merge (preconditions satisfied).
Exit code 1 = NOT ready fix issues before pushing.
Usage::
# CI (with VIKUNJA_TOKEN and CI_GITEA_API_TOKEN):
python3 -m devx.ci.check_auto_merge_ready \\
--branch "$HEAD_REF" \\
--pr-title "$PR_TITLE" \\
--repo "$REPOSITORY" \\
--pr-number "$PR_NUMBER"
# Local (pre-push hook, no PR yet — validates branch + title format only):
python3 -m devx.ci.check_auto_merge_ready --branch "$(git rev-parse --abbrev-ref HEAD)"
# Local (with PR number, fetches title from Gitea):
python3 -m devx.ci.check_auto_merge_ready --branch "$(git rev-parse --abbrev-ref HEAD)" \\
--repo owner/repo --pr-number 123
If ``VIKUNJA_TOKEN`` is not set, the Vikunja title match check is
skipped (with a warning) this allows local pre-push hooks to run
without CI secrets. In CI, the token is always set and the check is
mandatory.
If ``CI_GITEA_API_TOKEN`` is not set and ``--pr-number`` is not provided, only
branch-name and PR-title-format checks run (local mode).
"""
from __future__ import annotations
import subprocess # nosec B404
import click
from dotenv import load_dotenv # pyright: ignore[reportMissingImports,reportUnknownVariableType]
from devx.api_clients import GiteaClient, VikunjaClient
from devx.ci.auto_merge import extract_task_id
from devx.config import (
GITEA_API_URL,
VIKUNJA_API_URL,
VIKUNJA_PROJECT_ID,
)
from devx.exceptions import APIError
from devx.i18n import _
from devx.tokens import get_ci_token, get_vikunja_token
load_dotenv()
def is_branch_behind_master(branch: str) -> bool:
"""Check if the local branch is behind origin/master.
Fetches origin first (best-effort) then compares commit counts.
Returns ``True`` if master has commits not in branch.
"""
try:
subprocess.run( # nosec B603, B607
["git", "fetch", "origin", "master", "--quiet"],
check=False,
capture_output=True,
timeout=30,
)
result = subprocess.run( # nosec B603, B607
["git", "rev-list", "--count", f"origin/master..{branch}"],
capture_output=True,
text=True,
check=False,
timeout=10,
)
if result.returncode != 0:
return False # Can't determine — don't block
result = subprocess.run( # nosec B603, B607
["git", "rev-list", "--count", f"{branch}..origin/master"],
capture_output=True,
text=True,
check=False,
timeout=10,
)
if result.returncode != 0:
return False
behind = int(result.stdout.strip() or "0")
except (subprocess.TimeoutExpired, FileNotFoundError, ValueError):
return False # Don't block on git errors
return behind > 0
def get_pr_title_from_gitea(repo: str, pr_number: int) -> str | None:
"""Fetch the PR title from the Gitea API.
Returns ``None`` if no token is set or the PR cannot be fetched.
"""
try:
token = get_ci_token()
except click.ClickException:
return None
if "/" not in repo:
return None
owner, repo_name = repo.split("/", 1)
client = GiteaClient(GITEA_API_URL, token, owner, repo_name)
try:
pr = client.get_pr(pr_number)
return str(pr.get("title", ""))
except APIError:
return None
def get_vikunja_title_optional(task_id: str) -> str | None:
"""Fetch the Vikunja task title, returning None if token is not set.
Unlike :func:`devx.ci.auto_merge.get_vikunja_task_title`, this does NOT
raise when ``VIKUNJA_TOKEN`` is missing it returns ``None`` so the
caller can skip the check in local mode.
"""
try:
token = get_vikunja_token()
except click.ClickException:
return None
client = VikunjaClient(VIKUNJA_API_URL, token)
from devx.config import DEFAULT_PER_PAGE
page = 1
while True:
tasks = client.list_project_tasks(VIKUNJA_PROJECT_ID, page=page, per_page=DEFAULT_PER_PAGE)
if not tasks:
break
matches = [t for t in tasks if t.get("identifier") == task_id]
if matches:
return str(matches[0].get("title", ""))
if len(tasks) < DEFAULT_PER_PAGE:
break
page += 1
return None
@click.command()
@click.option("--branch", required=True, help=_("Branch name (e.g., DEVX-256-fix-foo)"))
@click.option("--pr-title", default=None, help=_("PR title (auto-fetched if --pr-number given)"))
@click.option("--repo", default=None, help=_("Repository in owner/name format"))
@click.option("--pr-number", type=int, default=None, help=_("PR number (to fetch title from Gitea)"))
@click.option("--skip-vikunja", is_flag=True, help=_("Skip Vikunja title match check"))
@click.option("--skip-behind-check", is_flag=True, help=_("Skip branch-behind-master check"))
def cli(
branch: str,
pr_title: str | None,
repo: str | None,
pr_number: int | None,
skip_vikunja: bool,
skip_behind_check: bool,
) -> None:
"""Validate auto-merge preconditions before expensive CI jobs."""
import re
from devx.config import TASK_PREFIX
pr_title_re = re.compile(rf"^{TASK_PREFIX}-\d+:\s+.+") # noqa: PLW1503
errors: list[str] = []
# 1. Branch task ID
task_id = extract_task_id(branch)
if not task_id:
errors.append(
_(
"No task ID found in branch name '{branch}'. Expected format: {prefix}-N-description.",
branch=branch,
prefix=TASK_PREFIX,
),
)
# Can't continue — no task ID to validate against
for e in errors:
click.echo(f"ERROR: {e}", err=True)
raise click.ClickException(_("Branch name must contain a task ID."))
click.echo(f"[pre-merge-check] Task ID: {task_id}")
# 2. Resolve PR title
if pr_title is None and pr_number is not None and repo is not None:
pr_title = get_pr_title_from_gitea(repo, pr_number)
if pr_title:
click.echo(f"[pre-merge-check] PR title (from Gitea): {pr_title}")
if pr_title is None:
# Local mode without PR — only validate branch name
if pr_number is not None:
raise click.ClickException(
_("Could not fetch PR title from Gitea (CI_GITEA_TOKEN not set or PR not found).")
)
click.echo("[pre-merge-check] No PR title provided — running branch-name-only check (local mode).")
click.echo("[pre-merge-check] Branch name OK. Push to create PR, then CI will validate the title.")
return
# 3. PR title format
if not pr_title_re.match(pr_title):
errors.append(
_(
"PR title must follow format '{prefix}-N: <task title>'.\n Got: {title}",
prefix=TASK_PREFIX,
title=pr_title,
),
)
# 4. PR title task ID matches branch task ID
if not pr_title.startswith(f"{task_id}:"):
errors.append(
_(
"PR title task ID mismatch.\n Branch task ID: {task_id}\n PR title: {title}",
task_id=task_id,
title=pr_title,
),
)
# 5. Vikunja task title match (skip if no token or --skip-vikunja)
if not skip_vikunja:
vikunja_title = get_vikunja_title_optional(task_id)
if vikunja_title is None:
try:
get_vikunja_token()
token_set = True
except click.ClickException:
token_set = False
if token_set:
errors.append(
_(
"Could not find Vikunja task {task_id} in project {project_id}.",
task_id=task_id,
project_id=VIKUNJA_PROJECT_ID,
),
)
else:
click.echo("[pre-merge-check] WARNING: VIKUNJA_TOKEN not set — skipping Vikunja title match check.")
else:
# Defensive check: warn if the Vikunja task title already includes
# the task ID prefix. The expected PR title is
# f"{task_id}: {vikunja_title}" — if vikunja_title already starts
# with "{task_id}:", the PR title will have a double prefix.
if vikunja_title.startswith(f"{task_id}:"):
errors.append(
_(
"Vikunja task title '{title}' starts with '{prefix}:'. "
"The task title should NOT include the '{prefix}' prefix — "
"it is automatically added to the PR title. "
"Update the Vikunja task title to remove the prefix.",
title=vikunja_title,
prefix=task_id,
),
)
else:
expected = f"{task_id}: {vikunja_title}"
if pr_title != expected:
errors.append(
_(
"PR title does not match Vikunja task title.\n Expected: {expected}\n Got: {title}",
expected=expected,
title=pr_title,
),
)
else:
click.echo(f"[pre-merge-check] Vikunja title match OK: {expected}")
# 6. Branch behind master (skip if --skip-behind-check)
if not skip_behind_check:
if is_branch_behind_master(branch):
errors.append(
_("Branch is behind origin/master. Rebase first: git fetch origin master && git rebase origin/master")
)
else:
click.echo("[pre-merge-check] Branch is up-to-date with origin/master.")
if errors:
click.echo("", err=True)
click.echo("=" * 60, err=True)
click.echo("Pre-merge validation FAILED — fix these before pushing:", err=True)
click.echo("=" * 60, err=True)
for e in errors:
click.echo(f" - {e}", err=True)
# Remediation hints for the most common failure: PR title format
title_errors = [
e for e in errors if "PR title must follow format" in str(e) or "PR title task ID mismatch" in str(e)
]
if title_errors and pr_number is not None and repo is not None:
click.echo("", err=True)
click.echo("REMEDIATION:", err=True)
click.echo(
_(
" Fix the PR title with:\n"
" python3 -m devx.ci.fix_pr_title --repo {repo} --pr-number {pr}\n"
" Or manually set the PR title to: '{expected}'",
repo=repo,
pr=pr_number,
expected=f"{task_id}: <Vikunja task title>",
),
err=True,
)
raise click.ClickException(_("Pre-merge validation failed."))
click.echo("[pre-merge-check] All auto-merge preconditions satisfied.")
if __name__ == "__main__": # pragma: no cover
cli() # pragma: no cover
+33 -9
View File
@@ -31,11 +31,11 @@ from pathlib import Path
import click
REPO_ROOT = Path(__file__).resolve().parent.parent.parent.parent
REPO_ROOT = Path.cwd()
SUPPORTED_LANGS = ("en", "bg", "de", "ru", "zh", "pl")
# Default translation set: devx package itself
# Default translation set: look for translations.json in the current repo
DEFAULT_TRANS_FILE = REPO_ROOT / "src" / "devx" / "translations.json"
DEFAULT_SRC_DIR = REPO_ROOT / "src" / "devx"
@@ -169,20 +169,44 @@ def print_result(result: TranslationCheckResult) -> None:
"translations",
multiple=True,
type=click.Path(exists=False, path_type=Path),
help="Path to a translations JSON file to check (can be repeated). Defaults to src/devx/translations.json.",
help="Path to a translations JSON file to check (can be repeated). Auto-detects by default.",
)
def main(translations: tuple[Path, ...]) -> None:
@click.option(
"--source-dir",
default=None,
help="Source directory to scan for _() calls (default: auto-detect).",
)
def main(translations: tuple[Path, ...], source_dir: str | None) -> None:
"""Check translation files for gaps, dead keys, and missing languages."""
results: list[TranslationCheckResult] = []
if not translations:
# Default: check the devx package's own translations
results = [
check_translation_set("devx", DEFAULT_SRC_DIR, DEFAULT_TRANS_FILE),
# Auto-detect translations file in the current repo
root = Path.cwd()
# Try common locations
candidates = [
root / "src" / "devx" / "translations.json",
root / "src" / "grm" / "translations.json",
]
# Also search for any translations.json in src/
for match in root.glob("src/*/translations.json"):
candidates.append(match)
found = False
for candidate in candidates:
if candidate.exists():
src_dir = Path(source_dir) if source_dir else candidate.parent
results.append(check_translation_set(candidate.parent.name, src_dir, candidate))
found = True
break
if not found:
# No translations file found — this repo doesn't use i18n
click.echo("PASS: No translations file found — skipping (repo does not use i18n).")
return
else:
results = []
for trans_file in translations:
# Infer source directory as the parent of the translations file
src_dir = trans_file.parent
src_dir = Path(source_dir) if source_dir else trans_file.parent
name = trans_file.parent.name
results.append(check_translation_set(name, src_dir, trans_file))
+24 -26
View File
@@ -138,7 +138,7 @@ from typing import Any
import click
from devx.ci._shared import get_latest_tag
from devx.ci._shared import get_latest_tag, write_github_output
from devx.i18n import _
# ---------------------------------------------------------------------------
@@ -393,14 +393,15 @@ class ChangeClassifier:
tags = self._compute_tags(file_path)
# 1. User-facing overrides (highest priority — safety)
if file_path in self._user_overrides:
return FileClassification(
path=file_path,
is_user_facing=True,
reason="User-facing override (safety override)",
matched_rule="user_facing_overrides",
tags=tags,
)
for pattern in self._user_overrides:
if _matches_glob(file_path, pattern):
return FileClassification(
path=file_path,
is_user_facing=True,
reason=f"User-facing override (matches '{pattern}')",
matched_rule="user_facing_overrides",
tags=tags,
)
# 2. Infrastructure overrides
if file_path in self._infra_overrides:
@@ -583,15 +584,8 @@ def has_user_facing_changes(
# ---------------------------------------------------------------------------
def _write_github_output(key: str, value: str) -> None:
"""Append a key=value line to the $GITHUB_OUTPUT file."""
gh_output = os.environ.get("GITHUB_OUTPUT")
if not gh_output:
raise click.ClickException("GITHUB_OUTPUT environment variable is not set")
with open(gh_output, "a") as f: # noqa: PTH123
f.write(f"{key}={value}\n")
# ---------------------------------------------------------------------------
# Classification logic
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
@@ -627,10 +621,14 @@ def main(base: str | None, head: str, quiet: bool, check: str, github_output: bo
classifier = _get_classifier()
available_tags = list(classifier.config.tags.keys())
# --force can also be activated via FORCE_DEPLOY env var (for workflow_dispatch)
if os.environ.get("FORCE_DEPLOY", "").lower() == "true":
force = True
if force and github_output:
_write_github_output("user-facing-changed", "true")
write_github_output("user-facing-changed", "true")
for tag in available_tags:
_write_github_output(f"{tag}-changed", "true")
write_github_output(f"{tag}-changed", "true")
click.echo("Forced user-facing-changed=true via --force flag.")
return
@@ -638,9 +636,9 @@ def main(base: str | None, head: str, quiet: bool, check: str, github_output: bo
base = get_latest_tag()
if not base:
if github_output:
_write_github_output("user-facing-changed", "true")
write_github_output("user-facing-changed", "true")
for tag in available_tags:
_write_github_output(f"{tag}-changed", "true")
write_github_output(f"{tag}-changed", "true")
click.echo("No tags found — treating all changes as user-facing.")
return
if quiet:
@@ -652,9 +650,9 @@ def main(base: str | None, head: str, quiet: bool, check: str, github_output: bo
files = get_changed_files(base, head)
if not files:
if github_output:
_write_github_output("user-facing-changed", "false")
write_github_output("user-facing-changed", "false")
for tag in available_tags:
_write_github_output(f"{tag}-changed", "false")
write_github_output(f"{tag}-changed", "false")
click.echo(f"No changes between {base} and {head}.")
return
if quiet:
@@ -666,9 +664,9 @@ def main(base: str | None, head: str, quiet: bool, check: str, github_output: bo
result = classifier.classify(files)
if github_output:
_write_github_output("user-facing-changed", "true" if result.has_user_facing else "false")
write_github_output("user-facing-changed", "true" if result.has_user_facing else "false")
for tag in available_tags:
_write_github_output(f"{tag}-changed", "true" if result.has_tag(tag) else "false")
write_github_output(f"{tag}-changed", "true" if result.has_tag(tag) else "false")
click.echo(f"User-facing files changed: {result.has_user_facing}")
for tag in available_tags:
click.echo(f"{tag.capitalize()} files changed: {result.has_tag(tag)}")
+24 -13
View File
@@ -1,7 +1,10 @@
#!/usr/bin/env python3
"""Detect whether the latest git commit is a release commit.
"""Detect whether the latest git commit is an automated CI commit.
Release commits have the format ``release: vX.Y.Z``.
Badge commits have the format ``chore: update badge URLs ... [skip ci]``.
Both are generated by CI and should skip post-merge jobs.
This script writes ``is-release=true`` or ``is-release=false`` to
``$GITHUB_OUTPUT`` for use in CI workflow conditionals.
@@ -12,13 +15,16 @@ Usage::
from __future__ import annotations
import os
import re
import subprocess # nosec B404
import click
from devx.ci._shared import write_github_output
from devx.i18n import _
RELEASE_RE = re.compile(r"^release: v\d+\.\d+\.\d+")
BADGE_RE = re.compile(r"^chore: update badge URLs.*\[skip ci\]")
def get_commit_message() -> str:
@@ -39,26 +45,31 @@ def is_release_commit(message: str) -> bool:
return bool(RELEASE_RE.match(message))
def write_github_output(key: str, value: str) -> None:
"""Append a key=value line to the $GITHUB_OUTPUT file."""
gh_output = os.environ.get("GITHUB_OUTPUT")
if not gh_output:
raise click.ClickException("GITHUB_OUTPUT environment variable is not set")
with open(gh_output, "a") as f: # noqa: PTH123
f.write(f"{key}={value}\n")
def is_badge_commit(message: str) -> bool:
"""Check if a commit message matches the badge commit format."""
return bool(BADGE_RE.match(message))
def is_automated_commit(message: str) -> bool:
"""Check if a commit is an automated CI commit (release or badge)."""
return is_release_commit(message) or is_badge_commit(message)
@click.command()
def main() -> None:
"""Detect if the latest commit is a release commit and set GITHUB_OUTPUT."""
"""Detect if the latest commit is an automated CI commit and set GITHUB_OUTPUT."""
msg = get_commit_message()
click.echo(f"Commit message: {msg}")
click.echo(_("Commit message: {msg}", msg=msg))
is_release = is_release_commit(msg)
is_automated = is_automated_commit(msg)
write_github_output("is-release", "true" if is_release else "false")
write_github_output("is-automated", "true" if is_automated else "false")
if is_release:
click.echo("Release commit — skipping all post-merge jobs.")
click.echo(_("Release commit — skipping all post-merge jobs."))
elif is_automated:
click.echo(_("Automated CI commit (badge) — skipping post-merge jobs."))
else:
click.echo("Regular merge commit — running all post-merge jobs.")
click.echo(_("Regular merge commit — running all post-merge jobs."))
if __name__ == "__main__": # pragma: no cover
+24 -16
View File
@@ -29,7 +29,9 @@ import os
import click
import requests
from devx.config import GITEA_API_URL
from devx.config import GITEA_API_URL, REPO_NAME, REPO_OWNER
from devx.i18n import _
from devx.tokens import get_ci_token
DEFAULT_MAX_RUNNERS = 3
@@ -55,9 +57,9 @@ def query_runners(api_url: str, token: str, owner: str, repo: str) -> int:
data = r.json()
total += data.get("total_count", 0)
else:
click.echo(f"Warning: repo-level runners query returned HTTP {r.status_code}", err=True)
click.echo(_("Warning: repo-level runners query returned HTTP {status}", status=r.status_code), err=True)
except (requests.RequestException, ValueError) as e:
click.echo(f"Warning: repo-level runners query failed: {e}", err=True)
click.echo(_("Warning: repo-level runners query failed: {error}", error=e), err=True)
# 2. Organization-level runners
try:
@@ -70,9 +72,9 @@ def query_runners(api_url: str, token: str, owner: str, repo: str) -> int:
data = r.json()
total += data.get("total_count", 0)
else:
click.echo(f"Warning: org-level runners query returned HTTP {r.status_code}", err=True)
click.echo(_("Warning: org-level runners query returned HTTP {status}", status=r.status_code), err=True)
except (requests.RequestException, ValueError) as e:
click.echo(f"Warning: org-level runners query failed: {e}", err=True)
click.echo(_("Warning: org-level runners query failed: {error}", error=e), err=True)
# 3. Instance-level runners (requires admin scope)
try:
@@ -85,14 +87,17 @@ def query_runners(api_url: str, token: str, owner: str, repo: str) -> int:
data = r.json()
total += data.get("total_count", 0)
elif r.status_code != 403: # 403 is expected without admin scope
click.echo(f"Warning: instance-level runners query returned HTTP {r.status_code}", err=True)
click.echo(
_("Warning: instance-level runners query returned HTTP {status}", status=r.status_code),
err=True,
)
except (requests.RequestException, ValueError) as e:
click.echo(f"Warning: instance-level runners query failed: {e}", err=True)
click.echo(_("Warning: instance-level runners query failed: {error}", error=e), err=True)
return total
def get_runner_count(api_url: str, token: str, owner: str, repo: str) -> int:
def get_runner_count(api_url: str, token: str | None, owner: str, repo: str) -> int:
"""Determine the number of available runners.
Tries the Gitea API first, then falls back to env vars, then default.
@@ -148,12 +153,15 @@ def main(
output_indices: bool,
github_output: bool,
) -> None:
token = os.environ.get("REPO_TOKEN", "")
try:
token = get_ci_token()
except click.ClickException:
token = None
if owner is None:
owner = os.environ.get("DEVX_REPO_OWNER", "oblachno-oss")
owner = os.environ.get("DEVX_REPO_OWNER", "") or REPO_OWNER
if repo is None:
repo = os.environ.get("DEVX_REPO_NAME", "devx")
repo = os.environ.get("DEVX_REPO_NAME", "") or REPO_NAME
count = get_runner_count(GITEA_API_URL, token, owner, repo)
indices = generate_indices(count)
@@ -162,11 +170,11 @@ def main(
gh_output = os.environ.get("GITHUB_OUTPUT")
if not gh_output:
raise click.ClickException("GITHUB_OUTPUT environment variable is not set")
with open(gh_output, "a") as f: # noqa: PTH123
with open(gh_output, "a", encoding="utf-8") as f: # noqa: PTH123
f.write(f"runner-count={count}\n")
f.write(f"runner-indices={json.dumps(indices)}\n")
click.echo(f"Runner count: {count}")
click.echo(f"Runner indices: {indices}")
click.echo(_("Runner count: {count}", count=count))
click.echo(_("Runner indices: {indices}", indices=indices))
return
if output_count:
@@ -178,8 +186,8 @@ def main(
return
# Default: output both as key=value pairs for CI consumption
click.echo(f"count={count}")
click.echo(f"indices={json.dumps(indices)}")
click.echo(_("count={count}", count=count))
click.echo(_("indices={indices}", indices=json.dumps(indices)))
if __name__ == "__main__": # pragma: no cover

Some files were not shown because too many files have changed in this diff Show More