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%%/*}"
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; }
flag_of() { grep -o "\"$1\":[a-z]*" | head -n 1 | cut -d : -f 2; }
number_of_each() { while IFS= read -r line; do printf '%s' "$line" | number_of "$1"; done; }
where() { grep -F "\"$1\":$2" || true; }
marker='release-kit bot identity'
scopes='scopes=["api","write_repository"]'
expires="$(($(date -u +%Y) + 1))-$(date -u +%m)-01"
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
}
token_of() { compact | text_of token; }
path=token
if [ "${RK_GITLAB_COM:-false}" = "true" ]; then
if ! namespace="$(glab api "namespaces/$root")"; then
echo "FAIL the namespace $root could not be read" >&2
echo 'remediation: check that glab is authenticated against gitlab.com with access to the namespace, then rerun' >&2
exit 1
fi
namespace="$(printf '%s' "$namespace" | compact)"
plan="$(printf '%s' "$namespace" | text_of plan)"
kind="$(printf '%s' "$namespace" | text_of kind)"
trial="$(printf '%s' "$namespace" | flag_of trial)"
group="$(printf '%s' "$namespace" | number_of id)"
echo "note: the namespace $root reads plan ${plan:-unknown}, kind ${kind:-unknown}, trial ${trial:-false}"
case "$plan" in
premium* | ultimate* | silver* | gold* | opensource) path=token ;;
free)
case "$kind" in
group) path=service-account ;;
user)
echo "FAIL the personal namespace $root has no bot identity form on GitLab.com Free" >&2
echo 'remediation: move the project under a group, whose Owner can create a service account, or use a Premium or Ultimate tier, which allows a project access token' >&2
exit 1
;;
*)
echo "FAIL the namespace $root reads kind ${kind:-unknown}, which names no identity form" >&2
echo 'remediation: check that glab reads the namespace the project lives in, then rerun' >&2
exit 1
;;
esac
;;
*)
echo "FAIL the namespace $root reads plan ${plan:-unknown}, which names no identity form" >&2
echo 'remediation: check that glab is authenticated against gitlab.com and reads the namespace, then rerun' >&2
exit 1
;;
esac
else
echo 'note: a self-managed or Dedicated instance takes a project access token with any license'
fi
if [ "$path" = "token" ]; then
name="${RK_BOT_NAME:-release-bot}"
if ! tokens="$(glab api --paginate "projects/$project/access_tokens?state=active&per_page=100")"; then
echo 'FAIL the access tokens could not be listed' >&2
echo 'remediation: check the caller has Maintainer access or above, then rerun' >&2
exit 1
fi
tokens="$(printf '%s' "$tokens" | compact | tr '}' '\n' | where active true)"
active="$(printf '%s\n' "$tokens" | where name "\"$name\"" | number_of id)"
other="$( { printf '%s\n' "$tokens" | where description "\"$marker\""; printf '%s\n' "$tokens" | where name '"release-bot"'; } | grep -F -v "\"name\":\"$name\"" | text_of name)"
if [ -n "$other" ]; then
echo "FAIL the release-kit bot $other serves this project, and setup.bot.name asks for $name" >&2
echo "remediation: revoke $other by hand before the rename, or set setup.bot.name back to $other; a rename would strand the first identity" >&2
exit 1
fi
if [ -n "$active" ] && [ "$stored" = "1" ]; then
echo "ok an active token named $name exists and its variable is stored"
else
if [ -n "$active" ]; then
glab api -X DELETE "projects/$project/access_tokens/$active" >/dev/null
echo "note: revoked an active $name token whose stored variable was missing"
fi
if ! response="$(glab api -X POST "projects/$project/access_tokens" \
-f "name=$name" \
-f "description=$marker" \
-F "$scopes" \
-F access_level=40 \
-f "expires_at=$expires")"; then
echo "FAIL the project access token $name could not be created" >&2
echo 'remediation: the caller needs the Maintainer role on the project; on GitLab.com the namespace needs Premium or Ultimate' >&2
exit 1
fi
token="$(printf '%s' "$response" | token_of)"
if [ -z "$token" ]; then
echo 'FAIL the create response carried no token value' >&2
echo 'remediation: check the caller has Maintainer access or above, then rerun' >&2
exit 1
fi
store "$token"
echo "ok created $name and stored RELEASE_BOT_TOKEN"
fi
echo "check: prints one active token named $name and the stored variable"
glab api --paginate "projects/$project/access_tokens?state=active&per_page=100" | compact | tr '}' '\n' | where name "\"$name\"" | grep -o '"active":true' | head -n 1
glab api "projects/$project/variables/RELEASE_BOT_TOKEN" | compact | grep -o '"key":"RELEASE_BOT_TOKEN"'
exit 0
fi
project_id="$(glab api "projects/$project" | compact | number_of id)"
if [ -z "$project_id" ] || [ -z "$group" ]; then
echo 'FAIL the project id or the group id could not be read' >&2
echo 'remediation: check that glab reads the project and its top-level group, then rerun' >&2
exit 1
fi
default="release-bot-$project_id"
name="${RK_BOT_NAME:-$default}"
if ! accounts="$(glab api --paginate "groups/$group/service_accounts?per_page=100")"; then
echo "FAIL the service accounts of $root could not be listed" >&2
echo "remediation: the caller needs the Owner role on the top-level group $root, then rerun" >&2
exit 1
fi
accounts="$(printf '%s' "$accounts" | compact | tr '}' '\n' | where username '')"
account_of() { printf '%s\n' "$accounts" | where username "\"$1\"" | number_of id; }
level_of() { glab api "projects/$project/members/all/$1" 2>/dev/null | compact | number_of access_level; }
marked() {
if ! listing="$(glab api --paginate "groups/$group/service_accounts/$1/personal_access_tokens?state=active&per_page=100")"; then
echo unreadable
return 0
fi
printf '%s' "$listing" | compact | tr '}' '\n' | where active true | where description "\"$marker\"" | head -n 1
}
if ! members="$(glab api --paginate "projects/$project/members/all?per_page=100")"; then
echo 'FAIL the members of this project could not be read, so the live bot is unknown' >&2
echo 'remediation: check that glab reads the project members, then rerun; nothing was written' >&2
exit 1
fi
members="$(printf '%s' "$members" | compact | tr '}' '\n' | number_of_each id)"
live_bot() {
printf '%s\n' "$accounts" | while IFS= read -r line; do
id="$(printf '%s' "$line" | number_of id)"
user="$(printf '%s' "$line" | text_of username)"
[ -n "$id" ] || continue
printf '%s\n' "$members" | grep -qx "$id" || continue
if [ "$user" = "$default" ]; then
printf '%s\n' "$user"
continue
fi
mark="$(marked "$id")"
if [ "$mark" = "unreadable" ]; then
printf 'unreadable %s\n' "$user"
elif [ -n "$mark" ]; then
printf '%s\n' "$user"
fi
done
}
mint() {
if ! response="$(glab api -X POST "groups/$group/service_accounts/$1/personal_access_tokens" \
-f "name=$name" \
-f "description=$marker" \
-F "$scopes" \
-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" | token_of)"
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"
}
account="$(account_of "$name")"
bots="$(live_bot)"
unreadable="$(printf '%s\n' "$bots" | grep '^unreadable ' | head -n 1 | cut -d ' ' -f 2)"
if [ -n "$unreadable" ]; then
echo "FAIL the tokens of the service account $unreadable could not be read, so the live bot is unknown" >&2
echo "remediation: check the caller is an Owner of $root, then rerun; nothing was written" >&2
exit 1
fi
previous="$(printf '%s\n' "$bots" | grep -F -v -x "$name" | head -n 1)"
if [ -n "$previous" ]; then
echo "FAIL the release-kit bot $previous serves this project, and setup.bot.name asks for $name" >&2
echo "remediation: remove $previous from the project by hand before the rename, or set setup.bot.name back to $previous; a rename would strand the first identity" >&2
exit 1
fi
if [ -z "$account" ]; then
if ! created="$(glab api -X POST "groups/$group/service_accounts" \
-f "name=$name" \
-f "username=$name")"; then
echo "FAIL the service account $name could not be created in $root" >&2
echo "remediation: creating a service account needs the Owner role on the top-level group $root, whose Owner must have verified their identity on a trial; a Free group holds up to 100" >&2
exit 1
fi
account="$(printf '%s' "$created" | compact | number_of id)"
if [ -z "$account" ]; then
echo 'FAIL the create response carried no account id' >&2
echo "remediation: check the caller is an Owner of $root, then rerun" >&2
exit 1
fi
glab api -X POST "projects/$project/members" -F "user_id=$account" -F access_level=40 >/dev/null
mint "$account"
echo "ok created the service account $name, granted it this project at Maintainer, and stored RELEASE_BOT_TOKEN"
else
level="$(level_of "$account")"
if [ -z "$level" ]; then
echo "FAIL the service account $name exists in $root and is not a member of this project" >&2
echo "remediation: grant it this project at Maintainer by hand if it is this project's own account from an interrupted run, or set another setup.bot.name; granting it here could make one identity reach two projects" >&2
exit 1
fi
if [ "$level" -lt 40 ]; then
echo "FAIL the service account $name is a member of this project below Maintainer (level $level)" >&2
echo 'remediation: raise its role on the project to Maintainer by hand, then rerun' >&2
exit 1
fi
if [ "$stored" = "1" ]; then
echo "ok a service account named $name is a Maintainer of this project and its variable is stored"
else
mint "$account"
echo "ok rotated: minted a new token on the service account $name and stored RELEASE_BOT_TOKEN"
fi
fi
echo "check: prints the account's Maintainer level and the stored variable"
glab api "projects/$project/members/all/$account" | compact | grep -o '"access_level":[0-9]*'
glab api "projects/$project/variables/RELEASE_BOT_TOKEN" | compact | grep -o '"key":"RELEASE_BOT_TOKEN"'