Skip to content

.github/workflows/ixuca-smoke-watchdog.yml #49

.github/workflows/ixuca-smoke-watchdog.yml

.github/workflows/ixuca-smoke-watchdog.yml #49

name: IXUCA-Smoke-Watchdog
on:
workflow_dispatch:
schedule:
- cron: "*/15 * * * *"
permissions:
actions: write
contents: read
concurrency:
group: ixuca-smoke-watchdog
cancel-in-progress: true
jobs:
check-and-alert:
name: Check IXUCA smoke status and alert
runs-on: ubuntu-latest
steps:
- name: Fetch previous watchdog state artifact
id: fetch_state
uses: actions/github-script@v7
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const artifactsResp = await github.rest.actions.listArtifactsForRepo({
owner,
repo,
per_page: 100
});
const artifacts = artifactsResp.data.artifacts || [];
const candidates = artifacts
.filter(a => a.name === "ixuca-smoke-watchdog-state" && !a.expired)
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
if (candidates.length > 0) {
core.setOutput("artifact_id", String(candidates[0].id));
core.info(`Found previous state artifact: ${candidates[0].id}`);
} else {
core.info("No previous state artifact found.");
}
- name: Download previous watchdog state zip
if: steps.fetch_state.outputs.artifact_id != ''
uses: actions/github-script@v7
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const artifactId = Number("${{ steps.fetch_state.outputs.artifact_id }}");
const fs = require("fs");
const response = await github.rest.actions.downloadArtifact({
owner,
repo,
artifact_id: artifactId,
archive_format: "zip"
});
fs.writeFileSync("state.zip", Buffer.from(response.data));
core.info("Downloaded previous state artifact to state.zip");
- name: Extract previous watchdog state
if: steps.fetch_state.outputs.artifact_id != ''
run: |
python3 - <<'PY'
import zipfile
with zipfile.ZipFile("state.zip") as zf:
if "state.json" not in zf.namelist():
raise SystemExit("state.json not found in artifact")
with zf.open("state.json") as src, open("prev_state.json", "wb") as dst:
dst.write(src.read())
print("Extracted previous state to prev_state.json")
PY
- name: Evaluate recent smoke runs
id: evaluate
uses: actions/github-script@v7
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const workflow_id = "ixuca-smoke-schedule.yml";
const fs = require("fs");
const now = new Date();
const oneDayMs = 24 * 60 * 60 * 1000;
let prevState = {
status: "UNKNOWN",
last_alerted_at: null,
last_alert_type: ""
};
if (fs.existsSync("prev_state.json")) {
try {
prevState = JSON.parse(fs.readFileSync("prev_state.json", "utf8"));
} catch (err) {
core.warning(`Failed to parse prev_state.json: ${err.message}`);
}
}
const runsResp = await github.rest.actions.listWorkflowRuns({
owner,
repo,
workflow_id,
status: "completed",
per_page: 10
});
const runs = runsResp.data.workflow_runs || [];
const recent = runs.slice(0, 2);
if (recent.length < 2) {
core.info("Not enough completed runs to evaluate. Need at least 2.");
core.setOutput("should_alert", "false");
return;
}
const latest = recent[0];
const previous = recent[1];
const schedulingIssue =
latest.conclusion === "cancelled" &&
previous.conclusion === "cancelled";
const smokeFailed = latest.conclusion === "failure";
let shouldAlert = false;
let alertType = "";
let alertText = "";
let status = "OK";
if (schedulingIssue) {
status = "SCHEDULING_ISSUE";
} else if (smokeFailed) {
status = "DOCKER_CHECK_FAILED";
}
const prevAlertAt = prevState.last_alerted_at
? new Date(prevState.last_alerted_at)
: null;
const sameIssue = prevState.status === status && status !== "OK";
const canRepeat = prevAlertAt && now - prevAlertAt >= oneDayMs;
if (status !== "OK") {
if (sameIssue && prevAlertAt && !canRepeat) {
shouldAlert = false;
alertText =
`已告警且状态未变化,暂不重复告警:status=${status}。\n` +
`latest: ${latest.html_url}\n` +
`previous: ${previous.html_url}`;
} else {
shouldAlert = true;
alertType = status;
if (status === "SCHEDULING_ISSUE") {
alertText =
`调度异常:最近两次已完成运行均为 cancelled。\n` +
`latest: ${latest.html_url}\n` +
`previous: ${previous.html_url}`;
} else {
alertText =
`容器检查失败:最近一次运行为 failure。\n` +
`run_url: ${latest.html_url}`;
}
if (sameIssue && canRepeat) {
alertText =
`持续未修复超过1天,重复告警。\n` +
alertText;
}
}
} else if (prevState.status && prevState.status !== "OK") {
if (latest.conclusion === "success") {
shouldAlert = true;
alertType = "RECOVERED";
alertText =
`问题已修复:previous=${prevState.status},latest=${latest.conclusion}。\n` +
`run_url: ${latest.html_url}`;
} else {
alertText =
`问题尚未修复:previous=${prevState.status},latest=${latest.conclusion}。\n` +
`run_url: ${latest.html_url}`;
}
} else {
alertText =
`IXUCA-Smoke-Schedule 正常:latest=${latest.conclusion}, previous=${previous.conclusion}`;
}
core.info(alertText);
core.setOutput("should_alert", shouldAlert ? "true" : "false");
core.setOutput("alert_type", alertType);
core.setOutput("alert_text", alertText);
const nextState = {
status,
updated_at: now.toISOString(),
last_alerted_at: prevState.last_alerted_at || null,
last_alert_type: prevState.last_alert_type || ""
};
if (status === "OK") {
nextState.last_alerted_at = null;
nextState.last_alert_type = "";
} else if (shouldAlert && alertType !== "RECOVERED") {
nextState.last_alerted_at = now.toISOString();
nextState.last_alert_type = alertType;
}
fs.writeFileSync("state.json", JSON.stringify(nextState, null, 2));
- name: Send alert to Baidu IM robot
if: steps.evaluate.outputs.should_alert == 'true'
env:
WEBHOOK_URL: ${{ secrets.BAIDU_IM_WEBHOOK_URL }}
ALERT_TEXT: ${{ steps.evaluate.outputs.alert_text }}
ALERT_TYPE: ${{ steps.evaluate.outputs.alert_type }}
TO_ID: "11992367"
run: |
python3 - <<'PY'
import json
import os
import time
import urllib.request
webhook_url = os.environ.get("WEBHOOK_URL", "").strip()
if not webhook_url:
raise SystemExit("Need secret BAIDU_IM_WEBHOOK_URL for alerting.")
alert_text = os.environ["ALERT_TEXT"]
alert_type = os.environ["ALERT_TYPE"]
to_id = int(os.environ.get("TO_ID", "11992367"))
payload = {
"message": {
"header": {
"toid": to_id,
"totype": "GROUP",
"msgtype": "TEXT",
"clientmsgid": int(time.time() * 1000),
"role": "robot"
},
"body": [
{
"content": f"[{alert_type}] {alert_text}",
"type": "TEXT"
}
]
}
}
req = urllib.request.Request(
webhook_url,
data=json.dumps(payload).encode("utf-8"),
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req, timeout=20) as resp:
print(f"alert sent, status={resp.status}")
PY
- name: Upload watchdog state
if: steps.evaluate.conclusion == 'success'
uses: actions/upload-artifact@v4
with:
name: ixuca-smoke-watchdog-state
path: state.json
retention-days: 7