deployment.sh (11265B)
1 #!/usr/bin/env bash 2 set -euo pipefail 3 4 usage() { 5 echo "Usage: $0 <version>" >&2 6 echo "Example: $0 0.2.48" >&2 7 } 8 9 die() { 10 echo "error: $*" >&2 11 exit 1 12 } 13 14 replace_in_file() { 15 local file="$1" 16 local pattern="$2" 17 local replacement="$3" 18 perl -0pi -e "s|${pattern}|${replacement}|g" "$file" 19 } 20 21 ensure_clean_worktree() { 22 if ! git diff --quiet || ! git diff --cached --quiet || [ -n "$(git ls-files --others --exclude-standard)" ]; then 23 die "worktree is not clean; commit or stash changes before releasing" 24 fi 25 } 26 27 ensure_tauri_cli() { 28 if ! cargo tauri --version >/dev/null 2>&1; then 29 cargo install tauri-cli --locked --version "^2" 30 fi 31 } 32 33 update_versions() { 34 local version="$1" 35 36 replace_in_file Cargo.toml '(\[package\]\nname = "iuna"\nversion = ")[^"]+' "\${1}${version}" 37 replace_in_file src-tauri/Cargo.toml '(\[package\]\nname = "iuna-desktop"\nversion = ")[^"]+' "\${1}${version}" 38 replace_in_file src-tauri/tauri.conf.json '("version": ")[^"]+' "\${1}${version}" 39 replace_in_file README.md 'downloads/iuna-v[0-9]+\.[0-9]+\.[0-9]+-macos-aarch64-desktop\.app\.zip' "downloads/iuna-v${version}-macos-aarch64-desktop.app.zip" 40 replace_in_file README.md 'downloads/iuna-v[0-9]+\.[0-9]+\.[0-9]+-windows-x86_64-desktop-setup\.exe' "downloads/iuna-v${version}-windows-x86_64-desktop-setup.exe" 41 42 cargo update -p iuna --precise "$version" 43 cargo update --manifest-path src-tauri/Cargo.toml -p iuna-desktop --precise "$version" 44 cargo check --locked >/dev/null 45 cargo check --locked --manifest-path src-tauri/Cargo.toml >/dev/null 46 } 47 48 commit_and_tag() { 49 local version="$1" 50 local tag="v${version}" 51 52 git add Cargo.toml Cargo.lock src-tauri/Cargo.toml src-tauri/Cargo.lock src-tauri/tauri.conf.json README.md 53 git commit -m "Release ${tag}" 54 git tag -a "$tag" -m "Release ${tag}" 55 } 56 57 build_macos_desktop_if_possible() { 58 local version="$1" 59 local artifact="downloads/iuna-v${version}-macos-aarch64-desktop.app.zip" 60 61 [ -f "$artifact" ] && return 0 62 [ "$(uname -s)" = "Darwin" ] || return 0 63 [ "$(uname -m)" = "arm64" ] || die "macOS desktop artifact requires Apple silicon; expected ${artifact}" 64 65 ensure_tauri_cli 66 cargo build --release --locked 67 mkdir -p src-tauri/binaries downloads 68 cp target/release/iuna src-tauri/binaries/iuna-sidecar-aarch64-apple-darwin 69 chmod +x src-tauri/binaries/iuna-sidecar-aarch64-apple-darwin 70 (cd src-tauri && cargo tauri build --bundles app) 71 72 local app="src-tauri/target/release/bundle/macos/iuna.app" 73 codesign --force --deep --sign - --options runtime "$app" 74 codesign --verify --deep --strict --verbose=4 "$app" 75 ditto -c -k --keepParent "$app" "$artifact" 76 } 77 78 build_windows_desktop_if_possible() { 79 local version="$1" 80 local artifact="downloads/iuna-v${version}-windows-x86_64-desktop-setup.exe" 81 82 [ -f "$artifact" ] && return 0 83 case "$(uname -s)" in 84 MINGW*|MSYS*|CYGWIN*) ;; 85 *) return 0 ;; 86 esac 87 88 ensure_tauri_cli 89 cargo build --release --locked 90 mkdir -p src-tauri/binaries downloads 91 cp target/release/iuna.exe src-tauri/binaries/iuna-sidecar-x86_64-pc-windows-msvc.exe 92 (cd src-tauri && cargo tauri build --bundles nsis) 93 94 local installer 95 installer="$(find src-tauri/target/release/bundle/nsis -maxdepth 1 -type f -name '*.exe' | head -n 1)" 96 [ -n "$installer" ] || die "Windows installer was not produced" 97 cp "$installer" "$artifact" 98 } 99 100 build_windows_desktop_in_docker_if_possible() { 101 local version="$1" 102 local artifact="downloads/iuna-v${version}-windows-x86_64-desktop-setup.exe" 103 104 [ -f "$artifact" ] && return 0 105 command -v docker >/dev/null 2>&1 || return 0 106 107 mkdir -p downloads 108 docker run --rm --platform=linux/amd64 \ 109 -e "IUNA_VERSION=${version}" \ 110 -e "HOST_UID=$(id -u)" \ 111 -e "HOST_GID=$(id -g)" \ 112 -v "$(pwd):/src/iuna:ro" \ 113 -v "$(pwd)/downloads:/out" \ 114 rust:1.86-bookworm \ 115 bash -c ' 116 set -euo pipefail 117 118 apt-get update 119 apt-get install -y --no-install-recommends clang lld llvm nsis 120 rm -rf /var/lib/apt/lists/* 121 rustup target add x86_64-pc-windows-msvc 122 cargo install --locked cargo-xwin --version 0.19.2 123 cargo install --locked tauri-cli --version "^2" 124 125 mkdir -p /work/iuna 126 tar -C /src/iuna \ 127 --exclude=./target \ 128 --exclude=./src-tauri/target \ 129 --exclude=./src-tauri/binaries \ 130 --exclude=./.agents \ 131 --exclude=./.codex \ 132 -cf - . | tar -C /work/iuna -xf - 133 134 cd /work/iuna 135 cargo xwin build --release --locked --target x86_64-pc-windows-msvc 136 mkdir -p src-tauri/binaries 137 cp target/x86_64-pc-windows-msvc/release/iuna.exe src-tauri/binaries/iuna-sidecar-x86_64-pc-windows-msvc.exe 138 139 cd src-tauri 140 cargo tauri build --runner cargo-xwin --target x86_64-pc-windows-msvc --bundles nsis 141 142 installer="$(find target/x86_64-pc-windows-msvc/release/bundle/nsis -maxdepth 1 -type f -name "*setup.exe" | head -n 1)" 143 [ -n "$installer" ] || { echo "Windows installer was not produced" >&2; exit 1; } 144 cp "$installer" "/out/iuna-v${IUNA_VERSION}-windows-x86_64-desktop-setup.exe" 145 chown "${HOST_UID}:${HOST_GID}" "/out/iuna-v${IUNA_VERSION}-windows-x86_64-desktop-setup.exe" 146 ' 147 } 148 149 require_desktop_artifacts() { 150 local version="$1" 151 local macos_artifact="downloads/iuna-v${version}-macos-aarch64-desktop.app.zip" 152 local windows_artifact="downloads/iuna-v${version}-windows-x86_64-desktop-setup.exe" 153 154 [ -f "$macos_artifact" ] || die "missing ${macos_artifact}" 155 [ -f "$windows_artifact" ] || die "missing ${windows_artifact}" 156 } 157 158 build_linux_cli_archives() { 159 local version="$1" 160 local tag="v${version}" 161 local linux_x86_64_package="iuna-${tag}-linux-x86_64" 162 local linux_aarch64_package="iuna-${tag}-linux-aarch64" 163 164 mkdir -p downloads 165 [ -f "downloads/${linux_x86_64_package}.tar.gz" ] && [ -f "downloads/${linux_aarch64_package}.tar.gz" ] && return 0 166 167 docker run --rm --platform=linux/amd64 \ 168 -e "IUNA_VERSION=${version}" \ 169 -e "HOST_UID=$(id -u)" \ 170 -e "HOST_GID=$(id -g)" \ 171 -v "$(pwd):/src/iuna:ro" \ 172 -v "$(pwd)/downloads:/out" \ 173 rust:1.86-bookworm \ 174 bash -c ' 175 set -euo pipefail 176 177 apt-get update 178 apt-get install -y --no-install-recommends gcc-aarch64-linux-gnu libc6-dev-arm64-cross 179 rm -rf /var/lib/apt/lists/* 180 rustup target add aarch64-unknown-linux-gnu 181 182 mkdir -p /work/iuna 183 tar -C /src/iuna \ 184 --exclude=./target \ 185 --exclude=./src-tauri/target \ 186 --exclude=./src-tauri/binaries \ 187 --exclude=./.agents \ 188 --exclude=./.codex \ 189 -cf - . | tar -C /work/iuna -xf - 190 191 cd /work/iuna 192 CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc \ 193 AR_aarch64_unknown_linux_gnu=aarch64-linux-gnu-ar \ 194 CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc \ 195 cargo build --release --locked --target aarch64-unknown-linux-gnu 196 cargo build --release --locked 197 198 tag="v${IUNA_VERSION}" 199 linux_x86_64_package="iuna-${tag}-linux-x86_64" 200 linux_aarch64_package="iuna-${tag}-linux-aarch64" 201 mkdir -p "/tmp/site/${linux_x86_64_package}" "/tmp/site/${linux_aarch64_package}" 202 cp target/release/iuna "/tmp/site/${linux_x86_64_package}/" 203 cp target/aarch64-unknown-linux-gnu/release/iuna "/tmp/site/${linux_aarch64_package}/" 204 cp README.md LICENSE "/tmp/site/${linux_x86_64_package}/" 205 cp README.md LICENSE "/tmp/site/${linux_aarch64_package}/" 206 tar -C /tmp/site -czf "/out/${linux_x86_64_package}.tar.gz" "${linux_x86_64_package}" 207 tar -C /tmp/site -czf "/out/${linux_aarch64_package}.tar.gz" "${linux_aarch64_package}" 208 chown "${HOST_UID}:${HOST_GID}" "/out/${linux_x86_64_package}.tar.gz" "/out/${linux_aarch64_package}.tar.gz" 209 ' 210 } 211 212 write_download_checksums() { 213 ( 214 cd downloads 215 rm -f SHA256SUMS 216 217 local files=() 218 local file 219 for file in *; do 220 [ -f "$file" ] || continue 221 case "$file" in 222 .gitkeep|index.html|SHA256SUMS) continue ;; 223 esac 224 files+=("$file") 225 done 226 227 [ "${#files[@]}" -gt 0 ] || return 0 228 if command -v sha256sum >/dev/null 2>&1; then 229 sha256sum "${files[@]}" > SHA256SUMS 230 else 231 for file in "${files[@]}"; do 232 shasum -a 256 "$file" | awk "{print \$1 \" \" \$2}" 233 done > SHA256SUMS 234 fi 235 ) 236 } 237 238 build_versions() { 239 local version="$1" 240 241 mkdir -p downloads 242 build_linux_cli_archives "$version" 243 build_macos_desktop_if_possible "$version" 244 build_windows_desktop_if_possible "$version" 245 build_windows_desktop_in_docker_if_possible "$version" 246 require_desktop_artifacts "$version" 247 write_download_checksums 248 } 249 250 build_docker_image() { 251 local version="$1" 252 local www_image="${IUNA_WWW_IMAGE:-iuna-www:v${version}}" 253 local node_image="${IUNA_NODE_IMAGE:-iuna-node:v${version}}" 254 255 docker build --platform=linux/amd64 --progress=plain -t "$www_image" . 256 docker build --platform=linux/amd64 --progress=plain -t "$node_image" -f Dockerfile.node . 257 echo "Built Docker images: ${www_image}, ${node_image}" 258 } 259 260 import_image_to_k3s() { 261 local image="$1" 262 local tmp_folder="$2" 263 local remote_host="${IUNA_DEPLOY_HOST:-root@jhx.app}" 264 local remote_file="${image//[:\/]/_}.tar" 265 local image_file="${tmp_folder}/${remote_file}" 266 267 docker save "$image" -o "$image_file" 268 scp "$image_file" "${remote_host}:~/" 269 ssh "$remote_host" "sudo k3s ctr -n k8s.io images import ~/${remote_file} && rm ~/${remote_file}" 270 } 271 272 render_manifest() { 273 local www_image="$1" 274 local node_image="$2" 275 local output="$3" 276 277 sed \ 278 -e "s|\${IUNA_WWW_IMAGE}|${www_image}|g" \ 279 -e "s|\${IUNA_NODE_IMAGE}|${node_image}|g" \ 280 config/deployment.yml > "$output" 281 } 282 283 deploy_docker_image() { 284 local version="$1" 285 local www_image="${IUNA_WWW_IMAGE:-iuna-www:v${version}}" 286 local node_image="${IUNA_NODE_IMAGE:-iuna-node:v${version}}" 287 local kubectl_context="${IUNA_KUBECTL_CONTEXT:-jhx-app}" 288 local tmp_folder 289 290 tmp_folder="$(mktemp -d)" 291 trap 'rm -rf "$tmp_folder"' RETURN 292 293 import_image_to_k3s "$www_image" "$tmp_folder" 294 import_image_to_k3s "$node_image" "$tmp_folder" 295 render_manifest "$www_image" "$node_image" "${tmp_folder}/deployment.yml" 296 297 local current_www_selector 298 current_www_selector="$(kubectl --context "$kubectl_context" -n iuna get deployment www -o jsonpath='{.spec.selector.matchLabels.app}' 2>/dev/null || true)" 299 if [ -n "$current_www_selector" ] && [ "$current_www_selector" != "iuna-www" ]; then 300 kubectl --context "$kubectl_context" -n iuna delete deployment www --wait=true 301 fi 302 303 kubectl --context "$kubectl_context" apply -f "${tmp_folder}/deployment.yml" 304 kubectl --context "$kubectl_context" -n iuna rollout restart deployment/www deployment/node 305 kubectl --context "$kubectl_context" -n iuna rollout status deployment/www 306 kubectl --context "$kubectl_context" -n iuna rollout status deployment/node 307 } 308 309 main() { 310 [ "$#" -eq 1 ] || { usage; exit 2; } 311 312 local version="${1#v}" 313 [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || die "version must look like 0.2.48" 314 315 ensure_clean_worktree 316 git rev-parse --verify "v${version}" >/dev/null 2>&1 && die "tag v${version} already exists" 317 318 update_versions "$version" 319 commit_and_tag "$version" 320 build_versions "$version" 321 build_docker_image "$version" 322 deploy_docker_image "$version" 323 } 324 325 main "$@"