Skip to content
quarry Artifact [8f1c9b0d0e]
Login

Artifact [8f1c9b0d0e]

Artifact 8f1c9b0d0e57143b450499a5966d930582eef1616d2b2b6f2a0d19e4fd3a75bf:


#!/bin/sh
# release.sh [--dry-run] [--out DIR] X.Y.Z
#
# Cut quarry's release X.Y.Z from the checkout this runs in (RFC 0020
# §releases): build, package, sum, describe, publish. ci/release.yml runs
# it on the tag version-X.Y.Z; deploy.sh --release X.Y.Z installs what it
# publishes, so the layout below is the one deploy.sh reads (docs/running.md,
# "Releases and rollback").
#
#   1. refuse unless VERSION holds X.Y.Z, CHANGELOG.md has a "## X.Y.Z"
#      heading, the checkout carries version-X.Y.Z itself (the rule the tag
#      trigger and the version stamp use, scripts/version-stamp.sh), and no
#      tracked file is changed and no untracked one is present
#   2. build with scripts/build-linux.sh: linux-amd64 natively, linux-arm64
#      with a cross C toolchain, the five runner downloads with the first
#   3. package into OUT (default $TMPDIR/quarry-release-X.Y.Z):
#        quarry-X.Y.Z-linux-<amd64|arm64>.tar.gz  one directory,
#            quarry-X.Y.Z-linux-<arch>/, holding quarry, zit, CHANGELOG.md, LICENSE
#        quarry-runner-<os>-<arch>[.exe]          the runner downloads
#        quarry-X.Y.Z-src.tar.gz                  the tagged tree (zit tarball)
#        SHA256SUMS                               one line per file above
#        release-manifest.json                    zit's schema_version 1
#   4. verify SHA256SUMS against the files on disk
#   5. publish each file as releases/X.Y.Z/<file> with `zit uv push`, as
#      QUARRY_USER with QUARRY_TOKEN, the manifest last: a release whose
#      manifest is there is whole
#
# --dry-run does all of it but step 5 and leaves OUT in place.
#
# Re-running is safe. A release whose manifest the forge already holds is
# published and is left alone (exit 0, nothing built). Otherwise every file
# is pushed again: `zit uv push` skips a name the forge holds with the same
# hash, and a rebuilt file with other bytes replaces the half-published one,
# since its time is later.
#
# Environment:
#   QUARRY_USER, QUARRY_TOKEN  the run token the release job receives (RFC 0021
#                    §runs); the token goes to zit as ZIT_PASSWORD, never as an
#                    argument, which every user on the machine can read
#   QUARRY_TAG       the tag that started the run; when set, it must be version-X.Y.Z
#   RELEASE_URL      the repository to publish to (default https://zeitforge.app/r/quarry)
#   ARM64_CC, ARM64_CXX  the cross C toolchain for linux-arm64
#                    (default aarch64-linux-gnu-gcc, aarch64-linux-gnu-g++)
#   ZIT_RELEASE_DIR, GITHUB_TOKEN  how build-linux.sh gets the pinned zit
#   RELEASE_BUILD    a command run as `$RELEASE_BUILD GOARCH PREFIX` (VERSION
#                    exported) in place of build-linux.sh, for the tests
set -eu

usage="usage: release.sh [--dry-run] [--out DIR] X.Y.Z"
dry=; out=; ver=
while [ $# -gt 0 ]; do
  case "$1" in
    --dry-run) dry=1 ;;
    --out) out=${2:?$usage}; shift ;;
    -*) echo "$usage" >&2; exit 2 ;;
    *) [ -z "$ver" ] || { echo "$usage" >&2; exit 2; }; ver=$1 ;;
  esac
  shift
done
printf '%s\n' "$ver" | grep -Eqx '[0-9]+\.[0-9]+\.[0-9]+' || { echo "$usage" >&2; exit 2; }
tag=version-$ver
url=${RELEASE_URL:-https://zeitforge.app/r/quarry}
die() { echo "release: $*" >&2; exit 1; }

sha256() {
  if command -v sha256sum >/dev/null 2>&1; then sha256sum "$@"; else shasum -a 256 "$@"; fi
}
size() { wc -c <"$1" | tr -d ' '; }

# ---- 1. is this check-in release X.Y.Z, and clean ----------------------
[ -f VERSION ] && [ -f scripts/version-stamp.sh ] || die "run this at the root of a quarry checkout"
have=$(tr -d ' \t\r\n' <VERSION)
[ "$have" = "$ver" ] || die "VERSION holds $have, not $ver: bump it, land it, and tag that check-in"
if [ -n "${QUARRY_TAG:-}" ] && [ "$QUARRY_TAG" != "$tag" ]; then
  die "the run was started by the tag $QUARRY_TAG, not $tag"
fi
# The heading the Releases page shows as the notes: ## followed by the
# version as a word of its own (v and version- allowed), as the page reads it.
awk -v v="$ver" '
  /^## / { n = split(substr($0, 4), w, /[][ \t()`*_,:;]+/)
           for (i = 1; i <= n; i++) if (w[i] == v || w[i] == "v" v || w[i] == "version-" v) found = 1 }
  END { exit !found }' CHANGELOG.md || die "CHANGELOG.md has no \"## $ver\" heading; a release says what changed"
checkin=$(zit info current 2>/dev/null | head -1) || true
case "$checkin" in
  *[!0-9a-f]* | "") die "not a zit checkout: no check-in to release (the release runner checks out with --checkout zit)" ;;
esac
[ "${#checkin}" -eq 40 ] || [ "${#checkin}" -eq 64 ] || die "zit info current named $checkin, not a full hash"
# Which tags the check-in carries itself, read as build-linux.sh and
# deploy.sh read them; any read that fails refuses, never guesses.
raw=$(zit tag list --raw "$checkin") || die "could not read $checkin's tags"
parent=$(zit info "$checkin" | sed -n 's/^parent: *//p') || die "could not read $checkin's parent"
praw=
if [ -n "$parent" ]; then
  praw=$(zit tag list --raw "$parent") || die "could not read the parent's tags"
fi
plain=$(zit tag list "$checkin") || die "could not read $checkin's tags"
stamp=$(printf '%s\n--\n%s\n--\n%s\n' "$raw" "$praw" "$plain" | sh scripts/version-stamp.sh VERSION "$checkin" -)
[ "$stamp" = "$ver" ] || die "check-in $(printf '%.12s' "$checkin") does not carry $tag itself (it would be stamped $stamp)"
dirty=$(zit changes) || die "zit changes failed"
[ -z "$dirty" ] || die "the checkout has changes; a release is the tagged tree as it is:
$dirty"
extra=$(zit extras) || die "zit extras failed"
[ -z "$extra" ] || die "the checkout has untracked files; a release is the tagged tree as it is:
$extra"

# ---- already published? -------------------------------------------------
# Asked before building: a published release is never rebuilt over.
if [ -z "$dry" ]; then
  [ -n "${QUARRY_USER:-}" ] && [ -n "${QUARRY_TOKEN:-}" ] ||
    die "publishing needs QUARRY_USER and QUARRY_TOKEN, the run token a release job receives; --dry-run builds without them"
  held=$(ZIT_PASSWORD="$QUARRY_TOKEN" zit uv list "$url" --user "$QUARRY_USER") ||
    die "could not list $url's unversioned files as $QUARRY_USER (zit $(zit version | awk '{print $2}') on PATH; uv needs 0.3.36)"
  if printf '%s\n' "$held" | grep -qxF "releases/$ver/release-manifest.json"; then
    echo "release: $url already holds release $ver (its manifest is there); nothing published"
    exit 0
  fi
fi

# ---- 2. build ------------------------------------------------------------
out=${out:-${TMPDIR:-/tmp}/quarry-release-$ver}
work=$(mktemp -d "${TMPDIR:-/tmp}/quarry-release-build.XXXXXX")
trap 'rm -rf "$work"' EXIT
trap 'exit 130' INT TERM HUP
# The job's HOME is its checkout: Go's caches go to the build directory,
# so neither dirties the tree nor outlives the run (-modcacherw lets the
# trap remove the module cache).
export GOCACHE="${GOCACHE:-$work/gocache}" GOPATH="${GOPATH:-$work/gopath}"
export GOFLAGS="${GOFLAGS:+$GOFLAGS }-modcacherw"
export VERSION="$ver"
build() { # build GOARCH PREFIX
  if [ -n "${RELEASE_BUILD:-}" ]; then
    $RELEASE_BUILD "$1" "$2"
  elif [ "$1" = arm64 ]; then
    GOARCH=arm64 PREFIX=$2 CC=${ARM64_CC:-aarch64-linux-gnu-gcc} CXX=${ARM64_CXX:-aarch64-linux-gnu-g++} sh scripts/build-linux.sh
  else
    GOARCH=$1 PREFIX=$2 sh scripts/build-linux.sh
  fi
}
for arch in amd64 arm64; do
  echo "release: building quarry $ver for linux-$arch"
  build "$arch" "$work/$arch" || die "the linux-$arch build failed"
  for b in quarry zit; do
    [ -s "$work/$arch/$b" ] || die "the linux-$arch build made no $b"
  done
done
# The runner downloads are pure Go and the same from either build; the
# set is checked as deploy.sh checks one, against this tree's list.
sh scripts/runner-check.sh "$work/amd64/runner" scripts/build-linux.sh >"$work/runner-check" ||
  die "the runner downloads are not a complete set: $(cat "$work/runner-check")"
# The forge build that can run here must say it is this release, as
# deploy.sh --release will insist.
case "$(uname -s)-$(uname -m)" in
  Linux-x86_64 | Linux-amd64)
    got=$("$work/amd64/quarry" version) || die "the linux-amd64 quarry does not run"
    [ "$got" = "quarry $ver" ] || die "the linux-amd64 quarry says \"$got\", not quarry $ver" ;;
esac

# ---- 3. package ----------------------------------------------------------
rm -rf "$out"
mkdir -p "$out"
for arch in amd64 arm64; do
  top=quarry-$ver-linux-$arch
  mkdir -p "$work/pkg/$top"
  install -m 0755 "$work/$arch/quarry" "$work/$arch/zit" "$work/pkg/$top/"
  install -m 0644 CHANGELOG.md LICENSE "$work/pkg/$top/"
  # COPYFILE_DISABLE: macOS tar adds ._ files, which deploy.sh refuses.
  (cd "$work/pkg" && COPYFILE_DISABLE=1 tar -cf - "$top") | gzip -n >"$out/$top.tar.gz"
done
for f in "$work/amd64/runner"/quarry-runner-*; do
  install -m 0755 "$f" "$out/"
done
zit tarball "$checkin" "$out/quarry-$ver-src.tar.gz" --name "quarry-$ver-src" >/dev/null ||
  die "zit tarball of $checkin failed"
(cd "$out" && sha256 quarry-*) >"$work/SHA256SUMS"
mv "$work/SHA256SUMS" "$out/SHA256SUMS"

# ---- the manifest -------------------------------------------------------
sum_of() { awk -v f="$1" '$2 == f || $2 == "*" f {print $1}' "$out/SHA256SUMS"; }
artifact() { # artifact NAME TARGET QUALIFICATION
  printf '    {"name": "%s", "target": "%s", "bytes": %s, "sha256": "%s", "qualification": "%s"}' \
    "$1" "$2" "$(size "$out/$1")" "$(sum_of "$1")" "$3"
}
native="built natively on linux-amd64 by scripts/build-linux.sh; quarry version prints quarry $ver"
crossq="cross-built on linux-amd64 by scripts/build-linux.sh with a linux-arm64 C toolchain; not run before publishing"
case "$(uname -s)-$(uname -m)" in
  Linux-x86_64 | Linux-amd64) ;;
  *) native="built by scripts/build-linux.sh; not run before publishing" ;;
esac
runnerq="cross-compiled pure Go (CGO_ENABLED=0) by scripts/build-linux.sh; the set is complete (scripts/runner-check.sh)"
src=quarry-$ver-src.tar.gz
{
  echo '{'
  echo '  "schema_version": 1,'
  echo '  "release": {'
  printf '    "version": "%s",\n    "date": "%s",\n    "fossil_check_in": "%s",\n    "fossil_tag": "%s"\n' \
    "$ver" "$(date -u +%Y-%m-%d)" "$checkin" "$tag"
  echo '  },'
  printf '  "source": {"name": "%s", "target": "source", "bytes": %s, "sha256": "%s", "fossil_check_in": "%s", "fossil_tag": "%s", "qualification": "%s"},\n' \
    "$src" "$(size "$out/$src")" "$(sum_of "$src")" "$checkin" "$tag" "zit tarball of the tagged check-in, from which every artifact here was built"
  echo '  "artifacts": ['
  artifact "quarry-$ver-linux-amd64.tar.gz" linux-amd64 "$native"
  echo ','
  artifact "quarry-$ver-linux-arm64.tar.gz" linux-arm64 "$crossq"
  for f in "$out"/quarry-runner-*; do
    n=${f##*/}
    t=${n#quarry-runner-}
    echo ','
    artifact "$n" "runner-${t%.exe}" "$runnerq"
  done
  echo
  echo '  ],'
  echo '  "omitted_targets": ['
  for t in darwin-arm64 darwin-amd64 windows-amd64; do
    [ "$t" = darwin-arm64 ] || echo ','
    printf '    {"target": "%s", "reason": "no forge build: the DuckDB bindings do not cross-link from Linux, and the forge is built on Linux (scripts/build-linux.sh); its runner download is published"}' "$t"
  done
  echo
  echo '  ],'
  echo '  "qualification": {'
  printf '    "linux-amd64": ["%s"],\n' "$native"
  printf '    "linux-arm64": ["%s"],\n' "$crossq"
  printf '    "runner": ["%s"]\n' "$runnerq"
  echo '  }'
  echo '}'
} >"$out/release-manifest.json"

# ---- 4. verify -----------------------------------------------------------
(cd "$out" && sha256 -c SHA256SUMS) >/dev/null 2>&1 || die "$out does not match its SHA256SUMS"
for f in "$out"/*; do
  n=${f##*/}
  case "$n" in SHA256SUMS | release-manifest.json) continue ;; esac
  [ -n "$(sum_of "$n")" ] || die "SHA256SUMS has no line for $n"
done

if [ -n "$dry" ]; then
  echo "release: dry run, nothing published; release $ver is in $out"
  exit 0
fi

# ---- 5. publish ----------------------------------------------------------
push() {
  echo "release: publishing releases/$ver/$1"
  ZIT_PASSWORD="$QUARRY_TOKEN" zit uv push "$url" "$out/$1" --as "releases/$ver/$1" --user "$QUARRY_USER" ||
    die "zit uv push of $1 failed; re-run to finish: what is already there is skipped"
}
for f in "$out"/quarry-*; do
  push "${f##*/}"
done
push SHA256SUMS
push release-manifest.json
echo "release: published $ver to $url/releases ($(printf '%.12s' "$checkin"))"