diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1414a1a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,20 @@ +name: CI + +on: + push: + branches: [ main ] + tags-ignore: [ '**' ] + pull_request: + branches: [ main ] + +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + # The tests create accounts on the ci-chatmail.testrun.org relay. + py-checks: + uses: chatmail/workflows/.github/workflows/py-checks.yml@main diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..457b876 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,34 @@ +name: Release to PyPI + +on: + push: + tags: + - "v*" + +permissions: + contents: read + +jobs: + # Checks and build via the shared reusable workflow, which uploads + # the built distributions as a "dist" artifact. + py-checks: + uses: chatmail/workflows/.github/workflows/py-checks.yml@main + + # The publish job must stay here in the repo's own workflow: PyPI + # trusted publishing rejects publishes from reusable workflows. + # See https://github.com/pypi/warehouse/issues/11096 + publish: + needs: py-checks + runs-on: ubuntu-latest + environment: pypi + permissions: + id-token: write + steps: + - uses: actions/download-artifact@v8 + with: + name: dist + path: dist/ + + - uses: pypa/gh-action-pypi-publish@release/v1 + with: + skip-existing: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d17769..7977966 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,19 +1,31 @@ -# cmsend changelog +# cmsend changelog + +## unreleased + +- adopt shared CI from chatmail/workflows (ruff lint+format, build) + and release via PyPI trusted publishing on v* tags + +- fix "cmsend -l" crashing with AttributeError when listing chat + members + +- `main()` accepts an optional argv list so tests can invoke it + +- fix pyproject description (was copied from cmping) ## 0.4.2 -- improve output on "cmsend -l" to show members of each chat +- improve output on "cmsend -l" to show members of each chat -## 0.4.1 +## 0.4.1 -- add warning +- add warning ## 0.4.0 tagged chats - added "-t" tagged chats option, so that "--join" can be accompanied by "-t" -- added "cmsend -l" to list all tagged chats +- added "cmsend -l" to list all tagged chats ## 0.3.2 @@ -26,8 +38,8 @@ ## 0.3.0 -- added more options +- added more options ## 0.1.0 -- initial release +- initial release diff --git a/README.md b/README.md index 3eb21a2..4d5ee43 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ To install use: - pip install cmsend + uv tool install cmsend To send and receive from a single chatmail relay: @@ -31,21 +31,22 @@ To show help: cmsend -h -## Example outputs - - ## Developing / Releasing cmsend 1. clone the git repository at https://github.com/chatmail/cmsend -2. install 'cmsend" in editing mode: `pip install -e .` +2. install 'cmsend' in editing mode: `uv pip install -e .` 3. edit cmsend.py and test, finally commit your changes -4. set a new git-tag +[chatmail/workflows](https://github.com/chatmail/workflows) +defines py-checks for this repository. +Run checks locally with `uvx ruff check .` and `uvx ruff format --check .` -5. install build/release tools: `pip install build twine` +To release, update CHANGELOG.md, then create and push a version tag: -6. run the following command: + git tag -a v0.5.0 -m "Release v0.5.0" + git push origin main v0.5.0 - rm -rf dist && python -m build && twine upload -r pypi dist/cmsend* +The release.yml workflow then builds and publishes to PyPI via +trusted publishing (OIDC); no local twine or PyPI token is involved. diff --git a/cmsend.py b/cmsend.py index 541dcca..df48c76 100644 --- a/cmsend.py +++ b/cmsend.py @@ -10,7 +10,7 @@ from xdg_base_dirs import xdg_config_home -def main(): +def main(argv=None): """Send end-to-end encrypted messages to groups/contacts.""" parser = argparse.ArgumentParser(description=main.__doc__) @@ -34,10 +34,7 @@ def main(): help="use the specified tag for joining a chat or sending a message (default: GENESIS)", ) parser.add_argument( - "-l", - dest="listtags", - action="store_true", - help="list existing tagged chats" + "-l", dest="listtags", action="store_true", help="list existing tagged chats" ) parser.add_argument( "-m", @@ -52,7 +49,7 @@ def main(): parser.add_argument( "-a", dest="filename", type=str, default=None, help="add file attachment" ) - args = parser.parse_args() + args = parser.parse_args(argv) try: return perform_main(args) @@ -161,8 +158,8 @@ def perform_listtags(self): chat = self.get_tagged_chat(tag) snap = chat.get_full_snapshot() print(f"{tag}: chat_id={chat.id} name={snap.name}") - for contact in snap.contacts: - print(f" - {contact.name_and_addr}") + for contact in chat.get_contacts(): + print(f" - {contact.get_snapshot().name_and_addr}") def perform_send(self, tag, text, filename=None): self._account.start_io() @@ -179,8 +176,10 @@ def perform_send(self, tag, text, filename=None): def get_tagged_chat(self, tag): chat_id = self._account.get_config(f"{self.UI_CONFIG_TAGGED_CHATS}.{tag}") if not chat_id: - print(f"No chat tagged with tag={tag} found for sending on {self!r}, " - f"use -t {tag} --join 'https://i.delta.chat/...'") + print( + f"No chat tagged with tag={tag} found for sending on {self!r}, " + f"use -t {tag} --join 'https://i.delta.chat/...'" + ) raise SystemExit(5) return self._account.get_chat_by_id(int(chat_id)) diff --git a/pyproject.toml b/pyproject.toml index aa677ef..a2298da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,8 @@ build-backend = "setuptools.build_meta" [project] name = "cmsend" readme = "README.md" -description = "ping messages between chatmail relays" +description = "send end-to-end encrypted messages to chats on chatmail relays" +requires-python = ">=3.11" dependencies = [ "deltachat-rpc-server>=2.24.0", "deltachat-rpc-client>=2.24.0", @@ -23,17 +24,22 @@ py-modules = ["cmsend"] [project.urls] Repository = "https://github.com/chatmail/cmsend" -Changelog = "https://github.com/chatmail/cmsend/blob/master/CHANGELOG.md" +Changelog = "https://github.com/chatmail/cmsend/blob/main/CHANGELOG.md" [project.scripts] cmsend = "cmsend:main" +[dependency-groups] +test = [ + "pytest", + "pytest-timeout", + "execnet", # imported by the deltachat_rpc_client pytest plugin +] + [tool.pytest.ini_options] addopts = "-v -ra --strict-markers" - -[tool.setuptools.packages.find] -where = ["."] - +# cmsend waits for core events without a deadline of its own. +timeout = 180 [tool.ruff] lint.select = [ diff --git a/test_cmsend.py b/test_cmsend.py new file mode 100644 index 0000000..db9abe1 --- /dev/null +++ b/test_cmsend.py @@ -0,0 +1,50 @@ +import pytest + +import cmsend + +pytest_plugins = ("deltachat_rpc_client.pytestplugin",) +ci_chatmail_domain = "ci-chatmail.testrun.org" + + +@pytest.fixture(autouse=True) +def _inject_xdg_config_home(tmp_path, monkeypatch): + xdg_config = tmp_path.joinpath("xdg-config") + monkeypatch.setattr(cmsend, "xdg_config_home", lambda: xdg_config) + monkeypatch.setenv("CHATMAIL_DOMAIN", ci_chatmail_domain) + + +@pytest.fixture +def invoke_main(capsys): + def invoke(*args): + with capsys.disabled(): + print(f"$ cmsend {' '.join(args)}") + ret = cmsend.main(args) + out, err = capsys.readouterr() + with capsys.disabled(): + if out: + print(out) + return ret, out, err + + return invoke + + +def test_init_join_and_send(acfactory, invoke_main): + (ac,) = acfactory.get_online_accounts(1) + + invoke_main("--init", ci_chatmail_domain) + + _ret, out, _err = invoke_main("-l") + assert "LOG" not in out + + # "--join" expects a Join-Group QR code, not a Setup-Contact one. + group = ac.create_group("cmsend log") + invoke_main("-t", "LOG", "--join", group.get_qr_code()) + + _ret, out, _err = invoke_main("-l") + assert "LOG" in out + assert "cmsend log" in out + + invoke_main("-t", "LOG", "-m", "hello from cmsend") + event = ac.wait_for_incoming_msg_event() + snapshot = ac.get_message_by_id(event.msg_id).get_snapshot() + assert snapshot.text == "hello from cmsend"