Add dependency-review caller workflow (#3)
Some checks are pending
Deploy / deploy (push) Waiting to run

* Add dependency-review caller workflow

Add a pull_request-triggered caller that invokes the org-level
callable-dependency-review workflow to scan dependency changes and
fail on high-severity advisories.

* chore: retrigger checks

* chore: retrigger dep review (post-fix)

* style: ruff format src
This commit is contained in:
Adam Moussa 2026-06-05 12:34:16 -04:00 • committed by GitHub
parent 899f07d09c
commit b7c65cedfb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 150 additions and 55 deletions

View file

@ -0,0 +1,6 @@
name: Dependency Review
on:
pull_request:
jobs:
review:
uses: Sea-Haven-Industries/.github/.github/workflows/callable-dependency-review.yaml@main

View file

@ -53,6 +53,7 @@ def _get_slack_token():
# Front API # Front API
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _front_get(url_or_path, params=None): def _front_get(url_or_path, params=None):
if url_or_path.startswith("http"): if url_or_path.startswith("http"):
url = url_or_path url = url_or_path
@ -61,10 +62,13 @@ def _front_get(url_or_path, params=None):
if params: if params:
url += "?" + parse.urlencode(params, doseq=True) url += "?" + parse.urlencode(params, doseq=True)
req = request.Request(url, headers={ req = request.Request(
"Authorization": f"Bearer {_get_front_token()}", url,
"Accept": "application/json", headers={
}) "Authorization": f"Bearer {_get_front_token()}",
"Accept": "application/json",
},
)
time.sleep(RATE_LIMIT_DELAY) time.sleep(RATE_LIMIT_DELAY)
@ -97,12 +101,17 @@ def _front_paginate(path, params=None):
# Slack API # Slack API
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _slack_post(method, payload): def _slack_post(method, payload):
data = json.dumps(payload).encode() data = json.dumps(payload).encode()
req = request.Request(f"{SLACK_BASE}/{method}", data=data, headers={ req = request.Request(
"Content-Type": "application/json; charset=utf-8", f"{SLACK_BASE}/{method}",
"Authorization": f"Bearer {_get_slack_token()}", data=data,
}) headers={
"Content-Type": "application/json; charset=utf-8",
"Authorization": f"Bearer {_get_slack_token()}",
},
)
try: try:
with request.urlopen(req) as resp: with request.urlopen(req) as resp:
@ -117,9 +126,12 @@ def _slack_post(method, payload):
def _slack_get(method, params): def _slack_get(method, params):
url = f"{SLACK_BASE}/{method}?" + parse.urlencode(params) url = f"{SLACK_BASE}/{method}?" + parse.urlencode(params)
req = request.Request(url, headers={ req = request.Request(
"Authorization": f"Bearer {_get_slack_token()}", url,
}) headers={
"Authorization": f"Bearer {_get_slack_token()}",
},
)
try: try:
with request.urlopen(req) as resp: with request.urlopen(req) as resp:
@ -146,17 +158,21 @@ def _resolve_slack_user(email):
def _send_slack(channel, blocks, text): def _send_slack(channel, blocks, text):
_slack_post("chat.postMessage", { _slack_post(
"channel": channel, "chat.postMessage",
"blocks": blocks, {
"text": text, "channel": channel,
}) "blocks": blocks,
"text": text,
},
)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Business time calculation (weekdays only, Eastern time) # Business time calculation (weekdays only, Eastern time)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _business_minutes_elapsed(since_utc, now_utc): def _business_minutes_elapsed(since_utc, now_utc):
since = since_utc.astimezone(EASTERN) since = since_utc.astimezone(EASTERN)
now = now_utc.astimezone(EASTERN) now = now_utc.astimezone(EASTERN)
@ -186,6 +202,7 @@ def _business_minutes_elapsed(since_utc, now_utc):
# DynamoDB dedup # DynamoDB dedup
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _already_alerted(conv_id, tier): def _already_alerted(conv_id, tier):
resp = _get_table().get_item(Key={"conversationId": conv_id}) resp = _get_table().get_item(Key={"conversationId": conv_id})
item = resp.get("Item") item = resp.get("Item")
@ -229,6 +246,7 @@ def _record_run(today_str):
# Slack message blocks # Slack message blocks
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _tier1_blocks(conv, assignee_email=None): def _tier1_blocks(conv, assignee_email=None):
subject = conv.get("subject", "No subject") subject = conv.get("subject", "No subject")
conv_id = conv.get("id", "") conv_id = conv.get("id", "")
@ -238,13 +256,25 @@ def _tier1_blocks(conv, assignee_email=None):
status = f"Assigned to {assignee_email}" if assignee_email else "Unassigned" status = f"Assigned to {assignee_email}" if assignee_email else "Unassigned"
return [ return [
{"type": "header", "text": {"type": "plain_text", "text": ":warning: SLA Breach: 1-Hour Acknowledgment"}}, {
{"type": "section", "text": {"type": "mrkdwn", "text": ( "type": "header",
f"*<{link}|{subject}>*\n" "text": {
f"Last inbound: {ts}\n" "type": "plain_text",
f"Status: {status}\n" "text": ":warning: SLA Breach: 1-Hour Acknowledgment",
f"_No reply for over 1 business hour._" },
)}}, },
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": (
f"*<{link}|{subject}>*\n"
f"Last inbound: {ts}\n"
f"Status: {status}\n"
f"_No reply for over 1 business hour._"
),
},
},
] ]
@ -258,13 +288,25 @@ def _tier2_blocks(conv):
status = f"Assigned to {assignee['email']}" if assignee else "Unassigned" status = f"Assigned to {assignee['email']}" if assignee else "Unassigned"
return [ return [
{"type": "header", "text": {"type": "plain_text", "text": ":rotating_light: SLA Breach: 1-Day Action Required"}}, {
{"type": "section", "text": {"type": "mrkdwn", "text": ( "type": "header",
f"*<{link}|{subject}>*\n" "text": {
f"Last inbound: {ts}\n" "type": "plain_text",
f"Status: {status}\n" "text": ":rotating_light: SLA Breach: 1-Day Action Required",
f"_No reply for over 1 business day. Immediate attention required._" },
)}}, },
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": (
f"*<{link}|{subject}>*\n"
f"Last inbound: {ts}\n"
f"Status: {status}\n"
f"_No reply for over 1 business day. Immediate attention required._"
),
},
},
] ]
@ -291,7 +333,13 @@ def _summary_blocks(tier1_breaches, tier2_breaches):
total = len(tier1_breaches) + len(tier2_breaches) total = len(tier1_breaches) + len(tier2_breaches)
return [ return [
{"type": "header", "text": {"type": "plain_text", "text": f":sunrise: Morning SLA Summary — {total} breach{'es' if total != 1 else ''}"}}, {
"type": "header",
"text": {
"type": "plain_text",
"text": f":sunrise: Morning SLA Summary — {total} breach{'es' if total != 1 else ''}",
},
},
{"type": "section", "text": {"type": "mrkdwn", "text": "\n".join(lines)}}, {"type": "section", "text": {"type": "mrkdwn", "text": "\n".join(lines)}},
] ]
@ -300,6 +348,7 @@ def _summary_blocks(tier1_breaches, tier2_breaches):
# Handler # Handler
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def handler(event, context): def handler(event, context):
ack_threshold = int(os.environ["ACK_SLA_MINUTES"]) ack_threshold = int(os.environ["ACK_SLA_MINUTES"])
action_threshold = int(os.environ["ACTION_SLA_MINUTES"]) action_threshold = int(os.environ["ACTION_SLA_MINUTES"])
@ -317,7 +366,9 @@ def handler(event, context):
return {"skipped": True, "reason": "before_start_date"} return {"skipped": True, "reason": "before_start_date"}
if now_et.hour < BH_START or now_et.hour >= BH_END: if now_et.hour < BH_START or now_et.hour >= BH_END:
logger.info("Outside business hours (%s ET), skipping", now_et.strftime("%H:%M")) logger.info(
"Outside business hours (%s ET), skipping", now_et.strftime("%H:%M")
)
return {"skipped": True, "reason": "outside_business_hours"} return {"skipped": True, "reason": "outside_business_hours"}
today_str = now_et.strftime("%Y-%m-%d") today_str = now_et.strftime("%Y-%m-%d")
@ -336,8 +387,11 @@ def handler(event, context):
if monitor_set: if monitor_set:
shared = [i for i in shared if i.get("name", "").lower() in monitor_set] shared = [i for i in shared if i.get("name", "").lower() in monitor_set]
logger.info("Monitoring %d inboxes: %s", len(shared), logger.info(
", ".join(i.get("name", "?") for i in shared)) "Monitoring %d inboxes: %s",
len(shared),
", ".join(i.get("name", "?") for i in shared),
)
tier1_breaches = [] tier1_breaches = []
tier2_breaches = [] tier2_breaches = []
@ -356,7 +410,9 @@ def handler(event, context):
logger.error("Failed to fetch conversations for %s: %s", inbox_name, e) logger.error("Failed to fetch conversations for %s: %s", inbox_name, e)
continue continue
logger.info("Inbox '%s': %d recent open conversations", inbox_name, len(conversations)) logger.info(
"Inbox '%s': %d recent open conversations", inbox_name, len(conversations)
)
for conv in conversations: for conv in conversations:
if conv.get("id") in seen: if conv.get("id") in seen:
@ -377,12 +433,18 @@ def handler(event, context):
logger.info("Tier 2 breach: %s", conv_id) logger.info("Tier 2 breach: %s", conv_id)
elif elapsed >= ack_threshold and not _already_alerted(conv_id, 1): elif elapsed >= ack_threshold and not _already_alerted(conv_id, 1):
email = assignee["email"] if assignee and assignee.get("email") else None email = (
assignee["email"]
if assignee and assignee.get("email")
else None
)
tier1_breaches.append((conv, email)) tier1_breaches.append((conv, email))
logger.info("Tier 1 breach: %s", conv_id) logger.info("Tier 1 breach: %s", conv_id)
except Exception: except Exception:
logger.exception("Error processing conversation %s", conv.get("id", "?")) logger.exception(
"Error processing conversation %s", conv.get("id", "?")
)
total = len(tier1_breaches) + len(tier2_breaches) total = len(tier1_breaches) + len(tier2_breaches)
@ -398,18 +460,27 @@ def handler(event, context):
for conv in tier2_breaches: for conv in tier2_breaches:
adam_uid = _resolve_slack_user(adam_email) adam_uid = _resolve_slack_user(adam_email)
target = adam_uid or alert_channel target = adam_uid or alert_channel
_send_slack(target, _tier2_blocks(conv), _send_slack(
f"SLA Breach: {conv.get('subject', '')} - 1 day without reply") target,
_tier2_blocks(conv),
f"SLA Breach: {conv.get('subject', '')} - 1 day without reply",
)
for conv, email in tier1_breaches: for conv, email in tier1_breaches:
if email: if email:
uid = _resolve_slack_user(email) uid = _resolve_slack_user(email)
target = uid or alert_channel target = uid or alert_channel
_send_slack(target, _tier1_blocks(conv, email), _send_slack(
f"SLA Breach: {conv.get('subject', '')} - 1 hour without reply") target,
_tier1_blocks(conv, email),
f"SLA Breach: {conv.get('subject', '')} - 1 hour without reply",
)
else: else:
_send_slack(alert_channel, _tier1_blocks(conv), _send_slack(
f"SLA Breach: {conv.get('subject', '')} - unassigned, 1 hour without reply") alert_channel,
_tier1_blocks(conv),
f"SLA Breach: {conv.get('subject', '')} - unassigned, 1 hour without reply",
)
for conv in tier2_breaches: for conv in tier2_breaches:
_record_alert(conv["id"], 2) _record_alert(conv["id"], 2)

View file

@ -52,14 +52,18 @@ def _list_google_users(org_units):
page_token = None page_token = None
while True: while True:
result = service.users().list( result = (
customer="my_customer", service.users()
maxResults=500, .list(
projection="full", customer="my_customer",
orderBy="email", maxResults=500,
query=f"orgUnitPath='{ou}'", projection="full",
pageToken=page_token, orderBy="email",
).execute() query=f"orgUnitPath='{ou}'",
pageToken=page_token,
)
.execute()
)
for user in result.get("users", []): for user in result.get("users", []):
email = user.get("primaryEmail", "") email = user.get("primaryEmail", "")
@ -71,7 +75,11 @@ def _list_google_users(org_units):
if not page_token: if not page_token:
break break
logger.info(" Found %d users in %s", len([u for u in all_users if u.get("primaryEmail") in seen]), ou) logger.info(
" Found %d users in %s",
len([u for u in all_users if u.get("primaryEmail") in seen]),
ou,
)
return all_users return all_users
@ -88,8 +96,12 @@ def _extract_user_fields(google_user):
phone = "" phone = ""
phones = google_user.get("phones", []) phones = google_user.get("phones", [])
if phones: if phones:
work_phone = next((p for p in phones if p.get("type") == "work" and p.get("value")), None) work_phone = next(
primary_phone = next((p for p in phones if p.get("primary") and p.get("value")), None) (p for p in phones if p.get("type") == "work" and p.get("value")), None
)
primary_phone = next(
(p for p in phones if p.get("primary") and p.get("value")), None
)
fallback = next((p for p in phones if p.get("value")), None) fallback = next((p for p in phones if p.get("value")), None)
phone = (work_phone or primary_phone or fallback or {}).get("value", "") phone = (work_phone or primary_phone or fallback or {}).get("value", "")
@ -147,7 +159,13 @@ def handler(event, context):
google_users = _list_google_users(org_units) google_users = _list_google_users(org_units)
logger.info("Found %d total users across %d OUs", len(google_users), len(org_units)) logger.info("Found %d total users across %d OUs", len(google_users), len(org_units))
summary = {"updated": 0, "not_in_front": 0, "no_data": 0, "errors": 0, "error_details": []} summary = {
"updated": 0,
"not_in_front": 0,
"no_data": 0,
"errors": 0,
"error_details": [],
}
for user in google_users: for user in google_users:
email, job_title, phone = _extract_user_fields(user) email, job_title, phone = _extract_user_fields(user)