Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,27 @@ Caching is best-effort: a cache miss, an unavailable cache service, or a GitHub

Actions cache entries are scoped to the ref that wrote them, and GitHub gives comment-triggered runs a read-only cache token. So automatic `pull_request` runs reuse each other's analysis and build the chain, `sync` publishes the base entry everyone shares, and a `/codeboarding` command reads both but writes neither — it costs one base-seeded incremental, and `/codeboarding refresh` improves the comment it posts rather than what later runs start from. 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.

### Analyzing without commenting

`post_comment: false` runs the analysis and uploads the artifact, but writes nothing to the pull request — no progress placeholder, no result, no failure notice. The webview still has everything it reads, so this is the setting for keeping a PR's analysis current without a bot comment on it. A `/codeboarding` command still gets its 👀 reaction, since with posting off that is the only sign it was picked up.

The action's outputs are unaffected, so a workflow can decide for itself what to say and when:

```yaml
- uses: CodeBoarding/CodeBoarding-action@v1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Point the example at the major containing this input

The surrounding setup and all other v2 examples use @v2, but this new example selects @v1; because the old major does not receive the breaking v2 action, it does not recognize post_comment and retains its normal commenting behavior. A user copying this exact silence recipe therefore gets an unexpected PR comment, so the example should use @v2.

AGENTS.md reference: AGENTS.md:L55-L58

Useful? React with 👍 / 👎.

id: codeboarding
with:
post_comment: false
- if: steps.codeboarding.outputs.n_changed != '0'
uses: marocchino/sticky-pull-request-comment@v2
with:
header: codeboarding-review
number: ${{ github.event.pull_request.number }}
path: ${{ steps.codeboarding.outputs.diagram_md }}
```

That example only comments when the architecture actually changed, which suits per-push reviews: most pushes change no components, and the comment stays put instead of being rewritten with the same content.

## Authentication and providers

With no LLM inputs, the action uses CodeBoarding's hosted OpenRouter tier. It mints short-lived GitHub OIDC credentials per request, so the job needs `id-token: write` and no stored LLM secret.
Expand Down Expand Up @@ -226,6 +247,7 @@ With the default `github.token`, the repository or organization must allow GitHu
| `sync_strategy` | sync | `push` | `push` or `pull_request`. |
| `target_branch` | sync | event branch | Branch receiving the baseline or rolling PR. |
| `force_full` | sync | `false` | Ignore the committed baseline for this run. |
| `post_comment` | review | `true` | `false` analyzes and uploads the artifact without writing to the pull request. |

The `/codeboarding` command, comment heading, Mermaid direction (`LR`), hosted webview URL, rolling sync branch, commit message, and CodeBoarding 0.13.8 version are intentionally fixed in v2 rather than exposed as configuration.

Expand Down
11 changes: 8 additions & 3 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ inputs:
description: 'Run a full sync analysis instead of reusing the committed baseline.'
required: false
default: 'false'
post_comment:
description: 'Post the review to the pull request. false still analyzes and uploads the artifact.'
required: false
default: 'true'
outputs:
diagram_md:
description: 'Path to the rendered Mermaid review diagram.'
Expand Down Expand Up @@ -102,6 +106,7 @@ runs:
HEAD_AUTHOR_EMAIL: ${{ github.event.head_commit.author.email }}
TARGET_BRANCH_INPUT: ${{ inputs.target_branch }}
SYNC_STRATEGY: ${{ inputs.sync_strategy }}
POST_COMMENT_INPUT: ${{ inputs.post_comment }}
COMMENT_BODY: ${{ github.event.comment.body }}
AUTHOR_ASSOCIATION: ${{ github.event.comment.author_association }}
ISSUE_PR_URL: ${{ github.event.issue.pull_request.url }}
Expand All @@ -127,7 +132,7 @@ runs:
run: GH_HOST="${GITHUB_SERVER_URL#*://}" gh api -X POST "repos/${REPOSITORY}/issues/comments/${COMMENT_ID}/reactions" -f content=eyes >/dev/null

- name: Post review progress
if: steps.guard.outputs.skip != 'true' && steps.guard.outputs.mode == 'review'
if: steps.guard.outputs.skip != 'true' && steps.guard.outputs.mode == 'review' && steps.guard.outputs.post_comment != 'false'
continue-on-error: true
uses: marocchino/sticky-pull-request-comment@v2
with:
Expand Down Expand Up @@ -402,7 +407,7 @@ runs:

- name: Post review comment
id: review_comment
if: steps.guard.outputs.skip != 'true' && steps.guard.outputs.mode == 'review'
if: steps.guard.outputs.skip != 'true' && steps.guard.outputs.mode == 'review' && steps.guard.outputs.post_comment != 'false'
uses: marocchino/sticky-pull-request-comment@v2
with:
header: ${{ steps.guard.outputs.comment_id }}
Expand All @@ -411,7 +416,7 @@ runs:
path: ${{ steps.review_body.outputs.path }}

- name: Post review failure
if: failure() && steps.guard.outputs.skip != 'true' && steps.guard.outputs.mode == 'review' && steps.review_comment.outcome != 'success'
if: failure() && steps.guard.outputs.skip != 'true' && steps.guard.outputs.mode == 'review' && steps.guard.outputs.post_comment != 'false' && steps.review_comment.outcome != 'success'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require an affirmative value before posting failures

When post_comment: false is set but event resolution fails before guard.sh emits post_comment—for example, a transient failure while fetching the PR for an issue_comment event—the output is empty, so steps.guard.outputs.post_comment != 'false' evaluates true and this failure handler posts a comment despite the explicit opt-out. Requiring the output to equal true, or emitting the validated value before later fallible work, keeps failures closed to posting.

Useful? React with 👍 / 👎.

continue-on-error: true
uses: marocchino/sticky-pull-request-comment@v2
with:
Expand Down
11 changes: 11 additions & 0 deletions scripts/action/guard.sh
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@ if [ "$MODE" = sync ]; then
exit 0
fi

# Validated here rather than read straight from the input, so a typo fails the
# run with a reason instead of silently posting when the author asked for
# silence. The reaction on a /codeboarding comment is not a comment and stays:
# with posting off it is the only sign the command was picked up.
post_comment="${POST_COMMENT_INPUT:-true}"
case "$post_comment" in
true|false) ;;
*) fail "post_comment must be true or false." ;;
esac

seed_mode=chain
case "$EVENT" in
pull_request|pull_request_target)
Expand Down Expand Up @@ -137,5 +147,6 @@ is_fork=false
echo "checkout_ref=$head_sha"
echo "comment_id=$comment_id"
echo "seed_mode=$seed_mode"
echo "post_comment=$post_comment"
echo "is_fork=$is_fork"
} >> "$GITHUB_OUTPUT"
19 changes: 17 additions & 2 deletions tests/test_action_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,7 @@ class CachePathParityTests(unittest.TestCase):
"""actions/cache derives its lookup version from the path strings, so a save
under a different path than the restore can never be found again."""

def _cache_steps(self) -> list[dict[str, str]]:
def _steps(self) -> list[dict[str, str]]:
steps: list[dict[str, str]] = []
current: dict[str, str] | None = None
for line in (ROOT / "action.yml").read_text(encoding="utf-8").splitlines():
Expand All @@ -419,7 +419,10 @@ def _cache_steps(self) -> list[dict[str, str]]:
for field in ("uses", "path", "key", "restore-keys", "if"):
if stripped.startswith(f"{field}:"):
current[field] = stripped.split(":", 1)[1].strip()
return [step for step in steps if step.get("uses", "").startswith("actions/cache/")]
return steps

def _cache_steps(self) -> list[dict[str, str]]:
return [step for step in self._steps() if step.get("uses", "").startswith("actions/cache/")]

def test_the_chain_is_restored_by_prefix_so_a_refresh_survives(self) -> None:
steps = {step["name"]: step for step in self._cache_steps()}
Expand Down Expand Up @@ -449,6 +452,18 @@ def test_reviews_do_not_attempt_a_save_a_comment_run_cannot_make(self) -> None:
f"{step['name']} would attempt a save that a comment-triggered run cannot make",
)

def test_every_comment_step_honours_post_comment(self) -> None:
# Analysis, artifact upload and outputs continue when posting is off;
# only the steps that write to the pull request are gated.
posting = [step for step in self._steps() if "sticky-pull-request-comment" in step.get("uses", "")]
self.assertEqual(len(posting), 3, "expected progress, result and failure comments")
for step in posting:
self.assertIn(
"steps.guard.outputs.post_comment != 'false'",
step.get("if", ""),
f"{step['name']} posts to the pull request regardless of post_comment",
)

def test_every_saved_path_is_a_restored_path(self) -> None:
steps = self._cache_steps()
self.assertTrue(steps, "no cache steps found in action.yml")
Expand Down
27 changes: 27 additions & 0 deletions tests/test_action_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,33 @@ def test_review_guard_accepts_trusted_fork_command(self) -> None:
self.assertIn("checkout_repo=contributor/repo\n", values)
self.assertIn("checkout_ref=head-sha\n", values)

def test_review_guard_rejects_an_unusable_post_comment_value(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
output = Path(tmp) / "github-output"
result = subprocess.run(
[str(GUARD)],
env={
"PATH": os.environ["PATH"],
"GITHUB_OUTPUT": str(output),
"MODE": "review",
"EVENT": "pull_request",
"POST_COMMENT_INPUT": "no",
"EVENT_PR_NUMBER": "42",
"PULL_BASE_SHA": "base-sha",
"PULL_HEAD_SHA": "head-sha",
"PULL_BASE_REPO": "owner/repo",
"PULL_HEAD_REPO": "owner/repo",
},
capture_output=True,
text=True,
check=False,
)

# Silently posting when the author asked for silence is worse than
# failing, so an unusable value stops the run.
self.assertEqual(result.returncode, 1, result.stdout)
self.assertIn("post_comment must be true or false", result.stdout)

def test_installs_core_manifest_and_preserves_user_configuration(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
Expand Down
Loading