Skip to content

Commit 024ee03

Browse files
committed
Add QGIS plugin publish workflow
1 parent a5c42aa commit 024ee03

2 files changed

Lines changed: 128 additions & 0 deletions

File tree

.github/workflows/publish.yml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
name: Publish
2+
3+
on:
4+
release:
5+
types: [published]
6+
workflow_dispatch:
7+
inputs:
8+
tag:
9+
description: "Release tag to publish (must already exist on GitHub)"
10+
required: true
11+
type: string
12+
13+
permissions:
14+
contents: write
15+
16+
jobs:
17+
publish:
18+
name: Publish plugin to plugins.qgis.org
19+
runs-on: ubuntu-latest
20+
env:
21+
TAG: ${{ github.event.release.tag_name || inputs.tag }}
22+
PLUGIN_DIR: plugin_template
23+
PLUGIN_NAME: plugin_template
24+
ZIP_PATH: dist/plugin_template.zip
25+
steps:
26+
- uses: actions/checkout@v6
27+
28+
- uses: actions/setup-python@v6
29+
with:
30+
python-version: "3.13"
31+
32+
- name: Build plugin zip
33+
run: python "package_plugin.py" --source "$PLUGIN_DIR" --name "$PLUGIN_NAME" --output "$ZIP_PATH"
34+
35+
- name: Verify metadata version matches release tag
36+
run: |
37+
metadata_version=$(grep '^version=' "$PLUGIN_DIR/metadata.txt" | cut -d'=' -f2 | tr -d '[:space:]')
38+
tag_version="${TAG#v}"
39+
if [ "$metadata_version" != "$tag_version" ]; then
40+
echo "::error::metadata.txt version ($metadata_version) does not match release tag ($tag_version)"
41+
exit 1
42+
fi
43+
44+
- name: Attach zip to GitHub release
45+
env:
46+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
47+
run: |
48+
gh release upload "$TAG" "$ZIP_PATH" --clobber
49+
50+
- name: Upload to plugins.qgis.org
51+
env:
52+
QGIS_PLUGIN_REPO_USERNAME: ${{ secrets.QGIS_PLUGIN_REPO_USERNAME }}
53+
QGIS_PLUGIN_REPO_PASSWORD: ${{ secrets.QGIS_PLUGIN_REPO_PASSWORD }}
54+
run: python scripts/upload_to_qgis_plugin_repo.py "$ZIP_PATH"
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#!/usr/bin/env python3
2+
"""Upload a packaged QGIS plugin zip to plugins.qgis.org via XML-RPC.
3+
4+
The official plugin repository exposes an XML-RPC endpoint at
5+
``https://plugins.qgis.org/plugins/RPC2/`` with a ``plugin.upload`` method
6+
that accepts the zipped plugin as a base64-encoded ``Binary`` payload and
7+
returns the new plugin id and version id on success.
8+
9+
Credentials must belong to a user with upload rights for the plugin and are
10+
read from the ``QGIS_PLUGIN_REPO_USERNAME`` and ``QGIS_PLUGIN_REPO_PASSWORD``
11+
environment variables so this script can be used from CI without leaking
12+
secrets onto the command line.
13+
"""
14+
15+
from __future__ import annotations
16+
17+
import argparse
18+
import os
19+
import sys
20+
from urllib.parse import quote
21+
from xmlrpc.client import Binary, Fault, ProtocolError, ServerProxy
22+
23+
REPO_URL_TEMPLATE = "https://{user}:{password}@plugins.qgis.org/plugins/RPC2/"
24+
25+
26+
def upload(zip_path: str, username: str, password: str) -> tuple[int, int]:
27+
"""Upload the given zip to plugins.qgis.org and return ``(plugin_id, version_id)``."""
28+
with open(zip_path, "rb") as fh:
29+
payload = Binary(fh.read())
30+
31+
endpoint = REPO_URL_TEMPLATE.format(
32+
user=quote(username, safe=""),
33+
password=quote(password, safe=""),
34+
)
35+
server = ServerProxy(endpoint, verbose=False)
36+
plugin_id, version_id = server.plugin.upload(payload)
37+
return plugin_id, version_id
38+
39+
40+
def main() -> int:
41+
parser = argparse.ArgumentParser(description=__doc__)
42+
parser.add_argument("zip_path", help="Path to the packaged plugin zip")
43+
args = parser.parse_args()
44+
45+
username = os.environ.get("QGIS_PLUGIN_REPO_USERNAME")
46+
password = os.environ.get("QGIS_PLUGIN_REPO_PASSWORD")
47+
if not username or not password:
48+
print(
49+
"Error: QGIS_PLUGIN_REPO_USERNAME and QGIS_PLUGIN_REPO_PASSWORD must be set.",
50+
file=sys.stderr,
51+
)
52+
return 1
53+
54+
if not os.path.isfile(args.zip_path):
55+
print(f"Error: zip file not found: {args.zip_path}", file=sys.stderr)
56+
return 1
57+
58+
try:
59+
plugin_id, version_id = upload(args.zip_path, username, password)
60+
except Fault as exc:
61+
print(
62+
f"Upload failed: {exc.faultString} (code {exc.faultCode})", file=sys.stderr
63+
)
64+
return 1
65+
except ProtocolError as exc:
66+
print(f"Upload failed: HTTP {exc.errcode} {exc.errmsg}", file=sys.stderr)
67+
return 1
68+
69+
print(f"Uploaded plugin id={plugin_id}, version id={version_id}")
70+
return 0
71+
72+
73+
if __name__ == "__main__":
74+
sys.exit(main())

0 commit comments

Comments
 (0)