name: 'CodeBoarding Action'
description: 'Review pull request architecture changes or sync a versioned CodeBoarding baseline.'
author: 'CodeBoarding'
branding:
icon: 'git-pull-request'
color: 'blue'
inputs:
mode:
description: 'review posts a PR architecture diff; sync updates the versioned analysis baseline.'
required: false
default: 'review'
llm:
description: 'Required. Where analysis credentials come from: hosted, license, or a provider name (anthropic, aws_bedrock, cerebras, deepseek, glm, google, kimi, litellm, ollama, openai, openrouter, orcarouter, requesty, vercel).'
required: true
license_key:
description: 'CodeBoarding license key. Required by llm: license; optional alongside a provider key.'
required: false
default: ''
# Anthropic - selected by llm: anthropic
anthropic_api_key:
description: 'Anthropic API key, used when llm is anthropic. Sets ANTHROPIC_API_KEY.'
required: false
default: ''
# AWS Bedrock - selected by llm: aws_bedrock
aws_bedrock_api_key:
description: 'AWS Bedrock API key, used when llm is aws_bedrock. Sets AWS_BEARER_TOKEN_BEDROCK.'
required: false
default: ''
aws_bedrock_region:
description: 'AWS Bedrock region, used when llm is aws_bedrock. Sets AWS_DEFAULT_REGION.'
required: false
default: ''
# Cerebras - selected by llm: cerebras
cerebras_api_key:
description: 'Cerebras API key, used when llm is cerebras. Sets CEREBRAS_API_KEY.'
required: false
default: ''
# DeepSeek - selected by llm: deepseek
deepseek_api_key:
description: 'DeepSeek API key, used when llm is deepseek. Sets DEEPSEEK_API_KEY.'
required: false
default: ''
deepseek_base_url:
description: 'DeepSeek endpoint, used when llm is deepseek. Sets DEEPSEEK_BASE_URL.'
required: false
default: ''
# GLM - selected by llm: glm
glm_api_key:
description: 'GLM API key, used when llm is glm. Sets GLM_API_KEY.'
required: false
default: ''
glm_base_url:
description: 'GLM endpoint, used when llm is glm. Sets GLM_BASE_URL.'
required: false
default: ''
# Google Gemini - selected by llm: google
google_api_key:
description: 'Google Gemini API key, used when llm is google. Sets GOOGLE_API_KEY.'
required: false
default: ''
# Kimi - selected by llm: kimi
kimi_api_key:
description: 'Kimi API key, used when llm is kimi. Sets KIMI_API_KEY.'
required: false
default: ''
kimi_base_url:
description: 'Kimi endpoint, used when llm is kimi. Sets KIMI_BASE_URL.'
required: false
default: ''
# LiteLLM - selected by llm: litellm
litellm_api_key:
description: 'LiteLLM API key, used when llm is litellm. Sets LITELLM_API_KEY.'
required: false
default: ''
litellm_base_url:
description: 'LiteLLM endpoint, used when llm is litellm. Sets LITELLM_BASE_URL.'
required: false
default: ''
# Ollama - selected by llm: ollama
ollama_api_key:
description: 'Ollama API key, used when llm is ollama. Sets OLLAMA_API_KEY.'
required: false
default: ''
ollama_base_url:
description: 'Ollama endpoint, used when llm is ollama. Sets OLLAMA_BASE_URL.'
required: false
default: ''
# OpenAI - selected by llm: openai
openai_api_key:
description: 'OpenAI API key, used when llm is openai. Sets OPENAI_API_KEY.'
required: false
default: ''
openai_base_url:
description: 'OpenAI endpoint, used when llm is openai. Sets OPENAI_BASE_URL.'
required: false
default: ''
# OpenRouter - selected by llm: openrouter
openrouter_api_key:
description: 'OpenRouter API key, used when llm is openrouter. Sets OPENROUTER_API_KEY.'
required: false
default: ''
# OrcaRouter - selected by llm: orcarouter
orcarouter_api_key:
description: 'OrcaRouter API key, used when llm is orcarouter. Sets ORCAROUTER_API_KEY.'
required: false
default: ''
# Requesty - selected by llm: requesty
requesty_api_key:
description: 'Requesty API key, used when llm is requesty. Sets REQUESTY_API_KEY.'
required: false
default: ''
requesty_base_url:
description: 'Optional Requesty endpoint, used when llm is requesty. Sets REQUESTY_BASE_URL.'
required: false
default: ''
# Vercel AI Gateway - selected by llm: vercel
vercel_api_key:
description: 'Vercel AI Gateway API key, used when llm is vercel. Sets VERCEL_API_KEY.'
required: false
default: ''
vercel_base_url:
description: 'Vercel AI Gateway endpoint, used when llm is vercel. Sets VERCEL_BASE_URL.'
required: false
default: ''
model:
description: 'Optional model used for both analysis and parsing.'
required: false
default: ''
agent_model:
description: 'Optional analysis-model override. Takes precedence over model.'
required: false
default: ''
parsing_model:
description: 'Optional parsing-model override. Takes precedence over model.'
required: false
default: ''
depth_cap:
description: 'Positive integer analysis depth cap, used for both review and sync, including full fallbacks.'
required: false
default: '2'
github_token:
description: 'Token used for comments and sync delivery.'
required: false
default: ${{ github.token }}
sync_strategy:
description: 'Sync delivery method: push or pull_request.'
required: false
default: 'push'
target_branch:
description: 'Branch updated by sync mode. Defaults to the event branch.'
required: false
default: ''
force_full:
description: 'Run a full sync analysis instead of reusing the committed baseline.'
required: false
default: 'false'
warmstart_retention_days:
description: 'Days to keep the reusable analysis. Only the next run reads it.'
required: false
default: '1'
outputs:
llm_tier:
description: 'Resolved credential tier: hosted, license, byok, or byok+license.'
value: ${{ steps.llm.outputs.tier }}
llm_provider:
description: 'Provider this run used for analysis.'
value: ${{ steps.llm.outputs.provider }}
llm_config_error:
description: 'Stable code for a credential configuration failure, empty when configured.'
value: ${{ steps.llm.outputs.error }}
diagram_md:
description: 'Path to the rendered Mermaid review diagram.'
value: ${{ steps.review_render.outputs.diagram_md }}
n_changed:
description: 'Number of changed components in review mode.'
value: ${{ steps.review_render.outputs.n_changed }}
truncated:
description: 'Whether the review graph was reduced to fit GitHub limits.'
value: ${{ steps.review_render.outputs.truncated }}
review_artifact_url:
description: 'URL of the uploaded review analysis artifact.'
value: ${{ steps.upload_review_artifact_dotcom.outputs.artifact-url }}
seed_source:
description: 'Which state the review head analysis grew from: pr-chain or base.'
value: ${{ steps.review_analyze.outputs.seed_source }}
merge_base_sha:
description: 'Merge base used as the review comparison baseline.'
value: ${{ steps.guard.outputs.merge_base_sha }}
analysis_mode:
description: 'Whether sync used incremental or full analysis.'
value: ${{ steps.sync_analyze.outputs.analysis_mode }}
files_written:
description: 'Number of persisted analysis artifacts produced by sync.'
value: ${{ steps.sync_commit.outputs.files_written }}
committed:
description: 'Whether sync delivered a baseline commit.'
value: ${{ steps.sync_commit.outputs.committed }}
sync_pr_url:
description: 'Rolling sync PR URL when sync_strategy is pull_request.'
value: ${{ steps.sync_commit.outputs.sync_pr_url }}
sync_pr_number:
description: 'Rolling sync PR number when sync_strategy is pull_request.'
value: ${{ steps.sync_commit.outputs.sync_pr_number }}
runs:
using: 'composite'
steps:
- name: Resolve event
id: guard
shell: bash
env:
GH_TOKEN: ${{ inputs.github_token }}
GH_ENTERPRISE_TOKEN: ${{ inputs.github_token }}
GH_HOST: ${{ github.server_url }}
MODE: ${{ inputs.mode }}
EVENT: ${{ github.event_name }}
REF_NAME: ${{ github.ref_name }}
REF_TYPE: ${{ github.ref_type }}
EVENT_SHA: ${{ github.sha }}
HEAD_AUTHOR_EMAIL: ${{ github.event.head_commit.author.email }}
TARGET_BRANCH_INPUT: ${{ inputs.target_branch }}
SYNC_STRATEGY: ${{ inputs.sync_strategy }}
COMMENT_BODY: ${{ github.event.comment.body }}
AUTHOR_ASSOCIATION: ${{ github.event.comment.author_association }}
ISSUE_PR_URL: ${{ github.event.issue.pull_request.url }}
EVENT_PR_NUMBER: ${{ github.event.pull_request.number }}
PULL_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PULL_BASE_SHA: ${{ github.event.pull_request.base.sha }}
PULL_BASE_REF: ${{ github.event.pull_request.base.ref }}
PULL_HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
PULL_BASE_REPO: ${{ github.event.pull_request.base.repo.full_name }}
REPOSITORY: ${{ github.repository }}
run: "$GITHUB_ACTION_PATH/scripts/action/guard.sh"
- name: Acknowledge review command
if: steps.guard.outputs.skip != 'true' && steps.guard.outputs.event == 'issue_comment'
continue-on-error: true
shell: bash
env:
GH_TOKEN: ${{ inputs.github_token }}
GH_ENTERPRISE_TOKEN: ${{ inputs.github_token }}
GITHUB_SERVER_URL: ${{ github.server_url }}
REPOSITORY: ${{ github.repository }}
COMMENT_ID: ${{ github.event.comment.id }}
run: GH_HOST="${GITHUB_SERVER_URL#*://}" gh api -X POST "repos/${REPOSITORY}/issues/comments/${COMMENT_ID}/reactions" -f content=eyes >/dev/null
# Ahead of the credential check rather than after the checkout, because that check
# is a Python program. Hosted runners ship a system python3 and would not notice;
# a self-hosted runner without one would fail a perfectly valid configuration. It
# costs a couple of seconds and still leaves the check ahead of the engine install,
# which is the part worth failing before.
- name: Setup Python
if: steps.guard.outputs.skip != 'true'
uses: actions/setup-python@v5
with:
python-version: '3.12'
# Resolved before the checkout and the engine install, so a workflow that names a
# provider it has no key for fails within seconds, naming the input to fix, instead
# of running to completion on credentials it never asked for. Nothing downstream
# re-decides this: the plan it writes is what the analysis runs with.
- name: Check LLM configuration
id: llm
if: steps.guard.outputs.skip != 'true'
continue-on-error: true
shell: bash
env:
ACTION_PATH: ${{ github.action_path }}
CB_IN_LLM: ${{ inputs.llm }}
CB_IN_LICENSE_KEY: ${{ inputs.license_key }}
CB_IN_ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}
CB_IN_AWS_BEDROCK_API_KEY: ${{ inputs.aws_bedrock_api_key }}
CB_IN_AWS_BEDROCK_REGION: ${{ inputs.aws_bedrock_region }}
CB_IN_CEREBRAS_API_KEY: ${{ inputs.cerebras_api_key }}
CB_IN_DEEPSEEK_API_KEY: ${{ inputs.deepseek_api_key }}
CB_IN_DEEPSEEK_BASE_URL: ${{ inputs.deepseek_base_url }}
CB_IN_GLM_API_KEY: ${{ inputs.glm_api_key }}
CB_IN_GLM_BASE_URL: ${{ inputs.glm_base_url }}
CB_IN_GOOGLE_API_KEY: ${{ inputs.google_api_key }}
CB_IN_KIMI_API_KEY: ${{ inputs.kimi_api_key }}
CB_IN_KIMI_BASE_URL: ${{ inputs.kimi_base_url }}
CB_IN_LITELLM_API_KEY: ${{ inputs.litellm_api_key }}
CB_IN_LITELLM_BASE_URL: ${{ inputs.litellm_base_url }}
CB_IN_OLLAMA_API_KEY: ${{ inputs.ollama_api_key }}
CB_IN_OLLAMA_BASE_URL: ${{ inputs.ollama_base_url }}
CB_IN_OPENAI_API_KEY: ${{ inputs.openai_api_key }}
CB_IN_OPENAI_BASE_URL: ${{ inputs.openai_base_url }}
CB_IN_OPENROUTER_API_KEY: ${{ inputs.openrouter_api_key }}
CB_IN_ORCAROUTER_API_KEY: ${{ inputs.orcarouter_api_key }}
CB_IN_REQUESTY_API_KEY: ${{ inputs.requesty_api_key }}
CB_IN_REQUESTY_BASE_URL: ${{ inputs.requesty_base_url }}
CB_IN_VERCEL_API_KEY: ${{ inputs.vercel_api_key }}
CB_IN_VERCEL_BASE_URL: ${{ inputs.vercel_base_url }}
run: "$GITHUB_ACTION_PATH/scripts/action/verify-credentials.sh"
# Said in the pull request, not only in the Actions tab, because the person who has
# to add the secret is the one reading the pull request.
- name: Report LLM configuration failure
if: steps.guard.outputs.skip != 'true' && steps.llm.outputs.error != '' && steps.guard.outputs.mode == 'review' && steps.guard.outputs.pr_number != ''
continue-on-error: true
uses: marocchino/sticky-pull-request-comment@v2
with:
header: ${{ steps.guard.outputs.comment_id }}
number: ${{ steps.guard.outputs.pr_number }}
GITHUB_TOKEN: ${{ inputs.github_token }}
message: |
### CodeBoarding review - not configured
${{ steps.llm.outputs.details }}
No analysis ran, and no CodeBoarding hosted usage was consumed.
run [${{ github.run_id }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) - attempt ${{ github.run_attempt }}
# Fires on a refusal AND on the check failing to reach a verdict at all. The step
# above is `continue-on-error` so the refusal can be reported before the job dies,
# which also means a crash in it -- an unreadable provider table, a missing python3 --
# would leave `error` empty and let the run carry on to the checkout and the engine
# install. Checking the outcome as well keeps "credentials are decided before anything
# expensive happens" true when the deciding itself breaks.
- name: Stop on LLM configuration failure
if: steps.guard.outputs.skip != 'true' && (steps.llm.outputs.error != '' || steps.llm.outcome != 'success')
shell: bash
env:
CB_ERROR: ${{ steps.llm.outputs.error }}
run: |
if [ -n "$CB_ERROR" ]; then
echo "CodeBoarding did not run: $CB_ERROR" >&2
else
echo "::error title=CodeBoarding LLM configuration::The credential check did not complete, so no analysis was attempted. See the step above."
fi
exit 1
- name: Post review progress
if: steps.guard.outputs.skip != 'true' && steps.guard.outputs.mode == 'review'
continue-on-error: true
uses: marocchino/sticky-pull-request-comment@v2
with:
header: ${{ steps.guard.outputs.comment_id }}
number: ${{ steps.guard.outputs.pr_number }}
GITHUB_TOKEN: ${{ inputs.github_token }}
message: |
### CodeBoarding review · analyzing…
⏳ CodeBoarding is analyzing this pull request's architecture changes.
Open it in [CodeBoarding](https://app.codeboarding.org/${{ github.repository }}/pull/${{ steps.guard.outputs.pr_number }}?utm_source=github&utm_medium=pr_comment&utm_campaign=gh_action) meanwhile: the files, comments and review are there already, and the diff appears when the run finishes.
run [${{ github.run_id }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) · attempt ${{ github.run_attempt }}
- name: Checkout analysis target
if: steps.guard.outputs.skip != 'true'
uses: actions/checkout@v4
with:
repository: ${{ steps.guard.outputs.checkout_repo }}
ref: ${{ steps.guard.outputs.checkout_ref }}
token: ${{ inputs.github_token }}
fetch-depth: 1
persist-credentials: false
path: .codeboarding-target
- name: Setup Java for CodeBoarding
if: steps.guard.outputs.skip != 'true'
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: '21'
- name: Install CodeBoarding
if: steps.guard.outputs.skip != 'true'
shell: bash
run: |
python -m pip install --disable-pip-version-check 'codeboarding==0.14.5'
# Fail here, with the reason, rather than mid-analysis with a traceback:
# pinning a release does not pin what its dependencies resolve to. Run
# the console script, not `python -c`, which would put the analyzed
# repository's own main.py ahead of the CLI's on sys.path.
codeboarding --help >/dev/null || {
echo "::error::The installed CodeBoarding CLI cannot start; a dependency it imports is missing."
exit 1
}
- name: Configure analysis authentication
if: steps.guard.outputs.skip != 'true'
shell: bash
env:
ACTION_PATH: ${{ github.action_path }}
run: "$GITHUB_ACTION_PATH/scripts/action/configure-auth.sh"
- name: Resolve analysis identity
id: state
if: steps.guard.outputs.skip != 'true'
continue-on-error: true
shell: bash
env:
CHECKOUT_DIR: ${{ github.workspace }}/.codeboarding-target
MERGE_BASE_SHA: ${{ steps.guard.outputs.merge_base_sha }}
PR_NUMBER: ${{ steps.guard.outputs.pr_number }}
IS_FORK: ${{ steps.guard.outputs.is_fork }}
LLM_PROVIDER: ${{ steps.llm.outputs.provider }}
BACKEND_ID: ${{ steps.llm.outputs.backend_id }}
DEPTH_CAP: ${{ inputs.depth_cap }}
MODEL: ${{ inputs.model }}
AGENT_MODEL_INPUT: ${{ inputs.agent_model }}
PARSING_MODEL_INPUT: ${{ inputs.parsing_model }}
run: "$GITHUB_ACTION_PATH/scripts/action/state-names.sh"
# Both lookups are best effort. Without them the run derives the base from
# the committed baseline and the head from the base, which is what every run
# did before any of this existed.
- name: Fetch published base analysis
id: fetch_base
if: steps.guard.outputs.skip != 'true' && steps.guard.outputs.mode == 'review' && steps.state.outputs.base_name != '' && github.server_url == 'https://github.com'
continue-on-error: true
shell: bash
env:
GH_TOKEN: ${{ inputs.github_token }}
GH_ENTERPRISE_TOKEN: ${{ inputs.github_token }}
GH_HOST: ${{ github.server_url }}
REPOSITORY: ${{ github.repository }}
ARTIFACT_NAME: ${{ steps.state.outputs.base_name }}
DEST: ${{ runner.temp }}/cb-state/${{ github.action }}/base
# Must match the review retention below: a review references a base by
# id for its whole life, so a base with less than that left is renewed.
RENEW_WITHIN_DAYS: '14'
run: "$GITHUB_ACTION_PATH/scripts/action/fetch-state.sh"
- name: Fetch this pull request's last analysis
if: steps.guard.outputs.skip != 'true' && steps.guard.outputs.mode == 'review' && steps.state.outputs.warmstart_name != '' && github.server_url == 'https://github.com'
continue-on-error: true
shell: bash
env:
GH_TOKEN: ${{ inputs.github_token }}
GH_ENTERPRISE_TOKEN: ${{ inputs.github_token }}
GH_HOST: ${{ github.server_url }}
REPOSITORY: ${{ github.repository }}
ARTIFACT_NAME: ${{ steps.state.outputs.warmstart_name }}
DEST: ${{ runner.temp }}/cb-state/${{ github.action }}/warmstart
run: "$GITHUB_ACTION_PATH/scripts/action/fetch-state.sh"
- name: Analyze baseline
id: sync_analyze
if: steps.guard.outputs.skip != 'true' && steps.guard.outputs.mode == 'sync'
shell: bash
env:
ACTION_PATH: ${{ github.action_path }}
ANALYSIS_KIND: sync
CHECKOUT_DIR: ${{ github.workspace }}/.codeboarding-target
STAGE_DIR: ${{ runner.temp }}/cb-state/${{ github.action }}/out
FORCE_FULL: ${{ inputs.force_full }}
DEPTH_CAP: ${{ inputs.depth_cap }}
MODEL: ${{ inputs.model }}
AGENT_MODEL_INPUT: ${{ inputs.agent_model }}
PARSING_MODEL_INPUT: ${{ inputs.parsing_model }}
run: '"$GITHUB_ACTION_PATH/scripts/action/with-auth.sh" "$GITHUB_ACTION_PATH/scripts/action/analyze.sh"'
- name: Deliver baseline
id: sync_commit
if: steps.guard.outputs.skip != 'true' && steps.guard.outputs.mode == 'sync'
shell: bash
env:
ACTION_PATH: ${{ github.action_path }}
ANALYSIS_DIR: ${{ steps.sync_analyze.outputs.analysis_dir }}
CHECKOUT_DIR: ${{ github.workspace }}/.codeboarding-target
TARGET_BRANCH: ${{ steps.guard.outputs.target_branch }}
SYNC_BRANCH_START_SHA: ${{ steps.guard.outputs.sync_branch_start_sha }}
SYNC_STRATEGY: ${{ inputs.sync_strategy }}
GITHUB_TOKEN: ${{ inputs.github_token }}
GH_TOKEN: ${{ inputs.github_token }}
GH_ENTERPRISE_TOKEN: ${{ inputs.github_token }}
GH_HOST: ${{ github.server_url }}
GITHUB_SERVER_URL: ${{ github.server_url }}
REPOSITORY: ${{ github.repository }}
run: "$GITHUB_ACTION_PATH/scripts/action/deliver-sync.sh"
# Named for the commit pull requests branch from, so a review that forks there
# finds this graph and skips analyzing the base entirely. Published under both
# the analyzed commit and the baseline commit written on top, because a pull
# request opened either side of that commit has a different merge base.
- name: Publish baseline analysis
if: steps.guard.outputs.skip != 'true' && steps.guard.outputs.mode == 'sync' && steps.sync_commit.outputs.baseline_sha != '' && steps.state.outputs.cfg_hash != '' && github.server_url == 'https://github.com'
continue-on-error: true
uses: actions/upload-artifact@v4
with:
name: codeboarding-base-${{ steps.state.outputs.cfg_hash }}-${{ steps.sync_commit.outputs.baseline_sha }}
path: ${{ runner.temp }}/cb-state/${{ github.action }}/out/base
# Longer than a review's 14 days on purpose, and the margin is the whole
# point: correctness comes from the renewal check, which republishes any
# base with less than a review's lifetime left, while this surplus
# decides how often that fires. Equal lifetimes renew on every run and
# publish a base per run rather than per merge base — more storage, not
# less. 30 leaves a 16-day reuse window.
retention-days: 30
if-no-files-found: ignore
- name: Publish baseline analysis for the analyzed commit
if: steps.guard.outputs.skip != 'true' && steps.guard.outputs.mode == 'sync' && steps.sync_commit.outputs.analyzed_sha != '' && steps.sync_commit.outputs.analyzed_sha != steps.sync_commit.outputs.baseline_sha && steps.state.outputs.cfg_hash != '' && github.server_url == 'https://github.com'
continue-on-error: true
uses: actions/upload-artifact@v4
with:
name: codeboarding-base-${{ steps.state.outputs.cfg_hash }}-${{ steps.sync_commit.outputs.analyzed_sha }}
path: ${{ runner.temp }}/cb-state/${{ github.action }}/out/base
# Longer than a review's 14 days on purpose, and the margin is the whole
# point: correctness comes from the renewal check, which republishes any
# base with less than a review's lifetime left, while this surplus
# decides how often that fires. Equal lifetimes renew on every run and
# publish a base per run rather than per merge base — more storage, not
# less. 30 leaves a 16-day reuse window.
retention-days: 30
if-no-files-found: ignore
- name: Write sync summary
if: always() && steps.guard.outputs.skip != 'true' && steps.guard.outputs.mode == 'sync'
shell: bash
env:
MODE: ${{ steps.sync_analyze.outputs.analysis_mode }}
COMMITTED: ${{ steps.sync_commit.outputs.committed }}
FILES: ${{ steps.sync_commit.outputs.files_written }}
STRATEGY: ${{ inputs.sync_strategy }}
PR_URL: ${{ steps.sync_commit.outputs.sync_pr_url }}
run: "$GITHUB_ACTION_PATH/scripts/action/sync-summary.sh"
- name: Analyze pull request
id: review_analyze
if: steps.guard.outputs.skip != 'true' && steps.guard.outputs.mode == 'review'
shell: bash
env:
ACTION_PATH: ${{ github.action_path }}
ANALYSIS_KIND: review
CHECKOUT_DIR: ${{ github.workspace }}/.codeboarding-target
REVIEW_BASE_SHA: ${{ steps.guard.outputs.merge_base_sha }}
REVIEW_HEAD_SHA: ${{ steps.guard.outputs.head_sha }}
REVIEW_BASE_REPO: ${{ steps.guard.outputs.base_repo }}
PR_NUMBER: ${{ steps.guard.outputs.pr_number }}
BASE_DIR: ${{ runner.temp }}/cb-state/${{ github.action }}/base
WARMSTART_DIR: ${{ runner.temp }}/cb-state/${{ github.action }}/warmstart
RENEW_BASE: ${{ steps.fetch_base.outputs.renew }}
STAGE_DIR: ${{ runner.temp }}/cb-state/${{ github.action }}/out
ENGINE_VERSION: ${{ steps.state.outputs.engine_version }}
CFG_HASH: ${{ steps.state.outputs.cfg_hash }}
GIT_TOKEN: ${{ inputs.github_token }}
GITHUB_SERVER_URL: ${{ github.server_url }}
DEPTH_CAP: ${{ inputs.depth_cap }}
MODEL: ${{ inputs.model }}
AGENT_MODEL_INPUT: ${{ inputs.agent_model }}
PARSING_MODEL_INPUT: ${{ inputs.parsing_model }}
run: '"$GITHUB_ACTION_PATH/scripts/action/with-auth.sh" "$GITHUB_ACTION_PATH/scripts/action/analyze.sh"'
# Published before the review is rendered: the analysis is the expensive part,
# so a later rendering or posting failure must not throw it away. Only the
# next run reads this, hence the short retention.
- name: Publish this analysis for the next run
if: steps.guard.outputs.skip != 'true' && steps.guard.outputs.mode == 'review' && steps.state.outputs.warmstart_name != '' && github.server_url == 'https://github.com'
continue-on-error: true
uses: actions/upload-artifact@v4
with:
name: ${{ steps.state.outputs.warmstart_name }}
path: ${{ runner.temp }}/cb-state/${{ github.action }}/out/warmstart
retention-days: ${{ inputs.warmstart_retention_days }}
if-no-files-found: ignore
# Only a trusted run publishes a base graph: it is named for a commit, so
# every pull request forking there would read it, and a fork's analysis must
# never be what they read.
- name: Publish base analysis
id: publish_base
if: steps.guard.outputs.skip != 'true' && steps.guard.outputs.mode == 'review' && steps.review_analyze.outputs.publish_base == 'true' && steps.guard.outputs.is_fork != 'true' && steps.state.outputs.base_name != '' && github.server_url == 'https://github.com'
continue-on-error: true
uses: actions/upload-artifact@v4
with:
name: ${{ steps.state.outputs.base_name }}
path: ${{ runner.temp }}/cb-state/${{ github.action }}/out/base
# Longer than a review's 14 days on purpose, and the margin is the whole
# point: correctness comes from the renewal check, which republishes any
# base with less than a review's lifetime left, while this surplus
# decides how often that fires. Equal lifetimes renew on every run and
# publish a base per run rather than per merge base — more storage, not
# less. 30 leaves a 16-day reuse window.
retention-days: 30
if-no-files-found: ignore
- name: Render review diagram
id: review_render
if: steps.guard.outputs.skip != 'true' && steps.guard.outputs.mode == 'review'
shell: bash
env:
ACTION_PATH: ${{ github.action_path }}
BASE_ANALYSIS_PATH: ${{ steps.review_analyze.outputs.base_analysis_path }}
HEAD_ANALYSIS_PATH: ${{ steps.review_analyze.outputs.analysis_path }}
run: "$GITHUB_ACTION_PATH/scripts/action/render-review.sh"
- name: Build review artifact
id: review_artifact
if: steps.guard.outputs.skip != 'true' && steps.guard.outputs.mode == 'review'
shell: bash
env:
ANALYSIS_PATH: ${{ steps.review_analyze.outputs.analysis_path }}
BASE_ARTIFACT_NAME: ${{ steps.state.outputs.base_name }}
BASE_ARTIFACT_ID: ${{ steps.publish_base.outputs.artifact-id || steps.fetch_base.outputs.artifact_id }}
BASE_ANALYSIS_PATH: ${{ steps.review_analyze.outputs.base_analysis_path }}
INLINE_BASE: ${{ steps.publish_base.outputs.artifact-id == '' && steps.fetch_base.outputs.artifact_id == '' }}
ANALYSIS_MODE: ${{ steps.review_analyze.outputs.analysis_mode }}
BASE_SHA: ${{ steps.guard.outputs.base_sha }}
MERGE_BASE_SHA: ${{ steps.guard.outputs.merge_base_sha }}
MERGE_BASE_RESOLVED: ${{ steps.guard.outputs.merge_base_resolved }}
HEAD_SHA: ${{ steps.guard.outputs.head_sha }}
PR_NUMBER: ${{ steps.guard.outputs.pr_number }}
SEED_SOURCE: ${{ steps.review_analyze.outputs.seed_source }}
CHAIN_DEPTH: ${{ steps.review_analyze.outputs.chain_depth }}
ANALYSED_FILES_CHANGED: ${{ steps.review_render.outputs.analysed_files_changed }}
run: "$GITHUB_ACTION_PATH/scripts/action/build-review-artifact.sh"
- name: Upload review artifact
id: upload_review_artifact_dotcom
if: steps.guard.outputs.skip != 'true' && steps.guard.outputs.mode == 'review' && github.server_url == 'https://github.com'
uses: actions/upload-artifact@v4
with:
name: codeboarding-review-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ steps.review_artifact.outputs.artifact_dir }}
if-no-files-found: error
# The dominant storage cost, since it is the one kept for weeks. A pull
# request open longer than this loses its rendered analysis until someone
# asks for it again, which costs one incremental over the pull request:
# the base graph is still published and needs no re-analysis.
retention-days: 14
- name: Build review comment
id: review_body
if: steps.guard.outputs.skip != 'true' && steps.guard.outputs.mode == 'review'
shell: bash
env:
DIAGRAM: ${{ steps.review_render.outputs.diagram_md }}
N_CHANGED: ${{ steps.review_render.outputs.n_changed }}
ARTIFACT_URL: ${{ steps.upload_review_artifact_dotcom.outputs.artifact-url }}
PR_NUMBER: ${{ steps.guard.outputs.pr_number }}
BEHIND_BY: ${{ steps.guard.outputs.behind_by }}
BASE_REF: ${{ steps.guard.outputs.base_ref }}
MERGE_BASE_RESOLVED: ${{ steps.guard.outputs.merge_base_resolved }}
HEAD_SHA: ${{ steps.guard.outputs.head_sha }}
# Analysed files whose content hash differs between base and head, from the render step.
ANALYSED_FILES_CHANGED: ${{ steps.review_render.outputs.analysed_files_changed }}
run: "$GITHUB_ACTION_PATH/scripts/action/build-review-comment.sh"
- name: Post review comment
id: review_comment
if: steps.guard.outputs.skip != 'true' && steps.guard.outputs.mode == 'review'
uses: marocchino/sticky-pull-request-comment@v2
with:
header: ${{ steps.guard.outputs.comment_id }}
number: ${{ steps.guard.outputs.pr_number }}
GITHUB_TOKEN: ${{ inputs.github_token }}
path: ${{ steps.review_body.outputs.path }}
# The same body, on the run page. Job summaries and logs are explicitly excluded from
# the artifact storage allowance, and the cap is 1MiB per step against a diagram
# measured in single-digit kilobytes, so this is free in both senses. It earns its
# place on the triggers where the comment cannot be the record: a manual dispatch, or
# any run whose comment write fails on a token without `pull-requests: write`.
- name: Add the review to the job summary
if: steps.guard.outputs.skip != 'true' && steps.guard.outputs.mode == 'review' && steps.review_body.outputs.path != ''
continue-on-error: true
shell: bash
env:
BODY: ${{ steps.review_body.outputs.path }}
run: cat "$BODY" >> "$GITHUB_STEP_SUMMARY"
# Skipped when the run stopped on a credential problem: that path already replaced
# this same sticky comment with the input and secret to fix, and "see the workflow
# logs" posted over the top of it would send the reader hunting for what they had
# just been told.
- name: Post review failure
if: failure() && steps.guard.outputs.skip != 'true' && steps.guard.outputs.mode == 'review' && steps.review_comment.outcome != 'success' && steps.llm.outputs.error == ''
continue-on-error: true
uses: marocchino/sticky-pull-request-comment@v2
with:
header: ${{ steps.guard.outputs.comment_id }}
number: ${{ steps.guard.outputs.pr_number }}
GITHUB_TOKEN: ${{ inputs.github_token }}
message: |
### CodeBoarding review · failed
${{ steps.review_analyze.outputs.failure_reason }}
See the [workflow logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}).