SnowLuma v2.4.2 source snapshot
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
Publish MCP to npm / Build, test, and publish MCP (push) Waiting to run
Publish SDK to npm / Build, test, and publish SDK (push) Waiting to run
Sync action catalog to docs / sync (push) Waiting to run
Publish UI to npm / Build and publish UI (push) Waiting to run
Build and Release / preflight (push) Waiting to run

This commit is contained in:
TanTan committed 2026-11-13 09:02:00 +08:00
commit fa9c73363d
1367 files changed
+355735

No files matched your search

+78
View File
@@ -0,0 +1,78 @@
// Isolated retained-heap measurements; no QQ session, disk logs or live requests.
// node --expose-gc tools/bench-common-memory.mjs logs|colored-logs|cache [source-directory]
// The optional directory contains log-sanitize.ts and single-flight-cache.ts,
// allowing the same workload to run against sources from a baseline revision.
import { performance } from 'node:perf_hooks';
import { setImmediate, setTimeout } from 'node:timers/promises';
import { resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
if (!global.gc) {
throw new Error('Run with node --expose-gc');
}
const mode = process.argv[2] ?? 'logs';
const source = (name) =>
process.argv[3]
? pathToFileURL(resolve(process.argv[3], name))
: new URL(`../packages/common/src/${name}`, import.meta.url);
async function heapUsed() {
await setImmediate();
global.gc();
global.gc();
return process.memoryUsage().heapUsed;
}
const mib = (bytes) => Number((bytes / 1024 / 1024).toFixed(2));
if (mode === 'logs' || mode === 'colored-logs') {
const { sanitizeLogLine } = await import(source('log-sanitize.ts'));
const before = await heapUsed();
const records = [];
const started = performance.now();
for (let i = 0; i < 1000; i += 1) {
const body = `${i}: ${'0123456789abcdef'.repeat(512)}`;
const line = mode === 'colored-logs' ? `\x1b[32m${body}\x1b[0m` : body;
records.push(sanitizeLogLine(line));
}
const elapsedMs = Number((performance.now() - started).toFixed(1));
const retainedHeapMiB = mib((await heapUsed()) - before);
console.log(JSON.stringify({ mode, records: records.length, retainedHeapMiB, elapsedMs }));
} else if (mode === 'cache') {
const { createSingleFlightCache } = await import(source('single-flight-cache.ts'));
const ttlMs = 2000;
const cache = createSingleFlightCache({
ttlMs,
load: async (key) =>
Array.from({ length: 1000 }, (_, i) => ({
uin: key * 1000 + i,
nickname: `member-${key}-${i}`,
})),
});
const before = await heapUsed();
for (let key = 0; key < 500; key += 1) {
await cache.get(key);
}
const populatedHeapMiB = mib((await heapUsed()) - before);
await setTimeout(ttlMs + 100);
const idleHeapMiB = mib((await heapUsed()) - before);
// Keep the cache itself live through both samples, as it is in ContactsApi.
cache.clear();
console.log(
JSON.stringify({
mode,
rosters: 500,
membersPerRoster: 1000,
ttlMs,
populatedHeapMiB,
idleHeapMiB,
}),
);
} else {
throw new Error(`Unknown workload: ${mode}`);
}
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env node
// Sync the `version` field across the monorepo root and every workspace package.
// Usage: pnpm bump [version]; without an argument, sync the root version.
import { readFile, writeFile, readdir } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { parseVersion } from '../packages/common/src/semver.ts';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(__dirname, '..');
const nextVersion = process.argv[2] ?? JSON.parse(await readFile(path.join(repoRoot, 'package.json'), 'utf8')).version;
if (!parseVersion(nextVersion)) {
console.error(`Invalid semver: "${nextVersion}"`);
process.exit(1);
}
async function updatePkg(file) {
const raw = await readFile(file, 'utf-8');
const pkg = JSON.parse(raw);
const prev = pkg.version;
if (prev === nextVersion) {
console.log(` = ${path.relative(repoRoot, file)} (already ${nextVersion})`);
return;
}
pkg.version = nextVersion;
// Preserve trailing newline if the original file had one.
const trailing = raw.endsWith('\n') ? '\n' : '';
await writeFile(file, JSON.stringify(pkg, null, 2) + trailing);
console.log(` ✓ ${path.relative(repoRoot, file)}: ${prev} → ${nextVersion}`);
}
const files = [path.join(repoRoot, 'package.json')];
const packagesDir = path.join(repoRoot, 'packages');
for (const entry of await readdir(packagesDir, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const pkgFile = path.join(packagesDir, entry.name, 'package.json');
if (existsSync(pkgFile)) files.push(pkgFile);
}
console.log(`Bumping ${files.length} package.json files to ${nextVersion}:`);
for (const file of files) await updatePkg(file);
console.log('Done.');
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env node
// Same catalog gate as the CI `test` job. Keep the git invocation and
// failure text in lockstep with `.github/workflows/ci.yml`.
import { spawnSync } from 'node:child_process';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
function runGit(args, { inherit = false } = {}) {
return spawnSync('git', args, {
cwd: repoRoot,
encoding: 'utf-8',
stdio: inherit ? 'inherit' : ['ignore', 'pipe', 'pipe'],
});
}
const status = runGit([
'status',
'--porcelain=v1',
'--untracked-files=all',
'--',
'packages/mcp/src/generated',
]);
if (status.status !== 0) {
process.stderr.write(status.stderr || 'git status failed\n');
process.exit(status.status === null ? 1 : status.status);
}
const catalogStatus = (status.stdout || '').trim();
if (!catalogStatus) process.exit(0);
runGit(['diff', '--', 'packages/mcp/src/generated'], { inherit: true });
process.stdout.write(`${catalogStatus}\n`);
process.stderr.write(
'::error::Generated action catalog is stale. Run `pnpm --filter @snowluma/mcp gen` and commit packages/mcp/src/generated/.\n',
);
process.exit(1);
+65
View File
@@ -0,0 +1,65 @@
import fs from 'node:fs';
import path from 'node:path';
const root = path.resolve(process.argv[2] ?? 'dist');
const entryPoint = path.join(root, 'index.mjs');
if (!fs.statSync(root, { throwIfNoEntry: false })?.isDirectory()) {
throw new Error(`release directory does not exist: ${root}`);
}
if (!fs.statSync(entryPoint, { throwIfNoEntry: false })?.isFile()) {
throw new Error(`release entry point must be at the archive root: ${entryPoint}`);
}
const release = JSON.parse(fs.readFileSync(path.join(root, 'release.json'), 'utf8'));
const runtimePackage = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
if (release.version !== runtimePackage.version) {
throw new Error('release version does not match its runtime package');
}
const actualEntries = fs.readdirSync(root).filter(name => !['node', 'node.exe'].includes(name));
if (JSON.stringify([...release.entries].sort()) !== JSON.stringify(actualEntries.sort())) {
throw new Error('release file manifest does not match the packaged files');
}
const sqliteRoot = path.join(root, 'native', 'sqlite');
const sqlite = JSON.parse(fs.readFileSync(path.join(sqliteRoot, 'manifest.json'), 'utf8'));
if (release.target !== sqlite.target) {
throw new Error('release target does not match its native artifacts');
}
if (sqlite.package !== '@appthreat/sqlite3' || sqlite.version !== '9.1.0' || sqlite.nativeInterface !== 1
|| !/^(win32|linux|darwin)-(x64|arm64)$/.test(sqlite.target)) {
throw new Error('release has an invalid SQLite native manifest');
}
const expectedSqliteFiles = (sqlite.target.startsWith('linux-')
? ['@appthreat+sqlite3.glibc.node', '@appthreat+sqlite3.musl.node']
: ['@appthreat+sqlite3.node']).map(file => `prebuilds/${sqlite.target}/${file}`);
if (JSON.stringify(sqlite.files) !== JSON.stringify(expectedSqliteFiles)) {
throw new Error('release SQLite manifest does not contain the required platform/libc variants');
}
for (const file of [...expectedSqliteFiles, 'LICENSE.sqlite3']) {
if (!fs.statSync(path.join(sqliteRoot, file), { throwIfNoEntry: false })?.isFile()) {
throw new Error(`release is missing SQLite runtime input: ${file}`);
}
}
const forbidden = [];
const pending = [root];
while (pending.length > 0) {
const directory = pending.pop();
if (!directory) continue;
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const absolute = path.join(directory, entry.name);
if (/snowluma/i.test(entry.name)) {
forbidden.push(path.relative(root, absolute));
}
pending.push(absolute);
}
}
if (forbidden.length > 0) {
throw new Error(`release contains forbidden directory names:\n${forbidden.join('\n')}`);
}
console.log(`Release layout verified: ${root}`);
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env node
// Local replica of `.github/workflows/ci.yml`.
// Every command and step title below must stay identical to that workflow.
// CI jobs run these in parallel after a frozen install; this script runs the
// same commands once each, in that job's internal order, and still executes
// later steps if an earlier one fails — same as four GitHub jobs reporting
// independently.
import { spawnSync } from 'node:child_process';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const checks = [
['Install dependencies', 'pnpm install --frozen-lockfile'],
['Run all package tests', 'pnpm test'],
['Verify generated action catalog is up to date', 'node tools/check-action-catalog.mjs'],
['Type check all packages', 'pnpm typecheck'],
['Build release inputs', 'pnpm run build:all'],
['Lint managed source files', 'pnpm lint'],
];
const failed = [];
for (const [title, command] of checks) {
console.log(`\n▸ ${title}\n$ ${command}\n`);
const result = spawnSync(command, {
cwd: repoRoot,
shell: true,
stdio: 'inherit',
env: process.env,
});
const code = result.status === 0 ? 0 : (result.status ?? 1);
if (code !== 0) {
failed.push({ title, command, code });
console.error(`\n✗ ${title} failed (exit ${code})`);
} else {
console.log(`\n✓ ${title}`);
}
}
if (failed.length > 0) {
console.error('\nCI suite failed:');
for (const item of failed) {
console.error(` - ${item.title}: \`${item.command}\` (exit ${item.code})`);
}
process.exit(1);
}
console.log('\nCI suite passed (same commands as .github/workflows/ci.yml).');
+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/env node
import { spawnSync } from 'node:child_process';
import { existsSync, readdirSync, readFileSync, rmSync } from 'node:fs';
import { createRequire } from 'node:module';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const packagesRoot = path.join(repoRoot, 'packages');
const coverageRoot = path.join(repoRoot, 'coverage');
function resolveVitestCli(manifestPath) {
const require = createRequire(manifestPath);
let directory = path.dirname(require.resolve('vitest'));
while (true) {
const packagePath = path.join(directory, 'package.json');
if (existsSync(packagePath)) {
const manifest = JSON.parse(readFileSync(packagePath, 'utf8'));
if (manifest.name === 'vitest') {
const bin = typeof manifest.bin === 'string' ? manifest.bin : manifest.bin?.vitest;
if (typeof bin !== 'string') throw new Error('Vitest does not declare a CLI binary');
return path.resolve(directory, bin);
}
}
const parent = path.dirname(directory);
if (parent === directory) throw new Error(`Could not resolve Vitest CLI from ${manifestPath}`);
directory = parent;
}
}
const workspaces = readdirSync(packagesRoot, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => {
const directory = path.join(packagesRoot, entry.name);
const manifestPath = path.join(directory, 'package.json');
if (!existsSync(manifestPath)) return null;
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
if (typeof manifest.scripts?.test !== 'string') return null;
if (typeof manifest.devDependencies?.vitest !== 'string') {
throw new Error(`${manifest.name ?? entry.name} declares tests without a package-local Vitest dependency`);
}
return {
directory,
reportDirectory: path.join(coverageRoot, entry.name),
name: manifest.name ?? entry.name,
vitestCli: resolveVitestCli(manifestPath),
};
})
.filter(Boolean);
if (workspaces.length === 0) {
throw new Error('No regular Vitest workspaces found');
}
rmSync(coverageRoot, { recursive: true, force: true });
for (const workspace of workspaces) {
console.log(`\n[coverage] ${workspace.name}`);
const result = spawnSync(
process.execPath,
[
workspace.vitestCli,
'run',
'--exclude=tests/native/**',
'--coverage',
'--coverage.provider=v8',
`--coverage.reportsDirectory=${workspace.reportDirectory}`,
'--coverage.reporter=text-summary',
'--coverage.reporter=json-summary',
'--coverage.reporter=html',
'--coverage.include=src/**/*.{js,mjs,cjs,ts,tsx}',
'--coverage.exclude=src/**/generated/**',
'--coverage.exclude=src/**/fixtures/**',
'--coverage.exclude=src/**/*.d.ts',
'--coverage.exclude=src/**/native/**',
'--coverage.exclude=dist/**',
'--coverage.exclude=**/native/**',
],
{
cwd: workspace.directory,
stdio: 'inherit',
},
);
if (result.error) throw result.error;
if (result.status !== 0) process.exit(result.status ?? 1);
}
console.log(`\n[coverage] Reports written to ${path.relative(repoRoot, coverageRoot)}/`);
+401
View File
@@ -0,0 +1,401 @@
#!/usr/bin/env node
// One-shot release driver.
//
// pnpm release <version> # full pipeline: dev → Promote → main → tag
// pnpm release <version> --no-wait # bump+push dev only, do main+tag manually
// pnpm release <version> --dry-run # print plan, change nothing
//
// What it does, in order:
// 1. Sanity-check: on `dev`, clean tree. Must not be behind origin/dev.
// Already being this version's `chore(release):` commit is a resume
// (after a dropped push, a failed fetch-while-waiting, or a failed
// tag push). Re-run `pnpm release <version>` as many times as needed.
// 2. Full CI suite (`node tools/ci-check.mjs`) — the same commands as
// `.github/workflows/ci.yml`. Skipped on resume. Fail here instead
// of after the bump.
// 3. `pnpm bump <version>` (writes every package.json).
// 4. Commit `chore(release): vX.Y.Z` — that prefix triggers the
// Promote workflow, which points `main` at the dev tip. Skipped
// when HEAD is already that commit.
// 5. Push to origin/dev (retries transient HTTPS / TLS failures).
// 6. (Unless `--no-wait`) poll until `origin/main` equals the dev
// tip we just pushed, then fetch and fast-forward `main` to
// `origin/main`, create `vX.Y.Z` tag, push it — that triggers
// `release.yml`. Fetch / tag push also retry.
// 7. Switch back to `dev` so the next `git log` view is back where
// you were.
//
// Re-run the same `pnpm release <version>` after any mid-pipeline
// failure (push, wait, tag). Do not amend the release commit.
import { execSync, spawnSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { parseVersion } from '../packages/common/src/semver.ts';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(__dirname, '..');
// ───────────── arg parsing ─────────────
const args = process.argv.slice(2);
const flags = new Set(args.filter(a => a.startsWith('--')));
const positional = args.filter(a => !a.startsWith('--'));
const version = positional[0];
const dryRun = flags.has('--dry-run');
const noWait = flags.has('--no-wait');
if (!version) {
console.error('Usage: pnpm release <version> [--no-wait] [--dry-run]');
console.error('Example: pnpm release 1.8.0');
process.exit(1);
}
if (!parseVersion(version)) {
console.error(`Invalid semver: "${version}"`);
process.exit(1);
}
const tag = `v${version}`;
const releaseCommit = `chore(release): ${tag}`;
// ───────────── shell helpers ─────────────
function sh(cmd, { capture = false, allowFail = false } = {}) {
if (dryRun) {
console.log(`[dry-run] $ ${cmd}`);
return '';
}
if (capture) {
return execSync(cmd, { cwd: repoRoot, encoding: 'utf-8' }).trim();
}
const result = spawnSync(cmd, { cwd: repoRoot, shell: true, stdio: 'inherit' });
if (result.status !== 0 && !allowFail) {
throw new Error(`Command failed: ${cmd}`);
}
return '';
}
function sleepSync(ms) {
execSync(`sleep ${Math.max(1, Math.ceil(ms / 1000))}`);
}
function gitOk(cmd) {
if (dryRun) {
console.log(`[dry-run] $ ${cmd}`);
return true;
}
const result = spawnSync(cmd, { cwd: repoRoot, shell: true, stdio: 'inherit' });
return result.status === 0;
}
/** Retry git fetch / push. TLS drops after a successful commit used to abort
* the whole pipeline; re-run `pnpm release <version>` to resume. */
function shRetry(cmd, { attempts = 8 } = {}) {
if (dryRun) {
console.log(`[dry-run] $ ${cmd}`);
return '';
}
let delayMs = 2000;
for (let i = 1; i <= attempts; i++) {
if (i > 1) info(`retry ${i}/${attempts}: ${cmd}`);
if (gitOk(cmd)) return '';
if (i === attempts) {
throw new Error(
`Command failed after ${attempts} attempts: ${cmd}\n` +
`Re-run: pnpm release ${version}`,
);
}
warn(`failed, retrying in ${delayMs / 1000}s`);
sleepSync(delayMs);
delayMs = Math.min(delayMs * 2, 30_000);
}
return '';
}
function headSubject() {
return shCapture('git log -1 --format=%s');
}
function packageVersion() {
return JSON.parse(readFileSync(path.join(repoRoot, 'package.json'), 'utf-8')).version;
}
function headIsThisRelease() {
return headSubject() === releaseCommit && packageVersion() === version;
}
/** The bump commit for this version, even if later tooling commits sit on top. */
function thisReleaseSha() {
try {
const sha = shCapture(`git log -1 --format=%H --grep=${JSON.stringify(`^${releaseCommit}$`)}`);
return sha || null;
} catch {
return null;
}
}
function shCapture(cmd) {
return execSync(cmd, { cwd: repoRoot, encoding: 'utf-8' }).trim();
}
function which(bin) {
try {
execSync(`command -v ${bin}`, { stdio: 'ignore' });
return true;
} catch { return false; }
}
function ok(msg) { console.log(`\x1b[32m✓\x1b[0m ${msg}`); }
function info(msg) { console.log(`\x1b[36mi\x1b[0m ${msg}`); }
function warn(msg) { console.log(`\x1b[33m!\x1b[0m ${msg}`); }
function printManualFinish() {
info(`Re-run the same command to resume: pnpm release ${version}`);
}
function packageVersionAt(sha) {
return JSON.parse(shCapture(`git show ${sha}:package.json`)).version;
}
// ───────────── step 1: preflight ─────────────
function preflightFail(msg) {
if (dryRun) { warn(`[dry-run] would have aborted: ${msg}`); return; }
console.error(msg);
process.exit(1);
}
function preflight() {
const branch = shCapture('git rev-parse --abbrev-ref HEAD');
if (branch !== 'dev') {
preflightFail(`Must be on \`dev\` to start a release. Current: \`${branch}\``);
}
const dirty = shCapture('git status --porcelain');
if (dirty) {
preflightFail(`Working tree is not clean. Commit or stash first.\n${dirty}`);
}
// Make sure we're not behind origin/dev so the bump commit doesn't
// collide with someone else's push. Already being this release commit
// (in sync, or ahead by the unpushed bump) is a resume.
shRetry('git fetch origin refs/heads/dev:refs/remotes/origin/dev');
const local = shCapture('git rev-parse dev');
const remote = shCapture('git rev-parse origin/dev');
if (local !== remote) {
const ahead = shCapture('git rev-list --count origin/dev..dev');
const behind = shCapture('git rev-list --count dev..origin/dev');
if (behind !== '0') {
preflightFail(`dev is behind origin/dev (behind=${behind}). Pull first.`);
}
if (headIsThisRelease()) {
warn(`dev is ahead of origin/dev by ${ahead}; HEAD is ${releaseCommit}, will resume`);
} else {
preflightFail(`dev is not in sync with origin/dev (ahead=${ahead}, behind=${behind}). Pull / push first.`);
}
}
if (!dryRun) ok(`on dev, clean, ready to release ${tag}`);
}
function runCiSuite() {
if (!dryRun && headIsThisRelease()) {
warn(`HEAD is already ${releaseCommit}; skipping CI suite (resume)`);
return;
}
info('running the full CI suite (same commands as .github/workflows/ci.yml)');
sh('node tools/ci-check.mjs');
if (!dryRun) ok('CI suite passed');
}
// ───────────── step 2-4: bump + commit + push dev ─────────────
function bumpAndPushDev() {
if (!dryRun && headIsThisRelease()) {
warn(`HEAD is already ${releaseCommit}; skipping bump and commit`);
} else {
sh(`node tools/bump-version.mjs ${version}`);
ok(`bumped all package.json files to ${version}`);
if (dryRun) {
info(`would commit "${releaseCommit}" and push origin dev`);
return;
}
const changed = shCapture('git status --porcelain');
if (!changed) {
warn(`no version changes — package.json already at ${version}, skipping commit`);
} else {
sh('git add package.json packages/*/package.json');
sh(`git commit -m "${releaseCommit}"`);
ok(`committed ${releaseCommit}`);
}
}
const local = shCapture('git rev-parse dev');
const remote = shCapture('git rev-parse origin/dev');
if (!dryRun && local === remote) {
info('origin/dev already has this tip; skipping push');
return;
}
shRetry('git push origin dev');
ok(`pushed to origin/dev — Promote workflow should kick off shortly`);
}
// ───────────── step 5: wait for main to match dev, then tag ─────────────
function promoteRunFailed(expectedSha) {
if (!which('gh')) return null;
try {
const list = shCapture('gh run list --workflow=promote-dev-to-main.yml --limit 8 --json conclusion,headSha,url,status');
const runs = JSON.parse(list);
return runs.find((run) => run.headSha === expectedSha && run.conclusion === 'failure') ?? null;
} catch (error) {
warn(`gh run list failed: ${error.message}`);
return null;
}
}
function localTagSha() {
const result = spawnSync('git', ['rev-parse', '-q', '--verify', `${tag}^{commit}`], {
cwd: repoRoot,
encoding: 'utf-8',
});
if (result.status !== 0) return null;
return (result.stdout || '').trim();
}
function remoteTagSha() {
try {
const out = shCapture(`git ls-remote --tags origin refs/tags/${tag} refs/tags/${tag}^{}`);
if (!out) return null;
const lines = out.split('\n').filter(Boolean);
const peeled = lines.find((line) => line.includes('^{}'));
return (peeled || lines[0]).split(/\s+/)[0];
} catch {
return null;
}
}
function originMainSha() {
try {
return shCapture('git rev-parse refs/remotes/origin/main');
} catch {
return null;
}
}
function ensureTag(expectedSha) {
const local = localTagSha();
if (local && local !== expectedSha) {
throw new Error(
`Local tag ${tag} points at ${local.slice(0, 12)}, expected ${expectedSha.slice(0, 12)}. ` +
`Delete it first if you meant to retarget: git tag -d ${tag}`,
);
}
if (!local) sh(`git tag ${tag} ${expectedSha}`);
else info(`tag ${tag} already points at ${expectedSha.slice(0, 12)}`);
const remote = remoteTagSha();
if (remote === expectedSha) {
info(`origin already has ${tag} at ${expectedSha.slice(0, 12)}`);
return;
}
if (remote && remote !== expectedSha) {
throw new Error(
`origin ${tag} points at ${remote.slice(0, 12)}, expected ${expectedSha.slice(0, 12)}. ` +
`Not moving a live tag.`,
);
}
shRetry(`git push origin ${tag}`);
ok(`tagged and pushed ${tag} — release workflow will pick it up`);
}
async function waitUntilMainIs(expectedSha) {
info(`waiting until origin/main equals dev @ ${expectedSha.slice(0, 12)}...`);
info('press Ctrl-C if you want to stop; re-run the same command to resume.');
const startedAt = Date.now();
const TIMEOUT_MS = 30 * 60 * 1000;
const POLL_MS = 15 * 1000;
while (Date.now() - startedAt < TIMEOUT_MS) {
if (gitOk('git fetch origin refs/heads/main:refs/remotes/origin/main')) {
const mainSha = originMainSha();
if (mainSha === expectedSha) {
ok(`origin/main is dev @ ${expectedSha.slice(0, 12)}`);
return true;
}
} else {
warn('fetch origin/main failed; will keep waiting (re-run this command if you abort)');
}
const failed = promoteRunFailed(expectedSha);
if (failed) {
warn(`a Promote run failed for ${expectedSha.slice(0, 12)}: ${failed.url}`);
warn('still waiting in case a later Promote succeeds');
}
await new Promise((resolve) => setTimeout(resolve, POLL_MS));
}
warn('30-minute timeout reached without origin/main matching dev.');
printManualFinish();
return false;
}
async function waitAndTag() {
const expectedSha = dryRun
? 'dry-run'
: (thisReleaseSha() || shCapture('git rev-parse dev'));
if (dryRun) {
info(`would wait until origin/main equals dev @ ${expectedSha.slice(0, 12)}`);
info(`would tag ${tag} and push origin ${tag}`);
return;
}
const alreadyTagged = remoteTagSha() === expectedSha || localTagSha() === expectedSha;
if (alreadyTagged && remoteTagSha() === expectedSha) {
ok(`origin already has ${tag} at ${expectedSha.slice(0, 12)}; nothing to do`);
return;
}
gitOk('git fetch origin refs/heads/main:refs/remotes/origin/main');
if (originMainSha() !== expectedSha) {
const ready = await waitUntilMainIs(expectedSha);
if (!ready) return;
} else {
ok(`origin/main already equals the release commit @ ${expectedSha.slice(0, 12)}`);
}
const taggedVersion = packageVersionAt(expectedSha);
if (taggedVersion !== version) {
warn(`release commit package.json is ${taggedVersion} but you asked for ${version}.`);
warn('Tagging anyway, but the release artifact name may not match.');
}
ensureTag(expectedSha);
}
// ───────────── go ─────────────
(async () => {
console.log(`Release pipeline → ${tag}${dryRun ? ' (dry-run)' : ''}\n`);
preflight();
runCiSuite();
bumpAndPushDev();
if (noWait) {
info('--no-wait set; not waiting for origin/main to match dev.');
printManualFinish();
return;
}
await waitAndTag();
})().catch(err => {
console.error(err.stack || err.message);
process.exit(1);
});
+101
View File
@@ -0,0 +1,101 @@
#!/usr/bin/env python3
import argparse
import asyncio
import json
import sys
import uuid
from typing import Any, Dict
try:
import websockets
except ImportError:
print("Missing dependency: websockets")
print("Install with: pip install websockets")
sys.exit(2)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="OneBot WS API smoke test")
parser.add_argument("--url", default="ws://127.0.0.1:3001/", help="OneBot WebSocket URL")
parser.add_argument("--token", default="", help="Bearer token if OneBot accessToken is set")
parser.add_argument("--group-id", type=int, default=1081372778, help="Target group_id for send_group_msg")
parser.add_argument("--message", default="hello from snowluma from refactor", help="Message text to send")
parser.add_argument("--timeout", type=float, default=20.0, help="Per-request timeout seconds")
return parser.parse_args()
async def recv_json(ws: Any, timeout: float) -> Dict[str, Any]:
raw = await asyncio.wait_for(ws.recv(), timeout)
if isinstance(raw, bytes):
raw = raw.decode("utf-8", errors="replace")
return json.loads(raw)
async def call_api(ws: Any, action: str, params: Dict[str, Any], timeout: float) -> Dict[str, Any]:
echo = f"py-{action}-{uuid.uuid4().hex[:8]}"
payload = {
"action": action,
"params": params,
"echo": echo,
}
await ws.send(json.dumps(payload, ensure_ascii=False))
while True:
resp = await recv_json(ws, timeout)
if resp.get("echo") == echo:
return resp
def short_json(data: Dict[str, Any], max_len: int = 240) -> str:
text = json.dumps(data, ensure_ascii=False)
if len(text) <= max_len:
return text
return text[:max_len] + "..."
async def main() -> int:
args = parse_args()
headers = {}
if args.token:
headers["Authorization"] = f"Bearer {args.token}"
try:
async with websockets.connect(args.url, additional_headers=headers, max_size=20 * 1024 * 1024) as ws:
try:
first = await recv_json(ws, 1.0)
print("[WS] first event:", short_json(first))
except Exception:
pass
status = await call_api(ws, "get_status", {}, args.timeout)
print("[API] get_status:", short_json(status))
groups = await call_api(ws, "get_group_list", {}, args.timeout)
group_count = len(groups.get("data", [])) if isinstance(groups.get("data"), list) else -1
print(f"[API] get_group_list: group_count={group_count}")
send_resp = await call_api(
ws,
"send_group_msg",
{
"group_id": args.group_id,
"message": args.message,
},
args.timeout,
)
print("[API] send_group_msg:", short_json(send_resp))
ok = (
status.get("status") == "ok"
and groups.get("status") == "ok"
and send_resp.get("status") == "ok"
)
return 0 if ok else 1
except Exception as e:
print("[ERROR]", str(e))
return 1
if __name__ == "__main__":
raise SystemExit(asyncio.run(main()))