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

184 lines
8.3 KiB
Python

import hmac
import logging
from django.conf import settings
from drf_spectacular.utils import OpenApiResponse, extend_schema
from rest_framework import serializers, status, viewsets
from rest_framework.decorators import action
from rest_framework.request import Request
from rest_framework.response import Response
from insights.models.integration import Integration
from insights.models.organization import OrganizationMembership
from insights.models.team import Team
from products.review.backend.temporal.client import start_review_pr_workflow
from products.review.backend.temporal.types import TRIGGER_LABEL
logger = logging.getLogger(__name__)
# v1 scope: Review only runs against the main Insights monorepo. Matched case-insensitively.
ALLOWED_REPOS = {"insights/insights"}
class ReviewTriggerRequestSerializer(serializers.Serializer):
repo = serializers.CharField(
help_text="GitHub repository to review, in 'owner/name' form; must be on the allowlist (e.g. 'Insights/insights').",
)
pr_number = serializers.IntegerField(
min_value=1,
help_text="Pull request number to review.",
)
publish = serializers.BooleanField(
required=False,
default=True,
help_text="Whether to post the review back to the PR. Defaults true (the label trigger publishes).",
)
class ReviewTriggerResponseSerializer(serializers.Serializer):
workflow_id = serializers.CharField(help_text="Temporal workflow id for the started review run.")
status = serializers.CharField(help_text="Run lifecycle marker; 'started' when the review was queued.")
class ReviewTriggerErrorSerializer(serializers.Serializer):
error = serializers.CharField(help_text="Human-readable explanation of why the trigger was rejected.")
def _bearer_token(request: Request) -> str:
"""Extract the bearer token from the Authorization header (accepts a bare token too)."""
auth = request.headers.get("Authorization", "")
if auth.startswith("Bearer "):
return auth[len("Bearer ") :].strip()
return auth.strip()
def _resolve_run_user_id(team_id: int) -> int | None:
"""The user the sandbox tasks run as: the GitHub integration creator if still an active org
member, else the oldest active org member (same semantics as signals' resolve_user_id_for_team).
A disabled run user is worse than none: every user-scoped sandbox credential 403s and the agent
hangs silently until the poll budget expires, so never return an inactive user here.
"""
team = Team.objects.select_related("organization").get(id=team_id)
integration = Integration.objects.filter(team_id=team_id, kind="github").order_by("id").first()
if integration is not None and integration.created_by_id:
creator_is_active = OrganizationMembership.objects.filter(
organization=team.organization,
user_id=integration.created_by_id,
user__is_active=True,
).exists()
if creator_is_active:
return integration.created_by_id
logger.warning(
"Review run-user fallback: integration creator %s is not an active org member",
integration.created_by_id,
)
membership = (
OrganizationMembership.objects.select_related("user")
.filter(organization=team.organization, user__is_active=True)
.order_by("id")
.first()
)
return membership.user_id if membership else None
class ReviewTriggerViewSet(viewsets.ViewSet):
"""Shared-secret-gated trigger that starts a Review review for a PR.
Unscoped (no team in the URL): the team and run user are resolved server-side from settings, and CI
authenticates with the `REVIEWFN_TRIGGER_TOKEN` shared secret — no session or API key. The first
client is the `review` label GitHub Action, but the endpoint is the durable reusable interface.
"""
authentication_classes = ()
permission_classes = ()
def _authenticate(self, request: Request) -> Response | None:
"""Return an error Response if the shared-secret check fails, else None."""
expected = settings.REVIEWFN_TRIGGER_TOKEN
if not expected:
# Fail closed in real deployments; allow locally so the endpoint is testable without a secret.
if settings.DEBUG or settings.TEST:
return None
return Response(
{"error": "Review trigger token is not configured"},
status=status.HTTP_403_FORBIDDEN,
)
if not hmac.compare_digest(_bearer_token(request), expected):
return Response({"error": "Invalid trigger token"}, status=status.HTTP_403_FORBIDDEN)
return None
@extend_schema(
request=ReviewTriggerRequestSerializer,
responses={
202: OpenApiResponse(response=ReviewTriggerResponseSerializer, description="Review run started"),
400: OpenApiResponse(
response=ReviewTriggerErrorSerializer, description="Invalid body or unresolved run user"
),
403: OpenApiResponse(
response=ReviewTriggerErrorSerializer, description="Missing/invalid token or disallowed repo"
),
503: OpenApiResponse(response=ReviewTriggerErrorSerializer, description="Trigger team not configured"),
},
summary="Trigger a Review PR review",
description=(
"Start a single-turn Review review for a pull request and (by default) publish it back to "
"the PR. Authenticated with the REVIEWFN_TRIGGER_TOKEN shared secret in the Authorization "
"header. Non-blocking: returns the Temporal workflow id immediately while the review runs in "
"the worker."
),
)
@action(detail=False, methods=["POST"], url_path="trigger")
def trigger(self, request: Request) -> Response:
auth_error = self._authenticate(request)
if auth_error is not None:
return auth_error
serializer = ReviewTriggerRequestSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
repo: str = serializer.validated_data["repo"]
pr_number: int = serializer.validated_data["pr_number"]
publish: bool = serializer.validated_data["publish"]
if repo.lower() not in ALLOWED_REPOS:
return Response({"error": f"Repository {repo} is not allowed"}, status=status.HTTP_403_FORBIDDEN)
team_id = settings.REVIEWFN_TEAM_ID
if not team_id:
return Response({"error": "Review team is not configured"}, status=status.HTTP_503_SERVICE_UNAVAILABLE)
user_id = settings.REVIEWFN_RUN_USER_ID or _resolve_run_user_id(team_id)
if not user_id:
return Response(
{"error": "No active run user found for the team (no GitHub integration creator or org member)"},
status=status.HTTP_400_BAD_REQUEST,
)
# Fail loud here rather than silently downstream: sandbox credentials are authorized against
# active org membership, so an unauthorized run user hangs the review until its poll budget expires.
run_user_is_authorized = OrganizationMembership.objects.filter(
organization__team__id=team_id,
user_id=user_id,
user__is_active=True,
).exists()
if not run_user_is_authorized:
# The Action echoes error bodies into public CI logs, so the id stays server-side.
logger.warning("Review trigger: run user %s is not an active member of the team's organization", user_id)
return Response(
{"error": "Configured run user is not an active member of the team's organization"},
status=status.HTTP_400_BAD_REQUEST,
)
# Forks are rejected server-side in the workflow's fetch activity (and by the Action gate); the
# endpoint stays free of GitHub I/O and returns immediately.
pr_url = f"https://github.com/{repo}/pull/{pr_number}"
workflow_id = start_review_pr_workflow(
pr_url=pr_url, team_id=team_id, user_id=user_id, publish=publish, trigger_source=TRIGGER_LABEL
)
logger.info(f"Review trigger started workflow {workflow_id} for {repo}#{pr_number} (publish={publish})")
return Response(
ReviewTriggerResponseSerializer({"workflow_id": workflow_id, "status": "started"}).data,
status=status.HTTP_202_ACCEPTED,
)