Skip to content

feat: Week 10 /pr command, GitHub App auth, and PR link comments - #38

Merged
shubham5080 merged 2 commits into
mainfrom
feat/pr-list-command
Aug 2, 2026
Merged

feat: Week 10 /pr command, GitHub App auth, and PR link comments#38
shubham5080 merged 2 commits into
mainfrom
feat/pr-list-command

Conversation

@shubham5080

@shubham5080 shubham5080 commented Aug 1, 2026

Copy link
Copy Markdown
Member

Summary

  • Add /pr Discord command to list a verified contributor’s PRs (closed/merged/open) with count/pagination and multi-message formatting
  • Prefer GitHub App installation auth (GitcordApp) for API writes/snapshots so actions appear as gitcordapp[bot]
  • Improve PR-opened Discord messages and post a GitHub PR comment for unverified authors (Discord invite + /link steps)
  • Snapshot/config/docs updates for App auth and invite URL

Test plan

  • Unit tests: pytest tests/test_pr_list.py tests/test_github_app_auth.py tests/test_notifications.py tests/test_open_prs_search.py
  • Rebuild bot + sync-scheduler; run /pr for a verified member in Discord
  • Open a PR from an unverified account → Discord channel nudge + GitHub comment as gitcordapp[bot]
  • Confirm snapshots still write with App auth when enabled

Made with Cursor

Summary by CodeRabbit

  • New Features

    • Added the /pr Discord command to list recent contributor pull requests by status, with optional count and skip filters.
    • Added optional GitHub App authentication.
    • Added account-linking prompts for unverified pull request authors.
    • Added configurable snapshot scheduling and repository settings.
  • Documentation

    • Updated setup, testing, technical, and command references.
  • Bug Fixes

    • Improved pull-request status detection, notification reliability, and snapshot write handling.

Ship contributor PR history in Discord, prefer GitcordApp installation tokens, and nudge unlinked authors on GitHub when they open a PR.

Co-authored-by: Cursor <cursoragent@cursor.com>
@socket-security

socket-security Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedpypi/​pyjwt@​2.13.0100100100100100

View full report

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The change adds GitHub App authentication with cached installation tokens, a /pr Discord command, GitHub comments for eligible unverified PR authors, updated configuration and documentation, and minimum snapshot write intervals.

Changes

GitHub authentication and client wiring

Layer / File(s) Summary
GitHub App authentication and client wiring
.env.example, pyproject.toml, src/ghdcbot/adapters/github/*, src/ghdcbot/bot.py, src/ghdcbot/cli.py, src/ghdcbot/config/models.py, docker-compose.yml, tests/test_github_app_auth.py
GitHub App credentials now resolve before the PAT. Installation tokens are cached and refreshed. GitHub clients accept static tokens or callable providers. Services mount the private key as read-only. Authentication tests cover fallback and dynamic bearer headers.

Contributor pull-request listing

Layer / File(s) Summary
Contributor pull-request listing
src/ghdcbot/adapters/github/rest.py, src/ghdcbot/bot.py, src/ghdcbot/engine/pr_list.py, README.md, TECHNICAL_DOCUMENTATION.md, INSTALLATION.md, docs/TESTING_DISCORD.md, tests/test_open_prs_search.py, tests/test_pr_list.py
The /pr command validates count and skip, retrieves author PRs, classifies statuses, groups results, splits messages, and sends ephemeral responses. Documentation and smoke tests describe the command.

PR-opened account-link notifications

Layer / File(s) Summary
PR-opened account-link notifications
src/ghdcbot/config/models.py, config/aussie.yaml, src/ghdcbot/engine/notifications.py, src/ghdcbot/engine/orchestrator.py, src/ghdcbot/adapters/github/rest.py, src/ghdcbot/adapters/github/writer.py, src/ghdcbot/adapters/storage/sqlite.py, tests/test_notifications.py
Configured PR-opened events can create deduplicated GitHub comments for eligible unverified authors. Discord messages now distinguish verified and unverified authors. Tests cover bots, verified authors, missing invites, and duplicates.

Snapshot repository settings and write intervals

Layer / File(s) Summary
Snapshot repository settings and write intervals
src/ghdcbot/config/models.py, config/aussie.yaml, src/ghdcbot/engine/snapshots.py, tests/test_snapshots.py
Snapshot configuration now includes repository, branch, and minimum interval settings. Snapshot writes skip recent runs and update the snapshots cursor after successful writes.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Discord
  participant Bot
  participant GitHubRestAdapter
  participant pr_list
  Discord->>Bot: invoke /pr with count and skip
  Bot->>GitHubRestAdapter: list_pull_requests_for_author
  GitHubRestAdapter->>pr_list: return classified PRs
  Bot->>pr_list: format recent results
  pr_list->>Discord: send ephemeral messages
Loading
sequenceDiagram
  participant Orchestrator
  participant Notifications
  participant GitHubPlanWriter
  Orchestrator->>Notifications: process pr_opened event
  Notifications->>GitHubPlanWriter: create_issue_comment
  GitHubPlanWriter-->>Notifications: return success or failure
  Notifications-->>Orchestrator: retain Discord notification flow
Loading

Poem

I’m a rabbit with tokens that hop,
Through GitHub PR lists without a stop.
/pr brings the rows, neat and bright,
Link comments guide authors right.
Snapshots wait, then write with care—
Fresh little changes in the air.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: the /pr command, GitHub App authentication, and PR link comments.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/pr-list-command

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 16

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/ghdcbot/engine/snapshots.py (1)

152-164: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Advance the snapshots cursor only after a complete snapshot.

The surrounding guard enters this block when at least one file succeeds. If another file fails, set_cursor("snapshots", now) records a successful timestamp for an incomplete snapshot. The next run can then skip the configured interval and delay recovery. Require files_written == len(snapshot_data) before updating the cursor and treating the snapshot as successful. Add a test with one successful and one failed file write.

Proposed fix
-    if files_written > 0:
+    if files_written == len(snapshot_data) and files_written > 0:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ghdcbot/engine/snapshots.py` around lines 152 - 164, Update the cursor
advancement in the snapshots-writing flow so set_cursor("snapshots", now) and
the corresponding success log run only when files_written equals
len(snapshot_data), not merely when files_written is positive. Preserve
partial-write handling without advancing the cursor, and add a test covering one
successful and one failed file write.
src/ghdcbot/adapters/github/rest.py (1)

204-212: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Keep the log message constant and move log_label into extra.

The f-string makes the log message vary per call site, which prevents grouping in log aggregation. Ruff flags this as G004.

♻️ Proposed fix
                     self._logger.warning(
-                        f"GitHub search for {log_label} failed",
+                        "GitHub search failed",
                         extra={
+                            "search_label": log_label,
                             "status_code": response.status_code,
                             "github_user": author,
                             "org": self._org,
                         },
                     )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ghdcbot/adapters/github/rest.py` around lines 204 - 212, Update the
warning call in the response-handling branch to use a constant message instead
of interpolating log_label. Preserve the existing status_code, github_user, and
org fields in extra, and add log_label there so the context remains available
without varying the primary log message.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/ghdcbot/adapters/github/app_auth.py`:
- Line 14: Replace typing.Callable with collections.abc.Callable in
src/ghdcbot/adapters/github/app_auth.py (line 14); in
src/ghdcbot/adapters/github/identity.py (line 5), move Callable and Iterable to
collections.abc; in src/ghdcbot/adapters/github/rest.py (line 9), move Callable,
Iterable, Iterator, and Sequence there; and in
src/ghdcbot/adapters/github/writer.py (line 4), move Callable and Iterable
there, preserving all other typing imports.
- Around line 25-35: Update the GitHubAppTokenProvider dataclass fields so
private_key_pem and _token use repr=False, preventing secrets from appearing in
generated representations; also set _token and _expires_at to init=False so
callers cannot provide cache values during construction, while preserving valid
dataclass field ordering.
- Around line 106-111: Update github_app_provider_from_env to detect when
exactly one of app_id or installation_id is set, log a warning describing the
incomplete GitHub App configuration, then return None as before. Do not warn
when both variables are absent or both are present, and preserve the existing
PAT fallback behavior.
- Around line 144-152: Update DynamicBearerAuth.auth_flow to catch failures from
self._get_token() and convert them into an httpx-compatible error, preserving
the original exception as the cause. Keep the existing Authorization header
assignment and request yield unchanged for successful token retrieval so
existing HTTP error handlers can handle token-mint failures.
- Around line 37-41: Protect the cache check and token minting in get_token with
a threading.Lock so concurrent worker threads serialize the full operation.
Acquire the lock before reading _token and _expires_at, keep the valid-token
return and _mint_installation_token call inside the critical section, and
release it safely on every path.

In `@src/ghdcbot/adapters/github/identity.py`:
- Around line 25-29: Update _raw_contains_code to fetch absolute raw gist URLs
with a separate unauthenticated HTTP client instead of self._client, while
continuing to use self._client for GitHub API requests. Ensure the client
created in __init__ with build_github_httpx_client and its installation-token
authentication are never used for raw gist content.

In `@src/ghdcbot/adapters/github/rest.py`:
- Around line 432-457: Update create_issue_comment to use the existing _request
path or equivalent rate-limit handling instead of calling self._client.post
directly, preserving its success and failure return behavior. Parse and log
GitHub rate-limit headers consistently with neighboring write methods such as
assign_issue, and classify a 403 response carrying Retry-After as retryable so
secondary-rate-limit backoff is applied.
- Around line 190-206: Update the GitHub search helper around the /search/issues
request to add allowlisted repositories and denylisted repositories as repo
qualifiers in the query before requesting results, while retaining the existing
local PR filter as a safety net. Ensure generated queries respect GitHub’s
256-character limit, and update the exact query assertion in
tests/test_open_prs_search.py to match the new scoped query.

In `@src/ghdcbot/bot.py`:
- Around line 633-641: Apply a per-user cooldown to the `/pr` command handler
containing the `list_pull_requests_for_author` flow, using the framework’s
`app_commands.checks.cooldown` mechanism. Configure an appropriate rate and
cooldown window for each user, and ensure cooldown violations are handled with
the command’s existing user-facing error response path without affecting other
commands.
- Around line 642-651: Replace the user-facing exception interpolation in both
the /pr handler and open_prs_cmd with a generic PR-fetch failure message, while
leaving logger.exception to capture the detailed exception internally. Ensure no
raw exc text is sent through interaction.followup.send, including ephemeral
responses.

In `@src/ghdcbot/cli.py`:
- Around line 64-71: Update the CLI initialization around build_orchestrator and
GitHubIdentityReader to reuse the orchestrator’s already resolved GitHub token
or shared token provider instead of calling resolve_github_token again. Ensure
app_auth.resolve_github_token uses a cached provider when needed, so only one
GitHubAppTokenProvider manages token minting and refresh for the installation.

In `@src/ghdcbot/engine/notifications.py`:
- Around line 341-357: Sanitize the author-controlled pr_title before
constructing header: escape Discord Markdown link delimiters and neutralize
mentions such as `@everyone`, then use the sanitized title in the linked PR
announcement. Add coverage for a title containing injected link syntax and
`@everyone`, while preserving normal title rendering and the existing author/link
behavior.
- Around line 268-293: The PR comment flow around _was_notification_sent and
_mark_notification_sent is not atomic, allowing concurrent syncs to create
duplicate comments. Replace the separate read-before-post sequence with a
storage-level atomic claim for dedupe_key, using an appropriate lease or
in-progress status; only the claimant may call create_issue_comment, release or
make the claim retryable when posting fails, and finalize the claim after
success. Add a concurrent test covering two syncs attempting the same
notification.

In `@tests/test_github_app_auth.py`:
- Around line 15-40: Add tests for GitHubAppTokenProvider.get_token covering
cached-token reuse and refresh at the skew boundary by setting _token and
_expires_at directly. Patch httpx.Client.post to test _mint_installation_token
parsing expires_at and its 3500-second fallback when parsing fails; also cover
RuntimeError when app credentials exist without a private key and verify
_load_private_key converts escaped newline sequences from the environment.

In `@tests/test_open_prs_search.py`:
- Around line 139-151: Extend the test coverage around
_search_pull_requests_for_author and list_open_pull_requests_for_author to
assert that returned rows include updated_at, while rows from
list_open_pull_requests_for_author do not include status. Keep the existing
request and status assertions unchanged.
- Line 145: Reverse the operands in the assertion within the open-PR search test
so params["q"] is compared against the expected query string, satisfying Ruff's
SIM300 rule while preserving the same assertion behavior.

---

Outside diff comments:
In `@src/ghdcbot/adapters/github/rest.py`:
- Around line 204-212: Update the warning call in the response-handling branch
to use a constant message instead of interpolating log_label. Preserve the
existing status_code, github_user, and org fields in extra, and add log_label
there so the context remains available without varying the primary log message.

In `@src/ghdcbot/engine/snapshots.py`:
- Around line 152-164: Update the cursor advancement in the snapshots-writing
flow so set_cursor("snapshots", now) and the corresponding success log run only
when files_written equals len(snapshot_data), not merely when files_written is
positive. Preserve partial-write handling without advancing the cursor, and add
a test covering one successful and one failed file write.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: aa6b43ed-cc5f-4075-a607-42c33b40788e

📥 Commits

Reviewing files that changed from the base of the PR and between a1b0efa and bb73aea.

⛔ Files ignored due to path filters (3)
  • public/gitcord-github-app-logo-dark.png is excluded by !**/*.png
  • public/gitcord-github-app-logo-preview.png is excluded by !**/*.png
  • public/gitcord-github-app-logo.png is excluded by !**/*.png
📒 Files selected for processing (23)
  • .env.example
  • INSTALLATION.md
  • README.md
  • TECHNICAL_DOCUMENTATION.md
  • config/aussie.yaml
  • docker-compose.yml
  • docs/TESTING_DISCORD.md
  • pyproject.toml
  • src/ghdcbot/adapters/github/app_auth.py
  • src/ghdcbot/adapters/github/identity.py
  • src/ghdcbot/adapters/github/rest.py
  • src/ghdcbot/adapters/github/writer.py
  • src/ghdcbot/bot.py
  • src/ghdcbot/cli.py
  • src/ghdcbot/config/models.py
  • src/ghdcbot/engine/notifications.py
  • src/ghdcbot/engine/orchestrator.py
  • src/ghdcbot/engine/pr_list.py
  • src/ghdcbot/engine/snapshots.py
  • tests/test_github_app_auth.py
  • tests/test_notifications.py
  • tests/test_open_prs_search.py
  • tests/test_pr_list.py

Comment thread src/ghdcbot/adapters/github/app_auth.py Outdated
Comment thread src/ghdcbot/adapters/github/app_auth.py Outdated
Comment thread src/ghdcbot/adapters/github/app_auth.py Outdated
Comment thread src/ghdcbot/adapters/github/app_auth.py
Comment thread src/ghdcbot/adapters/github/app_auth.py
Comment thread src/ghdcbot/engine/notifications.py
Comment thread src/ghdcbot/engine/notifications.py
Comment thread tests/test_github_app_auth.py
Comment thread tests/test_open_prs_search.py
Comment thread tests/test_open_prs_search.py Outdated
…safety.

Serialize token minting, reuse the shared provider, scope PR search queries,
and make unverified-PR comment dedupe atomic with safer Discord/error handling.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions github-actions Bot added size/XL and removed size/XL labels Aug 2, 2026
@shubham5080

Copy link
Copy Markdown
Member Author

Done coderabbit fixes .

@shubham5080
shubham5080 merged commit 16e5f4a into main Aug 2, 2026
4 of 6 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/ghdcbot/engine/notifications.py (2)

190-224: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Extend the atomic claim to send_pr_opened_channel_notification.

_claim_notification_sent/_release_notification_claim (428-456) close the duplicate-post race that a prior review flagged for GitHub comments. send_pr_opened_channel_notification (190-224) still uses the older pattern: _was_notification_sent (line 193) as a separate read, then send_msg (line 210), then _mark_notification_sent (line 221) as a separate write.

Two concurrent syncs can both pass the _was_notification_sent check before either marks the key, so both can post duplicate PR-opened Discord channel messages. This is the same race class the atomic claim was built to close, just for the Discord-channel path instead of the GitHub-comment path.

Migrate send_pr_opened_channel_notification to call _claim_notification_sent before send_msg, and _release_notification_claim when send_msg returns falsy or raises, mirroring send_pr_opened_github_link_comment.

Also applies to: 428-456

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ghdcbot/engine/notifications.py` around lines 190 - 224, Update
send_pr_opened_channel_notification to replace the separate
_was_notification_sent check with _claim_notification_sent before send_msg. When
sending returns falsy or raises, call _release_notification_claim for the same
dedupe key; preserve the existing successful notification handling and avoid
separately calling _mark_notification_sent if the claim helper already records
success, mirroring send_pr_opened_github_link_comment.

264-297: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Error-handle the atomic claim before posting the comment.

claim_notification_sent() performs an INSERT OR IGNORE inside sqlite3.connect(...) from src/ghdcbot/adapters/storage/sqlite.py, and _send_notifications_for_new_events() in src/ghdcbot/engine/orchestrator.py does not catch failures from send_pr_opened_github_link_comment(). If the claim write raises, the exception propagates instead of returning False, so wrap _claim_notification_sent() in a try/except that logs and returns False, then make the subsequent if not claimed path return False.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ghdcbot/engine/notifications.py` around lines 264 - 297, Wrap the
_claim_notification_sent call in the PR GitHub link notification flow with
exception handling that logs the failure and returns False. Store its result in
a claimed variable, then make the subsequent not claimed check return False
while preserving the existing dedupe key and posting behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/ghdcbot/adapters/github/rest.py`:
- Around line 489-491: Update create_issue_comment to avoid
_execute_request_with_retries for POST requests, using a single non-retrying
request while preserving the SQLite claim for uncertain outcomes. Add a
regression test covering a timeout after GitHub accepts the comment and verify
no duplicate /link comment is created.

In `@src/ghdcbot/engine/snapshots.py`:
- Around line 179-189: Update the snapshot-writing flow around write_file and
the files_written partial branch to avoid leaving incomplete per-run snapshot
directories when any file fails. Stage the full snapshot commit payload and
publish it atomically, or on partial failure remove the run’s written
files/directory and apply equivalent cleanup before leaving the cursor
unchanged; ensure retries do not accumulate incomplete directories.

In `@tests/test_notifications.py`:
- Around line 1225-1258: The concurrent claim test covers successful races but
not storage claim failures. Add a companion test near
test_pr_opened_github_link_comment_concurrent_claim that makes the storage claim
operation raise a transient or lock-related exception, then verify
send_pr_opened_github_link_comment handles it gracefully without propagating and
does not post the GitHub comment.
- Line 1196: Update the datetime construction in the affected test and the
adjacent occurrence to use datetime.now(datetime.UTC) instead of
datetime.now(timezone.utc), consistent with the project’s Python 3.11 minimum
target.

In `@tests/test_snapshots.py`:
- Around line 331-383: Strengthen
test_write_snapshots_partial_write_does_not_advance_cursor by having
PartialWriter record every write attempt separately from successful
files_written entries. Assert that at least two attempts occurred and exactly
one write succeeded, while retaining the assertion that the snapshots cursor was
not stored.

---

Outside diff comments:
In `@src/ghdcbot/engine/notifications.py`:
- Around line 190-224: Update send_pr_opened_channel_notification to replace the
separate _was_notification_sent check with _claim_notification_sent before
send_msg. When sending returns falsy or raises, call _release_notification_claim
for the same dedupe key; preserve the existing successful notification handling
and avoid separately calling _mark_notification_sent if the claim helper already
records success, mirroring send_pr_opened_github_link_comment.
- Around line 264-297: Wrap the _claim_notification_sent call in the PR GitHub
link notification flow with exception handling that logs the failure and returns
False. Store its result in a claimed variable, then make the subsequent not
claimed check return False while preserving the existing dedupe key and posting
behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 535883e0-56d7-4857-b180-8d5b71a7f18f

📥 Commits

Reviewing files that changed from the base of the PR and between bb73aea and f193000.

📒 Files selected for processing (13)
  • src/ghdcbot/adapters/github/app_auth.py
  • src/ghdcbot/adapters/github/identity.py
  • src/ghdcbot/adapters/github/rest.py
  • src/ghdcbot/adapters/github/writer.py
  • src/ghdcbot/adapters/storage/sqlite.py
  • src/ghdcbot/bot.py
  • src/ghdcbot/cli.py
  • src/ghdcbot/engine/notifications.py
  • src/ghdcbot/engine/snapshots.py
  • tests/test_github_app_auth.py
  • tests/test_notifications.py
  • tests/test_open_prs_search.py
  • tests/test_snapshots.py

Comment on lines +489 to +491
response = self._execute_request_with_retries(
"POST", path, params={}, json_body={"body": body}
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not automatically retry issue-comment POST requests.

_execute_request_with_retries retries timeouts and transient responses. GitHub can persist the comment before an ambiguous failure reaches this client. A retry can then create duplicate /link comments.

The SQLite claim prevents concurrent senders. It does not identify a completed external write. Disable ambiguous retries for create_issue_comment. Retain the claim for an uncertain outcome, or reconcile an existing bot comment before a later retry. Add a regression test for a timeout after server acceptance.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ghdcbot/adapters/github/rest.py` around lines 489 - 491, Update
create_issue_comment to avoid _execute_request_with_retries for POST requests,
using a single non-retrying request while preserving the SQLite claim for
uncertain outcomes. Add a regression test covering a timeout after GitHub
accepts the comment and verify no duplicate /link comment is created.

Comment on lines +179 to +189
elif files_written > 0:
logger.warning(
"GitHub snapshots partially written; cursor not advanced",
extra={
"org": config.github.org,
"repo": f"{owner}/{repo}",
"snapshot_dir": snapshot_dir,
"files_written": files_written,
"files_expected": len(snapshot_data),
},
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n -C 8 -P 'def write_file\s*\(|write_file\(|snapshot_dir|snapshots/' \
  src/ghdcbot/adapters/github src/ghdcbot/engine tests

Repository: AOSSIE-Org/Gitcord-GithubDiscordBot

Length of output: 12299


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== snapshots relevant implementation =="
sed -n '110,210p;360,395p' src/ghdcbot/engine/snapshots.py

echo
echo "== github rest write_file implementation =="
sed -n '560,640p;680,730p' src/ghdcbot/adapters/github/rest.py

echo
echo "== snapshot read/list references =="
rg -n -C 4 'read_snapshot|load_snapshot|snapshot_data|snapshot_dir|directories?|glob|listdir|README|snapshots' src/ghdcbot/tests src/ghdcbot/docs README.md docs 2>/dev/null || true

echo
echo "== test partial behavior =="
sed -n '332,370p' tests/test_snapshots.py

Repository: AOSSIE-Org/Gitcord-GithubDiscordBot

Length of output: 12097


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== complete GitHub write_file implementation with response handling =="
sed -n '591,680p' src/ghdcbot/adapters/github/rest.py

echo
echo "== snapshot config and full test partial expected assertions =="
rg -n -C 3 'SnapshotConfig|cursor|files_written|expected|read_.*snapshot|SnapshotReader|metadata' src tests 2>/dev/null || true
sed -n '332,380p' tests/test_snapshots.py

Repository: AOSSIE-Org/Gitcord-GithubDiscordBot

Length of output: 32571


🌐 Web query:

GitHub Contents API update file commit message default branch behavior current SHA

💡 Result:

The GitHub REST API endpoint for creating or updating file contents is PUT /repos/{owner}/{repo}/contents/{path} [1][2]. Key requirements and behaviors for updating a file are as follows: - Commit Message: The message parameter is required for all file creation and update requests [1][2]. - Branch Behavior: The branch parameter allows you to specify the target branch for the update [1][2]. If omitted, the request defaults to the repository’s default branch [1][2]. - SHA Requirement: When updating an existing file, the sha parameter is required [1][2]. This must be the blob SHA of the file being replaced [1][2]. If this SHA is omitted during an update request, the API will return a 422 Unprocessable Entity error because it cannot verify the state of the file being modified [3]. You can obtain this SHA by first performing a GET request on the file path [4].

Citations:


🌐 Web query:

"PUT /repos/{owner}/{repo}/contents/{path}" "if you supply a sha, commit will be applied to the given branch" GitHub

💡 Result:

When using the GitHub REST API endpoint PUT /repos/{owner}/{repo}/contents/{path} to update an existing file, the sha parameter is required [1][2]. This sha must correspond to the blob SHA of the specific file version you are replacing [1][2]. The purpose of this parameter is to ensure that the update is applied correctly to the desired file state and to prevent conflicts (e.g., if the file was modified by another user in the interim) [1][2]. When you provide the correct blob sha, the new content is committed to the specified branch (or the repository's default branch if no branch is specified) [1][2]. If you are creating a new file, the sha parameter should be omitted [1][2]. For updating existing files, you can typically retrieve the required blob sha by first performing a GET request on the file path or by using the Git Trees API to inspect the repository's file structure [3].

Citations:


🌐 Web query:

"PUT /repos/{owner}/{repo}/contents/{path}" "creating a file, the branch parameter is used to find the tree" GitHub

💡 Result:

When creating or updating a file using the GitHub REST API endpoint PUT /repos/{owner}/{repo}/contents/{path}, the branch parameter is indeed used to specify the branch where the commit should be made [1][2]. By including the branch parameter in your request body, you instruct GitHub to apply the changes to that specific branch [1][3]. If the branch parameter is omitted, the API defaults to the repository's default branch [1][2]. This parameter functions as a high-level instruction for the repository contents API. Behind the scenes, GitHub handles the necessary Git operations—such as identifying the current tree of that branch, creating the new file blob, generating a new tree, and creating a commit—to ensure the file is correctly updated on the requested branch [3][4]. This differs from the lower-level Git Database API (found under /repos/{owner}/{repo}/git/...), where you would manually need to fetch the reference, retrieve the tree, construct new objects, and update the branch reference yourself [4].

Citations:


Bundle partial snapshot writes into one atomic commit.

write_file writes each snapshot file with a single PUT and returns on failure. A later file in snapshot_data can fail while earlier files remain in a per-run snapshot_dir, but the partial case only logs counts and leaves the cursor unchanged. Repeated runs use a new run_id, so retries leave additional incomplete directories. Collect the commit payload and write it atomically, or add the same atomicity/cleanup logic as the partial branch leaves in GitHub.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ghdcbot/engine/snapshots.py` around lines 179 - 189, Update the
snapshot-writing flow around write_file and the files_written partial branch to
avoid leaving incomplete per-run snapshot directories when any file fails. Stage
the full snapshot commit payload and publish it atomically, or on partial
failure remove the run’s written files/directory and apply equivalent cleanup
before leaving the cursor unchanged; ensure retries do not accumulate incomplete
directories.

github_user="alice",
event_type="pr_opened",
repo="repo",
created_at=datetime.now(timezone.utc),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n 'target-version|requires-python' pyproject.toml

Repository: AOSSIE-Org/Gitcord-GithubDiscordBot

Length of output: 231


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '1188,1218p' tests/test_notifications.py

Repository: AOSSIE-Org/Gitcord-GithubDiscordBot

Length of output: 1127


Use datetime.UTC at the supported Python target.

pyproject.toml requires Python >=3.11, so replace datetime.now(timezone.utc) with datetime.now(datetime.UTC) here and the adjacent occurrence.

🧰 Tools
🪛 Ruff (0.16.0)

[warning] 1196-1196: Use datetime.UTC alias

Convert to datetime.UTC alias

(UP017)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_notifications.py` at line 1196, Update the datetime construction
in the affected test and the adjacent occurrence to use
datetime.now(datetime.UTC) instead of datetime.now(timezone.utc), consistent
with the project’s Python 3.11 minimum target.

Source: Linters/SAST tools

Comment on lines +1225 to +1258
def test_pr_opened_github_link_comment_concurrent_claim(tmp_path) -> None:
storage = SqliteStorage(str(tmp_path))
storage.init_schema()
github_writer = MockGithubWriter()
config = NotificationConfig(enabled=True, pr_opened_github_comment=True)
policy = MutationPolicy(mode=RunMode.ACTIVE, github_write_allowed=True, discord_write_allowed=True)
event = _pr_opened_event(github_user="stranger")
barrier = Barrier(2)
results: list[bool] = []

def _run() -> None:
barrier.wait()
results.append(
send_pr_opened_github_link_comment(
event,
storage,
github_writer,
policy,
config,
"AOSSIE-Org",
"https://discord.gg/hjUhu33uAn",
)
)

threads = [Thread(target=_run) for _ in range(2)]
for thread in threads:
thread.start()
for thread in threads:
thread.join()

assert sorted(results) == [False, True]
assert len(github_writer.comments) == 1
assert storage.was_notification_sent("pr_opened_github_link:Gitcord-GithubDiscordBot:42")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Good coverage for the atomic claim race.

This test confirms only one of two concurrent callers posts the GitHub comment and only one dedupe row persists. This directly addresses the prior review request for concurrent-claim coverage.

Consider adding a companion test that forces the storage claim call to raise (simulating lock contention or a transient DB error) to confirm the caller degrades gracefully instead of propagating an unhandled exception, tied to the error-handling gap noted in src/ghdcbot/engine/notifications.py.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_notifications.py` around lines 1225 - 1258, The concurrent claim
test covers successful races but not storage claim failures. Add a companion
test near test_pr_opened_github_link_comment_concurrent_claim that makes the
storage claim operation raise a transient or lock-related exception, then verify
send_pr_opened_github_link_comment handles it gracefully without propagating and
does not post the GitHub comment.

Comment thread tests/test_snapshots.py
Comment on lines +331 to +383


def test_write_snapshots_partial_write_does_not_advance_cursor() -> None:
"""Cursor advances only when every snapshot file write succeeds."""

class PartialWriter(MockGitHubWriter):
def write_file(self, owner: str, repo: str, file_path: str, content: str, commit_message: str, branch: str | None = None) -> bool:
# Fail the second write attempt.
if len(self.files_written) >= 1:
return False
return super().write_file(owner, repo, file_path, content, commit_message, branch)

class CursorStorage(MockStorage):
def __init__(self) -> None:
super().__init__()
self.cursors: dict[str, datetime] = {}

def set_cursor(self, source: str, cursor: datetime) -> None:
self.cursors[source] = cursor

storage = CursorStorage()
config = BotConfig(
runtime=RuntimeConfig(
mode=RunMode.DRY_RUN,
log_level="INFO",
data_dir="/tmp/test",
github_adapter="test",
discord_adapter="test",
storage_adapter="test",
activity_period_days=30,
),
github=GitHubConfig(org="test-org", token="test", api_base="https://api.github.com", permissions=PermissionConfig()),
discord=DiscordConfig(guild_id="123", token="test", permissions=PermissionConfig()),
assignments=AssignmentConfig(),
snapshots=SnapshotConfig(enabled=True, repo_path="org/repo"),
)
github_writer = PartialWriter()

write_snapshots_to_github(
storage=storage,
config=config,
github_writer=github_writer,
identity_mappings=[
IdentityMapping(discord_user_id="123", github_user="alice"),
],
scores=[],
member_roles={},
period_start=datetime(2024, 1, 1, tzinfo=timezone.utc),
period_end=datetime(2024, 1, 31, tzinfo=timezone.utc),
)

assert len(github_writer.files_written) >= 1
assert "snapshots" not in storage.cursors

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Record the failed write attempt in this regression test.

PartialWriter uses len(self.files_written) to fail after the first successful write, but it does not record failed attempts. assert len(github_writer.files_written) >= 1 proves only that one file succeeded. It does not prove that the second file was attempted.

Track attempts and assert at least two attempts and exactly one successful write.

Proposed test strengthening
     class PartialWriter(MockGitHubWriter):
+        def __init__(self) -> None:
+            super().__init__()
+            self.attempts = 0
+
         def write_file(self, owner: str, repo: str, file_path: str, content: str, commit_message: str, branch: str | None = None) -> bool:
+            self.attempts += 1
             # Fail the second write attempt.
             if len(self.files_written) >= 1:
                 return False
             return super().write_file(owner, repo, file_path, content, commit_message, branch)
...
-    assert len(github_writer.files_written) >= 1
+    assert github_writer.attempts >= 2
+    assert len(github_writer.files_written) == 1
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_write_snapshots_partial_write_does_not_advance_cursor() -> None:
"""Cursor advances only when every snapshot file write succeeds."""
class PartialWriter(MockGitHubWriter):
def write_file(self, owner: str, repo: str, file_path: str, content: str, commit_message: str, branch: str | None = None) -> bool:
# Fail the second write attempt.
if len(self.files_written) >= 1:
return False
return super().write_file(owner, repo, file_path, content, commit_message, branch)
class CursorStorage(MockStorage):
def __init__(self) -> None:
super().__init__()
self.cursors: dict[str, datetime] = {}
def set_cursor(self, source: str, cursor: datetime) -> None:
self.cursors[source] = cursor
storage = CursorStorage()
config = BotConfig(
runtime=RuntimeConfig(
mode=RunMode.DRY_RUN,
log_level="INFO",
data_dir="/tmp/test",
github_adapter="test",
discord_adapter="test",
storage_adapter="test",
activity_period_days=30,
),
github=GitHubConfig(org="test-org", token="test", api_base="https://api.github.com", permissions=PermissionConfig()),
discord=DiscordConfig(guild_id="123", token="test", permissions=PermissionConfig()),
assignments=AssignmentConfig(),
snapshots=SnapshotConfig(enabled=True, repo_path="org/repo"),
)
github_writer = PartialWriter()
write_snapshots_to_github(
storage=storage,
config=config,
github_writer=github_writer,
identity_mappings=[
IdentityMapping(discord_user_id="123", github_user="alice"),
],
scores=[],
member_roles={},
period_start=datetime(2024, 1, 1, tzinfo=timezone.utc),
period_end=datetime(2024, 1, 31, tzinfo=timezone.utc),
)
assert len(github_writer.files_written) >= 1
assert "snapshots" not in storage.cursors
def test_write_snapshots_partial_write_does_not_advance_cursor() -> None:
"""Cursor advances only when every snapshot file write succeeds."""
class PartialWriter(MockGitHubWriter):
def __init__(self) -> None:
super().__init__()
self.attempts = 0
def write_file(self, owner: str, repo: str, file_path: str, content: str, commit_message: str, branch: str | None = None) -> bool:
self.attempts += 1
# Fail the second write attempt.
if len(self.files_written) >= 1:
return False
return super().write_file(owner, repo, file_path, content, commit_message, branch)
class CursorStorage(MockStorage):
def __init__(self) -> None:
super().__init__()
self.cursors: dict[str, datetime] = {}
def set_cursor(self, source: str, cursor: datetime) -> None:
self.cursors[source] = cursor
storage = CursorStorage()
config = BotConfig(
runtime=RuntimeConfig(
mode=RunMode.DRY_RUN,
log_level="INFO",
data_dir="/tmp/test",
github_adapter="test",
discord_adapter="test",
storage_adapter="test",
activity_period_days=30,
),
github=GitHubConfig(org="test-org", token="test", api_base="https://api.github.com", permissions=PermissionConfig()),
discord=DiscordConfig(guild_id="123", token="test", permissions=PermissionConfig()),
assignments=AssignmentConfig(),
snapshots=SnapshotConfig(enabled=True, repo_path="org/repo"),
)
github_writer = PartialWriter()
write_snapshots_to_github(
storage=storage,
config=config,
github_writer=github_writer,
identity_mappings=[
IdentityMapping(discord_user_id="123", github_user="alice"),
],
scores=[],
member_roles={},
period_start=datetime(2024, 1, 1, tzinfo=timezone.utc),
period_end=datetime(2024, 1, 31, tzinfo=timezone.utc),
)
assert github_writer.attempts >= 2
assert len(github_writer.files_written) == 1
assert "snapshots" not in storage.cursors
🧰 Tools
🪛 ast-grep (0.45.0)

[info] 355-355: Do not hardcode temporary file or directory names
Context: "/tmp/test"
Note: [CWE-377] Insecure Temporary File.

(hardcoded-tmp-file)

🪛 Ruff (0.16.0)

[error] 356-356: Probable insecure usage of temporary file or directory: "/tmp/test"

(S108)


[error] 362-362: Possible hardcoded password assigned to argument: "token"

(S106)


[error] 363-363: Possible hardcoded password assigned to argument: "token"

(S106)


[warning] 378-378: Use datetime.UTC alias

Convert to datetime.UTC alias

(UP017)


[warning] 379-379: Use datetime.UTC alias

Convert to datetime.UTC alias

(UP017)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_snapshots.py` around lines 331 - 383, Strengthen
test_write_snapshots_partial_write_does_not_advance_cursor by having
PartialWriter record every write attempt separately from successful
files_written entries. Assert that at least two attempts occurred and exactly
one write succeeded, while retaining the assertion that the snapshots cursor was
not stored.

shubham5080 added a commit that referenced this pull request Aug 4, 2026
fix: unbreak Python Tests after PR #38 json= client.request
shubham5080 added a commit that referenced this pull request Aug 4, 2026
fix: CodeRabbit follow-ups for PR #38 notification/snapshot safety
PrithvijitBose pushed a commit to PrithvijitBose/Gitcord-GithubDiscordBot that referenced this pull request Aug 5, 2026
PR AOSSIE-Org#38 started passing json= to client.request(); outdated mocks
rejected it and broke Python Tests on main (23 failures).

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant