Commit 87d8a2c7 authored by xuwang's avatar xuwang
Browse files

feat(renovate): optional dedicated bot identity (create/list/revoke/ci-set)



Add renovate-bot.sh + targets to provision and manage an optional Renovate bot
token instead of running scans/CI as a personal PAT:
- renovate-bot-create: group token (RENOVATE_BOT_GROUP, for the multi-repo scan)
  or project token (per-repo CI); stored at RENOVATE_BOT_TOKEN_FILE.
- renovate-bot-list / renovate-bot-revoke: lifecycle (confirm-guarded revoke).
- renovate-bot-ci-set: set the masked RENOVATE_TOKEN CI/CD variable from the token.
Created with the consumer admin token on the repo's own server (mirrors
agent-bots.sh). Docs added.

Changelog: added
Co-Authored-By: default avatarClaude Opus 4.8 <noreply@anthropic.com>
parent 6986c242
Loading
Loading
Loading
Loading
+43 −0
Original line number Diff line number Diff line
@@ -210,6 +210,49 @@ OTICA's `make renovate-admin-update` writes the explicit `repositories` list; pi
one of the rows above for `onboarding`/`requireConfig` depending on whether you
want central control (Centralized) or explicit per-repo opt-in (Per-repo opt-in).

## Dedicated bot identity (optional, `renovate-admin.mk`)

By default scans/CI authenticate as a person's PAT. For unattended use, provision
a dedicated **Renovate bot** (a GitLab access token whose bot user owns the MRs).
Created with your admin token (`GITLAB_TOKEN` / `~/.gitlab-token`, Owner/Maintainer
+ `api`) on the repo's own server.

| Target | What it does |
| --- | --- |
| `renovate-bot-create` | Create the bot token — a **group** token if `RENOVATE_BOT_GROUP` is set (one identity for the whole multi-repo scan), else a **project** token (one repo's CI). Stored at `RENOVATE_BOT_TOKEN_FILE`. |
| `renovate-bot-list` | List `renovate-bot` tokens on the target group/project. |
| `renovate-bot-revoke` | Revoke the token and remove the local file (`confirm.sh`-guarded). |
| `renovate-bot-ci-set` | Set the masked `RENOVATE_TOKEN` CI/CD variable on a project (`REPO=ns/proj`) from the stored token. |

| Variable | Default | Purpose |
| --- | --- | --- |
| `RENOVATE_BOT_GROUP` | _(empty)_ | Group full path → create a group token (else project) |
| `RENOVATE_BOT_REPO` | origin | Project for project-token / `ci-set` |
| `RENOVATE_BOT_TOKEN_FILE` | `~/.otica-tokens/renovate.token` | Where the token is stored |
| `RENOVATE_BOT_ACCESS_LEVEL` | `30` (Developer) | `40` (Maintainer) if the bot must automerge protected branches |
| `RENOVATE_BOT_EXPIRES` | +365 days | Token expiry (`YYYY-MM-DD`); re-run create to rotate |

**Use it for the scan** — point Renovate at the bot token (it outranks the ambient
`GITLAB_TOKEN`):

```bash
# one group bot that can see every repo in the group
make renovate-bot-create RENOVATE_BOT_GROUP=mygroup
make renovate-admin-run RENOVATE_TOKEN_FILE=~/.otica-tokens/renovate.token
# (or set RENOVATE_TOKEN_FILE in env.mk so it's the default)
```

**Use it for CI/CD** — create a project bot and push it into the project's variable
the `renovate-ci.yml` template reads:

```bash
make renovate-bot-create        # project token (origin)
make renovate-bot-ci-set        # sets masked RENOVATE_TOKEN on the project
```

Rotate by re-running `renovate-bot-create` before `RENOVATE_BOT_EXPIRES`; tear down
with `renovate-bot-revoke`.

## Scheduled runs in GitLab CI (`.gitlab/renovate-ci.yml`)

For unattended updates, include the template and schedule a pipeline:
+27 −1
Original line number Diff line number Diff line
@@ -33,12 +33,38 @@ endif

RENOVATE_AUTODISCOVER_FILTER ?=
RENOVATE_ADMIN_CONFIG        ?= renovate-admin.json
export RENOVATE_AUTODISCOVER_FILTER RENOVATE_ADMIN_CONFIG
# Optional dedicated Renovate bot identity (access token). Group token if
# RENOVATE_BOT_GROUP is set (multi-repo scan), else a project token (per-repo CI).
RENOVATE_BOT_GROUP           ?=
RENOVATE_BOT_REPO            ?=
RENOVATE_BOT_TOKEN_FILE      ?= ${HOME}/.otica-tokens/renovate.token
RENOVATE_BOT_ACCESS_LEVEL    ?=
RENOVATE_BOT_EXPIRES         ?=
export RENOVATE_AUTODISCOVER_FILTER RENOVATE_ADMIN_CONFIG \
       RENOVATE_BOT_GROUP RENOVATE_BOT_REPO RENOVATE_BOT_TOKEN_FILE \
       RENOVATE_BOT_ACCESS_LEVEL RENOVATE_BOT_EXPIRES

.PHONY: renovate-admin-update
renovate-admin-update: ## regenerate ${RENOVATE_ADMIN_CONFIG} repos from ${SUB_REPOS_FILE} (tf/docker only)
	@renovate-admin-update.sh

# ── Optional Renovate bot identity ──────────────────────────────────────────────
.PHONY: renovate-bot-create
renovate-bot-create: ## create a dedicated Renovate bot token (group if RENOVATE_BOT_GROUP set, else project)
	@renovate-bot.sh create

.PHONY: renovate-bot-list
renovate-bot-list: ## list renovate-bot tokens on the target group/project
	@renovate-bot.sh list

.PHONY: renovate-bot-revoke
renovate-bot-revoke: ## revoke the renovate-bot token and remove the local file
	@if confirm.sh "Revoke the renovate-bot token?"; then renovate-bot.sh revoke ; fi

.PHONY: renovate-bot-ci-set
renovate-bot-ci-set: ## set the masked RENOVATE_TOKEN CI/CD variable from the bot token (REPO=ns/proj)
	@renovate-bot.sh ci-set ${REPO}

.PHONY: renovate-admin-dryrun
renovate-admin-dryrun: renovate-check ## autodiscover dry-run across accessible repos (read_api)
	@renovate-run.sh autodiscover-dryrun
+120 −0
Original line number Diff line number Diff line
#!/bin/bash -e
###############################################################################
# renovate-bot.sh — create / manage an OPTIONAL dedicated GitLab bot identity
# (access token) for Renovate, so scans and CI/CD don't run as a person's PAT.
#
# Two scopes:
#   - group  (RENOVATE_BOT_GROUP set): a group access token whose bot user can
#            see every project in the group — use for the multi-repo admin scan
#            (renovate-admin-*). Requires the scanned repos share that group.
#   - project (default): a project access token for one repo — use for that
#            repo's CI/CD (the RENOVATE_TOKEN CI variable) or a single-repo run.
#
# The bot is CREATED with your admin token (GITLAB_TOKEN / ~/.gitlab-token) on the
# repo's own server (derived from the git remote). That token must be
# Owner/Maintainer with `api` scope, and the instance must allow access tokens.
#
# Usage:
#   renovate-bot.sh create            # create the bot token, store it locally
#   renovate-bot.sh list              # list renovate-bot tokens on the target
#   renovate-bot.sh revoke            # revoke it + remove the local file
#   renovate-bot.sh ci-set [repo]     # set masked RENOVATE_TOKEN CI var on a
#                                      # project from the stored bot token
#
# Tunables:
#   RENOVATE_BOT_GROUP        group full path -> create a GROUP token (else project)
#   RENOVATE_BOT_REPO         project path for project/ci-set (default: origin)
#   RENOVATE_BOT_TOKEN_FILE   where to store the token (default ~/.otica-tokens/renovate.token)
#   RENOVATE_BOT_ACCESS_LEVEL 30=Developer (default) | 40=Maintainer (for automerge)
#   RENOVATE_BOT_EXPIRES      YYYY-MM-DD (default +365 days)
###############################################################################
THIS_DIR=$(cd "$(dirname "$0")" && pwd)
source "${THIS_DIR}/functions.sh"

# Target the repo's own GitLab server (not the OTICA hosting server).
_remote_url() { git config --get remote.origin.url 2>/dev/null || true; }
_host="$(_remote_url | sed -E 's#^[a-z]+://##; s#^[^@/]*@##; s#[:/].*$##')"
[ -n "${_host}" ] && export GITLAB_SERVER="https://${_host}"
GITLAB_TOKEN="${GITLAB_TOKEN:-}"                 # predeclare for set -u in gitlab.sh
source "${THIS_DIR}/gitlab.sh"                   # GITLAB_API, get_project_id, token

NAME="renovate-bot"
LEVEL="${RENOVATE_BOT_ACCESS_LEVEL:-30}"
EXPIRES="${RENOVATE_BOT_EXPIRES:-$(date -v+365d +%F 2>/dev/null || date -d '+365 days' +%F)}"
TOKEN_FILE="${RENOVATE_BOT_TOKEN_FILE:-${HOME}/.otica-tokens/renovate.token}"
hdr=(--header "PRIVATE-TOKEN: ${GITLAB_TOKEN}")

default_repo() {
    local url; url="$(_remote_url)"; url="${url%.git}"
    case "$url" in *://*) echo "${url#*://*/}";; *:*) echo "${url#*:}";; *) echo "$url";; esac
}

# Resolve the access-tokens API base for the chosen scope.
if [ -n "${RENOVATE_BOT_GROUP:-}" ]; then
    KIND="group"; TARGET="${RENOVATE_BOT_GROUP}"
    api="${GITLAB_API}/groups/$(urlencode "${TARGET}")/access_tokens"
else
    KIND="project"; TARGET="${RENOVATE_BOT_REPO:-$(default_repo)}"
    api="${GITLAB_API}/projects/$(get_project_id "${TARGET}")/access_tokens"
fi

cmd_create() {
    mkdir -p "$(dirname "${TOKEN_FILE}")"; chmod 700 "$(dirname "${TOKEN_FILE}")" 2>/dev/null || true
    local resp tok user
    resp=$(curl -s "${hdr[@]}" -X POST "${api}" \
        --data "name=${NAME}" \
        --data "scopes[]=api" --data "scopes[]=write_repository" \
        --data "access_level=${LEVEL}" --data "expires_at=${EXPIRES}")
    tok=$(echo "${resp}" | jq -r '.token // empty')
    [ -n "${tok}" ] || err "create failed on ${KIND} ${TARGET}: $(echo "${resp}" | jq -r '.message // .error // .')"
    printf '%s' "${tok}" > "${TOKEN_FILE}"; chmod 600 "${TOKEN_FILE}"
    user=$(curl -s --header "PRIVATE-TOKEN: ${tok}" "${GITLAB_API}/user" | jq -r '.username // "?"')
    echo "Created ${NAME} on ${KIND} ${TARGET}"
    echo "  user:    ${user}   access:${LEVEL}   expires:${EXPIRES}"
    echo "  stored:  ${TOKEN_FILE}"
    echo "  scan:    point Renovate at it -> RENOVATE_TOKEN_FILE=${TOKEN_FILE}"
    echo "  ci/cd:   renovate-bot.sh ci-set   (sets the RENOVATE_TOKEN CI variable)"
}

cmd_list() {
    echo "${NAME} tokens on ${KIND} ${TARGET}:"
    curl -s "${hdr[@]}" "${api}" \
        | jq -r --arg n "${NAME}" '.[]? | select(.name==$n) | "  id=\(.id) access=\(.access_level) active=\(.active) expires=\(.expires_at)"'
}

cmd_revoke() {
    local id
    id=$(curl -s "${hdr[@]}" "${api}" | jq -r --arg n "${NAME}" '.[]? | select(.name==$n and .active==true) | .id' | head -1)
    [ -n "${id}" ] && curl -s "${hdr[@]}" -X DELETE "${api}/${id}" -o /dev/null -w "  revoked ${NAME} (id=${id}): HTTP %{http_code}\n" \
        || echo "  (no active ${NAME} token on ${KIND} ${TARGET})"
    [ -f "${TOKEN_FILE}" ] && { rm -f "${TOKEN_FILE}"; echo "  removed ${TOKEN_FILE}"; } || true
}

# Set the masked RENOVATE_TOKEN CI/CD variable on a project from the stored token.
cmd_ci_set() {
    local repo="${1:-${RENOVATE_BOT_REPO:-$(default_repo)}}" pid tok vapi code
    [ -f "${TOKEN_FILE}" ] || err "No bot token at ${TOKEN_FILE} — run 'renovate-bot.sh create' first."
    tok="$(cat "${TOKEN_FILE}")"
    pid="$(get_project_id "${repo}")"
    vapi="${GITLAB_API}/projects/${pid}/variables"
    # Upsert: create, else update.
    code=$(curl -s "${hdr[@]}" -X POST "${vapi}" \
        --data "key=RENOVATE_TOKEN" --data-urlencode "value=${tok}" \
        --data "masked=true" --data "protected=false" -o /dev/null -w '%{http_code}')
    if [ "${code}" = "400" ]; then
        curl -s "${hdr[@]}" -X PUT "${vapi}/RENOVATE_TOKEN" \
            --data-urlencode "value=${tok}" --data "masked=true" --data "protected=false" -o /dev/null
    fi
    echo "Set masked RENOVATE_TOKEN CI/CD variable on ${repo}."
}

cmd="${1:-help}"; shift || true
case "${cmd}" in
    create)  cmd_create "$@";;
    list)    cmd_list "$@";;
    revoke)  cmd_revoke "$@";;
    ci-set)  cmd_ci_set "$@";;
    *) echo "usage: renovate-bot.sh <create | list | revoke | ci-set [repo]>";
       echo "  group token:   RENOVATE_BOT_GROUP=<group/path> renovate-bot.sh create";
       echo "  project token: renovate-bot.sh create   (uses origin or RENOVATE_BOT_REPO)";;
esac