release-kit 0.9.10

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 — the two trunk
# rulesets always, the tag ruleset wherever a release mints tags, and the
# optional release-line protection when it 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}"
: "${RK_BYPASS_ACTORS:?rk sets this; run this script through rk setup}"
: "${RK_TRUNK_RULESET:?rk sets this; run this script through rk setup}"
: "${RK_SAFETY_RULESET:?rk sets this; run this script through rk setup}"
: "${RK_TITLE_CHECK:?rk sets this; run this script through rk setup}"
# The merge settings of the recorded merge form, which rk derived from the
# configuration and the record before this ran.
: "${RK_MERGE_FORM:?rk sets this; run this script through rk setup}"
: "${RK_MERGE_METHODS:?rk sets this; run this script through rk setup}"
: "${RK_SAFETY_RULE_TYPES:?rk sets this; run this script through rk setup}"
# The two requirements the profile decides: the release tags are protected
# wherever a release mints them, and the trunk requires a status check
# wherever a forge gate lands to report one.
: "${RK_TAG_PROTECTION:?rk sets this; run this script through rk setup}"
: "${RK_FORGE_GATE:?rk sets this; run this script through rk setup}"

trunk_name="$RK_TRUNK_RULESET"
safety_name="$RK_SAFETY_RULESET"
tags_name=release-tags
lines_name=release-lines

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

set -- "$trunk_name:protect-trunk" "$safety_name:protect-trunk"
if [ "$RK_TAG_PROTECTION" = true ]; then
  set -- "$@" "$tags_name:protect-tags"
fi
for pair in "$@"; 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

# A release-less target requires no tag ruleset: absent is fine, and one
# that stands is still this setup's name rather than a stranger.
if [ "$RK_TAG_PROTECTION" != true ]; then
  extra="$(printf '%s\n' "$extra" | grep -vxF "$tags_name" || true)"
fi

# 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'

  # The three fields the policy states, projected before the comparison: a
  # field the forge adds later is not a drifted bypass.
  check "$id" "$trunk_name names the recorded bypass actors" "$RK_BYPASS_ACTORS" "$rerun" \
    '[.bypass_actors // [] | .[] | {actor_id, actor_type, bypass_mode}]
     | sort_by(.actor_type, .actor_id, .bypass_mode) | tojson'
  check "$id" "$trunk_name requires a pull request" true "$rerun" '[.rules[].type] | contains(["pull_request"])'
  if [ "$RK_FORGE_GATE" = true ]; then
    check "$id" "$trunk_name requires a status check" true "$rerun" '[.rules[].type] | contains(["required_status_checks"])'
  else
    # No forge gate lands, so no workflow reports a check, and a required
    # one would hold every request open.
    check "$id" "$trunk_name requires no status check" false "$rerun" '[.rules[].type] | contains(["required_status_checks"])'
  fi
  check "$id" "$trunk_name allows only the $RK_MERGE_FORM merge" "$(printf '%s' "$RK_MERGE_METHODS" | tr -d ' ')" \
    'any other merge method would take the trunk off its one linear history in the recorded merge form; '"$rerun" \
    '[.rules[] | select(.type=="pull_request") | .parameters.allowed_merge_methods] | first // [] | sort | tojson'
  if [ "$RK_FORGE_GATE" = true ] && [ -n "${RK_REQUIRED_CHECK:-}" ]; then
    # Both contexts the setup owns, sorted: the release check and the forge
    # gate — the title check under squash, the commit gate under rebase —
    # are installed together, and an extra stale context does not fail a
    # merge, it hangs one.
    owned="$(printf '%s\n%s\n' "$RK_REQUIRED_CHECK" "$RK_TITLE_CHECK" | sort | tr '\n' ',' | sed 's/,$//')"
    check "$id" "$trunk_name requires the checks the setup owns" "$owned" "$rerun" \
      '[.rules[] | select(.type=="required_status_checks") | .parameters.required_status_checks[].context] | sort | 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

# The safety ruleset is the one nobody is excused from. A bypass actor here
# would hand whoever it names the deletion and the force-push along with the
# push, which is the whole reason these two rules do not sit beside a
# bypassed rule.
sid="$(gh api "repos/$RK_REPO/rulesets" -q ".[] | select(.name == \"$safety_name\") | .id" | head -n 1)"
if [ -n "$sid" ]; then
  rerun='rk setup step protect-trunk --apply'
  check "$sid" "$safety_name names no bypass actor" 0 "$rerun" '.bypass_actors // [] | length'
  # The rule types the setup owns there: deletion and force-push, and the
  # linear-history rule under the rebase form.
  check "$sid" "$safety_name refuses deletion and force-push" "$RK_SAFETY_RULE_TYPES" "$rerun" \
    '[.rules[].type] | sort | tojson'
  check "$sid" "$safety_name covers the trunk alone" "[\"refs/heads/$RK_TRUNK_BRANCH\"]" "$rerun" \
    '.conditions.ref_name.include | tojson'
fi

# Under the rebase form the repository's own switch must allow the one
# method the ruleset allows, or no request can merge. A read that fails or
# answers anything but true fails.
if [ "$RK_MERGE_FORM" = rebase ]; then
  allowed="$(gh api "repos/$RK_REPO" -q .allow_rebase_merge 2>/dev/null || echo unreadable)"
  if [ "$allowed" = true ]; then
    echo 'ok the repository allows rebase merges'
  else
    echo "FAIL the repository allows rebase merges: allow_rebase_merge reads ${allowed:-nothing}" >&2
    echo 'remediation: rk setup step protect-trunk --apply' >&2
    status=1
  fi
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'