SDK release notifier #23
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |