mirror of
https://github.com/Sea-Haven-Industries/front-integrations.git
synced 2026-10-02 13:13:23 +00:00
Add dependency-review caller workflow (#3)
Some checks are pending
Deploy / deploy (push) Waiting to run
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:
parent
899f07d09c
commit
b7c65cedfb
3 changed files with 150 additions and 55 deletions
6
.github/workflows/dependency-review.yml
vendored
Normal file
6
.github/workflows/dependency-review.yml
vendored
Normal 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
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue