One GitHub Action with two modes:
review(default) compares a pull request's head with its merge base, posts an inline Mermaid architecture diff, and uploads both analyses as a workflow artifact.syncupdates the versioned analysis state used by future incremental runs. By default it saves to a branch of its own, so code branches are never written.
The action is a thin wrapper around the CodeBoarding CLI. Analysis logic and provider defaults live in Core, not in this repository.
CodeBoarding · Website · Examples · VS Code extension · Discord
CodeBoarding runs from one workflow, .github/workflows/codeboarding.yml, with a review job and a sync job. Each job asks only for the permissions it needs. Replace main with your default branch:
name: CodeBoarding
on:
pull_request:
types: [opened, reopened, synchronize]
issue_comment:
types: [created]
push:
branches: [main]
permissions: {}
# One review at a time per pull request, and one sync at a time per branch. Two
# pushes in quick succession would otherwise analyze concurrently, both starting
# from the same older analysis instead of the newer one continuing from its
# predecessor, and both writing the one sticky comment, so the run for the older
# commit could win. Queue rather than cancel, so a /codeboarding command waits for
# a running review instead of killing it.
concurrency:
group: codeboarding-${{ github.event.pull_request.number || github.event.issue.number || github.ref_name }}
cancel-in-progress: false
jobs:
review:
if: >
(github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.repository) ||
(github.event_name == 'issue_comment' && github.event.issue.pull_request != null &&
startsWith(github.event.comment.body, '/codeboarding') &&
contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association))
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
contents: read
actions: read # download the analysis an earlier run published
pull-requests: write
issues: write
id-token: write
steps:
- uses: CodeBoarding/CodeBoarding-action@v1
with:
llm: hosted # or license, or a provider name -- see Authentication
sync:
if: github.event_name == 'push'
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
contents: write
actions: read # catch up from an analysis a review saved
id-token: write
steps:
- uses: CodeBoarding/CodeBoarding-action@v1
with:
mode: sync
llm: hostedBoth jobs use the default codeboarding_analysis_location, so review always reads the analysis where sync saves it. If you set it, set it in both jobs. Keep both jobs in this one file: a repository running CodeBoarding from more than one workflow file is an older setup, which the CodeBoarding dashboard flags and offers to fold into one.
The review job's automatic runs review both draft and non-draft pull requests and update one sticky CodeBoarding review comment. Opening, reopening, or pushing a commit runs analysis; changing only the draft state does not. A trusted repository owner, member, or collaborator can comment /codeboarding to analyze the current PR head again, including on fork PRs; every command creates a new result comment.
synchronize re-runs the review on every push to the branch. Each of those runs covers only the commits pushed since the previous one, so a push costs a fraction of a first analysis — and a pushed commit is the only thing that builds the reusable analysis, since GitHub gives comment-triggered runs a read-only cache. Drop synchronize from the list if you would rather spend one analysis per pull request than one per push.
Keep the concurrency block if you keep synchronize: it is what makes a push continue from the push before it, and what stops a slower run for an older commit from overwriting the review comment for a newer one. Set cancel-in-progress: true instead to abandon a superseded run rather than queue it, which costs less when branches are pushed to rapidly, at the price of no analysis for the commits in between.
/codeboarding analyzes the current head, reusing this pull request's previous analysis when there is one. It takes no arguments.
The action checks out and analyzes the exact PR head SHA, and compares it with the PR's merge base — the commit the branch forked from, which is what GitHub's own "Files changed" tab uses. Commits pushed to the base branch after the fork point are therefore not reported as this PR's changes; the comment notes how far behind the branch is instead. It does not commit generated files to either branch.
Automatic fork runs are skipped because the pull_request event does not receive hosted OIDC credentials. A trusted /codeboarding command runs the released action code from the base repository and checks the fork's source into a separate analysis directory; it never executes an action definition from the fork with privileged credentials.
A review run uploads codeboarding-review-<run_id>-<attempt>:
| File | Contents |
|---|---|
analysis.json |
the head analysis, at head_sha |
health_report.json |
the head's health findings, when the engine produced any |
metadata.json |
which commits that graph describes, and the name of the base artifact — see the field list |
The graph it was compared against is published separately, named for the commit
it describes, because the merge base rarely changes during a pull request and a
copy per run would store the same bytes over and over. Read
metadata.base_artifact and fetch that artifact by name; the graph inside it is
analysis.json, and metadata.base_artifact_id records exactly which artifact
this review used, since two can share a name and disagree.
Every bundle the action publishes carries a metadata.json with a kind of
review, base or warmstart. Check it rather than inferring from the
payload: a base bundle is otherwise indistinguishable from a head one.
The action requests 14-day retention for it; a repository or organisation policy can shorten that, so treat an artifact's own expired flag as the truth rather than any fixed window. When a pull request outlives its review artifact, /codeboarding regenerates it at the cost of one incremental over the pull request — the base graph is kept longer and does not need re-analyzing.
Each review seeds the head analysis from this pull request's own previous run, so a run only covers the commits pushed since it. That analysis is published as a workflow artifact named for the pull request, and the next run fetches it by name. With nothing to fetch, the run seeds from the merge base's analysis instead, which sync publishes for every commit it processes.
Everything here is best-effort. A missing artifact, an expired one, or a token without actions: read all mean the run derives from the base instead — which is what every run did before any of this existed. Stored analyses are read only when the run that produced them worked on this repository's own code, so a fork cannot leave one behind for a later run to load. On GitHub Enterprise Server, where actions/upload-artifact@v4 is unsupported, nothing is stored or reused and every review derives from the committed baseline.
Because artifacts are readable and writable from every trigger, all paths behave the same: an automatic run, a /codeboarding command and a manual dispatch each reuse the previous analysis and publish their own. A stored analysis is discarded, and the head re-derived from the base, whenever the pinned CodeBoarding version, .codeboardingignore, the model selection, the analysis depth or the merge base changes — or when the base graph it grew from is not the one this run compares against.
Fork pull requests never carry an analysis forward. They are reviewed on request, each review starts from the base, and nothing they produce is read by a run on this repository's own code: untrusted code must not shape state that a later run loads. The action also accepts pull_request_target, which runs on the base branch ref and lets both share one chain; that trigger has its own trade-offs (a PR that adds this workflow will not run it until merged, and the fork gate becomes load-bearing), so pull_request remains the recommended default.
A pull request whose changed files are all outside what the engine analyses (docs, configuration, CI, tests the ignore file excludes, or a language the engine does not read) cannot have moved the architecture. The action says so by comparing the two analyses it already holds: the engine records a content hash for every file a component owns, and the review counts the analysed files whose hash differs between the base and the head, including files added or removed. At zero, no byte of analysed code changed, and the comment says (no analysed file changed) after the component count. If the count of changed components is not zero at the same time, the analysis grouped the same code differently, and the comment says that too rather than presenting it as a change. The review artifact's metadata.json carries the count as analysed_files_changed (a string, like every other field there, and unknown when either analysis has a file without a hash).
Every review comment ends with a machine-readable HTML comment, <!-- codeboarding: platform_url=… changed=… analysed_files_changed=… head=… -->, for readers that should not parse the prose or the diagram. The progress comment carries the platform link from the start, so a pull request can be opened there while the run is still going.
The llm input is required and says where analysis credentials come from. There are
three answers, and the action never picks one for you:
with:
llm: hosted # CodeBoarding's free tier with:
llm: license # a CodeBoarding plan
license_key: ${{ secrets.CODEBOARDING_LICENSE }} with:
llm: anthropic # your own provider key
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}hosted and license run through CodeBoarding's proxy and need id-token: write, which
mints short-lived credentials per request and stores no LLM secret in your repository. A
provider key is used directly and needs no OIDC permission.
An empty value is never a fallback. If you name a provider and its key is missing -- because the secret does not exist yet, or is misspelt — the run fails in its first seconds and says which input and which secret to fix. It does not quietly analyze on CodeBoarding's hosted tier instead. That was the old behaviour, and it meant a repository could report an Anthropic review that Anthropic never produced.
The same rule makes the combinations explicit rather than order-dependent:
| Workflow says | Result |
|---|---|
| nothing | refused: llm is required |
llm: hosted |
the free tier |
llm: hosted + any provider key |
refused: pick one |
llm: hosted + license_key |
refused: use llm: license |
llm: license without license_key |
refused: names the secret to add |
llm: anthropic + anthropic_api_key |
Anthropic, directly |
llm: anthropic, key empty or absent |
refused: names the input and the secret |
llm: anthropic + openai_api_key |
refused: a second provider's key |
llm: anthropic + key + license_key |
Anthropic, on a CodeBoarding plan |
A licence alongside your own key is deliberately allowed: it says "my CodeBoarding plan, my own tokens". Your key always wins. A direct provider call never reaches CodeBoarding, so the licence is recorded and reported but not spent, and nothing meters that combination today. The job summary says so on every run, rather than leaving you to infer it from the tier name.
Each provider has its own inputs, so which key a workflow uses is readable from the file without knowing any precedence rules.
llm |
Provider | Inputs | Needs at least one of |
|---|---|---|---|
anthropic |
Anthropic | anthropic_api_key |
anthropic_api_key |
aws_bedrock |
AWS Bedrock | aws_bedrock_api_key, aws_bedrock_region |
aws_bedrock_api_key |
cerebras |
Cerebras | cerebras_api_key |
cerebras_api_key |
deepseek |
DeepSeek | deepseek_api_key, deepseek_base_url |
deepseek_api_key or deepseek_base_url |
glm |
GLM | glm_api_key, glm_base_url |
glm_api_key or glm_base_url |
google |
Google Gemini | google_api_key |
google_api_key |
kimi |
Kimi | kimi_api_key, kimi_base_url |
kimi_api_key or kimi_base_url |
litellm |
LiteLLM | litellm_api_key, litellm_base_url |
litellm_base_url |
ollama |
Ollama | ollama_api_key, ollama_base_url |
ollama_base_url |
openai |
OpenAI | openai_api_key, openai_base_url |
openai_api_key or openai_base_url |
openrouter |
OpenRouter | openrouter_api_key |
openrouter_api_key |
orcarouter |
OrcaRouter | orcarouter_api_key |
orcarouter_api_key |
requesty |
Requesty | requesty_api_key, requesty_base_url |
requesty_api_key |
vercel |
Vercel AI Gateway | vercel_api_key, vercel_base_url |
vercel_api_key or vercel_base_url |
Each provider has exactly one accepted spelling, and its inputs are named after it, so
llm: X always pairs with X_api_key. There are no aliases: a second spelling is another
thing to document and keep in step, and an unrecognised value is refused with the accepted
list. ollama and litellm are selected by their endpoint rather than a key, which is why
a key alone does not configure them — that mirrors how Core itself decides.
This table is generated from scripts/action/supported-providers.json,
which mirrors the CodeBoarding release this action pins. tests/test_provider_table_drift.py
installs that release in CI and fails if the two disagree, so a provider cannot be added
to Core and silently stay unreachable here.
Every run reports what it resolved, so the answer never has to be inferred from behaviour:
- outputs
llm_tier(hosted,license,byok,byok+license),llm_provider(empty on the hosted tiers, where the upstream is ours rather than yours), andllm_config_error(empty when configured); - a job-summary table naming the tier and provider;
- on a configuration failure, an error annotation and — in review mode — a pull request comment with the fix, so the person who has to add the secret sees it where they are.
- when the provider rejects the key or its quota runs out during the analysis, the run fails with an error annotation saying which, and in review mode the failure comment says it too. No partial analysis is published.
All model inputs are optional and are passed directly to Core without action-side validation:
with:
model: google/gemini-3.7-flash
agent_model: anthropic/claude-sonnet-4 # optional analysis-only override
parsing_model: openai/gpt-5-mini # optional parsing-only overridePrecedence is intentionally simple:
| Work | Resolution |
|---|---|
| Analysis | agent_model → model → active provider's Core default |
| Parsing | parsing_model → model → active provider's Core default |
Set only model when both jobs should use the same model. Set either specialized input only when that job needs a different model. Model identifiers are not secrets and can be stored in GitHub repository variables.
Sync mode saves Core's persisted incremental-analysis state under .codeboarding/:
analysis.jsonfingerprint.jsonstatic_analysis.pklstatic_analysis.shacodeboarding_version.jsonwhen emitted by Corehealth/health_report.json
It does not render or commit architecture Markdown, and leaves every other file alone, including user-authored CodeBoarding configuration such as health/health_config.json and health/.healthignore.
Sync keeps the analysis of your repository's default branch current. codeboarding_analysis_location decides where that analysis is stored, codeboarding/baseline by default. The sync job does this, on pushes to the default branch: a sync that runs on any other branch, from any event, fails in seconds, before anything is installed.
main is only read. Each sync adds one commit to codeboarding/baseline in the same repository, an orphan branch that shares no history with main. It holds the .codeboarding/ files listed above, plus .codeboarding/source.json naming the commit they describe and the configuration that made them; the commit message carries both as CodeBoarding-Source: and CodeBoarding-Config: trailers. Pushes only ever fast-forward, and no code branch is ever written, so the push trigger needs no paths-ignore and a protected main needs no exception. Reviews read their base from the branch, and the web platform reads the latest diagram from it. The analysis branch section covers what happens if the branch is deleted, and a ruleset you should import to protect it: sync and review load a pickle from it.
The branch push itself needs nothing beyond contents: write: the first sync creates the branch with an ordinary push. If you import the ruleset that protects it, import the GitHub Actions variant: the example pushes with the default github.token, which the CodeBoarding app ruleset refuses, so the sync would fail at the push after a full analysis.
If sync pushes with an App token or a personal access token rather than the default github.token, its push to codeboarding/baseline starts your own on: push workflows, which then run on a branch with no code and fail. Exclude it from them:
on:
push:
branches-ignore: ['codeboarding/**']Entries are keyed by the commit they describe, so renaming the default branch, say master to main, just continues incrementally.
The first run, or one without a compatible saved analysis to continue from, is a full analysis. Otherwise sync asks Core for an incremental update. If the generated state is unchanged, no commit is created. If the synced branch advances while analysis is running, the result is still saved for the commit it analyzed, unless the analysis branch already holds a newer commit's.
Set codeboarding_analysis_location: in_place to commit .codeboarding/ to the synced branch itself instead, next to the code. Set it on the review job as well, so reviews read it from there, and add a paths-ignore so sync's own commit does not trigger another sync:
on:
push:
branches: [main]
paths-ignore:
- '.codeboarding/analysis.json'
- '.codeboarding/fingerprint.json'
- '.codeboarding/static_analysis.pkl'
- '.codeboarding/static_analysis.sha'
- '.codeboarding/codeboarding_version.json'
- '.codeboarding/health/health_report.json'
# ...
- uses: CodeBoarding/CodeBoarding-action@v1
with:
mode: sync
llm: hosted
codeboarding_analysis_location: in_placeThe list names only generated files on purpose: an edit to .codeboarding/.codeboardingignore or the health configuration changes what is analyzed, and must still trigger a sync. Sync pushes to main directly, so a branch rule protecting main needs the identity sync pushes with (the CodeBoarding app, or GitHub Actions for the default token) as a bypass actor, or a github_token that may push there.
Before this release sync committed .codeboarding/ to the synced branch by default; it now saves to codeboarding/baseline, and runs on the default branch only. A workflow that set sync_strategy: push keeps committing to the code branch, and one that set sync_strategy: pull_request moves to the analysis branch, each with a warning. A target_branch naming a branch other than the one sync runs on fails, since sync now analyzes the branch it runs on. A workflow that set none of them moves to the analysis branch; the .codeboarding/ committed on the code branch is then no longer updated. Separate review and sync workflow files still run, but belong in one file. The CodeBoarding dashboard flags all of these and opens the pull request that moves them; to make the move yourself, paste this into your coding agent:
Move this repository's CodeBoarding sync to the codeboarding/baseline analysis branch.
1. Put the review and sync jobs that run CodeBoarding/CodeBoarding-action in one
file, .github/workflows/codeboarding.yml, as the action README's "Set up" shows,
and delete the workflow files they came from once nothing else is left in them.
2. In those jobs, remove the sync_strategy, target_branch, force_full and
codeboarding_analysis_location inputs if set, and any workflow_dispatch trigger
that starts the sync job. Keep every other input. List only the default branch
under the push trigger.
3. Delete the files sync generated from the default branch:
.codeboarding/analysis.json, fingerprint.json, static_analysis.pkl,
static_analysis.sha, codeboarding_version.json and health/health_report.json.
Leave everything else under .codeboarding/.
4. Remove the paths-ignore entries for those files from the push trigger, and any
.gitattributes lines that mark them as linguist-generated.
5. Open a pull request with these changes. After it merges, close any open
pull request from the codeboarding/sync branch and delete that branch.
The first sync after the merge creates codeboarding/baseline, catching up from a saved analysis when there is one. To keep committing to the code branch instead, set codeboarding_analysis_location: in_place, as above.
| Input | Mode | Default | Description |
|---|---|---|---|
mode |
both | review |
review or sync. |
llm |
both | required | hosted, license, or a provider name. No default. |
<provider>_api_key |
both | empty | That provider's key, e.g. anthropic_api_key. See Providers. |
<provider>_base_url |
both | empty | That provider's endpoint, where it has one. |
aws_bedrock_region |
both | empty | Bedrock region. Core defaults to us-east-1. |
license_key |
both | empty | CodeBoarding license. Required by llm: license. |
model |
both | empty | Default model for both analysis and parsing. |
agent_model |
both | empty | Analysis-only override for model. |
parsing_model |
both | empty | Parsing-only override for model. |
depth_cap |
both | 2 |
Positive integer maximum analysis depth, including full-analysis fallbacks. Changing it rebuilds incompatible state. |
github_token |
both | ${{ github.token }} |
Token for comments and sync delivery. |
codeboarding_analysis_location |
both | codeboarding_branch |
Where the synced branch's analysis is stored: codeboarding_branch (on codeboarding/baseline, for the default branch; the code is never written) or in_place (committed as .codeboarding/ on the synced branch). Sync saves it there and review reads it from there. |
warmstart_retention_days |
review | 1 |
Days to keep the reusable analysis. Only the next run reads it. |
target_branch and sync_strategy are deprecated, and read as the codeboarding_analysis_location they stand for, with a warning: sync_strategy: push as in_place, sync_strategy: pull_request as codeboarding_branch. target_branch is accepted only when it names the branch the run is on: the push trigger picks the branch. force_full: true is ignored with a warning. Anything the new input cannot express fails, linking to moving an existing setup.
The /codeboarding command, comment heading, Mermaid direction (LR), hosted webview URL, commit message, and CodeBoarding 0.14.5 version are intentionally fixed rather than exposed as configuration.
Review mode needs no sync job or committed .codeboarding directory. If no
usable merge-base analysis exists, it runs full analysis there directly, then
seeds an incremental analysis of the PR head and publishes both states. Later
runs prefer compatible prior PR state for incremental updates, while the review
still compares the merge base with the current head.
Set depth_cap in the action's with: block (for example, depth_cap: 4).
This configuration is authoritative: stored metadata.depth_cap is checked for
compatibility, not inherited, and legacy metadata.depth_level is not used as a
fallback. Missing or incompatible baseline depth triggers a rebuild.
metadata.depth_cap records the configured maximum; metadata.depth_level
records the depth actually reached, which can be shallower. Comparing the cap
avoids rejecting valid state or reducing future rebuild depth when a run stops
early. The action input matches the metadata name and the engine receives only
--depth-cap. There are no old-name input aliases. Historical workflows using
the removed depth_level action input must switch to depth_cap.
This action pins Core 0.14.3 for the --depth-cap CLI contract. Publish that Core
release before releasing the action. Earlier eShop evidence predates this final
breaking CLI migration.
| Output | Mode | Description |
|---|---|---|
llm_tier |
both | hosted, license, byok, or byok+license. |
llm_provider |
both | Provider the run used. Empty on hosted and license: which upstream the proxy routes to is CodeBoarding's decision, not your configuration. |
llm_config_error |
both | Configuration failure code, empty when configured. |
diagram_md |
review | Path to the rendered Mermaid block on the runner. |
n_changed |
review | Number of changed components. |
truncated |
review | Whether the graph was reduced to fit GitHub limits. |
review_artifact_url |
review | URL of the uploaded head analysis. |
seed_source |
review | pr-chain when the head grew from this PR's previous analysis, base otherwise. |
merge_base_sha |
review | Merge base used as the comparison baseline. |
analysis_mode |
sync | incremental or full. |
files_written |
sync | Number of persisted analysis artifacts produced. |
committed |
sync | Whether a baseline commit was delivered. |
Repository fetches, comments, and pushes use github.server_url; GitHub.com is not hardcoded for repository operations. The hosted CodeBoarding webview and LLM proxy remain CodeBoarding-operated production services.
Render a diff without making LLM calls:
tests/run_local.sh --base-json /tmp/base.json --head-json /tmp/head.jsonRun the local analysis pipeline:
export OPENROUTER_API_KEY=sk-or-...
python -m pip install codeboarding==0.14.5
tests/run_local.sh --repo /path/to/repo --base main --head featureMIT. See LICENSE.