mirror of
https://github.com/Sea-Haven-Industries/front-integrations.git
synced 2026-09-30 06:03:13 +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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _front_get(url_or_path, params=None):
|
||||
if url_or_path.startswith("http"):
|
||||
url = url_or_path
|
||||
|
|
@ -61,10 +62,13 @@ def _front_get(url_or_path, params=None):
|
|||
if params:
|
||||
url += "?" + parse.urlencode(params, doseq=True)
|
||||
|
||||
req = request.Request(url, headers={
|
||||
"Authorization": f"Bearer {_get_front_token()}",
|
||||
"Accept": "application/json",
|
||||
})
|
||||
req = request.Request(
|
||||
url,
|
||||
headers={
|
||||
"Authorization": f"Bearer {_get_front_token()}",
|
||||
"Accept": "application/json",
|
||||
},
|
||||
)
|
||||
|
||||
time.sleep(RATE_LIMIT_DELAY)
|
||||
|
||||
|
|
@ -97,12 +101,17 @@ def _front_paginate(path, params=None):
|
|||
# Slack API
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _slack_post(method, payload):
|
||||
data = json.dumps(payload).encode()
|
||||
req = request.Request(f"{SLACK_BASE}/{method}", data=data, headers={
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
"Authorization": f"Bearer {_get_slack_token()}",
|
||||
})
|
||||
req = request.Request(
|
||||
f"{SLACK_BASE}/{method}",
|
||||
data=data,
|
||||
headers={
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
"Authorization": f"Bearer {_get_slack_token()}",
|
||||
},
|
||||
)
|
||||
|
||||
try:
|
||||
with request.urlopen(req) as resp:
|
||||
|
|
@ -117,9 +126,12 @@ def _slack_post(method, payload):
|
|||
|
||||
def _slack_get(method, params):
|
||||
url = f"{SLACK_BASE}/{method}?" + parse.urlencode(params)
|
||||
req = request.Request(url, headers={
|
||||
"Authorization": f"Bearer {_get_slack_token()}",
|
||||
})
|
||||
req = request.Request(
|
||||
url,
|
||||
headers={
|
||||
"Authorization": f"Bearer {_get_slack_token()}",
|
||||
},
|
||||
)
|
||||
|
||||
try:
|
||||
with request.urlopen(req) as resp:
|
||||
|
|
@ -146,17 +158,21 @@ def _resolve_slack_user(email):
|
|||
|
||||
|
||||
def _send_slack(channel, blocks, text):
|
||||
_slack_post("chat.postMessage", {
|
||||
"channel": channel,
|
||||
"blocks": blocks,
|
||||
"text": text,
|
||||
})
|
||||
_slack_post(
|
||||
"chat.postMessage",
|
||||
{
|
||||
"channel": channel,
|
||||
"blocks": blocks,
|
||||
"text": text,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Business time calculation (weekdays only, Eastern time)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _business_minutes_elapsed(since_utc, now_utc):
|
||||
since = since_utc.astimezone(EASTERN)
|
||||
now = now_utc.astimezone(EASTERN)
|
||||
|
|
@ -186,6 +202,7 @@ def _business_minutes_elapsed(since_utc, now_utc):
|
|||
# DynamoDB dedup
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _already_alerted(conv_id, tier):
|
||||
resp = _get_table().get_item(Key={"conversationId": conv_id})
|
||||
item = resp.get("Item")
|
||||
|
|
@ -229,6 +246,7 @@ def _record_run(today_str):
|
|||
# Slack message blocks
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _tier1_blocks(conv, assignee_email=None):
|
||||
subject = conv.get("subject", "No subject")
|
||||
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"
|
||||
|
||||
return [
|
||||
{"type": "header", "text": {"type": "plain_text", "text": ":warning: SLA Breach: 1-Hour Acknowledgment"}},
|
||||
{"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._"
|
||||
)}},
|
||||
{
|
||||
"type": "header",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": ":warning: SLA Breach: 1-Hour Acknowledgment",
|
||||
},
|
||||
},
|
||||
{
|
||||
"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"
|
||||
|
||||
return [
|
||||
{"type": "header", "text": {"type": "plain_text", "text": ":rotating_light: SLA Breach: 1-Day Action 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._"
|
||||
)}},
|
||||
{
|
||||
"type": "header",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": ":rotating_light: SLA Breach: 1-Day Action 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)
|
||||
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)}},
|
||||
]
|
||||
|
||||
|
|
@ -300,6 +348,7 @@ def _summary_blocks(tier1_breaches, tier2_breaches):
|
|||
# Handler
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def handler(event, context):
|
||||
ack_threshold = int(os.environ["ACK_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"}
|
||||
|
||||
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"}
|
||||
|
||||
today_str = now_et.strftime("%Y-%m-%d")
|
||||
|
|
@ -336,8 +387,11 @@ def handler(event, context):
|
|||
if monitor_set:
|
||||
shared = [i for i in shared if i.get("name", "").lower() in monitor_set]
|
||||
|
||||
logger.info("Monitoring %d inboxes: %s", len(shared),
|
||||
", ".join(i.get("name", "?") for i in shared))
|
||||
logger.info(
|
||||
"Monitoring %d inboxes: %s",
|
||||
len(shared),
|
||||
", ".join(i.get("name", "?") for i in shared),
|
||||
)
|
||||
|
||||
tier1_breaches = []
|
||||
tier2_breaches = []
|
||||
|
|
@ -356,7 +410,9 @@ def handler(event, context):
|
|||
logger.error("Failed to fetch conversations for %s: %s", inbox_name, e)
|
||||
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:
|
||||
if conv.get("id") in seen:
|
||||
|
|
@ -377,12 +433,18 @@ def handler(event, context):
|
|||
logger.info("Tier 2 breach: %s", conv_id)
|
||||
|
||||
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))
|
||||
logger.info("Tier 1 breach: %s", conv_id)
|
||||
|
||||
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)
|
||||
|
||||
|
|
@ -398,18 +460,27 @@ def handler(event, context):
|
|||
for conv in tier2_breaches:
|
||||
adam_uid = _resolve_slack_user(adam_email)
|
||||
target = adam_uid or alert_channel
|
||||
_send_slack(target, _tier2_blocks(conv),
|
||||
f"SLA Breach: {conv.get('subject', '')} - 1 day without reply")
|
||||
_send_slack(
|
||||
target,
|
||||
_tier2_blocks(conv),
|
||||
f"SLA Breach: {conv.get('subject', '')} - 1 day without reply",
|
||||
)
|
||||
|
||||
for conv, email in tier1_breaches:
|
||||
if email:
|
||||
uid = _resolve_slack_user(email)
|
||||
target = uid or alert_channel
|
||||
_send_slack(target, _tier1_blocks(conv, email),
|
||||
f"SLA Breach: {conv.get('subject', '')} - 1 hour without reply")
|
||||
_send_slack(
|
||||
target,
|
||||
_tier1_blocks(conv, email),
|
||||
f"SLA Breach: {conv.get('subject', '')} - 1 hour without reply",
|
||||
)
|
||||
else:
|
||||
_send_slack(alert_channel, _tier1_blocks(conv),
|
||||
f"SLA Breach: {conv.get('subject', '')} - unassigned, 1 hour without reply")
|
||||
_send_slack(
|
||||
alert_channel,
|
||||
_tier1_blocks(conv),
|
||||
f"SLA Breach: {conv.get('subject', '')} - unassigned, 1 hour without reply",
|
||||
)
|
||||
|
||||
for conv in tier2_breaches:
|
||||
_record_alert(conv["id"], 2)
|
||||
|
|
|
|||
|
|
@ -52,14 +52,18 @@ def _list_google_users(org_units):
|
|||
page_token = None
|
||||
|
||||
while True:
|
||||
result = service.users().list(
|
||||
customer="my_customer",
|
||||
maxResults=500,
|
||||
projection="full",
|
||||
orderBy="email",
|
||||
query=f"orgUnitPath='{ou}'",
|
||||
pageToken=page_token,
|
||||
).execute()
|
||||
result = (
|
||||
service.users()
|
||||
.list(
|
||||
customer="my_customer",
|
||||
maxResults=500,
|
||||
projection="full",
|
||||
orderBy="email",
|
||||
query=f"orgUnitPath='{ou}'",
|
||||
pageToken=page_token,
|
||||
)
|
||||
.execute()
|
||||
)
|
||||
|
||||
for user in result.get("users", []):
|
||||
email = user.get("primaryEmail", "")
|
||||
|
|
@ -71,7 +75,11 @@ def _list_google_users(org_units):
|
|||
if not page_token:
|
||||
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
|
||||
|
||||
|
|
@ -88,8 +96,12 @@ def _extract_user_fields(google_user):
|
|||
phone = ""
|
||||
phones = google_user.get("phones", [])
|
||||
if phones:
|
||||
work_phone = next((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)
|
||||
work_phone = next(
|
||||
(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)
|
||||
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)
|
||||
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:
|
||||
email, job_title, phone = _extract_user_fields(user)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue