release-kit 0.8.3

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
# Protect the trunk. Which rules that means is the recorded integration
# mode's answer, and rk composes them into RK_TRUNK_RULES from the one
# owned-rules key its floor table judges and its check reads. Under forge
# integration the trunk takes no direct push and no force-push: a pull
# request carrying the named passing check and the pr-title check is the
# only way in. Under local integration the request and required-check
# rules are absent, because no forge can require a check before the push
# that starts it, and deletion and force-push protection stand alone. The request must carry the trunk's tip: the release request
# has computed contents, and nothing else re-reads the trunk between that
# computation and the merge. Squash is the only merge method, so the trunk history
# stays linear on a forge that offers no fast-forward merge. The squash
# title is the pull request's title — never a lone branch commit's subject —
# because the bot derives the version from the trunk's commit messages.
# Nothing in the pipeline writes this branch — the bot pushes a tag, which
# release-tags governs — so it names no bypass actor. The method's
# invariants chapter owns these facts; this comment points there. Rerunning
# updates in place.
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_TRUNK_RULESET:?rk sets this; run this script through rk setup}"
: "${RK_TITLE_CHECK:?rk sets this; run this script through rk setup}"
# The policy below. rk judged every value against the floor table before
# this ran, so this script substitutes and never decides.
: "${RK_TRUNK_RULES:?rk sets this; run this script through rk setup}"
: "${RK_SQUASH_TITLE_SOURCE:?rk sets this; run this script through rk setup}"
: "${RK_SQUASH_BODY_SOURCE:?rk sets this; run this script through rk setup}"

name="$RK_TRUNK_RULESET"
id="$(gh api "repos/$RK_REPO/rulesets" -q ".[] | select(.name == \"$name\") | .id" | head -n 1)"

if [ -n "$id" ]; then
  gh api -X PUT "repos/$RK_REPO/rulesets/$id" --input - >/dev/null
else
  gh api -X POST "repos/$RK_REPO/rulesets" --input - >/dev/null
fi <<JSON
{
  "name": "$name",
  "target": "branch",
  "enforcement": "active",
  "bypass_actors": [],
  "conditions": {
    "ref_name": { "include": ["refs/heads/$RK_TRUNK_BRANCH"], "exclude": [] }
  },
  "rules": $RK_TRUNK_RULES
}
JSON

# The squash title source is a repository setting, not a ruleset rule: on a
# one-commit request the forge otherwise offers that commit's own subject,
# so a branch commit named wip could become the trunk's message.
gh api -X PATCH "repos/$RK_REPO" \
  -f "squash_merge_commit_title=$RK_SQUASH_TITLE_SOURCE" -f "squash_merge_commit_message=$RK_SQUASH_BODY_SOURCE" >/dev/null

echo "check: prints $name and the squash message sources"
gh api "repos/$RK_REPO/rulesets" -q ".[] | select(.name == \"$name\") | .name"
gh api "repos/$RK_REPO" -q .squash_merge_commit_title
gh api "repos/$RK_REPO" -q .squash_merge_commit_message