Files
nyaterm/.github/workflows/publish-gitee-release.yml
Kang b966ed458f ci: improve Python dependency installation in Gitee release workflow
- Enhanced the installation process by creating a new virtual environment for Python dependencies, ensuring a clean setup.
- Added environment variables to disable pip version checks and input prompts.
- Upgraded pip, setuptools, and wheel, and specified exact versions for requests and requests-toolbelt.
- Included checks for installed packages to verify successful installation.
2026-07-22 13:28:08 +08:00

708 lines
23 KiB
YAML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
name: Publish Gitee Release
on:
workflow_run:
workflows: ["Build and Release"]
types: [completed]
workflow_dispatch:
inputs:
tag:
description: "Release tag, for example v1.1.14"
required: true
type: string
upload_small_assets:
description: "Upload assets smaller than MAX_GITEE_ASSET_MB to Gitee"
required: false
default: "true"
type: choice
options:
- "true"
- "false"
concurrency:
group: gitee-release-${{ github.event.workflow_run.id || inputs.tag || github.run_id }}
cancel-in-progress: false
permissions:
contents: read
env:
GITEE_OWNER: ${{ vars.GITEE_OWNER }}
GITEE_REPO: ${{ vars.GITEE_REPO }}
GITEE_API_BASE: ${{ vars.GITEE_API_BASE || 'https://gitee.com/api/v5' }}
MAX_GITEE_ASSET_MB: ${{ vars.MAX_GITEE_ASSET_MB || '99' }}
R2_PUBLIC_BASE_URL: ${{ vars.R2_PUBLIC_BASE_URL }}
UPLOAD_SMALL_ASSETS: ${{ inputs.upload_small_assets || 'true' }}
FAIL_ON_GITEE_ASSET_UPLOAD_ERROR: "false"
jobs:
publish-gitee-release:
name: Create or update Gitee Release
if: >-
${{
github.event_name == 'workflow_dispatch' ||
(
github.event_name == 'workflow_run' &&
github.event.workflow_run.conclusion == 'success'
)
}}
runs-on: [self-hosted, linux, x64, gitee-release]
steps:
- name: Resolve release tag
env:
MANUAL_TAG: ${{ inputs.tag }}
UPSTREAM_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
shell: bash
run: |
set -euo pipefail
if [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" ]]; then
TAG="${MANUAL_TAG}"
else
HEAD_SHA="${UPSTREAM_HEAD_SHA}"
if [[ -z "$HEAD_SHA" ]]; then
echo "::error::Upstream workflow head SHA is empty"
exit 1
fi
echo "Resolving release tag for upstream commit: ${HEAD_SHA}"
TAG="$(
git ls-remote --tags "https://github.com/${GITHUB_REPOSITORY}.git" |
awk -v sha="$HEAD_SHA" '
$1 == sha {
ref = $2
sub(/^refs\/tags\//, "", ref)
sub(/\^\{\}$/, "", ref)
print ref
}
' |
sort -Vu |
tail -n 1
)"
fi
if [[ -z "$TAG" ]]; then
echo "::error::Unable to resolve release tag"
echo "::error::For workflow_run, make sure Build and Release runs against the tagged commit."
exit 1
fi
echo "TAG=$TAG" >> "$GITHUB_ENV"
echo "Publishing Gitee release for tag: $TAG"
- name: Validate Gitee config
shell: bash
run: |
set -euo pipefail
if [[ -z "${GITEE_OWNER}" ]]; then
echo "::error::GITEE_OWNER is empty. Please set it in GitHub repository Variables."
exit 1
fi
if [[ -z "${GITEE_REPO}" ]]; then
echo "::error::GITEE_REPO is empty. Please set it in GitHub repository Variables."
exit 1
fi
echo "Gitee repository: ${GITEE_OWNER}/${GITEE_REPO}"
- name: Prepare clean workspace
shell: bash
run: |
set -euo pipefail
rm -rf release-assets
rm -f github-release.json github-release.json.tmp
rm -rf "$RUNNER_TEMP/gitee-release-venv"
mkdir -p release-assets
- name: Wait for GitHub release assets
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
set -euo pipefail
echo "Waiting for GitHub release and assets: ${TAG}"
for i in {1..30}; do
rm -f github-release.json.tmp
if gh release view "$TAG" \
--repo "$GITHUB_REPOSITORY" \
--json tagName,name,body,isPrerelease,isDraft,url,targetCommitish,assets \
> github-release.json.tmp; then
IS_DRAFT="$(jq -r '.isDraft' github-release.json.tmp)"
ASSET_COUNT="$(jq '.assets | length' github-release.json.tmp)"
echo "GitHub release found: draft=${IS_DRAFT}, assets=${ASSET_COUNT}"
if [[ "$IS_DRAFT" == "false" && "$ASSET_COUNT" -gt 0 ]]; then
mv github-release.json.tmp github-release.json
echo "GitHub release metadata:"
jq '{
tagName,
name,
isPrerelease,
isDraft,
url,
targetCommitish,
assetCount: (.assets | length)
}' github-release.json
exit 0
fi
else
echo "GitHub release ${TAG} is not available yet."
fi
echo "Release assets are not ready, waiting 10s... ($i/30)"
sleep 10
done
echo "::error::GitHub release ${TAG} did not expose any assets after waiting."
echo "::error::Make sure Build and Release uploads all assets before the workflow completes."
exit 1
- name: Download GitHub release assets
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
set -euo pipefail
gh release download "$TAG" \
--repo "$GITHUB_REPOSITORY" \
--dir release-assets \
--clobber
ASSET_COUNT="$(find release-assets -maxdepth 1 -type f | wc -l)"
if [[ "$ASSET_COUNT" -eq 0 ]]; then
echo "::error::No GitHub release assets were downloaded for ${TAG}"
exit 1
fi
echo "Downloaded assets (${ASSET_COUNT}):"
find release-assets -maxdepth 1 -type f -printf '%f %s bytes\n' | sort
- name: Wait for Gitee mirror tag
shell: bash
run: |
set -euo pipefail
echo "Waiting for Gitee mirror tag: ${TAG}"
for i in {1..30}; do
if git ls-remote --tags \
"https://gitee.com/${GITEE_OWNER}/${GITEE_REPO}.git" \
"refs/tags/${TAG}" |
grep -q "refs/tags/${TAG}$"; then
echo "Gitee tag found: ${TAG}"
exit 0
fi
echo "Gitee tag ${TAG} not found yet, waiting 20s... ($i/30)"
sleep 20
done
echo "::error::Gitee tag ${TAG} was not found after waiting."
echo "::error::Make sure GitHub -> Gitee mirror has synced tags."
exit 1
- name: Install Python dependencies
shell: bash
env:
PIP_DISABLE_PIP_VERSION_CHECK: "1"
PIP_NO_INPUT: "1"
run: |
set -euo pipefail
VENV_DIR="$RUNNER_TEMP/gitee-release-venv"
PYTHON_BIN="$VENV_DIR/bin/python"
# 防止自托管 Runner 残留损坏或不完整的虚拟环境
rm -rf "$VENV_DIR"
python3 -m venv "$VENV_DIR"
echo "Python:"
"$PYTHON_BIN" --version
echo "Effective pip configuration:"
"$PYTHON_BIN" -m pip config debug || true
"$PYTHON_BIN" -m pip install \
--upgrade \
--no-cache-dir \
--index-url "https://pypi.org/simple" \
pip setuptools wheel
"$PYTHON_BIN" -m pip install \
--upgrade \
--no-cache-dir \
--index-url "https://pypi.org/simple" \
"requests==2.34.2" \
"requests-toolbelt==1.0.0"
"$PYTHON_BIN" -m pip check
echo "PYTHON_BIN=$PYTHON_BIN" >> "$GITHUB_ENV"
- name: Create or update Gitee release and upload assets
env:
GITEE_TOKEN: ${{ secrets.GITEE_TOKEN }}
shell: bash
run: |
"$PYTHON_BIN" <<'PY'
import json
import mimetypes
import os
import pathlib
import sys
import time
import urllib.parse
import requests
from requests_toolbelt.multipart.encoder import MultipartEncoder, MultipartEncoderMonitor
def require_env(name: str) -> str:
value = os.environ.get(name, "").strip()
if not value:
print(f"::error::{name} is required")
sys.exit(1)
return value
token = require_env("GITEE_TOKEN")
owner = require_env("GITEE_OWNER")
repo = require_env("GITEE_REPO")
tag = require_env("TAG")
api_base = os.environ.get("GITEE_API_BASE", "https://gitee.com/api/v5").rstrip("/")
r2_public_base_url = os.environ.get("R2_PUBLIC_BASE_URL", "").strip().rstrip("/")
max_mb = float(os.environ.get("MAX_GITEE_ASSET_MB", "99"))
max_bytes = int(max_mb * 1024 * 1024)
upload_small_assets = os.environ.get("UPLOAD_SMALL_ASSETS", "true").lower() == "true"
fail_on_upload_error = os.environ.get("FAIL_ON_GITEE_ASSET_UPLOAD_ERROR", "false").lower() == "true"
assets_dir = pathlib.Path("release-assets")
release_json_path = pathlib.Path("github-release.json")
if not release_json_path.exists():
print("::error::github-release.json not found")
sys.exit(1)
release = json.loads(release_json_path.read_text(encoding="utf-8"))
release_name = release.get("name") or tag
github_body = release.get("body") or ""
is_prerelease = bool(release.get("isPrerelease"))
target_commitish = release.get("targetCommitish") or ""
owner_q = urllib.parse.quote(owner, safe="")
repo_q = urllib.parse.quote(repo, safe="")
tag_q = urllib.parse.quote(tag, safe="")
session = requests.Session()
def gitee_request(method: str, path: str, *, expected=(200, 201), allow_404=False, **kwargs):
url = f"{api_base}{path}"
params = kwargs.pop("params", {}) or {}
params["access_token"] = token
response = session.request(
method,
url,
params=params,
timeout=120,
**kwargs,
)
if allow_404 and response.status_code == 404:
return None
if response.status_code not in expected:
print(f"::error::Gitee API failed: {method} {path} -> HTTP {response.status_code}")
print(response.text[:3000])
sys.exit(1)
if response.text.strip():
try:
return response.json()
except Exception:
return response.text
return None
def human_size(size: int) -> str:
units = ["B", "KB", "MB", "GB"]
value = float(size)
for unit in units:
if value < 1024 or unit == units[-1]:
if unit == "B":
return f"{int(value)} {unit}"
return f"{value:.2f} {unit}"
value /= 1024
return f"{size} B"
def make_upload_progress_callback(filename: str, total_bytes: int):
started_at = time.monotonic()
state = {
"last_percent": -1,
"last_report_at": 0.0,
}
def callback(monitor: MultipartEncoderMonitor):
if total_bytes <= 0:
return
now = time.monotonic()
uploaded = min(monitor.bytes_read, total_bytes)
percent = int(uploaded * 100 / total_bytes)
should_report = (
percent == 0
or percent == 100
or percent >= state["last_percent"] + 10
or now - state["last_report_at"] >= 15
)
if not should_report:
return
elapsed = max(now - started_at, 0.001)
speed = uploaded / elapsed
print(
f"Upload progress: {filename}: "
f"{percent}% "
f"({human_size(uploaded)}/{human_size(total_bytes)}, "
f"{human_size(int(speed))}/s)",
flush=True,
)
state["last_percent"] = percent
state["last_report_at"] = now
return callback
def build_r2_url(filename: str) -> str:
if not r2_public_base_url:
return ""
return (
f"{r2_public_base_url}/"
f"{urllib.parse.quote(tag, safe='')}/"
f"{urllib.parse.quote(filename, safe='')}"
)
asset_files = []
if assets_dir.exists():
asset_files = sorted(
[p for p in assets_dir.iterdir() if p.is_file()],
key=lambda p: p.name.lower(),
)
def build_release_body(
*,
uploadable_assets,
skipped_assets,
failed_assets,
no_assets=False,
uploading=False,
) -> str:
body = github_body.rstrip()
lines = []
lines.append("")
lines.append("---")
lines.append("")
lines.append("## 下载")
lines.append("")
if uploading:
lines.append("> 正在同步发行版附件,请稍后刷新页面。")
return body + "\n" + "\n".join(lines) + "\n"
if no_assets:
lines.append("- 本次 GitHub Release 未检测到附件。")
return body + "\n" + "\n".join(lines) + "\n"
if uploadable_assets:
lines.append("> 安装包已同步到本发行版附件区,请在本页附件区下载。")
elif not upload_small_assets:
lines.append("> 本次未上传附件到 Gitee Release 附件区。")
else:
lines.append("> 本次没有可上传到 Gitee Release 附件区的附件。")
fallback_lines = []
for file_path in skipped_assets:
size_text = human_size(file_path.stat().st_size)
r2_url = build_r2_url(file_path.name)
if r2_url:
fallback_lines.append(f"- [{file_path.name}]({r2_url}) `{size_text}`")
else:
fallback_lines.append(f"- `{file_path.name}` `{size_text}`(未上传到 Gitee 附件区)")
for file_path, reason in failed_assets:
size_text = human_size(file_path.stat().st_size)
r2_url = build_r2_url(file_path.name)
if r2_url:
fallback_lines.append(f"- [{file_path.name}]({r2_url}) `{size_text}`(Gitee 附件同步失败)")
else:
fallback_lines.append(f"- `{file_path.name}` `{size_text}`(Gitee 附件同步失败:{reason})")
if fallback_lines:
lines.append("")
lines.append("### 备用下载")
lines.append("")
lines.extend(fallback_lines)
return body + "\n" + "\n".join(lines) + "\n"
existing = gitee_request(
"GET",
f"/repos/{owner_q}/{repo_q}/releases/tags/{tag_q}",
expected=(200,),
allow_404=True,
)
initial_body = build_release_body(
uploadable_assets=[],
skipped_assets=[],
failed_assets=[],
no_assets=not bool(asset_files),
uploading=bool(asset_files),
)
if existing:
release_id = existing.get("id")
print(f"Updating existing Gitee release: id={release_id}, tag={tag}")
updated = gitee_request(
"PATCH",
f"/repos/{owner_q}/{repo_q}/releases/{release_id}",
data={
"tag_name": tag,
"name": release_name,
"body": initial_body,
"prerelease": str(is_prerelease).lower(),
},
expected=(200, 201),
)
if isinstance(updated, dict) and updated.get("id"):
release_id = updated["id"]
else:
print(f"Creating Gitee release: tag={tag}")
payload = {
"tag_name": tag,
"name": release_name,
"body": initial_body,
"prerelease": str(is_prerelease).lower(),
}
if target_commitish:
payload["target_commitish"] = target_commitish
created = gitee_request(
"POST",
f"/repos/{owner_q}/{repo_q}/releases",
data=payload,
expected=(200, 201),
)
if not isinstance(created, dict) or not created.get("id"):
print("::error::Gitee release was created but release id was not returned")
print(created)
sys.exit(1)
release_id = created["id"]
print(f"Gitee release id: {release_id}")
if not asset_files:
print("No assets to upload.")
final_body = build_release_body(
uploadable_assets=[],
skipped_assets=[],
failed_assets=[],
no_assets=True,
uploading=False,
)
gitee_request(
"PATCH",
f"/repos/{owner_q}/{repo_q}/releases/{release_id}",
data={
"tag_name": tag,
"name": release_name,
"body": final_body,
"prerelease": str(is_prerelease).lower(),
},
expected=(200, 201),
)
sys.exit(0)
uploadable_files = []
skipped_files = []
if upload_small_assets:
for file_path in asset_files:
size = file_path.stat().st_size
if size <= max_bytes:
uploadable_files.append(file_path)
else:
skipped_files.append(file_path)
else:
skipped_files = asset_files[:]
for file_path in skipped_files:
print(
f"Skip asset: {file_path.name} "
f"({human_size(file_path.stat().st_size)}, threshold={max_mb} MB, upload_small_assets={upload_small_assets})"
)
uploaded_or_existing_files = []
failed_files = []
def upload_asset(file_path: pathlib.Path) -> tuple[bool, str]:
filename = file_path.name
content_type = mimetypes.guess_type(filename)[0] or "application/octet-stream"
for attempt in range(1, 4):
print(f"Uploading asset to Gitee: {filename}, attempt {attempt}/3")
try:
with file_path.open("rb") as file_handle:
encoder = MultipartEncoder(
fields={
"name": filename,
"file": (filename, file_handle, content_type),
}
)
monitor = MultipartEncoderMonitor(
encoder,
make_upload_progress_callback(filename, encoder.len),
)
response = session.post(
f"{api_base}/repos/{owner_q}/{repo_q}/releases/{release_id}/attach_files",
params={"access_token": token},
data=monitor,
headers={
"Content-Type": monitor.content_type,
},
timeout=300,
)
except Exception as exc:
reason = str(exc)
if attempt < 3:
print(f"::warning::Upload failed with exception: {reason}. Retrying...")
time.sleep(5 * attempt)
continue
return False, reason
if response.status_code in (200, 201):
print(f"Uploaded: {filename}")
return True, "uploaded"
text = response.text or ""
lowered = text.lower()
duplicate_markers = [
"already",
"exist",
"duplicate",
"taken",
"已存在",
"重复",
]
if response.status_code in (400, 409, 422) and any(marker in lowered or marker in text for marker in duplicate_markers):
print(f"::warning::Asset may already exist, treat as available: {filename}")
print(text[:1000])
return True, "already_exists"
if attempt < 3:
print(f"::warning::Upload failed: HTTP {response.status_code}. Retrying...")
print(text[:1000])
time.sleep(5 * attempt)
continue
return False, f"HTTP {response.status_code}: {text[:1000]}"
for file_path in uploadable_files:
ok, reason = upload_asset(file_path)
if ok:
uploaded_or_existing_files.append(file_path)
else:
message = f"Upload failed permanently: {file_path.name}, {reason}"
if fail_on_upload_error:
print(f"::error::{message}")
sys.exit(1)
print(f"::warning::{message}")
failed_files.append((file_path, reason))
final_body = build_release_body(
uploadable_assets=uploaded_or_existing_files,
skipped_assets=skipped_files,
failed_assets=failed_files,
no_assets=False,
uploading=False,
)
gitee_request(
"PATCH",
f"/repos/{owner_q}/{repo_q}/releases/{release_id}",
data={
"tag_name": tag,
"name": release_name,
"body": final_body,
"prerelease": str(is_prerelease).lower(),
},
expected=(200, 201),
)
print("Gitee release publish completed.")
print(f"Uploaded or existing assets: {len(uploaded_or_existing_files)}")
print(f"Skipped assets: {len(skipped_files)}")
print(f"Failed assets: {len(failed_files)}")
PY
- name: Cleanup release assets
if: always()
shell: bash
run: |
rm -rf release-assets
rm -f github-release.json
rm -rf "$RUNNER_TEMP/gitee-release-venv"