iuna

iuna

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

deployment.sh (13645B)


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