ci: package the Decky zip and publish Gitea releases per version
ysj7705/mudfish-decky/pipeline/head This commit looks good

- scripts/package.py builds out/mudfish-decky-<version>.zip with the
  mudfish-decky/ folder Decky expects (runtime files only).
- scripts/gitea_release.py creates release v<version> and uploads the zip,
  skipping versions that already have a release.
- Jenkinsfile uses the organization folder's gitea-ysj7705 token.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ysj7705andClaude Opus 5.5 committed 2026-10-25 01:53:31 +09:00
1 parent c2841e8f21
commit 93baa1a781
4 files changed
+154 -3

No files matched your search

+93
View File
@@ -0,0 +1,93 @@
"""Publish the packaged zip as a Gitea release v<version>, once per version.
Usage: python scripts/gitea_release.py OUT_DIR
Env: GITEA_URL, GITEA_REPO (owner/name), GITEA_TOKEN (write access to releases), GIT_COMMIT.
"""
import json
import os
import sys
import urllib.error
import urllib.parse
import urllib.request
import uuid
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from package import PLUGIN_DIR, read_version # noqa: E402
NOTES = """Decky Loader plugin for Mudfish Cloud VPN (Full VPN toggle, Japan node picker, QR phone login,
Mudfish install/update/remove).
Install: Decky settings → Developer mode → Install Plugin from URL, and paste this zip's download URL.
"""
class ReleaseError(Exception):
pass
def _multipart(filename: str, data: bytes):
boundary = uuid.uuid4().hex
head = (f"--{boundary}\r\nContent-Disposition: form-data; name=\"attachment\"; filename=\"{filename}\"\r\n"
"Content-Type: application/zip\r\n\r\n").encode()
return head + data + f"\r\n--{boundary}--\r\n".encode(), f"multipart/form-data; boundary={boundary}"
def publish(api, repo: str, version: str, commit: str, zip_path: str):
"""Create release v<version> with the zip attached; None if that release already exists."""
tag = f"v{version}"
base = f"/api/v1/repos/{repo}/releases"
status, _ = api("GET", f"{base}/tags/{tag}")
if status == 200:
print(f"release {tag} already exists; nothing to publish")
return None
if status != 404:
raise ReleaseError(f"checking {tag}: HTTP {status}")
status, release = api("POST", base, {"tag_name": tag, "target_commitish": commit,
"name": f"Mudfish VPN {tag}", "body": NOTES})
if status not in (200, 201):
raise ReleaseError(f"creating {tag}: HTTP {status} {release.get('message', '')}")
name = os.path.basename(zip_path)
with open(zip_path, "rb") as f:
body, ctype = _multipart(name, f.read())
status, asset = api("POST", f"{base}/{release['id']}/assets?name={urllib.parse.quote(name)}",
body, {"Content-Type": ctype})
if status not in (200, 201):
raise ReleaseError(f"uploading {name}: HTTP {status} {asset.get('message', '')}")
return asset.get("browser_download_url")
def http_api(base_url: str, token: str):
def call(method, path, body=None, headers=None):
headers = dict(headers or {}, Authorization=f"token {token}", Accept="application/json")
if isinstance(body, dict):
body = json.dumps(body).encode()
headers["Content-Type"] = "application/json"
req = urllib.request.Request(base_url.rstrip("/") + path, data=body, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp:
status, raw = resp.status, resp.read()
except urllib.error.HTTPError as e:
status, raw = e.code, e.read()
try:
return status, json.loads(raw) if raw else {}
except ValueError:
return status, {}
return call
def main(out_dir: str) -> None:
version = read_version(".")
zip_path = os.path.join(out_dir, f"{PLUGIN_DIR}-{version}.zip")
if not os.path.isfile(zip_path):
sys.exit(f"release: {zip_path} not found (run scripts/package.py first)")
api = http_api(os.environ["GITEA_URL"], os.environ["GITEA_TOKEN"])
url = publish(api, os.environ["GITEA_REPO"], version, os.environ.get("GIT_COMMIT", "main"), zip_path)
if url:
print(f"published {url}")
if __name__ == "__main__":
try:
main(sys.argv[1] if len(sys.argv) > 1 else "out")
except ReleaseError as e:
sys.exit(f"release: {e}")
+46
View File
@@ -0,0 +1,46 @@
"""Build the installable Decky zip: <out>/mudfish-decky-<version>.zip with a top-level mudfish-decky/ folder.
Usage: python scripts/package.py [OUT_DIR] (run from the repository root after `pnpm build`)
"""
import json
import os
import sys
import zipfile
PLUGIN_DIR = "mudfish-decky"
INCLUDE = ["plugin.json", "package.json", "main.py", "README.md", "LICENSE", "dist", "py_modules"]
REQUIRED = ["plugin.json", "package.json", "main.py", os.path.join("dist", "index.js")]
def read_version(src: str) -> str:
with open(os.path.join(src, "package.json")) as f:
return json.load(f)["version"]
def _files(src: str):
for entry in INCLUDE:
path = os.path.join(src, entry)
if os.path.isfile(path):
yield entry
elif os.path.isdir(path):
for root, dirs, files in os.walk(path):
dirs[:] = sorted(d for d in dirs if d != "__pycache__")
for name in sorted(files):
if not name.endswith(".pyc"):
yield os.path.relpath(os.path.join(root, name), src)
def build_zip(src: str, out_dir: str) -> str:
missing = [p for p in REQUIRED if not os.path.isfile(os.path.join(src, p))]
if missing:
sys.exit(f"package: missing {', '.join(missing)} (run the build first)")
os.makedirs(out_dir, exist_ok=True)
out = os.path.join(out_dir, f"{PLUGIN_DIR}-{read_version(src)}.zip")
with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as z:
for rel in _files(src):
z.write(os.path.join(src, rel), f"{PLUGIN_DIR}/{rel.replace(os.sep, '/')}")
return out
if __name__ == "__main__":
print(build_zip(".", sys.argv[1] if len(sys.argv) > 1 else "out"))