2026-06-17 Repository implementation Issue gh-forgejo-shim-ewk

Surfaced Forgejo PR links in Codex.app

The shim now prints a Forgejo pull request URL that Codex.app can recognize after gh pr create, without changing the real Forgejo URL or JSON output contract.

Summary

Codex.app was creating Forgejo pull requests but not surfacing the resulting link. The local Codex.app bundle showed that its create-PR action scans command output for an HTTP URL containing GitHub-style /pull/7. Forgejo correctly returns /pulls/7, so the URL was skipped. This change appends a harmless fragment marker, such as #codex-pr=/pull/7, to default human output only.

Changes Made

Codex Link Marker

Added format_created_pull_url so default gh pr create output keeps the Forgejo /pulls/7 path and adds a Codex-recognized fragment marker.

Clean JSON Output

Left gh pr create --json ... unchanged, so API-style callers still receive the canonical Forgejo pull request URL.

Regression Test

Added a small parser copy of Codex.app's URL recognition rule and a focused test proving the shim output is now detectable.

README Note

Documented why the default URL includes a fragment marker and why it exists only for Codex.app compatibility.

Context

The important distinction is that the Forgejo URL was already valid for a browser, but Codex.app was filtering it out before presenting the link. The app's local create-PR action combines stdout and stderr, extracts HTTP URLs, trims trailing punctuation, and accepts only URLs that match /pull/\d+. A Forgejo URL like https://git.example.com/owner/repo/pulls/7 therefore failed the GUI's link detector even though it pointed to the right page.

Important Implementation Details

Relevant Diff Snippets

Focused unified diff fallback. It shows the output formatter, the Codex-style regression test, and the README note without embedding unrelated context.

diff --git a/src/gh_forgejo_shim/routing.py b/src/gh_forgejo_shim/routing.py
@@
 SUPPORTED_ISSUE_COMMANDS = {"create", "list", "ls", "new", "view"}
 SUPPORTED_REPO_COMMANDS = {"view"}
 SUPPORTED_AUTH_COMMANDS = {"status", "token"}
+CODEX_CREATE_PR_URL_RE = re.compile(r"/pull/\d+(?:\b|$)")
@@
     if options.json_fields:
         print(json.dumps(filter_fields(normalized, options.json_fields), sort_keys=True), file=stdout)
     else:
-        print(normalized.get("url") or pull.get("html_url") or pull.get("url") or "", file=stdout)
+        created_url = normalized.get("url") or pull.get("html_url") or pull.get("url") or ""
+        print(format_created_pull_url(created_url), file=stdout)
     return 0
@@
+def format_created_pull_url(value: object) -> str:
+    if not isinstance(value, str) or not value:
+        return ""
+    if CODEX_CREATE_PR_URL_RE.search(value):
+        return value
+    number = _number_from_url(value, kind="pull")
+    if number is None:
+        return value
+    marker = f"codex-pr=/pull/{number}"
+    parsed = urllib.parse.urlparse(value)
+    if CODEX_CREATE_PR_URL_RE.search(parsed.fragment):
+        return value
+    fragment = f"{parsed.fragment}&{marker}" if parsed.fragment else marker
+    return urllib.parse.urlunparse(parsed._replace(fragment=fragment))

diff --git a/tests/test_routing.py b/tests/test_routing.py
@@
+def codex_create_pr_url(output: str) -> str | None:
+    for match in re.finditer(r"https?://\S+", output):
+        url = match.group(0).rstrip("),.")
+        if re.search(r"/pull/\d+(?:\b|$)", url):
+            return url
+    return None
@@
+    def test_create_default_output_surfaces_url_for_codex_app(self) -> None:
+        client = FakeClient()
+        out = io.StringIO()
+        code = run_forgejo_pr(
+            ["pr", "create", "-R", "git.example.com/owner/repo", "--title", "Ship it", "--body", "body", "--base", "main", "--head", "feature"],
+            RepoRef("git.example.com", "owner", "repo"),
+            client,
+            stdout=out,
+            stderr=io.StringIO(),
+        )
+        self.assertEqual(code, 0)
+        self.assertEqual(
+            codex_create_pr_url(out.getvalue()),
+            "https://git.example.com/owner/repo/pulls/7#codex-pr=/pull/7",
+        )

diff --git a/README.md b/README.md
@@
 GitHub-only metadata flags such as reviewers, labels, assignees, projects, milestones, templates, recover, and maintainer-edit controls fail with a Forgejo-specific error.
+By default, `gh pr create` prints the created Forgejo pull request URL. For Codex.app compatibility, Forgejo `/pulls/7` URLs include a harmless fragment marker like `#codex-pr=/pull/7`, because Codex.app's create-PR action recognizes GitHub-style `/pull/7` URL markers when surfacing the new PR link.

Expected Impact for End-Users

After Codex.app creates a Forgejo pull request through the shim, the app should be able to surface the clickable PR link instead of silently losing it. CLI users still see a URL that opens the Forgejo pull request page, and JSON consumers continue to receive the unmodified Forgejo URL.

Validation

Issues, Limitations, and Mitigations

Follow-up Work