iuna

iuna - experimental devnet protocol
git clone https://iuna.jhx.app/git/iuna.git
Log | Files | Refs | README | LICENSE

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 "$@"