From 33063038a1a7f9089f3bc0b344fae620018b5ff7 Mon Sep 17 00:00:00 2001 From: emil Date: Mon, 22 Jun 2026 20:21:44 +0000 Subject: [PATCH] DEVX-6: fix: correct version tags, changelog, and release script recovery --- .taskid | 2 +- CHANGELOG.md | 43 ++++++++++++++++++++++---------------- cliff.toml | 7 +++++-- src/devx/__init__.py | 2 +- src/devx/ci/release.py | 26 +++++++++++++++++++---- src/devx/translations.json | 6 ++++++ tests/unit/test_release.py | 30 +++++++++++++++++++++++--- 7 files changed, 87 insertions(+), 29 deletions(-) diff --git a/.taskid b/.taskid index ef4c28c..3f03528 100644 --- a/.taskid +++ b/.taskid @@ -1 +1 @@ -DEVX-5 +DEVX-6 diff --git a/CHANGELOG.md b/CHANGELOG.md index 0aca6f2..38398d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,14 +2,31 @@ All notable changes to this project will be documented in this file. -## [0.1.0] - 2026-06-22 +## [0.4.0] - 2026-06-22 -## [0.1.0] - 2026-06-22 +### Features -## [0.1.0] - 2026-06-22 +- Add DEFAULT_INFRASTRUCTURE and configurable task prefix +## [0.3.0] - 2026-06-22 -## [0.1.0] - 2026-06-22 +### Features +- Add --no-ansible-collections option to setup tool +## [0.2.0] - 2026-06-22 + +### Features + +- Pluggable change classification framework +## [0.1.2] - 2026-06-22 + +### Bug Fixes + +- Make sync-wiki and vikunja depend on release +## [0.1.1] - 2026-06-22 + +### Bug Fixes + +- Disable push whitelist, allow direct pushes to master ## [0.1.0] - 2026-06-22 ## [0.1.0] - 2026-06-22 @@ -18,18 +35,8 @@ All notable changes to this project will be documented in this file. - Extract reusable dev/CI tools from GRM into devx package -## [unreleased] - -### Features - -- Extract reusable development and CI/CD tools from GRM into a standalone Python package -- Port core modules: config, exceptions, i18n, api_clients, gitea_cli -- Port 14 CI scripts: auto_merge, check_translations, classify_changes, detect_release_commit, discover_runners, doc_coverage, notify_failure, post_merge, pr_review, publish, push_badges, release, sync_wiki, validate_commit_msg -- Port 6 dev tools: check_test_speed, generate_badges, install_checkmake, install_tools, setup, configure_repo -- Port 5 molecule tools as optional extra: platforms, distribute_molecule, discover_runners, molecule_ci_guard, molecule_all -- Add CLI entry point with subcommands: devx ci, devx tools, devx molecule -- Add Gitea PyPI registry publishing support in publish.py -- Add configurable workflow-only patterns in classify_changes.py -- Add configurable version file path in release.py -- Replicate GRM's automated workflow: CI, auto-merge, post-merge, release, badges, wiki sync, Vikunja +### Bug Fixes +- Use python3 and venv python in workflows and Makefile +- Fix post-merge job failures (configure-repo, badges, notify-failure) +- Allow release bot to push to protected master diff --git a/cliff.toml b/cliff.toml index 9dbf0de..97e1b9f 100644 --- a/cliff.toml +++ b/cliff.toml @@ -39,8 +39,8 @@ sort_commits = "oldest" recurse_submodules = false commit_preprocessors = [ - # Strip DEVX-N task ID prefix from merge commits so git-cliff sees conventional commits - { pattern = "^DEVX-\\d+\\s+", replace = "" }, + # Strip DEVX-N: task ID prefix from squash-merge commits so git-cliff sees conventional commits + { pattern = "^DEVX-\\d+:\\s+", replace = "" }, ] commit_parsers = [ @@ -66,3 +66,6 @@ commit_parsers = [ features_always_bump_minor = true breaking_always_bump_major = false initial_tag = "0.1.0" +# Refactor commits bump patch — structural changes to src/ or pyproject.toml +# affect users even though no new feature was added. +refactor_always_bump_patch = true diff --git a/src/devx/__init__.py b/src/devx/__init__.py index 363147a..37b06f8 100644 --- a/src/devx/__init__.py +++ b/src/devx/__init__.py @@ -1,3 +1,3 @@ """devx — reusable development and CI/CD tools for oblachno-oss projects.""" -__version__ = "0.1.0" +__version__ = "0.4.0" diff --git a/src/devx/ci/release.py b/src/devx/ci/release.py index bef6c70..d6917f9 100644 --- a/src/devx/ci/release.py +++ b/src/devx/ci/release.py @@ -271,16 +271,34 @@ def main(dry_run: bool, skip_tests: bool) -> None: ) ) - # Release lock: if HEAD is already a release commit, another release - # run is in progress (or already completed). Skip to prevent duplicate tags. + # Release lock: if HEAD is already a release commit, check if the tag + # exists. If the tag is missing (e.g., tag push failed in a previous run), + # create and push it instead of skipping — this recovers from the + # common failure mode where the commit was pushed but the tag was not. head_msg = run_cmd(["git", "log", "-1", "--pretty=%s"]).stdout.strip() - if re.match(r"^release: v\d+\.\d+\.\d+", head_msg): + release_match = re.match(r"^release: v(\d+\.\d+\.\d+)", head_msg) + if release_match: + release_version = release_match.group(1) + release_tag = f"v{release_version}" + if tag_exists(release_tag): + click.echo( + _( + "HEAD is already a release commit ('{msg}') and tag {tag} exists. Skipping.", + msg=head_msg, + tag=release_tag, + ) + ) + return + # Tag is missing — recover by creating and pushing it click.echo( _( - "HEAD is already a release commit ('{msg}'). Another release may have just completed. Skipping.", + "HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag.", msg=head_msg, + tag=release_tag, ) ) + changelog = get_changelog(release_version) + create_and_push_tag(release_version, changelog, dry_run) return # Check if any user-facing files changed since the last tag. diff --git a/src/devx/translations.json b/src/devx/translations.json index 6d04cd6..c8a62bd 100644 --- a/src/devx/translations.json +++ b/src/devx/translations.json @@ -651,5 +651,11 @@ }, "Unknown check category '{check}'. Available: all, user-facing{tags}": { "en": "Unknown check category '{check}'. Available: all, user-facing{tags}" + }, + "HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag.": { + "en": "HEAD is a release commit ('{msg}') but tag {tag} is missing. Recovering by creating tag." + }, + "HEAD is already a release commit ('{msg}') and tag {tag} exists. Skipping.": { + "en": "HEAD is already a release commit ('{msg}') and tag {tag} exists. Skipping." } } diff --git a/tests/unit/test_release.py b/tests/unit/test_release.py index 4debdeb..c15a9d5 100644 --- a/tests/unit/test_release.py +++ b/tests/unit/test_release.py @@ -318,12 +318,15 @@ class TestMain: @patch.dict("os.environ", {}) @patch("devx.ci.release.has_user_facing_changes", return_value=True) @patch("devx.ci.release.run_cmd") - def test_release_lock_skips_when_head_is_release_commit(self, mock_run_cmd: MagicMock, mock_uf: MagicMock) -> None: - """If HEAD is already a release commit, should skip to prevent duplicate releases.""" - # First call: git rev-parse (master), second: git log -1 (release commit) + def test_release_lock_skips_when_head_is_release_commit_and_tag_exists( + self, mock_run_cmd: MagicMock, mock_uf: MagicMock + ) -> None: + """If HEAD is a release commit and the tag exists, skip.""" + # git rev-parse, git log -1, git tag -l (tag exists) mock_run_cmd.side_effect = [ MagicMock(returncode=0, stdout="master\n", stderr=""), MagicMock(returncode=0, stdout="release: v0.5.0\n", stderr=""), + MagicMock(returncode=0, stdout="v0.5.0\n", stderr=""), # tag -l finds tag ] runner = CliRunner() result = runner.invoke(main, []) @@ -331,6 +334,27 @@ class TestMain: assert "already a release commit" in result.output assert "Skipping" in result.output + @patch.dict("os.environ", {}) + @patch("devx.ci.release.get_changelog", return_value="## changelog") + @patch("devx.ci.release.create_and_push_tag", return_value=True) + @patch("devx.ci.release.run_cmd") + def test_release_lock_recovers_when_tag_missing( + self, mock_run_cmd: MagicMock, mock_create_tag: MagicMock, mock_changelog: MagicMock + ) -> None: + """If HEAD is a release commit but the tag is missing, create the tag.""" + # git rev-parse, git log -1, git tag -l (tag NOT found) + mock_run_cmd.side_effect = [ + MagicMock(returncode=0, stdout="master\n", stderr=""), + MagicMock(returncode=0, stdout="release: v0.5.0\n", stderr=""), + MagicMock(returncode=0, stdout="", stderr=""), # tag -l finds nothing + ] + runner = CliRunner() + result = runner.invoke(main, []) + assert result.exit_code == 0 + assert "tag v0.5.0 is missing" in result.output + assert "Recovering" in result.output + mock_create_tag.assert_called_once_with("0.5.0", "## changelog", False) + @patch.dict("os.environ", {}) @patch("devx.ci.release.has_user_facing_changes", return_value=True) @patch("devx.ci.release.has_unreleased_changes", return_value=False)