| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899 |
- #!/usr/bin/env node
- import { execFileSync } from 'node:child_process';
- import { createHash } from 'node:crypto';
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
- import path from 'node:path';
- import { fileURLToPath } from 'node:url';
- import vm from 'node:vm';
- const repo = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
- const sourcePath = 'scripts/deploy-admin-functions.mjs';
- const base = process.argv.find((arg) => arg.startsWith('--base='))?.slice(7);
- const commit = process.argv.includes('--commit');
- const masterKey = process.env.XIAOSHU_MASTER_KEY || '';
- const appId = process.env.XIAOSHU_PARSE_APP_ID || '7pIbDBJmKx_main';
- const parseUrl = (process.env.XIAOSHU_PARSE_URL || 'https://server.xiaoshu.pro/parse').replace(/\/$/, '');
- const sha = (value) => createHash('sha256').update(value).digest('hex');
- if (!base) throw new Error('请指定部署前的源码版本:--base=<commit>');
- if (!masterKey) throw new Error('缺少 XIAOSHU_MASTER_KEY');
- const previousSource = execFileSync('git', ['show', `${base}:${sourcePath}`], { cwd: repo, encoding: 'utf8', maxBuffer: 10_000_000 });
- const nextSource = await readFile(path.join(repo, sourcePath), 'utf8');
- const markers = [
- ['function appointmentProjection(', 'async function allAppointmentRows('],
- ['async function liveAppointmentRows(', 'async function cachedLiveAppointmentRows('],
- ['async function cachedLiveAppointmentRows(', 'function payrollDateIso('],
- ['async function professionalScheduleDataLocal(', 'function addBusinessDays('],
- ["if(operation==='ops/schedule/calendar')", "if(operation==='ops/coach-availability/list')"],
- ];
- function section(source, start, end) {
- const from = source.indexOf(start);
- const to = source.indexOf(end, from + start.length);
- if (from < 0 || to < 0) throw new Error(`找不到排课代码段:${start}`);
- return source.slice(from, to);
- }
- function replaceOnce(source, before, after) {
- const first = source.indexOf(before);
- if (first < 0 || source.indexOf(before, first + before.length) >= 0) throw new Error('线上排课代码与预期不一致,已拒绝覆盖');
- return source.slice(0, first) + after + source.slice(first + before.length);
- }
- const headers = {
- 'X-Parse-Application-Id': appId,
- 'X-Parse-Master-Key': masterKey,
- 'Content-Type': 'application/json',
- };
- const functionName = 'xiaoshu.ops.gateway.v3';
- const where = encodeURIComponent(JSON.stringify({ name: functionName }));
- const queryPath = `/classes/Function?where=${where}&keys=objectId,name,code,updatedAt`;
- async function parse(pathname, init = {}) {
- const response = await fetch(parseUrl + pathname, { ...init, headers });
- const body = await response.json();
- if (!response.ok || body.error) throw new Error(`云函数接口失败:HTTP ${response.status} ${String(body.error || '')}`);
- return body;
- }
- const initial = (await parse(queryPath)).results || [];
- if (initial.length !== 1 || typeof initial[0].code !== 'string') throw new Error('未找到唯一的线上运营网关');
- const original = initial[0];
- let patched = original.code;
- for (const [start, end] of markers) {
- const before = section(previousSource, start, end);
- const after = section(nextSource, start, end);
- if (before === after) throw new Error(`本地排课代码段没有变化:${start}`);
- patched = replaceOnce(patched, before, after);
- }
- const helper = section(nextSource, 'function reconcileScheduleAppointments(', 'async function professionalScheduleDataLocal(');
- if (patched.includes('function reconcileScheduleAppointments(')) throw new Error('线上已有排课对账函数,不能重复部署');
- patched = replaceOnce(patched, 'async function professionalScheduleDataLocal(', helper + 'async function professionalScheduleDataLocal(');
- new vm.Script(patched, { filename: functionName });
- const result = { functionName, objectId: original.objectId, before: sha(original.code), after: sha(patched), sourceUpdatedAt: original.updatedAt, changedSections: markers.length + 1 };
- if (!commit) {
- console.log(JSON.stringify({ ...result, dryRun: true }, null, 2));
- process.exit(0);
- }
- const backupDir = path.join(repo, 'dist', 'schedule-deploy-backup');
- await mkdir(backupDir, { recursive: true });
- const backupPath = path.join(backupDir, `${new Date().toISOString().replace(/[:.]/g, '-')}-${original.objectId}.json`);
- await writeFile(backupPath, JSON.stringify({ ...result, code: original.code }, null, 2));
- const beforeWrite = (await parse(queryPath)).results?.[0];
- if (!beforeWrite || sha(beforeWrite.code || '') !== result.before || beforeWrite.updatedAt !== original.updatedAt) throw new Error('线上网关在部署期间发生变化,已停止写入');
- try {
- await parse(`/classes/Function/${original.objectId}`, { method: 'PUT', body: JSON.stringify({ code: patched }) });
- const afterWrite = (await parse(queryPath)).results?.[0];
- if (!afterWrite || sha(afterWrite.code || '') !== result.after) throw new Error('写入后代码校验失败');
- console.log(JSON.stringify({ ...result, deployed: true, backupPath }, null, 2));
- } catch (error) {
- const current = (await parse(queryPath).catch(() => ({ results: [] }))).results?.[0];
- if (current && sha(current.code || '') === result.after) {
- await parse(`/classes/Function/${original.objectId}`, { method: 'PUT', body: JSON.stringify({ code: original.code }) });
- }
- throw error;
- }
|