Files
computer-playbook/docs/contributing/tools/github/actions/secrets.md
T
kevinveenbirkenbachandClaude Opus 5 786899700f feat(litellm): serve OpenAI, Anthropic and OpenRouter from declared keys
The gateway could reach exactly one remote provider, through a bare
OPENROUTER_API_KEY group_var with an empty-string default and an `{% if %}`
around a single hardcoded model entry. OpenAI and Anthropic had no path at all.

All three are now declared in roles/svc-ai-litellm/meta/secrets.yml, which
mapping.py already calls the application config schema, so a key lands at
applications.svc-ai-litellm.secrets.credentials.<provider>_api_key like every
other credential. Each carries the `type` and `regex` its value must satisfy and
defaults to the central API.<provider>.api_key block, which keeps one
operator-facing place to fill a provider in while the role owns the shape.

type and regex had to become real to be worth declaring. 192 `validation:`
blocks sit in the role schemas today and not one line of Python enforces them,
so a `regex` would have joined a pile of decoration. validate_supplied_value
now runs on every path a value arrives from outside — the --set override, a
value already in the inventory, and the value standing next to a `default:`.
A generated value stays exempt because the algorithm that produced it is named
by the same schema and generate_value takes no schema argument. min_length is
deliberately left unenforced: the generators ignore it, so holding them to it
would fail where nothing is wrong.

The pattern is written for the paste, not for the vendor. OpenAI moved issuance
from sk- to sk-proj- without notice and Anthropic issues sk-ant-api03-; a schema
pinning a prefix would reject a valid key and abort inventory creation. `\S{20,}`
accepts every form those three issue today and one nobody has announced, and
rejects a trailing newline, an embedded space, a truncated copy and a
<placeholder>. A key pasted into the wrong provider's slot is caught at deploy
instead, where the route it publishes answers nothing.

The gateway iterates one LITELLM_REMOTE_BACKENDS list instead of three `{% if %}`
blocks; config.yaml.j2 and env.j2 read the same list and selectattr('key')
decides. LITELLM_CHAT_MODEL falls back to the first configured remote alias
rather than to a hardcoded openrouter/auto.

For CI the keys travel as INFINITO_API_{OPENAI,ANTHROPIC,OPENROUTER}_KEY through
default.env, the passthrough allowlist, the infinito service in compose.yml, the
workflow env block and the development inventory. The compose leg is not
optional: the env lint rejected the change without it, because a key the
inventory reads but compose does not forward resolves to empty inside the
container with no error. The workflow reads `secrets.X || vars.X` so a variable
works as asked while a secret wins, which matters because a variable is not
masked and the development inventory sets MASK_CREDENTIALS_IN_LOGS: false.

An unset key is not an error anywhere: the provider's routes stay unpublished
and the CLI test asserts their absence rather than skipping.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 19:57:21 +02:00

7.6 KiB

CI Secrets 🔐

This page lists every repository secret that GitHub Actions workflows in this repository consume, and documents the failure mode when a secret is missing. For repository variables see configuration.md. For the workflow catalog see workflows.md.

Repository secrets MUST be set under Settings → Secrets and variables → Actions → Secrets.

Secrets 📋

Secret Workflow Purpose Required when
BOT_APP_CLIENT_ID cron-update.yml GitHub App Client ID (OAuth-style Iv… identifier shown on the App's General page) used to mint a short-lived installation token for the update PR. CI_ENABLE_AUTO_UPDATES == 'true'
BOT_APP_PRIVATE_KEY cron-update.yml PEM-encoded private key of the same GitHub App. Used to sign the JWT that exchanges for the installation token. CI_ENABLE_AUTO_UPDATES == 'true'
API_OPENAI_KEY call-test-deploy.yml Publishes the openai/* models on the LiteLLM gateway and lets the CLI test exercise the route. Never. Unset leaves the route unpublished.
API_ANTHROPIC_KEY call-test-deploy.yml Publishes the anthropic/* models. Never.
API_OPENROUTER_KEY call-test-deploy.yml Publishes openrouter/auto. Never.

API_OPENAI_KEY, API_ANTHROPIC_KEY, API_OPENROUTER_KEY 🤖

These are optional and fail soft: unset means the provider's route is never published, and the deployment falls back to whatever local backend the round deploys. The workflow reads each as ${{ secrets.X || vars.X }}, so the same name works as a repository variable; see configuration.md for why the secret is the better half of that pair.

Set them the same way as the App credentials: Settings → Secrets and variables → Actions → Secrets → New repository secret, one per provider, the value being the provider's key verbatim.

The shape each key must have is declared per role rather than here, in roles/svc-ai-litellm/meta/secrets.yml with a type: and a regex:. The pattern rejects a truncated or whitespace-carrying paste and deliberately accepts any vendor prefix, so a provider changing its issuance format cannot turn a valid key into a failed inventory run.

BOT_APP_CLIENT_ID and BOT_APP_PRIVATE_KEY 🤖

Why these secrets exist 🎯

Actions performed with the workflow-provided GITHUB_TOKEN do not start further workflow runs. When cron-update.yml opens an update PR with GITHUB_TOKEN, the pull_request and pull_request_target events that drive entry-pr-change-orchestrate.yml do not fire on that PR, so the CI orchestrator never runs against it.

Opening the PR through a GitHub App installation token bypasses this restriction. The App counts as a distinct actor, so PR-lifecycle events fire normally and the full CI pipeline runs against every update PR.

Why a GitHub App and not a PAT 🏷️

A GitHub App installation token SHOULD be used because:

  • The token lifetime is bounded to one hour, and a new token is minted on every workflow run.
  • Permissions are scoped at the App installation level, independent of any user account.
  • No personal account footprint appears in audit logs.

A classic personal access token MAY be used as a fallback for unrelated automation, but it MUST NOT be used here because user-bound credentials expand the blast radius and require manual rotation.

How to generate the App 🛠️

  1. Create a new GitHub App under Organization settings → Developer settings → GitHub Apps → New GitHub App.
  2. Disable the webhook (uncheck Active under Webhook). The App MUST NOT receive events.
  3. Grant the following repository permissions and nothing else:
    • Contents: Read and write (the update script force-pushes to update/* branches).
    • Pull requests: Read and write (gh pr create / gh pr edit).
    • Metadata: Read (default, cannot be removed).
  4. Restrict installation to the owning account only.
  5. Generate a private key and download the PEM file.
  6. Install the App on the core repository only. The App MUST NOT be installed on unrelated repositories.

How to hand the credentials to CI 📥

  1. Open the repository on GitHub.
  2. Go to Settings → Secrets and variables → Actions.
  3. Stay on the Secrets tab.
  4. Click New repository secret and add BOT_APP_CLIENT_ID with the App's Client ID (shown on the App's General page under About, in the Iv… format — not the numeric App ID).
  5. Click New repository secret and add BOT_APP_PRIVATE_KEY with the complete PEM content, including the -----BEGIN PRIVATE KEY----- and -----END PRIVATE KEY----- lines.

How the workflow consumes the secrets ⚙️

Each PR-opening job in cron-update.yml runs an actions/create-github-app-token@v2 step gated on steps.diff.outputs.changed == 'true', then forwards the minted token as GH_TOKEN and the resolved APP_SLUG to the subsequent PR step. The job's permissions: block keeps the workflow-provided GITHUB_TOKEN read-only; write access is provided exclusively by the App token, scoped to the App's repository permissions.

Commit identity propagation 🪪

pr.sh reads APP_SLUG from the environment and resolves the App's bot user via gh api /users/<slug>[bot]. The resolved login and numeric user ID are written into git config user.name and git config user.email in the <id>+<slug>[bot]@users.noreply.github.com form that GitHub accepts for verified commits. When APP_SLUG is unset, the script falls back to the github-actions[bot] identity. That branch is reached only when the script is invoked outside the workflow, for example a local smoke test where the caller has set GH_TOKEN to something other than an App token.

Failure mode when secrets are missing 💥

A missing secret resolves to an empty string in workflow expressions. The workflow run starts and proceeds normally until the token-minting step:

Stage Behaviour when both secrets are missing
Workflow parse Run starts. GitHub emits the warning The following secrets are referenced but not defined: BOT_APP_CLIENT_ID, BOT_APP_PRIVATE_KEY.
Generate app token step actions/create-github-app-token declares both inputs as required and fails with Error: Input required and not supplied: client-id.
Open PR if … changed step Skipped. Its implicit if: condition is success() && (steps.diff.outputs.changed == 'true'), and success() is false after the token step failed.
pr.sh Not executed. No commit, no push, no PR.
Outcome The run is marked failed. GitHub notifies repository admins. No PR is created with a wrong identity.

The behaviour is fail-loud by design. Missing or mistyped secret names MUST surface as a failed workflow run rather than as a PR with an unintended author.