This repository provides centralized, reusable GitHub Actions workflows for AI-assisted code review and automation. As of v3, it supports any Bedrock model in addition to the direct Anthropic API path, with routing handled inside the orchestrator so exactly one executor runs per call.
Two diagrams below: the first shows where this repository sits in the broader dotCMS AI toolchain; the second is the repo-internal control flow.
High-level overview of the dotCMS AI toolchain, illustrating where we have flexibility to avoid vendor lock-in. Items shaded green are flexible for dotCMS to modify by configuration.
The Anthropic Action (used in GitOps) routes through dotCMS Cloud Compute but is constrained by IAM to Anthropic models only. All dotCMS-owned tools — dotCMS GitHub Actions — route through the same Cloud Compute layer AND can reach any model in the LLM layer.
Claude Code, Claude Chat, and Claude Co-Work default to Anthropic Hosted Compute and can optionally route through dotCMS Cloud Compute (AWS Bedrock) instead, but must still use the Anthropic Claude models in Bedrock.
flowchart TD
subgraph UI["UI Layer"]
GH[GitHub]
CC["Claude Code"]
CCHAT["Claude Chat"]
CCOWORK["Claude Co-Work"]
end
subgraph gitops["GitOps Zone"]
ORCH["dotCMS Orchestrator"]
AA["Anthropic Action"]
DA["dotCMS Action"]
ORCH --> AA
ORCH --> DA
end
subgraph ANTHCOMP["Anthropic Hosted Compute"]
AAPI["Anthropic API"]
end
subgraph CLOUD["dotCMS Cloud Compute"]
bedrock["AWS Bedrock"]
end
subgraph LLM["LLM Layer"]
AM["Anthropic Models"]
OM["Other Models"]
end
GH --> ORCH
CC -. "optional: must be Claude models" .-> CLOUD
CC -->|"default"| ANTHCOMP
CCHAT -->|"default"| ANTHCOMP
CCOWORK -->|"default"| ANTHCOMP
AA -->|"Must use Anthropic models"| CLOUD
DA --> CLOUD
CCOWORK -. "optional: must be Claude models" .-> CLOUD
bedrock --> AM
bedrock -->|"dotCMS tools only"| OM
classDef flexible fill:#e8f5e9,stroke:#1b5e20,stroke-width:1px
class DA,CC,CCOWORK,ORCH,bedrock flexible
In this picture, the workflows in this repository implement the dotCMS Orchestrator + the two action paths beneath it.
Consumer repositories handle their own webhook triggers and call this repo's claude-orchestrator.yml. The orchestrator inspects model_id and fans out to exactly one executor.
flowchart TD
subgraph consumer["Consumer Repository"]
consumer_wf["Consumer workflow<br/>(.github/workflows/*.yml)"]
triggers["Webhook triggers<br/>• pull_request<br/>• issue_comment<br/>• pull_request_review<br/>• issues"]
cond["Conditional logic<br/>• @claude detection<br/>• custom guards<br/>• id-token: write"]
triggers --> cond --> consumer_wf
end
subgraph this_repo["dotCMS/ai-workflows"]
orch["claude-orchestrator.yml<br/>(reusable)"]
route{"route job<br/>inspect model_id"}
claude_exec["claude-executor.yml<br/>(Anthropic models)"]
generic_exec["bedrock-generic-executor.yml<br/>(any other Bedrock model)"]
codex_exec["codex-executor.yml<br/>(OpenAI GPT/Codex)"]
orch --> route
route -->|"empty or *.anthropic.*"| claude_exec
route -->|"openai.*"| codex_exec
route -->|"anything else"| generic_exec
end
subgraph upstreams["External calls"]
anth_api["Anthropic API<br/>(api.anthropic.com)"]
bedrock["AWS Bedrock<br/>(Converse / claude-code-action use_bedrock)"]
mantle["bedrock-mantle<br/>(OpenAI Responses API)"]
end
consumer_wf -->|"workflow_call"| orch
claude_exec -->|"provider=anthropic-api"| anth_api
claude_exec -->|"provider=anthropic-bedrock<br/>via OIDC"| bedrock
generic_exec -->|"OIDC + Converse"| bedrock
codex_exec -->|"OIDC + short-term bearer (SDK)"| mantle
classDef new fill:#e8f5e9,stroke:#1b5e20,stroke-width:1px
class generic_exec,route,codex_exec new
Nodes shaded green are new in v3 (codex-executor added later for the OpenAI/mantle path). The route job uses anchored regexes (^([a-z]+\.)?anthropic\., ^([a-z]+\.)?openai\.) so model IDs that merely contain the substring "anthropic."/"openai." (e.g. us.not-anthropic.foo) are not misrouted.
model_id value |
Provider mode | Executor |
|---|---|---|
| (empty) | anthropic-api |
claude-executor.yml |
anthropic.* or <region>.anthropic.* |
anthropic-bedrock |
claude-executor.yml |
openai.* (e.g. openai.gpt-5.5, openai.gpt-5.4) |
openai-mantle |
codex-executor.yml |
Anything else (us.amazon.*, meta.*, mistral.*, ...) |
bedrock-generic |
bedrock-generic-executor.yml |
The non-matching executor jobs are skipped by job-level if: conditional, not "ran and exited" — billable runner time is zero for the skipped paths.
Located in consuming repositories (e.g. infrastructure-as-code, core):
- Handle webhook events directly (preserves
github.event_namecontext, whichworkflow_callwould lose) - Implement trigger conditions and any custom guards (org-membership checks, allowlists, etc.)
- Call centralized workflows with appropriate parameters
- Provide repository-specific configuration (model, prompt, tools, timeouts, IAM role ARN)
Lightweight wrapper that handles @claude mention detection and routes by model_id to the appropriate executor. Consumer repositories almost always call this rather than calling an executor directly.
Runs anthropics/claude-code-action@v1 with either:
provider=anthropic-api— direct Anthropic API (requiresANTHROPIC_API_KEYsecret)provider=anthropic-bedrock—use_bedrock: trueover AWS Bedrock (requiresbedrock_role_arninput + OIDC)
Includes a pre-flight API health check on the anthropic-api path that skips gracefully on Anthropic 5xx.
Uses the Bedrock Converse API, which is model-family-agnostic. Maintains its own sticky comment via an inline helper (a setup step writes a bash find-or-update helper to /tmp so the logic isn't dependent on the consumer's checkout), replicating the auto-update behavior claude-code-action provides for free on the Anthropic path. Accepts a sticky_namespace input so multiple review jobs on the same PR don't clobber each other.
For openai.* models (GPT-5.5, GPT-5.4), which are not on bedrock-runtime — there is no InvokeModel/Converse. They are served only by the separate bedrock-mantle endpoint exposing the OpenAI Responses API. The frontier GPT-5.x/Codex models live under https://bedrock-mantle.{region}.api.aws/openai/v1/responses; the open-weight gpt-oss-* models live under …/v1/responses. The executor:
- Calls mantle with the OpenAI SDK, authenticated by a short-term Bedrock bearer token minted in-process from the assumed-role session via
aws-bedrock-token-generator(provide_token()). The SDK can't consume SigV4 directly, but a short-term key keeps the OIDC-only posture: it's derived from the current STS credentials (no long-lived secret), inherits the role's permissions, expires with the role session (≤1h here, ≤12h cap), is not a stored resource (nothing to delete), and is never written to env/disk/logs. No marketplace subscription; no long-term API key. - Streams Server-Sent Events and accumulates
response.output_text.deltachunks. Streaming is mandatory: GPT-5.x reasons before emitting, so a non-streaming call buffers and looks like a 60–100s hang. - Selects the OpenAI-compat base path by model id:
/openai/v1for frontier GPT-5.x/Codex,/v1forgpt-oss-*. The two families reject each other's path (400 validation_error: "does not support the '…/responses' API") — verified live 2026-06-11 (#34). Sending all models to/v1(the original behavior) is why GPT-5.5/5.4 looked unavailable. Amantle_api_pathinput overrides the auto-selection if AWS unifies the routing. - Uses the requested region as-is. GPT-5.5/5.4 are served in us-east-1 and us-east-2 (GPT-5.4 also us-west-2) and gpt-oss in all, so the orchestrator's us-east-1 default works for every model (verified live 2026-06-11 — at v3.1.0's authoring GPT-5.x were us-east-2-only, which is why an earlier revision remapped the region; that remap has been removed).
- Sends
store: falseon each request for zero data retention — the Responses API otherwise defaultsstore: true, retaining input+output for 30 days in-region forprevious_response_idchaining, which single-shot review doesn't need. - Reuses the same
/tmpsticky-comment helper andsticky_namespaceinput as the generic executor. Exposesreasoning_effort(defaultmedium). Notemax_output_tokenscaps only the visible answer, not reasoning tokens.
IAM: bedrock-mantle:CreateInference scoped by bedrock-mantle:Model StringLike openai.* (no per-model ARNs exist on mantle), plus bedrock-mantle:CallWithBearerToken scoped to BearerTokenType=SHORT_TERM (required to use any API key; short-term-only blocks long-term keys). Provisioned in dotCMS/Infrastructure-as-code bedrock-code-review/ (#7836). No AWS Marketplace subscription is required — mantle bills on-demand directly.
Consumer repositories maintain full control over webhook events — no loss of event context, no double triggering.
Centralized execution logic. One place to update Claude Code action versions, IAM patterns, sticky-comment behavior.
Exactly one executor runs per orchestrator call. Selecting a Claude model never invokes the generic Bedrock path in parallel.
- Anthropic API path requires per-consumer
ANTHROPIC_API_KEY(cost tracked per repo, no shared credentials) - Bedrock paths use short-lived OIDC-issued STS credentials — no long-lived AWS secrets in GitHub Actions secrets
ANTHROPIC_API_KEYisrequired: falseon the orchestrator schema since v3 — consumers that only use Bedrock paths don't need it
- Any model AWS Bedrock offers, swappable via the
model_idinput - Per-repo prompts, tool configurations, timeouts
- Per-job
sticky_namespaceto prevent comment collisions
jobs:
claude-automatic:
if: github.event_name == 'pull_request'
uses: dotCMS/ai-workflows/.github/workflows/claude-orchestrator.yml@v2.1.0
with:
trigger_mode: automatic
prompt: Review this PR for code quality and security.
secrets:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}The v2 invocation above continues to work unchanged. model_id defaults to empty → anthropic-api path.
jobs:
claude-bedrock:
permissions:
id-token: write
contents: write
pull-requests: write
issues: write
uses: dotCMS/ai-workflows/.github/workflows/claude-orchestrator.yml@v3.0.0
with:
trigger_mode: automatic
enable_mention_detection: false
prompt: Review this PR for correctness, security, and design issues.
model_id: global.anthropic.claude-sonnet-4-6
bedrock_role_arn: arn:aws:iam::123456789012:role/GitHubActions-BedrockReviewjobs:
nova-review:
permissions:
id-token: write
contents: read
pull-requests: write
uses: dotCMS/ai-workflows/.github/workflows/claude-orchestrator.yml@v3.0.0
with:
trigger_mode: automatic
enable_mention_detection: false
prompt: Review this PR for backend Java/Spring issues.
model_id: us.amazon.nova-pro-v1:0
bedrock_role_arn: arn:aws:iam::123456789012:role/GitHubActions-BedrockReview
sticky_namespace: backend-reviewerNote on permissions: when invoking the orchestrator, the caller's
permissions:block must be at least as broad as the strictest executor it could route to.claude-executor.ymlrequirescontents: write+issues: write(legacy of the v2 interactive flow).bedrock-generic-executor.ymlonly needscontents: read+pull-requests: write+id-token: write. If your consumer is wired to use both paths (or might in the future), grant the union.
The original orchestrator design attempted to centralize trigger logic, but GitHub Actions workflow_call loses the original webhook event context. When a consumer workflow calls a reusable workflow:
- Consumer receives
issue_commentevent - Consumer calls orchestrator via
workflow_call - Orchestrator sees
github.event_nameas"workflow_call", not"issue_comment" - All conditional logic fails
- Multiple jobs may trigger unexpectedly
The current architecture solves this by keeping trigger logic where the event context is available (in the consumer workflow) and using centralized workflows only for execution and model routing.
For the cost-doubling concern that motivated v3's routing job: see the route job in claude-orchestrator.yml and the mutually-exclusive if: conditions on the two Run Claude Code steps inside claude-executor.yml. The non-matching executor is skipped at the job level, before any runner is allocated.