Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7eb8dde3da | |||
| 54b6ce0d57 |
@@ -547,12 +547,12 @@ def file_or_update(
|
||||
|
||||
if dry_run:
|
||||
print(f"::notice::[dry-run] would file/update drift issue for {branch}")
|
||||
print(f"::group::[dry-run] title")
|
||||
print("::group::[dry-run] title")
|
||||
print(title)
|
||||
print(f"::endgroup::")
|
||||
print(f"::group::[dry-run] body")
|
||||
print("::endgroup::")
|
||||
print("::group::[dry-run] body")
|
||||
print(body)
|
||||
print(f"::endgroup::")
|
||||
print("::endgroup::")
|
||||
return
|
||||
|
||||
existing = find_open_issue(title)
|
||||
|
||||
@@ -13,7 +13,6 @@ from __future__ import annotations
|
||||
import argparse
|
||||
import glob
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import NamedTuple
|
||||
|
||||
|
||||
@@ -283,7 +283,7 @@ def _ensure_labels(repo: str, names: list[str]) -> list[int]:
|
||||
if status != "ok" or not isinstance(labels, list):
|
||||
return []
|
||||
out: list[int] = []
|
||||
by_name = {l["name"]: l["id"] for l in labels if isinstance(l, dict)}
|
||||
by_name = {lbl["name"]: lbl["id"] for lbl in labels if isinstance(lbl, dict)}
|
||||
for n in names:
|
||||
if n in by_name:
|
||||
out.append(by_name[n])
|
||||
|
||||
@@ -82,7 +82,7 @@ import sys
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
@@ -338,7 +338,6 @@ def compute_ack_state(
|
||||
# Filter out self-acks and unknown slugs.
|
||||
ackers_per_slug: dict[str, list[str]] = {s: [] for s in items_by_slug}
|
||||
rejected_self: dict[str, list[str]] = {s: [] for s in items_by_slug}
|
||||
rejected_unknown: dict[str, list[str]] = {s: [] for s in items_by_slug}
|
||||
pending_team_check: dict[str, list[str]] = {s: [] for s in items_by_slug}
|
||||
|
||||
for (user, slug), kind in latest_directive.items():
|
||||
@@ -842,7 +841,7 @@ def render_status(
|
||||
def get_tier_mode(pr: dict[str, Any], cfg: dict[str, Any]) -> str:
|
||||
"""Read tier label, return 'hard' or 'soft' per cfg.tier_failure_mode."""
|
||||
labels = pr.get("labels") or []
|
||||
tier_labels = [l.get("name", "") for l in labels if (l.get("name", "") or "").startswith("tier:")]
|
||||
tier_labels = [lbl.get("name", "") for lbl in labels if (lbl.get("name", "") or "").startswith("tier:")]
|
||||
mode_map = cfg.get("tier_failure_mode") or {}
|
||||
default_mode = cfg.get("default_mode", "hard")
|
||||
for tl in tier_labels:
|
||||
@@ -865,7 +864,7 @@ def is_high_risk(pr: dict[str, Any], cfg: dict[str, Any]) -> bool:
|
||||
Governance fix for internal#442 — closes the inconsistency between
|
||||
sop-tier-check (tier-aware) and sop-checklist (was tier-blind).
|
||||
"""
|
||||
label_set = {(l.get("name") or "") for l in (pr.get("labels") or [])}
|
||||
label_set = {(lbl.get("name") or "") for lbl in (pr.get("labels") or [])}
|
||||
if "tier:high" in label_set:
|
||||
return True
|
||||
high_risk_labels = set(cfg.get("high_risk_labels") or [])
|
||||
|
||||
@@ -80,7 +80,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
# GET /repos/{owner}/{name}/pulls/{pr_number}
|
||||
m = re.match(r"^/api/v1/repos/([^/]+)/([^/]+)/pulls/(\d+)$", path)
|
||||
if m:
|
||||
owner, name, pr_num = m.group(1), m.group(2), m.group(3)
|
||||
_owner, _name, pr_num = m.group(1), m.group(2), m.group(3)
|
||||
if sc == "T2_pr_closed":
|
||||
return self._json(200, {
|
||||
"number": int(pr_num),
|
||||
@@ -146,7 +146,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
# GET /teams/{team_id}/members/{username}
|
||||
m = re.match(r"^/api/v1/teams/(\d+)/members/([^/]+)$", path)
|
||||
if m:
|
||||
team_id, login = m.group(1), m.group(2)
|
||||
_team_id, _login = m.group(1), m.group(2)
|
||||
if sc == "T8_team_not_member":
|
||||
return self._empty(404)
|
||||
if sc == "T9_team_403":
|
||||
|
||||
@@ -15,7 +15,6 @@ Mirrors the pattern in scripts/ops/test_check_migration_collisions.py
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
@@ -22,7 +22,6 @@ from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
# Resolve sibling script regardless of where pytest is invoked from.
|
||||
|
||||
@@ -281,8 +281,8 @@ def main() -> int:
|
||||
for prefix, peers in sorted(open_pr_collisions.items()):
|
||||
peer_str = ", ".join(f"#{p['number']} ({p['headRefName']})" for p in peers)
|
||||
print(f"::error::migration prefix {prefix:03d} also claimed by open PR(s): {peer_str}")
|
||||
print(f"::error::rebase coordination needed — only one PR can land a given prefix; "
|
||||
f"renumber yours or theirs")
|
||||
print("::error::rebase coordination needed — only one PR can land a given prefix; "
|
||||
"renumber yours or theirs")
|
||||
return 1
|
||||
|
||||
|
||||
|
||||
@@ -18,9 +18,7 @@ No network. No live Gitea calls.
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import textwrap
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
@@ -55,9 +55,7 @@ from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -164,7 +162,7 @@ def test_bp_orphan_context_fails(envset, monkeypatch, capsys):
|
||||
" all-required:\n runs-on: x\n steps:\n - run: echo hi\n",
|
||||
)
|
||||
m = _import_lint()
|
||||
posted = _stub_api(
|
||||
_posted = _stub_api(
|
||||
monkeypatch,
|
||||
m,
|
||||
("ok", {"status_check_contexts": [
|
||||
|
||||
@@ -60,10 +60,8 @@ from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@@ -53,10 +53,7 @@ from __future__ import annotations
|
||||
import importlib.util
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import textwrap
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@@ -61,9 +61,7 @@ from __future__ import annotations
|
||||
import importlib.util
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@@ -38,9 +38,7 @@ from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@ from __future__ import annotations
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.error
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
@@ -542,7 +541,6 @@ def test_auto_close_skips_when_main_pending(wd_module, monkeypatch):
|
||||
"""main pending (CI still running) at NEW_SHA → leave old issue alone.
|
||||
Pending could resolve to red, so closing prematurely would lose the
|
||||
breadcrumb of the prior red."""
|
||||
old_title = f"[main-red] owner/repo: {SHA_RED[:10]}"
|
||||
stub = _make_stub_api({
|
||||
("GET", "/repos/owner/repo/branches/main"): (200, _branches_response(SHA_GREEN)),
|
||||
("GET", f"/repos/owner/repo/commits/{SHA_GREEN}/status"): (
|
||||
@@ -790,7 +788,7 @@ def test_emit_loki_event_prints_json_line(wd_module, capsys, monkeypatch):
|
||||
captured = capsys.readouterr()
|
||||
assert "main-red-watchdog event:" in captured.out
|
||||
# Find the JSON payload after the prefix and verify it parses
|
||||
line = [l for l in captured.out.splitlines() if "main-red-watchdog event:" in l][0]
|
||||
line = [ln for ln in captured.out.splitlines() if "main-red-watchdog event:" in ln][0]
|
||||
payload = json.loads(line.split("main-red-watchdog event:", 1)[1].strip())
|
||||
assert payload["event_type"] == "main_red_detected"
|
||||
assert payload["repo"] == "owner/repo"
|
||||
|
||||
@@ -40,7 +40,6 @@ Dependencies: stdlib + pytest + PyYAML. No network.
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
@@ -853,7 +852,6 @@ def test_reap_skips_combined_success_shas(sr_module, monkeypatch):
|
||||
Mock 2 SHAs with combined=success + 1 with combined=failure → only
|
||||
the failure-SHA's statuses get the per-context loop applied.
|
||||
"""
|
||||
per_context_iterated_for: list[str] = []
|
||||
posts: list[tuple[str, dict]] = []
|
||||
|
||||
failure_statuses = [
|
||||
|
||||
@@ -23,11 +23,9 @@ import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Optional
|
||||
|
||||
# ── Gitea API client ────────────────────────────────────────────────────────
|
||||
|
||||
@@ -160,9 +158,9 @@ def signal_1_comment_scan(pr_number: int, repo: str) -> dict:
|
||||
# Build reverse map: login -> (group, agent_key)
|
||||
login_to_group = {}
|
||||
for group, login in relevant_roles.items():
|
||||
for role, l in AGENT_LOGIN_MAP.items():
|
||||
if l == login:
|
||||
login_to_group[l] = (group, f"core-{role}")
|
||||
for role, agt_login in AGENT_LOGIN_MAP.items():
|
||||
if agt_login == login:
|
||||
login_to_group[agt_login] = (group, f"core-{role}")
|
||||
|
||||
# Collect all agent-tag matches from comments
|
||||
comments = []
|
||||
|
||||
@@ -33,7 +33,12 @@ const (
|
||||
//
|
||||
// StartPolling returns nil immediately — Discord does not support long-polling;
|
||||
// use the Interactions webhook route instead.
|
||||
type DiscordAdapter struct{}
|
||||
type DiscordAdapter struct {
|
||||
// httpClient overrides the default HTTP client used by SendMessage.
|
||||
// Used in tests to inject a fake transport so token-redaction checks
|
||||
// are deterministic and do not depend on real network behavior.
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func (d *DiscordAdapter) Type() string { return "discord" }
|
||||
func (d *DiscordAdapter) DisplayName() string { return "Discord" }
|
||||
@@ -95,7 +100,10 @@ func (d *DiscordAdapter) SendMessage(ctx context.Context, config map[string]inte
|
||||
// Split long messages into chunks at word boundaries where possible.
|
||||
chunks := splitMessage(text, maxLen)
|
||||
|
||||
client := &http.Client{Timeout: discordHTTPTimeout}
|
||||
client := d.httpClient
|
||||
if client == nil {
|
||||
client = &http.Client{Timeout: discordHTTPTimeout}
|
||||
}
|
||||
for _, chunk := range chunks {
|
||||
payload, err := json.Marshal(map[string]string{"content": chunk})
|
||||
if err != nil {
|
||||
|
||||
@@ -3,6 +3,7 @@ package channels
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
@@ -288,17 +289,25 @@ func TestSplitMessage_LongMessage(t *testing.T) {
|
||||
}
|
||||
|
||||
// TestDiscordAdapter_SendMessage_ErrorDoesNotLeakToken verifies that when the
|
||||
// HTTP call to the Discord webhook fails (e.g. DNS error), the returned error
|
||||
// message does NOT contain the webhook URL — which embeds the Discord token.
|
||||
// HTTP call to the Discord webhook fails, the returned error message does NOT
|
||||
// contain the webhook URL — which embeds the Discord token. Uses a fake
|
||||
// http.RoundTripper so the test is deterministic and does not depend on real
|
||||
// network behavior (fixes RCA #1763 Finding 2).
|
||||
// Regression test for the MEDIUM security finding in PR #659.
|
||||
func TestDiscordAdapter_SendMessage_ErrorDoesNotLeakToken(t *testing.T) {
|
||||
a := &DiscordAdapter{}
|
||||
// Use a valid-looking webhook URL with a fake token so we can check it
|
||||
// doesn't appear in the error string.
|
||||
fakeToken := "SUPER_SECRET_DISCORD_TOKEN_12345"
|
||||
webhookURL := discordWebhookPrefix + "123456789/" + fakeToken
|
||||
|
||||
// Point at an unroutable address to force a dial error.
|
||||
// Fake transport that returns an error containing the full webhook URL,
|
||||
// mimicking the real behavior of net/http when a dial fails.
|
||||
fakeTransport := roundTripperFunc(func(req *http.Request) (*http.Response, error) {
|
||||
return nil, fmt.Errorf("dial tcp: connect to %s: connection refused", req.URL.String())
|
||||
})
|
||||
|
||||
a := &DiscordAdapter{
|
||||
httpClient: &http.Client{Transport: fakeTransport},
|
||||
}
|
||||
|
||||
err := a.SendMessage(
|
||||
context.Background(),
|
||||
map[string]interface{}{"webhook_url": webhookURL},
|
||||
@@ -307,14 +316,20 @@ func TestDiscordAdapter_SendMessage_ErrorDoesNotLeakToken(t *testing.T) {
|
||||
)
|
||||
|
||||
if err == nil {
|
||||
// In some environments the request might actually succeed; that's fine.
|
||||
t.Skip("request unexpectedly succeeded — skipping token-leak check")
|
||||
t.Fatal("expected error from fake transport, got nil")
|
||||
}
|
||||
if strings.Contains(err.Error(), fakeToken) {
|
||||
t.Errorf("error message leaks Discord webhook token: %q", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// roundTripperFunc adapts a function to the http.RoundTripper interface.
|
||||
type roundTripperFunc func(*http.Request) (*http.Response, error)
|
||||
|
||||
func (f roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return f(req)
|
||||
}
|
||||
|
||||
func TestSplitMessage_SplitsAtNewline(t *testing.T) {
|
||||
// Build a message where a newline falls within the split window.
|
||||
line1 := strings.Repeat("a", 1500) + "\n"
|
||||
|
||||
Reference in New Issue
Block a user