Skip to content

[ops] Add scripts/ops/discord.py for one-way channel notifications#5229

Merged
rjpower merged 2 commits intomainfrom
ops-discord-notifier
Apr 28, 2026
Merged

[ops] Add scripts/ops/discord.py for one-way channel notifications#5229
rjpower merged 2 commits intomainfrom
ops-discord-notifier

Conversation

@rjpower
Copy link
Copy Markdown
Collaborator

@rjpower rjpower commented Apr 28, 2026

Webhook URLs resolve from DISCORD_WEBHOOK_ env vars first, falling back to gcloud secret manager (marin-discord-webhook-) so the same script works locally and in GH Actions. Channels: internal-discuss, code-review.

Webhook URLs resolve from DISCORD_WEBHOOK_<CHANNEL> env vars first, falling
back to gcloud secret manager (marin-discord-webhook-<channel>) so the same
script works locally and in GH Actions. Channels: internal-discuss,
code-review.
@rjpower rjpower added the agent-generated Created by automation/agent label Apr 28, 2026
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 28, 2026

Claude finished @rjpower's task in 4m 10s —— View job


Code review

  • Check PR is open and reviewable
  • Identify relevant CLAUDE.md/AGENTS.md files
  • Summarize PR changes
  • Run review agents (compliance + bugs)
  • Validate flagged issue
  • Post review

1 issue posted inline (scripts/ops/discord.py#L81-L83):

  • The if resp.status >= 300 branch in post() is unreachable — urllib.request.urlopen raises urllib.error.HTTPError for non-2xx responses before returning, so the formatted RuntimeError never fires in production. test_post_raises_on_http_error only passes because urlopen is mocked to return a fake status=500 response, which real urllib would never do.

Other items considered and not flagged (not high-signal under the review bar):

  • scripts/ops/__init__.py is missing, but Python implicit namespace packages cover this; from scripts.ops import discord resolves fine.
  • pyproject.toml has testpaths = ["tests", "experiments"], so scripts/ops/tests/test_discord.py won't be picked up by the default pytest invocation. Worth knowing if you intended these to run in CI, but it may be deliberate given scripts/ is also excluded from black/ruff/mypy.
  • assert channel in CHANNELS would be stripped under python -O. Library callers can already pass arbitrary channel strings; a hardened version would raise ValueError. CLAUDE.md actually endorses asserting liberally, so this is not a violation — just a defense-in-depth nit.
    • Branch: ops-discord-notifier

@rjpower rjpower requested a review from dlwh April 28, 2026 01:04
@rjpower rjpower enabled auto-merge (squash) April 28, 2026 01:04
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9b234c02f1

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread scripts/ops/discord.py
text=True,
stderr=subprocess.PIPE,
).strip()
except subprocess.CalledProcessError as e:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Handle missing gcloud binary in webhook resolution

When DISCORD_WEBHOOK_<CHANNEL> is unset, _webhook_url shells out to gcloud; however, only CalledProcessError is caught. On machines without the gcloud CLI installed, this raises FileNotFoundError and bypasses the intended contextual RuntimeError, so local ops runs fail with an unhelpful traceback instead of a clear remediation message.

Useful? React with 👍 / 👎.

Comment thread scripts/ops/discord.py
},
method="POST",
)
with urllib.request.urlopen(req, timeout=10) as resp:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Catch HTTPError for non-2xx Discord responses

urllib.request.urlopen raises HTTPError for 4xx/5xx before yielding a response object, so the resp.status >= 300 branch is skipped for real Discord failures. That means webhook errors return an uncaught HTTPError instead of the script’s structured RuntimeError path (with channel/body context), making failure handling inconsistent in production.

Useful? React with 👍 / 👎.

Comment thread scripts/ops/discord.py
Comment on lines +81 to +83
with urllib.request.urlopen(req, timeout=10) as resp:
if resp.status >= 300:
raise RuntimeError(f"discord webhook {channel!r} returned {resp.status}: {resp.read()!r}")
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.

The resp.status >= 300 branch is essentially dead code, and the corresponding test only passes because urlopen is mocked.

urllib.request.urlopen runs through the default OpenerDirector, whose HTTPDefaultErrorHandler raises urllib.error.HTTPError for any non-2xx response that no other handler claims (3xx codes that are not redirects, plus all 4xx/5xx). It will not return a response object whose .status is >= 300 — which means the docstring claim "Raises on non-2xx response" is honored only by HTTPError propagating from urlopen(...), not by the RuntimeError you construct here.

Concretely:

  • In production, a 4xx/5xx from Discord raises urllib.error.HTTPError from line 81, never reaches lines 82-83, and surfaces to callers as HTTPError rather than the formatted RuntimeError.
  • test_post_raises_on_http_error sets up fake_resp.status = 500 and asserts RuntimeError, but real urlopen would never hand a 500 back as a response — it would raise. The test exercises a code path that cannot happen.

Two reasonable fixes (pick one — not a one-line suggestion):

  1. Catch urllib.error.HTTPError around urlopen and re-raise as RuntimeError(f"discord webhook {channel!r} returned {e.code}: {e.read()!r}"). Then update the test to make urlopen raise HTTPError instead of returning a fake response, and drop the now-unreachable if resp.status >= 300 block.
  2. Or just delete the dead branch and let HTTPError propagate; update the test and the post() docstring to match.

Fix this →

@rjpower rjpower merged commit 2f765a8 into main Apr 28, 2026
48 checks passed
@rjpower rjpower deleted the ops-discord-notifier branch April 28, 2026 01:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent-generated Created by automation/agent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants