Files
zeekayandHanzo Dev b497511c04 no hogs: stamphog and review_hog are stamp and review
Both are Django apps, and both pin `label` in apps.py — "stamp" and "review".
django_migrations keys a row on (app_label, name), and app_label comes from that
pin and not from the import path, so moving the directory does not orphan a
single migration. All six stay where they are, untouched.

  products/stamphog -> products/stamp     name = products.stamp.backend
  products/review_hog -> products/review  name = products.review.backend
  StamphogScene/ -> StampScene/, and stamphogSceneLogic.ts with it

189 files follow. An earlier attempt at this move failed twice for reasons worth
recording: `git mv A B` where B exists puts A INSIDE B, and a stray
products/stamp/node_modules from an abandoned run was enough to make B exist —
the app ended up nested two deep. And `git ls-files` does not list a file moved
with plain mv and not yet staged, so a pass over "every tracked file" silently
skips it; that is how StampScene.tsx kept importing ./stampSceneLogic after the
file beside it had been renamed back.

bin/check-imports-are-committed: all first-party imports across 15,843 files
resolve.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-09 10:18:18 -07:00

81 lines
3.5 KiB
Python

"""Resolve a merged PR to a digest ``audience_key``.
Global best-effort cascade: repo-declared digest channel (see logic/digest_config.py) ->
PR author -> GitHub team slug -> "repo:{repository}" fallback. Digest grouping and channel
routing key off that opaque string alone (see models.DigestChannel).
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
import structlog
from .digest_config import load_repo_digest_config
from .github_client import StampGitHubClient
if TYPE_CHECKING:
from ..models import StampRepoConfig
logger = structlog.get_logger(__name__)
def _repository_audience_key(repo_config: StampRepoConfig) -> str:
# The pending distributed owners.yaml resolver (PR #68872, contact.slack) is a channel-
# resolution clue, not an audience_key source — it slots into logic/channel_resolution.py to
# correct which Slack channel a "repo:" fallback lands in, not this cascade.
return f"repo:{repo_config.repository}"
def _author_team_audience_key(repo_config: StampRepoConfig, pr_payload: dict[str, Any]) -> str:
"""Resolve the PR author's GitHub team, live, via the org's team memberships.
Falls back to the repository key (with a warning) whenever the author has no resolvable team —
missing fields, no team membership, or a failed lookup. Wrapped end-to-end so a flaky GitHub call
never blocks capture of the merged PR itself.
"""
try:
login = ((pr_payload or {}).get("user") or {}).get("login")
org = (repo_config.repository or "").split("/", 1)[0]
if not login or not org:
logger.warning(
"stamp_author_team_audience_missing_fields",
repository=repo_config.repository,
has_login=bool(login),
)
return _repository_audience_key(repo_config)
# One GraphQL call per merged PR — merge volume is tiny next to API limits, not worth a cache.
slugs = StampGitHubClient(repo_config.installation_id).get_user_team_slugs(org, login)
if not slugs:
logger.warning("stamp_author_team_audience_no_team", repository=repo_config.repository, login=login)
return _repository_audience_key(repo_config)
chosen, *other_teams = slugs
if other_teams:
logger.info(
"stamp_author_team_audience_multiple_teams",
login=login,
chosen=chosen,
other_teams=other_teams,
)
# `chosen` (the raw team slug) becomes the audience_key unchanged, and channel_resolution
# later matches it against a real Slack channel name for auto-provisioning. This trusts
# GitHub org team-slug governance; only PUBLIC channels are matched (see channel_resolution).
return chosen
except Exception:
logger.warning(
"stamp_author_team_audience_resolution_failed", repository=repo_config.repository, exc_info=True
)
return _repository_audience_key(repo_config)
def resolve_audience_key(repo_config: StampRepoConfig, pr_payload: dict[str, Any]) -> str:
"""Map a merged PR to its digest audience_key via the single global cascade."""
digest_config = load_repo_digest_config(repo_config)
if digest_config is not None and digest_config.channel:
# Repo declared a single digest channel for all its PRs, so they group per-repo
# instead of cascading through the author -> team lookup.
return _repository_audience_key(repo_config)
return _author_team_audience_key(repo_config, pr_payload)