Public Access
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35c72ef595 | ||
|
|
c0fcaef25f | ||
|
|
3dd5b452c0 | ||
|
|
b2515bbf37 | ||
|
|
ad2e59980f |
@@ -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
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.29.0] - 2026-07-01
|
||||
|
||||
### Features
|
||||
|
||||
- Detect badge commits as automated CI commits
|
||||
|
||||
## [0.28.0] - 2026-07-01
|
||||
|
||||
### Features
|
||||
|
||||
@@ -16,12 +16,12 @@ quality badges.
|
||||
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/LICENSE)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
|
||||
[](https://www.python.org/downloads/)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
|
||||
[](https://www.python.org/downloads/)
|
||||
|
||||
## Why devx?
|
||||
|
||||
|
||||
+6
-6
@@ -12,12 +12,12 @@ project to be reusable across all oblachno-oss repositories.
|
||||
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/LICENSE)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
|
||||
[](https://www.python.org/downloads/)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
|
||||
[](https://www.python.org/downloads/)
|
||||
|
||||
## Overview
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""devx — reusable development and CI/CD tools for oblachno-oss projects."""
|
||||
|
||||
__version__ = "0.28.0"
|
||||
__version__ = "0.29.0"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -21,6 +24,7 @@ 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:
|
||||
@@ -41,15 +45,29 @@ def is_release_commit(message: str) -> bool:
|
||||
return bool(RELEASE_RE.match(message))
|
||||
|
||||
|
||||
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(_("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."))
|
||||
elif is_automated:
|
||||
click.echo(_("Automated CI commit (badge) — skipping post-merge jobs."))
|
||||
else:
|
||||
click.echo(_("Regular merge commit — running all post-merge jobs."))
|
||||
|
||||
|
||||
@@ -2127,6 +2127,14 @@
|
||||
"ru": "Release commit — skipping all post-merge jobs.",
|
||||
"zh": "Release commit — skipping all post-merge jobs."
|
||||
},
|
||||
"Automated CI commit (badge) — skipping post-merge jobs.": {
|
||||
"bg": "Automated CI commit (badge) — skipping post-merge jobs.",
|
||||
"de": "Automated CI commit (badge) — skipping post-merge jobs.",
|
||||
"en": "Automated CI commit (badge) — skipping post-merge jobs.",
|
||||
"pl": "Automated CI commit (badge) — skipping post-merge jobs.",
|
||||
"ru": "Automated CI commit (badge) — skipping post-merge jobs.",
|
||||
"zh": "Automated CI commit (badge) — skipping post-merge jobs."
|
||||
},
|
||||
"Release creation failed: {error}": {
|
||||
"bg": "Release creation failed: {error}",
|
||||
"de": "Release creation failed: {error}",
|
||||
|
||||
@@ -38,6 +38,31 @@ class TestIsReleaseCommit:
|
||||
assert detect_release_commit.is_release_commit("") is False
|
||||
|
||||
|
||||
class TestIsBadgeCommit:
|
||||
def test_badge_commit(self) -> None:
|
||||
assert detect_release_commit.is_badge_commit("chore: update badge URLs to commit abc123 [skip ci]") is True
|
||||
|
||||
def test_regular_chore(self) -> None:
|
||||
assert detect_release_commit.is_badge_commit("chore: cleanup deps") is False
|
||||
|
||||
def test_empty(self) -> None:
|
||||
assert detect_release_commit.is_badge_commit("") is False
|
||||
|
||||
|
||||
class TestIsAutomatedCommit:
|
||||
def test_release_is_automated(self) -> None:
|
||||
assert detect_release_commit.is_automated_commit("release: v1.0.0 [skip ci]") is True
|
||||
|
||||
def test_badge_is_automated(self) -> None:
|
||||
assert detect_release_commit.is_automated_commit("chore: update badge URLs to commit abc123 [skip ci]") is True
|
||||
|
||||
def test_regular_is_not_automated(self) -> None:
|
||||
assert detect_release_commit.is_automated_commit("OBL-INFRA-363: fix: something") is False
|
||||
|
||||
def test_empty(self) -> None:
|
||||
assert detect_release_commit.is_automated_commit("") is False
|
||||
|
||||
|
||||
class TestWriteGithubOutput:
|
||||
def test_write(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
gh_file = tmp_path / "output.txt"
|
||||
@@ -62,7 +87,26 @@ class TestMain:
|
||||
assert result.exit_code == 0
|
||||
assert "Release commit" in result.output
|
||||
with open(gh_file) as f:
|
||||
assert "is-release=true" in f.read()
|
||||
content = f.read()
|
||||
assert "is-release=true" in content
|
||||
assert "is-automated=true" in content
|
||||
|
||||
def test_badge_commit(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
gh_file = tmp_path / "output.txt"
|
||||
monkeypatch.setenv("GITHUB_OUTPUT", str(gh_file))
|
||||
with patch.object(
|
||||
detect_release_commit,
|
||||
"get_commit_message",
|
||||
return_value="chore: update badge URLs to commit abc123 [skip ci]",
|
||||
):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(detect_release_commit.main, [])
|
||||
assert result.exit_code == 0
|
||||
assert "Automated CI commit" in result.output
|
||||
with open(gh_file) as f:
|
||||
content = f.read()
|
||||
assert "is-release=false" in content
|
||||
assert "is-automated=true" in content
|
||||
|
||||
def test_regular_commit(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
gh_file = tmp_path / "output.txt"
|
||||
@@ -73,4 +117,6 @@ class TestMain:
|
||||
assert result.exit_code == 0
|
||||
assert "Regular merge commit" in result.output
|
||||
with open(gh_file) as f:
|
||||
assert "is-release=false" in f.read()
|
||||
content = f.read()
|
||||
assert "is-release=false" in content
|
||||
assert "is-automated=false" in content
|
||||
|
||||
Reference in New Issue
Block a user