diff --git a/scripts/ci/_env.sh b/scripts/ci/_env.sh new file mode 100755 index 0000000..a06330f --- /dev/null +++ b/scripts/ci/_env.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# _env.sh — shared CI mirror env + tag-version helper. `source` this from other +# scripts; do not execute directly. Idempotent: only sets vars if not already +# provided by the environment (e.g. workflow-level overrides). + +export GOPROXY="${GOPROXY:-https://goproxy.cn,direct}" +export PUB_HOSTED_URL="${PUB_HOSTED_URL:-https://pub.flutter-io.cn}" +export FLUTTER_STORAGE_BASE_URL="${FLUTTER_STORAGE_BASE_URL:-https://storage.flutter-io.cn}" + +# Forgejo/Gitea repo coordinates (used by lib-forgejo.sh). Secrets +# FORGEJO_TOKEN / FORGEJO_URL are injected by the CI runner, not set here. +export FORGEJO_REPO="${FORGEJO_REPO:-wangjia/pangolin}" + +# Ensure Homebrew tools are on PATH (macOS runners); harmless elsewhere. +case ":${PATH}:" in + *":/opt/homebrew/bin:"*) ;; + *) export PATH="/opt/homebrew/bin:${PATH}" ;; +esac + +# ver_from_tag +# Strips an optional "refs/tags/" prefix, then the "-v" prefix, +# leaving a bare semver. Uses parameter expansion only (no command +# substitution). Examples: +# ver_from_tag server server-v1.2.3 -> 1.2.3 +# ver_from_tag server refs/tags/server-v1.2.3 -> 1.2.3 +ver_from_tag() { + local prefix="$1" ref="$2" + ref="${ref#refs/tags/}" + ref="${ref#"${prefix}"-v}" + printf '%s' "$ref" +} diff --git a/scripts/ci/lib-forgejo.sh b/scripts/ci/lib-forgejo.sh new file mode 100755 index 0000000..6f453dc --- /dev/null +++ b/scripts/ci/lib-forgejo.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +# lib-forgejo.sh — Forgejo/Gitea release API helpers shared by the pangolin +# release pipelines (website / server / client). `source` this after _env.sh. +# +# Provides: +# forgejo_release_ensure -> looks up or creates the release +# for <tag>; sets/exports RELEASE_ID +# forgejo_upload_asset <tag> <file> -> uploads one asset to that release +# +# Requires env: FORGEJO_URL, FORGEJO_TOKEN, FORGEJO_REPO (see _env.sh for the +# default FORGEJO_REPO=wangjia/pangolin). +# +# No command substitution ($()) is used anywhere: HTTP status codes and JSON +# fields are written to temp files by curl/python3 and read back with `read`. + +: "${FORGEJO_REPO:=wangjia/pangolin}" + +# forgejo_release_ensure <tag> <title> +forgejo_release_ensure() { + local tag="$1" title="$2" + local get_code_file get_body_file get_code + + get_code_file="/tmp/forgejo_get_code.$$" + get_body_file="/tmp/forgejo_get_body.$$.json" + + curl -k -s -o "$get_body_file" -w '%{http_code}' \ + -H "Authorization: token ${FORGEJO_TOKEN}" \ + "${FORGEJO_URL}/api/v1/repos/${FORGEJO_REPO}/releases/tags/${tag}" \ + > "$get_code_file" + read -r get_code < "$get_code_file" + rm -f "$get_code_file" + + if [ "$get_code" = "200" ]; then + echo "==> forgejo: release ${tag} already exists" + _forgejo_read_release_id "$get_body_file" + rm -f "$get_body_file" + return 0 + fi + rm -f "$get_body_file" + + echo "==> forgejo: creating release ${tag}" + local create_code_file create_body_file create_code + create_code_file="/tmp/forgejo_create_code.$$" + create_body_file="/tmp/forgejo_create_body.$$.json" + + curl -k -s -o "$create_body_file" -w '%{http_code}' \ + -X POST "${FORGEJO_URL}/api/v1/repos/${FORGEJO_REPO}/releases" \ + -H "Authorization: token ${FORGEJO_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{\"tag_name\":\"${tag}\",\"name\":\"${title}\",\"draft\":false,\"prerelease\":false}" \ + > "$create_code_file" + read -r create_code < "$create_code_file" + rm -f "$create_code_file" + + if [ "$create_code" -lt 200 ] || [ "$create_code" -ge 300 ]; then + echo "==> forgejo: release create FAILED (HTTP ${create_code})" >&2 + cat "$create_body_file" >&2 + rm -f "$create_body_file" + return 1 + fi + + _forgejo_read_release_id "$create_body_file" + rm -f "$create_body_file" + echo "==> forgejo: release_id=${RELEASE_ID}" +} + +# _forgejo_read_release_id <json_file> — internal: sets/exports RELEASE_ID. +_forgejo_read_release_id() { + local json_file="$1" id_file + id_file="/tmp/forgejo_release_id.$$" + python3 -c "import json,sys; print(json.load(open(sys.argv[1]))['id'])" \ + "$json_file" > "$id_file" + read -r RELEASE_ID < "$id_file" + rm -f "$id_file" + export RELEASE_ID +} + +# forgejo_upload_asset <tag> <file> +forgejo_upload_asset() { + local tag="$1" file="$2" + + if [ -z "${RELEASE_ID:-}" ]; then + forgejo_release_ensure "$tag" "$tag" || return 1 + fi + + local code_file body_file code + code_file="/tmp/forgejo_upload_code.$$" + body_file="/tmp/forgejo_upload_body.$$.json" + + curl -k -s -o "$body_file" -w '%{http_code}' \ + -X POST "${FORGEJO_URL}/api/v1/repos/${FORGEJO_REPO}/releases/${RELEASE_ID}/assets" \ + -H "Authorization: token ${FORGEJO_TOKEN}" \ + -F "attachment=@${file}" \ + > "$code_file" + read -r code < "$code_file" + rm -f "$code_file" + + echo "==> forgejo: uploaded ${file} (HTTP ${code})" + if [ "$code" -lt 200 ] || [ "$code" -ge 300 ]; then + cat "$body_file" >&2 + rm -f "$body_file" + return 1 + fi + rm -f "$body_file" +} diff --git a/scripts/ci/notify.sh b/scripts/ci/notify.sh new file mode 100755 index 0000000..bb8b725 --- /dev/null +++ b/scripts/ci/notify.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# notify.sh — Telegram CI notifications. `source` this from other scripts. +# +# Provides: +# notify_ok <msg> -> send a "success" notification +# notify_fail <msg> -> send a "failure" notification +# +# Uses the same TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID convention as +# deploy/bootstrap/monitor/pangolin-monitor.sh. Safe no-op (never fails the +# pipeline) when those are unset, or when the Telegram API call itself fails. + +_notify_send() { + local text="$1" + if [ -z "${TELEGRAM_BOT_TOKEN:-}" ] || [ -z "${TELEGRAM_CHAT_ID:-}" ]; then + echo "==> notify: (skipped, no TELEGRAM_BOT_TOKEN/TELEGRAM_CHAT_ID) ${text}" + return 0 + fi + curl -fsS --max-time 15 \ + "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \ + --data-urlencode "chat_id=${TELEGRAM_CHAT_ID}" \ + --data-urlencode "text=${text}" \ + --data "disable_web_page_preview=true" >/dev/null 2>&1 \ + || echo "==> notify: Telegram 发送失败(已忽略,不影响流水线)" >&2 +} + +# notify_ok <msg> +notify_ok() { + local msg="$1" + _notify_send "✅ Pangolin CI: ${msg}" +} + +# notify_fail <msg> +notify_fail() { + local msg="$1" + _notify_send "❌ Pangolin CI: ${msg}" +}