From c94374b62a403a2d695b3222f6129c2dacb00b0e Mon Sep 17 00:00:00 2001 From: TommyFang Date: Mon, 17 Aug 2026 00:39:52 +0800 Subject: [PATCH] feat: publish Gitea releases via Gitea Actions runner --- .gitea/workflows/publish.yml | 25 +++++++++++++ scripts/build-gitea-update.py | 54 ++++++++++++++++++++++++++- scripts/publish-gitea-actions.sh | 50 +++++++++++++++++++++++++ tests/test_release.py | 63 ++++++++++++++++++++++++++++++++ 4 files changed, 190 insertions(+), 2 deletions(-) create mode 100644 .gitea/workflows/publish.yml create mode 100755 scripts/publish-gitea-actions.sh diff --git a/.gitea/workflows/publish.yml b/.gitea/workflows/publish.yml new file mode 100644 index 0000000..b67be7b --- /dev/null +++ b/.gitea/workflows/publish.yml @@ -0,0 +1,25 @@ +name: publish-gitea +on: + workflow_dispatch: + inputs: + tag: + description: "GitHub release tag to publish (default: latest)" + required: false + default: latest + +jobs: + publish: + runs-on: [self-hosted, linux, x64] + steps: + - name: Download publish scripts + run: | + BASE=https://raw.githubusercontent.com/TommyFang2077/dsh-easy-desktop/main + curl -fsSL -o build-gitea-update.py "$BASE/scripts/build-gitea-update.py" + curl -fsSL -o publish-gitea-release.sh "$BASE/scripts/publish-gitea-release.sh" + curl -fsSL -o publish-gitea-actions.sh "$BASE/scripts/publish-gitea-actions.sh" + - name: Publish to Gitea + run: bash publish-gitea-actions.sh + env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + GITEA_BASE_URL: http://192.168.30.33:3000 + RELEASE_TAG: ${{ github.event.inputs.tag }} \ No newline at end of file diff --git a/scripts/build-gitea-update.py b/scripts/build-gitea-update.py index 9c5ccd0..471ece3 100755 --- a/scripts/build-gitea-update.py +++ b/scripts/build-gitea-update.py @@ -5,6 +5,7 @@ from __future__ import annotations import argparse import json +import re import shutil from pathlib import Path @@ -39,6 +40,27 @@ INSTALLER_KIND = { "linux": "appimage", "windows": "nsis", } +# Asset-name patterns for GitHub Release downloads (flat directory): maps a +# fileName to its canonical (os, arch, deliverable suffix). +FLAT_PATTERNS: list[tuple[re.Pattern, str, str, str]] = [ + (re.compile(r"^.*_aarch64\.app\.tar\.gz\.sig$"), "darwin", "aarch64", ".app.tar.gz.sig"), + (re.compile(r"^.*_aarch64\.app\.tar\.gz$"), "darwin", "aarch64", ".app.tar.gz"), + (re.compile(r"^.*_aarch64\.dmg$"), "darwin", "aarch64", ".dmg"), + (re.compile(r"^.*_x64\.app\.tar\.gz\.sig$"), "darwin", "x86_64", ".app.tar.gz.sig"), + (re.compile(r"^.*_x64\.app\.tar\.gz$"), "darwin", "x86_64", ".app.tar.gz"), + (re.compile(r"^.*_x64\.dmg$"), "darwin", "x86_64", ".dmg"), + (re.compile(r"^.*_amd64\.deb\.sig$"), "linux", "x86_64", ".deb.sig"), + (re.compile(r"^.*_amd64\.deb$"), "linux", "x86_64", ".deb"), + (re.compile(r"^.*_amd64\.AppImage\.sig$"), "linux", "x86_64", ".AppImage.sig"), + (re.compile(r"^.*_amd64\.AppImage$"), "linux", "x86_64", ".AppImage"), + (re.compile(r"^.*?\.x86_64\.rpm\.sig$"), "linux", "x86_64", ".rpm.sig"), + (re.compile(r"^.*?\.x86_64\.rpm$"), "linux", "x86_64", ".rpm"), + (re.compile(r"^.*_x64-setup\.exe\.sig$"), "windows", "x86_64", ".exe.sig"), + (re.compile(r"^.*_x64-setup\.exe$"), "windows", "x86_64", ".exe"), + (re.compile(r"^.*_x64_en-US\.msi\.sig$"), "windows", "x86_64", ".msi.sig"), + (re.compile(r"^.*_x64_en-US\.msi$"), "windows", "x86_64", ".msi"), + (re.compile(r"^.*\.flatpak$"), "linux", "x86_64", ".flatpak"), +] def artifact_target(path: Path, root: Path) -> tuple[str, str] | None: @@ -73,6 +95,28 @@ def stage_artifacts(source: Path, output: Path, version: str) -> dict[tuple[str, return staged +def stage_flat_artifacts(source: Path, output: Path, version: str) -> dict[tuple[str, str, str], Path]: + output.mkdir(parents=True, exist_ok=True) + staged: dict[tuple[str, str, str], Path] = {} + for path in sorted(source.iterdir()): + if not path.is_file(): + continue + match = next( + ((pattern, os_name, arch, suffix) for pattern, os_name, arch, suffix in FLAT_PATTERNS if pattern.match(path.name)), + None, + ) + if match is None: + continue + _, os_name, arch, suffix = match + key = (os_name, arch, suffix) + if key in staged: + raise ValueError(f"duplicate {os_name}-{arch}{suffix}: {staged[key]} and {path}") + destination = output / f"dsh-easy-desktop_{version}_{os_name}_{arch}{suffix}" + shutil.copy2(path, destination) + staged[key] = destination + return staged + + def build_manifest( staged: dict[tuple[str, str, str], Path], version: str, @@ -109,7 +153,8 @@ def build_manifest( def main() -> None: parser = argparse.ArgumentParser() - parser.add_argument("--artifacts", required=True, type=Path) + parser.add_argument("--artifacts", type=Path, help="matrix-layout artifact directory") + parser.add_argument("--flat-artifacts", type=Path, help="flat GitHub Release asset directory") parser.add_argument("--output", required=True, type=Path) parser.add_argument("--version", required=True) parser.add_argument("--package-base-url", required=True) @@ -117,7 +162,12 @@ def main() -> None: parser.add_argument("--pub-date") args = parser.parse_args() - staged = stage_artifacts(args.artifacts, args.output, args.version) + if (args.artifacts is None) == (args.flat_artifacts is None): + raise SystemExit("exactly one of --artifacts or --flat-artifacts is required") + if args.artifacts is not None: + staged = stage_artifacts(args.artifacts, args.output, args.version) + else: + staged = stage_flat_artifacts(args.flat_artifacts, args.output, args.version) manifest = build_manifest( staged, args.version, diff --git a/scripts/publish-gitea-actions.sh b/scripts/publish-gitea-actions.sh new file mode 100755 index 0000000..2206b80 --- /dev/null +++ b/scripts/publish-gitea-actions.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# Gitea Actions publisher: download a GitHub release's assets and publish +# them to the local Gitea instance over the LAN interface (bypassing the +# public reverse proxy, which times out on large uploads). +set -euo pipefail + +GITHUB_REPO="${GITHUB_REPO:-TommyFang2077/dsh-easy-desktop}" +REPO_OWNER="${GITEA_OWNER:-TomHanck4}" +REPO_NAME="${GITEA_REPO:-dsh-easy-desktop}" +GITEA_BASE_URL="${GITEA_BASE_URL:-http://192.168.30.33:3000}" +PACKAGE_BASE_URL="${PACKAGE_BASE_URL:-https://git.fangsiyuan.top/api/packages/TomHanck4/generic/dsh-easy-desktop-updater}" +RELEASE_TAG="${RELEASE_TAG:-latest}" +: "${GITEA_TOKEN:?GITEA_TOKEN is required}" + +if [[ "$RELEASE_TAG" == "latest" ]]; then + RELEASE_TAG=$(curl -fsS "https://api.github.com/repos/$GITHUB_REPO/releases?per_page=1" | jq -r '.[0].tag_name') +fi +[[ -n "$RELEASE_TAG" && "$RELEASE_TAG" != "null" ]] || { + echo "no GitHub release found" >&2 + exit 1 +} +VERSION="${RELEASE_TAG#v}" +echo "target: $RELEASE_TAG ($VERSION)" +echo "gitea: $GITEA_BASE_URL" + +if curl -fsS -H "Authorization: token $GITEA_TOKEN" \ + "$GITEA_BASE_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/releases/tags/$RELEASE_TAG" >/dev/null 2>&1; then + echo "release $RELEASE_TAG already published on Gitea; nothing to do" + exit 0 +fi + +rm -rf artifacts staged +mkdir -p artifacts + +curl -fsS "https://api.github.com/repos/$GITHUB_REPO/releases/tags/$RELEASE_TAG" -o /tmp/release.json +jq -r '.assets[].browser_download_url' /tmp/release.json | while read -r url; do + curl -fsSL --retry 3 --retry-delay 5 -o "artifacts/$(basename "$url")" "$url" & +done +wait +echo "downloaded $(find artifacts -type f | wc -l) assets" + +python3 build-gitea-update.py \ + --flat-artifacts artifacts \ + --output staged \ + --version "$VERSION" \ + --pub-date "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + --package-base-url "$PACKAGE_BASE_URL" + +bash publish-gitea-release.sh staged +echo "published $RELEASE_TAG to Gitea" \ No newline at end of file diff --git a/tests/test_release.py b/tests/test_release.py index f31a40e..a979663 100644 --- a/tests/test_release.py +++ b/tests/test_release.py @@ -68,5 +68,68 @@ class GiteaUpdateManifestTests(unittest.TestCase): self.assertTrue(windows["url"].endswith("/1.2.3/dsh-easy-desktop_1.2.3_windows_x86_64.exe")) + def test_normalizes_flat_github_assets(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + artifacts = root / "artifacts" + output = root / "staged" + artifacts.mkdir() + fixtures = { + "DeepSeek.Harness_0.1.1_aarch64.app.tar.gz": "darwin-aarch64", + "DeepSeek.Harness_0.1.1_aarch64.app.tar.gz.sig": "darwin-aarch64", + "DeepSeek.Harness_0.1.1_aarch64.dmg": "darwin-aarch64", + "DeepSeek.Harness_0.1.1_x64.app.tar.gz": "darwin-x86_64", + "DeepSeek.Harness_0.1.1_x64.app.tar.gz.sig": "darwin-x86_64", + "DeepSeek.Harness_0.1.1_x64.dmg": "darwin-x86_64", + "DeepSeek.Harness_0.1.1_amd64.deb": "linux-x86_64", + "DeepSeek.Harness_0.1.1_amd64.AppImage": "linux-x86_64", + "DeepSeek.Harness_0.1.1_amd64.AppImage.sig": "linux-x86_64", + "DeepSeek.Harness-0.1.1-1.x86_64.rpm": "linux-x86_64", + "DeepSeek.Harness_0.1.1_x64-setup.exe": "windows-x86_64", + "DeepSeek.Harness_0.1.1_x64-setup.exe.sig": "windows-x86_64", + "DeepSeek.Harness_0.1.1_x64_en-US.msi": "windows-x86_64", + "io.github.tommyfang.DshDesktop.flatpak": "linux-x86_64", + } + for name, marker in fixtures.items(): + (artifacts / name).write_bytes(marker.encode()) + + subprocess.run( + [ + "python3", + str(SCRIPT), + "--flat-artifacts", + str(artifacts), + "--output", + str(output), + "--version", + "1.2.3", + "--package-base-url", + "http://gitea.example/api/packages/u/generic/app", + ], + check=True, + ) + manifest = json.loads((output / "latest.json").read_text(encoding="utf-8")) + self.assertEqual( + set(manifest["platforms"]), + { + "darwin-aarch64-app", + "darwin-aarch64", + "darwin-x86_64-app", + "darwin-x86_64", + "linux-x86_64-appimage", + "linux-x86_64", + "windows-x86_64-nsis", + "windows-x86_64", + }, + ) + # Each updater target embeds its own .sig content; fixed-name + # assets (dmg/rpm/deb/flatpak) are deliverables only. + self.assertEqual( + manifest["platforms"]["linux-x86_64"]["signature"], + "linux-x86_64", + ) + self.assertNotIn("linux-x86_64-app", manifest["platforms"]) + + if __name__ == "__main__": unittest.main()