Skip to content

SDK release notifier #23

SDK release notifier

SDK release notifier #23

name: SDK release notifier
on:
schedule:
- cron: "0 0 * * *"
workflow_dispatch:
jobs:
scan:
runs-on: ubuntu-latest
outputs:
payload: ${{ steps.scan.outputs.payload }}
count: ${{ steps.scan.outputs.count }}
steps:
- name: Scan releases
id: scan
run: |
node <<'JS'
const fs = require("fs");
const REPOS = [
["Milvus", "milvus-io/milvus"],
["PyMilvus", "milvus-io/pymilvus"],
["Milvus C++ SDK", "milvus-io/milvus-sdk-cpp"],
["Milvus Java SDK", "milvus-io/milvus-sdk-java"],
["Milvus Node SDK", "milvus-io/milvus-sdk-node"],
["Zilliz CLI", "zilliztech/zilliz-cli"],
];
const WINDOW_HOURS = 25;
async function fetchReleases(repo) {
const res = await fetch(`https://api.github.com/repos/${repo}/releases?per_page=20`, {
headers: {
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
"User-Agent": "sdk-release-notifier",
},
});
if (!res.ok) throw new Error(`GitHub ${res.status}: ${await res.text()}`);
return res.json();
}
const isNew = (r, since) => {
if (r.draft || r.prerelease) return false;
if (!r.published_at) return false;
return new Date(r.published_at).getTime() >= since;
};
(async () => {
const since = Date.now() - WINDOW_HOURS * 3600 * 1000;
console.log(`Scanning releases published since ${new Date(since).toISOString()}`);
const toSend = [];
for (const [sdkName, repo] of REPOS) {
try {
const releases = await fetchReleases(repo);
const hits = releases.filter(r => isNew(r, since));
console.log(`[${repo}] ${hits.length} new release(s)`);
for (const r of hits) {
toSend.push({
sdkName, repo,
tag_name: r.tag_name || "",
published_at: r.published_at,
body: r.body || "",
html_url: r.html_url || `https://github.com/${repo}/releases`,
});
}
} catch (e) {
console.error(`[${repo}] fetch failed: ${e.message}`);
}
}
fs.appendFileSync(process.env.GITHUB_OUTPUT, `count=${toSend.length}\n`);
fs.appendFileSync(process.env.GITHUB_OUTPUT, `payload<<EOF\n${JSON.stringify(toSend)}\nEOF\n`);
})();
JS
notify:
needs: scan
if: needs.scan.outputs.count != '0'
runs-on: ubuntu-latest
steps:
- name: Send Feishu notifications
env:
FEISHU_BOT_WEBHOOK: ${{ secrets.FEISHU_BOT_WEBHOOK }}
PAYLOAD: ${{ needs.scan.outputs.payload }}
run: |
node <<'JS'
const BODY_MAX = 100;
const SEND_INTERVAL_MS = 1000;
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const truncate = (s, n) => {
s = (s || "").trim();
if (!s) return "_无 Release Notes_";
return s.length <= n ? s : s.slice(0, n).trimEnd() + "…";
};
const formatCST = (iso) => {
const d = new Date(iso);
const p = new Intl.DateTimeFormat("zh-CN", {
timeZone: "Asia/Shanghai", year: "numeric", month: "2-digit",
day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: false,
}).formatToParts(d).reduce((a, x) => (a[x.type] = x.value, a), {});
return `${p.year}-${p.month}-${p.day} ${p.hour}:${p.minute} CST`;
};
const buildCard = (r) => ({
msg_type: "interactive",
card: {
config: { wide_screen_mode: true },
header: {
template: "blue",
title: { tag: "plain_text", content: `🎉 ${r.sdkName} ${r.tag_name}` },
},
elements: [
{ tag: "div", fields: [
{ is_short: true, text: { tag: "lark_md", content: `**仓库**\n${r.repo}` } },
{ is_short: true, text: { tag: "lark_md", content: `**版本**\n${r.tag_name}` } },
{ is_short: false, text: { tag: "lark_md", content: `**发布时间**\n${formatCST(r.published_at)}` } },
]},
{ tag: "hr" },
{ tag: "div", text: { tag: "lark_md", content: truncate(r.body, BODY_MAX) } },
{ tag: "action", actions: [{
tag: "button",
text: { tag: "plain_text", content: "查看 Release" },
type: "primary",
url: r.html_url,
}]},
],
},
});
async function sendFeishu(webhook, card) {
const res = await fetch(webhook, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(card),
});
const body = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(`HTTP ${res.status}: ${JSON.stringify(body)}`);
const code = body.code ?? body.StatusCode ?? 0;
if (code !== 0) throw new Error(`Feishu error: ${JSON.stringify(body)}`);
}
(async () => {
const webhook = process.env.FEISHU_BOT_WEBHOOK;
if (!webhook) { console.error("ERROR: FEISHU_BOT_WEBHOOK not set"); process.exit(2); }
const toSend = JSON.parse(process.env.PAYLOAD || "[]");
if (toSend.length === 0) { console.log("No new releases."); return; }
let failures = 0;
for (let i = 0; i < toSend.length; i++) {
if (i > 0) await sleep(SEND_INTERVAL_MS);
const r = toSend[i];
try {
await sendFeishu(webhook, buildCard(r));
console.log(`Sent: ${r.repo} ${r.tag_name}`);
} catch (e) {
failures++;
console.error(`FAILED: ${r.repo} ${r.tag_name}: ${e.message}`);
}
}
if (failures) process.exit(1);
})();
JS