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.
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.
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.
Added format_created_pull_url so default gh pr create output keeps the Forgejo /pulls/7 path and adds a Codex-recognized fragment marker.
Left gh pr create --json ... unchanged, so API-style callers still receive the canonical Forgejo pull request URL.
Added a small parser copy of Codex.app's URL recognition rule and a focused test proving the shim output is now detectable.
Documented why the default URL includes a fragment marker and why it exists only for Codex.app compatibility.
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.
https://host/owner/repo/pulls/7#codex-pr=/pull/7 still loads the real Forgejo pull request page./pull/7 path or fragment.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.
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.
python3 -m unittest tests.test_routing.RoutingTests.test_create_default_output_surfaces_url_for_codex_app.python3 -m unittest tests.test_routing.RoutingTests.test_create_calls_forgejo_client, confirming JSON output still returns the clean Forgejo URL.python3 -m unittest ran 99 tests successfully.pipx install --force . updated the active local wrapper package./Users/kell/.local/bin/gh-forgejo-shim version reported 0.1.1.https://git.example.com/owner/repo/pulls/7#codex-pr=/pull/7 for a Forgejo pull URL./pulls/7 directly, remove the compatibility fragment and simplify the formatter.