build: add unified particle release pipeline

This commit is contained in:
tei
2026-09-19 07:50:31 +09:00
parent 9e20233288
commit fd5595ebbc
6 changed files with 634 additions and 0 deletions

View File

@@ -2,6 +2,9 @@
## Unreleased
## 13.4.1-particle.6(历史 iOS 过渡发行)
- 已公开的 RN `.6` 与 iOS Metal `.6` 用于 iOS 接入验证;RN `.6` 仍显式依赖 Android Native `.5`,因此 `.6` 不是 Android/iOS 统一发行版,且不得覆盖重发。
- 新增 iOS `WeatherRuntimeBridge` RN 0.85 codegen TurboModule,实现既有 V3/V4 与视觉控制 ABI;每个命令从当前 Fabric/UIManager view registry 重新解析 MLRN MapView,不缓存 map/runtime 指针。
- 新增 iOS pod/autolinking/codegen provider。pod 复用 `MapLibreReactNative` 的唯一 MapLibre product,不嵌入第二份 framework。
- iOS Podfile 的 post-install 现在必须同时调用 `$MLRN.post_install(installer)` 和 `$MLRN_PARTICLE.post_install(installer)`;两者消费同一 `$MLRN_SPM_SPEC`,以让 MLRN、App 与 Particle pod target 共享一个 MapLibre package/product。

View File

@@ -0,0 +1,62 @@
# MapLibre Particle 统一发行流水线
本目录用于在 Mac release builder 上统一生成并发布 Android Native、React Native package 和 iOS Metal XCFramework。
## 唯一输入
只输入 Particle revision,例如:
tools/particle_release_pipeline/release.sh 7
版本自动推导:
- Android Native / RN:13.4.1-particle.7
- iOS SwiftPM:6.28.0-particle.7
以后升级到 .8 时只输入 8,不再手工维护多个平台版本号。
## 固定构建环境
- macOS release builder
- JDK 17
- Gradle Wrapper 9.5.1
- Android SDK 36
- Android NDK 28.2.13676358
- CMake 3.31.6
- Android renderer:OpenGL
- iOS renderer:Metal
- iOS binary build:bazel build --//:renderer=metal //platform/ios:MapLibre.dynamic
## 发布目标
- Android Maven:https://pbf.jpcn.ccwu.cc/maven
- RN tgz:https://pbf.jpcn.ccwu.cc/npm
- iOS binary:https://pbf.jpcn.ccwu.cc/swift/maplibre-particle
- SwiftPM Git:https://pbf.jpcn.ccwu.cc/swift/maplibre-particle-spm.git
## 不可变规则
同一个版本号禁止覆盖。任一公网目标已经存在时,prepare/publish 阶段直接失败;必须增加 Particle revision。
流水线从同一个版本冻结 Git commit 构建 Android 与 iOS。发布后必须从公网重新下载 artifact,并与本地产物逐字节比较;只有 public readback 通过才生成最终 release manifest 和 tag。
## 阶段
可单独执行:
release.sh 7 prepare
release.sh 7 build
release.sh 7 publish
release.sh 7 finalize
完整执行:
release.sh 7
prepare 会产生版本冻结 commit;finalize 会产生最终 manifest commit、tag,并推送 particle/main。发布工作必须在干净 release worktree 中运行。
## .6 历史说明
13.4.1-particle.6 是 iOS 接入过渡发行。公开 RN .6 仍依赖 Android Native .5,因此不得把 .6 重新定义或覆盖成统一双平台发行。
第一个由本流水线定义的 Android/iOS 统一发行是 Particle revision 7。

View File

@@ -0,0 +1,178 @@
#!/usr/bin/env python3
from __future__ import annotations
import hashlib
import json
import os
import re
import sys
from datetime import datetime, timezone
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
PUBLIC = "https://pbf.jpcn.ccwu.cc"
def sha256(path: Path) -> str:
h = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
h.update(chunk)
return h.hexdigest()
def base_version(value: str) -> str:
return re.sub(r"-particle\.\d+$", "", value.strip())
def artifact(path: Path, url: str) -> dict:
return {
"fileName": path.name,
"sizeBytes": path.stat().st_size,
"sha256": sha256(path),
"url": url,
}
def main() -> None:
if len(sys.argv) != 4:
raise SystemExit(
"usage: finalize_release.py <revision> <source-commit> <artifact-dir>"
)
revision = int(sys.argv[1])
source_commit = sys.argv[2]
artifact_dir = Path(sys.argv[3])
android_base = base_version((ROOT / "platform/android/VERSION").read_text())
ios_base = base_version((ROOT / "platform/ios/VERSION").read_text())
android_version = f"{android_base}-particle.{revision}"
ios_version = f"{ios_base}-particle.{revision}"
aar_name = f"android-sdk-particle-opengl-{android_version}.aar"
tgz_name = f"navsea-maplibre-particle-rn-{android_version}.tgz"
ios_name = "MapLibre.dynamic.xcframework.zip"
aar = artifact(
artifact_dir / aar_name,
f"{PUBLIC}/maven/cc/navsea/maplibre/android-sdk-particle-opengl/"
f"{android_version}/{aar_name}",
)
tgz = artifact(
artifact_dir / tgz_name,
f"{PUBLIC}/npm/{tgz_name}",
)
ios = artifact(
artifact_dir / ios_name,
f"{PUBLIC}/swift/maplibre-particle/{ios_version}/{ios_name}",
)
manifest = {
"schemaVersion": 2,
"releaseState": "PUBLISHED_PUBLIC_READBACK_PASS",
"particleRevision": revision,
"sourceCommit": source_commit,
"generatedAtUtc": datetime.now(timezone.utc).isoformat(),
"reactNative": {
"name": "@navsea/maplibre-particle-rn",
"version": android_version,
"artifact": tgz,
"mapLibreReactNative": "11.3.6",
},
"android": {
"version": android_version,
"coordinate": (
"cc.navsea.maplibre:android-sdk-particle-opengl:"
+ android_version
),
"renderer": "opengl",
"artifact": aar,
"ndk": os.environ.get("PARTICLE_NDK_VERSION"),
"gradle": os.environ.get("PARTICLE_GRADLE_VERSION"),
"jdk": os.environ.get("PARTICLE_JDK_VERSION"),
},
"ios": {
"distributionVersion": ios_version,
"renderer": "metal",
"moduleName": "MapLibre",
"artifact": ios,
"swiftPackage": {
"url": f"{PUBLIC}/swift/maplibre-particle-spm.git",
"requirement": "exactVersion",
"version": ios_version,
"productName": "MapLibre",
},
"xcode": os.environ.get("PARTICLE_XCODE_VERSION"),
"bazel": os.environ.get("PARTICLE_BAZEL_VERSION"),
},
}
(ROOT / "release/maplibre-particle/release-manifest.json").write_text(
json.dumps(manifest, ensure_ascii=False, indent=2) + "\n"
)
ios_manifest = {
"schemaVersion": 2,
"status": "PUBLISHED_PUBLIC_READBACK_PASS",
"distributionVersion": ios_version,
"moduleName": "MapLibre",
"artifact": ios,
"sourceCommit": source_commit,
"swiftPackage": manifest["ios"]["swiftPackage"],
"notes": (
"Metal renderer release built with "
"bazel build --//:renderer=metal //platform/ios:MapLibre.dynamic"
),
}
(ROOT / "release/maplibre-particle/ios/release-manifest.json").write_text(
json.dumps(ios_manifest, ensure_ascii=False, indent=2) + "\n"
)
package_template = f"""// swift-tools-version: 5.7
import PackageDescription
let package = Package(
name: "MapLibreDistribution",
products: [
.library(name: "MapLibre", targets: ["MapLibre"]),
],
targets: [
.binaryTarget(
name: "MapLibre",
url: "{ios['url']}",
checksum: "{ios['sha256']}"
),
]
)
"""
(ROOT / "release/maplibre-particle/ios/Package.swift.template").write_text(
package_template
)
release_readme = f"""# MapLibre Particle iOS 发行
当前正式发行:{ios_version}
- Renderer:Metal
- Product / Module:MapLibre
- Binary:{ios['url']}
- SHA-256:{ios['sha256']}
- SwiftPM:{PUBLIC}/swift/maplibre-particle-spm.git
- Exact Version:{ios_version}
- Source Commit:{source_commit}
构建命令固定为:
bazel build --//:renderer=metal //platform/ios:MapLibre.dynamic
不得用默认 OpenGL XCFramework 覆盖同版本。新版本必须使用新的 Particle revision。
"""
(ROOT / "release/maplibre-particle/ios/README.md").write_text(
release_readme
)
print(json.dumps(manifest, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,116 @@
#!/usr/bin/env python3
from __future__ import annotations
import json
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
ANDROID_VERSION_FILE = ROOT / "platform/android/VERSION"
IOS_VERSION_FILE = ROOT / "platform/ios/VERSION"
RN_ROOT = ROOT / "packages/maplibre-particle-rn"
def die(message: str) -> None:
raise SystemExit(f"PREPARE_RELEASE_ERROR: {message}")
def base_version(value: str) -> str:
return re.sub(r"-particle\.\d+$", "", value.strip())
def replace_current_particle(path: Path, base: str, target: str) -> None:
text = path.read_text()
pattern = rf"{re.escape(base)}-particle\.\d+"
updated, count = re.subn(pattern, target, text)
if count == 0:
die(f"no particle version reference found in {path}")
path.write_text(updated)
def update_changelog(target: str, revision: int) -> None:
path = RN_ROOT / "CHANGELOG.md"
text = path.read_text()
heading = f"## {target}(Android / iOS 统一发行)"
if heading in text:
return
marker = "## Unreleased\n\n"
if marker not in text:
die("CHANGELOG missing Unreleased heading")
entry = (
marker
+ heading
+ "\n\n"
+ "- Mac unified release pipeline 统一生成 Android Native、RN package 与 iOS SwiftPM/XCFramework;"
+ f"共享 Particle revision {revision}。\n"
)
path.write_text(text.replace(marker, entry, 1))
def update_testhost_spm(ios_version: str) -> None:
path = ROOT / "integration/react-native/MapLibreParticleRNTestHost/ios/Podfile"
text = path.read_text()
text = re.sub(
r'url: "[^"]+"',
'url: "https://pbf.jpcn.ccwu.cc/swift/maplibre-particle-spm.git"',
text,
count=1,
)
text = re.sub(
r'version: "[^"]+"',
f'version: "{ios_version}"',
text,
count=1,
)
path.write_text(text)
def main() -> None:
if len(sys.argv) != 2 or not sys.argv[1].isdigit():
die("usage: prepare_release.py <particle-revision>")
revision = int(sys.argv[1])
if revision < 1:
die("revision must be positive")
android_base = base_version(ANDROID_VERSION_FILE.read_text())
ios_base = base_version(IOS_VERSION_FILE.read_text())
android_version = f"{android_base}-particle.{revision}"
ios_version = f"{ios_base}-particle.{revision}"
ANDROID_VERSION_FILE.write_text(android_version + "\n")
package_path = RN_ROOT / "package.json"
package = json.loads(package_path.read_text())
package["version"] = android_version
package_path.write_text(json.dumps(package, ensure_ascii=False, indent=2) + "\n")
for relative in (
"packages/maplibre-particle-rn/android/build.gradle",
"packages/maplibre-particle-rn/app.plugin.js",
"integration/react-native/MapLibreParticleRNTestHost/plugins/withParticleNativeAndroid.js",
):
replace_current_particle(ROOT / relative, android_base, android_version)
readme = RN_ROOT / "README.md"
readme_text = readme.read_text()
readme_text = re.sub(
rf"{re.escape(android_base)}-particle\.\d+",
android_version,
readme_text,
)
readme_text = re.sub(
r"必须指向同一 `\.\d+`",
f"必须指向同一 `.{revision}`",
readme_text,
)
readme.write_text(readme_text)
update_changelog(android_version, revision)
update_testhost_spm(ios_version)
print(f"PARTICLE_REVISION={revision}")
print(f"ANDROID_RN_VERSION={android_version}")
print(f"IOS_DISTRIBUTION_VERSION={ios_version}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,88 @@
#!/usr/bin/env python3
from __future__ import annotations
import subprocess
import sys
from pathlib import Path
PUBLIC_ROOT = "https://pbf.jpcn.ccwu.cc/swift/maplibre-particle"
GIT_REMOTE = "nas:/volume1/work/pbf/swift/maplibre-particle-spm.git"
WORK = Path.home() / "dev/MapLibreParticleDistribution"
def run(*args: str, cwd: Path | None = None) -> str:
result = subprocess.run(
args,
cwd=cwd,
check=True,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
return result.stdout
def main() -> None:
if len(sys.argv) != 3:
raise SystemExit("usage: publish_swiftpm.py <ios-version> <zip-sha256>")
version, checksum = sys.argv[1], sys.argv[2]
if not WORK.exists():
run("git", "clone", GIT_REMOTE, str(WORK))
run("git", "fetch", "origin", cwd=WORK)
run("git", "checkout", "main", cwd=WORK)
run("git", "pull", "--ff-only", "origin", "main", cwd=WORK)
tag_check = subprocess.run(
["git", "ls-remote", "--tags", "origin", f"refs/tags/{version}"],
cwd=WORK,
text=True,
stdout=subprocess.PIPE,
check=True,
).stdout.strip()
if tag_check:
raise SystemExit(f"SwiftPM tag already exists: {version}")
url = f"{PUBLIC_ROOT}/{version}/MapLibre.dynamic.xcframework.zip"
package = f'''// swift-tools-version: 5.7
import PackageDescription
let package = Package(
name: "MapLibreDistribution",
products: [
.library(name: "MapLibre", targets: ["MapLibre"]),
],
targets: [
.binaryTarget(
name: "MapLibre",
url: "{url}",
checksum: "{checksum}"
),
]
)
'''
(WORK / "Package.swift").write_text(package)
(WORK / "README.md").write_text(
"# MapLibre Particle SwiftPM Distribution\n\n"
"该仓库只保存 NavSea MapLibre Particle iOS binary distribution 的 SwiftPM manifest。\n"
"产品和模块名固定为 MapLibre,供 MapLibre React Native 11.3.6 消费。\n\n"
f"当前版本:{version}。\n"
f"对应 binary:{url}\n"
)
run("git", "add", "Package.swift", "README.md", cwd=WORK)
run("git", "commit", "-m", f"release: MapLibre Particle iOS {version}", cwd=WORK)
run("git", "tag", "-a", version, "-m", f"MapLibre Particle iOS {version}", cwd=WORK)
run("git", "push", "origin", "main", cwd=WORK)
run("git", "push", "origin", version, cwd=WORK)
run(
"ssh", "nas",
"git --git-dir=/volume1/work/pbf/swift/maplibre-particle-spm.git "
"symbolic-ref HEAD refs/heads/main && "
"git --git-dir=/volume1/work/pbf/swift/maplibre-particle-spm.git update-server-info",
)
print(f"SWIFTPM_PUBLISHED={version}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,187 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(cd -- "$SCRIPT_DIR/../.." && pwd)"
REV="${1:-}"
PHASE="${2:-all}"
die() { echo "RELEASE_ERROR: $*" >&2; exit 1; }
note() { echo "RELEASE: $*"; }
require() { command -v "$1" >/dev/null 2>&1 || die "missing command: $1"; }
[[ "$REV" =~ ^[0-9]+$ ]] || die "usage: release.sh <particle-revision> [prepare|build|publish|finalize|all]"
[[ "$PHASE" =~ ^(prepare|build|publish|finalize|all)$ ]] || die "invalid phase: $PHASE"
[[ "$(uname -s)" == "Darwin" ]] || die "unified release builder must run on macOS"
export JAVA_HOME="${JAVA_HOME:-/usr/local/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home}"
export ANDROID_HOME="${ANDROID_HOME:-$HOME/Library/Android/sdk}"
export ANDROID_SDK_ROOT="$ANDROID_HOME"
export ANDROID_NDK_HOME="$ANDROID_HOME/ndk/28.2.13676358"
export PATH="$JAVA_HOME/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/cmake/3.31.6/bin:$PATH"
if [[ -d /usr/local/opt/gnu-sed/libexec/gnubin ]]; then
export PATH="/usr/local/opt/gnu-sed/libexec/gnubin:$PATH"
fi
for cmd in git python3 node npm java curl ssh scp shasum bazel xcodebuild ruby tar cmp awk sed; do require "$cmd"; done
[[ -d "$ANDROID_NDK_HOME" ]] || die "NDK missing: $ANDROID_NDK_HOME"
[[ -x "$ROOT/platform/android/gradlew" ]] || die "Gradle wrapper missing"
ANDROID_BASE="$(sed -E 's/-particle\.[0-9]+$//' "$ROOT/platform/android/VERSION")"
IOS_BASE="$(sed -E 's/-particle\.[0-9]+$//' "$ROOT/platform/ios/VERSION")"
ANDROID_VERSION="$ANDROID_BASE-particle.$REV"
IOS_VERSION="$IOS_BASE-particle.$REV"
RN_ROOT="$ROOT/packages/maplibre-particle-rn"
ARTIFACT_DIR="$ROOT/artifacts/maplibre-particle/$ANDROID_VERSION"
M2_DIR="$HOME/.m2/repository/cc/navsea/maplibre/android-sdk-particle-opengl/$ANDROID_VERSION"
AAR_NAME="android-sdk-particle-opengl-$ANDROID_VERSION.aar"
TGZ_NAME="navsea-maplibre-particle-rn-$ANDROID_VERSION.tgz"
IOS_ZIP_NAME="MapLibre.dynamic.xcframework.zip"
ANDROID_URL="https://pbf.jpcn.ccwu.cc/maven/cc/navsea/maplibre/android-sdk-particle-opengl/$ANDROID_VERSION/$AAR_NAME"
RN_URL="https://pbf.jpcn.ccwu.cc/npm/$TGZ_NAME"
IOS_URL="https://pbf.jpcn.ccwu.cc/swift/maplibre-particle/$IOS_VERSION/$IOS_ZIP_NAME"
SPM_URL="https://pbf.jpcn.ccwu.cc/swift/maplibre-particle-spm.git"
public_status() { curl -L -s -o /dev/null -w '%{http_code}' "$1"; }
require_absent() {
local code; code="$(public_status "$1")"
[[ "$code" == "404" ]] || die "target already published (HTTP $code): $1"
}
require_clean() {
[[ -z "$(git -C "$ROOT" status --porcelain=v1)" ]] || die "release worktree must be clean"
}
require_target_versions() {
[[ "$(tr -d '\r\n' < "$ROOT/platform/android/VERSION")" == "$ANDROID_VERSION" ]] || die "Android VERSION mismatch"
[[ "$(node -p "require(process.argv[1]).version" "$RN_ROOT/package.json")" == "$ANDROID_VERSION" ]] || die "RN version mismatch"
grep -Fq "android-sdk-particle-opengl:$ANDROID_VERSION" "$RN_ROOT/android/build.gradle" || die "RN Android coordinate mismatch"
grep -Fq "android-sdk-particle-opengl:$ANDROID_VERSION" "$RN_ROOT/app.plugin.js" || die "Expo plugin coordinate mismatch"
}
prepare_release() {
require_clean
require_absent "$ANDROID_URL"
require_absent "$RN_URL"
require_absent "$IOS_URL"
[[ -z "$(git ls-remote --tags "$SPM_URL" "refs/tags/$IOS_VERSION")" ]] || die "SwiftPM tag already exists: $IOS_VERSION"
python3 "$SCRIPT_DIR/prepare_release.py" "$REV"
git -C "$ROOT" diff --check
require_target_versions
git -C "$ROOT" add \
platform/android/VERSION \
packages/maplibre-particle-rn/package.json \
packages/maplibre-particle-rn/android/build.gradle \
packages/maplibre-particle-rn/app.plugin.js \
packages/maplibre-particle-rn/README.md \
packages/maplibre-particle-rn/CHANGELOG.md \
integration/react-native/MapLibreParticleRNTestHost/plugins/withParticleNativeAndroid.js \
integration/react-native/MapLibreParticleRNTestHost/ios/Podfile
if ! git -C "$ROOT" diff --cached --quiet; then
git -C "$ROOT" commit -m "release: prepare unified particle.$REV"
fi
require_clean
note "prepared sourceCommit=$(git -C "$ROOT" rev-parse HEAD)"
}
build_release() {
require_clean
require_target_versions
git -C "$ROOT" submodule update --init --recursive
rm -rf "$ARTIFACT_DIR"
mkdir -p "$ARTIFACT_DIR"
printf 'sdk.dir=%s\n' "$ANDROID_HOME" > "$ROOT/platform/android/local.properties"
note "building Android OpenGL four-ABI release"
(cd "$ROOT/platform/android" && RENDERER=opengl BUILDTYPE=Release make apackage)
note "publishing Android artifact to Maven Local"
"$ROOT/platform/android/gradlew" -p "$ROOT/platform/android" -Pmaplibre.abis=all \
:MapLibreAndroid:publishOpenglreleasePublicationToMavenLocal --console=plain
for f in "$AAR_NAME" "android-sdk-particle-opengl-$ANDROID_VERSION.pom" "android-sdk-particle-opengl-$ANDROID_VERSION.module"; do
[[ -f "$M2_DIR/$f" ]] || die "missing Maven Local artifact: $M2_DIR/$f"
cp "$M2_DIR/$f" "$ARTIFACT_DIR/"
done
note "building iOS Metal XCFramework"
(cd "$ROOT" && bazel build --//:renderer=metal //platform/ios:MapLibre.dynamic)
IOS_SOURCE="$ROOT/bazel-out/darwin_x86_64-fastbuild/bin/platform/ios/$IOS_ZIP_NAME"
[[ -f "$IOS_SOURCE" ]] || die "iOS XCFramework zip missing"
cp "$IOS_SOURCE" "$ARTIFACT_DIR/$IOS_ZIP_NAME"
note "packing RN package"
(cd "$RN_ROOT" && npm pack --pack-destination "$ARTIFACT_DIR")
[[ -f "$ARTIFACT_DIR/$TGZ_NAME" ]] || die "RN tgz missing"
tar -xOf "$ARTIFACT_DIR/$TGZ_NAME" package/package.json | node -e 'let s="";process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{let j=JSON.parse(s);if(j.version!==process.argv[1])process.exit(2)})' "$ANDROID_VERSION"
tar -xOf "$ARTIFACT_DIR/$TGZ_NAME" package/android/build.gradle | grep -Fq "android-sdk-particle-opengl:$ANDROID_VERSION" || die "packed RN points to wrong Android version"
ruby -c "$RN_ROOT/MapLibreParticleRN.podspec" >/dev/null
shasum -a 256 "$ARTIFACT_DIR/$AAR_NAME" "$ARTIFACT_DIR/$TGZ_NAME" "$ARTIFACT_DIR/$IOS_ZIP_NAME"
note "build complete sourceCommit=$(git -C "$ROOT" rev-parse HEAD)"
}
verify_public_file() {
local local_file="$1" url="$2" tmp
tmp="$(mktemp)"
curl -fL "$url?nocache=$(date +%s)" -o "$tmp"
cmp -s "$local_file" "$tmp" || { rm -f "$tmp"; die "public readback mismatch: $url"; }
rm -f "$tmp"
}
publish_release() {
require_clean
require_target_versions
[[ -f "$ARTIFACT_DIR/$AAR_NAME" && -f "$ARTIFACT_DIR/$TGZ_NAME" && -f "$ARTIFACT_DIR/$IOS_ZIP_NAME" ]] || die "build artifacts missing"
require_absent "$ANDROID_URL"
require_absent "$RN_URL"
require_absent "$IOS_URL"
ssh nas "test ! -e /volume1/work/pbf/maven/cc/navsea/maplibre/android-sdk-particle-opengl/$ANDROID_VERSION" || die "remote Android version exists"
ssh nas "test ! -e /volume1/work/pbf/npm/$TGZ_NAME" || die "remote RN tgz exists"
ssh nas "test ! -e /volume1/work/pbf/swift/maplibre-particle/$IOS_VERSION" || die "remote iOS version exists"
ssh nas "mkdir -p /volume1/work/pbf/maven/cc/navsea/maplibre/android-sdk-particle-opengl/$ANDROID_VERSION /volume1/work/pbf/swift/maplibre-particle/$IOS_VERSION"
scp "$M2_DIR"/* "nas:/volume1/work/pbf/maven/cc/navsea/maplibre/android-sdk-particle-opengl/$ANDROID_VERSION/"
scp "$ARTIFACT_DIR/$TGZ_NAME" "nas:/volume1/work/pbf/npm/$TGZ_NAME"
scp "$ARTIFACT_DIR/$IOS_ZIP_NAME" "nas:/volume1/work/pbf/swift/maplibre-particle/$IOS_VERSION/$IOS_ZIP_NAME"
IOS_SHA="$(shasum -a 256 "$ARTIFACT_DIR/$IOS_ZIP_NAME" | awk '{print $1}')"
printf '%s %s\n' "$IOS_SHA" "$IOS_ZIP_NAME" > "$ARTIFACT_DIR/$IOS_ZIP_NAME.sha256"
scp "$ARTIFACT_DIR/$IOS_ZIP_NAME.sha256" "nas:/volume1/work/pbf/swift/maplibre-particle/$IOS_VERSION/$IOS_ZIP_NAME.sha256"
verify_public_file "$ARTIFACT_DIR/$AAR_NAME" "$ANDROID_URL"
verify_public_file "$ARTIFACT_DIR/$TGZ_NAME" "$RN_URL"
verify_public_file "$ARTIFACT_DIR/$IOS_ZIP_NAME" "$IOS_URL"
python3 "$SCRIPT_DIR/publish_swiftpm.py" "$IOS_VERSION" "$IOS_SHA"
git ls-remote "$SPM_URL" "refs/tags/$IOS_VERSION" | grep -q . || die "public SwiftPM tag missing"
export PARTICLE_NDK_VERSION="28.2.13676358"
export PARTICLE_GRADLE_VERSION="$("$ROOT/platform/android/gradlew" -p "$ROOT/platform/android" --version | awk '/^Gradle /{print $2; exit}')"
export PARTICLE_JDK_VERSION="$(java -version 2>&1 | head -1)"
export PARTICLE_XCODE_VERSION="$(xcodebuild -version | tr '\n' ' ')"
export PARTICLE_BAZEL_VERSION="$(bazel --version)"
python3 "$SCRIPT_DIR/finalize_release.py" "$REV" "$(git -C "$ROOT" rev-parse HEAD)" "$ARTIFACT_DIR"
git -C "$ROOT" diff --check
note "publish and public readback passed"
}
finalize_release() {
git -C "$ROOT" add \
release/maplibre-particle/release-manifest.json \
release/maplibre-particle/ios/release-manifest.json \
release/maplibre-particle/ios/Package.swift.template \
release/maplibre-particle/ios/README.md
if ! git -C "$ROOT" diff --cached --quiet; then git -C "$ROOT" commit -m "release: finalize unified particle.$REV"; fi
TAG="maplibre-particle/rn/$ANDROID_VERSION"
[[ -z "$(git -C "$ROOT" tag -l "$TAG")" ]] || die "local tag exists: $TAG"
git -C "$ROOT" fetch origin particle/main
git -C "$ROOT" merge-base --is-ancestor origin/particle/main HEAD || die "origin/particle/main is not an ancestor of release HEAD"
git -C "$ROOT" tag -a "$TAG" -m "Unified Android/iOS Particle release $ANDROID_VERSION"
git -C "$ROOT" push origin HEAD:particle/main
git -C "$ROOT" push origin "$TAG"
note "RELEASE_PASS version=$ANDROID_VERSION ios=$IOS_VERSION tag=$TAG"
}
case "$PHASE" in
prepare) prepare_release ;;
build) build_release ;;
publish) publish_release ;;
finalize) finalize_release ;;
all)
prepare_release
build_release
publish_release
finalize_release
;;
esac