jevhooks.git / tools / release.sh
1#!/usr/bin/env bash

OWNER: make a release of the plugin, and publish it.

tools/release.sh build      the files, into dist/<version>/ (needs no key and no account)
tools/release.sh sign       the signed manifest and the signed list of releases
tools/release.sh publish    a GitHub release holding all of it
tools/release.sh refresh    sign the list of releases again and replace it on the latest release

A release is the plugin folder with the daemon already built in it, once for each Linux architecture, so that installing needs no Rust:

jevhooks-<version>-x86_64-linux.tar.gz
jevhooks-<version>-aarch64-linux.tar.gz
SHA256SUMS                  for someone who downloads by hand
packslip.sigstore.json      the signed manifest: each file, its platform and its digest
packslip.json               the signed list of releases, which mise looks for first

The daemon is linked statically against musl (cargo-zigbuild), so one file runs on any Linux of its architecture. The version is the workspace's, in Cargo.toml; the tag is v<version>.

Signing needs JEVHOOKS_PACKSLIP_KEY, the secret key packslip keygen wrote (tools/release.pub is its public half). It is signed with --no-log: nothing is sent to the public transparency log, which is why whoever installs must say allow_unlogged = true. The repository is private on GitHub and its release files reach everyone through code.lmjtfy.fun, which is the name signed.

25set -euo pipefail
26cd "$(dirname "$0")/.."
28PROJECT="code.lmjtfy.fun/jevhooks"
29REPO="deizel/jevhooks"

A year: the list must be signed again before this runs out (refresh), or installs stop.

31VALID_FOR="52w"
32ARCHES="x86_64 aarch64"
34version="$(sed -n '/^\[workspace.package\]/,/^\[/s/^version = "\(.*\)"/\1/p' Cargo.toml)"
35[ -n "$version" ] || { echo "release: no version in Cargo.toml's [workspace.package]" >&2; exit 1; }
36tag="v$version"
37out="dist/$version"
38base="https://code.lmjtfy.fun/jevhooks/releases"
39
40files() { for arch in $ARCHES; do echo "$out/jevhooks-$version-$arch-linux.tar.gz"; done; }
41
42build() {
43  # A release is of a commit: what is in the archive is what `git archive` gives, plus the daemon.
44  if ! git diff --quiet HEAD; then
45    echo "release: the tree has uncommitted changes; a release is made from a commit" >&2
46    exit 1
47  fi
48  rm -rf "$out"
49  mkdir -p "$out"
50  local targets=()
51  for arch in $ARCHES; do targets+=(--target "$arch-unknown-linux-musl"); done
52  cargo zigbuild --release --locked -p jevhooks-daemon "${targets[@]}"
53  # The commit's time on every file, so the same commit makes the same bytes.
54  local when
55  when="$(git log -1 --format=%cI)"
56  for arch in $ARCHES; do
57    local stage
58    stage="$(mktemp -d)"
59    git archive --format=tar --prefix=jevhooks/ HEAD:plugin | tar -x -C "$stage"
60    mkdir -p "$stage/jevhooks/bin"
61    install -m 0755 "target/$arch-unknown-linux-musl/release/jevhooks" "$stage/jevhooks/bin/jevhooks"
62    tar --sort=name --mtime="$when" --owner=0 --group=0 --numeric-owner -C "$stage" -cf - jevhooks \
63      | gzip -n -9 > "$out/jevhooks-$version-$arch-linux.tar.gz"
64    rm -rf "$stage"
65  done
66  (cd "$out" && sha256sum jevhooks-*.tar.gz > SHA256SUMS)
67  echo "release: built $out"
68  ls -l "$out"
69}

Runs packslip with the secret key in a file only it can read, removed afterwards.

72signed() {
73  [ -n "${JEVHOOKS_PACKSLIP_KEY:-}" ] || { echo "release: JEVHOOKS_PACKSLIP_KEY is not set (fnox.toml declares it)" >&2; exit 1; }
74  local key status=0
75  key="$(umask 077 && mktemp)"
76  printf '%s\n' "$JEVHOOKS_PACKSLIP_KEY" > "$key"
77  packslip "$@" --key "$key" --no-log || status=$?
78  rm -f "$key"
79  return "$status"
80}

The list of every release, this one the latest. Each earlier release's manifest is fetched from its GitHub release, because the list carries every manifest's digest.

84list() {
85  local releases=(--release "$base/$tag/packslip.sigstore.json=$out/packslip.sigstore.json")
86  local earlier
87  for earlier in $(gh release list -R "$REPO" --json tagName -q '.[].tagName'); do
88    [ "$earlier" = "$tag" ] && continue
89    mkdir -p "dist/earlier/$earlier"
90    gh release download "$earlier" -R "$REPO" -p packslip.sigstore.json -D "dist/earlier/$earlier" --clobber
91    releases+=(--release "$base/$earlier/packslip.sigstore.json=dist/earlier/$earlier/packslip.sigstore.json")
92  done
93  # The time as the sequence: it only has to rise, and then no counter is kept anywhere.
94  signed releases --project "$PROJECT" --sequence "$(date +%s)" --valid-for "$VALID_FOR" --latest "$version" \
95    "${releases[@]}" --out "$out/packslip.json"
96}
98sign() {
99  local file
100  for file in $(files); do [ -f "$file" ] || { echo "release: $file is not built (tools/release.sh build)" >&2; exit 1; }; done
101  # shellcheck disable=SC2046
102  signed create --project "$PROJECT" --version "$version" --url-base "$base/$tag" \
103    --bin jevhooks=jevhooks/bin/jevhooks --out "$out" $(files)
104  packslip verify "$out/packslip.sigstore.json" --pubkey tools/release.pub --allow-unlogged --artifact "$(files | head -1)"
105  list
106  echo "release: signed $out"
107}
108
109publish() {
110  local file
111  for file in $(files) "$out/SHA256SUMS" "$out/packslip.sigstore.json" "$out/packslip.json"; do
112    [ -f "$file" ] || { echo "release: $file is missing (build, then sign)" >&2; exit 1; }
113  done
114  # shellcheck disable=SC2046
115  gh release create "$tag" -R "$REPO" --title "jevhooks $version" \
116    --notes "The plugin with the daemon built in, for Linux on x86_64 and arm64. How to install: https://hooks.lmjtfy.fun/" \
117    $(files) "$out/SHA256SUMS" "$out/packslip.sigstore.json" "$out/packslip.json"
118}
119
120refresh() {
121  latest="$(gh release view -R "$REPO" --json tagName -q .tagName)"
122  [ "$latest" = "$tag" ] || { echo "release: the latest release is $latest and this tree is $tag; refresh from the latest release's commit" >&2; exit 1; }
123  mkdir -p "$out"
124  gh release download "$tag" -R "$REPO" -p packslip.sigstore.json -D "$out" --clobber
125  list
126  gh release upload "$tag" -R "$REPO" "$out/packslip.json" --clobber
127}
128
129case "${1:-}" in
130  build) build ;;
131  sign) sign ;;
132  publish) publish ;;
133  refresh) refresh ;;
134  *) sed -n '2,7p' "$0" >&2; exit 2 ;;
135esac