ops/forge/package-relup.sh

58e6347eeb72 · 7 KB

#!/bin/sh
# Package one two-way relup transition: two production release tarballs plus
# the generated relup, digest-addressed and ready for RelupDeployment.
#
# Usage:
#   ops/forge/package-relup.sh --from-version 0.2.0 --to-version 0.3.0 \
#     [--from-rev <sha|ref>] [--to-rev <sha|ref>] \
#     [--from-state 2] [--to-state 2] [--out-dir DIR]
#
# Builds each revision in an isolated temporary worktree, so the working
# checkout is untouched. The steps mirror ops/relup-proof/run.sh, which is the
# proven recipe; this script parameterizes it for real version pairs. Whether
# a specific pair installs is still decided by the packaged appup and
# `release_handler` on the target nodes at check_install time.

set -eu

script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
repo_root=$(CDPATH= cd -- "$script_dir/../.." && pwd)

usage() {
  echo "usage: $0 --from-version V --to-version V [--from-rev REF] [--to-rev REF] [--from-state N] [--to-state N] [--out-dir DIR]" >&2
  exit 2
}

from_version=""
to_version=""
from_rev="HEAD"
to_rev="HEAD"
from_state="2"
to_state="2"
out_dir=""

while [ $# -gt 0 ]; do
  case "$1" in
    --from-version) from_version="$2"; shift 2 ;;
    --to-version) to_version="$2"; shift 2 ;;
    --from-rev) from_rev="$2"; shift 2 ;;
    --to-rev) to_rev="$2"; shift 2 ;;
    --from-state) from_state="$2"; shift 2 ;;
    --to-state) to_state="$2"; shift 2 ;;
    --out-dir) out_dir="$2"; shift 2 ;;
    *) usage ;;
  esac
done

[ -n "$from_version" ] || usage
[ -n "$to_version" ] || usage

version_ok() {
  echo "$1" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.-]+)?$'
}

state_ok() {
  echo "$1" | grep -Eq '^[12]$'
}

version_ok "$from_version" || { echo "from-version must be X.Y.Z" >&2; exit 2; }
version_ok "$to_version" || { echo "to-version must be X.Y.Z" >&2; exit 2; }
state_ok "$from_state" || { echo "from-state must be 1 or 2" >&2; exit 2; }
state_ok "$to_state" || { echo "to-state must be 1 or 2" >&2; exit 2; }

if [ "$from_version" = "$to_version" ]; then
  echo "from-version and to-version must differ" >&2
  exit 2
fi

if [ "$to_state" -lt "$from_state" ]; then
  echo "to-state must not regress below from-state" >&2
  exit 2
fi

from_sha=$(git -C "$repo_root" rev-parse --verify "${from_rev}^{commit}")
to_sha=$(git -C "$repo_root" rev-parse --verify "${to_rev}^{commit}")
target_system=$(erl -noshell -eval 'io:format("~s", [erlang:system_info(system_architecture)]), halt().' 2>/dev/null)

[ -n "$target_system" ] || { echo "could not determine the release target system" >&2; exit 1; }

build_root=$(mktemp -d "${TMPDIR:-/tmp}/openagents-relup-package.XXXXXX")
assets_digested=0

cleanup() {
  git -C "$repo_root" worktree remove --force "$build_root/from" >/dev/null 2>&1 || true
  git -C "$repo_root" worktree remove --force "$build_root/to" >/dev/null 2>&1 || true

  if [ "$assets_digested" = "1" ]; then
    (git -C "$repo_root" worktree list --porcelain >/dev/null 2>&1) || true
  fi

  rm -rf "$build_root"
}

trap cleanup EXIT INT TERM

git -C "$repo_root" worktree add --detach "$build_root/from" "$from_sha" >/dev/null
git -C "$repo_root" worktree add --detach "$build_root/to" "$to_sha" >/dev/null

echo "Fetching dependencies (from tree)"
(cd "$build_root/from" && MIX_ENV=prod mix deps.get >/dev/null)
(cd "$build_root/from" && npm install --prefix assets --no-audit --no-fund >/dev/null 2>&1)

echo "Fetching dependencies (to tree)"
(cd "$build_root/to" && MIX_ENV=prod mix deps.get >/dev/null)
(cd "$build_root/to" && npm install --prefix assets --no-audit --no-fund >/dev/null 2>&1)

echo "Building production assets (from tree)"
(cd "$build_root/from" && MIX_ENV=prod mix assets.deploy)
assets_digested=1

echo "Building production assets (to tree)"
(cd "$build_root/to" && MIX_ENV=prod mix assets.deploy)

echo "Building $from_version release"
env -u RELUP_FROM -u RELUP_TO -u OPENAGENTS_RELUP_PATH \
  MIX_ENV=prod \
  OPENAGENTS_RELEASE_PATH="$build_root/from/release" \
  OPENAGENTS_RELEASE_VSN="$from_version" \
  OPENAGENTS_RELUP_STATE_VERSION="$from_state" \
  OPENAGENTS_BUILD_REVISION="$from_sha" \
  sh -c 'cd "$1" && mix do compile --force --warnings-as-errors + release --overwrite' sh "$build_root/from"

# The appup derives its instruction list from these two module sets, so every
# module that differs between the revisions is covered. Both sides must be
# _build ebin directories: protocol consolidation happens later, in `mix
# release`, and comparing a release ebin against a _build ebin would report
# modules as added or deleted that neither revision touched.
from_ebin="$build_root/from/_build/prod/lib/openagents/ebin"
[ -d "$from_ebin" ] || { echo "missing from-build ebin: $from_ebin" >&2; exit 1; }

echo "Building $to_version release resource"
env -u OPENAGENTS_RELUP_PATH \
  MIX_ENV=prod \
  OPENAGENTS_RELEASE_PATH="$build_root/to/release" \
  OPENAGENTS_RELEASE_VSN="$to_version" \
  OPENAGENTS_RELUP_STATE_VERSION="$to_state" \
  OPENAGENTS_BUILD_REVISION="$to_sha" \
  RELUP_FROM="$from_version" \
  RELUP_TO="$to_version" \
  RELUP_FROM_EBIN="$from_ebin" \
  RELUP_FROM_STATE="$from_state" \
  RELUP_TO_STATE="$to_state" \
  sh -c 'cd "$1" && mix do compile --force --warnings-as-errors + release --overwrite' sh "$build_root/to"

to_ebin="$build_root/to/_build/prod/lib/openagents/ebin"
[ -d "$to_ebin" ] || { echo "missing to-build ebin: $to_ebin" >&2; exit 1; }

echo "Generating the two-way relup"
(cd "$build_root/to" && MIX_ENV=prod mix openagents.relup \
  --target "$build_root/to/release/releases/$to_version/openagents" \
  --from "$build_root/from/release/releases/$from_version/openagents" \
  --outdir "$build_root" \
  --from-ebin "$from_ebin" \
  --to-ebin "$to_ebin" \
  --from-state "$from_state" \
  --to-state "$to_state")

echo "Reassembling $to_version with the embedded relup"
env \
  MIX_ENV=prod \
  OPENAGENTS_RELEASE_PATH="$build_root/to/release" \
  OPENAGENTS_RELEASE_VSN="$to_version" \
  OPENAGENTS_RELUP_PATH="$build_root/relup" \
  OPENAGENTS_RELUP_STATE_VERSION="$to_state" \
  OPENAGENTS_BUILD_REVISION="$to_sha" \
  RELUP_FROM="$from_version" \
  RELUP_TO="$to_version" \
  RELUP_FROM_EBIN="$from_ebin" \
  RELUP_FROM_STATE="$from_state" \
  RELUP_TO_STATE="$to_state" \
  sh -c 'cd "$1" && mix release --overwrite' sh "$build_root/to"

tar -tzf "$build_root/to/release/openagents-$to_version.tar.gz" |
  grep -Fxq "releases/$to_version/relup"

publish_root="$out_dir"
if [ -z "$publish_root" ]; then
  publish_root="$repo_root/artifacts/relup/$from_version-to-$to_version"
fi

from_digest=$(sha256sum "$build_root/from/release/openagents-$from_version.tar.gz" | cut -d ' ' -f 1)
to_digest=$(sha256sum "$build_root/to/release/openagents-$to_version.tar.gz" | cut -d ' ' -f 1)
relup_digest=$(sha256sum "$build_root/relup" | cut -d ' ' -f 1)

mkdir -p "$publish_root"
cp "$build_root/from/release/openagents-$from_version.tar.gz" "$publish_root/"
cp "$build_root/to/release/openagents-$to_version.tar.gz" "$publish_root/"
cp "$build_root/relup" "$publish_root/"

cat >"$publish_root/package.json" <<EOF
{
  "schema": "openagents.relup-package.v1",
  "release_name": "openagents",
  "from_revision": "$from_sha",
  "to_revision": "$to_sha",
  "from_version": "$from_version",
  "to_version": "$to_version",
  "from_state_version": $from_state,
  "to_state_version": $to_state,
  "target_system": "$target_system",
  "from_artifact_digest": "$from_digest",
  "to_artifact_digest": "$to_digest",
  "relup_digest": "$relup_digest"
}
EOF

echo "Relup package ready: $publish_root"
echo "  openagents-$from_version.tar.gz  sha256:$from_digest"
echo "  openagents-$to_version.tar.gz    sha256:$to_digest"
echo "  relup                            sha256:$relup_digest"