Skip to content

Commit 6f6bb39

Browse files
feat(cta): emit short GitHub-style webview PR links
webview_url now builds /owner/repo/pull/<pr>?run=<run_id> instead of the long ?repo=&ref=&compare=&pr=&run_id=&artifact=&artifact_url= query. The webview re-derives the head SHA, base SHA, and artifact name from the run's uploaded artifact + its metadata.json, so the comment link carries only the PR number and run id. Trim the CTA plumbing in action.yml accordingly (ARTIFACT_NAME stays the readiness gate; only --run-id is passed through). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 1fc5268 commit 6f6bb39

3 files changed

Lines changed: 46 additions & 95 deletions

File tree

action.yml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1368,15 +1368,14 @@ runs:
13681368
# committed to the PR branch.
13691369
cta() {
13701370
local extra=()
1371+
# ARTIFACT_NAME presence is the readiness gate (an analysis artifact was
1372+
# uploaded for this run). The short webview link itself needs only the run id
1373+
# — the webview re-derives head/base/artifact from the run + its metadata.
13711374
if [ -n "$WEBVIEW_BASE" ] && [ -n "$ARTIFACT_NAME" ]; then
13721375
extra+=(
13731376
--webview-ready
13741377
--webview-base "$WEBVIEW_BASE"
1375-
--head-sha "$HEAD_SHA"
1376-
--base-sha "$BASE_SHA"
13771378
--run-id "$RUN_ID"
1378-
--artifact-name "$ARTIFACT_NAME"
1379-
--artifact-url "$ARTIFACT_URL"
13801379
)
13811380
fi
13821381
python3 "$ACTION_PATH/scripts/build_cta.py" \

scripts/build_cta.py

Lines changed: 14 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
The body is a single line — "Explore this PR's architecture in your browser or
44
VS Code" — that merges the hosted-webview link with the editor link(s), preceded by
55
a warning banner when real health findings exist. The "browser" link (a no-install
6-
hosted webview) is included only when ``webview_ready`` and points at the uploaded
7-
GitHub Actions artifact for this PR analysis. With a click proxy (``cta_base``) the
6+
hosted webview) is included only when ``webview_ready``; it's a short GitHub-style
7+
link (``/owner/repo/pull/<pr>?run=<run_id>``) that the webview resolves to this PR's
8+
uploaded analysis artifact for the run. With a click proxy (``cta_base``) the
89
editor link routes through it (owner/repo/pr tracked) and deep-links into the editor
910
(the proxy redirects to a ``vscode:``/``cursor:`` URL), and a separate "install the
1011
extension" link is appended. Without a proxy GitHub's comment sanitizer strips custom
@@ -63,35 +64,21 @@ def webview_url(
6364
webview_base: str,
6465
owner: str,
6566
repo: str,
66-
head_sha: str,
67-
base_sha: str,
6867
*,
6968
pr: str = "",
7069
run_id: str = "",
71-
artifact_name: str = "",
72-
artifact_url: str = "",
7370
) -> str | None:
74-
"""Return the hosted-webview deep-link URL for this PR's head-vs-comparison-branch diff, or None.
71+
"""Return the hosted-webview PR deep-link, or None.
7572
76-
Deep-links straight to the diff. The base/head SHAs pin the code comparison;
77-
the optional run/artifact fields let the webview load PR-specific analysis
78-
data from the workflow artifact instead of from the PR branch.
73+
A GitHub-style short link: ``{base}/{owner}/{repo}/pull/{pr}?run={run_id}``. The
74+
webview re-derives the head SHA, base SHA, and artifact name from the workflow
75+
run's uploaded artifact (+ its metadata.json), so the link carries only the PR
76+
number and the run id — short, and stable across re-runs.
7977
"""
80-
if not (webview_base and owner and repo and head_sha):
78+
if not (webview_base and owner and repo and pr and run_id):
8179
return None
8280
base = webview_base.rstrip("/")
83-
params = {"repo": f"{owner}/{repo}", "ref": head_sha}
84-
if base_sha:
85-
params["compare"] = base_sha
86-
if pr:
87-
params["pr"] = pr
88-
if run_id:
89-
params["run_id"] = run_id
90-
if artifact_name:
91-
params["artifact"] = artifact_name
92-
if artifact_url:
93-
params["artifact_url"] = artifact_url
94-
return f"{base}/?{urlencode(params)}"
81+
return f"{base}/{owner}/{repo}/pull/{pr}?{urlencode({'run': run_id})}"
9582

9683

9784
def _join_or(items: list[str]) -> str:
@@ -112,12 +99,8 @@ def build_cta(
11299
issues: int = 0,
113100
*,
114101
webview_base: str = "",
115-
head_sha: str = "",
116-
base_sha: str = "",
117102
webview_ready: bool = False,
118103
run_id: str = "",
119-
artifact_name: str = "",
120-
artifact_url: str = "",
121104
) -> str:
122105
"""Return the markdown CTA footer: a health-warning banner plus an editor link.
123106
@@ -127,8 +110,9 @@ def build_cta(
127110
``vscode:``/``cursor:`` schemes), and the redundant install link is dropped.
128111
The ⚠️ banner shows whenever ``issues > 0``.
129112
130-
When ``webview_ready`` a "explore in browser" line deep-links the hosted
131-
webview to this PR's artifact-backed head-vs-comparison-branch diff.
113+
When ``webview_ready`` an "explore in browser" line deep-links the hosted webview
114+
to this PR's diff (``/owner/repo/pull/<pr>?run=<run_id>``); the webview re-derives
115+
the head/base/artifact from the run, so only the PR number and run id are needed.
132116
"""
133117
parts: list[str] = []
134118
if issues > 0:
@@ -154,17 +138,7 @@ def link(path: str, **extra: str) -> str:
154138
# "in your browser or VS Code" / "in VS Code".
155139
targets: list[str] = []
156140
if webview_ready:
157-
wv = webview_url(
158-
webview_base,
159-
owner,
160-
repo,
161-
head_sha,
162-
base_sha,
163-
pr=pr,
164-
run_id=run_id,
165-
artifact_name=artifact_name,
166-
artifact_url=artifact_url,
167-
)
141+
wv = webview_url(webview_base, owner, repo, pr=pr, run_id=run_id)
168142
if wv:
169143
targets.append(f"your [**browser**]({wv})")
170144
targets += [f"[**{_EDITOR_LABEL[e]}**]({editor_href[e]})" for e in editors]
@@ -187,11 +161,7 @@ def main() -> int:
187161
p.add_argument("--repo-path", required=True, type=Path, help="Path to the analyzed repo checkout")
188162
p.add_argument("--issues", default="0", help="Real architecture-issue count (0 -> no warning banner)")
189163
p.add_argument("--webview-base", default="", help="Hosted webview base URL (e.g. https://app.codeboarding.org)")
190-
p.add_argument("--head-sha", default="", help="PR head SHA the webview link pins to")
191-
p.add_argument("--base-sha", default="", help="Comparison-branch SHA the webview link compares against")
192164
p.add_argument("--run-id", default="", help="GitHub Actions run id containing the PR analysis artifact")
193-
p.add_argument("--artifact-name", default="", help="GitHub Actions artifact name containing PR analysis data")
194-
p.add_argument("--artifact-url", default="", help="GitHub Actions artifact URL containing PR analysis data")
195165
p.add_argument(
196166
"--webview-ready",
197167
action="store_true",
@@ -212,12 +182,8 @@ def main() -> int:
212182
args.repo_path,
213183
issues,
214184
webview_base=args.webview_base,
215-
head_sha=args.head_sha,
216-
base_sha=args.base_sha,
217185
webview_ready=args.webview_ready,
218186
run_id=args.run_id,
219-
artifact_name=args.artifact_name,
220-
artifact_url=args.artifact_url,
221187
)
222188
)
223189
return 0

tests/test_build_cta.py

Lines changed: 29 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -79,39 +79,31 @@ def test_trailing_slash_in_base_is_normalized(self):
7979
class TestWebviewUrl(unittest.TestCase):
8080
WV = "https://app.codeboarding.org"
8181

82-
def test_url_built_with_head_ref_and_compare_base(self):
83-
url = bc.webview_url(self.WV, "Org", "Repo", "headsha", "basesha")
84-
self.assertIn("https://app.codeboarding.org/?", url)
85-
self.assertIn("repo=Org%2FRepo", url)
86-
self.assertIn("ref=headsha", url)
87-
self.assertIn("compare=basesha", url)
88-
self.assertFalse(url.startswith("🌐")) # a bare URL now, not a markdown line
89-
90-
def test_url_omits_compare_when_no_base(self):
91-
url = bc.webview_url(self.WV, "o", "r", "headsha", "")
92-
self.assertIn("ref=headsha", url)
82+
def test_url_is_github_style_pr_path(self):
83+
url = bc.webview_url(self.WV, "Org", "Repo", pr="9", run_id="123")
84+
self.assertEqual(url, "https://app.codeboarding.org/Org/Repo/pull/9?run=123")
85+
86+
def test_url_carries_only_pr_path_and_run(self):
87+
# Head/base SHAs and the artifact name/url are re-derived by the webview, so
88+
# none of them appear in the short link.
89+
url = bc.webview_url(self.WV, "o", "r", pr="9", run_id="123")
90+
self.assertIn("/o/r/pull/9", url)
91+
self.assertIn("run=123", url)
92+
self.assertNotIn("ref=", url)
9393
self.assertNotIn("compare=", url)
94+
self.assertNotIn("artifact", url)
95+
self.assertNotIn("repo=o%2Fr", url) # not the old query-style link
9496

95-
def test_url_includes_artifact_lookup_params(self):
96-
url = bc.webview_url(
97-
self.WV,
98-
"o",
99-
"r",
100-
"headsha",
101-
"basesha",
102-
pr="9",
103-
run_id="123",
104-
artifact_name="codeboarding-pr-9-headsha",
105-
artifact_url="https://github.com/o/r/actions/runs/123/artifacts/456",
106-
)
107-
self.assertIn("pr=9", url)
108-
self.assertIn("run_id=123", url)
109-
self.assertIn("artifact=codeboarding-pr-9-headsha", url)
110-
self.assertIn("artifact_url=https%3A%2F%2Fgithub.com%2Fo%2Fr%2Factions%2Fruns%2F123%2Fartifacts%2F456", url)
97+
def test_url_none_without_pr_or_run(self):
98+
self.assertIsNone(bc.webview_url(self.WV, "o", "r", pr="9")) # no run
99+
self.assertIsNone(bc.webview_url(self.WV, "o", "r", run_id="123")) # no pr
100+
self.assertIsNone(bc.webview_url("", "o", "r", pr="9", run_id="123")) # no base
111101

112-
def test_url_none_without_head_sha_or_base(self):
113-
self.assertIsNone(bc.webview_url(self.WV, "o", "r", "", "basesha"))
114-
self.assertIsNone(bc.webview_url("", "o", "r", "headsha", "basesha"))
102+
def test_trailing_slash_in_webview_base_is_normalized(self):
103+
a = bc.webview_url("https://app.codeboarding.org/", "o", "r", pr="9", run_id="1")
104+
b = bc.webview_url("https://app.codeboarding.org", "o", "r", pr="9", run_id="1")
105+
self.assertEqual(a, b)
106+
self.assertNotIn(".org//", a)
115107

116108
def test_cta_includes_browser_link_when_ready(self):
117109
out = bc.build_cta(
@@ -122,14 +114,12 @@ def test_cta_includes_browser_link_when_ready(self):
122114
repo_with(),
123115
issues=0,
124116
webview_base=self.WV,
125-
head_sha="headsha",
126-
base_sha="basesha",
127117
webview_ready=True,
118+
run_id="123",
128119
)
129120
self.assertIn("Explore this PR", out)
130121
self.assertIn("your [**browser**](", out)
131-
self.assertIn("ref=headsha", out)
132-
self.assertIn("compare=basesha", out)
122+
self.assertIn("/Org/Repo/pull/9?run=123", out)
133123
self.assertIn("VS Code", out) # editor merged into the same line
134124

135125
def test_cta_omits_browser_link_when_not_ready(self):
@@ -142,11 +132,10 @@ def test_cta_omits_browser_link_when_not_ready(self):
142132
repo_with(),
143133
issues=0,
144134
webview_base=self.WV,
145-
head_sha="headsha",
146-
base_sha="basesha",
147135
webview_ready=False,
136+
run_id="123",
148137
)
149-
self.assertNotIn("ref=headsha", out) # no browser link
138+
self.assertNotIn("/pull/", out) # no browser link
150139
self.assertNotIn("[**browser**]", out)
151140
self.assertIn("Explore this PR", out) # the line is still there, editor-only
152141
self.assertIn("VS Code", out)
@@ -160,12 +149,11 @@ def test_cta_omits_browser_link_when_ready_but_no_base_url(self):
160149
repo_with(),
161150
issues=0,
162151
webview_base="",
163-
head_sha="headsha",
164-
base_sha="basesha",
165152
webview_ready=True,
153+
run_id="123",
166154
)
167155
self.assertNotIn("[**browser**]", out)
168-
self.assertNotIn("ref=headsha", out)
156+
self.assertNotIn("/pull/", out)
169157

170158

171159
class TestJoinOr(unittest.TestCase):
@@ -179,9 +167,7 @@ class TestMergedExploreLine(unittest.TestCase):
179167
WV = "https://app.codeboarding.org"
180168

181169
def _ready(self, repo, cta=""):
182-
return bc.build_cta(
183-
cta, "o", "r", "1", repo, webview_base=self.WV, head_sha="h", base_sha="b", webview_ready=True
184-
)
170+
return bc.build_cta(cta, "o", "r", "1", repo, webview_base=self.WV, webview_ready=True, run_id="123")
185171

186172
def test_browser_and_single_editor_joined_with_or(self):
187173
out = self._ready(repo_with()) # default VS Code

0 commit comments

Comments
 (0)