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 protections are exactly the ones this setup owns, with the
# shapes a release needs, and state what the forge actually enforces rather
# than passing on a claim it cannot meet: tag protection here stops accident,
# not authority, and a check that hides that difference is worse than none.
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}"
# 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_GITLAB_MERGE_METHOD:?rk sets this; run this script through rk setup}"
: "${RK_GITLAB_SQUASH_OPTION:?rk sets this; run this script through rk setup}"
# The access levels the recorded integration authority names: no push under
# forge integration, the narrowest level that admits one under local.
: "${RK_GITLAB_PUSH_LEVEL:?rk sets this; run this script through rk setup}"
: "${RK_GITLAB_MERGE_LEVEL:?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 whole pipeline is required wherever
# a forge gate lands to run in it.
: "${RK_TAG_PROTECTION:?rk sets this; run this script through rk setup}"
: "${RK_FORGE_GATE:?rk sets this; run this script through rk setup}"
case "$RK_FORGE_GATE" in
  true | false) ;;
  *) echo "RK_FORGE_GATE is $RK_FORGE_GATE; rk sets it to true or false" >&2; exit 1 ;;
esac

project="$(printf '%s' "$RK_REPO" | sed 's|/|%2F|g')"

# The forge CLI may pretty-print JSON; stripping whitespace makes every
# pattern below hold for the compact and the pretty form alike, and no
# value a pattern touches can carry whitespace of its own.
compact() { tr -d ' \t\r\n'; }
status=0

assert() {
  label="$1"
  pattern="$2"
  body="$3"
  remedy="$4"
  if printf '%s' "$body" | grep -q "$pattern"; then
    echo "ok $label"
  else
    echo "FAIL $label" >&2
    echo "remediation: $remedy" >&2
    status=1
  fi
}

trunk="$(glab api "projects/$project/protected_branches/$RK_TRUNK_BRANCH" 2>/dev/null | compact || true)"
assert "$RK_TRUNK_BRANCH is protected" "\"name\":\"$RK_TRUNK_BRANCH\"" "$trunk" \
  'rk setup step protect-trunk --apply'
assert "$RK_TRUNK_BRANCH refuses force pushes" '"allow_force_push":false' "$trunk" \
  'rk setup step protect-trunk --apply'
trunk_levels="$(printf '%s' "$trunk" | grep -o '"push_access_levels":\[[^]]*\]' || true)"
trunk_grants="$(printf '%s' "$trunk_levels" | grep -o '{' | grep -c . || true)"
if [ "$trunk_grants" = "1" ] && printf '%s' "$trunk_levels" | grep -q "\"access_level\":$RK_GITLAB_PUSH_LEVEL[,}]"; then
  echo "ok $RK_TRUNK_BRANCH takes a push at access level $RK_GITLAB_PUSH_LEVEL alone"
else
  echo "FAIL $RK_TRUNK_BRANCH push grants: the setup owns exactly one at access level $RK_GITLAB_PUSH_LEVEL, and the forge honors the most permissive of $trunk_grants" >&2
  echo 'remediation: rk setup step protect-trunk --apply' >&2
  status=1
fi
trunk_merge_levels="$(printf '%s' "$trunk" | grep -o '"merge_access_levels":\[[^]]*\]' || true)"
trunk_merge_grants="$(printf '%s' "$trunk_merge_levels" | grep -o '{' | grep -c . || true)"
if [ "$trunk_merge_grants" = "1" ] && printf '%s' "$trunk_merge_levels" | grep -q "\"access_level\":$RK_GITLAB_MERGE_LEVEL[,}]"; then
  echo "ok $RK_TRUNK_BRANCH merges only at access level $RK_GITLAB_MERGE_LEVEL"
else
  echo "FAIL $RK_TRUNK_BRANCH merge grants: the setup owns exactly one at access level $RK_GITLAB_MERGE_LEVEL, found $trunk_merge_grants" >&2
  echo 'remediation: rk setup step protect-trunk --apply' >&2
  status=1
fi

settings="$(glab api "projects/$project" | compact)"
# The pipeline requirement is judged in both directions: required exactly
# where a forge gate lands, and off where none does, because a requirement
# no pipeline can satisfy holds every merge request. A value that is
# missing or unreadable matches neither, and fails.
if [ "$RK_FORGE_GATE" = true ]; then
  pipeline_label='the gate requires the whole pipeline'
else
  pipeline_label='no pipeline is required: no forge gate lands, so none runs'
fi
assert "$pipeline_label" "\"only_allow_merge_if_pipeline_succeeds\":$RK_FORGE_GATE[,}]" "$settings" \
  'rk setup step protect-trunk --apply'
assert 'merges fast-forward' "\"merge_method\":\"$RK_GITLAB_MERGE_METHOD\"" "$settings" \
  'rk setup step protect-trunk --apply'
if [ "$RK_MERGE_FORM" = squash ]; then
  squash_label='every merge request squashes'
else
  squash_label='no merge request squashes, so every commit keeps its own message'
fi
assert "$squash_label" "\"squash_option\":\"$RK_GITLAB_SQUASH_OPTION\"" "$settings" \
  'rk setup step protect-trunk --apply'

if [ "$RK_TAG_PROTECTION" = true ]; then
  tags="$(glab api "projects/$project/protected_tags/v%2A" 2>/dev/null | compact || true)"
  assert 'v* is protected' '"name":"v\*"' "$tags" \
    'rk setup step protect-tags --apply'
  echo 'note: tag protection here stops git clients and non-privileged users; an Owner or Maintainer can still delete a protected tag through the UI or API'
else
  echo 'ok no tag protection is required: the release mode is none, so no release tag is minted'
fi

# The optional release-line protection: absent is fine, present is owned.
lines="$(glab api "projects/$project/protected_branches/release%2F%2A" 2>/dev/null | compact || true)"
if [ -n "$lines" ]; then
  assert 'release/* refuses force pushes' '"allow_force_push":false' "$lines" \
    'rk setup step protect-release-lines --apply'
fi

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