From abcbb811c8cfd20cf87eb00824dbc2333c50b0e7 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Thu, 26 Jun 2025 21:42:39 +0800 Subject: [PATCH] build: release sync to gitee --- .github/workflows/push_image.yml | 1 - .github/workflows/release_sync_gitee.py | 275 +++++++++++++++++++++++ .github/workflows/release_sync_gitee.yml | 27 +++ 3 files changed, 302 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/release_sync_gitee.py create mode 100644 .github/workflows/release_sync_gitee.yml diff --git a/.github/workflows/push_image.yml b/.github/workflows/push_image.yml index 0b5cb04a..52db22d6 100644 --- a/.github/workflows/push_image.yml +++ b/.github/workflows/push_image.yml @@ -16,7 +16,6 @@ on: jobs: build-and-push: runs-on: ubuntu-latest - steps: - name: Checkout code uses: actions/checkout@v4 diff --git a/.github/workflows/release_sync_gitee.py b/.github/workflows/release_sync_gitee.py new file mode 100644 index 00000000..e6af715b --- /dev/null +++ b/.github/workflows/release_sync_gitee.py @@ -0,0 +1,275 @@ +#!/usr/bin/env python3 +import logging +import json +import mimetypes +import tempfile +import os +import random +import re +import shutil +import time +from urllib import request +from urllib.error import HTTPError + +GITHUB_REPO = "certimate-go/certimate" +GITEE_REPO = "certimate-go/certimate" +GITEE_TOKEN = os.getenv("GITEE_TOKEN", "") + +SYNC_MARKER = "SYNCING FROM GITHUB, PLEASE WAIT ..." +TEMP_DIR = tempfile.mkdtemp() + +logging.basicConfig(level=logging.INFO) + + +def do_httpreq(url, method="GET", headers=None, data=None): + req = request.Request(url, data=data, method=method) + headers = headers or {} + for key, value in headers.items(): + req.add_header(key, value) + + try: + with request.urlopen(req) as resp: + resp_data = resp.read().decode("utf-8") + if resp_data: + try: + return json.loads(resp_data) + except json.JSONDecodeError: + pass + return None + except HTTPError as e: + errmsg = "" + if e.readable(): + try: + errmsg = e.read().decode('utf-8') + errmsg = errmsg.replace("\r", "\\r").replace("\n", "\\n") + except: + pass + logging.error(f"Error occurred when sending request: status={e.status}, response={errmsg}") + raise e + except Exception as e: + raise e + + +def get_github_stable_release(): + page = 1 + while True: + releases = do_httpreq( + url=f"https://api.github.com/repos/{GITHUB_REPO}/releases?page={page}&per_page=100", + headers={"Accept": "application/vnd.github+json"}, + ) + if not releases or len(releases) == 0: + break + + for release in releases: + release_name = release.get("name", "") + if re.match(r"^v[0-9]", release_name): + if any( + x in release_name + for x in ["alpha", "beta", "rc", "preview", "test", "unstable"] + ): + continue + return release + + page += 1 + + return None + + +def get_gitee_release_list(): + page = 1 + list = [] + while True: + releases = do_httpreq( + url=f"https://gitee.com/api/v5/repos/{GITEE_REPO}/releases?access_token={GITEE_TOKEN}&page={page}&per_page=100", + ) + if not releases or len(releases) == 0: + break + + list.extend(releases) + page += 1 + + return list + + +def get_gitee_release_by_tag(tag_name): + releases = get_gitee_release_list() + for release in releases: + if release.get("tag_name") == tag_name: + return release + + return None + + +def delete_gitee_release(release_info): + if not release_info: + raise ValueError("Release info is invalid") + + release_id = release_info.get("id", "") + release_name = release_info.get("tag_name", "") + if not release_id: + raise ValueError("Release ID is missing") + + attachpage = 1 + attachfiles = [] + while True: + releases = do_httpreq( + url=f"https://gitee.com/api/v5/repos/{GITEE_REPO}/releases/{release_id}/attach_files?access_token={GITEE_TOKEN}&page={attachpage}&per_page=100", + ) + if not releases or len(releases) == 0: + break + + attachfiles.extend(releases) + attachpage += 1 + + for attachfile in attachfiles: + attachfile_id = attachfile.get("id") + attachfile_name = attachfile.get("name") + logging.info("Trying to delete Gitee attach file: %s/%s", release_name, attachfile_name) + do_httpreq( + url=f"https://gitee.com/api/v5/repos/{GITEE_REPO}/releases/{release_id}/attach_files/{attachfile_id}?access_token={GITEE_TOKEN}", + method="DELETE", + ) + + logging.info("Trying to delete Gitee release: %s", release_name) + do_httpreq( + url=f"https://gitee.com/api/v5/repos/{GITEE_REPO}/releases/{release_id}?access_token={GITEE_TOKEN}", + method="DELETE", + ) + + +def create_gitee_release(name, tag, body, prerelease, gh_assets): + release_info = do_httpreq( + f"https://gitee.com/api/v5/repos/{GITEE_REPO}/releases?access_token={GITEE_TOKEN}", + method="POST", + headers={"Content-Type": "application/json"}, + data=json.dumps({ + "tag_name": tag, + "name": name, + "body": SYNC_MARKER, + "prerelease": prerelease, + "target_commitish": "", + }).encode("utf-8"), + ) + + if not release_info or "id" not in release_info: + return None + logging.info("Gitee release created") + + release_id = release_info["id"] + + assets_dir = os.path.join(TEMP_DIR, "assets") + os.makedirs(assets_dir, exist_ok=True) + + gh_assets = gh_assets or [] + for asset in gh_assets: + logging.info("Tring to download asset from GitHub: %s", asset["name"]) + + opener = request.build_opener() + request.install_opener(opener) + download_ts = time.time() + download_url = asset.get("browser_download_url") + download_path = os.path.join(assets_dir, asset["name"]) + def _hook(blocknum, blocksize, totalsize): + nonlocal download_ts + TIMESPAN = 5 # print progress every 5sec + ts = time.time() + pct = min(round(100 * blocknum * blocksize / totalsize, 2), 100) + if (ts - download_ts < TIMESPAN) and (pct < 100): + return + download_ts = ts + logging.info(f"Downloading {download_url} >>> {pct}%") + + # request.urlretrieve(download_url, download_path, _hook) + + for asset in gh_assets: + logging.info("Tring to upload asset to Gitee: %s", asset["name"]) + + boundary = '----boundary' + ''.join(random.choice('0123456789abcdef') for _ in range(16)) + print(f"Using boundary: {boundary}") + with open(os.path.join(assets_dir, asset["name"]), 'rb') as f: + attachfile_mime = mimetypes.guess_type(asset["name"])[0] or 'application/octet-stream' + attachfile_req = [] + attachfile_req.append(f"--{boundary}") + attachfile_req.append(f'Content-Disposition: form-data; name="file"; filename="{asset["name"]}"') + attachfile_req.append(f"Content-Type: {attachfile_mime}") + attachfile_req.append("") + attachfile_req.append(f.read().decode('latin-1')) + attachfile_req.append(f"--{boundary}--") + attachfile_req.append("") + attachfile_req = "\r\n".join(attachfile_req).encode('latin-1') + + do_httpreq( + f"https://gitee.com/api/v5/repos/{GITEE_REPO}/releases/{release_id}/attach_files?access_token={GITEE_TOKEN}", + method="POST", + headers={'Content-Type': f'multipart/form-data; boundary={boundary}'}, + data=attachfile_req, + ) + logging.info("Asset uploaded: %s", asset["name"]) + + release_info = do_httpreq( + f"https://gitee.com/api/v5/repos/{GITEE_REPO}/releases/{release_id}?access_token={GITEE_TOKEN}", + method="PATCH", + headers={"Content-Type": "application/json"}, + data=json.dumps({ + "tag_name": tag, + "name": name, + "body": f"**此发行版同步自 GitHub,完整变更日志请访问 https://github.com/{GITHUB_REPO}/releases/{tag} 查看。**\n\n**因 Gitee 存储空间容量有限,仅能保留最新一个发行版,如需其余版本请访问 GitHub 获取。**\n\n---\n\n" + body, + "prerelease": prerelease, + }).encode("utf-8"), + ) + logging.info("Gitee release updated") + return release_info + + +def main(): + try: + # 获取 GitHub 最新稳定发行版 + github_release = get_github_stable_release() + if not github_release: + logging.warning("GitHub stable release not found. Foget to release?") + return + else: + logging.info("GitHub stable release found: %s", github_release.get('name')) + + # 提取稳定版的信息 + release_name = github_release.get("name") + release_tag = github_release.get("tag_name") + release_body = github_release.get("body") + release_prerelease = github_release.get("prerelease", False) + release_assets = github_release.get("assets", []) + + # 检查 Gitee 是否已有同名发行版 + gitee_release = get_gitee_release_by_tag(release_tag) + if gitee_release and gitee_release.get("body") == SYNC_MARKER: + logging.warning("Gitee syncing release found, cleaning up...") + delete_gitee_release(gitee_release) + elif gitee_release: + logging.info("Gitee release already exists, exit.") + return + + # 同步发行版 + gitee_release = create_gitee_release(release_name, release_tag, release_body, release_prerelease, release_assets) + if not gitee_release: + logging.warning("Failed to create Gitee release.") + return + + # 清除历史发行版 + gitee_release_list = get_gitee_release_list() + for release in gitee_release_list: + if release.get("tag_name") == release_tag: + continue + else: + delete_gitee_release(release) + + logging.info("Sync release completed.") + + except Exception as e: + logging.fatal(str(e)) + + finally: + if os.path.exists(TEMP_DIR): + shutil.rmtree(TEMP_DIR) + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/release_sync_gitee.yml b/.github/workflows/release_sync_gitee.yml new file mode 100644 index 00000000..79df3eef --- /dev/null +++ b/.github/workflows/release_sync_gitee.yml @@ -0,0 +1,27 @@ +name: Release Sync to Gitee + +on: + release: + types: [published, unpublished, deleted] + workflow_dispatch: + +jobs: + sync-to-gitee: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python3 + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Run script + env: + GITEE_TOKEN: ${{ secrets.GITEE_TOKEN }} + run: | + cd .github/workflows + python ./release_sync_gitee.py