From f20517f243e88a355adc39639176e31cda2aeb57 Mon Sep 17 00:00:00 2001 From: Svilen Stefanov Date: Thu, 11 Jun 2026 17:26:40 +0200 Subject: [PATCH 1/2] Link GitHub action with CodeBoarding-webview --- README.md | 2 + action.yml | 86 ++++++++++++++++++++++++++++++++++++++++- scripts/build_cta.py | 61 +++++++++++++++++++++++++++-- tests/test_build_cta.py | 46 ++++++++++++++++++++++ 4 files changed, 189 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 6063451..0927c61 100644 --- a/README.md +++ b/README.md @@ -166,6 +166,8 @@ The command needs the `issue_comment` trigger and runs from your default branch | `comment_header` | `Architecture review` | Heading for the PR comment. | | `trigger_command` | `/codeboarding` | Slash command for trusted on-demand runs. | | `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). | +| `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. | +| `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). | ## Outputs diff --git a/action.yml b/action.yml index 34349fa..be6427a 100644 --- a/action.yml +++ b/action.yml @@ -54,6 +54,14 @@ inputs: 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.' required: false default: '' + webview_base_url: + 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.' + required: false + default: 'https://app.codeboarding.org' + commit_head_analysis: + 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.' + required: false + default: 'true' trigger_command: 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.' required: false @@ -88,6 +96,7 @@ runs: PR_NUMBER_PULL: ${{ github.event.pull_request.number }} PULL_BASE_SHA: ${{ github.event.pull_request.base.sha }} PULL_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + PULL_HEAD_REF: ${{ github.event.pull_request.head.ref }} PULL_BASE_REF: ${{ github.event.pull_request.base.ref }} PULL_BASE_REPO: ${{ github.event.pull_request.base.repo.full_name }} PULL_HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }} @@ -115,6 +124,7 @@ runs: PR_NUMBER="$PR_NUMBER_PULL" BASE_SHA="$PULL_BASE_SHA" HEAD_SHA="$PULL_HEAD_SHA" + HEAD_REF="$PULL_HEAD_REF" BASE_REF="$PULL_BASE_REF" BASE_REPO="$PULL_BASE_REPO" HEAD_REPO="$PULL_HEAD_REPO" @@ -137,6 +147,7 @@ runs: PR_JSON="$(gh api "repos/${REPOSITORY}/pulls/${PR_NUMBER}" 2>/dev/null)" || skip "Could not fetch PR #$PR_NUMBER from the API." 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." 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." + HEAD_REF="$(printf '%s' "$PR_JSON" | python3 -c 'import json,sys;print(json.load(sys.stdin)["head"]["ref"])' 2>/dev/null)" || HEAD_REF="" BASE_REF="$(printf '%s' "$PR_JSON" | python3 -c 'import json,sys;print(json.load(sys.stdin)["base"]["ref"])' 2>/dev/null)" || BASE_REF="" 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." 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: echo "pr_number=$PR_NUMBER" echo "base_sha=$BASE_SHA" echo "head_sha=$HEAD_SHA" + echo "head_ref=$HEAD_REF" echo "base_ref=$BASE_REF" echo "base_repo=$BASE_REPO" echo "head_repo=$HEAD_REPO" + # same_repo gates pushing the head analysis: forks give a read-only token. + if [ "$HEAD_REPO" = "$REPOSITORY" ]; then echo "same_repo=true"; else echo "same_repo=false"; fi } >> "$GITHUB_OUTPUT" echo "Resolved PR #$PR_NUMBER (base=$BASE_REPO@$BASE_SHA head=$HEAD_REPO@$HEAD_SHA) via $EVENT" @@ -576,6 +590,62 @@ runs: "${RUNNER_TEMP}/cb-agent-model" \ "${RUNNER_TEMP}/cb-parsing-model" + # Commit the generated head analysis (+ health report) to the PR branch so the + # hosted webview can fetch .codeboarding/analysis.json at the head SHA and open + # this PR's head-vs-base diff. Same-repo PRs only — the token is read-only on + # forks (the step is skipped there and the webview link is omitted). The push + # creates a NEW head commit; its SHA (webview_sha) is what the comment links to. + - name: Commit head analysis to PR branch + id: commit_head + if: >- + steps.guard.outputs.skip != 'true' + && inputs.commit_head_analysis == 'true' + && steps.guard.outputs.same_repo == 'true' + && inputs.webview_base_url != '' + shell: bash + working-directory: target-repo + env: + GH_TOKEN: ${{ inputs.github_token }} + HEAD_DIR: ${{ steps.base.outputs.head_dir }} + HEAD_REF: ${{ steps.guard.outputs.head_ref }} + HEAD_SHA: ${{ steps.guard.outputs.head_sha }} + REPOSITORY: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + run: | + echo "ready=false" >> "$GITHUB_OUTPUT" + echo "webview_sha=$HEAD_SHA" >> "$GITHUB_OUTPUT" + [ -n "$HEAD_REF" ] || { echo "::notice::No head branch ref resolved; skipping head-analysis commit."; exit 0; } + [ -f "$HEAD_DIR/analysis.json" ] || { echo "::notice::No head analysis.json to commit."; exit 0; } + + mkdir -p .codeboarding/health + cp "$HEAD_DIR/analysis.json" .codeboarding/analysis.json + if [ -f "$HEAD_DIR/health/health_report.json" ]; then + cp "$HEAD_DIR/health/health_report.json" .codeboarding/health/health_report.json + fi + + git add .codeboarding/analysis.json .codeboarding/health/health_report.json 2>/dev/null || git add .codeboarding/analysis.json + if git diff --cached --quiet; then + echo "::notice::Head analysis unchanged; nothing to commit." + echo "ready=true" >> "$GITHUB_OUTPUT" # already committed at this SHA → webview can read it + exit 0 + fi + + git config user.name "codeboarding[bot]" + git config user.email "codeboarding[bot]@users.noreply.github.com" + git commit -m "chore(codeboarding): update architecture analysis [skip ci]" >/dev/null + + # Push to the PR head branch. The checkout used persist-credentials:false, so + # authenticate the push explicitly with the workflow token (same-repo only). + AUTH_URL="https://x-access-token:${GH_TOKEN}@${SERVER_URL#https://}/${REPOSITORY}.git" + if git push "$AUTH_URL" "HEAD:refs/heads/${HEAD_REF}" 2>/dev/null; then + NEW_SHA="$(git rev-parse HEAD)" + echo "webview_sha=$NEW_SHA" >> "$GITHUB_OUTPUT" + echo "ready=true" >> "$GITHUB_OUTPUT" + echo "Committed head analysis to ${HEAD_REF} as ${NEW_SHA}." + else + echo "::warning::Could not push head analysis to ${HEAD_REF}; the webview link will be omitted." + fi + - name: Diff analyses → Mermaid if: steps.guard.outputs.skip != 'true' id: diagram @@ -634,6 +704,12 @@ runs: TRUNC: ${{ steps.diagram.outputs.truncated }} PR: ${{ steps.guard.outputs.pr_number }} ISSUES: ${{ steps.health.outputs.issues }} + WEBVIEW_BASE: ${{ inputs.webview_base_url }} + # SHA the head analysis.json was committed at (post-push), and whether that + # commit succeeded — gates the webview "explore in browser" link. + WEBVIEW_SHA: ${{ steps.commit_head.outputs.webview_sha }} + WEBVIEW_READY: ${{ steps.commit_head.outputs.ready }} + BASE_SHA: ${{ steps.guard.outputs.base_sha }} run: | BODY_FILE=$(mktemp) OWNER="${OWNER_REPO%%/*}"; REPO="${OWNER_REPO##*/}" @@ -646,11 +722,17 @@ runs: } # CTA footer: an editor link (proxy deep-link when CTA_BASE is set, else the - # extension's https listing — GitHub strips vscode:/cursor:) plus the ⚠️ banner. + # extension's https listing — GitHub strips vscode:/cursor:), the ⚠️ banner, + # and — when the head analysis was committed (WEBVIEW_READY) — a webview + # "explore in browser" link to this PR's head-vs-base diff. cta() { + local extra=() + if [ "$WEBVIEW_READY" = "true" ]; then + extra+=(--webview-ready --webview-base "$WEBVIEW_BASE" --head-sha "$WEBVIEW_SHA" --base-sha "$BASE_SHA") + fi python3 "$ACTION_PATH/scripts/build_cta.py" \ --cta-base "$CTA_BASE" --owner "$OWNER" --repo "$REPO" --pr "$PR" \ - --repo-path "$TARGET_REPO" --issues "${ISSUES:-0}" + --repo-path "$TARGET_REPO" --issues "${ISSUES:-0}" "${extra[@]}" } { diff --git a/scripts/build_cta.py b/scripts/build_cta.py index 5ee509d..8cb29aa 100644 --- a/scripts/build_cta.py +++ b/scripts/build_cta.py @@ -8,8 +8,9 @@ custom ``vscode:``/``cursor:`` schemes — a deep link would render as dead text — so the editor link points at the extension's plain-https listing instead (VS Code Marketplace, Cursor via Open VSX), which is the only clickable option. A no-install -hosted-webview ("explore in browser") tier is intentionally deferred (see -docs/COMMIT_STRATEGY.md) — the committed analysis already supports it later. +hosted-webview ("explore in browser") line is added when ``webview_ready`` — i.e. the +head ``analysis.json`` was committed to the PR branch and this isn't a fork PR — so +the webview can fetch a committed analysis at the head SHA (see docs/COMMIT_STRATEGY.md). Editor coverage is deliberately limited to **VS Code and Cursor**. Per the 2025 Stack Overflow Developer Survey (https://survey.stackoverflow.co/2025/technology/), @@ -58,7 +59,37 @@ def detect_editors(repo_path: Path) -> list[str]: } -def build_cta(cta_base: str, owner: str, repo: str, pr: str, repo_path: Path, issues: int = 0) -> str: +def build_webview_link(webview_base: str, owner: str, repo: str, head_sha: str, base_sha: str) -> str | None: + """Return the markdown "explore in browser" line, or None if not buildable. + + Deep-links the hosted webview straight to this PR's head-vs-base architecture + diff: ``?repo=owner/repo&ref=&compare=``. Pinned to exact + SHAs so the committed ``analysis.json`` the webview fetches matches this run. For + a private repo the webview itself sends the viewer through GitHub sign-in and then + loads the same diff. Returns None when the base/head pieces aren't all present. + """ + if not (webview_base and owner and repo and head_sha): + return None + base = webview_base.rstrip("/") + params = {"repo": f"{owner}/{repo}", "ref": head_sha} + if base_sha: + params["compare"] = base_sha + return f"🌐 [**Explore this PR’s architecture in your browser →**]({base}/?{urlencode(params)})" + + +def build_cta( + cta_base: str, + owner: str, + repo: str, + pr: str, + repo_path: Path, + issues: int = 0, + *, + webview_base: str = "", + head_sha: str = "", + base_sha: str = "", + webview_ready: bool = False, +) -> str: """Return the markdown CTA footer: a health-warning banner plus an editor link. With a ``cta_base`` proxy the links route through it (owner/repo/pr tracked), @@ -66,12 +97,22 @@ def build_cta(cta_base: str, owner: str, repo: str, pr: str, repo_path: Path, is a proxy the editor link is the extension's https listing (GitHub strips custom ``vscode:``/``cursor:`` schemes), and the redundant install link is dropped. The ⚠️ banner shows whenever ``issues > 0``. + + When ``webview_ready`` (the head ``analysis.json`` was committed and this isn't a + fork PR) a "explore in browser" line deep-links the hosted webview to this PR's + head-vs-base diff. Otherwise that line is omitted (the webview couldn't fetch a + committed analysis at the head SHA). """ parts: list[str] = [] if issues > 0: noun = "issue" if issues == 1 else "issues" parts.append(f"⚠️ **{issues} architecture {noun} found** — open CodeBoarding to explore them.") + if webview_ready: + webview_line = build_webview_link(webview_base, owner, repo, head_sha, base_sha) + if webview_line: + parts.append(webview_line) + editors = detect_editors(repo_path) if cta_base: base = cta_base.rstrip("/") @@ -104,13 +145,25 @@ def main() -> int: p.add_argument("--pr", required=True) p.add_argument("--repo-path", required=True, type=Path, help="Path to the analyzed repo checkout") p.add_argument("--issues", default="0", help="Real architecture-issue count (0 -> no warning banner)") + p.add_argument("--webview-base", default="", help="Hosted webview base URL (e.g. https://app.codeboarding.org)") + p.add_argument("--head-sha", default="", help="PR head SHA the webview link pins to") + p.add_argument("--base-sha", default="", help="PR base SHA the webview link compares against") + p.add_argument( + "--webview-ready", + action="store_true", + help="Emit the webview link (head analysis.json was committed; not a fork PR)", + ) args = p.parse_args() try: issues = int(args.issues or 0) except ValueError: issues = 0 - print(build_cta(args.cta_base, args.owner, args.repo, args.pr, args.repo_path, issues)) + print(build_cta( + args.cta_base, args.owner, args.repo, args.pr, args.repo_path, issues, + webview_base=args.webview_base, head_sha=args.head_sha, base_sha=args.base_sha, + webview_ready=args.webview_ready, + )) return 0 diff --git a/tests/test_build_cta.py b/tests/test_build_cta.py index 1373315..d6264b8 100644 --- a/tests/test_build_cta.py +++ b/tests/test_build_cta.py @@ -77,5 +77,51 @@ def test_trailing_slash_in_base_is_normalized(self): self.assertEqual(a, b) +class TestWebviewLink(unittest.TestCase): + WV = "https://app.codeboarding.org" + + def test_link_built_with_head_ref_and_compare_base(self): + link = bc.build_webview_link(self.WV, "Org", "Repo", "headsha", "basesha") + self.assertIn("https://app.codeboarding.org/?", link) + self.assertIn("repo=Org%2FRepo", link) + self.assertIn("ref=headsha", link) + self.assertIn("compare=basesha", link) + + def test_link_omits_compare_when_no_base(self): + link = bc.build_webview_link(self.WV, "o", "r", "headsha", "") + self.assertIn("ref=headsha", link) + self.assertNotIn("compare=", link) + + def test_link_none_without_head_sha_or_base(self): + self.assertIsNone(bc.build_webview_link(self.WV, "o", "r", "", "basesha")) + self.assertIsNone(bc.build_webview_link("", "o", "r", "headsha", "basesha")) + + def test_cta_emits_webview_line_when_ready(self): + out = bc.build_cta( + "", "Org", "Repo", "9", repo_with(), issues=0, + webview_base=self.WV, head_sha="headsha", base_sha="basesha", webview_ready=True, + ) + self.assertIn("Explore this PR", out) + self.assertIn("ref=headsha", out) + self.assertIn("compare=basesha", out) + + def test_cta_omits_webview_line_when_not_ready(self): + # Fork PR / head analysis not committed -> webview can't fetch at head SHA. + out = bc.build_cta( + "", "Org", "Repo", "9", repo_with(), issues=0, + webview_base=self.WV, head_sha="headsha", base_sha="basesha", webview_ready=False, + ) + self.assertNotIn("Explore this PR", out) + # Editor CTA is still present regardless. + self.assertIn("Open in VS Code", out) + + def test_cta_omits_webview_line_when_ready_but_no_base_url(self): + out = bc.build_cta( + "", "Org", "Repo", "9", repo_with(), issues=0, + webview_base="", head_sha="headsha", base_sha="basesha", webview_ready=True, + ) + self.assertNotIn("Explore this PR", out) + + if __name__ == "__main__": unittest.main() From 7e07060289fb7516657e6c428fdec5e631fbfd72 Mon Sep 17 00:00:00 2001 From: Svilen Stefanov Date: Thu, 11 Jun 2026 17:40:04 +0200 Subject: [PATCH 2/2] Allow writes in CI for analysis.json to be commited --- .github/workflows/codeboarding.yml | 4 +++- README.md | 5 ++++- action.yml | 15 ++++++++++--- scripts/build_cta.py | 19 +++++++++++----- tests/test_build_cta.py | 36 +++++++++++++++++++++++++----- 5 files changed, 63 insertions(+), 16 deletions(-) diff --git a/.github/workflows/codeboarding.yml b/.github/workflows/codeboarding.yml index 451103d..e5362fe 100644 --- a/.github/workflows/codeboarding.yml +++ b/.github/workflows/codeboarding.yml @@ -11,7 +11,9 @@ on: types: [created] permissions: - contents: read + # write: the action commits the generated .codeboarding/analysis.json back to the + # PR branch so the webview can open this PR's diff at the head SHA (same-repo PRs). + contents: write pull-requests: write issues: write diff --git a/README.md b/README.md index 0927c61..7744753 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,9 @@ on: types: [created] permissions: - contents: read + # write lets the action commit analysis.json to the PR branch so the comment can + # link to the webview diff. Drop to `read` to keep the comment without that link. + contents: write pull-requests: write issues: write @@ -156,6 +158,7 @@ The command needs the `issue_comment` trigger and runs from your default branch | `llm_api_key` | required | Your LLM provider API key (see `llm_provider`). | | `llm_provider` | `openrouter` | Provider for the key, mapped to `_API_KEY` (e.g. `anthropic`, `openai`, `google`). | | `github_token` | `${{ github.token }}` | Token used to post or update the PR comment. | +| `push_token` | `${{ github.token }}` | Token used to push the generated `analysis.json` to the PR branch (for the webview link). The workflow token can push when the workflow grants `permissions: contents: write`. Separate from `github_token` so commenting can use a GitHub App token while the push uses the workflow token. | | `engine_ref` | `v0.12.0` | CodeBoarding engine ref. Pin for reproducibility. | | `depth_level` | `1` | Analysis depth, 1 to 3. Higher is slower and richer. | | `render_depth` | `1` | Display depth for the PR diagram. Keep `1` for a clean top-level view. | diff --git a/action.yml b/action.yml index be6427a..e9b24b8 100644 --- a/action.yml +++ b/action.yml @@ -18,6 +18,10 @@ inputs: description: 'GITHUB_TOKEN used to post the PR comment. Defaults to the workflow token.' required: false default: ${{ github.token }} + push_token: + description: 'Token used to push the generated .codeboarding/analysis.json to the PR branch (for the webview link). Defaults to the workflow github.token, which can push when the calling workflow grants "permissions: contents: write". Kept separate from github_token so commenting can use a GitHub App token while the push uses the workflow token (whose write access the consumer controls).' + required: false + default: ${{ github.token }} engine_ref: description: 'Git ref (tag/branch/SHA) of CodeBoarding/CodeBoarding used as the analysis engine. Pinned to a release for reproducibility; override to track a newer ref.' required: false @@ -605,7 +609,10 @@ runs: shell: bash working-directory: target-repo env: - GH_TOKEN: ${{ inputs.github_token }} + # Push with push_token (defaults to the workflow github.token, gated by the + # consumer's `permissions: contents: write`) — NOT github_token, which may be + # a GitHub App token used only for commenting and need not have write access. + GH_TOKEN: ${{ inputs.push_token }} HEAD_DIR: ${{ steps.base.outputs.head_dir }} HEAD_REF: ${{ steps.guard.outputs.head_ref }} HEAD_SHA: ${{ steps.guard.outputs.head_sha }} @@ -636,14 +643,16 @@ runs: # Push to the PR head branch. The checkout used persist-credentials:false, so # authenticate the push explicitly with the workflow token (same-repo only). + # Requires `contents: write` on the job's token — a read-only token (the + # default) is rejected here; the push error below names the cause. AUTH_URL="https://x-access-token:${GH_TOKEN}@${SERVER_URL#https://}/${REPOSITORY}.git" - if git push "$AUTH_URL" "HEAD:refs/heads/${HEAD_REF}" 2>/dev/null; then + if git push "$AUTH_URL" "HEAD:refs/heads/${HEAD_REF}"; then NEW_SHA="$(git rev-parse HEAD)" echo "webview_sha=$NEW_SHA" >> "$GITHUB_OUTPUT" echo "ready=true" >> "$GITHUB_OUTPUT" echo "Committed head analysis to ${HEAD_REF} as ${NEW_SHA}." else - echo "::warning::Could not push head analysis to ${HEAD_REF}; the webview link will be omitted." + echo "::warning::Could not push head analysis to ${HEAD_REF}; the webview link will be omitted. Most likely the job's token lacks 'contents: write' (add 'permissions: contents: write' to the calling workflow), or the branch is protected against this pusher." fi - name: Diff analyses → Mermaid diff --git a/scripts/build_cta.py b/scripts/build_cta.py index 8cb29aa..0a4947e 100644 --- a/scripts/build_cta.py +++ b/scripts/build_cta.py @@ -159,11 +159,20 @@ def main() -> int: issues = int(args.issues or 0) except ValueError: issues = 0 - print(build_cta( - args.cta_base, args.owner, args.repo, args.pr, args.repo_path, issues, - webview_base=args.webview_base, head_sha=args.head_sha, base_sha=args.base_sha, - webview_ready=args.webview_ready, - )) + print( + build_cta( + args.cta_base, + args.owner, + args.repo, + args.pr, + args.repo_path, + issues, + webview_base=args.webview_base, + head_sha=args.head_sha, + base_sha=args.base_sha, + webview_ready=args.webview_ready, + ) + ) return 0 diff --git a/tests/test_build_cta.py b/tests/test_build_cta.py index d6264b8..22226fc 100644 --- a/tests/test_build_cta.py +++ b/tests/test_build_cta.py @@ -98,8 +98,16 @@ def test_link_none_without_head_sha_or_base(self): def test_cta_emits_webview_line_when_ready(self): out = bc.build_cta( - "", "Org", "Repo", "9", repo_with(), issues=0, - webview_base=self.WV, head_sha="headsha", base_sha="basesha", webview_ready=True, + "", + "Org", + "Repo", + "9", + repo_with(), + issues=0, + webview_base=self.WV, + head_sha="headsha", + base_sha="basesha", + webview_ready=True, ) self.assertIn("Explore this PR", out) self.assertIn("ref=headsha", out) @@ -108,8 +116,16 @@ def test_cta_emits_webview_line_when_ready(self): def test_cta_omits_webview_line_when_not_ready(self): # Fork PR / head analysis not committed -> webview can't fetch at head SHA. out = bc.build_cta( - "", "Org", "Repo", "9", repo_with(), issues=0, - webview_base=self.WV, head_sha="headsha", base_sha="basesha", webview_ready=False, + "", + "Org", + "Repo", + "9", + repo_with(), + issues=0, + webview_base=self.WV, + head_sha="headsha", + base_sha="basesha", + webview_ready=False, ) self.assertNotIn("Explore this PR", out) # Editor CTA is still present regardless. @@ -117,8 +133,16 @@ def test_cta_omits_webview_line_when_not_ready(self): def test_cta_omits_webview_line_when_ready_but_no_base_url(self): out = bc.build_cta( - "", "Org", "Repo", "9", repo_with(), issues=0, - webview_base="", head_sha="headsha", base_sha="basesha", webview_ready=True, + "", + "Org", + "Repo", + "9", + repo_with(), + issues=0, + webview_base="", + head_sha="headsha", + base_sha="basesha", + webview_ready=True, ) self.assertNotIn("Explore this PR", out)