release-kit 0.9.13

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
# Prove the bot credential is stored on the project, and overwrite it when a
# replacement is supplied: a rotated credential must be able to replace an
# old one. install-bot stores the token at creation, because the create
# response is the only time the forge shows its value; this step is the
# rotation path and the assertion. The value travels on standard input, and
# printf is a shell builtin, so no process argument list ever holds it.
#
# On gitlab.com a Free group's bot is a service account, and with no value
# exported this step mints a fresh token on that account and stores it,
# carrying the marker install-bot gives every token it mints. rk runs it so
# where the stored value is missing, or where the run names this step,
# which is the rotation request. A value minted elsewhere still arrives as
# RK_BOT_TOKEN.
set -eu
: "${RK_REPO:?rk sets this; run this script through rk setup}"

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

# The forge CLI may pretty-print JSON. Dropping the whitespace between
# tokens, and keeping the whitespace inside strings, makes every pattern
# below hold for the compact and the pretty form alike without making two
# different names equal.
compact() {
  awk '{ text = text $0 "\n" } END {
    out = ""; inside = 0; escaped = 0
    for (i = 1; i <= length(text); i++) {
      c = substr(text, i, 1)
      if (inside) {
        out = out c
        if (escaped) escaped = 0
        else if (c == "\\") escaped = 1
        else if (c == "\"") inside = 0
      } else if (c == "\"") { out = out c; inside = 1 }
      else if (c !~ /[ \t\r\n]/) out = out c
    }
    printf "%s", out
  }'
}
text_of() { grep -o "\"$1\":\"[^\"]*\"" | head -n 1 | cut -d '"' -f 4; }
number_of() { grep -o "\"$1\":[0-9]*" | head -n 1 | cut -d : -f 2; }

stored=0
if glab api "projects/$project/variables/RELEASE_BOT_TOKEN" >/dev/null 2>&1; then
  stored=1
fi
store() {
  if [ "$stored" = "1" ]; then
    printf '%s' "$1" | glab variable update RELEASE_BOT_TOKEN --masked --repo "$RK_REPO"
  else
    printf '%s' "$1" | glab variable set RELEASE_BOT_TOKEN --masked --repo "$RK_REPO"
  fi
}

if [ -n "${RK_BOT_TOKEN:-}" ]; then
  store "$RK_BOT_TOKEN"
  echo 'ok RELEASE_BOT_TOKEN overwritten from RK_BOT_TOKEN'
elif [ "${RK_GITLAB_COM:-false}" = "true" ]; then
  namespace="$(glab api "namespaces/$root" | compact)"
  plan="$(printf '%s' "$namespace" | text_of plan)"
  kind="$(printf '%s' "$namespace" | text_of kind)"
  group="$(printf '%s' "$namespace" | number_of id)"
  if [ "$plan" != "free" ] || [ "$kind" != "group" ]; then
    echo "FAIL the namespace $root reads plan ${plan:-unknown}, kind ${kind:-unknown}, whose bot is a project access token no step can re-read" >&2
    echo 'remediation: rk setup step install-bot --apply replaces the token, or export RK_BOT_TOKEN to rotate one' >&2
    exit 1
  fi
  project_id="$(glab api "projects/$project" | compact | number_of id)"
  name="${RK_BOT_NAME:-release-bot-$project_id}"
  # Matched literally: setup.bot.name holds to the username shape. A failed
  # listing is no absence, so it stops before any write.
  if ! accounts="$(glab api --paginate "groups/$group/service_accounts?per_page=100")"; then
    echo "FAIL the service accounts of $root could not be read" >&2
    echo "remediation: the caller needs the Owner role on the top-level group $root, then rerun" >&2
    exit 1
  fi
  account="$(printf '%s' "$accounts" | compact | tr '}' '\n' | grep -F "\"username\":\"$name\"" | number_of id)"
  if [ -z "$account" ]; then
    echo "FAIL no service account named $name exists in $root" >&2
    echo 'remediation: rk setup step install-bot --apply creates it' >&2
    exit 1
  fi
  expires="$(($(date -u +%Y) + 1))-$(date -u +%m)-01"
  if ! response="$(glab api -X POST "groups/$group/service_accounts/$account/personal_access_tokens" \
    -f "name=$name" \
    -f 'description=release-kit bot identity' \
    -F 'scopes=["api","write_repository"]' \
    -f "expires_at=$expires")"; then
    echo "FAIL a token could not be minted for the service account $name" >&2
    echo "remediation: the caller needs the Owner role on the top-level group $root, then rerun" >&2
    exit 1
  fi
  token="$(printf '%s' "$response" | compact | text_of token)"
  if [ -z "$token" ]; then
    echo 'FAIL the mint response carried no token value' >&2
    echo "remediation: check the caller is an Owner of $root, then rerun" >&2
    exit 1
  fi
  store "$token"
  echo "ok minted a new token on the service account $name and stored RELEASE_BOT_TOKEN"
elif [ "$stored" != "1" ]; then
  echo 'FAIL RELEASE_BOT_TOKEN is not stored on the project' >&2
  echo 'remediation: rk setup step install-bot --apply creates and stores it' >&2
  exit 1
fi

echo 'check: prints the variable key'
glab api "projects/$project/variables/RELEASE_BOT_TOKEN" | compact | grep -o '"key":"RELEASE_BOT_TOKEN"'