云服务推送
This commit is contained in:
65
server/tools/ci-deploy.ps1
Normal file
65
server/tools/ci-deploy.ps1
Normal file
@@ -0,0 +1,65 @@
|
||||
<#
|
||||
Simple local CI helper (PowerShell):
|
||||
- 打包 zip
|
||||
- 可选上传到上传接口
|
||||
- 可选触发部署 API
|
||||
- 可选调用云函数做一次 smoke test
|
||||
|
||||
Usage:
|
||||
# 只打包
|
||||
.\ci-deploy.ps1 -Pack
|
||||
|
||||
# 打包并上传(提供 uploadUrl/uploadToken)
|
||||
.\ci-deploy.ps1 -Pack -UploadUrl 'https://your-upload' -UploadToken 'token'
|
||||
|
||||
# 上传后触发部署 API
|
||||
.\ci-deploy.ps1 -Pack -UploadUrl 'https://your-upload' -UploadToken 'token' -DeployApi 'https://your-deploy-api' -DeployToken 'token'
|
||||
|
||||
#>
|
||||
param(
|
||||
[switch]$Pack,
|
||||
[string]$UploadUrl,
|
||||
[string]$UploadToken,
|
||||
[string]$DeployApi,
|
||||
[string]$DeployToken,
|
||||
[string]$FuncInvokeUrl,
|
||||
[string]$PushToken,
|
||||
[string]$TestCid
|
||||
)
|
||||
|
||||
Push-Location (Split-Path -Parent $MyInvocation.MyCommand.Definition)
|
||||
if ($Pack) {
|
||||
Write-Host "Packing cloud function..."
|
||||
npm install archiver node-fetch form-data | Out-Null
|
||||
node .\deploy-cloudfunc.js
|
||||
}
|
||||
|
||||
if ($UploadUrl) {
|
||||
Write-Host "Uploading to $UploadUrl"
|
||||
$zip = Join-Path ..\dist testUnipush2.zip
|
||||
if (!(Test-Path $zip)) { Write-Error "zip not found: $zip"; exit 2 }
|
||||
$headers = @{}
|
||||
if ($UploadToken) { $headers.Add('Authorization', "Bearer $UploadToken") }
|
||||
$form = @{ file = Get-Item $zip }
|
||||
# Use curl.exe for simple multipart upload
|
||||
$curlArgs = @('-sS','-X','POST',$UploadUrl)
|
||||
if ($UploadToken) { $curlArgs += @('-H',"Authorization: Bearer $UploadToken") }
|
||||
$curlArgs += @('-F',"file=@$zip")
|
||||
& curl.exe @curlArgs
|
||||
}
|
||||
|
||||
if ($DeployApi) {
|
||||
Write-Host "Triggering deploy API: $DeployApi"
|
||||
$body = @{ uploadUrl = $UploadUrl } | ConvertTo-Json
|
||||
$headers = @()
|
||||
if ($DeployToken) { $headers += @('-H',"Authorization: Bearer $DeployToken") }
|
||||
& curl.exe -sS -X POST $DeployApi -H 'Content-Type: application/json' $headers -d $body
|
||||
}
|
||||
|
||||
if ($FuncInvokeUrl) {
|
||||
Write-Host "Invoking function: $FuncInvokeUrl"
|
||||
$payload = @{ token = $PushToken; push_clientid = $TestCid; title='CI Test'; content='hello from CI'; payload = @{} } | ConvertTo-Json
|
||||
& curl.exe -sS -X POST $FuncInvokeUrl -H 'Content-Type: application/json' -d $payload
|
||||
}
|
||||
|
||||
Pop-Location
|
||||
81
server/tools/deploy-cloudfunc-service.js
Normal file
81
server/tools/deploy-cloudfunc-service.js
Normal file
@@ -0,0 +1,81 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const archiver = require('archiver');
|
||||
const fetch = require('node-fetch');
|
||||
const FormData = require('form-data');
|
||||
|
||||
function packDirToZip(srcDir, outPath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const output = fs.createWriteStream(outPath);
|
||||
const archive = archiver('zip', { zlib: { level: 9 } });
|
||||
output.on('close', () => resolve({ bytes: archive.pointer(), path: outPath }));
|
||||
archive.on('error', err => reject(err));
|
||||
archive.pipe(output);
|
||||
archive.directory(srcDir, false);
|
||||
archive.finalize();
|
||||
});
|
||||
}
|
||||
|
||||
async function uploadZip(url, token, zipPath, extraFields = {}) {
|
||||
const form = new FormData();
|
||||
form.append('file', fs.createReadStream(zipPath));
|
||||
for (const k of Object.keys(extraFields)) form.append(k, extraFields[k]);
|
||||
const headers = token ? { Authorization: `Bearer ${token}` } : {};
|
||||
const resp = await fetch(url, { method: 'POST', headers: Object.assign(headers, form.getHeaders()), body: form });
|
||||
const text = await resp.text();
|
||||
let json;
|
||||
try { json = JSON.parse(text); } catch (e) { json = { statusText: text }; }
|
||||
return { ok: resp.ok, status: resp.status, body: json };
|
||||
}
|
||||
|
||||
async function triggerDeployApi(deployApi, deployToken, payload = {}) {
|
||||
const headers = { 'Content-Type': 'application/json' };
|
||||
if (deployToken) headers.Authorization = `Bearer ${deployToken}`;
|
||||
const resp = await fetch(deployApi, { method: 'POST', headers, body: JSON.stringify(payload) });
|
||||
const json = await resp.json().catch(() => null);
|
||||
return { ok: resp.ok, status: resp.status, body: json };
|
||||
}
|
||||
|
||||
async function invokeFunction(funcUrl, pushToken, testCid, title = 'CI Test', content = 'hello from backend') {
|
||||
const body = { token: pushToken, push_clientid: testCid, title, content, payload: {} };
|
||||
const resp = await fetch(funcUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
||||
const json = await resp.json().catch(() => null);
|
||||
return { ok: resp.ok, status: resp.status, body: json };
|
||||
}
|
||||
|
||||
async function deployCloudFunction(options = {}) {
|
||||
const funcDir = options.funcDir || path.join(__dirname, '..', '..', 'uniCloud-alipay', 'cloudfunctions', 'testUnipush2');
|
||||
const outDir = path.join(__dirname, '..', 'dist');
|
||||
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
|
||||
const zipPath = path.join(outDir, path.basename(funcDir) + '.zip');
|
||||
|
||||
const result = { packed: null, uploaded: null, deployed: null, invoked: null };
|
||||
|
||||
// pack
|
||||
const packRes = await packDirToZip(funcDir, zipPath);
|
||||
result.packed = packRes;
|
||||
|
||||
// upload
|
||||
if (!options.uploadUrl) return result;
|
||||
const extra = {};
|
||||
if (options.uniAppId) extra.appId = options.uniAppId;
|
||||
const up = await uploadZip(options.uploadUrl, options.uploadToken, zipPath, extra);
|
||||
result.uploaded = up;
|
||||
|
||||
// trigger deploy API (optional)
|
||||
if (options.deployApi) {
|
||||
const payload = Object.assign({}, options.deployPayload || {}, { uploadUrl: options.uploadUrl, uploadResponse: up.body });
|
||||
const dp = await triggerDeployApi(options.deployApi, options.deployToken, payload);
|
||||
result.deployed = dp;
|
||||
}
|
||||
|
||||
// invoke function for smoke test (optional)
|
||||
if (options.funcInvokeUrl && options.pushToken && options.testCid) {
|
||||
const iv = await invokeFunction(options.funcInvokeUrl, options.pushToken, options.testCid, options.testTitle, options.testContent);
|
||||
result.invoked = iv;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = { deployCloudFunction, packDirToZip, uploadZip, triggerDeployApi, invokeFunction };
|
||||
233
server/tools/deploy-cloudfunc.js
Normal file
233
server/tools/deploy-cloudfunc.js
Normal file
@@ -0,0 +1,233 @@
|
||||
#!/usr/bin/env node
|
||||
// 打包并可选上传云函数到指定上传接口的工具脚本(通用模版)
|
||||
// 依赖: archiver, node-fetch, form-data
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
async function ensureDeps() {
|
||||
try {
|
||||
require.resolve('archiver');
|
||||
require.resolve('node-fetch');
|
||||
require.resolve('form-data');
|
||||
} catch (e) {
|
||||
console.error('\n依赖缺失:请先在仓库根或 server 目录安装依赖:');
|
||||
console.error(' cd server');
|
||||
console.error(' npm install archiver node-fetch form-data');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const archiver = require('archiver');
|
||||
const fetch = require('node-fetch');
|
||||
const FormData = require('form-data');
|
||||
const { spawnSync } = require('child_process');
|
||||
|
||||
function packDirToZip(srcDir, outPath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const output = fs.createWriteStream(outPath);
|
||||
const archive = archiver('zip', { zlib: { level: 9 } });
|
||||
output.on('close', () => resolve({ bytes: archive.pointer(), path: outPath }));
|
||||
archive.on('error', err => reject(err));
|
||||
archive.pipe(output);
|
||||
archive.directory(srcDir, false);
|
||||
archive.finalize();
|
||||
});
|
||||
}
|
||||
|
||||
async function uploadZip(url, token, zipPath, extraFields = {}) {
|
||||
const form = new FormData();
|
||||
form.append('file', fs.createReadStream(zipPath));
|
||||
for (const k of Object.keys(extraFields)) form.append(k, extraFields[k]);
|
||||
|
||||
const headers = token ? { Authorization: `Bearer ${token}` } : {};
|
||||
const resp = await fetch(url, { method: 'POST', headers: Object.assign(headers, form.getHeaders()), body: form });
|
||||
const text = await resp.text();
|
||||
let json;
|
||||
try { json = JSON.parse(text); } catch (e) { json = { statusText: text }; }
|
||||
return { ok: resp.ok, status: resp.status, body: json };
|
||||
}
|
||||
|
||||
function runUnicloudUpload(spaceId, funcDir, funcName) {
|
||||
try {
|
||||
// 尝试使用全局 unicloud
|
||||
let args = ['upload', '-p', spaceId, '-f', funcName, funcDir];
|
||||
console.log('尝试使用全局 `unicloud` CLI:', ['unicloud', ...args].join(' '));
|
||||
let r = spawnSync('unicloud', args, { stdio: 'inherit' });
|
||||
if (r && r.status === 0) return { ok: true };
|
||||
|
||||
// 否则尝试使用 npx 调用包内命令(不要求全局安装)
|
||||
args = ['@dcloudio/unicloud-cli', 'upload', '-p', spaceId, '-f', funcName, funcDir];
|
||||
console.log('尝试使用 npx 调用 unicloud-cli:', ['npx', ...args].join(' '));
|
||||
r = spawnSync('npx', args, { stdio: 'inherit' });
|
||||
if (r && r.status === 0) return { ok: true };
|
||||
|
||||
return { ok: false, error: 'unicloud CLI 上传失败,返回码: ' + (r && r.status) };
|
||||
} catch (e) {
|
||||
return { ok: false, error: String(e) };
|
||||
}
|
||||
}
|
||||
|
||||
async function callDeployApi(deployApi, deployToken, uploadResult) {
|
||||
try {
|
||||
const headers = { 'Content-Type': 'application/json' };
|
||||
if (deployToken) headers.Authorization = `Bearer ${deployToken}`;
|
||||
const body = Object.assign({ uploadResult }, {});
|
||||
const resp = await fetch(deployApi, { method: 'POST', headers, body: JSON.stringify(body) });
|
||||
const txt = await resp.text();
|
||||
let json;
|
||||
try { json = JSON.parse(txt); } catch (e) { json = { statusText: txt }; }
|
||||
return { ok: resp.ok, status: resp.status, body: json };
|
||||
} catch (e) {
|
||||
return { ok: false, error: String(e) };
|
||||
}
|
||||
}
|
||||
|
||||
async function invokeCloudFunction(funcUrl, pushToken, testCid) {
|
||||
try {
|
||||
const body = { token: pushToken || process.env.PUSH_TOKEN || null };
|
||||
if (testCid) body.push_clientid = testCid;
|
||||
body.title = 'smoke-test';
|
||||
body.content = 'smoke-test';
|
||||
const resp = await fetch(funcUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
||||
const txt = await resp.text();
|
||||
let json;
|
||||
try { json = JSON.parse(txt); } catch (e) { json = { statusText: txt }; }
|
||||
return { ok: resp.ok, status: resp.status, body: json };
|
||||
} catch (e) {
|
||||
return { ok: false, error: String(e) };
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await ensureDeps();
|
||||
|
||||
const argv = process.argv.slice(2);
|
||||
const args = {};
|
||||
// 明确哪些参数需要值;若缺失则报错并退出,避免像 --upload 没给 URL 的情况
|
||||
const expectsValue = new Set(['upload','deployApi','deployToken','invokeUrl','funcInvokeUrl','pushToken','testCid','dir','spaceId','name']);
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const a = argv[i];
|
||||
if (!a.startsWith('--')) continue;
|
||||
const key = a.replace('--','');
|
||||
const next = argv[i+1] && !argv[i+1].startsWith('--') ? argv[i+1] : undefined;
|
||||
if (expectsValue.has(key)) {
|
||||
if (!next) {
|
||||
console.error(`参数错误:--${key} 需要一个值(例如 --${key} <value>)。`);
|
||||
process.exit(1);
|
||||
}
|
||||
args[key] = next;
|
||||
i++; // skip value
|
||||
} else {
|
||||
args[key] = true;
|
||||
}
|
||||
}
|
||||
|
||||
const funcDir = args.dir || process.env.CLOUD_FUNCTION_DIR || path.join(__dirname, '..', '..', 'uniCloud-alipay', 'cloudfunctions', 'testUnipush2');
|
||||
const outDir = path.join(__dirname, '..', 'dist');
|
||||
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
|
||||
const zipPath = path.join(outDir, path.basename(funcDir) + '.zip');
|
||||
|
||||
console.log('打包目录:', funcDir);
|
||||
try {
|
||||
const r = await packDirToZip(funcDir, zipPath);
|
||||
console.log('打包完成 ->', zipPath, `(${r.bytes} bytes)`);
|
||||
} catch (e) {
|
||||
console.error('打包失败', e);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
let uploadUrl = args.upload || process.env.CLOUD_UPLOAD_URL;
|
||||
const uploadToken = process.env.CLOUD_UPLOAD_TOKEN || process.env.CLOUD_TOKEN;
|
||||
// 如果没有提供 upload URL,但提供了 UniCloud 的 spaceId,则默认使用官方上传 API
|
||||
const spaceId = args.spaceId || process.env.CLOUD_UNICLOUD_SPACEID || process.env.SPACE_ID || null;
|
||||
if (!uploadUrl && spaceId) {
|
||||
uploadUrl = process.env.CLOUD_UPLOAD_URL || 'https://unicloud.dcloud.net.cn/api/uni-cloud/function/upload';
|
||||
console.log('未指定 --upload,检测到 spaceId,使用 UniCloud 上传 API:', uploadUrl);
|
||||
}
|
||||
if (!uploadUrl) {
|
||||
console.log('\n未配置上传地址 (CLOUD_UPLOAD_URL),只生成 zip 文件。');
|
||||
console.log('若要上传,请设置环境变量 CLOUD_UPLOAD_URL 与 CLOUD_UPLOAD_TOKEN,或使用 --upload <URL> 参数,或提供 --spaceId 使用 unicloud 上传 API。');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log('上传到:', uploadUrl);
|
||||
try {
|
||||
const extra = {};
|
||||
if (process.env.UNI_PUSH_APPID) extra.appId = process.env.UNI_PUSH_APPID;
|
||||
// 若使用 UniCloud 官方上传接口,需要传入 spaceId 与 name
|
||||
if (spaceId) extra.spaceId = spaceId;
|
||||
const funcName = args.name || path.basename(funcDir);
|
||||
if (funcName) extra.name = funcName;
|
||||
// 本地文件校验
|
||||
if (!fs.existsSync(zipPath)) {
|
||||
console.error('打包文件不存在:', zipPath);
|
||||
process.exit(7);
|
||||
}
|
||||
|
||||
let res = await uploadZip(uploadUrl, uploadToken, zipPath, extra);
|
||||
console.log('上传响应:', res.status, JSON.stringify(res.body));
|
||||
|
||||
// 若 HTTP 上传被拒绝(例如 405)或不被接受,且提供了 spaceId,则尝试使用 unicloud CLI 上传回退
|
||||
if ((!res.ok || res.status === 405) && spaceId) {
|
||||
console.log('HTTP 上传失败或被拒绝,尝试使用 unicloud CLI 上传回退...');
|
||||
const funcName = args.name || path.basename(funcDir);
|
||||
const cli = runUnicloudUpload(spaceId, funcDir, funcName);
|
||||
if (cli.ok) {
|
||||
console.log('unicloud CLI 上传成功(回退)。');
|
||||
// 将 published 设为 true,后续进行 smoke-test
|
||||
res = { ok: true, status: 0, body: { source: 'unicloud-cli' } };
|
||||
} else {
|
||||
console.error('unicloud CLI 上传也失败:', cli.error);
|
||||
process.exit(8);
|
||||
}
|
||||
}
|
||||
if (!res.ok) process.exit(3);
|
||||
|
||||
// 常见错误检测:如果把包 POST 到了函数的 invoke URL,云函数通常直接返回业务层错误
|
||||
// e.g. {"errCode":400,"errMsg":"push_clientid required"}
|
||||
try {
|
||||
const probe = JSON.stringify(res.body || {});
|
||||
if (/push_clientid|required/i.test(probe) && /errCode|errMsg|error/i.test(probe)) {
|
||||
console.error('\n检测到上传目标看起来像云函数的调用(invoke)URL,而不是上传/发布 API。');
|
||||
console.error('请使用提供商的上传/发布接口(不是函数的 /test 或类似 invoke 路径)。');
|
||||
console.error('建议:在 HBuilderX 或 控制台 的 Network 面板中捕获 “上传/发布” 请求,或联系平台文档获取上传 API。');
|
||||
console.error('当前响应(供参考):', JSON.stringify(res.body));
|
||||
process.exit(6);
|
||||
}
|
||||
} catch (e) { /* ignore probe errors */ }
|
||||
|
||||
// 简单判断上传响应是否已经标记为已发布/上线(根据不同厂商响应结构调整)
|
||||
let published = false;
|
||||
try {
|
||||
const b = res.body || {};
|
||||
if (b.errCode === 0 || b.errMsg === 'success' || /success|online|published/i.test(JSON.stringify(b))) published = true;
|
||||
} catch (e) { /* ignore */ }
|
||||
|
||||
// 如果未明确发布且提供了 deploy API,则调用之以触发发布
|
||||
const deployApi = args.deployApi || process.env.CLOUD_DEPLOY_API || null;
|
||||
const deployToken = args.deployToken || process.env.CLOUD_DEPLOY_TOKEN || null;
|
||||
let deployResult = null;
|
||||
if (!published && deployApi) {
|
||||
console.log('调用部署 API:', deployApi);
|
||||
deployResult = await callDeployApi(deployApi, deployToken, res.body);
|
||||
console.log('部署 API 响应:', deployResult && (deployResult.body || deployResult.error));
|
||||
if (deployResult && deployResult.ok) published = true;
|
||||
}
|
||||
|
||||
// 可选:若指定了 funcInvokeUrl 或 CLOUD_FUNC_URL,则在上传/发布后做一次 smoke-test 调用
|
||||
const funcInvokeUrl = args.invokeUrl || args.funcInvokeUrl || process.env.CLOUD_FUNC_URL || null;
|
||||
const pushToken = args.pushToken || process.env.PUSH_TOKEN || null;
|
||||
const testCid = args.testCid || process.env.TEST_DEVICE_CID || null;
|
||||
if (funcInvokeUrl) {
|
||||
console.log('调用云函数做 smoke-test:', funcInvokeUrl);
|
||||
const inv = await invokeCloudFunction(funcInvokeUrl, pushToken, testCid);
|
||||
console.log('smoke-test 响应:', inv && (inv.body || inv.error));
|
||||
if (!inv.ok) process.exit(5);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('上传失败', e);
|
||||
process.exit(4);
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) main();
|
||||
Reference in New Issue
Block a user