Skip to content

feat(zulip_bot): close-pr command#155

Merged
bryangingechen merged 24 commits intomasterfrom
close-pr-command
Apr 17, 2026
Merged

feat(zulip_bot): close-pr command#155
bryangingechen merged 24 commits intomasterfrom
close-pr-command

Conversation

@bryangingechen
Copy link
Copy Markdown
Contributor

@bryangingechen bryangingechen commented Apr 12, 2026

close-pr (something containing a URL to a PR) will DM the user a URL to a form that can trigger closure of PR using bot credentials.

prepared with claude code

bryangingechen and others added 7 commits April 12, 2026 15:02
Covers command flow, permission check (PR author + collaborator
write/admin via GitHub App), token service, confirmation form,
post-close actions (sync, DM, repo log thread), and settings
(ZULIP_CLOSE_PR_MUTATIONS_ENABLED, ZULIP_REPO_LOG).

Notes required queueboard-assignment app permission upgrades:
Pull requests: Read and write, Members: Read (org).

Custom close-message follow-up planned as commit 3.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rmation form

Adds a `close-pr` Zulip bot command that:
- Checks the invoker's linked GitHub account has permission to close
  the target PR (PR author or write/admin collaborator), using a
  GitHub App token for the permission check
- Issues a short-lived encrypted private link to a confirmation form
- Closes the PR via PATCH /repos/{owner}/{repo}/pulls/{number} on
  form submission
- Enqueues a sync task, sends a DM to the invoker, and posts to the
  per-repo log thread (ZULIP_REPO_LOG) on success

New settings:
- ZULIP_CLOSE_PR_MUTATIONS_ENABLED (feature flag)
- ZULIP_REPO_LOG (per-repo stream/topic for bot action logging)
- ZULIP_CLOSE_PR_TOKEN_{SECRET,SALT,TTL_SECONDS,URL_BASE}

Requires queueboard-assignment GitHub App to be updated with:
- Pull requests: Read and write (upgrade)
- Members: Read (new org-level permission)
and close_pr added to GITHUB_APP_TOKEN_CONFIG operation_app_map.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- test_valid_open_pr_shows_confirmation_form: add
  ZULIP_CLOSE_PR_MUTATIONS_ENABLED override so the form renders
  instead of the preflight-disabled branch
- test_github_close_error_shown_in_template: close_error holds
  exc.message (human-readable), not exc.code; check for "permission
  denied" substring instead

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Users may type `close_pr`, `assigned_prs`, or `pr_info` expecting them
to work. The parser now replaces `_` with `-` in the command token
before any lookup, so underscore variants resolve to the same command
as their hyphen canonical names without needing per-command aliases.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…d assigned-prs

The parser now normalises underscores to hyphens before command lookup,
making these aliases unnecessary.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mark implementation commits as complete, note the two follow-up fix
commits, and add a new Operational Deployment Notes section covering
the GitHub App permission upgrade steps, required env vars
(GITHUB_APP_TOKEN_CONFIG, ZULIP_REPO_LOG, ZULIP_CLOSE_PR_MUTATIONS_ENABLED),
and a manual verification checklist for post-deployment testing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…d-org-read app

Introduce a dedicated `check_collaborator_permission` operation so the
`Members: Read` org-level permission can be carried by a separate
`queueboard-org-read` GitHub App rather than `queueboard-assignment`.
The execution service falls back to the `close_pr` token when
`check_collaborator_permission` is not mapped, keeping single-app setups
working without config changes.

Update docs/github_app_setup.md and 041 design doc to reflect the
three-app layout and new operation name.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…bled

When ZULIP_CLOSE_PR_MUTATIONS_ENABLED is unset, the confirmation form
now shows a dry-run warning and still allows submission. On POST it
skips the actual close_pull_request() call but runs all post-actions
(sync enqueue, DM, log post) normally, allowing a full end-to-end test
without touching the PR.

Template: replaces the old static preflight-only message with a
dry-run callout on the form (GET) and a preflight_only success message
(POST). View: restructures the POST guard so mutations_enabled controls
only the close call, not the post-actions. Test updated accordingly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
bryangingechen and others added 3 commits April 15, 2026 15:48
Without a close-pr entry in ZULIP_COMMAND_POLICY the command is
silently blocked for all users. Add step 4 with an example policy
snippet and a pointer to manage.py zulip_policy. Also fix the duplicate
step-3 numbering introduced in an earlier edit, and update the
ZULIP_CLOSE_PR_MUTATIONS_ENABLED description to reflect the new
dry-run behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the close-pr-specific ZULIP_CLOSE_PR_URL_BASE with the shared
ZULIP_PREFS_URL_BASE that prefs and registration links already use.
There is no reason to have separate base URL settings for different
Zulip forms on the same server.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add ZULIP_CLOSE_PR_MUTATIONS_ENABLED and ZULIP_REPO_LOG entries.
Update GITHUB_APP_TOKEN_CONFIG inline example to include close_pr and
check_collaborator_permission operations and the queueboard-org-read app.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ZULIP_REPO_LOG was read via getattr(settings, ...) but never parsed
from the env var in settings/base.py, so it remained a raw string and
_resolve_repo_log_target silently returned None on every call.
Add the same JSON-parse-and-validate pattern used by GITHUB_APP_TOKEN_CONFIG
and ZULIP_COMMAND_POLICY.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Link the PR (markdown link to github.com) instead of bare backtick ref
  in both the DM and the log post.
- Use a Zulip silent mention (@_**Name|id**) for the requester in the
  log post, looked up from User.zulip_full_name; falls back to
  `github_login` if the name is not populated.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
bryangingechen and others added 2 commits April 17, 2026 03:52
…close-pr form

Adds a textarea to the close-PR confirmation form where users can type
a message that is posted as a GitHub comment before the PR is closed.
Preset messages are loaded from .md files in close_pr_presets/ (first
# heading = button label, remainder = body; sorted by filename). No code
change is needed to add or edit presets.

If the comment post fails the PR is not closed and an inline error is
shown with the message preserved for retry. The error state now renders
inline in the form rather than as a dead-end page in all cases.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…iry countdown

- Show PR author, opened/updated dates, labels (colored chips), and
  body preview in a compact info card above the confirmation form.
- Link all owner/repo#number occurrences to the GitHub PR URL.
- Replace static "expires at" text with a live countdown that disables
  the submit button when the token expires, matching prefs-form behavior.
  Shared getExpiryState/formatRemaining logic imported from prefs_form.js.
- Style close_pr_invalid.html with shared_pages.css for consistency.
- Add close_pr_form.js (module) and move preset-button logic out of
  the inline <script> tag.
- Add vitest suite for the new JS mount function.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ody display

- Consolidate layout: PR title moves from hero into the PR card so all
  identity info (title, author, dates, labels, body) lives in one place;
  hero now shows only the page heading and owner/repo#number.
- Remove attribution paragraph from the action card (PR card above makes
  the target obvious); keep only the bot-attribution warning.
- Show full PR body with no character truncation or max-height cap.
- Always show updated_at (dropped the same-day equality suppression).
- Pass raw ISO strings (pr_opened_at_iso, pr_updated_at_iso) to template;
  JS formats <time class="ts" data-iso="..."> elements on load as
  "Apr 5, 2026, 2:34 PM (3 days ago)" using the user's local timezone.
- Add formatTimestamp / formatAgo helpers to close_pr_form.js.
- Expand frontend tests to cover timestamp formatting and .ts DOM wiring.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Restore max-height + overflow-y: auto on .pr-body so full text is
  accessible by scrolling without expanding the page.
- Wrap pr-title in a link to the PR URL; colour inherits from heading
  so it stays visually prominent (underline on hover).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The env var was never loaded in settings/base.py, so getattr(settings,
"ZULIP_CLOSE_PR_MUTATIONS_ENABLED", "") always returned "" and the form
always showed dry-run mode regardless of the env var value.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
bryangingechen and others added 2 commits April 17, 2026 08:58
Two complementary fixes:

1. Post/Redirect/Get after successful POST: instead of rendering the
   success template directly (which leaves the browser on a POST URL),
   redirect to the same path with ?closed=1 or ?preflight=1. A browser
   refresh then replays the harmless GET, not the POST.

2. pr_is_open guard at POST entry: if the live PR state is already
   closed when a POST arrives (e.g. back-button resubmit), skip all
   mutations and post-actions and render the "already closed" view.

Together these prevent a comment being posted twice, and the Zulip DM
and repo-log entry being sent twice, on any form of POST replay.

Update tests to expect 302 on successful POSTs and follow the redirect
to assert success state; add test for the pr_is_open short-circuit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Makes the message useful in both the back-button resubmit case ("you
closed it just now, you're done") and the concurrent-close case
("closed by someone else, no action needed").

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… comment

- DM: simplified to "Pull request X has been closed." with an optional
  ```quote block containing the close comment below.
- Log: "PR X was closed by <user> on Apr 17, 2026, 14:32 UTC." with
  the same optional quote block; drops the "via bot, requested by"
  phrasing.
- Add confirm() dialog on form submit when the close-message textarea
  is empty, guarding against accidental closes without a comment.
- Expand frontend tests to cover the confirm dialog behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@bryangingechen bryangingechen merged commit 1894426 into master Apr 17, 2026
4 checks passed
@bryangingechen bryangingechen deleted the close-pr-command branch April 17, 2026 13:54
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.

1 participant