feat(publish): add PyPI publish workflow, CLI command, parser integration, and tests
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / linter-shell (push) Has been cancelled
Mark stable commit / linter-python (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / linter-shell (push) Has been cancelled
Mark stable commit / linter-python (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
* Introduce publish action with PyPI target detection via MIRRORS * Resolve version from SemVer git tags on HEAD * Support preview mode and non-interactive CI usage * Build and upload artifacts using build + twine with token resolution * Add CLI wiring (dispatch, command handler, parser) * Add E2E publish help tests for pkgmgr and nix run * Add integration tests for publish preview and mirror handling * Add unit tests for git tag parsing, PyPI URL parsing, workflow preview, and CLI handler * Clean up dispatch and parser structure while integrating publish https://chatgpt.com/share/693f0f00-af68-800f-8846-193dca69bd2e
This commit is contained in:
119
tests/e2e/test_publish_commands.py
Normal file
119
tests/e2e/test_publish_commands.py
Normal file
@@ -0,0 +1,119 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from contextlib import redirect_stdout
|
||||
from types import SimpleNamespace
|
||||
|
||||
from pkgmgr.cli.commands.publish import handle_publish
|
||||
|
||||
|
||||
def _run(cmd: list[str], cwd: str) -> None:
|
||||
subprocess.run(
|
||||
cmd,
|
||||
cwd=cwd,
|
||||
check=True,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
|
||||
|
||||
class TestIntegrationPublish(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
if shutil.which("git") is None:
|
||||
self.skipTest("git is required for this integration test")
|
||||
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.repo_dir = self.tmp.name
|
||||
|
||||
# Initialize git repository
|
||||
_run(["git", "init"], cwd=self.repo_dir)
|
||||
_run(["git", "config", "user.email", "ci@example.invalid"], cwd=self.repo_dir)
|
||||
_run(["git", "config", "user.name", "CI"], cwd=self.repo_dir)
|
||||
|
||||
with open(os.path.join(self.repo_dir, "README.md"), "w", encoding="utf-8") as f:
|
||||
f.write("test\n")
|
||||
|
||||
_run(["git", "add", "README.md"], cwd=self.repo_dir)
|
||||
_run(["git", "commit", "-m", "init"], cwd=self.repo_dir)
|
||||
_run(["git", "tag", "-a", "v1.2.3", "-m", "v1.2.3"], cwd=self.repo_dir)
|
||||
|
||||
# Create MIRRORS file with PyPI target
|
||||
with open(os.path.join(self.repo_dir, "MIRRORS"), "w", encoding="utf-8") as f:
|
||||
f.write("https://pypi.org/project/pkgmgr/\n")
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.tmp.cleanup()
|
||||
|
||||
def test_publish_preview_end_to_end(self) -> None:
|
||||
ctx = SimpleNamespace(
|
||||
repositories_base_dir=self.repo_dir,
|
||||
all_repositories=[
|
||||
{
|
||||
"name": "pkgmgr",
|
||||
"directory": self.repo_dir,
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
selected = [
|
||||
{
|
||||
"name": "pkgmgr",
|
||||
"directory": self.repo_dir,
|
||||
}
|
||||
]
|
||||
|
||||
args = SimpleNamespace(
|
||||
preview=True,
|
||||
non_interactive=False,
|
||||
)
|
||||
|
||||
buf = io.StringIO()
|
||||
with redirect_stdout(buf):
|
||||
handle_publish(args=args, ctx=ctx, selected=selected)
|
||||
|
||||
out = buf.getvalue()
|
||||
|
||||
self.assertIn("[pkgmgr] Publishing repository", out)
|
||||
self.assertIn("[INFO] Publishing pkgmgr for tag v1.2.3", out)
|
||||
self.assertIn("[PREVIEW] Would build and upload to PyPI.", out)
|
||||
|
||||
# Preview must not create dist/
|
||||
self.assertFalse(os.path.isdir(os.path.join(self.repo_dir, "dist")))
|
||||
|
||||
def test_publish_skips_without_pypi_mirror(self) -> None:
|
||||
with open(os.path.join(self.repo_dir, "MIRRORS"), "w", encoding="utf-8") as f:
|
||||
f.write("git@github.com:example/example.git\n")
|
||||
|
||||
ctx = SimpleNamespace(
|
||||
repositories_base_dir=self.repo_dir,
|
||||
all_repositories=[
|
||||
{
|
||||
"name": "pkgmgr",
|
||||
"directory": self.repo_dir,
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
selected = [
|
||||
{
|
||||
"name": "pkgmgr",
|
||||
"directory": self.repo_dir,
|
||||
}
|
||||
]
|
||||
|
||||
args = SimpleNamespace(
|
||||
preview=True,
|
||||
non_interactive=False,
|
||||
)
|
||||
|
||||
buf = io.StringIO()
|
||||
with redirect_stdout(buf):
|
||||
handle_publish(args=args, ctx=ctx, selected=selected)
|
||||
|
||||
out = buf.getvalue()
|
||||
self.assertIn("[INFO] No PyPI mirror found. Skipping publish.", out)
|
||||
119
tests/integration/test_publish_integration.py
Normal file
119
tests/integration/test_publish_integration.py
Normal file
@@ -0,0 +1,119 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from contextlib import redirect_stdout
|
||||
from types import SimpleNamespace
|
||||
|
||||
from pkgmgr.cli.commands.publish import handle_publish
|
||||
|
||||
|
||||
def _run(cmd: list[str], cwd: str) -> None:
|
||||
subprocess.run(
|
||||
cmd,
|
||||
cwd=cwd,
|
||||
check=True,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
|
||||
|
||||
class TestIntegrationPublish(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
if shutil.which("git") is None:
|
||||
self.skipTest("git is required for this integration test")
|
||||
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.repo_dir = self.tmp.name
|
||||
|
||||
# Initialize git repository
|
||||
_run(["git", "init"], cwd=self.repo_dir)
|
||||
_run(["git", "config", "user.email", "ci@example.invalid"], cwd=self.repo_dir)
|
||||
_run(["git", "config", "user.name", "CI"], cwd=self.repo_dir)
|
||||
|
||||
with open(os.path.join(self.repo_dir, "README.md"), "w", encoding="utf-8") as f:
|
||||
f.write("test\n")
|
||||
|
||||
_run(["git", "add", "README.md"], cwd=self.repo_dir)
|
||||
_run(["git", "commit", "-m", "init"], cwd=self.repo_dir)
|
||||
_run(["git", "tag", "-a", "v1.2.3", "-m", "v1.2.3"], cwd=self.repo_dir)
|
||||
|
||||
# Create MIRRORS file with PyPI target
|
||||
with open(os.path.join(self.repo_dir, "MIRRORS"), "w", encoding="utf-8") as f:
|
||||
f.write("https://pypi.org/project/pkgmgr/\n")
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.tmp.cleanup()
|
||||
|
||||
def test_publish_preview_end_to_end(self) -> None:
|
||||
ctx = SimpleNamespace(
|
||||
repositories_base_dir=self.repo_dir,
|
||||
all_repositories=[
|
||||
{
|
||||
"name": "pkgmgr",
|
||||
"directory": self.repo_dir,
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
selected = [
|
||||
{
|
||||
"name": "pkgmgr",
|
||||
"directory": self.repo_dir,
|
||||
}
|
||||
]
|
||||
|
||||
args = SimpleNamespace(
|
||||
preview=True,
|
||||
non_interactive=False,
|
||||
)
|
||||
|
||||
buf = io.StringIO()
|
||||
with redirect_stdout(buf):
|
||||
handle_publish(args=args, ctx=ctx, selected=selected)
|
||||
|
||||
out = buf.getvalue()
|
||||
|
||||
self.assertIn("[pkgmgr] Publishing repository", out)
|
||||
self.assertIn("[INFO] Publishing pkgmgr for tag v1.2.3", out)
|
||||
self.assertIn("[PREVIEW] Would build and upload to PyPI.", out)
|
||||
|
||||
# Preview must not create dist/
|
||||
self.assertFalse(os.path.isdir(os.path.join(self.repo_dir, "dist")))
|
||||
|
||||
def test_publish_skips_without_pypi_mirror(self) -> None:
|
||||
with open(os.path.join(self.repo_dir, "MIRRORS"), "w", encoding="utf-8") as f:
|
||||
f.write("git@github.com:example/example.git\n")
|
||||
|
||||
ctx = SimpleNamespace(
|
||||
repositories_base_dir=self.repo_dir,
|
||||
all_repositories=[
|
||||
{
|
||||
"name": "pkgmgr",
|
||||
"directory": self.repo_dir,
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
selected = [
|
||||
{
|
||||
"name": "pkgmgr",
|
||||
"directory": self.repo_dir,
|
||||
}
|
||||
]
|
||||
|
||||
args = SimpleNamespace(
|
||||
preview=True,
|
||||
non_interactive=False,
|
||||
)
|
||||
|
||||
buf = io.StringIO()
|
||||
with redirect_stdout(buf):
|
||||
handle_publish(args=args, ctx=ctx, selected=selected)
|
||||
|
||||
out = buf.getvalue()
|
||||
self.assertIn("[INFO] No PyPI mirror found. Skipping publish.", out)
|
||||
0
tests/unit/pkgmgr/actions/publish/__init__.py
Normal file
0
tests/unit/pkgmgr/actions/publish/__init__.py
Normal file
20
tests/unit/pkgmgr/actions/publish/test_git_tags.py
Normal file
20
tests/unit/pkgmgr/actions/publish/test_git_tags.py
Normal file
@@ -0,0 +1,20 @@
|
||||
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from pkgmgr.actions.publish.git_tags import head_semver_tags
|
||||
|
||||
|
||||
class TestHeadSemverTags(unittest.TestCase):
|
||||
@patch("pkgmgr.actions.publish.git_tags.run_git")
|
||||
def test_no_tags(self, mock_run_git):
|
||||
mock_run_git.return_value = ""
|
||||
self.assertEqual(head_semver_tags(), [])
|
||||
|
||||
@patch("pkgmgr.actions.publish.git_tags.run_git")
|
||||
def test_filters_and_sorts_semver(self, mock_run_git):
|
||||
mock_run_git.return_value = "v1.0.0\nv2.0.0\nfoo\n"
|
||||
self.assertEqual(
|
||||
head_semver_tags(),
|
||||
["v1.0.0", "v2.0.0"],
|
||||
)
|
||||
13
tests/unit/pkgmgr/actions/publish/test_pypi_url.py
Normal file
13
tests/unit/pkgmgr/actions/publish/test_pypi_url.py
Normal file
@@ -0,0 +1,13 @@
|
||||
|
||||
import unittest
|
||||
from pkgmgr.actions.publish.pypi_url import parse_pypi_project_url
|
||||
|
||||
|
||||
class TestParsePyPIUrl(unittest.TestCase):
|
||||
def test_valid_pypi_url(self):
|
||||
t = parse_pypi_project_url("https://pypi.org/project/example/")
|
||||
self.assertIsNotNone(t)
|
||||
self.assertEqual(t.project, "example")
|
||||
|
||||
def test_invalid_url(self):
|
||||
self.assertIsNone(parse_pypi_project_url("https://example.com/foo"))
|
||||
21
tests/unit/pkgmgr/actions/publish/test_workflow_preview.py
Normal file
21
tests/unit/pkgmgr/actions/publish/test_workflow_preview.py
Normal file
@@ -0,0 +1,21 @@
|
||||
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from pkgmgr.actions.publish.workflow import publish
|
||||
|
||||
|
||||
class TestPublishWorkflowPreview(unittest.TestCase):
|
||||
@patch("pkgmgr.actions.publish.workflow.read_mirrors_file")
|
||||
@patch("pkgmgr.actions.publish.workflow.head_semver_tags")
|
||||
def test_preview_does_not_build(self, mock_tags, mock_mirrors):
|
||||
mock_mirrors.return_value = {
|
||||
"pypi": "https://pypi.org/project/example/"
|
||||
}
|
||||
mock_tags.return_value = ["v1.0.0"]
|
||||
|
||||
publish(
|
||||
repo={},
|
||||
repo_dir=".",
|
||||
preview=True,
|
||||
)
|
||||
12
tests/unit/pkgmgr/cli/commands/test_publish.py
Normal file
12
tests/unit/pkgmgr/cli/commands/test_publish.py
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from pkgmgr.cli.commands.publish import handle_publish
|
||||
|
||||
|
||||
class TestHandlePublish(unittest.TestCase):
|
||||
@patch("pkgmgr.cli.commands.publish.publish")
|
||||
def test_no_selected_repos(self, mock_publish):
|
||||
handle_publish(args=object(), ctx=None, selected=[])
|
||||
mock_publish.assert_not_called()
|
||||
Reference in New Issue
Block a user