Compare commits

..
Author SHA1 Message Date
Emil Simeonov 604db26a11 ci: retrigger validation
CI / validate (pull_request) Successful in 1m58s
CI / auto-merge (pull_request) Successful in 15s
2026-09-28 14:32:38 +00:00
gitea-actions-bot 2ea38931b7 chore: update badge URLs to commit 4422c70b [skip ci] 2026-09-28 14:32:38 +00:00
7 changed files with 20 additions and 150 deletions
-3
View File
@@ -21,9 +21,6 @@ name: Post-merge
on:
push:
branches: [master]
# Implements: REQ-1 — manual recovery when a [skip ci] squash title
# suppresses the push-triggered post-merge run.
workflow_dispatch:
concurrency:
group: post-merge-${{ github.ref }}
+6 -6
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/32e239ca6b5c1d1a1918c31ae536917b9914f847/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/32e239ca6b5c1d1a1918c31ae536917b9914f847/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/32e239ca6b5c1d1a1918c31ae536917b9914f847/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/32e239ca6b5c1d1a1918c31ae536917b9914f847/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/32e239ca6b5c1d1a1918c31ae536917b9914f847/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/32e239ca6b5c1d1a1918c31ae536917b9914f847/python.svg)](https://www.python.org/downloads/)
[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/4422c70b23b4a707c24ab91f80bb4fd2e2fff921/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/4422c70b23b4a707c24ab91f80bb4fd2e2fff921/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/4422c70b23b4a707c24ab91f80bb4fd2e2fff921/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/4422c70b23b4a707c24ab91f80bb4fd2e2fff921/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/4422c70b23b4a707c24ab91f80bb4fd2e2fff921/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/4422c70b23b4a707c24ab91f80bb4fd2e2fff921/python.svg)](https://www.python.org/downloads/)
## Why devx?
+6 -6
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/32e239ca6b5c1d1a1918c31ae536917b9914f847/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/32e239ca6b5c1d1a1918c31ae536917b9914f847/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/32e239ca6b5c1d1a1918c31ae536917b9914f847/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/32e239ca6b5c1d1a1918c31ae536917b9914f847/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/32e239ca6b5c1d1a1918c31ae536917b9914f847/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/32e239ca6b5c1d1a1918c31ae536917b9914f847/python.svg)](https://www.python.org/downloads/)
[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/4422c70b23b4a707c24ab91f80bb4fd2e2fff921/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/4422c70b23b4a707c24ab91f80bb4fd2e2fff921/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/4422c70b23b4a707c24ab91f80bb4fd2e2fff921/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/4422c70b23b4a707c24ab91f80bb4fd2e2fff921/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/4422c70b23b4a707c24ab91f80bb4fd2e2fff921/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/4422c70b23b4a707c24ab91f80bb4fd2e2fff921/python.svg)](https://www.python.org/downloads/)
## Overview
-49
View File
@@ -1,49 +0,0 @@
# 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.
-35
View File
@@ -1,35 +0,0 @@
# DEVX-180: Add workflow_dispatch trigger to post-merge workflow
## Problem
When auto-merge produces a `[skip ci]` squash title (DEVX-179), the
post-merge push run is suppressed — release, publish, wiki sync, Vikunja
close, and badges are all skipped with no manual recovery path, because
`post-merge.yml` only triggers on `push`.
## Approach
REQ-1: Add `workflow_dispatch:` to the `on:` block of
`.gitea/workflows/post-merge.yml` so a missed or skipped post-merge run
can be dispatched manually on master.
## Test Plan
- `make workflow-check` (actionlint + act_runner dryrun) validates the
trigger syntax.
- Post-merge: dispatch the workflow manually once and confirm it runs
on master.
## Deploy Plan
Merge → post-merge push run fires normally → then dispatch
`post-merge.yml` on master to recover the suppressed DEVX-178 release.
## Rollback Plan
Revert the one-line trigger addition.
## Acceptance Criteria
- [x] REQ-1: `workflow_dispatch` present in `post-merge.yml` `on:` block;
actionlint and act_runner dryrun pass.
+8 -17
View File
@@ -184,31 +184,22 @@ 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:
"""Extract the conventional commit message from PR commits.
Picks the highest-priority conventional commit message from the PR.
Priority: feat > fix > refactor > docs > chore > other. The Gitea
``/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 > 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 commits:
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]
# Strip any leading task ID prefix (e.g. "OBL-INFRA-364: fix: ...") so
# conventional commit matching works on the remainder; drop CI-skip
# tokens so they never reach the merge title.
stripped = _SKIP_CI_RE.sub("", _TASK_ID_PREFIX_RE.sub("", message)).strip()
# 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)"
@@ -218,11 +209,11 @@ def extract_conventional_msg(commits: list[dict[str, Any]]) -> str:
best_msg = stripped
if best_msg:
return best_msg
# Fallback: the newest commit's first line (newest-first API order).
# Fallback: use the newest commit's first line (strip task ID prefix if present)
if commits:
commit_info = commits[0].get("commit", {})
commit_info = commits[-1].get("commit", {})
raw = str(commit_info.get("message", "") if isinstance(commit_info, dict) else "").split("\n")[0]
return _SKIP_CI_RE.sub("", _TASK_ID_PREFIX_RE.sub("", raw)).strip()
return _TASK_ID_PREFIX_RE.sub("", raw)
return ""
-34
View File
@@ -232,40 +232,6 @@ class TestExtractConventionalMsg:
]
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 --