Public Access
DEVX-43: feat: add publish step to post-merge release job, make publish idempotent
Post-merge / detect-type (push) Successful in 12s
Post-merge / validate-commit-msg (push) Successful in 7s
Post-merge / configure-repo (push) Successful in 9s
Post-merge / release (push) Failing after 44s
Post-merge / sync-wiki (push) Has been skipped
Post-merge / vikunja (push) Has been skipped
Post-merge / badges (push) Successful in 44s
Post-merge / detect-type (push) Successful in 12s
Post-merge / validate-commit-msg (push) Successful in 7s
Post-merge / configure-repo (push) Successful in 9s
Post-merge / release (push) Failing after 44s
Post-merge / sync-wiki (push) Has been skipped
Post-merge / vikunja (push) Has been skipped
Post-merge / badges (push) Successful in 44s
This commit was merged in pull request #67.
This commit is contained in:
@@ -75,7 +75,7 @@ jobs:
|
|||||||
needs: [detect-type]
|
needs: [detect-type]
|
||||||
if: needs.detect-type.outputs.is-release == 'false'
|
if: needs.detect-type.outputs.is-release == 'false'
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
timeout-minutes: 10
|
timeout-minutes: 15
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
@@ -94,6 +94,25 @@ jobs:
|
|||||||
. .venv/bin/activate
|
. .venv/bin/activate
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
python3 -m devx.ci.release
|
python3 -m devx.ci.release
|
||||||
|
- name: Publish release
|
||||||
|
env:
|
||||||
|
REPO_TOKEN: ${{ secrets.REPO_TOKEN }}
|
||||||
|
PYTHONPATH: src
|
||||||
|
run: |
|
||||||
|
. .venv/bin/activate
|
||||||
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
|
||||||
|
if [ -z "$TAG" ]; then
|
||||||
|
echo "No tag found — skipping publish"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
HEAD_MSG=$(git log -1 --format=%s)
|
||||||
|
if echo "$HEAD_MSG" | grep -q "^release: ${TAG}"; then
|
||||||
|
echo "Publishing release $TAG..."
|
||||||
|
python3 -m devx.ci.publish "$TAG" "${{ github.repository }}"
|
||||||
|
else
|
||||||
|
echo "HEAD is not a release commit for $TAG — skipping publish"
|
||||||
|
fi
|
||||||
- name: Notify on failure
|
- name: Notify on failure
|
||||||
if: failure()
|
if: failure()
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -211,6 +211,17 @@ def main(tag: str, repo: str, registry_url: str | None, skip_build: bool) -> Non
|
|||||||
click.echo(_("--skip-build: skipping package build and PyPI publish."))
|
click.echo(_("--skip-build: skipping package build and PyPI publish."))
|
||||||
|
|
||||||
tea = TeaCLI(repo=repo)
|
tea = TeaCLI(repo=repo)
|
||||||
|
|
||||||
|
# Check if release already exists (idempotent — avoids failure when
|
||||||
|
# called multiple times, e.g. by both post-merge and publish workflows)
|
||||||
|
try:
|
||||||
|
releases = tea.list_releases(repo)
|
||||||
|
if any(r.get("tag_name") == tag for r in releases):
|
||||||
|
click.echo(_("Gitea release {tag} already exists — skipping creation.", tag=tag))
|
||||||
|
return
|
||||||
|
except TeaCLIError:
|
||||||
|
pass # If listing fails, proceed to create
|
||||||
|
|
||||||
release_body = generate_release_notes(tag)
|
release_body = generate_release_notes(tag)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -657,6 +657,13 @@
|
|||||||
"ru": "Директория molecule не найдена: {path}",
|
"ru": "Директория molecule не найдена: {path}",
|
||||||
"zh": "未找到 molecule 目录: {path}"
|
"zh": "未找到 molecule 目录: {path}"
|
||||||
},
|
},
|
||||||
|
"Gitea release {tag} already exists — skipping creation.": {
|
||||||
|
"bg": "Gitea release {tag} вече съществува — прескачане на създаването.",
|
||||||
|
"de": "Gitea-Release {tag} existiert bereits — Erstellung übersprungen.",
|
||||||
|
"en": "Gitea release {tag} already exists — skipping creation.",
|
||||||
|
"ru": "Gitea release {tag} уже существует — пропуск создания.",
|
||||||
|
"zh": "Gitea release {tag} 已存在 — 跳过创建。"
|
||||||
|
},
|
||||||
"Nice! Gitea release {tag} created.": {
|
"Nice! Gitea release {tag} created.": {
|
||||||
"bg": "Отлично! Gitea release {tag} е създаден.",
|
"bg": "Отлично! Gitea release {tag} е създаден.",
|
||||||
"de": "Prima! Gitea-Release {tag} erstellt.",
|
"de": "Prima! Gitea-Release {tag} erstellt.",
|
||||||
|
|||||||
@@ -162,6 +162,7 @@ class TestMain:
|
|||||||
mock_notes: MagicMock,
|
mock_notes: MagicMock,
|
||||||
) -> None:
|
) -> None:
|
||||||
mock_tea = MagicMock()
|
mock_tea = MagicMock()
|
||||||
|
mock_tea.list_releases.return_value = []
|
||||||
mock_tea_cls.return_value = mock_tea
|
mock_tea_cls.return_value = mock_tea
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
result = runner.invoke(main, ["v1.0.0", "owner/repo"])
|
result = runner.invoke(main, ["v1.0.0", "owner/repo"])
|
||||||
@@ -187,6 +188,7 @@ class TestMain:
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""When no PYPI_TOKEN, publishes to Gitea PyPI registry."""
|
"""When no PYPI_TOKEN, publishes to Gitea PyPI registry."""
|
||||||
mock_tea = MagicMock()
|
mock_tea = MagicMock()
|
||||||
|
mock_tea.list_releases.return_value = []
|
||||||
mock_tea_cls.return_value = mock_tea
|
mock_tea_cls.return_value = mock_tea
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
result = runner.invoke(main, ["v1.0.0", "owner/repo"])
|
result = runner.invoke(main, ["v1.0.0", "owner/repo"])
|
||||||
@@ -209,6 +211,7 @@ class TestMain:
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""--registry-url flag publishes to the specified Gitea registry."""
|
"""--registry-url flag publishes to the specified Gitea registry."""
|
||||||
mock_tea = MagicMock()
|
mock_tea = MagicMock()
|
||||||
|
mock_tea.list_releases.return_value = []
|
||||||
mock_tea_cls.return_value = mock_tea
|
mock_tea_cls.return_value = mock_tea
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
result = runner.invoke(
|
result = runner.invoke(
|
||||||
@@ -236,6 +239,7 @@ class TestMain:
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""DEVX_PYPI_REGISTRY_URL env var sets the registry URL."""
|
"""DEVX_PYPI_REGISTRY_URL env var sets the registry URL."""
|
||||||
mock_tea = MagicMock()
|
mock_tea = MagicMock()
|
||||||
|
mock_tea.list_releases.return_value = []
|
||||||
mock_tea_cls.return_value = mock_tea
|
mock_tea_cls.return_value = mock_tea
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
result = runner.invoke(main, ["v1.0.0", "owner/repo"])
|
result = runner.invoke(main, ["v1.0.0", "owner/repo"])
|
||||||
@@ -256,6 +260,7 @@ class TestMain:
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""When no PYPI_TOKEN and no registry URL, skips publish and creates release only."""
|
"""When no PYPI_TOKEN and no registry URL, skips publish and creates release only."""
|
||||||
mock_tea = MagicMock()
|
mock_tea = MagicMock()
|
||||||
|
mock_tea.list_releases.return_value = []
|
||||||
mock_tea_cls.return_value = mock_tea
|
mock_tea_cls.return_value = mock_tea
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
result = runner.invoke(main, ["v1.0.0", "owner/repo", "--registry-url", ""])
|
result = runner.invoke(main, ["v1.0.0", "owner/repo", "--registry-url", ""])
|
||||||
@@ -307,6 +312,7 @@ class TestMain:
|
|||||||
self, mock_build: MagicMock, mock_publish: MagicMock, mock_tea_cls: MagicMock, mock_notes: MagicMock
|
self, mock_build: MagicMock, mock_publish: MagicMock, mock_tea_cls: MagicMock, mock_notes: MagicMock
|
||||||
) -> None:
|
) -> None:
|
||||||
mock_tea = MagicMock()
|
mock_tea = MagicMock()
|
||||||
|
mock_tea.list_releases.return_value = []
|
||||||
mock_tea.create_release.side_effect = TeaCLIError("server error")
|
mock_tea.create_release.side_effect = TeaCLIError("server error")
|
||||||
mock_tea_cls.return_value = mock_tea
|
mock_tea_cls.return_value = mock_tea
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
@@ -323,6 +329,7 @@ class TestMain:
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""--skip-build skips build_package and PyPI publish, only creates Gitea release."""
|
"""--skip-build skips build_package and PyPI publish, only creates Gitea release."""
|
||||||
mock_tea = MagicMock()
|
mock_tea = MagicMock()
|
||||||
|
mock_tea.list_releases.return_value = []
|
||||||
mock_tea_cls.return_value = mock_tea
|
mock_tea_cls.return_value = mock_tea
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
result = runner.invoke(main, ["v1.0.0", "owner/repo", "--skip-build"])
|
result = runner.invoke(main, ["v1.0.0", "owner/repo", "--skip-build"])
|
||||||
@@ -330,3 +337,39 @@ class TestMain:
|
|||||||
assert "skip" in result.output.lower()
|
assert "skip" in result.output.lower()
|
||||||
mock_build.assert_not_called()
|
mock_build.assert_not_called()
|
||||||
mock_tea.create_release.assert_called_once()
|
mock_tea.create_release.assert_called_once()
|
||||||
|
|
||||||
|
@patch.dict("os.environ", {"REPO_TOKEN": "gitea-tok", "PYPI_TOKEN": "pypi-tok"})
|
||||||
|
@patch("devx.ci.publish.generate_release_notes", return_value="Release notes")
|
||||||
|
@patch("devx.ci.publish.TeaCLI")
|
||||||
|
@patch("devx.ci.publish.publish_to_pypi")
|
||||||
|
@patch("devx.ci.publish.build_package")
|
||||||
|
def test_skips_release_creation_when_already_exists(
|
||||||
|
self, mock_build: MagicMock, mock_publish: MagicMock, mock_tea_cls: MagicMock, mock_notes: MagicMock
|
||||||
|
) -> None:
|
||||||
|
"""If the Gitea release already exists, skip creation (idempotent)."""
|
||||||
|
mock_tea = MagicMock()
|
||||||
|
mock_tea.list_releases.return_value = [{"tag_name": "v1.0.0"}]
|
||||||
|
mock_tea_cls.return_value = mock_tea
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(main, ["v1.0.0", "owner/repo"])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert "already exists" in result.output
|
||||||
|
mock_tea.create_release.assert_not_called()
|
||||||
|
|
||||||
|
@patch.dict("os.environ", {"REPO_TOKEN": "gitea-tok", "PYPI_TOKEN": "pypi-tok"})
|
||||||
|
@patch("devx.ci.publish.generate_release_notes", return_value="Release notes")
|
||||||
|
@patch("devx.ci.publish.TeaCLI")
|
||||||
|
@patch("devx.ci.publish.publish_to_pypi")
|
||||||
|
@patch("devx.ci.publish.build_package")
|
||||||
|
def test_proceeds_to_create_when_list_releases_fails(
|
||||||
|
self, mock_build: MagicMock, mock_publish: MagicMock, mock_tea_cls: MagicMock, mock_notes: MagicMock
|
||||||
|
) -> None:
|
||||||
|
"""If list_releases raises TeaCLIError, proceed to create the release."""
|
||||||
|
mock_tea = MagicMock()
|
||||||
|
mock_tea.list_releases.side_effect = TeaCLIError("api error")
|
||||||
|
mock_tea_cls.return_value = mock_tea
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(main, ["v1.0.0", "owner/repo"])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert "Gitea release v1.0.0 created" in result.output
|
||||||
|
mock_tea.create_release.assert_called_once()
|
||||||
|
|||||||
Reference in New Issue
Block a user