Commit 7883a91f authored by xuwang's avatar xuwang
Browse files

feat(renovate): cross-group service-account bot (sa-create + add-groups)



Support sub-repos spanning multiple top-level groups, where a single group token
can't reach them all. Add:
- renovate-bot-sa-create: mint a GitLab service account + PAT via the admin-only
  /service_accounts API (needs RENOVATE_BOT_ADMIN_TOKEN_FILE).
- renovate-bot-add-groups: add the bot user (RENOVATE_BOT_USER) as a member of
  each group in the scan set (RENOVATE_BOT_GROUPS, else derived from
  renovate-admin.json) — runnable by a group owner, no instance admin needed.
Docs updated with the cross-group flow.

Changelog: added
Co-Authored-By: default avatarClaude Opus 4.8 <noreply@anthropic.com>
parent 87d8a2c7
Loading
Loading
Loading
Loading
+28 −0
Original line number Diff line number Diff line
@@ -253,6 +253,34 @@ 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`.

### Cross-group bot (sub-repos in several top-level groups)

A group/project access token only covers **one** group, and Renovate uses **one
token per run** — so if your scan set spans multiple top-level groups (e.g.
`gcp/*`, `irt-dcs/*`, `irt-db/*`, with no common parent) a single group token
won't reach them all. Use **one bot user (service account)** added to each group:

| Target | Who runs it | What it does |
| --- | --- | --- |
| `renovate-bot-sa-create` | an **instance admin** | Creates a GitLab service account + a PAT (api, write_repository) via the admin-only `/service_accounts` API; stores the PAT at `RENOVATE_BOT_TOKEN_FILE`. Needs `RENOVATE_BOT_ADMIN_TOKEN_FILE`. |
| `renovate-bot-add-groups` | a **group Owner** (you) | Adds `RENOVATE_BOT_USER` as a member (Developer) of every group in the scan set — taken from `RENOVATE_BOT_GROUPS`, else the distinct top-level groups in `renovate-admin.json`. |

```bash
# 1. admin (once): mint the service account + token
make renovate-bot-sa-create RENOVATE_BOT_ADMIN_TOKEN_FILE=~/.gitlab-admin-token RENOVATE_BOT_USER=renovate-bot

# 2. you (group owner): grant it the groups it must scan
make renovate-bot-add-groups RENOVATE_BOT_USER=<sa-username>     # groups inferred from renovate-admin.json

# 3. point scans at the bot
make renovate-admin-run RENOVATE_TOKEN_FILE=~/.otica-tokens/renovate.token
```

If you can't create a service account (the API is admin-only — it returns 404 to
non-admins), the equivalent is any **dedicated user** added to those groups; just
store its PAT at `RENOVATE_BOT_TOKEN_FILE`. Either way, one identity that's a
member of all the groups is what lets a single Renovate run see every sub-repo.

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

For unattended updates, include the template and schedule a pipeline:
+15 −1
Original line number Diff line number Diff line
@@ -40,9 +40,14 @@ RENOVATE_BOT_REPO ?=
RENOVATE_BOT_TOKEN_FILE      ?= ${HOME}/.otica-tokens/renovate.token
RENOVATE_BOT_ACCESS_LEVEL    ?=
RENOVATE_BOT_EXPIRES         ?=
# Cross-group service-account bot (sub-repos spanning several top-level groups).
RENOVATE_BOT_USER            ?=
RENOVATE_BOT_GROUPS          ?=
RENOVATE_BOT_ADMIN_TOKEN_FILE ?=
export RENOVATE_AUTODISCOVER_FILTER RENOVATE_ADMIN_CONFIG \
       RENOVATE_BOT_GROUP RENOVATE_BOT_REPO RENOVATE_BOT_TOKEN_FILE \
       RENOVATE_BOT_ACCESS_LEVEL RENOVATE_BOT_EXPIRES
       RENOVATE_BOT_ACCESS_LEVEL RENOVATE_BOT_EXPIRES \
       RENOVATE_BOT_USER RENOVATE_BOT_GROUPS RENOVATE_BOT_ADMIN_TOKEN_FILE

.PHONY: renovate-admin-update
renovate-admin-update: ## regenerate ${RENOVATE_ADMIN_CONFIG} repos from ${SUB_REPOS_FILE} (tf/docker only)
@@ -65,6 +70,15 @@ renovate-bot-revoke: ## revoke the renovate-bot token and remove the local file
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}

# Cross-group: one bot user across several top-level groups.
.PHONY: renovate-bot-sa-create
renovate-bot-sa-create: ## create a service-account bot + token (needs RENOVATE_BOT_ADMIN_TOKEN_FILE, instance admin)
	@renovate-bot.sh sa-create

.PHONY: renovate-bot-add-groups
renovate-bot-add-groups: ## add RENOVATE_BOT_USER to each group it must scan (group owner; groups from admin config)
	@renovate-bot.sh add-groups

.PHONY: renovate-admin-dryrun
renovate-admin-dryrun: renovate-check ## autodiscover dry-run across accessible repos (read_api)
	@renovate-run.sh autodiscover-dryrun
+72 −7
Original line number Diff line number Diff line
@@ -108,13 +108,78 @@ cmd_ci_set() {
    echo "Set masked RENOVATE_TOKEN CI/CD variable on ${repo}."
}

# ── Cross-group service-account bot ─────────────────────────────────────────────
# When sub-repos span multiple top-level groups, a single group/project token
# can't cover them. Use one bot USER (service account) added to each group.

# Create an instance service account + a PAT for it. Needs an INSTANCE-ADMIN token
# (RENOVATE_BOT_ADMIN_TOKEN[_FILE]) — the service-accounts API is admin-only.
cmd_sa_create() {
    local atok ahdr resp sid suser tok
    atok="${RENOVATE_BOT_ADMIN_TOKEN:-}"
    [ -z "${atok}" ] && [ -n "${RENOVATE_BOT_ADMIN_TOKEN_FILE:-}" ] && [ -f "${RENOVATE_BOT_ADMIN_TOKEN_FILE}" ] \
        && atok="$(cat "${RENOVATE_BOT_ADMIN_TOKEN_FILE}")"
    [ -n "${atok}" ] || err "sa-create needs an INSTANCE-ADMIN token: set RENOVATE_BOT_ADMIN_TOKEN or RENOVATE_BOT_ADMIN_TOKEN_FILE (api scope)."
    ahdr=(--header "PRIVATE-TOKEN: ${atok}")
    resp=$(curl -s "${ahdr[@]}" -X POST "${GITLAB_API}/service_accounts" \
        --data-urlencode "name=${RENOVATE_BOT_SA_NAME:-Renovate Bot}" \
        --data "username=${RENOVATE_BOT_USER:-renovate-bot}")
    sid=$(echo "${resp}" | jq -r '.id // empty')
    suser=$(echo "${resp}" | jq -r '.username // empty')
    [ -n "${sid}" ] || err "service-account create failed (needs instance admin): $(echo "${resp}" | jq -r '.message // .error // .')"
    mkdir -p "$(dirname "${TOKEN_FILE}")"; chmod 700 "$(dirname "${TOKEN_FILE}")" 2>/dev/null || true
    resp=$(curl -s "${ahdr[@]}" -X POST "${GITLAB_API}/service_accounts/${sid}/personal_access_tokens" \
        --data "name=renovate-bot" --data "scopes[]=api" --data "scopes[]=write_repository" --data "expires_at=${EXPIRES}")
    tok=$(echo "${resp}" | jq -r '.token // empty')
    [ -n "${tok}" ] || err "SA token create failed: $(echo "${resp}" | jq -r '.message // .error // .')"
    printf '%s' "${tok}" > "${TOKEN_FILE}"; chmod 600 "${TOKEN_FILE}"
    echo "Created service account: ${suser} (id=${sid})"
    echo "  token stored: ${TOKEN_FILE}  (expires ${EXPIRES})"
    echo "  next: renovate-bot.sh add-groups   RENOVATE_BOT_USER=${suser}"
    echo "        then point scans at RENOVATE_TOKEN_FILE=${TOKEN_FILE}"
}

# Distinct top-level groups of the repos in the admin config.
groups_from_config() {
    local cfg="${RENOVATE_ADMIN_CONFIG:-renovate-admin.json}"
    [ -f "${cfg}" ] || return 1
    jq -r '.repositories[]?' "${cfg}" | awk -F/ 'NF>1{print $1}' | sort -u
}

# Add the bot user (RENOVATE_BOT_USER) as a member of each group it must scan
# (RENOVATE_BOT_GROUPS, else derived from the admin config). Runnable by a group
# Owner — this is the part you don't need an instance admin for.
cmd_add_groups() {
    local user="${RENOVATE_BOT_USER:-}" uid groups g gid lvl code
    [ -n "${user}" ] || err "set RENOVATE_BOT_USER to the bot username."
    lvl="${RENOVATE_BOT_ACCESS_LEVEL:-30}"
    uid=$(curl -s "${hdr[@]}" "${GITLAB_API}/users?username=${user}" | jq -r '.[0].id // empty')
    [ -n "${uid}" ] || err "user '${user}' not found on ${GITLAB_SERVER}."
    groups="${RENOVATE_BOT_GROUPS:-$(groups_from_config || true)}"
    [ -n "${groups}" ] || err "no groups: set RENOVATE_BOT_GROUPS or provide ${RENOVATE_ADMIN_CONFIG:-renovate-admin.json}."
    echo "Adding ${user} (id=${uid}) as member (access ${lvl}) of:"
    for g in ${groups}; do
        gid=$(urlencode "${g}")
        code=$(curl -s "${hdr[@]}" -X POST "${GITLAB_API}/groups/${gid}/members" \
            --data "user_id=${uid}" --data "access_level=${lvl}" -o /dev/null -w '%{http_code}')
        case "${code}" in
            201) echo "  ${g}: added";;
            409) curl -s "${hdr[@]}" -X PUT "${GITLAB_API}/groups/${gid}/members/${uid}" --data "access_level=${lvl}" -o /dev/null
                 echo "  ${g}: already a member (level set ${lvl})";;
            *)   echo "  ${g}: HTTP ${code} (need Owner on the group?)";;
        esac
    done
}

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)";;
    sa-create)  cmd_sa_create "$@";;
    add-groups) cmd_add_groups "$@";;
    *) echo "usage: renovate-bot.sh <create | list | revoke | ci-set [repo] | sa-create | add-groups>";
       echo "  single group/project token: renovate-bot.sh create";
       echo "  cross-group service account: sa-create (admin) then add-groups (group owner)";;
esac