ci: add arm64 Android release pipeline
This commit is contained in:
Vendored
+172
@@ -0,0 +1,172 @@
|
||||
pipeline {
|
||||
agent {
|
||||
node {
|
||||
label '构建机1'
|
||||
customWorkspace '/home/nanxunai/goujian/workspace/jizhang-android-release'
|
||||
}
|
||||
}
|
||||
|
||||
parameters {
|
||||
string(name: 'API_BASE_URL', defaultValue: 'http://192.168.5.8:5000', description: '内测后端 HTTP(S) 地址;Webhook 自动构建使用默认局域网地址')
|
||||
}
|
||||
|
||||
options {
|
||||
timestamps()
|
||||
disableConcurrentBuilds()
|
||||
skipDefaultCheckout(true)
|
||||
timeout(time: 180, unit: 'MINUTES')
|
||||
buildDiscarder(logRotator(daysToKeepStr: '30', numToKeepStr: '20'))
|
||||
}
|
||||
|
||||
environment {
|
||||
PATH = '/home/nanxunai/.local/bin:/home/nanxunai/flutter/bin:/home/nanxunai/Android/Sdk/platform-tools:/home/nanxunai/Android/Sdk/cmdline-tools/latest/bin:/usr/local/bin:/usr/bin:/bin'
|
||||
JAVA_HOME = '/home/nanxunai/.local/lib/jvm/zulu-21'
|
||||
ANDROID_HOME = '/home/nanxunai/Android/Sdk'
|
||||
ANDROID_SDK_ROOT = '/home/nanxunai/Android/Sdk'
|
||||
FLUTTER_ROOT = '/home/nanxunai/flutter'
|
||||
GIT_EXEC_PATH = '/home/nanxunai/.local/usr/lib/git-core'
|
||||
LD_LIBRARY_PATH = '/home/nanxunai/.local/usr/lib/x86_64-linux-gnu'
|
||||
PUB_HOSTED_URL = 'https://pub.flutter-io.cn'
|
||||
FLUTTER_STORAGE_BASE_URL = 'https://storage.flutter-io.cn'
|
||||
HTTP_PROXY = 'http://192.168.5.200:7890'
|
||||
HTTPS_PROXY = 'http://192.168.5.200:7890'
|
||||
http_proxy = 'http://192.168.5.200:7890'
|
||||
https_proxy = 'http://192.168.5.200:7890'
|
||||
NO_PROXY = '127.0.0.1,localhost,192.168.5.8,.nxsir.cn,pub.flutter-io.cn,storage.flutter-io.cn'
|
||||
no_proxy = '127.0.0.1,localhost,192.168.5.8,.nxsir.cn,pub.flutter-io.cn,storage.flutter-io.cn'
|
||||
GRADLE_USER_HOME = "${WORKSPACE}/.ci/gradle"
|
||||
PUB_CACHE = "${WORKSPACE}/.ci/pub-cache"
|
||||
OPENLIST_CREDENTIALS = 'openlist_key'
|
||||
OPENLIST_BASE_URL = 'https://openlist.nxsir.cn'
|
||||
OPENLIST_REMOTE_DIR = '/yidongpan/构建产物/jizhang'
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Checkout') {
|
||||
steps {
|
||||
deleteDir()
|
||||
checkout scm
|
||||
}
|
||||
}
|
||||
|
||||
stage('Metadata And Preflight') {
|
||||
steps {
|
||||
script {
|
||||
env.PUBSPEC_VERSION = sh(script: "sed -n 's/^version:[[:space:]]*//p' frontend/pubspec.yaml | head -n 1", returnStdout: true).trim()
|
||||
def versionParts = env.PUBSPEC_VERSION.tokenize('+')
|
||||
if (versionParts.size() != 2) { error("Invalid pubspec version: ${env.PUBSPEC_VERSION}") }
|
||||
env.APP_VERSION = versionParts[0]
|
||||
env.BASE_VERSION_CODE = versionParts[1]
|
||||
env.ANDROID_VERSION_CODE = "${versionParts[1].toInteger() + env.BUILD_NUMBER.toInteger()}"
|
||||
env.SHORT_SHA = sh(script: 'git rev-parse --short=8 HEAD', returnStdout: true).trim()
|
||||
env.IMMUTABLE_TAG = "${env.APP_VERSION}-internal-b${env.BUILD_NUMBER}-${env.SHORT_SHA}"
|
||||
env.APK_BASENAME = "JiZhi-${env.IMMUTABLE_TAG}-vc${env.ANDROID_VERSION_CODE}-arm64-v8a.apk"
|
||||
env.APK_PATH = "${env.WORKSPACE}/artifacts/${env.APK_BASENAME}"
|
||||
env.MANIFEST_PATH = "${env.WORKSPACE}/artifacts/JiZhi-${env.IMMUTABLE_TAG}-build-manifest.json"
|
||||
currentBuild.displayName = "#${env.BUILD_NUMBER} ${env.IMMUTABLE_TAG}"
|
||||
currentBuild.description = "versionCode ${env.ANDROID_VERSION_CODE} · ${params.API_BASE_URL}"
|
||||
}
|
||||
sh '''
|
||||
set -euo pipefail
|
||||
test "$(uname -m)" = x86_64
|
||||
for command_name in git java curl python3 sha256sum unzip; do command -v "$command_name" >/dev/null; done
|
||||
test -x "$FLUTTER_ROOT/bin/flutter"
|
||||
test -x "$ANDROID_HOME/build-tools/36.1.0/apksigner"
|
||||
test -x "$ANDROID_HOME/build-tools/36.1.0/aapt2"
|
||||
python3 - "$API_BASE_URL" <<'PY'
|
||||
import sys, urllib.parse
|
||||
value = sys.argv[1].strip()
|
||||
parsed = urllib.parse.urlparse(value)
|
||||
if parsed.scheme not in {'http','https'} or not parsed.hostname or parsed.username or parsed.password:
|
||||
raise SystemExit(f'invalid API_BASE_URL: {value}')
|
||||
PY
|
||||
mkdir -p "$GRADLE_USER_HOME" "$PUB_CACHE" artifacts
|
||||
available_kb=$(df -Pk "$WORKSPACE" | awk 'NR == 2 { print $4 }')
|
||||
test "$available_kb" -ge 4194304
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
stage('Analyze And Test') {
|
||||
steps {
|
||||
dir('frontend') {
|
||||
sh '''
|
||||
set -euo pipefail
|
||||
"$FLUTTER_ROOT/bin/flutter" config --no-analytics
|
||||
"$FLUTTER_ROOT/bin/flutter" pub get
|
||||
# Keep warnings/errors fatal while allowing the repository's existing
|
||||
# informational style lints to be cleaned up independently.
|
||||
"$FLUTTER_ROOT/bin/flutter" analyze --no-pub --no-fatal-infos
|
||||
"$FLUTTER_ROOT/bin/flutter" test --no-pub
|
||||
'''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Build arm64 Internal Release') {
|
||||
steps {
|
||||
dir('frontend') {
|
||||
sh '''
|
||||
set -euo pipefail
|
||||
"$FLUTTER_ROOT/bin/flutter" build apk \
|
||||
--flavor internal --release --target-platform android-arm64 \
|
||||
--build-name "$APP_VERSION" --build-number "$ANDROID_VERSION_CODE" \
|
||||
--dart-define=INTERNAL_BUILD=true \
|
||||
--dart-define="API_BASE_URL=$API_BASE_URL" \
|
||||
--dart-define="APP_VERSION=$IMMUTABLE_TAG"
|
||||
source_apk=build/app/outputs/flutter-apk/app-internal-release.apk
|
||||
test -s "$source_apk"
|
||||
cp "$source_apk" "$APK_PATH"
|
||||
'''
|
||||
}
|
||||
sh '''
|
||||
set -euo pipefail
|
||||
"$ANDROID_HOME/build-tools/36.1.0/apksigner" verify --verbose "$APK_PATH"
|
||||
badging=$("$ANDROID_HOME/build-tools/36.1.0/aapt2" dump badging "$APK_PATH")
|
||||
printf '%s\n' "$badging" | grep -q "package: name='com.nx.miaoji.internal'"
|
||||
printf '%s\n' "$badging" | grep -q "versionCode='$ANDROID_VERSION_CODE'"
|
||||
unzip -l "$APK_PATH" >artifacts/apk-files.txt
|
||||
grep -q 'lib/arm64-v8a/' artifacts/apk-files.txt
|
||||
if grep -Eq 'lib/(x86|x86_64|armeabi-v7a)/' artifacts/apk-files.txt; then
|
||||
echo 'APK contains a non-arm64 native ABI' >&2
|
||||
exit 1
|
||||
fi
|
||||
sha256sum "$APK_PATH" >"$APK_PATH.sha256"
|
||||
apk_sha=$(awk '{print $1}' "$APK_PATH.sha256"); export apk_sha
|
||||
python3 - <<'PY'
|
||||
import json, os, pathlib, subprocess
|
||||
path = pathlib.Path(os.environ['MANIFEST_PATH'])
|
||||
payload = {
|
||||
'project':'jizhang','buildNumber':os.environ['BUILD_NUMBER'],
|
||||
'commit':subprocess.check_output(['git','rev-parse','HEAD'],text=True).strip(),
|
||||
'productVersion':os.environ['APP_VERSION'],'versionName':os.environ['APP_VERSION']+'-internal',
|
||||
'versionCode':int(os.environ['ANDROID_VERSION_CODE']),'displayVersion':os.environ['IMMUTABLE_TAG'],
|
||||
'apiBaseUrl':os.environ['API_BASE_URL'],'abi':'arm64-v8a',
|
||||
'apk':pathlib.Path(os.environ['APK_PATH']).name,'apkSha256':os.environ['apk_sha']
|
||||
}
|
||||
path.write_text(json.dumps(payload,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
|
||||
PY
|
||||
sha256sum "$MANIFEST_PATH" >"$MANIFEST_PATH.sha256"
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
stage('Upload Artifacts') {
|
||||
steps {
|
||||
withCredentials([usernamePassword(credentialsId: "${OPENLIST_CREDENTIALS}", usernameVariable: 'OPENLIST_USERNAME', passwordVariable: 'OPENLIST_PASSWORD')]) {
|
||||
sh '''
|
||||
set -euo pipefail
|
||||
for artifact in "$APK_PATH" "$APK_PATH.sha256" "$MANIFEST_PATH" "$MANIFEST_PATH.sha256"; do
|
||||
./scripts/upload-openlist-artifact.sh "$artifact" "$OPENLIST_REMOTE_DIR"
|
||||
done
|
||||
'''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
success { archiveArtifacts artifacts: 'artifacts/*.apk,artifacts/*.sha256,artifacts/*.json', fingerprint: true }
|
||||
always { cleanWs(deleteDirs: true, notFailBuild: true) }
|
||||
}
|
||||
}
|
||||
Executable
+189
@@ -0,0 +1,189 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
FILE=${1:?usage: upload-openlist-artifact.sh file [remote-directory]}
|
||||
REMOTE_DIR=${2:-/yidongpan/构建产物/liverecorder}
|
||||
OPENLIST_BASE_URL=${OPENLIST_BASE_URL:-https://openlist.nxsir.cn}
|
||||
OPENLIST_USERNAME=${OPENLIST_USERNAME:?OPENLIST_USERNAME is required}
|
||||
OPENLIST_PASSWORD=${OPENLIST_PASSWORD:?OPENLIST_PASSWORD is required}
|
||||
OPENLIST_UPLOAD_ATTEMPTS=${OPENLIST_UPLOAD_ATTEMPTS:-6}
|
||||
|
||||
test -f "$FILE" || { printf 'artifact does not exist: %s\n' "$FILE" >&2; exit 1; }
|
||||
for required_command in curl python3 stat basename sleep; do
|
||||
command -v "$required_command" >/dev/null 2>&1 || {
|
||||
printf 'required upload command is missing: %s\n' "$required_command" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
|
||||
OPENLIST_BASE_URL=${OPENLIST_BASE_URL%/}
|
||||
REMOTE_DIR="/${REMOTE_DIR#/}"
|
||||
REMOTE_DIR=${REMOTE_DIR%/}
|
||||
FILE_NAME=$(basename -- "$FILE")
|
||||
BUILD_MARKER=${BUILD_TAG:-${BUILD_ID:-$$}}
|
||||
BUILD_MARKER=$(printf '%s' "$BUILD_MARKER" | tr -c 'A-Za-z0-9._-' '-')
|
||||
TEMP_NAME=".${FILE_NAME}.uploading-${BUILD_MARKER}"
|
||||
TEMP_PATH="$REMOTE_DIR/$TEMP_NAME"
|
||||
FINAL_PATH="$REMOTE_DIR/$FILE_NAME"
|
||||
LOCAL_SIZE=$(stat -c '%s' "$FILE")
|
||||
TOKEN=
|
||||
|
||||
json_string() {
|
||||
python3 -c 'import json,sys; print(json.dumps(sys.argv[1], ensure_ascii=False))' "$1"
|
||||
}
|
||||
|
||||
envelope_code() {
|
||||
python3 -c 'import json,sys
|
||||
try:
|
||||
value=json.load(sys.stdin)
|
||||
print(value.get("code", -1))
|
||||
except Exception:
|
||||
print(-1)'
|
||||
}
|
||||
|
||||
envelope_message() {
|
||||
python3 -c 'import json,sys
|
||||
try:
|
||||
value=json.load(sys.stdin)
|
||||
print(value.get("message", "unknown OpenList response"))
|
||||
except Exception:
|
||||
print("invalid OpenList response")'
|
||||
}
|
||||
|
||||
login() {
|
||||
local payload response code
|
||||
payload=$(printf '{"username":%s,"password":%s}' \
|
||||
"$(json_string "$OPENLIST_USERNAME")" \
|
||||
"$(json_string "$OPENLIST_PASSWORD")")
|
||||
response=$(curl --silent --show-error --location --fail-with-body \
|
||||
--connect-timeout 30 --max-time 120 \
|
||||
--request POST "$OPENLIST_BASE_URL/api/auth/login" \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw "$payload") || return 1
|
||||
code=$(printf '%s' "$response" | envelope_code)
|
||||
[ "$code" = "200" ] || {
|
||||
printf 'OpenList login failed: %s\n' "$(printf '%s' "$response" | envelope_message)" >&2
|
||||
return 1
|
||||
}
|
||||
TOKEN=$(printf '%s' "$response" | python3 -c 'import json,sys; print((json.load(sys.stdin).get("data") or {}).get("token", ""))')
|
||||
[ -n "$TOKEN" ]
|
||||
}
|
||||
|
||||
post_json() {
|
||||
local endpoint=$1 payload=$2
|
||||
curl --silent --show-error --location --fail-with-body \
|
||||
--connect-timeout 30 --max-time 180 \
|
||||
--request POST "$OPENLIST_BASE_URL$endpoint" \
|
||||
--header "Authorization: $TOKEN" \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw "$payload"
|
||||
}
|
||||
|
||||
remote_size() {
|
||||
local path=$1 response code
|
||||
response=$(post_json /api/fs/get "{\"path\":$(json_string "$path"),\"password\":\"\"}") || return 1
|
||||
code=$(printf '%s' "$response" | envelope_code)
|
||||
[ "$code" = "200" ] || return 1
|
||||
printf '%s' "$response" | python3 -c 'import json,sys; print((json.load(sys.stdin).get("data") or {}).get("size", -1))'
|
||||
}
|
||||
|
||||
ensure_remote_directory() {
|
||||
local current= segment response code
|
||||
while IFS= read -r segment; do
|
||||
[ -n "$segment" ] || continue
|
||||
current="$current/$segment"
|
||||
response=$(post_json /api/fs/list "{\"path\":$(json_string "$current"),\"password\":\"\",\"refresh\":false,\"page\":1,\"per_page\":1}" || true)
|
||||
code=$(printf '%s' "$response" | envelope_code)
|
||||
if [ "$code" = "200" ]; then
|
||||
continue
|
||||
fi
|
||||
response=$(post_json /api/fs/mkdir "{\"path\":$(json_string "$current")}") || return 1
|
||||
code=$(printf '%s' "$response" | envelope_code)
|
||||
[ "$code" = "200" ] || {
|
||||
printf 'OpenList mkdir failed for %s: %s\n' "$current" "$(printf '%s' "$response" | envelope_message)" >&2
|
||||
return 1
|
||||
}
|
||||
done < <(python3 -c 'import sys; print("\n".join(part for part in sys.argv[1].split("/") if part))' "$REMOTE_DIR")
|
||||
}
|
||||
|
||||
remove_temporary_file() {
|
||||
local response
|
||||
[ -n "$TOKEN" ] || login >/dev/null 2>&1 || return 0
|
||||
response=$(post_json /api/fs/remove "{\"dir\":$(json_string "$REMOTE_DIR"),\"names\":[$(json_string "$TEMP_NAME")]}" 2>/dev/null || true)
|
||||
[ "$(printf '%s' "$response" | envelope_code)" = "200" ] || true
|
||||
}
|
||||
|
||||
publish_temporary_file() {
|
||||
local response code
|
||||
response=$(post_json /api/fs/rename "{\"path\":$(json_string "$TEMP_PATH"),\"name\":$(json_string "$FILE_NAME")}") || return 1
|
||||
code=$(printf '%s' "$response" | envelope_code)
|
||||
[ "$code" = "200" ] || {
|
||||
printf 'OpenList rename failed: %s\n' "$(printf '%s' "$response" | envelope_message)" >&2
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
upload_once() {
|
||||
local encoded_path response code uploaded_size
|
||||
encoded_path=$(python3 -c 'import sys,urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "$TEMP_PATH")
|
||||
response=$(curl --silent --show-error --location --fail-with-body \
|
||||
--connect-timeout 30 --max-time 3600 --speed-limit 1024 --speed-time 120 \
|
||||
--request PUT "$OPENLIST_BASE_URL/api/fs/put" \
|
||||
--header "Authorization: $TOKEN" \
|
||||
--header "File-Path: $encoded_path" \
|
||||
--header 'As-Task: false' \
|
||||
--header 'Content-Type: application/octet-stream' \
|
||||
--header 'Expect:' \
|
||||
--data-binary "@$FILE") || return 1
|
||||
code=$(printf '%s' "$response" | envelope_code)
|
||||
[ "$code" = "200" ] || {
|
||||
printf 'OpenList upload failed: %s\n' "$(printf '%s' "$response" | envelope_message)" >&2
|
||||
return 1
|
||||
}
|
||||
uploaded_size=$(remote_size "$TEMP_PATH") || return 1
|
||||
[ "$uploaded_size" = "$LOCAL_SIZE" ] || {
|
||||
printf 'OpenList size mismatch for temporary upload: local=%s remote=%s\n' "$LOCAL_SIZE" "$uploaded_size" >&2
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
printf 'Uploading %s (%s bytes) to OpenList %s\n' "$FILE_NAME" "$LOCAL_SIZE" "$REMOTE_DIR"
|
||||
|
||||
for attempt in $(seq 1 "$OPENLIST_UPLOAD_ATTEMPTS"); do
|
||||
TOKEN=
|
||||
upload_ready=1
|
||||
if login && ensure_remote_directory; then
|
||||
existing_size=$(remote_size "$FINAL_PATH" 2>/dev/null || true)
|
||||
if [ "$existing_size" = "$LOCAL_SIZE" ]; then
|
||||
printf 'OpenList artifact already exists with matching size: %s\n' "$FINAL_PATH"
|
||||
exit 0
|
||||
fi
|
||||
if [ -n "$existing_size" ]; then
|
||||
remove_response=$(post_json /api/fs/remove "{\"dir\":$(json_string "$REMOTE_DIR"),\"names\":[$(json_string "$FILE_NAME")]}" || true)
|
||||
if [ "$(printf '%s' "$remove_response" | envelope_code)" != "200" ]; then
|
||||
printf 'Unable to remove mismatched OpenList artifact before retry: %s\n' "$FINAL_PATH" >&2
|
||||
upload_ready=0
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$upload_ready" = "1" ] && upload_once && publish_temporary_file; then
|
||||
final_size=$(remote_size "$FINAL_PATH") || final_size=-1
|
||||
if [ "$final_size" = "$LOCAL_SIZE" ]; then
|
||||
printf 'OpenList upload verified: %s (%s bytes)\n' "$FINAL_PATH" "$final_size"
|
||||
exit 0
|
||||
fi
|
||||
printf 'OpenList final size mismatch: local=%s remote=%s\n' "$LOCAL_SIZE" "$final_size" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$attempt" -lt "$OPENLIST_UPLOAD_ATTEMPTS" ]; then
|
||||
delay=$((5 * (1 << (attempt - 1))))
|
||||
[ "$delay" -le 120 ] || delay=120
|
||||
printf 'OpenList upload attempt %s/%s failed; retrying in %ss\n' "$attempt" "$OPENLIST_UPLOAD_ATTEMPTS" "$delay" >&2
|
||||
sleep "$delay"
|
||||
fi
|
||||
done
|
||||
|
||||
remove_temporary_file
|
||||
printf 'OpenList upload failed after %s attempts: %s\n' "$OPENLIST_UPLOAD_ATTEMPTS" "$FILE_NAME" >&2
|
||||
exit 1
|
||||
Reference in New Issue
Block a user