Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
ced2185
start GitHub App integration
Pxx500 Aug 28, 2026
e652ada
add GitHub App authentication and transport
Pxx500 Aug 28, 2026
aaa4be8
harden GitHub App transport failures
Pxx500 Aug 28, 2026
84e5e2c
address GitHub transport review findings
Pxx500 Aug 28, 2026
01a081f
install GitHub App dependencies in CI
Pxx500 Aug 29, 2026
efd9185
persist GitHub pull request synchronization
Pxx500 Aug 29, 2026
80f5ffb
align privacy cleanup with nullable ticket authors
Pxx500 Aug 29, 2026
8182c68
preserve pull request action across observations
Pxx500 Aug 29, 2026
1355538
receive GitHub webhooks durably
Pxx500 Aug 29, 2026
88aa3aa
centralize operational error reporting
Pxx500 Aug 29, 2026
4787a83
accept non pull request webhook deliveries
Pxx500 Aug 29, 2026
a4ba61d
keep operational error configuration private
Pxx500 Aug 29, 2026
3505023
process GitHub deliveries and assignee intents
Pxx500 Aug 29, 2026
c79aa39
keep runtime tests compatible with Python 3.10
Pxx500 Aug 29, 2026
be94aec
handle GitHub pull request lifecycle transitions
Pxx500 Aug 29, 2026
506600e
avoid duplicate GitHub delivery recovery
Pxx500 Aug 29, 2026
9176c20
model GitHub integration runtime settings
Pxx500 Aug 29, 2026
dedd33e
bind manual tickets to GitHub pull requests
Pxx500 Aug 29, 2026
272a26c
route GitHub events through ticket lifecycle
Pxx500 Aug 29, 2026
73ee1c8
synchronize GitHub pull request title changes
Pxx500 Aug 29, 2026
3206c68
redact GitHub App secrets from console dumps
Pxx500 Aug 29, 2026
e5fd857
defer unsettled GitHub event transitions
Pxx500 Aug 29, 2026
160fa17
order GitHub pull request observations
Pxx500 Aug 29, 2026
30b97c6
create manual tickets from GitHub links
Pxx500 Aug 29, 2026
7c08234
load GitHub App credentials at runtime
Pxx500 Aug 29, 2026
61fa299
resolve GitHub members through live participation policy
Pxx500 Aug 29, 2026
85e758c
start GitHub integration with cog lifecycle
Pxx500 Aug 29, 2026
860e10a
configure GitHub integration at runtime
Pxx500 Aug 29, 2026
319b8e4
require absolute GitHub secret paths
Pxx500 Aug 29, 2026
5543b6d
document GitHub App integration
Pxx500 Aug 29, 2026
8128370
complete GitHub ticket lifecycle controls
Pxx500 Aug 29, 2026
af7cf77
tighten GitHub integration types
Pxx500 Aug 29, 2026
2f2874f
clarify process-wide GitHub configuration
Pxx500 Aug 29, 2026
8d2edee
disclose GitHub integration data
Pxx500 Aug 29, 2026
e14136f
synchronize Discord ticket actions with GitHub
Pxx500 Aug 29, 2026
4eb869f
harden GitHub delivery recovery
Pxx500 Aug 29, 2026
edb9fd7
align aggregate data contract test
Pxx500 Aug 29, 2026
8bcf681
complete GitHub delivery recovery
Pxx500 Aug 29, 2026
a7ff812
coalesce overlapping GitHub recovery
Pxx500 Aug 29, 2026
8432609
fix review findings for GitHub integration
Pxx500 Aug 29, 2026
82a2f50
keep lifecycle events independent of title updates
Pxx500 Aug 29, 2026
9fdce83
settle empty-title ticket creation events
Pxx500 Aug 29, 2026
362eb49
retry GitHub category prompts after activation
Pxx500 Aug 29, 2026
576872b
fix category prompt recovery scheduling
Pxx500 Aug 29, 2026
547b6fb
use profile links and fixed GitHub secrets
Pxx500 Aug 30, 2026
408140e
merge master into GitHub App integration
Pxx500 Aug 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
with:
python-version: "3.10"
- name: Install quality tools
run: python -m pip install ruff mypy Pillow pillow-avif-plugin matplotlib aiohttp==3.9.5 pytest pytest-xdist
run: python -m pip install ruff mypy Pillow pillow-avif-plugin matplotlib aiohttp==3.9.5 PyJWT cryptography pytest pytest-xdist
- name: Ruff
run: ruff check .
# Typing is a separate phase. mypy still reports errors across both cogs,
Expand Down
4 changes: 4 additions & 0 deletions NHCogs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,10 @@ async def setup(bot: Red) -> None:
try:
if consoledump := await _load_subcog(bot, ".consoledump", "ConsoleDump"):
loaded.append(consoledump)
if operational_errors := await _load_subcog(
bot, ".operationalerrors", "OperationalErrors"
):
loaded.append(operational_errors)
nhmisc = await _load_subcog(bot, ".nhmisc", "NHMisc")
if nhmisc is not None:
loaded.append(nhmisc)
Expand Down
11 changes: 9 additions & 2 deletions NHCogs/consoledump/log_buffer.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,15 @@
r"[^&#\s]+"
)
_NAMED_SECRET_PATTERN = re.compile(
r"(?i)\b((?:access[_-]?token|api[_-]?key|password|secret|token)\s*[:=]\s*)"
r"(?i)\b((?:access[_-]?token|api[_-]?key|client[_-]?secret|"
r"webhook[_-]?secret|private[_-]?key|password|secret|token)\s*[:=]\s*)"
r"(?:['\"])?[^\s,'\";]+(?:['\"])?"
)
_PRIVATE_KEY_BLOCK_PATTERN = re.compile(
r"-----BEGIN (?P<label>(?:RSA |EC |OPENSSH )?PRIVATE KEY)-----.*?"
r"-----END (?P=label)-----",
re.DOTALL,
)
_DISCORD_TOKEN_PATTERN = re.compile(
r"(?<![A-Za-z0-9_-])(?:mfa\.[A-Za-z0-9_-]{20,}|"
r"[A-Za-z0-9_-]{23,28}\.[A-Za-z0-9_-]{6}\.[A-Za-z0-9_-]{27,})"
Expand Down Expand Up @@ -66,9 +72,10 @@ class LogDump:


def redact_log_text(text: str) -> str:
redacted = _PRIVATE_KEY_BLOCK_PATTERN.sub(_REDACTED, text)
redacted = _AUTHORIZATION_PATTERN.sub(
lambda match: match.group(1) + match.group(2) + _REDACTED,
text,
redacted,
)
redacted = _BEARER_PATTERN.sub("Bearer " + _REDACTED, redacted)
redacted = _WEBHOOK_PATTERN.sub(lambda match: match.group(1) + _REDACTED, redacted)
Expand Down
4 changes: 2 additions & 2 deletions NHCogs/custom_commands/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ Save validates and writes the complete draft in one transaction. Cancel and the
30-minute timeout make no changes. Completed threads are locked and archived.

Missing arguments, invalid values, cooldowns, and other expected command failures use
Red's normal command feedback. Unexpected failures are also reported through NHMisc's
configured error destination and maintainer ping.
Red's normal command feedback. Unexpected failures use the shared OperationalErrors
reporter configured through `[p]nhcogs errors`.

## Cooldowns and deletion

Expand Down
25 changes: 16 additions & 9 deletions NHCogs/custom_commands/cog.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from redbot.core.utils import menus
from redbot.core.utils.chat_formatting import pagify

from ..operational_errors import report_operational_error
from .catalog import (
CatalogError,
CustomCommand,
Expand Down Expand Up @@ -126,7 +127,8 @@ async def on_error(
_item: discord.ui.Item[CommandListView],
) -> None:
if interaction.guild is not None:
await self._cog.nhmisc.report_operational_error(
await report_operational_error(
self._cog.bot,
guild_id=interaction.guild.id,
source="CustomCommands",
action="browse custom command list",
Expand Down Expand Up @@ -226,7 +228,8 @@ async def on_error(
_item: discord.ui.Item[RawResponseView],
) -> None:
if interaction.guild is not None:
await self._cog.nhmisc.report_operational_error(
await report_operational_error(
self._cog.bot,
guild_id=interaction.guild.id,
source="CustomCommands",
action="browse raw custom command responses",
Expand Down Expand Up @@ -329,7 +332,8 @@ async def on_error(
_item: discord.ui.Item[DeleteConfirmationView],
) -> None:
if interaction.guild is not None:
await self._cog.nhmisc.report_operational_error(
await report_operational_error(
self._cog.bot,
guild_id=interaction.guild.id,
source="CustomCommands",
action="delete custom command",
Expand Down Expand Up @@ -377,10 +381,9 @@ def __init__(
self.runtime = CustomCommandRuntime(
bot,
self.catalog,
nhmisc.operational_errors,
logger=log,
)
self.workflows = WorkflowManager(self.catalog, nhmisc, logger=log)
self.workflows = WorkflowManager(bot, self.catalog, nhmisc, logger=log)

async def cog_load(self) -> None:
await self.catalog.initialize()
Expand Down Expand Up @@ -430,7 +433,8 @@ async def on_command_error(
return
command = getattr(ctx, "command", None)
action = getattr(command, "qualified_name", None) or "unknown command"
await self.nhmisc.report_operational_error(
await report_operational_error(
self.bot,
guild_id=guild.id,
source="CustomCommands",
action=action,
Expand All @@ -443,7 +447,8 @@ async def _log_moderation_action(self, guild, content: str) -> None:
try:
await self.nhmisc.send_moderation_log(guild, content)
except Exception as error:
await self.nhmisc.report_operational_error(
await report_operational_error(
self.bot,
guild_id=guild.id,
source="CustomCommands",
action="publish custom command moderator log",
Expand All @@ -464,7 +469,8 @@ async def _report_view_timeout_error(
exc_info=(type(error), error, error.__traceback__),
)
return
await self.nhmisc.report_operational_error(
await report_operational_error(
self.bot,
guild_id=guild.id,
source="CustomCommands",
action=action,
Expand Down Expand Up @@ -942,7 +948,8 @@ async def on_message_without_command(self, message: discord.Message) -> None:
await self.runtime.handle_message(message)
except Exception as error:
channel = message.channel
await self.nhmisc.report_operational_error(
await report_operational_error(
self.bot,
guild_id=message.guild.id,
source="CustomCommands",
action="process custom command message",
Expand Down
14 changes: 9 additions & 5 deletions NHCogs/custom_commands/migration_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from redbot.core import Config, commands
from redbot.core.data_manager import cog_data_path

from ..operational_errors import report_operational_error
from .catalog import CustomCommand, CustomCommandCatalog
from .lifecycle import CutoverController, ReplacementActivator
from .migration import (
Expand Down Expand Up @@ -93,7 +94,8 @@ async def cog_command_error(
return
if ctx.guild is None:
return
await self.nhmisc.report_operational_error(
await report_operational_error(
self.bot,
guild_id=ctx.guild.id,
source="CustomCommands",
action="legacy migration command",
Expand Down Expand Up @@ -223,7 +225,8 @@ async def _apply_confirmed(self, ctx: commands.Context) -> None:
latest = await self.state_store.get()
if latest.phase is not MigrationPhase.COMPLETE:
await self.controller.restore_official()
await self.nhmisc.report_operational_error(
await report_operational_error(
self.bot,
guild_id=ctx.guild.id,
source="CustomCommands",
action="apply legacy migration",
Expand All @@ -238,7 +241,8 @@ async def _apply_confirmed(self, ctx: commands.Context) -> None:
try:
await self.bot.remove_cog(self.qualified_name)
except Exception as error:
await self.nhmisc.report_operational_error(
await report_operational_error(
self.bot,
guild_id=ctx.guild.id,
source="CustomCommands",
action="remove completed migration command",
Expand Down Expand Up @@ -287,7 +291,6 @@ async def _require_private_migration_context(self, ctx: commands.Context) -> Non
raise commands.UserFeedbackCheckFailure(
"Run migration in a channel hidden from @everyone"
)
await self.nhmisc.require_private_error_channel(ctx.guild)

@staticmethod
def _write_artifacts(plan: MigrationPlan) -> Path:
Expand Down Expand Up @@ -366,7 +369,8 @@ async def build_custom_commands_component(bot: Any, nhmisc: Any):
log.exception("Custom Commands replacement startup failed")
guilds = tuple(bot.guilds)
if guilds:
await nhmisc.report_operational_error(
await report_operational_error(
bot,
guild_id=guilds[0].id,
source="CustomCommands",
action="activate replacement startup",
Expand Down
25 changes: 11 additions & 14 deletions NHCogs/custom_commands/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from redbot.core.commands import Parameter
from redbot.core.utils.chat_formatting import humanize_list

from ..operational_errors import report_operational_error
from .arguments import (
MAX_ARGUMENT_INDEX,
PLACEHOLDER_PATTERN,
Expand Down Expand Up @@ -45,14 +46,12 @@ def __init__(
self,
bot: Any,
catalog: CustomCommandCatalog,
operational_errors: Any,
*,
random_index: Callable[[int], int] = random.randrange,
logger: Any,
):
self._bot = bot
self._catalog = catalog
self._operational_errors = operational_errors
self._random_index = random_index
self._logger = logger
self._cooldown_deadlines: dict[tuple[str, int, str, int], float] = {}
Expand Down Expand Up @@ -330,18 +329,16 @@ async def _report(self, ctx: Any, action: str, error: BaseException) -> None:
if getattr(channel, "parent", None) is not None
else None
)
try:
await self._operational_errors.report(
guild_id=guild.id,
source="CustomCommands",
action=action,
error=error,
channel_id=getattr(channel, "id", None),
thread_id=thread_id,
message_id=getattr(getattr(ctx, "message", None), "id", None),
)
except Exception:
self._logger.exception("Failed to report CustomCommands operational error")
await report_operational_error(
self._bot,
guild_id=guild.id,
source="CustomCommands",
action=action,
error=error,
channel_id=getattr(channel, "id", None),
thread_id=thread_id,
message_id=getattr(getattr(ctx, "message", None), "id", None),
)

@staticmethod
async def _callback(*_args: Any, **_kwargs: Any) -> None:
Expand Down
24 changes: 12 additions & 12 deletions NHCogs/custom_commands/workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import discord

from ..operational_errors import report_operational_error
from .arguments import ArgumentSignatureError, argument_signature
from .catalog import (
MAX_RESPONSE_LENGTH,
Expand Down Expand Up @@ -730,15 +731,16 @@ async def report_interaction_error(
class WorkflowManager:
def __init__(
self,
bot: Any,
catalog: CustomCommandCatalog,
nhmisc: Any,
*,
logger: logging.Logger,
session_timeout_seconds: float = SESSION_TIMEOUT_SECONDS,
):
self._bot = bot
self.catalog = catalog
self._nhmisc = nhmisc
self._operational_errors = nhmisc.operational_errors
self.logger = logger
self.session_timeout_seconds = session_timeout_seconds
self._sessions: dict[int, WorkflowSession] = {}
Expand Down Expand Up @@ -865,14 +867,12 @@ async def _report_failure(
thread_id: int | None = None,
message_id: int | None = None,
) -> None:
try:
await self._operational_errors.report(
guild_id=guild_id,
source="CustomCommands",
action=action,
error=error,
thread_id=thread_id,
message_id=message_id,
)
except Exception:
self.logger.exception("Failed to report CustomCommands workflow error")
await report_operational_error(
self._bot,
guild_id=guild_id,
source="CustomCommands",
action=action,
error=error,
thread_id=thread_id,
message_id=message_id,
)
Loading
Loading