Skip to content

Commit f20517f

Browse files
Link GitHub action with CodeBoarding-webview
1 parent c9a1af1 commit f20517f

4 files changed

Lines changed: 189 additions & 6 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,8 @@ The command needs the `issue_comment` trigger and runs from your default branch
166166
| `comment_header` | `Architecture review` | Heading for the PR comment. |
167167
| `trigger_command` | `/codeboarding` | Slash command for trusted on-demand runs. |
168168
| `cta_base_url` | empty | Click-proxy base URL: deep-links the editor link into VS Code/Cursor and adds a "get the extension" link (tracks owner/repo/pr). Empty links to the extension listing instead (GitHub strips `vscode:`/`cursor:` from comments). |
169+
| `webview_base_url` | `https://app.codeboarding.org` | Hosted webview base URL. The PR comment adds an "explore in browser" link to this PR's head-vs-base diff. Needs `commit_head_analysis` (same-repo PRs only); omitted on forks. Set empty to disable. |
170+
| `commit_head_analysis` | `true` | Commit the generated head `.codeboarding/analysis.json` (+ health report) to the PR branch so the webview can read it at the head SHA. Same-repo PRs only (the token is read-only on forks). |
169171

170172
## Outputs
171173

action.yml

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,14 @@ inputs:
5454
description: 'Base URL of the click proxy (e.g. https://go.codeboarding.org). When set, the editor link deep-links into VS Code/Cursor via the proxy and a "get the extension" link is added (owner/repo/pr tracked). Empty (default) links to the extension listing instead, since GitHub strips vscode:/cursor: schemes from comment links.'
5555
required: false
5656
default: ''
57+
webview_base_url:
58+
description: 'Base URL of the hosted webview (default https://app.codeboarding.org). The PR comment adds an "explore in browser" link deep-linking to this PR''s head-vs-base architecture diff. Requires the head analysis.json to be committed to the PR branch (commit_head_analysis), so it is omitted on fork PRs. Set empty to disable the webview link.'
59+
required: false
60+
default: 'https://app.codeboarding.org'
61+
commit_head_analysis:
62+
description: 'Commit the generated head .codeboarding/analysis.json (+ health report) back to the PR branch so the webview can fetch it at the head SHA. Same-repo PRs only (the token is read-only on forks). Required for the webview "explore in browser" link.'
63+
required: false
64+
default: 'true'
5765
trigger_command:
5866
description: 'Slash-command that triggers the action from a PR comment (issue_comment event). A comment whose first word is this runs the diagram on-demand.'
5967
required: false
@@ -88,6 +96,7 @@ runs:
8896
PR_NUMBER_PULL: ${{ github.event.pull_request.number }}
8997
PULL_BASE_SHA: ${{ github.event.pull_request.base.sha }}
9098
PULL_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
99+
PULL_HEAD_REF: ${{ github.event.pull_request.head.ref }}
91100
PULL_BASE_REF: ${{ github.event.pull_request.base.ref }}
92101
PULL_BASE_REPO: ${{ github.event.pull_request.base.repo.full_name }}
93102
PULL_HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
@@ -115,6 +124,7 @@ runs:
115124
PR_NUMBER="$PR_NUMBER_PULL"
116125
BASE_SHA="$PULL_BASE_SHA"
117126
HEAD_SHA="$PULL_HEAD_SHA"
127+
HEAD_REF="$PULL_HEAD_REF"
118128
BASE_REF="$PULL_BASE_REF"
119129
BASE_REPO="$PULL_BASE_REPO"
120130
HEAD_REPO="$PULL_HEAD_REPO"
@@ -137,6 +147,7 @@ runs:
137147
PR_JSON="$(gh api "repos/${REPOSITORY}/pulls/${PR_NUMBER}" 2>/dev/null)" || skip "Could not fetch PR #$PR_NUMBER from the API."
138148
BASE_SHA="$(printf '%s' "$PR_JSON" | python3 -c 'import json,sys;print(json.load(sys.stdin)["base"]["sha"])' 2>/dev/null)" || skip "Could not parse base SHA from the PR API."
139149
HEAD_SHA="$(printf '%s' "$PR_JSON" | python3 -c 'import json,sys;print(json.load(sys.stdin)["head"]["sha"])' 2>/dev/null)" || skip "Could not parse head SHA from the PR API."
150+
HEAD_REF="$(printf '%s' "$PR_JSON" | python3 -c 'import json,sys;print(json.load(sys.stdin)["head"]["ref"])' 2>/dev/null)" || HEAD_REF=""
140151
BASE_REF="$(printf '%s' "$PR_JSON" | python3 -c 'import json,sys;print(json.load(sys.stdin)["base"]["ref"])' 2>/dev/null)" || BASE_REF=""
141152
BASE_REPO="$(printf '%s' "$PR_JSON" | python3 -c 'import json,sys;print(json.load(sys.stdin)["base"]["repo"]["full_name"])' 2>/dev/null)" || skip "Could not parse base repo from the PR API."
142153
HEAD_REPO="$(printf '%s' "$PR_JSON" | python3 -c 'import json,sys;print(json.load(sys.stdin)["head"]["repo"]["full_name"])' 2>/dev/null)" || skip "Could not parse head repo from the PR API."
@@ -151,9 +162,12 @@ runs:
151162
echo "pr_number=$PR_NUMBER"
152163
echo "base_sha=$BASE_SHA"
153164
echo "head_sha=$HEAD_SHA"
165+
echo "head_ref=$HEAD_REF"
154166
echo "base_ref=$BASE_REF"
155167
echo "base_repo=$BASE_REPO"
156168
echo "head_repo=$HEAD_REPO"
169+
# same_repo gates pushing the head analysis: forks give a read-only token.
170+
if [ "$HEAD_REPO" = "$REPOSITORY" ]; then echo "same_repo=true"; else echo "same_repo=false"; fi
157171
} >> "$GITHUB_OUTPUT"
158172
echo "Resolved PR #$PR_NUMBER (base=$BASE_REPO@$BASE_SHA head=$HEAD_REPO@$HEAD_SHA) via $EVENT"
159173
@@ -576,6 +590,62 @@ runs:
576590
"${RUNNER_TEMP}/cb-agent-model" \
577591
"${RUNNER_TEMP}/cb-parsing-model"
578592
593+
# Commit the generated head analysis (+ health report) to the PR branch so the
594+
# hosted webview can fetch .codeboarding/analysis.json at the head SHA and open
595+
# this PR's head-vs-base diff. Same-repo PRs only — the token is read-only on
596+
# forks (the step is skipped there and the webview link is omitted). The push
597+
# creates a NEW head commit; its SHA (webview_sha) is what the comment links to.
598+
- name: Commit head analysis to PR branch
599+
id: commit_head
600+
if: >-
601+
steps.guard.outputs.skip != 'true'
602+
&& inputs.commit_head_analysis == 'true'
603+
&& steps.guard.outputs.same_repo == 'true'
604+
&& inputs.webview_base_url != ''
605+
shell: bash
606+
working-directory: target-repo
607+
env:
608+
GH_TOKEN: ${{ inputs.github_token }}
609+
HEAD_DIR: ${{ steps.base.outputs.head_dir }}
610+
HEAD_REF: ${{ steps.guard.outputs.head_ref }}
611+
HEAD_SHA: ${{ steps.guard.outputs.head_sha }}
612+
REPOSITORY: ${{ github.repository }}
613+
SERVER_URL: ${{ github.server_url }}
614+
run: |
615+
echo "ready=false" >> "$GITHUB_OUTPUT"
616+
echo "webview_sha=$HEAD_SHA" >> "$GITHUB_OUTPUT"
617+
[ -n "$HEAD_REF" ] || { echo "::notice::No head branch ref resolved; skipping head-analysis commit."; exit 0; }
618+
[ -f "$HEAD_DIR/analysis.json" ] || { echo "::notice::No head analysis.json to commit."; exit 0; }
619+
620+
mkdir -p .codeboarding/health
621+
cp "$HEAD_DIR/analysis.json" .codeboarding/analysis.json
622+
if [ -f "$HEAD_DIR/health/health_report.json" ]; then
623+
cp "$HEAD_DIR/health/health_report.json" .codeboarding/health/health_report.json
624+
fi
625+
626+
git add .codeboarding/analysis.json .codeboarding/health/health_report.json 2>/dev/null || git add .codeboarding/analysis.json
627+
if git diff --cached --quiet; then
628+
echo "::notice::Head analysis unchanged; nothing to commit."
629+
echo "ready=true" >> "$GITHUB_OUTPUT" # already committed at this SHA → webview can read it
630+
exit 0
631+
fi
632+
633+
git config user.name "codeboarding[bot]"
634+
git config user.email "codeboarding[bot]@users.noreply.github.com"
635+
git commit -m "chore(codeboarding): update architecture analysis [skip ci]" >/dev/null
636+
637+
# Push to the PR head branch. The checkout used persist-credentials:false, so
638+
# authenticate the push explicitly with the workflow token (same-repo only).
639+
AUTH_URL="https://x-access-token:${GH_TOKEN}@${SERVER_URL#https://}/${REPOSITORY}.git"
640+
if git push "$AUTH_URL" "HEAD:refs/heads/${HEAD_REF}" 2>/dev/null; then
641+
NEW_SHA="$(git rev-parse HEAD)"
642+
echo "webview_sha=$NEW_SHA" >> "$GITHUB_OUTPUT"
643+
echo "ready=true" >> "$GITHUB_OUTPUT"
644+
echo "Committed head analysis to ${HEAD_REF} as ${NEW_SHA}."
645+
else
646+
echo "::warning::Could not push head analysis to ${HEAD_REF}; the webview link will be omitted."
647+
fi
648+
579649
- name: Diff analyses → Mermaid
580650
if: steps.guard.outputs.skip != 'true'
581651
id: diagram
@@ -634,6 +704,12 @@ runs:
634704
TRUNC: ${{ steps.diagram.outputs.truncated }}
635705
PR: ${{ steps.guard.outputs.pr_number }}
636706
ISSUES: ${{ steps.health.outputs.issues }}
707+
WEBVIEW_BASE: ${{ inputs.webview_base_url }}
708+
# SHA the head analysis.json was committed at (post-push), and whether that
709+
# commit succeeded — gates the webview "explore in browser" link.
710+
WEBVIEW_SHA: ${{ steps.commit_head.outputs.webview_sha }}
711+
WEBVIEW_READY: ${{ steps.commit_head.outputs.ready }}
712+
BASE_SHA: ${{ steps.guard.outputs.base_sha }}
637713
run: |
638714
BODY_FILE=$(mktemp)
639715
OWNER="${OWNER_REPO%%/*}"; REPO="${OWNER_REPO##*/}"
@@ -646,11 +722,17 @@ runs:
646722
}
647723
648724
# CTA footer: an editor link (proxy deep-link when CTA_BASE is set, else the
649-
# extension's https listing — GitHub strips vscode:/cursor:) plus the ⚠️ banner.
725+
# extension's https listing — GitHub strips vscode:/cursor:), the ⚠️ banner,
726+
# and — when the head analysis was committed (WEBVIEW_READY) — a webview
727+
# "explore in browser" link to this PR's head-vs-base diff.
650728
cta() {
729+
local extra=()
730+
if [ "$WEBVIEW_READY" = "true" ]; then
731+
extra+=(--webview-ready --webview-base "$WEBVIEW_BASE" --head-sha "$WEBVIEW_SHA" --base-sha "$BASE_SHA")
732+
fi
651733
python3 "$ACTION_PATH/scripts/build_cta.py" \
652734
--cta-base "$CTA_BASE" --owner "$OWNER" --repo "$REPO" --pr "$PR" \
653-
--repo-path "$TARGET_REPO" --issues "${ISSUES:-0}"
735+
--repo-path "$TARGET_REPO" --issues "${ISSUES:-0}" "${extra[@]}"
654736
}
655737
656738
{

scripts/build_cta.py

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
custom ``vscode:``/``cursor:`` schemes — a deep link would render as dead text —
99
so the editor link points at the extension's plain-https listing instead (VS Code
1010
Marketplace, Cursor via Open VSX), which is the only clickable option. A no-install
11-
hosted-webview ("explore in browser") tier is intentionally deferred (see
12-
docs/COMMIT_STRATEGY.md) — the committed analysis already supports it later.
11+
hosted-webview ("explore in browser") line is added when ``webview_ready`` — i.e. the
12+
head ``analysis.json`` was committed to the PR branch and this isn't a fork PR — so
13+
the webview can fetch a committed analysis at the head SHA (see docs/COMMIT_STRATEGY.md).
1314
1415
Editor coverage is deliberately limited to **VS Code and Cursor**. Per the 2025
1516
Stack Overflow Developer Survey (https://survey.stackoverflow.co/2025/technology/),
@@ -58,20 +59,60 @@ def detect_editors(repo_path: Path) -> list[str]:
5859
}
5960

6061

61-
def build_cta(cta_base: str, owner: str, repo: str, pr: str, repo_path: Path, issues: int = 0) -> str:
62+
def build_webview_link(webview_base: str, owner: str, repo: str, head_sha: str, base_sha: str) -> str | None:
63+
"""Return the markdown "explore in browser" line, or None if not buildable.
64+
65+
Deep-links the hosted webview straight to this PR's head-vs-base architecture
66+
diff: ``?repo=owner/repo&ref=<head_sha>&compare=<base_sha>``. Pinned to exact
67+
SHAs so the committed ``analysis.json`` the webview fetches matches this run. For
68+
a private repo the webview itself sends the viewer through GitHub sign-in and then
69+
loads the same diff. Returns None when the base/head pieces aren't all present.
70+
"""
71+
if not (webview_base and owner and repo and head_sha):
72+
return None
73+
base = webview_base.rstrip("/")
74+
params = {"repo": f"{owner}/{repo}", "ref": head_sha}
75+
if base_sha:
76+
params["compare"] = base_sha
77+
return f"🌐 [**Explore this PR’s architecture in your browser →**]({base}/?{urlencode(params)})"
78+
79+
80+
def build_cta(
81+
cta_base: str,
82+
owner: str,
83+
repo: str,
84+
pr: str,
85+
repo_path: Path,
86+
issues: int = 0,
87+
*,
88+
webview_base: str = "",
89+
head_sha: str = "",
90+
base_sha: str = "",
91+
webview_ready: bool = False,
92+
) -> str:
6293
"""Return the markdown CTA footer: a health-warning banner plus an editor link.
6394
6495
With a ``cta_base`` proxy the links route through it (owner/repo/pr tracked),
6596
deep-link into the editor, and add a separate "get the extension" link. Without
6697
a proxy the editor link is the extension's https listing (GitHub strips custom
6798
``vscode:``/``cursor:`` schemes), and the redundant install link is dropped.
6899
The ⚠️ banner shows whenever ``issues > 0``.
100+
101+
When ``webview_ready`` (the head ``analysis.json`` was committed and this isn't a
102+
fork PR) a "explore in browser" line deep-links the hosted webview to this PR's
103+
head-vs-base diff. Otherwise that line is omitted (the webview couldn't fetch a
104+
committed analysis at the head SHA).
69105
"""
70106
parts: list[str] = []
71107
if issues > 0:
72108
noun = "issue" if issues == 1 else "issues"
73109
parts.append(f"⚠️ **{issues} architecture {noun} found** — open CodeBoarding to explore them.")
74110

111+
if webview_ready:
112+
webview_line = build_webview_link(webview_base, owner, repo, head_sha, base_sha)
113+
if webview_line:
114+
parts.append(webview_line)
115+
75116
editors = detect_editors(repo_path)
76117
if cta_base:
77118
base = cta_base.rstrip("/")
@@ -104,13 +145,25 @@ def main() -> int:
104145
p.add_argument("--pr", required=True)
105146
p.add_argument("--repo-path", required=True, type=Path, help="Path to the analyzed repo checkout")
106147
p.add_argument("--issues", default="0", help="Real architecture-issue count (0 -> no warning banner)")
148+
p.add_argument("--webview-base", default="", help="Hosted webview base URL (e.g. https://app.codeboarding.org)")
149+
p.add_argument("--head-sha", default="", help="PR head SHA the webview link pins to")
150+
p.add_argument("--base-sha", default="", help="PR base SHA the webview link compares against")
151+
p.add_argument(
152+
"--webview-ready",
153+
action="store_true",
154+
help="Emit the webview link (head analysis.json was committed; not a fork PR)",
155+
)
107156
args = p.parse_args()
108157

109158
try:
110159
issues = int(args.issues or 0)
111160
except ValueError:
112161
issues = 0
113-
print(build_cta(args.cta_base, args.owner, args.repo, args.pr, args.repo_path, issues))
162+
print(build_cta(
163+
args.cta_base, args.owner, args.repo, args.pr, args.repo_path, issues,
164+
webview_base=args.webview_base, head_sha=args.head_sha, base_sha=args.base_sha,
165+
webview_ready=args.webview_ready,
166+
))
114167
return 0
115168

116169

tests/test_build_cta.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,5 +77,51 @@ def test_trailing_slash_in_base_is_normalized(self):
7777
self.assertEqual(a, b)
7878

7979

80+
class TestWebviewLink(unittest.TestCase):
81+
WV = "https://app.codeboarding.org"
82+
83+
def test_link_built_with_head_ref_and_compare_base(self):
84+
link = bc.build_webview_link(self.WV, "Org", "Repo", "headsha", "basesha")
85+
self.assertIn("https://app.codeboarding.org/?", link)
86+
self.assertIn("repo=Org%2FRepo", link)
87+
self.assertIn("ref=headsha", link)
88+
self.assertIn("compare=basesha", link)
89+
90+
def test_link_omits_compare_when_no_base(self):
91+
link = bc.build_webview_link(self.WV, "o", "r", "headsha", "")
92+
self.assertIn("ref=headsha", link)
93+
self.assertNotIn("compare=", link)
94+
95+
def test_link_none_without_head_sha_or_base(self):
96+
self.assertIsNone(bc.build_webview_link(self.WV, "o", "r", "", "basesha"))
97+
self.assertIsNone(bc.build_webview_link("", "o", "r", "headsha", "basesha"))
98+
99+
def test_cta_emits_webview_line_when_ready(self):
100+
out = bc.build_cta(
101+
"", "Org", "Repo", "9", repo_with(), issues=0,
102+
webview_base=self.WV, head_sha="headsha", base_sha="basesha", webview_ready=True,
103+
)
104+
self.assertIn("Explore this PR", out)
105+
self.assertIn("ref=headsha", out)
106+
self.assertIn("compare=basesha", out)
107+
108+
def test_cta_omits_webview_line_when_not_ready(self):
109+
# Fork PR / head analysis not committed -> webview can't fetch at head SHA.
110+
out = bc.build_cta(
111+
"", "Org", "Repo", "9", repo_with(), issues=0,
112+
webview_base=self.WV, head_sha="headsha", base_sha="basesha", webview_ready=False,
113+
)
114+
self.assertNotIn("Explore this PR", out)
115+
# Editor CTA is still present regardless.
116+
self.assertIn("Open in VS Code", out)
117+
118+
def test_cta_omits_webview_line_when_ready_but_no_base_url(self):
119+
out = bc.build_cta(
120+
"", "Org", "Repo", "9", repo_with(), issues=0,
121+
webview_base="", head_sha="headsha", base_sha="basesha", webview_ready=True,
122+
)
123+
self.assertNotIn("Explore this PR", out)
124+
125+
80126
if __name__ == "__main__":
81127
unittest.main()

0 commit comments

Comments
 (0)