chore: import upstream snapshot with attribution
CI / Clippy (push) Failing after 15m13s
CI / Test (ubuntu-latest) (push) Failing after 16m1s
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Build (no embeddings / no ORT) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Cookbook (Node) (push) Has been cancelled
CI / Pi Extension (Node) (push) Has been cancelled
CI / Rust SDK (lean-ctx-client) (push) Has been cancelled
CI / Embed SDK (lean-ctx-sdk) (push) Has been cancelled
CI / Python SDK (leanctx) (push) Has been cancelled
CI / Hermes Plugin (Python) (push) Has been cancelled
CI / SDK Conformance Matrix (push) Has been cancelled
CI / Coverage (push) Has been cancelled
CI / cargo-deny (push) Has been cancelled
CI / Adversarial Safety (push) Has been cancelled
CI / Benchmarks (push) Has been cancelled
CI / Output-Quality Gate (eval A/B) (push) Has been cancelled
CI / Documentation (push) Has been cancelled
CI / CI Green (push) Has been cancelled
JetBrains Plugin / Actionlint (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (rust) (push) Has been cancelled
JetBrains Plugin / Validation (push) Has been cancelled
JetBrains Plugin / Build (push) Has been cancelled
JetBrains Plugin / Test (push) Has been cancelled
Security Check / Security Scan (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:35:30 +08:00
commit 26382a7ac6
2353 changed files with 629146 additions and 0 deletions
+211
View File
@@ -0,0 +1,211 @@
#!/usr/bin/env node
// Posts a single release-announcement tweet for @leanctx.
//
// Triggered from .github/workflows/release.yml after a stable release is
// created. Dependency-free (Node built-ins only) so the workflow needs no
// `npm install`. Auth is Twitter/X API v2 + OAuth 1.0a user context.
//
// Required env:
// TWITTER_CONSUMER_KEY, TWITTER_CONSUMER_SECRET,
// TWITTER_ACCESS_TOKEN, TWITTER_ACCESS_SECRET
// RELEASE_TAG e.g. "v3.6.26"
// REPO e.g. "yvgude/lean-ctx"
// Optional env:
// DRY_RUN=1 compose + print the tweet, do not post
// CHANGELOG path to changelog (default: CHANGELOG.md)
import crypto from "node:crypto";
import https from "node:https";
import { readFileSync } from "node:fs";
const MAX_TWEET = 280;
function requireEnv(name) {
const v = process.env[name];
if (!v) {
console.error(`Missing required env: ${name}`);
process.exit(1);
}
return v;
}
/**
* Pull a concise, human highlight for `version` from the changelog.
* Prefers the section's blockquote summary (the "EPIC …" one-liner); falls
* back to a factual count of Added/Fixed/Changed/Security entries.
* Returns "" when nothing meaningful is available (caller posts version+link).
*/
function extractHighlight(changelog, version) {
const lines = changelog.split(/\r?\n/);
// Match `## [<version>]` literally — `version` is data, never compiled into a
// RegExp, so there is no escaping to get wrong and no regex-injection surface.
const needle = `[${version}]`;
const start = lines.findIndex(
(l) => l.startsWith("##") && l.slice(2).trimStart().startsWith(needle),
);
if (start === -1) return "";
const section = [];
for (let i = start + 1; i < lines.length; i++) {
if (/^##\s*\[/.test(lines[i])) break;
section.push(lines[i]);
}
// Join the section's leading blockquote (the "EPIC …" summary) into one line.
const quoteLines = [];
for (const l of section) {
if (/^\s*>/.test(l)) {
quoteLines.push(l.replace(/^\s*>\s?/, ""));
} else if (quoteLines.length) {
break; // end of the contiguous blockquote block
} else if (l.trim() === "") {
continue; // skip blank lines before the quote
} else {
break; // section starts with real content -> no summary quote
}
}
if (quoteLines.length) {
const clean = quoteLines.join(" ").replace(/\*\*/g, "").replace(/\s+/g, " ").trim();
if (clean) return clean;
}
const counts = {};
let cat = null;
for (const l of section) {
const h = l.match(/^###\s+(\w+)/);
if (h) {
cat = h[1];
continue;
}
if (cat && /^\s*-\s+/.test(l)) counts[cat] = (counts[cat] || 0) + 1;
}
const order = ["Added", "Fixed", "Changed", "Security"];
const labels = {
Added: ["new feature", "new features"],
Fixed: ["fix", "fixes"],
Changed: ["change", "changes"],
Security: ["security fix", "security fixes"],
};
const parts = order
.filter((c) => counts[c])
.map((c) => `${counts[c]} ${labels[c][counts[c] === 1 ? 0 : 1]}`);
return parts.length ? parts.join(" · ") : "";
}
function composeTweet(tag, repo, highlight) {
const url = `https://github.com/${repo}/releases/tag/${tag}`;
const header = `🚀 lean-ctx ${tag} is out`;
// Twitter counts every URL as 23 chars (t.co), so reserve that, not url.length.
const urlCost = 23;
let body = header;
if (highlight) {
const budget = MAX_TWEET - header.length - urlCost - 4; // 2×"\n\n"
let h = highlight;
if (h.length > budget) h = h.slice(0, Math.max(0, budget - 1)).trimEnd() + "…";
if (h) body += `\n\n${h}`;
}
return `${body}\n\n${url}`;
}
function postTweet(text) {
const CK = requireEnv("TWITTER_CONSUMER_KEY");
const CS = requireEnv("TWITTER_CONSUMER_SECRET");
const AT = requireEnv("TWITTER_ACCESS_TOKEN");
const AS = requireEnv("TWITTER_ACCESS_SECRET");
const endpoint = "https://api.twitter.com/2/tweets";
const pct = (s) =>
encodeURIComponent(s).replace(/[!'()*]/g, (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase());
const oauth = {
oauth_consumer_key: CK,
oauth_nonce: crypto.randomBytes(16).toString("hex"),
oauth_signature_method: "HMAC-SHA1",
oauth_timestamp: Math.floor(Date.now() / 1000).toString(),
oauth_token: AT,
oauth_version: "1.0",
};
const paramStr = Object.keys(oauth)
.sort()
.map((k) => `${pct(k)}=${pct(oauth[k])}`)
.join("&");
const baseStr = `POST&${pct(endpoint)}&${pct(paramStr)}`;
const signingKey = `${pct(CS)}&${pct(AS)}`;
const signature = crypto.createHmac("sha1", signingKey).update(baseStr).digest("base64");
const authHeader =
"OAuth " +
Object.entries({ ...oauth, oauth_signature: signature })
.map(([k, v]) => `${k}="${pct(v)}"`)
.join(", ");
const payload = JSON.stringify({ text });
return new Promise((resolve, reject) => {
const req = https.request(
endpoint,
{
method: "POST",
headers: {
Authorization: authHeader,
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(payload),
},
},
(res) => {
let data = "";
res.on("data", (c) => (data += c));
res.on("end", () => resolve({ status: res.statusCode, body: data }));
},
);
req.on("error", reject);
req.write(payload);
req.end();
});
}
async function main() {
const tag = requireEnv("RELEASE_TAG");
const repo = requireEnv("REPO");
const version = tag.replace(/^v/, "");
const changelogPath = process.env.CHANGELOG || "CHANGELOG.md";
let highlight = "";
try {
highlight = extractHighlight(readFileSync(changelogPath, "utf8"), version);
} catch (e) {
console.warn(`Could not read ${changelogPath}: ${e.message} — posting version + link only`);
}
const tweet = composeTweet(tag, repo, highlight);
// Twitter weights every URL as 23 chars (t.co), regardless of real length.
const weighted = tweet.replace(/https?:\/\/\S+/g, "x".repeat(23)).length;
console.log(`Tweet (${weighted}/${MAX_TWEET} weighted chars):\n---\n${tweet}\n---`);
if (process.env.DRY_RUN === "1" || process.argv.includes("--dry-run")) {
console.log("DRY_RUN — not posting.");
return;
}
const res = await postTweet(tweet);
if (res.status === 403 && res.body.includes("duplicate content")) {
console.log("Tweet already posted (duplicate) — treating as success.");
return;
}
if (res.status !== 201) {
console.error(`Twitter API error ${res.status}: ${res.body}`);
process.exit(1);
}
let id = "";
try {
id = JSON.parse(res.body).data.id;
} catch {
/* keep id empty */
}
console.log(`Posted: https://x.com/leanctx/status/${id}`);
}
main().catch((e) => {
console.error("Fatal:", e);
process.exit(1);
});