Public Access
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5081e10b1 | ||
|
|
cabc0d1adc | ||
|
|
7c1ecd6ff9 | ||
|
|
5063f659bc | ||
|
|
96c77a0ba4 | ||
|
|
40dd578d89 | ||
|
|
7ea9b4a96b | ||
|
|
08ceaf484f | ||
|
|
fb6b0fda1d |
@@ -1,13 +1,13 @@
|
||||
name: Post-merge
|
||||
|
||||
# Runs on every push to master. A single workflow with conditional jobs
|
||||
# replaces separate workflows for release, wiki sync, badges, and
|
||||
# Vikunja task updates.
|
||||
# for release, publish, wiki sync, badges, and Vikunja task updates.
|
||||
#
|
||||
# Job dependency graph:
|
||||
#
|
||||
# detect-type ──┬── validate-commit-msg (skip if release commit)
|
||||
# ├── release (skip if release commit)
|
||||
# │ └── publish (needs release — builds & publishes to PyPI)
|
||||
# ├── badges (ALWAYS runs — even on release commits)
|
||||
# ├── configure-repo (independent — skip if release commit)
|
||||
# ├── sync-wiki (skip if release commit — runs for ALL merges)
|
||||
@@ -21,9 +21,10 @@ name: Post-merge
|
||||
# runs on every push to master, including release commits. This ensures
|
||||
# badges (tests, coverage, version, etc.) are always current.
|
||||
#
|
||||
# When release creates a "release: vX.Y.Z" commit, the release
|
||||
# commit's post-merge run still updates badges (version badge picks
|
||||
# up the new version). Other jobs skip. The tag push triggers publish.yml.
|
||||
# When release creates a "release: vX.Y.Z" commit and tag, the publish
|
||||
# job (which depends on release) builds and publishes the package to the
|
||||
# Gitea PyPI registry. The release commit's post-merge run still updates
|
||||
# badges (version badge picks up the new version). Other jobs skip.
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -74,6 +75,8 @@ jobs:
|
||||
if: needs.detect-type.outputs.is-release == 'false'
|
||||
runs-on: docker
|
||||
timeout-minutes: 15
|
||||
outputs:
|
||||
tag: ${{ steps.release-tag.outputs.tag }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -88,12 +91,20 @@ jobs:
|
||||
git config user.name "devx-ci-bot"
|
||||
git config user.email "devx-ci-bot@oblachno.fyi"
|
||||
- name: Run release
|
||||
id: release-tag
|
||||
env:
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
python3 -m devx.ci.release
|
||||
- name: Extract tag (fallback if GITHUB_OUTPUT not set)
|
||||
if: steps.release-tag.outputs.tag == ''
|
||||
run: |
|
||||
tag=$(git describe --tags --abbrev=0 2>/dev/null || true)
|
||||
if [ -n "$tag" ]; then
|
||||
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
- name: Notify on failure
|
||||
if: failure()
|
||||
env:
|
||||
@@ -108,6 +119,41 @@ jobs:
|
||||
--workflow "post-merge/release" \
|
||||
--commit "${{ github.sha }}"
|
||||
|
||||
publish:
|
||||
needs: [release]
|
||||
if: needs.release.outputs.tag != ''
|
||||
runs-on: docker
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up environment
|
||||
env:
|
||||
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
||||
run: make setup-release
|
||||
- name: Build and publish release
|
||||
env:
|
||||
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
python3 -m devx.ci.publish "${{ needs.release.outputs.tag }}" "${{ github.repository }}"
|
||||
- name: Notify on failure
|
||||
if: failure()
|
||||
env:
|
||||
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
python3 -m devx.ci.notify_failure \
|
||||
--repo "${{ github.repository }}" \
|
||||
--run-id "${{ github.run_id }}" \
|
||||
--workflow "post-merge/publish" \
|
||||
--commit "${{ github.sha }}"
|
||||
|
||||
sync-wiki:
|
||||
needs: [detect-type]
|
||||
if: needs.detect-type.outputs.is-release == 'false'
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
name: Publish Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Tag to publish (e.g. v0.9.11)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: docker
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up environment
|
||||
env:
|
||||
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
||||
run: make setup-release
|
||||
- name: Build and publish release
|
||||
env:
|
||||
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
python3 -m devx.ci.publish "${{ github.event.inputs.tag || github.ref_name }}" "${{ github.repository }}"
|
||||
- name: Notify on failure
|
||||
if: failure()
|
||||
env:
|
||||
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
||||
PYTHONPATH: src
|
||||
run: |
|
||||
. .venv/bin/activate 2>/dev/null || true
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
python3 -m devx.ci.notify_failure \
|
||||
--repo "${{ github.repository }}" \
|
||||
--run-id "${{ github.run_id }}" \
|
||||
--workflow "publish" \
|
||||
--commit "${{ github.sha }}"
|
||||
@@ -204,8 +204,9 @@ After a PR is merged to master, the **post-merge workflow**
|
||||
non-release commits (not just when release succeeds), so infrastructure-only
|
||||
changes still update the task tracker.
|
||||
|
||||
The tag push triggers the **publish workflow** (`.gitea/workflows/publish.yml`)
|
||||
which builds and publishes the package to the Gitea PyPI registry.
|
||||
6. **publish** — Runs after release succeeds (needs: release). Builds and
|
||||
publishes the package to the Gitea PyPI registry. Gets the tag from the
|
||||
release job's `tag` output (written via `GITHUB_OUTPUT`).
|
||||
|
||||
### Smart CI: User-Facing vs Workflow-Only Changes
|
||||
|
||||
|
||||
@@ -2,6 +2,18 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.19.1] - 2026-06-26
|
||||
|
||||
### Refactor
|
||||
|
||||
- Consolidate publish.yml into post-merge.yml
|
||||
|
||||
## [0.19.0] - 2026-06-26
|
||||
|
||||
### Features
|
||||
|
||||
- Add skip_ref_prefixes config to check_agent_docs
|
||||
|
||||
## [0.18.0] - 2026-06-26
|
||||
|
||||
### 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.18.0"
|
||||
__version__ = "0.19.1"
|
||||
|
||||
@@ -317,6 +317,21 @@ def run_tests() -> None:
|
||||
click.echo(_("Tests passed."))
|
||||
|
||||
|
||||
def _write_github_output(tag: str) -> None:
|
||||
"""Write the release tag to GITHUB_OUTPUT for downstream jobs.
|
||||
|
||||
This allows a publish job (needs: release) to read the tag via
|
||||
``${{ needs.release.outputs.tag }}`` instead of relying on
|
||||
tag-push event triggering a separate workflow.
|
||||
"""
|
||||
github_output = os.environ.get("GITHUB_OUTPUT")
|
||||
if not github_output:
|
||||
return
|
||||
with open(github_output, "a") as f: # noqa: PTH123
|
||||
f.write(f"tag={tag}\n")
|
||||
click.echo(_("Wrote tag {tag} to GITHUB_OUTPUT.", tag=tag))
|
||||
|
||||
|
||||
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.
|
||||
|
||||
@@ -345,6 +360,7 @@ def create_and_push_tag(new_version: str, changelog: str, dry_run: bool) -> bool
|
||||
if not dry_run:
|
||||
# Ensure the existing tag is pushed
|
||||
run_cmd(["git", "push", "origin", f"refs/tags/{tag}"], check=False)
|
||||
_write_github_output(tag)
|
||||
return False
|
||||
tag_msg = f"Release v{new_version}\n\n{changelog}"
|
||||
if dry_run:
|
||||
@@ -352,6 +368,7 @@ def create_and_push_tag(new_version: str, changelog: str, dry_run: bool) -> bool
|
||||
return True
|
||||
run_cmd(["git", "tag", "-a", tag, "-m", tag_msg])
|
||||
run_cmd(["git", "push", "origin", f"refs/tags/{tag}"])
|
||||
_write_github_output(tag)
|
||||
return True
|
||||
|
||||
|
||||
@@ -617,6 +634,7 @@ def main(dry_run: bool, skip_tests: bool, verify: bool) -> None:
|
||||
tag=release_tag,
|
||||
)
|
||||
)
|
||||
_write_github_output(release_tag)
|
||||
return
|
||||
# Tag is missing — recover by creating and pushing it
|
||||
click.echo(
|
||||
|
||||
@@ -117,6 +117,7 @@ def _check_file(
|
||||
legitimate_indicators: list[str],
|
||||
repo_path_prefixes: list[str],
|
||||
min_path_ref_length: int,
|
||||
skip_ref_prefixes: list[str],
|
||||
) -> list[str]:
|
||||
"""Check a single file for stale references."""
|
||||
issues: list[str] = []
|
||||
@@ -147,6 +148,9 @@ def _check_file(
|
||||
# Only check references that look like repo paths
|
||||
if not any(ref.startswith(prefix) for prefix in repo_path_prefixes):
|
||||
continue
|
||||
# Skip references matching configured skip prefixes (e.g. aspirational test files)
|
||||
if any(ref.startswith(prefix) for prefix in skip_ref_prefixes):
|
||||
continue
|
||||
candidate = repo_root / ref
|
||||
if not candidate.exists():
|
||||
issues.append(f"{rel_path}:{lineno}: references non-existent file '{ref}'")
|
||||
@@ -179,6 +183,9 @@ def cli() -> None:
|
||||
min_len_raw = cfg.get("min_path_ref_length")
|
||||
min_path_ref_length: int = int(min_len_raw) if isinstance(min_len_raw, int) else MIN_PATH_REF_LENGTH_DEFAULT
|
||||
|
||||
skip_prefixes_raw = cfg.get("skip_ref_prefixes", [])
|
||||
skip_ref_prefixes: list[str] = [str(d) for d in skip_prefixes_raw] if isinstance(skip_prefixes_raw, list) else []
|
||||
|
||||
deleted_files: set[str] = set()
|
||||
deleted_raw = cfg.get("deleted_files", [])
|
||||
if isinstance(deleted_raw, list):
|
||||
@@ -209,6 +216,7 @@ def cli() -> None:
|
||||
legitimate_indicators,
|
||||
repo_path_prefixes,
|
||||
min_path_ref_length,
|
||||
skip_ref_prefixes,
|
||||
)
|
||||
all_issues.extend(issues)
|
||||
|
||||
|
||||
@@ -1846,5 +1846,13 @@
|
||||
"pl": "[check-dep-docs] Passed: all dependencies are documented",
|
||||
"ru": "[check-dep-docs] Passed: all dependencies are documented",
|
||||
"zh": "[check-dep-docs] Passed: all dependencies are documented"
|
||||
},
|
||||
"Wrote tag {tag} to GITHUB_OUTPUT.": {
|
||||
"bg": "Wrote tag {tag} to GITHUB_OUTPUT.",
|
||||
"de": "Wrote tag {tag} to GITHUB_OUTPUT.",
|
||||
"en": "Wrote tag {tag} to GITHUB_OUTPUT.",
|
||||
"ru": "Wrote tag {tag} to GITHUB_OUTPUT.",
|
||||
"zh": "Wrote tag {tag} to GITHUB_OUTPUT.",
|
||||
"pl": "Wrote tag {tag} to GITHUB_OUTPUT."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ class TestCheckFile:
|
||||
doc.parent.mkdir(parents=True)
|
||||
doc.write_text("See scripts/old.py for details.\n")
|
||||
issues = _check_file(
|
||||
doc, tmp_path, {"scripts/old.py"}, [], [], DEFAULT_REPO_PATH_PREFIXES, MIN_PATH_REF_LENGTH_DEFAULT
|
||||
doc, tmp_path, {"scripts/old.py"}, [], [], DEFAULT_REPO_PATH_PREFIXES, MIN_PATH_REF_LENGTH_DEFAULT, []
|
||||
)
|
||||
assert any("deleted file" in i for i in issues)
|
||||
|
||||
@@ -97,7 +97,7 @@ class TestCheckFile:
|
||||
doc = tmp_path / "docs" / "guide.md"
|
||||
doc.parent.mkdir(parents=True)
|
||||
doc.write_text("See scripts/nonexistent.py for details.\n")
|
||||
issues = _check_file(doc, tmp_path, set(), [], [], DEFAULT_REPO_PATH_PREFIXES, MIN_PATH_REF_LENGTH_DEFAULT)
|
||||
issues = _check_file(doc, tmp_path, set(), [], [], DEFAULT_REPO_PATH_PREFIXES, MIN_PATH_REF_LENGTH_DEFAULT, [])
|
||||
assert any("non-existent file" in i for i in issues)
|
||||
|
||||
def test_does_not_flag_existing_file(self, tmp_path: Path) -> None:
|
||||
@@ -106,7 +106,7 @@ class TestCheckFile:
|
||||
doc = tmp_path / "docs" / "guide.md"
|
||||
doc.parent.mkdir(parents=True)
|
||||
doc.write_text("See scripts/exists.py for details.\n")
|
||||
issues = _check_file(doc, tmp_path, set(), [], [], DEFAULT_REPO_PATH_PREFIXES, MIN_PATH_REF_LENGTH_DEFAULT)
|
||||
issues = _check_file(doc, tmp_path, set(), [], [], DEFAULT_REPO_PATH_PREFIXES, MIN_PATH_REF_LENGTH_DEFAULT, [])
|
||||
assert issues == []
|
||||
|
||||
def test_detects_deprecated_pattern(self, tmp_path: Path) -> None:
|
||||
@@ -115,7 +115,7 @@ class TestCheckFile:
|
||||
doc.write_text("Use ansible/envs/prod/secrets.yml for config.\n")
|
||||
patterns = [re.compile(r"ansible/envs/[^/]+/secrets\.yml")]
|
||||
issues = _check_file(
|
||||
doc, tmp_path, set(), patterns, [], DEFAULT_REPO_PATH_PREFIXES, MIN_PATH_REF_LENGTH_DEFAULT
|
||||
doc, tmp_path, set(), patterns, [], DEFAULT_REPO_PATH_PREFIXES, MIN_PATH_REF_LENGTH_DEFAULT, []
|
||||
)
|
||||
assert any("deprecated pattern" in i for i in issues)
|
||||
|
||||
@@ -129,7 +129,7 @@ class TestCheckFile:
|
||||
doc.write_text("The legacy ansible/envs/prod/secrets.yml is deprecated.\n")
|
||||
patterns = [re.compile(r"ansible/envs/[^/]+/secrets\.yml")]
|
||||
issues = _check_file(
|
||||
doc, tmp_path, set(), patterns, ["deprecated"], DEFAULT_REPO_PATH_PREFIXES, MIN_PATH_REF_LENGTH_DEFAULT
|
||||
doc, tmp_path, set(), patterns, ["deprecated"], DEFAULT_REPO_PATH_PREFIXES, MIN_PATH_REF_LENGTH_DEFAULT, []
|
||||
)
|
||||
assert issues == []
|
||||
|
||||
@@ -137,14 +137,14 @@ class TestCheckFile:
|
||||
doc = tmp_path / "docs" / "guide.md"
|
||||
doc.parent.mkdir(parents=True)
|
||||
doc.write_bytes(b"\xff\xfe\x00\x00")
|
||||
issues = _check_file(doc, tmp_path, set(), [], [], DEFAULT_REPO_PATH_PREFIXES, MIN_PATH_REF_LENGTH_DEFAULT)
|
||||
issues = _check_file(doc, tmp_path, set(), [], [], DEFAULT_REPO_PATH_PREFIXES, MIN_PATH_REF_LENGTH_DEFAULT, [])
|
||||
assert issues == []
|
||||
|
||||
def test_skips_short_ref(self, tmp_path: Path) -> None:
|
||||
doc = tmp_path / "docs" / "guide.md"
|
||||
doc.parent.mkdir(parents=True)
|
||||
doc.write_text("See a.py for details.\n")
|
||||
issues = _check_file(doc, tmp_path, set(), [], [], DEFAULT_REPO_PATH_PREFIXES, 5)
|
||||
issues = _check_file(doc, tmp_path, set(), [], [], DEFAULT_REPO_PATH_PREFIXES, 5, [])
|
||||
# "a.py" is only 4 chars, below min_path_ref_length
|
||||
assert issues == []
|
||||
|
||||
@@ -152,10 +152,19 @@ class TestCheckFile:
|
||||
doc = tmp_path / "docs" / "guide.md"
|
||||
doc.parent.mkdir(parents=True)
|
||||
doc.write_text("See vendor/some/long/path.py for details.\n")
|
||||
issues = _check_file(doc, tmp_path, set(), [], [], DEFAULT_REPO_PATH_PREFIXES, MIN_PATH_REF_LENGTH_DEFAULT)
|
||||
issues = _check_file(doc, tmp_path, set(), [], [], DEFAULT_REPO_PATH_PREFIXES, MIN_PATH_REF_LENGTH_DEFAULT, [])
|
||||
# "vendor/" is not in repo_path_prefixes
|
||||
assert issues == []
|
||||
|
||||
def test_skip_ref_prefixes_skips_nonexistent(self, tmp_path: Path) -> None:
|
||||
doc = tmp_path / "docs" / "guide.md"
|
||||
doc.parent.mkdir(parents=True)
|
||||
doc.write_text("See scripts/test_foo.py for details.\n")
|
||||
issues = _check_file(
|
||||
doc, tmp_path, set(), [], [], DEFAULT_REPO_PATH_PREFIXES, MIN_PATH_REF_LENGTH_DEFAULT, ["scripts/test_"]
|
||||
)
|
||||
assert issues == []
|
||||
|
||||
|
||||
class TestCli:
|
||||
def test_passes_when_no_issues(self, tmp_path: Path) -> None:
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Unit tests for scripts/ci/release.py."""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import click
|
||||
@@ -815,11 +817,23 @@ class TestCommitReleaseChanges:
|
||||
class TestCreateAndPushTag:
|
||||
@patch("devx.ci.release.tag_exists", return_value=False)
|
||||
@patch("devx.ci.release.run_cmd")
|
||||
def test_creates_tag(self, mock_run_cmd: MagicMock, mock_tag_exists: MagicMock) -> None:
|
||||
create_and_push_tag("0.2.0", "changelog", dry_run=False)
|
||||
def test_creates_tag(self, mock_run_cmd: MagicMock, mock_tag_exists: MagicMock, tmp_path: Path) -> None:
|
||||
github_output = tmp_path / "output.txt"
|
||||
with patch.dict(os.environ, {"GITHUB_OUTPUT": str(github_output)}):
|
||||
create_and_push_tag("0.2.0", "changelog", dry_run=False)
|
||||
calls = [c.args[0] for c in mock_run_cmd.call_args_list]
|
||||
assert ["git", "tag", "-a", "v0.2.0", "-m", "Release v0.2.0\n\nchangelog"] in calls
|
||||
assert ["git", "push", "origin", "refs/tags/v0.2.0"] in calls
|
||||
assert github_output.read_text() == "tag=v0.2.0\n"
|
||||
|
||||
@patch("devx.ci.release.tag_exists", return_value=False)
|
||||
@patch("devx.ci.release.run_cmd")
|
||||
def test_no_github_output_skips_write(self, mock_run_cmd: MagicMock, mock_tag_exists: MagicMock) -> None:
|
||||
with patch.dict(os.environ, {}, clear=True):
|
||||
create_and_push_tag("0.2.0", "changelog", dry_run=False)
|
||||
# Should still create tag, just not write GITHUB_OUTPUT
|
||||
calls = [c.args[0] for c in mock_run_cmd.call_args_list]
|
||||
assert ["git", "tag", "-a", "v0.2.0", "-m", "Release v0.2.0\n\nchangelog"] in calls
|
||||
|
||||
@patch("devx.ci.release.tag_exists", return_value=False)
|
||||
@patch("devx.ci.release.run_cmd")
|
||||
|
||||
Reference in New Issue
Block a user