iuna

iuna

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

deployment.sh (14407B)


      1 #!/usr/bin/env bash
      2 set -euo pipefail
      3 
      4 cd "$(dirname "${BASH_SOURCE[0]}")"
      5 
      6 usage() {
      7   echo "Usage: $0 <version>" >&2
      8   echo "Example: $0 0.2.48" >&2
      9 }
     10 
     11 die() {
     12   echo "error: $*" >&2
     13   exit 1
     14 }
     15 
     16 require_command() {
     17   local command_name="$1"
     18 
     19   command -v "$command_name" >/dev/null 2>&1 || die "missing required command: ${command_name}"
     20 }
     21 
     22 is_apple_silicon_macos() {
     23   [ "$(uname -s)" = "Darwin" ] || return 1
     24   [ "$(uname -m)" = "arm64" ] && return 0
     25   [ "$(sysctl -n hw.optional.arm64 2>/dev/null || true)" = "1" ]
     26 }
     27 
     28 confirm() {
     29   local prompt="$1"
     30   local answer
     31 
     32   if ! read -r -p "$prompt" answer || [[ ! "$answer" =~ ^[Yy]$ ]]; then
     33     return 1
     34   fi
     35 }
     36 
     37 escape_sed_replacement() {
     38   printf '%s' "$1" | sed -e 's/[\/&]/\\&/g'
     39 }
     40 
     41 replace_in_file() {
     42   local file="$1"
     43   local pattern="$2"
     44   local replacement="$3"
     45   perl -0pi -e "s|${pattern}|${replacement}|g" "$file"
     46 }
     47 
     48 ensure_clean_worktree() {
     49   require_command git
     50 
     51   if ! git diff --quiet || ! git diff --cached --quiet || [ -n "$(git ls-files --others --exclude-standard)" ]; then
     52     die "worktree is not clean; commit or stash changes before releasing"
     53   fi
     54 }
     55 
     56 ensure_head_matches_tag() {
     57   local tag="$1"
     58   local head_commit
     59   local tag_commit
     60 
     61   head_commit="$(git rev-parse HEAD)"
     62   tag_commit="$(git rev-parse "${tag}^{commit}")"
     63   [ "$head_commit" = "$tag_commit" ] || die "${tag} exists, but HEAD is not at ${tag}; checkout ${tag} before redeploying it"
     64 }
     65 
     66 ensure_tauri_cli() {
     67   require_command cargo
     68 
     69   if ! cargo tauri --version >/dev/null 2>&1; then
     70     cargo install tauri-cli --locked --version "^2"
     71   fi
     72 }
     73 
     74 update_versions() {
     75   local version="$1"
     76 
     77   require_command cargo
     78   require_command perl
     79 
     80   replace_in_file Cargo.toml '(\[package\]\nname = "iuna"\nversion = ")[^"]+' "\${1}${version}"
     81   replace_in_file src-tauri/Cargo.toml '(\[package\]\nname = "iuna-desktop"\nversion = ")[^"]+' "\${1}${version}"
     82   replace_in_file src-tauri/tauri.conf.json '("version": ")[^"]+' "\${1}${version}"
     83   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"
     84   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"
     85 
     86   cargo update -p iuna --precise "$version"
     87   cargo update --manifest-path src-tauri/Cargo.toml -p iuna-desktop --precise "$version"
     88   cargo check --locked >/dev/null
     89   cargo check --locked --manifest-path src-tauri/Cargo.toml >/dev/null
     90 }
     91 
     92 commit_and_tag() {
     93   local version="$1"
     94   local tag="v${version}"
     95 
     96   require_command git
     97 
     98   git add Cargo.toml Cargo.lock src-tauri/Cargo.toml src-tauri/Cargo.lock src-tauri/tauri.conf.json README.md
     99   git commit -m "Release ${tag}"
    100   git tag -a "$tag" -m "Release ${tag}"
    101 }
    102 
    103 build_macos_desktop_if_possible() {
    104   local version="$1"
    105   local artifact="downloads/iuna-v${version}-macos-aarch64-desktop.app.zip"
    106 
    107   [ -f "$artifact" ] && return 0
    108   [ "$(uname -s)" = "Darwin" ] || return 0
    109   is_apple_silicon_macos || die "macOS desktop artifact requires Apple silicon; expected ${artifact}"
    110 
    111   require_command codesign
    112   require_command ditto
    113   require_command rustup
    114   ensure_tauri_cli
    115   rustup target add aarch64-apple-darwin
    116   cargo build --release --locked --target aarch64-apple-darwin
    117   mkdir -p src-tauri/binaries downloads
    118   cp target/aarch64-apple-darwin/release/iuna src-tauri/binaries/iuna-sidecar-aarch64-apple-darwin
    119   chmod +x src-tauri/binaries/iuna-sidecar-aarch64-apple-darwin
    120   (cd src-tauri && cargo tauri build --target aarch64-apple-darwin --bundles app)
    121 
    122   local app="src-tauri/target/aarch64-apple-darwin/release/bundle/macos/iuna.app"
    123   codesign --force --deep --sign - --options runtime "$app"
    124   codesign --verify --deep --strict --verbose=4 "$app"
    125   ditto -c -k --keepParent "$app" "$artifact"
    126 }
    127 
    128 build_windows_desktop_if_possible() {
    129   local version="$1"
    130   local artifact="downloads/iuna-v${version}-windows-x86_64-desktop-setup.exe"
    131 
    132   [ -f "$artifact" ] && return 0
    133   case "$(uname -s)" in
    134     MINGW*|MSYS*|CYGWIN*) ;;
    135     *) return 0 ;;
    136   esac
    137 
    138   ensure_tauri_cli
    139   cargo build --release --locked
    140   mkdir -p src-tauri/binaries downloads
    141   cp target/release/iuna.exe src-tauri/binaries/iuna-sidecar-x86_64-pc-windows-msvc.exe
    142   (cd src-tauri && cargo tauri build --bundles nsis)
    143 
    144   local installer
    145   installer="$(find src-tauri/target/release/bundle/nsis -maxdepth 1 -type f -name '*.exe' | head -n 1)"
    146   [ -n "$installer" ] || die "Windows installer was not produced"
    147   cp "$installer" "$artifact"
    148 }
    149 
    150 build_windows_desktop_in_docker_if_possible() {
    151   local version="$1"
    152   local artifact="downloads/iuna-v${version}-windows-x86_64-desktop-setup.exe"
    153 
    154   [ -f "$artifact" ] && return 0
    155   command -v docker >/dev/null 2>&1 || return 0
    156 
    157   mkdir -p downloads
    158   docker run --rm --platform=linux/amd64 \
    159     -e "IUNA_VERSION=${version}" \
    160     -e "HOST_UID=$(id -u)" \
    161     -e "HOST_GID=$(id -g)" \
    162     -v iuna-windows-cargo-registry:/usr/local/cargo/registry \
    163     -v iuna-windows-cargo-git:/usr/local/cargo/git \
    164     -v iuna-windows-root-cache:/root/.cache \
    165     -v iuna-windows-target:/work/iuna/target \
    166     -v iuna-windows-tauri-target:/work/iuna/src-tauri/target \
    167     -v "$(pwd):/src/iuna:ro" \
    168     -v "$(pwd)/downloads:/out" \
    169     rust:1.86-bookworm \
    170     bash -c '
    171       set -euo pipefail
    172 
    173       apt-get update
    174       apt-get install -y --no-install-recommends clang lld llvm nsis
    175       rm -rf /var/lib/apt/lists/*
    176       rustup target add x86_64-pc-windows-msvc
    177       cargo install --locked cargo-xwin --version 0.19.2
    178       cargo install --locked tauri-cli --version "^2"
    179 
    180       nsis_utils_path=/root/.cache/tauri/NSIS/Plugins/x86-unicode/additional/nsis_tauri_utils.dll
    181       mkdir -p "$(dirname "$nsis_utils_path")"
    182       if [ ! -f "$nsis_utils_path" ]; then
    183         curl --fail --location --retry 8 --retry-all-errors --retry-delay 3 \
    184           --output "$nsis_utils_path" \
    185           https://github.com/tauri-apps/nsis-tauri-utils/releases/download/nsis_tauri_utils-v0.5.3/nsis_tauri_utils.dll
    186         echo "75197fee3c6a814fe035788d1c34ead39349b860  $nsis_utils_path" | sha1sum -c -
    187       fi
    188 
    189       mkdir -p /work/iuna
    190       tar -C /src/iuna \
    191         --exclude=./target \
    192         --exclude=./src-tauri/target \
    193         --exclude=./src-tauri/binaries \
    194         --exclude=./.agents \
    195         --exclude=./.codex \
    196         -cf - . | tar -C /work/iuna -xf -
    197 
    198       cd /work/iuna
    199       cargo xwin build --release --locked --target x86_64-pc-windows-msvc
    200       mkdir -p src-tauri/binaries
    201       cp target/x86_64-pc-windows-msvc/release/iuna.exe src-tauri/binaries/iuna-sidecar-x86_64-pc-windows-msvc.exe
    202 
    203       cd src-tauri
    204       cargo tauri build --runner cargo-xwin --target x86_64-pc-windows-msvc --bundles nsis
    205 
    206       installer="$(find target/x86_64-pc-windows-msvc/release/bundle/nsis -maxdepth 1 -type f -name "*setup.exe" | head -n 1)"
    207       [ -n "$installer" ] || { echo "Windows installer was not produced" >&2; exit 1; }
    208       cp "$installer" "/out/iuna-v${IUNA_VERSION}-windows-x86_64-desktop-setup.exe"
    209       chown "${HOST_UID}:${HOST_GID}" "/out/iuna-v${IUNA_VERSION}-windows-x86_64-desktop-setup.exe"
    210     '
    211 }
    212 
    213 require_desktop_artifacts() {
    214   local version="$1"
    215   local macos_artifact="downloads/iuna-v${version}-macos-aarch64-desktop.app.zip"
    216   local windows_artifact="downloads/iuna-v${version}-windows-x86_64-desktop-setup.exe"
    217 
    218   [ -f "$macos_artifact" ] || die "missing ${macos_artifact}"
    219   [ -f "$windows_artifact" ] || die "missing ${windows_artifact}"
    220 }
    221 
    222 build_linux_cli_archives() {
    223   local version="$1"
    224   local tag="v${version}"
    225   local linux_x86_64_package="iuna-${tag}-linux-x86_64"
    226   local linux_aarch64_package="iuna-${tag}-linux-aarch64"
    227 
    228   mkdir -p .docker-build downloads
    229   [ -f "downloads/${linux_x86_64_package}.tar.gz" ] \
    230     && [ -f "downloads/${linux_aarch64_package}.tar.gz" ] \
    231     && [ -f .docker-build/iuna-node-linux-x86_64 ] \
    232     && return 0
    233 
    234   require_command docker
    235 
    236   docker run --rm --platform=linux/amd64 \
    237     -e "IUNA_VERSION=${version}" \
    238     -e "HOST_UID=$(id -u)" \
    239     -e "HOST_GID=$(id -g)" \
    240     -v "$(pwd):/src/iuna:ro" \
    241     -v "$(pwd)/downloads:/out" \
    242     -v "$(pwd)/.docker-build:/node-out" \
    243     rust:1.86-bookworm \
    244     bash -c '
    245       set -euo pipefail
    246 
    247       apt-get update
    248       apt-get install -y --no-install-recommends gcc-aarch64-linux-gnu libc6-dev-arm64-cross
    249       rm -rf /var/lib/apt/lists/*
    250       rustup target add aarch64-unknown-linux-gnu
    251 
    252       mkdir -p /work/iuna
    253       tar -C /src/iuna \
    254         --exclude=./target \
    255         --exclude=./src-tauri/target \
    256         --exclude=./src-tauri/binaries \
    257         --exclude=./.agents \
    258         --exclude=./.codex \
    259         --exclude=./.docker-build \
    260         -cf - . | tar -C /work/iuna -xf -
    261 
    262       cd /work/iuna
    263       CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc \
    264       AR_aarch64_unknown_linux_gnu=aarch64-linux-gnu-ar \
    265       CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc \
    266       cargo build --release --locked --target aarch64-unknown-linux-gnu
    267       cargo build --release --locked
    268 
    269       tag="v${IUNA_VERSION}"
    270       linux_x86_64_package="iuna-${tag}-linux-x86_64"
    271       linux_aarch64_package="iuna-${tag}-linux-aarch64"
    272       mkdir -p "/tmp/site/${linux_x86_64_package}" "/tmp/site/${linux_aarch64_package}"
    273       cp target/release/iuna "/tmp/site/${linux_x86_64_package}/"
    274       cp target/aarch64-unknown-linux-gnu/release/iuna "/tmp/site/${linux_aarch64_package}/"
    275       cp target/release/iuna /node-out/iuna-node-linux-x86_64
    276       cp README.md LICENSE "/tmp/site/${linux_x86_64_package}/"
    277       cp README.md LICENSE "/tmp/site/${linux_aarch64_package}/"
    278       tar -C /tmp/site -czf "/out/${linux_x86_64_package}.tar.gz" "${linux_x86_64_package}"
    279       tar -C /tmp/site -czf "/out/${linux_aarch64_package}.tar.gz" "${linux_aarch64_package}"
    280       chown "${HOST_UID}:${HOST_GID}" "/out/${linux_x86_64_package}.tar.gz" "/out/${linux_aarch64_package}.tar.gz" /node-out/iuna-node-linux-x86_64
    281     '
    282 }
    283 
    284 write_download_checksums() {
    285   (
    286     cd downloads
    287     rm -f SHA256SUMS
    288 
    289     local files=()
    290     local file
    291     for file in *; do
    292       [ -f "$file" ] || continue
    293       case "$file" in
    294         .gitkeep|index.html|SHA256SUMS) continue ;;
    295       esac
    296       files+=("$file")
    297     done
    298 
    299     [ "${#files[@]}" -gt 0 ] || return 0
    300     if command -v sha256sum >/dev/null 2>&1; then
    301       sha256sum "${files[@]}" > SHA256SUMS
    302     else
    303       for file in "${files[@]}"; do
    304         shasum -a 256 "$file" | awk "{print \$1 \"  \" \$2}"
    305       done > SHA256SUMS
    306     fi
    307   )
    308 }
    309 
    310 build_versions() {
    311   local version="$1"
    312 
    313   mkdir -p downloads
    314   build_linux_cli_archives "$version"
    315   build_macos_desktop_if_possible "$version"
    316   build_windows_desktop_if_possible "$version"
    317   build_windows_desktop_in_docker_if_possible "$version"
    318   require_desktop_artifacts "$version"
    319   write_download_checksums
    320 }
    321 
    322 build_docker_image() {
    323   local version="$1"
    324   local www_image="${IUNA_WWW_IMAGE:-iuna-www:v${version}}"
    325   local node_image="${IUNA_NODE_IMAGE:-iuna-node:v${version}}"
    326 
    327   require_command docker
    328 
    329   [ -f .docker-build/iuna-node-linux-x86_64 ] || die "missing .docker-build/iuna-node-linux-x86_64; run build_versions first"
    330 
    331   docker build --platform=linux/amd64 --progress=plain -t "$www_image" .
    332   docker build --platform=linux/amd64 --progress=plain -t "$node_image" -f Dockerfile.node .
    333   echo "Built Docker images: ${www_image}, ${node_image}"
    334 }
    335 
    336 import_image_to_k3s() {
    337   local image="$1"
    338   local tmp_folder="$2"
    339   local remote_host="${IUNA_DEPLOY_HOST:-root@jhx.app}"
    340   local remote_file="${image//[:\/]/_}.tar"
    341   local image_file="${tmp_folder}/${remote_file}"
    342 
    343   require_command docker
    344   require_command scp
    345   require_command ssh
    346 
    347   docker save "$image" -o "$image_file"
    348   scp "$image_file" "${remote_host}:~/"
    349   ssh "$remote_host" "sudo k3s ctr -n k8s.io images import ~/${remote_file} && rm ~/${remote_file}"
    350 }
    351 
    352 render_manifest() {
    353   local www_image="$1"
    354   local node_image="$2"
    355   local output="$3"
    356   local escaped_www_image
    357   local escaped_node_image
    358 
    359   escaped_www_image="$(escape_sed_replacement "$www_image")"
    360   escaped_node_image="$(escape_sed_replacement "$node_image")"
    361 
    362   sed \
    363     -e "s|\${IUNA_WWW_IMAGE}|${escaped_www_image}|g" \
    364     -e "s|\${IUNA_NODE_IMAGE}|${escaped_node_image}|g" \
    365     config/deployment.yml > "$output"
    366 }
    367 
    368 deploy_docker_image() {
    369   local version="$1"
    370   local www_image="${IUNA_WWW_IMAGE:-iuna-www:v${version}}"
    371   local node_image="${IUNA_NODE_IMAGE:-iuna-node:v${version}}"
    372   local kubectl_context="${IUNA_KUBECTL_CONTEXT:-jhx-app}"
    373   local tmp_folder
    374 
    375   require_command kubectl
    376 
    377   tmp_folder="$(mktemp -d)"
    378   trap 'rm -rf "$tmp_folder"' RETURN
    379 
    380   import_image_to_k3s "$www_image" "$tmp_folder"
    381   import_image_to_k3s "$node_image" "$tmp_folder"
    382   render_manifest "$www_image" "$node_image" "${tmp_folder}/deployment.yml"
    383 
    384   local current_www_selector
    385   current_www_selector="$(kubectl --context "$kubectl_context" -n iuna get deployment www -o jsonpath='{.spec.selector.matchLabels.app}' 2>/dev/null || true)"
    386   if [ -n "$current_www_selector" ] && [ "$current_www_selector" != "iuna-www" ]; then
    387     kubectl --context "$kubectl_context" -n iuna delete deployment www --wait=true
    388   fi
    389 
    390   kubectl --context "$kubectl_context" apply -f "${tmp_folder}/deployment.yml"
    391   kubectl --context "$kubectl_context" -n iuna rollout restart deployment/www deployment/node
    392   kubectl --context "$kubectl_context" -n iuna rollout status deployment/www
    393   kubectl --context "$kubectl_context" -n iuna rollout status deployment/node
    394 }
    395 
    396 main() {
    397   [ "$#" -eq 1 ] || { usage; exit 2; }
    398 
    399   local version="${1#v}"
    400   [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || die "version must look like 0.2.48"
    401 
    402   ensure_clean_worktree
    403 
    404   # Check if the tag already exists; if it does, only deploy
    405   if git rev-parse --verify "v${version}" >/dev/null 2>&1; then
    406     ensure_head_matches_tag "v${version}"
    407     echo "Tag v${version} already exists; rebuilding Docker images and deploying"
    408     if ! confirm "Are you sure you want to deploy v${version}? (y/N) "; then
    409       echo "Aborting deployment"
    410       exit 1
    411     fi
    412     build_linux_cli_archives "$version"
    413     build_docker_image "$version"
    414     deploy_docker_image "$version"
    415     exit 0
    416   fi
    417 
    418   update_versions "$version"
    419   build_versions "$version"
    420   commit_and_tag "$version"
    421   build_docker_image "$version"
    422   deploy_docker_image "$version"
    423 }
    424 
    425 main "$@"