fix(cron_vm): publish from agent-shin fork + harden systemd unit

The cron host has no write access to BerriAI/litellm-docs by design. PRs
now open from a long-lived fork at agent-shin/litellm-docs:

- run_daily.sh validates AGENT_SHIN_GITHUB_TOKEN up front (failing 30 min
  into a run because the env file is missing one line is wasted spend).
- The pre-commit shim adds a transient `fork` remote with the token
  embedded in the URL, force-pushes the branch, then removes the remote
  so the token never lives on disk.
- `gh pr create --head agent-shin:<branch>` opens the cross-repo PR
  with GH_TOKEN scoped to AGENT_SHIN_GITHUB_TOKEN. A second
  `gh pr edit --add-reviewer` runs under GITHUB_TOKEN (mateo-berri's
  PAT) because agent-shin's PAT lacks RequestReviewsByLogin permission
  on the upstream repo.
- PR_REVIEWERS env var (default `mateo-berri`) controls who gets
  auto-tagged; empty disables.

Also bring litellm-compat-matrix.service to working state:

- Hardcode `/home/mateo` paths everywhere %h was used. systemd expands
  %h against the *manager's* home (/root for PID 1) in *system* units,
  not against the User= directive. The mismatch made ReadWritePaths
  point at /root/.cache and the namespace setup failed with
  status=226/NAMESPACE before run_daily.sh ever started.
- Explicit Environment=PATH so `uv` and `claude` under
  ~/.local/bin are visible to the up-front command-presence check;
  systemd's default PATH excludes them.
- Expand ReadWritePaths to include ~/.claude (CLI per-session state)
  and ~/.config/gh (gh host config fallback); both are written under
  ProtectHome=read-only.

env.example refreshed: drop AWS_ACCESS_KEY_ID/SECRET +
GOOGLE_APPLICATION_CREDENTIALS in favor of AWS_BEARER_TOKEN_BEDROCK and
ADC via the VM's metadata server; document AGENT_SHIN_GITHUB_TOKEN,
FORK_OWNER/FORK_REPO overrides, and VERTEXAI_LOCATION=global.
This commit is contained in:
mateo-berri 2026-05-16 20:31:11 +00:00
parent 1b30abf4a8
commit 891da2372d
3 changed files with 139 additions and 21 deletions

View file

@ -8,22 +8,36 @@
# Anthropic
ANTHROPIC_API_KEY=
# Bedrock (invoke + converse columns)
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
# Bedrock (invoke + converse columns).
# Use Anthropic's Bedrock API-key passthrough (long-lived bearer token).
# No AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY required for the matrix --
# both the LiteLLM invoke and converse routes pick up
# AWS_BEARER_TOKEN_BEDROCK when present.
AWS_BEARER_TOKEN_BEDROCK=
AWS_REGION_NAME=us-east-1
# Vertex AI
# Vertex AI.
# On the GCP VM, the default service-account ADC from the metadata server
# is used -- no JSON key file is needed. If you ever need to run outside
# GCP, also export GOOGLE_APPLICATION_CREDENTIALS=/path/to/sa.json.
VERTEXAI_PROJECT=
VERTEXAI_LOCATION=us-east5
GOOGLE_APPLICATION_CREDENTIALS=/etc/litellm-compat-matrix.gcp.json
VERTEXAI_LOCATION=global
# Microsoft Foundry (Azure column)
AZURE_FOUNDRY_API_KEY=
AZURE_FOUNDRY_API_BASE=
# REQUIRED for publishing: PAT for the `agent-shin` user, used to push
# the daily compat-matrix branch to its fork (agent-shin/litellm-docs)
# and open the cross-repo PR against BerriAI/litellm-docs. Scopes:
# classic `repo` + `workflow`, or fine-grained on agent-shin/litellm-docs
# with Contents:RW + Pull requests:RW + Workflows:RW.
# Skip by setting SKIP_PUBLISH=1 (publishes nothing; only writes the
# matrix JSON locally).
AGENT_SHIN_GITHUB_TOKEN=
# Optional: lifts the unauthenticated rate limit on the GitHub Releases
# API used by `resolver.py`. Not required.
# API used by `resolver.py`. Any token works (read-only). Not required.
# GITHUB_TOKEN=
# Optional overrides; defaults are sensible for the cron VM.
@ -32,3 +46,5 @@ AZURE_FOUNDRY_API_BASE=
# DOCS_REPO=BerriAI/litellm-docs
# DOCS_BRANCH=main
# DOCS_TARGET_PATH=src/data/compatibility-matrix.json
# FORK_OWNER=agent-shin
# FORK_REPO=agent-shin/litellm-docs

View file

@ -13,9 +13,13 @@
# sudo systemctl daemon-reload
# sudo systemctl enable --now litellm-compat-matrix.timer
#
# The %h specifier expands to the runtime user's home directory, which
# avoids hard-coding /home/mateo into a unit that should be portable
# across operator handoffs. The runtime user (`User=mateo`) must:
# Paths are hard-coded to /home/mateo rather than using systemd's %h
# specifier. Why: in *system* units (this one), %h is expanded at
# parse time against the *manager's* home -- which is /root for PID 1
# -- and *not* against the User= directive. That mismatch makes
# ReadWritePaths point at /root/.cache (which doesn't exist), causing
# the namespace setup to fail with status=226/NAMESPACE before the
# script ever runs. The runtime user (`User=mateo`) must:
#
# * have a checkout of `BerriAI/litellm` at `~/litellm/litellm` so the
# publisher module is importable;
@ -28,7 +32,7 @@
[Unit]
Description=Claude Code compatibility-matrix populator (oneshot)
Documentation=file://%h/litellm/litellm/tests/claude_code/cron_vm/README.md
Documentation=file:///home/mateo/litellm/litellm/tests/claude_code/cron_vm/README.md
Wants=network-online.target
After=network-online.target
@ -41,9 +45,21 @@ Group=mateo
# is the standard `KEY=value` one line per env var.
EnvironmentFile=-/etc/litellm-compat-matrix.env
WorkingDirectory=%h/litellm/litellm
# systemd starts with a minimal PATH (~/usr/local/bin:/usr/bin:/bin).
# `uv` and `claude` are installed under the runtime user's `~/.local/bin`
# so we have to prepend it explicitly; otherwise run_daily.sh fails at
# the up-front command-presence check.
Environment=PATH=/home/mateo/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ExecStart=%h/litellm/litellm/tests/claude_code/cron_vm/run_daily.sh
# `HOME` is auto-set to /home/mateo when User=mateo is honored, but be
# explicit so anything that reads $HOME (e.g. uv's cache lookup, the
# claude CLI's per-session dir) sees the right value even if a future
# refactor flips DynamicUser= or PrivateUsers= on.
Environment=HOME=/home/mateo
WorkingDirectory=/home/mateo/litellm/litellm
ExecStart=/home/mateo/litellm/litellm/tests/claude_code/cron_vm/run_daily.sh
# 90 minutes is generous: cold runs do `git clone` + `uv sync` of a new
# tag's lockfile, which can take a couple of minutes on a 2-vCPU VM,
@ -57,10 +73,27 @@ Restart=no
# Security hardening: the populator only reads the litellm checkout and
# the env-file; everything else it writes lives in either the worktree
# (managed) or `/tmp` (cleaned up by tempfile).
#
# ReadWritePaths whitelist:
# * litellm-cron-worktree - the long-lived stable-tag checkout +
# its `.venv` (`uv sync` rewrites every
# run) + `.uv-bin` (pinned `uv` binary
# cache).
# * .cache - uv's wheel cache (~/.cache/uv) so we
# don't redownload pinned deps each run.
# * .claude - `claude` CLI's per-session state under
# `~/.claude/projects/<sha>/`; created
# on every `claude --print` invocation.
# * .config/gh - `gh` CLI host config; technically not
# needed when we pass GH_TOKEN inline,
# but cheap to whitelist and prevents
# future regressions if a code path
# ever falls back to the host config.
# * /tmp - mktemp -d workdir + proxy logs.
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=%h/litellm-cron-worktree %h/.cache /tmp
ReadWritePaths=/home/mateo/litellm-cron-worktree /home/mateo/.cache /home/mateo/.claude /home/mateo/.config/gh /tmp
PrivateTmp=true
[Install]

View file

@ -40,6 +40,11 @@ DOCS_BRANCH="${DOCS_BRANCH:-main}"
DOCS_TARGET_PATH="${DOCS_TARGET_PATH:-src/data/compatibility-matrix.json}"
SKIP_PUBLISH="${SKIP_PUBLISH:-0}"
PYTEST_K="${PYTEST_K:-}"
# Comma-separated GitHub usernames to request a review from on every PR.
# Reviewers must have at least read access to ${DOCS_REPO}. PR-author
# (agent-shin) has implicit rights to request reviews from anyone with
# read access, so no extra token scope is needed. Set to empty to skip.
PR_REVIEWERS="${PR_REVIEWERS:-mateo-berri}"
POPULATOR_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WORKDIR="$(mktemp -d -t litellm-compat-matrix.XXXXXX)"
@ -89,6 +94,15 @@ for cmd in git uv gh jq curl claude; do
command -v "${cmd}" >/dev/null 2>&1 || die "missing required command: ${cmd}"
done
# Publishing is from a fork (agent-shin/litellm-docs) so neither the cron
# host nor the bot identity needs write access to BerriAI/litellm-docs. We
# require the fork token up front -- failing 30 minutes into a run because
# the env file is missing one line is a waste of CI quota.
if [[ "${SKIP_PUBLISH}" != "1" ]]; then
[[ -n "${AGENT_SHIN_GITHUB_TOKEN:-}" ]] \
|| die "AGENT_SHIN_GITHUB_TOKEN required to open PRs from agent-shin/litellm-docs (or set SKIP_PUBLISH=1)"
fi
# ---------------------------------------------------------------------------
# 1. Resolve versions
# ---------------------------------------------------------------------------
@ -340,6 +354,8 @@ fi
DATE_UTC="$(date -u +%Y-%m-%d)"
BRANCH_NAME="compat-matrix/${LITELLM_VERSION}-${CLAUDE_CODE_VERSION}-${DATE_UTC}"
DOCS_CLONE="${WORKDIR}/litellm-docs"
FORK_OWNER="${FORK_OWNER:-agent-shin}"
FORK_REPO="${FORK_REPO:-${FORK_OWNER}/litellm-docs}"
log "cloning ${DOCS_REPO}@${DOCS_BRANCH}"
gh repo clone "${DOCS_REPO}" "${DOCS_CLONE}" -- --depth 1 --branch "${DOCS_BRANCH}"
@ -369,9 +385,24 @@ EOF
)"
git commit -m "${COMMIT_MSG}"
# --force-with-lease so a same-day rerun fast-forwards (or rebases) the
# existing branch without clobbering a maintainer's manual fixup.
git push --force-with-lease --set-upstream origin "${BRANCH_NAME}"
# Push to the fork (agent-shin/litellm-docs), not to BerriAI/litellm-docs.
# The cron host has no write access to BerriAI/litellm-docs by design --
# only agent-shin's PAT does, and only over its own fork. The temp remote
# carries the token in its URL, so we add it, push, then immediately
# remove it so the token never lingers in ${DOCS_CLONE}/.git/config.
# (${DOCS_CLONE} is also rm -rf'd by the cleanup trap on exit.)
#
# Plain --force (not --force-with-lease) is acceptable here: the fork
# branch is bot-owned, only this script ever writes to it, and runs are
# serialized by the systemd timer. --force-with-lease would require a
# fetch to populate the remote-tracking ref before each push and adds
# no safety in this single-writer setup.
FORK_PUSH_URL="https://x-access-token:${AGENT_SHIN_GITHUB_TOKEN}@github.com/${FORK_REPO}.git"
git remote remove fork 2>/dev/null || true
git remote add fork "${FORK_PUSH_URL}"
git push --force --set-upstream fork "${BRANCH_NAME}"
git remote remove fork
unset FORK_PUSH_URL
# Per-feature status table for the PR body. Reviewers triage from this.
PR_FEATURE_TABLE="$(jq -r '
@ -402,13 +433,26 @@ Generated by \`tests/claude_code/cron_vm/run_daily.sh\`. Close without merging i
EOF
)"
log "opening PR"
log "opening PR from ${FORK_OWNER}:${BRANCH_NAME} -> ${DOCS_REPO}:${DOCS_BRANCH}"
# GH_TOKEN here is scoped to this single subshell so we don't bleed the
# fork token into the rest of the script (release-listing earlier uses
# ${GITHUB_TOKEN}, which may be a different identity). gh's --head accepts
# `OWNER:BRANCH` for cross-repo PRs from a fork.
#
# Reviewer assignment is done in a *separate* call below: as the PR
# author from a fork, agent-shin has no write/triage access on
# ${DOCS_REPO} and the `RequestReviewsByLogin` GraphQL mutation
# (which backs `gh pr create --reviewer` and `gh pr edit --add-reviewer`)
# rejects with "does not have the correct permissions". We use the
# collaborator-scoped ${GITHUB_TOKEN} for that instead. Don't fold
# --reviewer into `gh pr create` here -- it would fail the whole
# create on the very first cron run.
set +e
PR_OUT="$(
gh pr create \
GH_TOKEN="${AGENT_SHIN_GITHUB_TOKEN}" gh pr create \
--repo "${DOCS_REPO}" \
--base "${DOCS_BRANCH}" \
--head "${BRANCH_NAME}" \
--head "${FORK_OWNER}:${BRANCH_NAME}" \
--title "${PR_TITLE}" \
--body "${PR_BODY}" 2>&1
)"
@ -418,10 +462,35 @@ echo "${PR_OUT}"
if [[ ${PR_EXIT} -ne 0 ]]; then
if grep -q "a pull request for branch.*already exists" <<<"${PR_OUT}"; then
log "PR already exists for ${BRANCH_NAME}; updated branch in place"
log "PR already exists for ${FORK_OWNER}:${BRANCH_NAME}; updated branch in place"
else
die "gh pr create failed (exit ${PR_EXIT})"
fi
fi
# Request reviews from PR_REVIEWERS using the collaborator-scoped
# ${GITHUB_TOKEN} (mateo-berri's token, already provisioned for release
# listing). This is idempotent: `gh pr edit --add-reviewer` is a no-op
# on a user who's already in reviewRequests, and silently re-adds
# anyone whose prior review was dismissed -- so same-day reruns stay
# clean. Reviewer-add failures are non-fatal: the matrix JSON has
# already landed on the PR; the worst case is a manual ping.
if [[ -n "${PR_REVIEWERS}" ]]; then
if [[ -z "${GITHUB_TOKEN:-}" ]]; then
log "WARN: PR_REVIEWERS set but GITHUB_TOKEN missing -- cannot request reviews; skipping"
else
log "requesting reviews from: ${PR_REVIEWERS}"
set +e
GH_TOKEN="${GITHUB_TOKEN}" gh pr edit \
"${FORK_OWNER}:${BRANCH_NAME}" \
--repo "${DOCS_REPO}" \
--add-reviewer "${PR_REVIEWERS}" 2>&1 | sed 's/^/ /'
REVIEWER_EXIT=${PIPESTATUS[0]}
set -e
if [[ ${REVIEWER_EXIT} -ne 0 ]]; then
log "WARN: gh pr edit --add-reviewer exited ${REVIEWER_EXIT} (non-fatal)"
fi
fi
fi
log "done"