Skip to content

feat: emit URL on marked final line after create/edit/comment#18

Merged
ultimatile merged 9 commits into
mainfrom
feat/17-marked-final-url-line
May 19, 2026
Merged

feat: emit URL on marked final line after create/edit/comment#18
ultimatile merged 9 commits into
mainfrom
feat/17-marked-final-url-line

Conversation

@ultimatile
Copy link
Copy Markdown
Owner

@ultimatile ultimatile commented May 19, 2026

Summary

Closes #17.

Reorders post-action output on a successful gh-post issue|pr create|edit|comment so the auto-gh view body is printed first and a single marker line carries the URL last.
The marker has the form [gh-post] {created|edited|commented}: <URL>, making the URL recoverable by tail -N or grep '[gh-post] ' regardless of view body length.
This closes the duplicate-create regression where a long view body pushed gh's early URL echo out of tail -N range and led callers to retry the action.

Changes

  • gh_post/subcommands/post.py: add extract_url (path-anchored, host-agnostic) and _is_dry_run (pflag-aware, last-occurrence semantics, POSIX -- honored); reorder cmd_post success path; return the JSON URL from view_short for use as the marker URL when gh edit exits silently; emit the marker line as the last stdout line.
  • gh_post/_subprocess.py: factor _emit_stream(text, stream) from _emit_subprocess_output so the single-stream mirror in the success path shares the trailing-newline write shape.
  • gh_post/__init__.py: re-export the new helpers.
  • README.md: document the post-action output contract and the precise --no-view interaction.
  • test_gh_post.py: marker-line, ordering, --no-view, failure, GitHub Enterprise, dry-run, pflag-form, and -- separator tests.

Test plan

uv run pytest test_gh_post.py -q — 172 passed.
uv run ruff check / uv run ruff format --check — clean.

Behavior coverage: marker placement under create / edit / comment; ordering vs. view body; --no-view; gh failure (no marker); gh silent-stdout edit (marker recovered via view JSON); current-repo edit without --repo; GitHub Enterprise create / comment; --dry-run / --dry-run=true / --dry-run=false; pflag last-occurrence override; POSIX -- end-of-options separator.

Out of scope

The marker convention is intentionally not applied to comment-edit (PATCH on existing comments) or reply-inline (batched inline review replies).
Those subcommands have different output contracts — comment-edit does not run an auto-followup view, and reply-inline already emits per-reply un-sent indices on failure.
Extending the marker convention to them is left to a separate change when a caller surfaces a concrete need.

Notes

The marker URL extractor is host-agnostic.
It path-anchors on /issues/N or /pull/N, so GitHub Enterprise hosts work without the wrapper learning the host out-of-band.

--no-view is precise.
The marker fires when gh printed the target URL on stdout (the standard behavior of gh create|edit|comment).
The wrapper does not issue an additional network call to recover an unusually silent invocation.

After a successful `gh-post issue|pr create|edit|comment`, the wrapper
now suppresses the early `gh` stdout URL echo, runs the auto-`gh view`
summary first, and emits a final marker line of the form
`[gh-post] {created|edited|commented}: <URL>`. The marker is greppable
and is always the last stdout line, so callers using `tail -N` recover
the URL regardless of view body length — closing the duplicate-create
regression where a long view body pushed the URL out of tail range.

On `gh` failure or on a clean exit without a recognizable github.com
URL, the marker is suppressed and stdout is mirrored verbatim so no
information is silently dropped.
`gh issue|pr edit` can exit 0 without printing the target URL on
stdout under some versions / configurations. The earlier URL-only
gate dropped both the view summary and the `[gh-post] edited:`
marker line in that case even though the wrapper already knows the
target number and repo from argv.

On the edit path, synthesize the URL from --repo + number when
extraction from gh stdout returns None, so the view + marker
contract still holds. Create and comment retain the URL-required
behavior — the wrapper has no other source of truth for the issue /
PR number on create or the comment id on comment.
The previous edit-fallback synthesis hardcoded `https://github.com`
as the host. That breaks against GitHub Enterprise repositories
(`--repo HOST/OWNER/REPO`) and also failed for current-repo edits
invoked without `--repo` at all, since the synthesizer required the
caller to have supplied `--repo`.

Make `view_short` return its JSON-reported URL alongside the rc, and
let `cmd_post` pick `stdout_url or view_url` for the marker. The
follow-up `gh view` already runs for create/edit (target number is
known either from argv or from the host-agnostic stdout regex), so
its JSON URL is a host-aware fallback that handles both GHE and the
silent-edit case without any hardcoded host.
`_URL_RE` was anchored to `github.com`, so URLs printed by `gh` on
Enterprise hosts (`--repo HOST/OWNER/REPO`) slipped past extraction.
For comments — which run no auto-view fallback — that meant no
marker line at all on Enterprise.

Re-anchor on the issue / PR path (`/issues/N` or `/pull/N`) instead,
which is host-agnostic but still rejects stray help URLs (e.g.
`https://docs.github.com/manual`) in warning text.

Also tighten the `--no-view` README note: the marker fires as long as
`gh` printed the target URL on stdout (the standard behavior); the
wrapper does not issue an additional network call to recover a URL
`gh` did not print, which would contradict the spirit of `--no-view`.
`gh pr create --dry-run` succeeds without creating anything and
prints the would-be body to stdout. If the body contains an
issue / PR URL — a common case for `Closes #N` style references —
the new marker contract would emit `[gh-post] created: <body URL>`
for a non-event.

Detect `--dry-run` in the passthrough args and fall back to verbatim
gh output mirroring for that case. No marker, no auto-view.
`gh` uses pflag, which also accepts the boolean-form
`--dry-run=true` (and `1`, `t`, `T`, `TRUE`, `True`). The earlier
exact-token `--dry-run` check missed those, allowing a dry-run
invocation to fall through to the marker / view path and emit a
spurious `[gh-post] created:` line for a body-embedded URL.

Promote the detection to `_is_dry_run`, which accepts the bare form
and any pflag truthy literal in the `--dry-run=` form, and rejects
the explicit falsy literals (`--dry-run=false` and friends) so a
caller can selectively re-enable the marker contract.
pflag processes repeated flags in order and uses the final value, so
`--dry-run --dry-run=false` ends up with dry-run disabled and gh
performs the real action. The earlier first-match-wins detection
would incorrectly suppress the success marker in that case.

Scan every occurrence and let the last one decide. Both
`--dry-run=false` followed by bare `--dry-run` and bare `--dry-run`
followed by `--dry-run=false` are now correctly resolved.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements issue #17 by reordering the success-path output of gh-post issue|pr create|edit|comment so the auto-gh view summary prints first and a single [gh-post] {created|edited|commented}: <URL> marker line is always last on stdout. This makes the URL reliably recoverable via tail -N or grep, fixing a duplicate-create regression where long view bodies pushed the URL out of tail -N range.

Changes:

  • Add extract_url (path-anchored, host-agnostic regex supporting GHE + comment fragments) and _is_dry_run (pflag-aware, last-occurrence semantics) helpers; change view_short to return (rc, url) so the marker URL can fall back to the JSON-reported URL on silent gh edit.
  • Reorder cmd_post success path to suppress gh stdout, run the view, and emit the marker line last; suppress marker on --dry-run and on gh failure.
  • Re-export new helpers in gh_post/__init__.py; document the contract in README.md; add extensive tests covering ordering, --no-view, failure, GHE hosts, dry-run pflag forms, and silent-edit fallback.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
gh_post/subcommands/post.py Adds extract_url, _is_dry_run, _MARKER_VERB; updates view_short to return URL; rewrites cmd_post success path to emit the marker line last.
gh_post/init.py Re-exports new helpers and constants for test/public access.
README.md Documents post-action output contract and --no-view interaction.
test_gh_post.py Adds tests for extract_url, _is_dry_run, marker ordering, GHE hosts, failure/no-view/dry-run handling, and silent-edit fallback.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread gh_post/subcommands/post.py Outdated
Comment thread gh_post/subcommands/post.py
- Remove the "even though `extract_url` is github.com-only" clause
  from `cmd_post`'s target_number comment; after the
  path-anchored refactor, both `extract_url` and
  `extract_created_number` are host-agnostic.
- Document the shared scan limitation in `_is_dry_run`: a literal
  `--dry-run` passed as another flag's value (`--title --dry-run`)
  is mis-detected. The same shape applies to `extract_repo`,
  `extract_url`, and `extract_created_number`. The recommended
  workaround is the single-argument `--title=--dry-run` form.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

Comment thread gh_post/subcommands/post.py Outdated
Comment on lines +276 to +334
# Success path. Mirror stderr verbatim (warnings, deprecation
# notices) but suppress gh's stdout — the URL it prints is
# reordered to the trailing marker line below so a `tail -N`
# heuristic surfaces the URL rather than the tail of the view
# body.
if result.stderr:
sys.stderr.write(result.stderr)
if not result.stderr.endswith("\n"):
sys.stderr.write("\n")

# Extracted URL from gh's stdout, when present, is the
# authoritative marker source. For create/comment it is the
# only source — the wrapper has no other way to learn the
# newly-allocated issue / PR number or the new comment id.
# For edit, gh may exit cleanly without printing the URL on
# some versions / configurations; the view call below then
# supplies the URL from its JSON response.
stdout_url = extract_url(result.stdout)

# Determine target_number for the optional auto-followup view.
# edit / comment: already in argv. create: parse from stdout
# using a host-agnostic regex so GitHub Enterprise URLs are
# handled.
target_number: Optional[str] = None
if spec["number"]:
target_number = args.number
else:
target_number = extract_created_number(result.stdout)

view_url: Optional[str] = None
if not args.no_view and args.action != "comment" and target_number is not None:
_, view_url = view_short(
args.kind,
target_number,
extract_repo(args.passthrough),
args.verbose,
)

marker_url = stdout_url or view_url

if marker_url is None:
# Clean exit without any recoverable URL — neither gh's
# stdout nor a view call yielded one (or view was skipped
# for comment / --no-view). Mirror stdout so the
# information is not silently dropped, and skip the
# marker.
if result.stdout:
sys.stdout.write(result.stdout)
if not result.stdout.endswith("\n"):
sys.stdout.write("\n")
return 0

# Final marker line so callers can `tail -N` or grep
# `[gh-post] ` to extract the URL regardless of view body
# length. The marker is the last stdout line on the success
# path by construction; anything appended after this would
# break that contract and is caught by the marker-ordering
# test.
print(f"[gh-post] {_MARKER_VERB[args.action]}: {marker_url}")
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged and documented in the next commit. For the wrapped subcommands gh currently prints only the target URL on stdout (informational content goes to stderr), so suppressing all of stdout is safe today. The comment now flags this as an assumption that needs a per-line URL scrub if upstream starts emitting non-URL stdout content.

Comment thread gh_post/subcommands/post.py
- Extract `_emit_stream(text, stream)` from `_emit_subprocess_output`
  so the success path in `cmd_post` (which mirrors only stderr, or
  only stdout in the no-URL fallback) shares the single-newline
  write shape with the existing combined helper instead of
  open-coding it.
- Strengthen the stdout-suppression comment in `cmd_post` to make
  the assumption explicit: the wrapped `gh` subcommands currently
  print only the target URL on stdout; if upstream ever starts
  emitting informational stdout content, the suppression branch
  will need to scrub only the URL line.
- Make `_is_dry_run` stop at the POSIX-style `--` end-of-options
  separator so a literal `--dry-run` token passed as a positional
  value is not mis-detected as a flag.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

@ultimatile ultimatile merged commit cfb8b82 into main May 19, 2026
1 check passed
@ultimatile ultimatile deleted the feat/17-marked-final-url-line branch May 19, 2026 02:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

auto-view output should place created/edited URL on a marked final line

2 participants