release-kit 0.2.9

A canonical release workflow: a technology-agnostic method, per-technology bindings, and the rk CLI that lands and serves them.
Documentation
#!/usr/bin/env sh
# Verify the rulesets are exactly the ones this setup owns — two always,
# three when the optional release-line protection is applied — and that the
# trunk protection still has the shape a release merge needs: a ruleset that
# rejects or corrupts that merge is invisible until the release merge, and a
# ruleset no step owns is protection nothing here can reproduce or explain.
set -eu
: "${RK_REPO:?rk sets this; run this script through rk setup}"
: "${RK_TRUNK_BRANCH:?rk sets this; run this script through rk setup}"

trunk_name="$RK_TRUNK_BRANCH-protection"
tags_name=release-tags
lines_name=release-lines

names="$(gh api "repos/$RK_REPO/rulesets" -q '.[].name' | sort)"
extra="$names"
status=0

for pair in "$trunk_name:protect-trunk" "$tags_name:protect-tags"; do
  name="${pair%%:*}"
  extra="$(printf '%s\n' "$extra" | grep -vxF "$name" || true)"
  if printf '%s\n' "$names" | grep -qxF "$name"; then
    echo "ok $name"
    continue
  fi
  echo "FAIL missing ruleset: $name" >&2
  echo "remediation: rk setup step ${pair#*:} --apply" >&2
  status=1
done

# The optional release-line protection: absent is fine, present is owned.
extra="$(printf '%s\n' "$extra" | grep -vxF "$lines_name" || true)"
if printf '%s\n' "$names" | grep -qxF "$lines_name"; then
  echo "ok $lines_name"
fi

if [ -n "$extra" ]; then
  echo 'FAIL rulesets no step owns:' >&2
  printf '%s\n' "$extra" >&2
  echo 'remediation: delete each one in the repository settings' >&2
  status=1
fi

check() {
  id="$1"
  label="$2"
  expected="$3"
  remedy="$4"
  actual="$(gh api "repos/$RK_REPO/rulesets/$id" -q "$5")"
  if [ "$actual" = "$expected" ]; then
    echo "ok $label"
  else
    echo "FAIL $label: expected $expected, got $actual" >&2
    echo "remediation: $remedy" >&2
    status=1
  fi
}

id="$(gh api "repos/$RK_REPO/rulesets" -q ".[] | select(.name == \"$trunk_name\") | .id" | head -n 1)"
if [ -n "$id" ]; then
  rerun='rk setup step protect-trunk --apply'

  check "$id" "$trunk_name names no bypass actor" 0 "$rerun" '.bypass_actors // [] | length'
  check "$id" "$trunk_name requires a pull request" true "$rerun" '[.rules[].type] | contains(["pull_request"])'
  check "$id" "$trunk_name requires a status check" true "$rerun" '[.rules[].type] | contains(["required_status_checks"])'
  check "$id" "$trunk_name allows only the squash merge" '["squash"]' \
    'a merge commit or a rebase would take the trunk off its one linear history; '"$rerun" \
    '[.rules[] | select(.type=="pull_request") | .parameters.allowed_merge_methods] | first // [] | sort | tojson'
  if [ -n "${RK_REQUIRED_CHECK:-}" ]; then
    check "$id" "$trunk_name requires the named check" "$RK_REQUIRED_CHECK" "$rerun" \
      '[.rules[] | select(.type=="required_status_checks") | .parameters.required_status_checks[].context] | join(",")'
  fi
  check "$id" "$trunk_name asks for no approving review" 0 "$rerun" \
    '[.rules[] | select(.type=="pull_request") | .parameters.required_approving_review_count] | first'
fi

lid="$(gh api "repos/$RK_REPO/rulesets" -q ".[] | select(.name == \"$lines_name\") | .id" | head -n 1)"
if [ -n "$lid" ]; then
  check "$lid" "$lines_name pins the lines" true 'rk setup step protect-release-lines --apply' \
    '[.rules[].type] | contains(["deletion","non_fast_forward"])'
fi

[ "$status" -eq 0 ] || exit 1
echo 'ok protections-check'