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 }}).