Skip to content

feat(notifications): add opt-in Discord webhook via 'gac discord' subcommands#76

Merged
thomwebb merged 4 commits into
thomwebb:mainfrom
mpfaffenberger:main
May 23, 2026
Merged

feat(notifications): add opt-in Discord webhook via 'gac discord' subcommands#76
thomwebb merged 4 commits into
thomwebb:mainfrom
mpfaffenberger:main

Conversation

@mpfaffenberger

@mpfaffenberger mpfaffenberger commented May 20, 2026

Copy link
Copy Markdown
Contributor

🐶 Add Discord webhook notifications via a new gac discord command

Adds an opt-in Discord webhook integration so gac can announce each successful commit to a Discord channel of your choice — handy for team channels, side-project log feeds, or just vibes.

✨ What this adds

  • A new gac discord command group for managing the integration — gac init is unchanged:
    • uvx gac discord setup — interactively configure (or replace) the webhook URL
    • uvx gac discord show — show whether a webhook is configured (URL masked)
    • uvx gac discord test — fire a test notification to verify your setup
    • uvx gac discord remove — delete the configured URL
  • A new GAC_DISCORD_WEBHOOK_URL environment variable. When set, gac POSTs an embed to your webhook after every successful commit.
  • A clean embed-style payload (à la GitHub's commit cards):
    • 🟩 Green left-edge color stripe (#2DA44E)
    • 🐶 Author line: <repo> · <branch> with the gac avatar
    • 📛 Bold title = commit subject
    • 📜 Description = commit body (split on the conventional blank line)
    • 🏷️ Footer: commit <short-sha>
  • Works for both the single-commit workflow (main.py) and the grouped-commit workflow (grouped_commit_executor.py — fires once per commit in the group).

🛡️ Opt-in by design

The integration is strictly opt-in, gated multiple ways:

  1. gac init never mentions Discord — users must run gac discord setup explicitly.
  2. No env key is written to ~/.gac.env until the user passes a URL through discord setup.
  3. At runtime, notify_commit() short-circuits and returns False if GAC_DISCORD_WEBHOOK_URL is unset, blank, or whitespace-only — no HTTP request is made.
  4. setup validates that the URL starts with http(s):// before writing anything.

🪨 Robustness

  • Webhook failures (network errors, 4xx/5xx, timeouts) are caught, logged at WARNING, and never block a successful commit.
  • Skipped automatically on --dry-run and --message-only (no commit happened, so no notification).
  • Embed title and description are truncated to Discord's documented limits (256 / 4096 chars) with an ellipsis.
  • 5-second HTTP timeout so a hung Discord endpoint can't stall the CLI.

📁 Files changed

New

  • src/gac/discord_webhook.py — the notifier (~135 lines, single responsibility)
  • src/gac/discord_cli.py — the gac discord command group (~150 lines)
  • tests/test_discord_webhook.py — 13 unit tests
  • tests/test_discord_cli.py — 13 unit tests covering all four subcommands

Modified

  • src/gac/cli.py — registers the new discord command group
  • src/gac/main.py — fires notifier after a successful single commit
  • src/gac/grouped_commit_executor.py — fires notifier per commit in grouped mode
  • src/gac/__version__.py3.35.23.36.0
  • CHANGELOG.md — new v3.36.0 entry
  • docs/en/USAGE.md — new "Discord Webhook Notifications" section

Notably untouched

  • src/gac/init_cli.py, tests/test_init_cli.py, tests/test_init_cli_extended.pyzero diff vs main (confirmed via git diff).

✅ PR checklist

  • Version bumped in __version__.py (3.36.0)
  • CHANGELOG.md updated
  • Tests added (26 new, no existing tests modified)
  • uv run -- pytest passing — 2951 passed, 0 failed
  • uv run ruff check clean on all touched files
  • uv run ruff format --check clean on all touched files
  • uv run mypy clean on the new modules
  • No file exceeds the 600-line limit (discord_webhook.py ≈ 135, discord_cli.py ≈ 150)
  • gac init byte-for-byte unchanged vs upstream
  • End-to-end manually verified against a real Discord webhook

🖼️ What it looks like

Roughly the same shape as GitHub's commit notifications: bordered card, colored left stripe, repo/branch as the author row, commit subject as a bold title, body as the description, short SHA in the footer.

Note: the APP badge next to the webhook name is added automatically by Discord for any webhook/bot message and cannot be suppressed via the API — same as how GitHub's own webhook shows APP.

🔧 How to use it

uvx gac discord setup        # paste your Discord webhook URL when prompted
uvx gac discord test         # confirm it works
# ...then just commit normally — every commit pings the channel.

To check status or disable later:

uvx gac discord show         # see whether a webhook is configured (masked)
uvx gac discord remove       # delete the configured URL

Or set it directly in ~/.gac.env:

GAC_DISCORD_WEBHOOK_URL='https://discord.com/api/webhooks/XXXX/YYYY'

🐾 Authored with the help of Biscuit, the most loyal digital puppy.

…ications

- Introduce discord_webhook module with embed-style notifications featuring
  repo name, branch, commit hash, and message in a GitHub-style card format
- Add interactive Discord webhook configuration to gac init workflow with
  options to keep, replace, or remove existing webhook URLs
- Integrate webhook notifications into both single and grouped commit workflows
- Implement graceful error handling: network failures are logged but never
  block successful commits
- Support configurable timeout (5 seconds) and environment-based URL storage
  via GAC_DISCORD_WEBHOOK_URL
- Add unit tests for webhook URL parsing, embed building, and message
  truncation logic
- Test HTTP error handling to verify failures are swallowed gracefully
- Test GitError handling to ensure webhook falls back to sensible defaults
- Update existing init CLI tests to account for new Discord webhook prompt
  in the configuration flow
- Verify embed payload structure matches Discord API requirements
- Add Discord Webhook Notifications section to USAGE.md with setup
  instructions and behavior details
- Document GAC_DISCORD_WEBHOOK_URL environment variable configuration
- Note that webhook failures are logged at WARNING level and never block
  commits
- Update CHANGELOG with v3.36.0 release notes describing the new feature
- Bump version from 3.35.2 to 3.36.0
@coderabbitai

coderabbitai Bot commented May 20, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR adds Discord webhook notifications to gac. After successful commits, the tool can now post a formatted message to a Discord channel configured via GAC_DISCORD_WEBHOOK_URL. Setup occurs interactively during gac init, webhook failures are logged without blocking, and comprehensive tests validate all paths including truncation and error handling.

Changes

Discord Webhook Notifications

Layer / File(s) Summary
Discord webhook module implementation
src/gac/discord_webhook.py
Core module with constants, helpers for git metadata retrieval, string formatting/truncation, commit message parsing, and public functions to read webhook URL and send notifications with graceful HTTP error handling.
Integration into commit execution paths
src/gac/main.py, src/gac/grouped_commit_executor.py
Wire notify_commit calls into both single and grouped commit flows immediately after successful commit record, passing the finalized commit message.
Interactive Discord webhook setup in init
src/gac/init_cli.py
Add _configure_discord_webhook helper to interactively prompt users during gac init, supporting keep/replace/remove actions on existing values with HTTP(S) URL validation and dotenv persistence.
Documentation and version bump
src/gac/__version__.py, CHANGELOG.md, docs/en/USAGE.md
Update version to 3.36.0, document the feature in changelog, and add comprehensive usage guide covering setup, notification content, truncation rules, and failure behavior.
Discord webhook module test coverage
tests/test_discord_webhook.py
Comprehensive test suite validating webhook URL environment normalization, notify_commit no-op and posting behavior, HTTP error handling, embed construction with truncation and edge cases, and helper function parsing.
Integration test updates
tests/test_init_cli.py, tests/test_init_cli_extended.py
Adjust mock questionary.confirm sequences in five existing init tests from single boolean to [True, False] (enable stats, decline Discord) to match the expanded initialization flow.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A webhook hops to Discord bright,
Commits now dance in channels tonight,
With truncated tales and emoji flair,
No failures block—just logged with care! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ 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 describes the main change: adding an opt-in Discord webhook feature for commit notifications, which aligns with the changeset's primary purpose.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (1)
tests/test_discord_webhook.py (1)

57-67: ⚡ Quick win

Assert the webhook timeout contract in this test.

On Line 62-Line 67, please also assert timeout=5 in httpx.post kwargs, since timeout behavior is part of the feature contract and regressions here would silently weaken reliability.

🤖 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_discord_webhook.py` around lines 57 - 67, The test
test_notify_commit_posts_embed_payload currently inspects the httpx.post call
payload but doesn't assert the timeout contract; update the test to also check
that the httpx.post call was invoked with timeout=5 by extracting the call
kwargs from mpost.call_args (same as payload extraction) and asserting
kwargs["timeout"] == 5 so the timeout behavior for notify_commit (and the
underlying httpx.post call) is enforced; reference the test function
test_notify_commit_posts_embed_payload and the mocked httpx.post (mpost) when
adding this assertion.
🤖 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 `@docs/en/USAGE.md`:
- Around line 652-654: Update the documented message format (the line stating
"Message format: `**repo** · `branch` · `short-sha`` followed by the commit
message in a code block" and the "Uses the gac avatar and the username `gac`"
note) to describe the actual embed payload: list which parts go into
embed.title, embed.description, embed.author.name and embed.footer.text, remove
the "code block" and 2000-char statement, and state the correct Discord embed
truncation limits (title 256, description 4096, author name 256, footer 2048) so
the docs match the implemented embed contract and truncation behavior.
- Line 653: The Markdown line with the example message format uses mismatched
inline code ticks causing MD038; fix it by properly pairing or escaping
backticks—e.g., wrap the whole format string as inline code or escape inner
backticks so the phrase **repo** · `branch` · `short-sha` and the following
commit message code block are valid; ensure each inline code span around
repo/branch/short-sha uses matching backticks or replace the inline spans with a
fenced code block to avoid nested backtick conflicts.

In `@src/gac/__version__.py`:
- Line 3: The __version__ module-level variable currently lacks a type
annotation; update the assignment for __version__ in __version__.py to include
an explicit type annotation (i.e., annotate __version__ as str) so it satisfies
the src/**/*.py typing rule and static type checks (modify the __version__
binding to use a type annotation rather than an untyped assignment).

In `@src/gac/discord_webhook.py`:
- Around line 24-37: Module-level variables/constants in this file lack explicit
type annotations; add them so the module complies with the repository typing
rules. Annotate logger as logger: logging.Logger = logging.getLogger(__name__),
make constants annotated (prefer using typing.Final) e.g. GAC_AVATAR_URL:
Final[str] = "...", GAC_USERNAME: Final[str] = "gac", WEBHOOK_TIMEOUT_SECONDS:
Final[float] = 5.0, ENV_KEY: Final[str] = "GAC_DISCORD_WEBHOOK_URL",
EMBED_COLOR: Final[int] = 0x2DA44E, _MAX_TITLE_LEN: Final[int] = 256, and
_MAX_DESCRIPTION_LEN: Final[int] = 4096 (and import typing.Final if not already
imported).
- Around line 63-71: _split_subject_body currently splits the commit message on
the first newline, which violates the blank-line rule; update it to split on the
first blank line instead (handle one or more blank lines possibly containing
spaces). Locate the function _split_subject_body and replace the split logic to
split on a blank-line separator (e.g., using a regex like r'\n\s*\n' with a
maxsplit of 1), then strip and return the two parts as (subject, body) as
before.

In `@src/gac/init_cli.py`:
- Around line 231-233: The current check only uses url.startswith and allows
malformed inputs; update the validation where the variable url is checked (the
block that currently does if not url.startswith(("http://", "https://")):) to
parse the URL (e.g., using urllib.parse.urlparse), ensure the scheme is exactly
"http" or "https" and that netloc/host is non-empty (and optionally reject
inputs that are just the scheme like "https://"); if the parsed URL is invalid,
echo a clear error and return instead of saving the value. Use the same variable
names (url) and replace the simple startswith check with this stricter
validation.

---

Nitpick comments:
In `@tests/test_discord_webhook.py`:
- Around line 57-67: The test test_notify_commit_posts_embed_payload currently
inspects the httpx.post call payload but doesn't assert the timeout contract;
update the test to also check that the httpx.post call was invoked with
timeout=5 by extracting the call kwargs from mpost.call_args (same as payload
extraction) and asserting kwargs["timeout"] == 5 so the timeout behavior for
notify_commit (and the underlying httpx.post call) is enforced; reference the
test function test_notify_commit_posts_embed_payload and the mocked httpx.post
(mpost) when adding this assertion.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1044ef0d-091d-43cf-9b03-12c3831ad0af

📥 Commits

Reviewing files that changed from the base of the PR and between dd11c7b and d511758.

📒 Files selected for processing (10)
  • CHANGELOG.md
  • docs/en/USAGE.md
  • src/gac/__version__.py
  • src/gac/discord_webhook.py
  • src/gac/grouped_commit_executor.py
  • src/gac/init_cli.py
  • src/gac/main.py
  • tests/test_discord_webhook.py
  • tests/test_init_cli.py
  • tests/test_init_cli_extended.py

Comment thread docs/en/USAGE.md
Comment thread docs/en/USAGE.md Outdated
Comment thread src/gac/__version__.py
Comment thread src/gac/discord_webhook.py
Comment thread src/gac/discord_webhook.py
Comment thread src/gac/init_cli.py Outdated
…discord subcommands

Per review feedback, gac init is restored to upstream and Discord webhook
configuration now lives in a dedicated 'gac discord' command group:

  - gac discord setup   interactively configure the webhook URL
  - gac discord show    display whether a webhook is configured (masked)
  - gac discord test    send a test notification
  - gac discord remove  delete the configured URL

This keeps the init flow unchanged, gives Discord its own ergonomic home,
and adds an explicit 'test' command for users to verify their setup
without making a real commit.
@mpfaffenberger mpfaffenberger changed the title feat(notifications): add opt-in Discord webhook for commit notifications feat(notifications): add opt-in Discord webhook via 'gac discord' subcommands May 23, 2026
@thomwebb thomwebb merged commit 4e1ee4b into thomwebb:main May 23, 2026
6 checks passed
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.

2 participants