Public Access
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1ee03d129 | ||
|
|
c523a80848 |
@@ -2,6 +2,12 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [0.55.5] - 2026-09-28
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Strip [skip ci] tokens and prefer newest commit in squash titles
|
||||||
|
|
||||||
## [0.55.4] - 2026-09-23
|
## [0.55.4] - 2026-09-23
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ extra index and list devx in your dependencies:
|
|||||||
```toml
|
```toml
|
||||||
[project]
|
[project]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"devx>=0.55.4",
|
"devx>=0.55.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.pip]
|
[tool.pip]
|
||||||
@@ -101,8 +101,8 @@ pip install -e .
|
|||||||
```
|
```
|
||||||
|
|
||||||
> **Note:** If your project requires a specific devx version, pin it in
|
> **Note:** If your project requires a specific devx version, pin it in
|
||||||
> `dependencies` (for example, `"devx==0.55.4"`) or use a version constraint
|
> `dependencies` (for example, `"devx==0.55.5"`) or use a version constraint
|
||||||
> (for example, `"devx>=0.55.4,<0.56"`).
|
> (for example, `"devx>=0.55.5,<0.56"`).
|
||||||
|
|
||||||
### Optional extras
|
### Optional extras
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -74,14 +74,14 @@ Add devx to your `pyproject.toml` dependencies and configure the registry:
|
|||||||
```toml
|
```toml
|
||||||
[project]
|
[project]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"devx>=0.55.4",
|
"devx>=0.55.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.pip]
|
[tool.pip]
|
||||||
extra-index-url = "https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple"
|
extra-index-url = "https://git.oblachno.oblachno.fyi/api/packages/oblachno-oss/pypi/simple"
|
||||||
```
|
```
|
||||||
|
|
||||||
Pin a specific version if needed: `"devx==0.55.4"` or `"devx>=0.55.4,<0.56"`.
|
Pin a specific version if needed: `"devx==0.55.5"` or `"devx>=0.55.5,<0.56"`.
|
||||||
|
|
||||||
### Optional extras
|
### Optional extras
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# DEVX-179: auto-merge must never select `[skip ci]` squash titles
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
PR #351 squash-merged as `DEVX-178: chore: update badge URLs to commit
|
||||||
|
4422c70b [skip ci]`. `extract_conventional_msg` picked a commit subject
|
||||||
|
carrying `[skip ci]` — a master badge commit surfaced in the PR's commit
|
||||||
|
list after force-push/amend rewrote branch history. The resulting merge
|
||||||
|
commit suppressed the post-merge push run: no release, no publish, no
|
||||||
|
wiki sync, no Vikunja close.
|
||||||
|
|
||||||
|
PR #352 shows the secondary defect: two `ci:` commits on the branch and
|
||||||
|
the older one won the tie, so the squash title described a throwaway
|
||||||
|
retrigger commit instead of the real change.
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
REQ-1: Subjects containing `[skip ci]`, `[ci skip]`, or `[skip actions]`
|
||||||
|
(case-insensitive) are ineligible merge titles. They are excluded before
|
||||||
|
priority scoring, so a badge/chore/`[skip ci]` commit can never suppress
|
||||||
|
the post-merge pipeline again.
|
||||||
|
|
||||||
|
REQ-2: Equal-priority ties resolve to the newest commit in the returned
|
||||||
|
list (Gitea returns PR commits newest-first; iterate in returned order
|
||||||
|
and keep the first best candidate).
|
||||||
|
|
||||||
|
## Test Plan
|
||||||
|
|
||||||
|
- `test_auto_merge.py`: commits `[ci retrigger, badge-chore-skipci]` →
|
||||||
|
the `ci` subject wins; commits `[older-ci, newer-ci]` → newest wins;
|
||||||
|
all commits `[skip ci]` → falls back to newest non-skipped subject.
|
||||||
|
- `make pytest-cov` (100% gate), `make lint-all`.
|
||||||
|
|
||||||
|
## Deploy Plan
|
||||||
|
|
||||||
|
Merge as `fix:` → post-merge cuts a release that also ships DEVX-178's
|
||||||
|
PLAYBOOK_ROLE_MAP entry.
|
||||||
|
|
||||||
|
## Rollback Plan
|
||||||
|
|
||||||
|
Revert the squash commit; extract behavior returns to the previous
|
||||||
|
(unhardened) selection.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [x] REQ-1: `[skip ci]`/`[ci skip]`/`[skip actions]` subjects are never
|
||||||
|
selected.
|
||||||
|
- [x] REQ-2: Equal-priority ties pick the newest commit.
|
||||||
|
- [x] Unit tests cover both; 100% coverage maintained.
|
||||||
@@ -48,12 +48,12 @@ Add devx to your `pyproject.toml`:
|
|||||||
```toml
|
```toml
|
||||||
[project]
|
[project]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"devx>=0.55.4",
|
"devx>=0.55.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
"devx>=0.55.4",
|
"devx>=0.55.5",
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ create_dependency_pr, auto_merge, release, publish), developer tooling
|
|||||||
molecule testing helpers for Ansible projects.
|
molecule testing helpers for Ansible projects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = "0.55.4"
|
__version__ = "0.55.5"
|
||||||
|
|||||||
@@ -184,22 +184,31 @@ def validate_pr_title_matches_vikunja(pr_title: str, task_id: str) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Subjects carrying these tokens suppress post-merge CI entirely — strip them
|
||||||
|
# before a commit message can become the squash title (see DEVX-179).
|
||||||
|
_SKIP_CI_RE = re.compile(r"\s*\[(?:ci skip|skip ci|skip actions|actions skip|skip)\]", re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
def extract_conventional_msg(commits: list[dict[str, Any]]) -> str:
|
def extract_conventional_msg(commits: list[dict[str, Any]]) -> str:
|
||||||
"""Extract the conventional commit message from PR commits.
|
"""Extract the conventional commit message from PR commits.
|
||||||
|
|
||||||
Picks the highest-priority conventional commit message from the PR.
|
Picks the highest-priority conventional commit message from the PR.
|
||||||
Priority: feat > fix > refactor > docs > chore > other.
|
Priority: feat > fix > refactor > docs > chore > other. The Gitea
|
||||||
Falls back to the newest commit message if none match.
|
``/pulls/{n}/commits`` endpoint returns commits newest-first, so the
|
||||||
|
first best-scoring subject wins equal-priority ties.
|
||||||
|
``[skip ci]``-style tokens are stripped from every candidate so the
|
||||||
|
merge title can never suppress the post-merge release pipeline.
|
||||||
"""
|
"""
|
||||||
priority = {"feat": 5, "fix": 4, "refactor": 3, "docs": 2, "chore": 1, "ci": 1, "style": 1, "test": 1}
|
priority = {"feat": 5, "fix": 4, "refactor": 3, "docs": 2, "chore": 1, "ci": 1, "style": 1, "test": 1}
|
||||||
best_msg = ""
|
best_msg = ""
|
||||||
best_score = 0
|
best_score = 0
|
||||||
for commit in reversed(commits):
|
for commit in commits:
|
||||||
commit_info = commit.get("commit", {})
|
commit_info = commit.get("commit", {})
|
||||||
message = str(commit_info.get("message", "") if isinstance(commit_info, dict) else "").split("\n")[0]
|
message = str(commit_info.get("message", "") if isinstance(commit_info, dict) else "").split("\n")[0]
|
||||||
# Strip any leading task ID prefix (e.g. "OBL-INFRA-364: fix: ...") so
|
# Strip any leading task ID prefix (e.g. "OBL-INFRA-364: fix: ...") so
|
||||||
# conventional commit matching works on the remainder.
|
# conventional commit matching works on the remainder; drop CI-skip
|
||||||
stripped = _TASK_ID_PREFIX_RE.sub("", message)
|
# tokens so they never reach the merge title.
|
||||||
|
stripped = _SKIP_CI_RE.sub("", _TASK_ID_PREFIX_RE.sub("", message)).strip()
|
||||||
m = CONVENTIONAL_RE.match(stripped)
|
m = CONVENTIONAL_RE.match(stripped)
|
||||||
if m:
|
if m:
|
||||||
prefix = m.group(1).split("(")[0].strip() # e.g. "feat" from "feat(scope)"
|
prefix = m.group(1).split("(")[0].strip() # e.g. "feat" from "feat(scope)"
|
||||||
@@ -209,11 +218,11 @@ def extract_conventional_msg(commits: list[dict[str, Any]]) -> str:
|
|||||||
best_msg = stripped
|
best_msg = stripped
|
||||||
if best_msg:
|
if best_msg:
|
||||||
return best_msg
|
return best_msg
|
||||||
# Fallback: use the newest commit's first line (strip task ID prefix if present)
|
# Fallback: the newest commit's first line (newest-first API order).
|
||||||
if commits:
|
if commits:
|
||||||
commit_info = commits[-1].get("commit", {})
|
commit_info = commits[0].get("commit", {})
|
||||||
raw = 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 _SKIP_CI_RE.sub("", _TASK_ID_PREFIX_RE.sub("", raw)).strip()
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -232,6 +232,40 @@ class TestExtractConventionalMsg:
|
|||||||
]
|
]
|
||||||
assert extract_conventional_msg(commits) == "random message"
|
assert extract_conventional_msg(commits) == "random message"
|
||||||
|
|
||||||
|
def test_skip_ci_subjects_are_ineligible(self) -> None:
|
||||||
|
"""[skip ci] commits must never become the merge title (REQ-1)."""
|
||||||
|
commits = [
|
||||||
|
{"commit": {"message": "ci: retrigger validation"}},
|
||||||
|
{"commit": {"message": "chore: update badge URLs to commit abc123 [skip ci]"}},
|
||||||
|
]
|
||||||
|
assert extract_conventional_msg(commits) == "ci: retrigger validation"
|
||||||
|
|
||||||
|
def test_skip_ci_variants_excluded(self) -> None:
|
||||||
|
"""All skip-token spellings are ineligible."""
|
||||||
|
for token in ("[skip ci]", "[ci skip]", "[skip actions]", "[actions skip]", "[SKIP CI]"):
|
||||||
|
commits = [
|
||||||
|
{"commit": {"message": "feat: real change"}},
|
||||||
|
{"commit": {"message": f"chore: noise {token}"}},
|
||||||
|
]
|
||||||
|
assert extract_conventional_msg(commits) == "feat: real change"
|
||||||
|
|
||||||
|
def test_tie_prefers_newest_commit(self) -> None:
|
||||||
|
"""Equal-priority ties resolve to the newest commit (REQ-2); the
|
||||||
|
API returns commits newest-first."""
|
||||||
|
commits = [
|
||||||
|
{"commit": {"message": "ci: add workflow_dispatch trigger"}},
|
||||||
|
{"commit": {"message": "ci: retrigger validation"}},
|
||||||
|
]
|
||||||
|
assert extract_conventional_msg(commits) == "ci: add workflow_dispatch trigger"
|
||||||
|
|
||||||
|
def test_all_skip_ci_fallback_strips_token(self) -> None:
|
||||||
|
"""When every commit carries [skip ci], the token is stripped so
|
||||||
|
the merge still triggers post-merge."""
|
||||||
|
commits = [
|
||||||
|
{"commit": {"message": "chore: noise [skip ci]"}},
|
||||||
|
]
|
||||||
|
assert extract_conventional_msg(commits) == "chore: noise"
|
||||||
|
|
||||||
|
|
||||||
# -- run_cmd --
|
# -- run_cmd --
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user