feat(ci): 客户端 CI 脚手架(Android+Windows, 照 jiu)——起草+Android 本地验证

- compile-android.sh: 建 libbox.aar + split-per-abi 取 arm64-v8a(~82MB, 避 232MB
  universal) + release 签名(RELEASE_KEYSTORE/KEY_PASSWORD)。本地已验证构建通过、
  libbox.so 打进 APK。
- compile-windows.sh: 复用 client/windows/installer/pangolin.iss(Inno Setup),不签名。
- release-client.sh / deploy-client.sh: 复用 pangolin lib-forgejo/lib-ssh,传 Gitea
  release + 部署最新到 pangolin1:/var/lib/pangolin/downloads(仅留最新)。
- deploy-client.yml: client-v* tag → build-android(mac)+build-windows(windows) → release-deploy(mac)。
- build.gradle: 补 release 签名 config(读 key.properties, 缺则回退 debug)。
- 待确认: keystore alias/密码(见脚本 TODO);runner 需提到 user 级;服务器 /downloads + 官网链接下一步。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
This commit is contained in:
wangjia
2026-07-06 19:40:34 +08:00
parent 973c2c021b
commit 2698116696
8 changed files with 564 additions and 2 deletions
+5 -1
View File
@@ -55,7 +55,11 @@ jobs:
/mnt/scripts/ci/release-server.sh \
/mnt/scripts/ci/deploy-server.sh \
/mnt/scripts/ci/test.sh \
/mnt/scripts/ci/backup-db.sh
/mnt/scripts/ci/backup-db.sh \
/mnt/scripts/ci/compile-android.sh \
/mnt/scripts/ci/compile-windows.sh \
/mnt/scripts/ci/release-client.sh \
/mnt/scripts/ci/deploy-client.sh
- name: shellcheck CI 脚本(ci/)
run: |
+117
View File
@@ -0,0 +1,117 @@
name: Deploy Client
# Mirrors ~/code/jiu/.gitea/workflows/deploy-client.yml's tag→build→release→
# deploy shape, trimmed to the two platforms drafted so far (Android +
# Windows — no client-web/macOS/iOS jobs; pangolin has no Flutter-web build
# in this pipeline and macOS/iOS are a separate not-yet-drafted phase, see
# docs/superpowers/plans/2026-07-05-cicd.md Phase 3).
#
# TODO(controller) — RUNNER AVAILABILITY: per docs/ci-runner.md, pangolin
# currently has exactly ONE registered Gitea Actions runner
# ("mac-pangolin-2", label `nas:host`). Neither `runs-on: mac` nor
# `runs-on: windows` below has any runner registered to pick it up yet — this
# workflow will queue forever until that's fixed. Options: (a) register
# mac-pangolin-2 with an additional `mac` label (it's already a mac host —
# cheapest fix for build-android/release-deploy) and separately stand up +
# register an actual Windows host runner labeled `windows` for build-windows
# (no such machine exists per docs/ci-runner.md), or (b) repoint both at
# `nas` and accept that Android/Windows builds then compete with the
# docker-in-domain nas jobs on the same single mac host. This mirrors the
# `runs-on: mac` / `runs-on: windows` split already planned in
# docs/superpowers/plans/2026-07-05-cicd.md Task 7/10 — written that way here
# for fidelity to that plan, NOT because the runners are confirmed to exist.
on:
push:
tags:
- 'client-v[0-9]*.[0-9]*.[0-9]*'
workflow_dispatch:
concurrency:
group: deploy-client
cancel-in-progress: false
jobs:
build-android:
runs-on: mac
env:
GOPROXY: https://goproxy.cn,direct
PUB_HOSTED_URL: https://pub.flutter-io.cn
FLUTTER_STORAGE_BASE_URL: https://storage.flutter-io.cn
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Compile (Android APK)
env:
RELEASE_KEYSTORE: ${{ secrets.RELEASE_KEYSTORE }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
run: bash scripts/ci/compile-android.sh "${{ gitea.ref_name }}"
- name: Upload android artifact
uses: actions/upload-artifact@v3
with:
name: android
path: dist/
build-windows:
runs-on: windows
env:
GOPROXY: https://goproxy.cn,direct
PUB_HOSTED_URL: https://pub.flutter-io.cn
FLUTTER_STORAGE_BASE_URL: https://storage.flutter-io.cn
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Compile (Windows installer)
shell: bash
run: bash scripts/ci/compile-windows.sh "${{ gitea.ref_name }}"
- name: Upload windows artifact
uses: actions/upload-artifact@v3
with:
name: windows
path: dist/
release-deploy:
needs: [build-android, build-windows]
runs-on: mac
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download android artifact
uses: actions/download-artifact@v3
with:
name: android
path: dist/
- name: Download windows artifact
uses: actions/download-artifact@v3
with:
name: windows
path: dist/
- name: Release → Forgejo
env:
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
FORGEJO_URL: ${{ secrets.FORGEJO_URL }}
run: bash scripts/ci/release-client.sh "${{ gitea.ref_name }}"
- name: Deploy → pangolin1 (downloads/)
env:
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
run: bash scripts/ci/deploy-client.sh "${{ gitea.ref_name }}"
- name: Notify
if: always()
env:
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
run: |
. scripts/ci/notify.sh
if [ "${{ job.status }}" = "success" ]; then
notify_ok "client ${{ gitea.ref_name }} released + deployed"
else
notify_fail "client ${{ gitea.ref_name }} pipeline failed"
fi
+8
View File
@@ -56,6 +56,14 @@ client/android/app/src/main/java/
client/ios/Flutter/ephemeral/
client/pubspec.lock
# Android release 签名材料(本地或 CI 落盘,绝不入库)
client/android/key.properties
client/android/*.jks
client/android/*.keystore
# CI 客户端产物暂存目录(scripts/ci/compile-{android,windows}.sh 输出,构建期生成)
/dist/
# Claude worktrees(临时工作目录,本地专用)
.claude/worktrees/
+25 -1
View File
@@ -22,6 +22,19 @@ if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
// ── release 签名(key.properties 不入库,由本地或 CI 生成)────────────────
// 缺失时回退 debug 签名,保证本地 `flutter run --release`/无密钥环境仍可构建。
// CI 侧由 scripts/ci/compile-android.sh 落盘:client/android/key.properties
// (storeFile/storePassword/keyAlias/keyPassword)。
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
def hasReleaseSigning = keystorePropertiesFile.exists()
if (hasReleaseSigning) {
keystorePropertiesFile.withReader('UTF-8') { reader ->
keystoreProperties.load(reader)
}
}
android {
namespace "com.pangolin.pangolin_vpn"
compileSdkVersion flutter.compileSdkVersion
@@ -49,9 +62,20 @@ android {
versionName flutterVersionName
}
signingConfigs {
if (hasReleaseSigning) {
release {
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
}
}
}
buildTypes {
release {
signingConfig signingConfigs.debug
signingConfig hasReleaseSigning ? signingConfigs.release : signingConfigs.debug
}
}
}
+156
View File
@@ -0,0 +1,156 @@
#!/usr/bin/env bash
# compile-android.sh <tag> — build+embed libbox.aar, build a signed Flutter
# Android APK, package into dist/.
#
# Mirrors ~/code/jiu/scripts/ci/compile-android.sh, adapted for pangolin:
# - version derived via pangolin's ver_from_tag (client-v* tag; _env.sh),
# not jiu's ad-hoc `${TAG#client-v}` strip.
# - pangolin embeds sing-box as a native library (io.nekohasekai.libbox,
# gomobile bind) — jiu's Flutter app has no embedded core at all. This
# script MUST build+deploy libbox.aar (scripts/build-libbox.sh android)
# before `flutter build apk`, or the APK links no VPN kernel at all.
# Needs JDK 17 (gomobile bind javac step) — see CLAUDE.md "client/ 移动端"
# and "已知坑" for why: JDK 21 is rejected outright, NDK 27 fails link.
# - signing secrets: RELEASE_KEYSTORE (base64) + KEY_PASSWORD are the
# secret names given for this task ("actual configured" names). jiu uses
# 4 separate secrets (ANDROID_KEYSTORE_BASE64/ANDROID_KEYSTORE_PASSWORD/
# ANDROID_KEY_ALIAS/ANDROID_KEY_PASSWORD), and pangolin's OWN prior CI
# design docs (docs/cicd-design.html, docs/superpowers/plans/2026-07-05-
# cicd.md, docs/superpowers/specs/2026-07-05-cicd-design.md) planned the
# same 4-secret jiu-style naming. That is a real conflict with this
# task's brief — see the TODO(controller) block below and the draft
# report. This script follows the 2-secret brief as instructed, assuming
# storePassword == keyPassword == KEY_PASSWORD and a derivable keyAlias
# (default "pangolin", overridable via optional RELEASE_KEY_ALIAS).
# - dart-define is PANGOLIN_API_URL (client/lib/services/api_config.dart),
# not jiu's BASE_URL/PUBLIC_URL/APP_VERSION trio.
# - build.gradle previously signed release builds with signingConfigs.debug
# (no release signing config existed at all) — this task's draft also
# wires client/android/app/build.gradle to add a `release` signingConfig
# that reads client/android/key.properties (falls back to debug when
# key.properties is absent, so local/no-secret builds still work). See
# report for the exact diff.
# - universal APK (`flutter build apk --release`, no --split-per-abi) — see
# report for the size-vs-single-stable-URL tradeoff discussion; this
# deviates from docs/superpowers/plans/2026-07-05-cicd.md Task 7 which
# had planned --split-per-abi.
# - output dist/pangolin-android.apk (stable name; deploy-client.sh keeps
# only the latest one under this name on pangolin1).
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=scripts/ci/_env.sh
. "${SCRIPT_DIR}/_env.sh"
TAG="${1:?usage: compile-android.sh <tag>}"
# No $() anywhere (repo convention): ver_from_tag prints to stdout, captured
# via a temp file + `read`, same pattern as release-server.sh.
ver_file="/tmp/compile_android_ver.$$"
ver_from_tag client "$TAG" > "$ver_file"
VER=""
# `|| true`: ver_from_tag uses printf '%s' (no trailing newline); `read`
# hitting EOF without a newline returns 1 even though VER was assigned —
# under `set -e` that would abort here. Same rationale as release-server.sh.
read -r VER < "$ver_file" || true
rm -f "$ver_file"
# versionCode 必须单调递增,否则 Android 无法覆盖升级安装(同 jiu 注释)。
# 由版本号推导:major*10000 + minor*100 + patch(如 1.0.48 -> 10048)。
# Pure parameter expansion (no `cut`/`$()`), and strip any `-suffix` off the
# patch component so the arithmetic below stays numeric (e.g. "48-rc1"->48).
MAJOR="${VER%%.*}"
_REST="${VER#*.}"
MINOR="${_REST%%.*}"
PATCH="${_REST#*.}"
PATCH="${PATCH%%-*}"
BUILD=$(( MAJOR * 10000 + MINOR * 100 + PATCH ))
echo "==> compile-android: tag=${TAG} version=${VER} build=${BUILD}"
API_URL="${PANGOLIN_API_URL:-https://api.yanmeiai.com}"
# ── [1/3] build+embed libbox.aar (io.nekohasekai.libbox) ────────────────────
# Must run BEFORE `flutter build apk` — build.gradle's dependency block reads
# app/kernel/dist/android/libbox.aar and just logs a warning (does not fail
# the build!) if it's missing, which would silently ship an APK with no VPN
# kernel. Fail loudly here instead.
JAVA_HOME_17="${JAVA_HOME_17:-/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home}"
if [ ! -d "$JAVA_HOME_17" ]; then
echo "ERROR: JDK 17 not found at ${JAVA_HOME_17} (required for gomobile Android bind)." >&2
echo " See CLAUDE.md 'libbox 构建' — install via 'brew install openjdk@17' or set JAVA_HOME_17." >&2
exit 1
fi
echo "==> compile-android: building libbox.aar (JAVA_HOME=${JAVA_HOME_17}, NDK auto-detected >=28)"
JAVA_HOME="$JAVA_HOME_17" bash scripts/build-libbox.sh android
LIBBOX_AAR="app/kernel/dist/android/libbox.aar"
if [ ! -f "$LIBBOX_AAR" ]; then
echo "ERROR: ${LIBBOX_AAR} not found after build-libbox.sh — aborting." >&2
exit 1
fi
# ── [2/3] sync pubspec version + release signing ────────────────────────────
# BSD sed (macOS runner — mac-pangolin-2, see docs/ci-runner.md), same as jiu.
sed -i '' "s/^version:.*/version: ${VER}+${BUILD}/" client/pubspec.yaml
# 必须 release 签名。缺任一签名 secret 直接失败,绝不回退 debug:debug 包用公开
# 调试密钥签名,不能正式分发,也无法与正式版互相覆盖升级(同 jiu 注释)。
: "${RELEASE_KEYSTORE:?缺少 RELEASE_KEYSTORE(base64 编码的 release keystore):未配置签名,构建中止(不回退 debug)}"
: "${KEY_PASSWORD:?缺少 KEY_PASSWORD:未配置签名,构建中止}"
# TODO(controller): confirm this 2-secret assumption before relying on it.
# - Assumes storePassword == keyPassword == KEY_PASSWORD (true only if the
# keystore was generated with the same password for both).
# - keyAlias is NOT among the secrets this task named, so it defaults to
# "pangolin" below — override with RELEASE_KEY_ALIAS if the real alias
# differs (e.g. keytool default "androiddebugkey" is NOT this — that
# alias belongs to the debug keystore, never use it for release signing).
# - pangolin's OWN prior CI design docs (docs/cicd-design.html §secrets,
# docs/superpowers/plans/2026-07-05-cicd.md Task 6/7,
# docs/superpowers/specs/2026-07-05-cicd-design.md) planned 4 separate
# secrets instead: ANDROID_KEYSTORE_BASE64 / ANDROID_KEYSTORE_PASSWORD /
# ANDROID_KEY_ALIAS / ANDROID_KEY_PASSWORD. If THAT naming is what is
# actually configured in Forgejo (not RELEASE_KEYSTORE/KEY_PASSWORD),
# this script needs updating before first real run.
RELEASE_KEY_ALIAS="${RELEASE_KEY_ALIAS:-pangolin}"
echo "==> compile-android: configuring release signing (keyAlias=${RELEASE_KEY_ALIAS})"
KEYSTORE_PATH="${PWD}/client/android/pangolin-release.jks"
printf '%s' "$RELEASE_KEYSTORE" | base64 --decode > "$KEYSTORE_PATH"
cat > client/android/key.properties <<EOF
storeFile=${KEYSTORE_PATH}
storePassword=${KEY_PASSWORD}
keyAlias=${RELEASE_KEY_ALIAS}
keyPassword=${KEY_PASSWORD}
EOF
# ── [3/3] flutter build apk (--split-per-abi) ───────────────────────────────
# split-per-abi 而非 universal:嵌入的 libbox.so(gomobile 编的 sing-box)每 ABI
# ~60MB,universal 三合一直接 232MB,做下载链接太大。拆分后取 **arm64-v8a**(覆盖
# 现代绝大多数 Android 机)作为唯一下载,约 ~80MB;32 位老机(armeabi-v7a)/模拟器
# (x86_64)本轮不分发(需要再加)。这样官网 + deploy-client 仍是单一稳定 URL。
cd client
flutter build apk --release --split-per-abi \
"--dart-define=PANGOLIN_API_URL=${API_URL}"
cd ..
# Locate the arm64-v8a APK (path differs across Flutter versions) and copy to dist/
mkdir -p dist
APK=""
for cand in \
client/build/app/outputs/flutter-apk/app-arm64-v8a-release.apk \
client/build/app/outputs/apk/release/app-arm64-v8a-release.apk; do
if [ -f "$cand" ]; then APK="$cand"; break; fi
done
if [ -z "$APK" ]; then
echo "ERROR: built arm64-v8a APK not found" >&2
exit 1
fi
cp "$APK" dist/pangolin-android.apk
# Clean up signing material so it never lingers on the runner workspace.
rm -f client/android/key.properties "$KEYSTORE_PATH" 2>/dev/null || true
echo "==> compile-android: done — dist/ contents:"
ls -lh dist/
+125
View File
@@ -0,0 +1,125 @@
#!/usr/bin/env bash
# compile-windows.sh <tag> — build Flutter Windows desktop app, package into
# dist/ via the existing Inno Setup script (client/windows/installer/pangolin.iss).
#
# Mirrors ~/code/jiu/scripts/ci/compile-windows.sh's short-path fix, adapted:
# - version derived via pangolin's ver_from_tag, not jiu's ad-hoc strip.
# - pangolin's Windows client does NOT embed libbox — it bundles a
# downloaded sing-box.exe + wintun.dll as a subprocess (see
# client/windows/README.md, CLAUDE.md "client/ 移动端"'s sibling desktop
# note). Those are fetched by app/kernel/fetch-desktop-bin.sh, NOT built
# by scripts/build-libbox.sh — no JDK/NDK/gomobile needed on Windows.
# - pangolin already has a working local packaging path:
# client/windows/build.ps1 + client/windows/installer/pangolin.iss. This
# script re-implements that flow in bash (matching the repo's
# jiu-mirrored bash-script-per-platform convention) rather than shelling
# out to build.ps1, so it can apply jiu's short-path fix below.
# - jiu's short-path fix: the Forgejo Windows runner checks out under
# C:\Windows\System32\config\systemprofile\.cache\act\...\hostexecutor,
# which triggers WOW64 filesystem redirection that breaks CMake/Flutter
# native paths. jiu copies client/ to C:\jiu-build\client and builds
# there. pangolin's windows/CMakeLists.txt additionally resolves the
# kernel binaries via a path RELATIVE to CMAKE_SOURCE_DIR
# (client/windows/../../app/kernel/dist/desktop/windows-* — see
# client/windows/CMakeLists.txt "Pangolin sing-box kernel" section), so
# this script must copy app/kernel/dist alongside client/ preserving the
# SAME relative layout (BUILD_ROOT/client + BUILD_ROOT/app/kernel/dist),
# not just client/ alone like jiu does — jiu has no such cross-directory
# kernel reference.
# - dart-define is PANGOLIN_API_URL, not jiu's BASE_URL/PUBLIC_URL/APP_VERSION.
# - pangolin.iss hardcodes `#define MyAppVersion "1.0.47"` (no #ifndef guard
# like jiu's jiu-installer.iss), so ISCC's /D command-line override would
# be silently ignored. This script `sed`s the version into the copied
# .iss file instead of passing /DAppVer.
# - output dist/pangolin-windows-x64-setup.exe (stable name — the .iss's
# own OutputBaseFilename is version-suffixed "pangolin-setup-<ver>.exe",
# so this script copies+renames it to the stable name deploy-client.sh
# and release-client.sh expect).
# - unsigned (like jiu) — first install shows a SmartScreen warning; no
# code-signing cert configured (see report).
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=scripts/ci/_env.sh
. "${SCRIPT_DIR}/_env.sh"
TAG="${1:?usage: compile-windows.sh <tag>}"
ver_file="/tmp/compile_windows_ver.$$"
ver_from_tag client "$TAG" > "$ver_file"
VER=""
read -r VER < "$ver_file" || true # 无结尾换行的 read 退出码,见 compile-android.sh 同注释
rm -f "$ver_file"
echo "==> compile-windows: tag=${TAG} version=${VER}"
API_URL="${PANGOLIN_API_URL:-https://api.yanmeiai.com}"
# ── [1/4] fetch sing-box.exe + wintun.dll into the ORIGINAL checkout ───────
# Must run before the short-path copy below (step 2), so app/kernel/dist/
# exists to be copied alongside client/.
echo "==> compile-windows: fetching sing-box.exe + wintun.dll"
bash app/kernel/fetch-desktop-bin.sh windows amd64
# ── [2/4] copy client/ + app/kernel/dist to a short path ───────────────────
BUILD_ROOT="/c/pangolin-build"
BUILD_CLIENT="${BUILD_ROOT}/client"
echo "==> copying client/ + app/kernel/dist to ${BUILD_ROOT}"
rm -rf "$BUILD_ROOT"
mkdir -p "${BUILD_ROOT}/app/kernel"
cp -r client "$BUILD_CLIENT"
cp -r app/kernel/dist "${BUILD_ROOT}/app/kernel/dist"
# `cp -r` turns Flutter's plugin symlinks (windows/flutter/ephemeral/.plugin_symlinks/*)
# into real directories. flutter's createPluginSymlinks only cleans up *symlinks*, so it
# then fails to create a symlink over the copied real dir (errno 183 / PathExists).
# Drop all regenerable artifacts so the build below recreates them cleanly (same fix as jiu).
rm -rf "${BUILD_CLIENT}/windows/flutter/ephemeral" \
"${BUILD_CLIENT}/.dart_tool" \
"${BUILD_CLIENT}/build"
# ── [3/4] build ──────────────────────────────────────────────────────────────
# NOTE(controller): `flutter create` on an existing project only fills in
# scaffolding it manages (ephemeral/, generated_plugins.cmake, ...) and should
# not overwrite files that already exist — jiu relies on exactly this. But
# pangolin's windows/ tree is more heavily customized than jiu's (CMakeLists
# kernel-bundling block, runner.exe.manifest requireAdministrator + the
# /MANIFESTUAC:NO link flag — see client/windows/README.md "已知坑"). Verify
# on first real CI run that none of those get clobbered.
pushd "$BUILD_CLIENT" > /dev/null
flutter create --platforms=windows . --project-name pangolin_vpn
flutter build windows --release "--dart-define=PANGOLIN_API_URL=${API_URL}"
popd > /dev/null
# ── [4/4] package with Inno Setup (existing client/windows/installer/pangolin.iss) ──
ISCC=""
for c in "/c/Program Files (x86)/Inno Setup 6/ISCC.exe" "/c/Program Files/Inno Setup 6/ISCC.exe"; do
if [ -f "$c" ]; then ISCC="$c"; break; fi
done
if [ -z "$ISCC" ]; then
echo "ERROR: Inno Setup (ISCC) not found. Install Inno Setup 6 on this runner first." >&2
exit 1
fi
ISS="${BUILD_CLIENT}/windows/installer/pangolin.iss"
# pangolin.iss hardcodes MyAppVersion (no #ifndef guard) — sed it to match the
# release tag rather than relying on an ISCC /D override (which would be
# shadowed by the unconditional #define — see header note above).
sed -i "s/^#define MyAppVersion .*/#define MyAppVersion \"${VER}\"/" "$ISS"
echo "==> building installer with Inno Setup (version ${VER})"
# MSYS_NO_PATHCONV / MSYS2_ARG_CONV_EXCL disable Git Bash's automatic conversion
# of paths, matching jiu's ISCC invocation fix.
MSYS_NO_PATHCONV=1 MSYS2_ARG_CONV_EXCL='*' "$ISCC" "$ISS"
# pangolin.iss has no explicit OutputDir -> Inno defaults to {src}\Output
# (relative to the .iss file), filename OutputBaseFilename=pangolin-setup-<ver>.
mkdir -p dist
SETUP_SRC="C:\\pangolin-build\\client\\windows\\installer\\Output\\pangolin-setup-${VER}.exe"
echo "==> copying installer -> dist/pangolin-windows-x64-setup.exe"
powershell -NoProfile -Command \
"Copy-Item '${SETUP_SRC}' 'dist\\pangolin-windows-x64-setup.exe' -Force"
echo "==> compile-windows: done — dist/ contents:"
ls -lh dist/
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env bash
# deploy-client.sh <tag> — publish the latest Android/Windows client
# installers to pangolin1's public downloads directory. Keeps only the single
# latest file per platform (jiu-style "only latest" — see jiu's
# deploy-client.sh /opt/jiu/downloads handling), overwriting whatever a
# previous release published under the same stable filename.
#
# Mirrors ~/code/jiu/scripts/ci/deploy-client.sh's "only latest" downloads
# publish, but uses pangolin's own lib-ssh.sh API (setup_ssh / $SSH /
# $RSYNC_SSH, DEPLOY_HOST defaulting to pangolin1's IP — different from
# jiu's lib-forgejo.sh-embedded setup_ssh/EC2_HOST convention) and drops
# jiu's Flutter-web + version.yaml swap entirely: pangolin's client has no
# web build in this pipeline, and no version.yaml self-update manifest.
#
# TODO(controller): this script assumes pangolin-server (or nginx/whatever
# fronts pangolin1's public HTTP) serves ${DOWNLOADS_DIR} at /downloads/ —
# that vhost/route wiring is a SEPARATE, not-yet-done task (same status as
# docs/superpowers/plans/2026-07-05-cicd.md Task 8's "官网下载链接接
# Android release", which is also not part of this draft).
#
# Usage: scripts/ci/deploy-client.sh <tag> (e.g. client-v1.0.49)
# Requires env: DEPLOY_SSH_KEY (see lib-ssh.sh). Assumes compile-android.sh /
# compile-windows.sh (or a prior download-artifact step) have already
# produced dist/pangolin-android.apk and/or dist/pangolin-windows-x64-setup.exe.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=scripts/ci/lib-ssh.sh
. "${SCRIPT_DIR}/lib-ssh.sh"
# TODO(controller): confirm this path. Proposed to sit alongside the existing
# /var/lib/pangolin/ tree (pangolin.db lives at /var/lib/pangolin/pangolin.db
# per deploy-server.sh) rather than under nginx's webroot directly, so the
# web server config just needs one `location /downloads/ { alias ...; }`
# (or equivalent) added — no file ownership overlap with the control-plane
# SQLite DB / systemd units.
DOWNLOADS_DIR=/var/lib/pangolin/downloads
TAG="${1:?usage: deploy-client.sh <tag>}"
# Refuse anything that isn't a strict client-vX.Y.Z[-suffix] tag before it can
# reach the remote commands below (command-injection guard, mirroring
# deploy-server.sh's rationale: an anchored regex, not a `case` glob, since a
# trailing `*` would match shell metacharacters too).
if ! [[ "$TAG" =~ ^client-v[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.]+)?$ ]]; then
echo "deploy-client: refusing unexpected tag '$TAG'" >&2
exit 1
fi
if [ ! -f dist/pangolin-android.apk ] && [ ! -f dist/pangolin-windows-x64-setup.exe ]; then
echo "deploy-client: no artifacts found in dist/ — nothing to deploy" >&2
exit 1
fi
setup_ssh
SCP="scp -i ${SSH_KEY_FILE} -P ${DEPLOY_PORT} -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=${SSH_KNOWN_HOSTS_FILE}"
echo "==> deploy-client: tag=${TAG} host=${DEPLOY_HOST} downloads_dir=${DOWNLOADS_DIR}"
$SSH "root@${DEPLOY_HOST}" "mkdir -p ${DOWNLOADS_DIR}"
if [ -f dist/pangolin-android.apk ]; then
echo "==> deploy-client: uploading pangolin-android.apk"
$SCP dist/pangolin-android.apk "root@${DEPLOY_HOST}:/tmp/pangolin-android.apk.new"
$SSH "root@${DEPLOY_HOST}" "rm -f ${DOWNLOADS_DIR}/pangolin-android.apk && mv /tmp/pangolin-android.apk.new ${DOWNLOADS_DIR}/pangolin-android.apk && chmod 644 ${DOWNLOADS_DIR}/pangolin-android.apk"
fi
if [ -f dist/pangolin-windows-x64-setup.exe ]; then
echo "==> deploy-client: uploading pangolin-windows-x64-setup.exe"
$SCP dist/pangolin-windows-x64-setup.exe "root@${DEPLOY_HOST}:/tmp/pangolin-windows-x64-setup.exe.new"
$SSH "root@${DEPLOY_HOST}" "rm -f ${DOWNLOADS_DIR}/pangolin-windows-x64-setup.exe && mv /tmp/pangolin-windows-x64-setup.exe.new ${DOWNLOADS_DIR}/pangolin-windows-x64-setup.exe && chmod 644 ${DOWNLOADS_DIR}/pangolin-windows-x64-setup.exe"
fi
# TODO(controller): macOS artifact not yet produced by this draft — add the
# same guarded upload+swap once compile-macos.sh lands (Phase 3).
echo "==> deploy-client: done"
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
# release-client.sh <tag> — ensure the Forgejo release for a client-v* tag
# exists and upload whichever client artifacts are present in dist/.
#
# Mirrors ~/code/jiu/scripts/ci/release-client.sh's asset-guarding pattern,
# but uses pangolin's own lib-forgejo.sh API (forgejo_release_ensure /
# forgejo_upload_asset — different names/signature than jiu's create_release/
# upload_asset) and drops jiu's version.yaml/CHANGELOG bookkeeping entirely:
# pangolin has no equivalent backend/config/version.yaml manifest — client
# self-update checking is out of scope here (server just serves the release
# assets; see deploy-client.sh).
#
# Multiple platform build jobs (android/windows/...) all upload into the SAME
# release, guarded by `[ -f ... ]` since a given CI run may only have built a
# subset (e.g. a partial manual re-dispatch, or before macOS/iOS land).
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=scripts/ci/_env.sh
. "${SCRIPT_DIR}/_env.sh"
# shellcheck source=scripts/ci/lib-forgejo.sh
. "${SCRIPT_DIR}/lib-forgejo.sh"
TAG="${1:?usage: release-client.sh <tag>}"
ver_file="/tmp/release_client_ver.$$"
ver_from_tag client "$TAG" > "$ver_file"
VER=""
read -r VER < "$ver_file" || true # 无结尾换行的 read 退出码,见 release-server.sh 同注释
rm -f "$ver_file"
echo "==> release-client: tag=${TAG} ver=${VER}"
forgejo_release_ensure "$TAG" "client ${VER}"
if [ -f dist/pangolin-android.apk ]; then
forgejo_upload_asset "$TAG" dist/pangolin-android.apk
fi
if [ -f dist/pangolin-windows-x64-setup.exe ]; then
forgejo_upload_asset "$TAG" dist/pangolin-windows-x64-setup.exe
fi
# TODO(controller): macOS artifact not yet built by any compile-*.sh in this
# draft (Phase 3 / Task 9 of docs/superpowers/plans/2026-07-05-cicd.md is out
# of scope for this task). Once compile-macos.sh lands, add here:
# if [ -f dist/pangolin-macos-x64.zip ]; then
# forgejo_upload_asset "$TAG" dist/pangolin-macos-x64.zip
# fi
echo "==> release-client: done — Release ${TAG} updated. dist/ contents:"
ls -lh dist/ 2>/dev/null || true