Claude on Vertex AI from GitHub Actions
Reusable infrastructure for invoking Claude (via anthropics/claude-code-action)
on Google Vertex AI from GitHub Actions workflows in this repo. Authentication
is via Workload Identity Federation — no long-lived secrets.
When to Use
Add a new Claude-in-CI use case (PR review, CVE triage, dependency-update analysis, automated docs generation, etc.) when:
- The task is well-suited to an LLM with tool use (reading files, running CLI
commands, calling
gh) - The task fires on a GitHub event (schedule, workflow_dispatch, push, PR)
- You want an issue, PR comment, or artifact as the output
The first use case (and template) is the CVE triage step in
container-scan.yml — see .agents/skills/container-vulns/SKILL.md.
What's Already Provisioned
GCP project: viral-seq-ai
| Resource | Identifier |
|---|---|
| Workload Identity Pool | github-actions-pool (global) |
| OIDC provider | broadinstitute-github |
| Provider attribute condition | assertion.repository_owner == 'broadinstitute' |
| Required APIs | aiplatform.googleapis.com, iamcredentials.googleapis.com |
Service accounts (one per use case):
| SA email | Use case | Workflow |
|---|---|---|
viral-ngs-cve-triage@viral-seq-ai.iam.gserviceaccount.com |
Weekly CVE triage | .github/workflows/container-scan.yml |
The pool + provider are reusable across use cases. Add a new SA per use case (principle of least privilege; easier to audit and disable individually).
Adding a New Claude-in-CI Use Case
You need GCP IAM Admin on viral-seq-ai for steps 1–3.
1. Create a service account scoped to the use case
gcloud iam service-accounts create <use-case-name> \
--project=viral-seq-ai \
--display-name="<Human-readable name>" \
--description="Used by <workflow-file>.yml to invoke Claude on Vertex AI for <purpose>"
2. Grant minimum roles
Vertex invocation requires both:
gcloud projects add-iam-policy-binding viral-seq-ai \
--member="serviceAccount:<use-case-name>@viral-seq-ai.iam.gserviceaccount.com" \
--role="roles/aiplatform.user" \
--condition=None
gcloud projects add-iam-policy-binding viral-seq-ai \
--member="serviceAccount:<use-case-name>@viral-seq-ai.iam.gserviceaccount.com" \
--role="roles/serviceusage.serviceUsageConsumer" \
--condition=None
3. Bind the GitHub repo to the SA via WIF
PROJECT_NUMBER=$(gcloud projects describe viral-seq-ai --format='value(projectNumber)')
gcloud iam service-accounts add-iam-policy-binding \
<use-case-name>@viral-seq-ai.iam.gserviceaccount.com \
--project=viral-seq-ai \
--role="roles/iam.workloadIdentityUser" \
--member="principalSet://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/github-actions-pool/attribute.repository/broadinstitute/viral-ngs"
For tighter scope, use attribute.ref (branch) or attribute.workflow (specific
workflow file) in place of (or in addition to) attribute.repository.
4. Set GitHub repo variables
If the new use case lives alongside the CVE triage one, you can reuse
GCP_PROJECT_ID and GCP_WIP_PROVIDER. The SA email differs per use case —
either create a use-case-specific variable (e.g., GCP_PR_REVIEW_SA_EMAIL) or
hard-code it in the workflow file.
gh variable set GCP_<USECASE>_SA_EMAIL \
--body "<use-case-name>@viral-seq-ai.iam.gserviceaccount.com" \
--repo broadinstitute/viral-ngs
5. Add the workflow steps
See the canonical pattern below.
Canonical Workflow Pattern
permissions:
id-token: write # for OIDC token to GCP via WIF
# plus whatever else your workflow needs (contents: read, issues: write, etc.)
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
# If Claude needs git history (`git log --grep`, `git show`), use 0.
# Otherwise the default shallow clone is fine.
fetch-depth: 0
- name: Authenticate to GCP via Workload Identity Federation
uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ vars.GCP_WIP_PROVIDER }}
service_account: ${{ vars.GCP_<USECASE>_SA_EMAIL }}
- name: Claude on Vertex AI
# Pin to commit SHA for supply-chain safety; bump when picking up new releases.
uses: anthropics/claude-code-action@<FULL_40_CHAR_SHA> # v1
env:
CLAUDE_CODE_USE_VERTEX: '1'
CLOUD_ML_REGION: global # Sonnet 5 supports the global endpoint
ANTHROPIC_VERTEX_PROJECT_ID: ${{ vars.GCP_PROJECT_ID }}
with:
use_vertex: 'true'
github_token: ${{ secrets.GITHUB_TOKEN }} # avoids needing the Claude Code GitHub App
claude_args: '--model claude-sonnet-5 --max-turns 30'
settings: |
{
"permissions": {
"allow": [
"Read",
"Write",
"Bash(git log:*)",
"Bash(grep:*)",
"Bash(jq:*)"
# ... add only what the prompt actually needs
]
}
}
prompt: |
<Your prompt here. Claude sees the GH workspace as cwd.>
Gotchas (Things We Learned the Hard Way)
iamcredentials.googleapis.commust be enabled. WIF impersonation calls this API; without it you getIAM Service Account Credentials API has not been used in project ... before or it is disabledfrom the Claude action, AFTER auth appears to succeed. Confusing.Use
@v1, not@beta.claude-code-action@betais the older API shape with separatedirect_prompt,max_turns,allowed_toolsinputs.@v1consolidates everything intoprompt+claude_args+settings. Pin to a commit SHA, not the floating tag.Both
use_vertex: 'true'ANDCLAUDE_CODE_USE_VERTEX=1are needed — the input goes to the action wrapper, the env var goes to the underlyingclaude-codeCLI.Pass
github_token: ${{ secrets.GITHUB_TOKEN }}to skip the Claude Code GitHub App requirement. Without this you getError: Claude Code is not installed on this repository.The permission DSL doesn't take path globs on
Writeor paths onBashcommand names.Write(/tmp/issues/**)andBash(mkdir:/tmp/issues*)are silently rejected and Claude hitspermission_denials_count > 0. TheBash(<cmd>:*)pattern is for the args after the command, not paths embedded in the command name. For now, use unrestrictedWriteand rely on prompt instructions to constrain output paths.Region
global(recommended) gives dynamic routing across regions for Sonnet 5. Pin to a specific region (e.g.,us-east5,europe-west1) only if you need data-residency control.fetch-depth: 0if Claude needs git history. Defaultactions/checkoutis shallow (depth=1);git log --all --grepandgit show <sha>will produce empty results without unshallowing.Always
jq-query authoritative data sources in the prompt. When Claude has multiple ways to learn a fact (training data vs reading a workspace file), tell it explicitly which is canonical and require the tool call. Otherwise it sometimes infers from training data.
Cost / Safety
- Cost gate: invoke Claude only when the workflow has real work to do (e.g., new CVEs detected, PR opened by non-bot). Don't invoke on every schedule tick unconditionally.
- Turn cap:
--max-turns 30is generous for triage-style tasks; tighten if you can. Observed: 6–10 turns for one-CVE analyses. - Cost order of magnitude: Sonnet 5 ≈ $0.10–1 per non-trivial task (one-CVE analysis with full repo reading). Opus 4.7 is ~5× more expensive for marginal quality gain on most CI tasks.
- Provider gate: the OIDC provider attribute condition limits token
minting to repos owned by
broadinstitute. Other GitHub orgs cannot use this pool. - SA gate: each SA's
workloadIdentityUserbinding limits which repos can impersonate it. Default scope:attribute.repository/broadinstitute/viral-ngs. Tighten withattribute.reforattribute.workflowif needed. - Tool allowlist: only allow tools the prompt actually uses. Avoid wildcard
Bash(*:*)— name specific commands likeBash(git log:*).
Key Files
| File | Purpose |
|---|---|
.github/workflows/container-scan.yml |
First use case (CVE triage); reference for the workflow pattern |
.agents/skills/container-vulns/SKILL.md |
The CVE triage playbook this infra serves |
References
anthropics/claude-code-action- Claude Code on Google Vertex AI
google-github-actions/auth(WIF action)
Source: broadinstitute/viral-ngs — distributed by TomeVault.