Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7fe85423b4 | ||
|
|
76d9983514 | ||
|
|
3dcdde80ad | ||
|
|
2e5ca5a88f | ||
|
|
e7f8e4ac66 |
@@ -12,6 +12,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ secrets.REPO_TOKEN }}
|
token: ${{ secrets.REPO_TOKEN }}
|
||||||
|
- name: Set up environment
|
||||||
|
run: make setup
|
||||||
- name: Install git-cliff
|
- name: Install git-cliff
|
||||||
run: |
|
run: |
|
||||||
GIT_CLIFF_VERSION="2.13.0"
|
GIT_CLIFF_VERSION="2.13.0"
|
||||||
@@ -23,8 +25,6 @@ jobs:
|
|||||||
chmod +x "$HOME/.local/bin/git-cliff"
|
chmod +x "$HOME/.local/bin/git-cliff"
|
||||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||||
"$HOME/.local/bin/git-cliff" --version
|
"$HOME/.local/bin/git-cliff" --version
|
||||||
- name: Install Python dependencies
|
|
||||||
run: python3 -m pip install --break-system-packages requests python-dotenv click
|
|
||||||
- name: Configure git
|
- name: Configure git
|
||||||
run: |
|
run: |
|
||||||
git config user.name "grm-ci-bot"
|
git config user.name "grm-ci-bot"
|
||||||
@@ -33,6 +33,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
PYTHONPATH: src
|
PYTHONPATH: src
|
||||||
run: |
|
run: |
|
||||||
|
. .venv/bin/activate
|
||||||
python3 scripts/release.py
|
python3 scripts/release.py
|
||||||
- name: Notify on failure
|
- name: Notify on failure
|
||||||
if: failure()
|
if: failure()
|
||||||
@@ -40,6 +41,7 @@ jobs:
|
|||||||
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
||||||
PYTHONPATH: src
|
PYTHONPATH: src
|
||||||
run: |
|
run: |
|
||||||
|
. .venv/bin/activate
|
||||||
python3 scripts/notify_failure.py \
|
python3 scripts/notify_failure.py \
|
||||||
--repo "${{ github.repository }}" \
|
--repo "${{ github.repository }}" \
|
||||||
--run-id "${{ github.run_id }}" \
|
--run-id "${{ github.run_id }}" \
|
||||||
|
|||||||
@@ -124,13 +124,17 @@ After a PR is merged to master, the release pipeline runs automatically:
|
|||||||
|
|
||||||
1. **Release workflow** (`.gitea/workflows/release.yml`):
|
1. **Release workflow** (`.gitea/workflows/release.yml`):
|
||||||
- Triggers on push to master
|
- Triggers on push to master
|
||||||
|
- Sets up full dev environment (`make setup`) so lint and tests can run
|
||||||
- Runs `scripts/release.py` which uses **git-cliff** to:
|
- Runs `scripts/release.py` which uses **git-cliff** to:
|
||||||
- Calculate the next semver version from conventional commits since the last tag
|
- Calculate the next semver version from conventional commits since the last tag
|
||||||
- Update `__version__` in `src/gitea_runner_manager/__init__.py` (single source of truth)
|
- Update `__version__` in `src/gitea_runner_manager/__init__.py` (single source of truth)
|
||||||
- Update `CHANGELOG.md` with the new version section
|
- Update `CHANGELOG.md` with the new version section
|
||||||
|
- **Run `make lint-ruff` and `make pytest-cov`** to verify the release is healthy
|
||||||
|
- If lint or tests fail, **abort immediately** — no commit, no tag
|
||||||
- Commit with `release: vX.Y.Z` prefix (cleaner than `chore(release):`)
|
- Commit with `release: vX.Y.Z` prefix (cleaner than `chore(release):`)
|
||||||
- Create an annotated tag `vX.Y.Z` on the release commit
|
- Create an annotated tag `vX.Y.Z` on the release commit
|
||||||
- Push both the commit and tag to master
|
- Push both the commit and tag to master
|
||||||
|
- `--skip-tests` flag bypasses test verification (emergency use only, not recommended)
|
||||||
- Loops are prevented by `has_unreleased_changes` — after a release commit is tagged, the next run finds no unreleased changes and exits
|
- Loops are prevented by `has_unreleased_changes` — after a release commit is tagged, the next run finds no unreleased changes and exits
|
||||||
- On failure, creates a Gitea issue via `scripts/notify_failure.py`
|
- On failure, creates a Gitea issue via `scripts/notify_failure.py`
|
||||||
|
|
||||||
|
|||||||
+11
-2
@@ -2,9 +2,18 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
# Changelog
|
## [0.2.2] - 2026-06-21
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Enforce tests pass before tagging a release
|
||||||
|
- Bypass commit-msg hook for release commits
|
||||||
|
|
||||||
|
## [0.2.1] - 2026-06-21
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Strip git-cliff header from CHANGELOG.md updates
|
||||||
|
|
||||||
## [0.2.0] - 2026-06-21
|
## [0.2.0] - 2026-06-21
|
||||||
|
|
||||||
|
|||||||
+62
-6
@@ -8,6 +8,12 @@ source of truth, read by setuptools via ``dynamic = ["version"]``) and
|
|||||||
with the changelog as the tag message, and pushes both to trigger the publish
|
with the changelog as the tag message, and pushes both to trigger the publish
|
||||||
workflow.
|
workflow.
|
||||||
|
|
||||||
|
**Test enforcement**: Before committing or tagging, the script runs
|
||||||
|
``make lint-ruff`` and ``make pytest-cov`` to verify the release is healthy.
|
||||||
|
If either fails, the release is aborted — no commit, no tag. This ensures
|
||||||
|
we never release a version that fails tests. Use ``--skip-tests`` only for
|
||||||
|
emergency releases (not recommended).
|
||||||
|
|
||||||
The ``release:`` prefix (instead of ``chore(release):``) keeps the history
|
The ``release:`` prefix (instead of ``chore(release):``) keeps the history
|
||||||
clean while still being descriptive. Loops are prevented by the
|
clean while still being descriptive. Loops are prevented by the
|
||||||
``has_unreleased_changes`` check — after a release commit is tagged, the next
|
``has_unreleased_changes`` check — after a release commit is tagged, the next
|
||||||
@@ -18,7 +24,7 @@ last tag, it exits with a message and does nothing. If the tag already exists
|
|||||||
(e.g., from a partial previous run), it skips tag creation and only pushes.
|
(e.g., from a partial previous run), it skips tag creation and only pushes.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
REPO_TOKEN=<token> python3 scripts/release.py [--dry-run]
|
REPO_TOKEN=<token> python3 scripts/release.py [--dry-run] [--skip-tests]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -138,10 +144,15 @@ def update_init_version(new_version: str) -> None:
|
|||||||
def update_changelog(changelog: str) -> None:
|
def update_changelog(changelog: str) -> None:
|
||||||
"""Prepend the new changelog section to CHANGELOG.md.
|
"""Prepend the new changelog section to CHANGELOG.md.
|
||||||
|
|
||||||
If the file doesn't exist, create it with the changelog as the sole content.
|
The changelog from git-cliff may include a header (e.g., "# Changelog").
|
||||||
If it exists, insert the new version section after the header (before the
|
This function strips everything before the first ``## [`` version section
|
||||||
first existing version section).
|
before inserting, to avoid duplicating the header.
|
||||||
"""
|
"""
|
||||||
|
# Strip git-cliff header — keep only from the first version section
|
||||||
|
section_match = re.search(r"^## \[", changelog, flags=re.MULTILINE)
|
||||||
|
if section_match:
|
||||||
|
changelog = changelog[section_match.start() :]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(CHANGELOG_FILE) as f:
|
with open(CHANGELOG_FILE) as f:
|
||||||
existing = f.read()
|
existing = f.read()
|
||||||
@@ -167,6 +178,9 @@ def commit_release_changes(new_version: str) -> bool:
|
|||||||
"""Stage version file and changelog, then create a release commit.
|
"""Stage version file and changelog, then create a release commit.
|
||||||
|
|
||||||
Uses ``release:`` prefix (not ``chore(release):``) for clarity.
|
Uses ``release:`` prefix (not ``chore(release):``) for clarity.
|
||||||
|
The commit is created with ``--no-verify`` to bypass the commit-msg hook
|
||||||
|
(which requires ``GRM-N:`` prefix for master commits) since release
|
||||||
|
commits are a special case generated by the release script.
|
||||||
Returns True if a commit was created, False if there were no staged changes.
|
Returns True if a commit was created, False if there were no staged changes.
|
||||||
"""
|
"""
|
||||||
run_cmd(["git", "add", INIT_FILE, CHANGELOG_FILE])
|
run_cmd(["git", "add", INIT_FILE, CHANGELOG_FILE])
|
||||||
@@ -174,10 +188,39 @@ def commit_release_changes(new_version: str) -> bool:
|
|||||||
if status.returncode == 0:
|
if status.returncode == 0:
|
||||||
click.echo(_("No staged changes — version and changelog already up to date."))
|
click.echo(_("No staged changes — version and changelog already up to date."))
|
||||||
return False
|
return False
|
||||||
run_cmd(["git", "commit", "-m", f"release: v{new_version}"])
|
run_cmd(["git", "commit", "--no-verify", "-m", f"release: v{new_version}"])
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def run_tests() -> None:
|
||||||
|
"""Run lint and tests to verify the release is healthy.
|
||||||
|
|
||||||
|
This is called *after* version files are updated but *before* the tag is
|
||||||
|
created, ensuring we never tag a release that fails tests.
|
||||||
|
"""
|
||||||
|
click.echo(_("Running lint checks..."))
|
||||||
|
lint = run_cmd(["make", "lint-ruff"], check=False)
|
||||||
|
if lint.returncode != 0:
|
||||||
|
raise click.ClickException(
|
||||||
|
_(
|
||||||
|
"Lint failed — refusing to release. Fix lint errors first.\n{stderr}",
|
||||||
|
stderr=lint.stderr.strip() if lint.stderr else lint.stdout.strip(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
click.echo(_("Lint passed."))
|
||||||
|
|
||||||
|
click.echo(_("Running tests..."))
|
||||||
|
tests = run_cmd(["make", "pytest-cov"], check=False)
|
||||||
|
if tests.returncode != 0:
|
||||||
|
raise click.ClickException(
|
||||||
|
_(
|
||||||
|
"Tests failed — refusing to release. Fix test failures first.\n{stderr}",
|
||||||
|
stderr=tests.stderr.strip() if tests.stderr else tests.stdout.strip(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
click.echo(_("Tests passed."))
|
||||||
|
|
||||||
|
|
||||||
def create_and_push_tag(new_version: str, changelog: str, dry_run: bool) -> bool:
|
def create_and_push_tag(new_version: str, changelog: str, dry_run: bool) -> bool:
|
||||||
"""Create an annotated tag with the changelog as message and push it.
|
"""Create an annotated tag with the changelog as message and push it.
|
||||||
|
|
||||||
@@ -201,7 +244,13 @@ def create_and_push_tag(new_version: str, changelog: str, dry_run: bool) -> bool
|
|||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.option("--dry-run", is_flag=True, default=False, help="Show what would happen without making changes.")
|
@click.option("--dry-run", is_flag=True, default=False, help="Show what would happen without making changes.")
|
||||||
def main(dry_run: bool) -> None:
|
@click.option(
|
||||||
|
"--skip-tests",
|
||||||
|
is_flag=True,
|
||||||
|
default=False,
|
||||||
|
help="Skip lint and test verification (NOT recommended — only for emergency releases).",
|
||||||
|
)
|
||||||
|
def main(dry_run: bool, skip_tests: bool) -> None:
|
||||||
# Ensure we're on master
|
# Ensure we're on master
|
||||||
branch = run_cmd(["git", "rev-parse", "--abbrev-ref", "HEAD"]).stdout.strip()
|
branch = run_cmd(["git", "rev-parse", "--abbrev-ref", "HEAD"]).stdout.strip()
|
||||||
if branch != "master":
|
if branch != "master":
|
||||||
@@ -246,6 +295,13 @@ def main(dry_run: bool) -> None:
|
|||||||
update_changelog(changelog)
|
update_changelog(changelog)
|
||||||
click.echo(_("Updated {changelog_file}", changelog_file=CHANGELOG_FILE))
|
click.echo(_("Updated {changelog_file}", changelog_file=CHANGELOG_FILE))
|
||||||
|
|
||||||
|
# Verify tests pass BEFORE committing or tagging.
|
||||||
|
# This ensures we never release a version that fails tests.
|
||||||
|
if skip_tests:
|
||||||
|
click.echo(_("WARNING: --skip-tests passed — skipping test verification."))
|
||||||
|
else:
|
||||||
|
run_tests()
|
||||||
|
|
||||||
# Commit version + changelog (Gap 11: use 'release:' prefix, not 'chore(release):')
|
# Commit version + changelog (Gap 11: use 'release:' prefix, not 'chore(release):')
|
||||||
committed = commit_release_changes(new_version)
|
committed = commit_release_changes(new_version)
|
||||||
if committed:
|
if committed:
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
"""Gitea Runner Manager — lean CLI for managing Gitea Actions runners."""
|
"""Gitea Runner Manager — lean CLI for managing Gitea Actions runners."""
|
||||||
|
|
||||||
__version__ = "0.2.0"
|
__version__ = "0.2.2"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from unittest.mock import MagicMock, patch
|
|||||||
|
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
|
|
||||||
|
from gitea_runner_manager import __version__
|
||||||
from gitea_runner_manager.cli import cli
|
from gitea_runner_manager.cli import cli
|
||||||
|
|
||||||
|
|
||||||
@@ -12,7 +13,7 @@ class TestCLI:
|
|||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
result = runner.invoke(cli, ["--version"])
|
result = runner.invoke(cli, ["--version"])
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert "0.1.0" in result.output
|
assert __version__ in result.output
|
||||||
|
|
||||||
@patch("gitea_runner_manager.cli.RunnerManager")
|
@patch("gitea_runner_manager.cli.RunnerManager")
|
||||||
def test_install(self, mock_manager_class: MagicMock) -> None:
|
def test_install(self, mock_manager_class: MagicMock) -> None:
|
||||||
|
|||||||
+157
-2
@@ -15,6 +15,7 @@ from scripts.release import (
|
|||||||
has_unreleased_changes,
|
has_unreleased_changes,
|
||||||
main,
|
main,
|
||||||
run_cmd,
|
run_cmd,
|
||||||
|
run_tests,
|
||||||
tag_exists,
|
tag_exists,
|
||||||
update_changelog,
|
update_changelog,
|
||||||
update_init_version,
|
update_init_version,
|
||||||
@@ -186,6 +187,30 @@ class TestUpdateChangelog:
|
|||||||
assert "Some intro text" in content
|
assert "Some intro text" in content
|
||||||
assert "## [0.2.0]" in content
|
assert "## [0.2.0]" in content
|
||||||
|
|
||||||
|
def test_strips_git_cliff_header(self, tmp_path, monkeypatch) -> None:
|
||||||
|
"""git-cliff output includes a header — should be stripped before inserting."""
|
||||||
|
changelog_file = tmp_path / "CHANGELOG.md"
|
||||||
|
changelog_file.write_text("# Changelog\n\n## [0.1.0] - 2026-06-20\n\n### Features\n- old thing\n")
|
||||||
|
monkeypatch.setattr("scripts.release.CHANGELOG_FILE", str(changelog_file))
|
||||||
|
# Simulate git-cliff output with header
|
||||||
|
cliff_output = "# Changelog\n\nAll notable changes...\n\n## [0.2.0] - 2026-06-21\n\n### Features\n- new thing"
|
||||||
|
update_changelog(cliff_output)
|
||||||
|
content = changelog_file.read_text()
|
||||||
|
# Header should appear only once (from the existing file)
|
||||||
|
assert content.count("# Changelog") == 1
|
||||||
|
assert "## [0.2.0]" in content
|
||||||
|
assert "new thing" in content
|
||||||
|
|
||||||
|
def test_strips_header_when_creating_new_file(self, tmp_path, monkeypatch) -> None:
|
||||||
|
"""When creating a new file, strip the git-cliff header."""
|
||||||
|
changelog_file = tmp_path / "CHANGELOG.md"
|
||||||
|
monkeypatch.setattr("scripts.release.CHANGELOG_FILE", str(changelog_file))
|
||||||
|
cliff_output = "# Changelog\n\nAll notable changes...\n\n## [0.2.0] - 2026-06-21\n\n### Features\n- new thing"
|
||||||
|
update_changelog(cliff_output)
|
||||||
|
content = changelog_file.read_text()
|
||||||
|
assert "# Changelog" not in content
|
||||||
|
assert "## [0.2.0]" in content
|
||||||
|
|
||||||
|
|
||||||
class TestCommitReleaseChanges:
|
class TestCommitReleaseChanges:
|
||||||
@patch("scripts.release.run_cmd")
|
@patch("scripts.release.run_cmd")
|
||||||
@@ -196,7 +221,7 @@ class TestCommitReleaseChanges:
|
|||||||
assert result is True
|
assert result is True
|
||||||
calls = [c.args[0] for c in mock_run_cmd.call_args_list]
|
calls = [c.args[0] for c in mock_run_cmd.call_args_list]
|
||||||
assert ["git", "add", "src/gitea_runner_manager/__init__.py", "CHANGELOG.md"] in calls
|
assert ["git", "add", "src/gitea_runner_manager/__init__.py", "CHANGELOG.md"] in calls
|
||||||
assert ["git", "commit", "-m", "release: v0.2.0"] in calls
|
assert ["git", "commit", "--no-verify", "-m", "release: v0.2.0"] in calls
|
||||||
|
|
||||||
@patch("scripts.release.run_cmd")
|
@patch("scripts.release.run_cmd")
|
||||||
def test_skips_when_no_changes(self, mock_run_cmd: MagicMock) -> None:
|
def test_skips_when_no_changes(self, mock_run_cmd: MagicMock) -> None:
|
||||||
@@ -205,7 +230,7 @@ class TestCommitReleaseChanges:
|
|||||||
result = commit_release_changes("0.1.0")
|
result = commit_release_changes("0.1.0")
|
||||||
assert result is False
|
assert result is False
|
||||||
calls = [c.args[0] for c in mock_run_cmd.call_args_list]
|
calls = [c.args[0] for c in mock_run_cmd.call_args_list]
|
||||||
assert ["git", "commit", "-m", "release: v0.1.0"] not in calls
|
assert ["git", "commit", "--no-verify", "-m", "release: v0.1.0"] not in calls
|
||||||
|
|
||||||
|
|
||||||
class TestCreateAndPushTag:
|
class TestCreateAndPushTag:
|
||||||
@@ -245,6 +270,28 @@ class TestCreateAndPushTag:
|
|||||||
mock_run_cmd.assert_not_called()
|
mock_run_cmd.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
class TestRunTests:
|
||||||
|
@patch("scripts.release.run_cmd")
|
||||||
|
def test_lint_and_tests_pass(self, mock_run_cmd: MagicMock) -> None:
|
||||||
|
mock_run_cmd.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
||||||
|
run_tests() # should not raise
|
||||||
|
|
||||||
|
@patch("scripts.release.run_cmd")
|
||||||
|
def test_lint_fails_raises(self, mock_run_cmd: MagicMock) -> None:
|
||||||
|
mock_run_cmd.return_value = MagicMock(returncode=1, stdout="", stderr="lint error")
|
||||||
|
with pytest.raises(click.ClickException, match="Lint failed"):
|
||||||
|
run_tests()
|
||||||
|
|
||||||
|
@patch("scripts.release.run_cmd")
|
||||||
|
def test_tests_fail_raises(self, mock_run_cmd: MagicMock) -> None:
|
||||||
|
mock_run_cmd.side_effect = [
|
||||||
|
MagicMock(returncode=0, stdout="", stderr=""), # lint passes
|
||||||
|
MagicMock(returncode=1, stdout="", stderr="test failure"), # tests fail
|
||||||
|
]
|
||||||
|
with pytest.raises(click.ClickException, match="Tests failed"):
|
||||||
|
run_tests()
|
||||||
|
|
||||||
|
|
||||||
class TestMain:
|
class TestMain:
|
||||||
@patch.dict("os.environ", {})
|
@patch.dict("os.environ", {})
|
||||||
@patch("scripts.release.run_cmd")
|
@patch("scripts.release.run_cmd")
|
||||||
@@ -327,6 +374,7 @@ class TestMain:
|
|||||||
mock_tag.assert_not_called()
|
mock_tag.assert_not_called()
|
||||||
|
|
||||||
@patch.dict("os.environ", {})
|
@patch.dict("os.environ", {})
|
||||||
|
@patch("scripts.release.run_tests")
|
||||||
@patch("scripts.release.create_and_push_tag", return_value=True)
|
@patch("scripts.release.create_and_push_tag", return_value=True)
|
||||||
@patch("scripts.release.commit_release_changes", return_value=True)
|
@patch("scripts.release.commit_release_changes", return_value=True)
|
||||||
@patch("scripts.release.update_changelog")
|
@patch("scripts.release.update_changelog")
|
||||||
@@ -347,6 +395,7 @@ class TestMain:
|
|||||||
mock_update_changelog: MagicMock,
|
mock_update_changelog: MagicMock,
|
||||||
mock_commit: MagicMock,
|
mock_commit: MagicMock,
|
||||||
mock_tag: MagicMock,
|
mock_tag: MagicMock,
|
||||||
|
mock_run_tests: MagicMock,
|
||||||
) -> None:
|
) -> None:
|
||||||
mock_run_cmd.return_value = MagicMock(returncode=0, stdout="master\n", stderr="")
|
mock_run_cmd.return_value = MagicMock(returncode=0, stdout="master\n", stderr="")
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
@@ -355,10 +404,12 @@ class TestMain:
|
|||||||
assert "Bumping version" in result.output
|
assert "Bumping version" in result.output
|
||||||
mock_update_init.assert_called_once_with("0.2.0")
|
mock_update_init.assert_called_once_with("0.2.0")
|
||||||
mock_update_changelog.assert_called_once_with("changelog")
|
mock_update_changelog.assert_called_once_with("changelog")
|
||||||
|
mock_run_tests.assert_called_once()
|
||||||
mock_commit.assert_called_once_with("0.2.0")
|
mock_commit.assert_called_once_with("0.2.0")
|
||||||
mock_tag.assert_called_once_with("0.2.0", "changelog", False)
|
mock_tag.assert_called_once_with("0.2.0", "changelog", False)
|
||||||
|
|
||||||
@patch.dict("os.environ", {})
|
@patch.dict("os.environ", {})
|
||||||
|
@patch("scripts.release.run_tests")
|
||||||
@patch("scripts.release.create_and_push_tag", return_value=False)
|
@patch("scripts.release.create_and_push_tag", return_value=False)
|
||||||
@patch("scripts.release.commit_release_changes", return_value=False)
|
@patch("scripts.release.commit_release_changes", return_value=False)
|
||||||
@patch("scripts.release.update_changelog")
|
@patch("scripts.release.update_changelog")
|
||||||
@@ -379,6 +430,7 @@ class TestMain:
|
|||||||
mock_update_changelog: MagicMock,
|
mock_update_changelog: MagicMock,
|
||||||
mock_commit: MagicMock,
|
mock_commit: MagicMock,
|
||||||
mock_tag: MagicMock,
|
mock_tag: MagicMock,
|
||||||
|
mock_run_tests: MagicMock,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""When tag already exists, still update files but report existing tag."""
|
"""When tag already exists, still update files but report existing tag."""
|
||||||
mock_run_cmd.return_value = MagicMock(returncode=0, stdout="master\n", stderr="")
|
mock_run_cmd.return_value = MagicMock(returncode=0, stdout="master\n", stderr="")
|
||||||
@@ -387,3 +439,106 @@ class TestMain:
|
|||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert "already existed" in result.output
|
assert "already existed" in result.output
|
||||||
mock_tag.assert_called_once_with("0.1.0", "changelog", False)
|
mock_tag.assert_called_once_with("0.1.0", "changelog", False)
|
||||||
|
|
||||||
|
@patch.dict("os.environ", {})
|
||||||
|
@patch("scripts.release.create_and_push_tag", return_value=True)
|
||||||
|
@patch("scripts.release.commit_release_changes", return_value=True)
|
||||||
|
@patch("scripts.release.update_changelog")
|
||||||
|
@patch("scripts.release.update_init_version")
|
||||||
|
@patch("scripts.release.get_changelog", return_value="changelog")
|
||||||
|
@patch("scripts.release.get_latest_tag", return_value="v0.1.0")
|
||||||
|
@patch("scripts.release.get_bumped_version", return_value="0.2.0")
|
||||||
|
@patch("scripts.release.has_unreleased_changes", return_value=True)
|
||||||
|
@patch("scripts.release.run_cmd")
|
||||||
|
def test_full_flow_skip_tests(
|
||||||
|
self,
|
||||||
|
mock_run_cmd: MagicMock,
|
||||||
|
mock_has: MagicMock,
|
||||||
|
mock_bumped: MagicMock,
|
||||||
|
mock_latest: MagicMock,
|
||||||
|
mock_changelog: MagicMock,
|
||||||
|
mock_update_init: MagicMock,
|
||||||
|
mock_update_changelog: MagicMock,
|
||||||
|
mock_commit: MagicMock,
|
||||||
|
mock_tag: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""--skip-tests bypasses test verification."""
|
||||||
|
mock_run_cmd.return_value = MagicMock(returncode=0, stdout="master\n", stderr="")
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(main, ["--skip-tests"])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert "WARNING: --skip-tests" in result.output
|
||||||
|
# run_tests should NOT be called — verify no "make lint-ruff" or "make pytest-cov" calls
|
||||||
|
make_calls = [c.args[0] for c in mock_run_cmd.call_args_list if c.args[0][:1] == ["make"]]
|
||||||
|
assert make_calls == []
|
||||||
|
|
||||||
|
@patch.dict("os.environ", {})
|
||||||
|
@patch("scripts.release.create_and_push_tag")
|
||||||
|
@patch("scripts.release.commit_release_changes")
|
||||||
|
@patch("scripts.release.update_changelog")
|
||||||
|
@patch("scripts.release.update_init_version")
|
||||||
|
@patch("scripts.release.get_changelog", return_value="changelog")
|
||||||
|
@patch("scripts.release.get_latest_tag", return_value="v0.1.0")
|
||||||
|
@patch("scripts.release.get_bumped_version", return_value="0.2.0")
|
||||||
|
@patch("scripts.release.has_unreleased_changes", return_value=True)
|
||||||
|
@patch("scripts.release.run_cmd")
|
||||||
|
def test_tests_fail_aborts_before_tag(
|
||||||
|
self,
|
||||||
|
mock_run_cmd: MagicMock,
|
||||||
|
mock_has: MagicMock,
|
||||||
|
mock_bumped: MagicMock,
|
||||||
|
mock_latest: MagicMock,
|
||||||
|
mock_changelog: MagicMock,
|
||||||
|
mock_update_init: MagicMock,
|
||||||
|
mock_update_changelog: MagicMock,
|
||||||
|
mock_commit: MagicMock,
|
||||||
|
mock_tag: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""If tests fail, release aborts — no commit, no tag."""
|
||||||
|
# First call: git rev-parse (master), then make lint-ruff (success),
|
||||||
|
# then make pytest-cov (failure)
|
||||||
|
mock_run_cmd.side_effect = [
|
||||||
|
MagicMock(returncode=0, stdout="master\n", stderr=""),
|
||||||
|
MagicMock(returncode=0, stdout="", stderr=""),
|
||||||
|
MagicMock(returncode=1, stdout="", stderr="test failure"),
|
||||||
|
]
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(main, [])
|
||||||
|
assert result.exit_code != 0
|
||||||
|
assert "Tests failed" in result.output
|
||||||
|
mock_commit.assert_not_called()
|
||||||
|
mock_tag.assert_not_called()
|
||||||
|
|
||||||
|
@patch.dict("os.environ", {})
|
||||||
|
@patch("scripts.release.create_and_push_tag")
|
||||||
|
@patch("scripts.release.commit_release_changes")
|
||||||
|
@patch("scripts.release.update_changelog")
|
||||||
|
@patch("scripts.release.update_init_version")
|
||||||
|
@patch("scripts.release.get_changelog", return_value="changelog")
|
||||||
|
@patch("scripts.release.get_latest_tag", return_value="v0.1.0")
|
||||||
|
@patch("scripts.release.get_bumped_version", return_value="0.2.0")
|
||||||
|
@patch("scripts.release.has_unreleased_changes", return_value=True)
|
||||||
|
@patch("scripts.release.run_cmd")
|
||||||
|
def test_lint_fail_aborts_before_tag(
|
||||||
|
self,
|
||||||
|
mock_run_cmd: MagicMock,
|
||||||
|
mock_has: MagicMock,
|
||||||
|
mock_bumped: MagicMock,
|
||||||
|
mock_latest: MagicMock,
|
||||||
|
mock_changelog: MagicMock,
|
||||||
|
mock_update_init: MagicMock,
|
||||||
|
mock_update_changelog: MagicMock,
|
||||||
|
mock_commit: MagicMock,
|
||||||
|
mock_tag: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""If lint fails, release aborts — no commit, no tag."""
|
||||||
|
mock_run_cmd.side_effect = [
|
||||||
|
MagicMock(returncode=0, stdout="master\n", stderr=""),
|
||||||
|
MagicMock(returncode=1, stdout="", stderr="lint error"),
|
||||||
|
]
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(main, [])
|
||||||
|
assert result.exit_code != 0
|
||||||
|
assert "Lint failed" in result.output
|
||||||
|
mock_commit.assert_not_called()
|
||||||
|
mock_tag.assert_not_called()
|
||||||
|
|||||||
Reference in New Issue
Block a user