Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55e43e3451 |
+19
-6
@@ -34,12 +34,18 @@ if [[ "$(git branch --show-current)" != "main" ]] || ! git diff --quiet || ! git
|
||||
exit 1
|
||||
fi
|
||||
release_commit="$(git rev-parse HEAD)"
|
||||
release_created_at="$(git show -s --format=%cI "${release_commit}")"
|
||||
remote_main="$(git ls-remote origin refs/heads/main | cut -f1)"
|
||||
if [[ -z "${remote_main}" || "${remote_main}" != "${release_commit}" ]]; then
|
||||
echo "当前 main 提交尚未同步到 Gitea origin/main,拒绝创建发布标签。" >&2
|
||||
exit 1
|
||||
fi
|
||||
release_tag_commit="$(git ls-remote origin "refs/tags/${release_tag}" "refs/tags/${release_tag}^{}" | tail -n1 | cut -f1)"
|
||||
release_created_at_commit="${release_tag_commit:-${release_commit}}"
|
||||
if ! git cat-file -e "${release_created_at_commit}^{commit}" 2>/dev/null; then
|
||||
echo "现有发布标签指向本地不可验证的提交,拒绝重新生成同版本发布物。" >&2
|
||||
exit 1
|
||||
fi
|
||||
release_created_at="$(git show -s --format=%cI "${release_created_at_commit}")"
|
||||
|
||||
if [[ ! -f "${project_root}/.runtime/dingtalk-mcp.env" ]]; then
|
||||
echo "缺少 .runtime/dingtalk-mcp.env;首次部署前需先配置网关企业凭证。" >&2
|
||||
@@ -74,18 +80,25 @@ npm run test-bundle:build -- \
|
||||
(cd "${release_dir}" && sha256sum -c SHA256SUMS)
|
||||
npm run release:verify -- --release-dir release
|
||||
|
||||
echo "[3/8] 发布或复用 Gitea 国内备用下载源"
|
||||
echo "[3/8] 发布或复用 Gitea 国内首选下载源"
|
||||
gitea_releases_json="$(mktemp)"
|
||||
trap 'rm -f "${gitea_releases_json}"' EXIT
|
||||
tea releases list --remote origin --limit 100 --output json >"${gitea_releases_json}"
|
||||
if ! node -e 'const fs=require("fs");const tag=process.argv[1];const rows=JSON.parse(fs.readFileSync(0,"utf8"));process.exit(rows.some((row)=>row.tag_name===tag||row.tag===tag||row.Tag===tag)?0:1)' \
|
||||
"${release_tag}" <"${gitea_releases_json}"; then
|
||||
if node scripts/release-tag-exists.mjs "${release_tag}" <"${gitea_releases_json}"; then
|
||||
:
|
||||
else
|
||||
release_lookup_status=$?
|
||||
if [[ "${release_lookup_status}" -ne 1 ]]; then
|
||||
echo "无法可靠判断 Gitea Release 是否存在,拒绝继续部署。" >&2
|
||||
exit "${release_lookup_status}"
|
||||
fi
|
||||
release_flags=()
|
||||
[[ "${release_version}" == *-* ]] && release_flags+=(--prerelease)
|
||||
tea releases create --remote origin --tag "${release_tag}" --target "${release_commit}" \
|
||||
--title "${release_tag}" --note "Signed DingTalk approval client ${release_version}" "${release_flags[@]}"
|
||||
fi
|
||||
mapfile -t mirror_assets < <(find "${release_dir}" -mindepth 1 -maxdepth 1 -type f -print | sort)
|
||||
mapfile -t mirror_assets < <(find "${release_dir}" -mindepth 1 -maxdepth 1 -type f \
|
||||
! -name 'agent-test-install.md' ! -name 'dingmcp-test-bundle.tar.gz' -print | sort)
|
||||
missing_mirror_assets=()
|
||||
mirror_check_file="$(mktemp)"
|
||||
for asset in "${mirror_assets[@]}"; do
|
||||
@@ -106,7 +119,7 @@ for asset in "${mirror_assets[@]}"; do
|
||||
asset_name="$(basename "${asset}")"
|
||||
curl --fail --silent --show-error --location "${gitea_release_base_url}/${asset_name}" -o "${mirror_check_file}"
|
||||
[[ "$(sha256sum "${mirror_check_file}" | cut -d' ' -f1)" == "$(sha256sum "${asset}" | cut -d' ' -f1)" ]] || {
|
||||
echo "Gitea 备用发布物校验失败:${asset_name}" >&2
|
||||
echo "Gitea 首选发布物校验失败:${asset_name}" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
|
||||
+1
-1
@@ -118,7 +118,7 @@ https://approval.justconn.top/downloads/dingmcp/install.md
|
||||
|
||||
Agent 先校验当前平台引导脚本的固定 SHA-256,再执行该引导。平台引导选择并校验 Node 运行时,同时校验首次安装器和发布公钥;安装器随后验证 `manifest.sig` 和全部 artifact 哈希,将内置运行依赖的客户端以 npm 离线模式安装到稳定用户目录并返回标准 MCP 配置。终端安装过程不会从 npm registry 下载额外代码。Bridge 的稳定绝对路径不会包含 NVM 的具体 Node 版本,配置也不再携带 `--url`;网关地址由受签名保护的安装状态提供。
|
||||
|
||||
正式部署会先把同一批不可变发布文件上传到公开 Gitea Release,并把它写成每个 artifact 的首选下载地址;`approval.justconn.top` 静态目录作为签名清单固定的备用地址。平台引导、首次安装和后续更新都会在网络失败、超时或 404 时自动回退;任何来源一旦返回内容,Ed25519 签名或 SHA-256 不匹配都会立即终止。两个域名仍可能共享基础设施,如需机器级容灾,可再向 `release:build` 追加一个或多个 `--download-mirror-base-url` 指向独立的国内 OSS/COS。
|
||||
正式部署会先把同一批不可变安装文件上传到公开 Gitea Release,并把它写成每个 artifact 的首选下载地址;`approval.justconn.top` 静态目录作为签名清单固定的备用地址。黑盒测试包及其动态提示词只发布到自建静态目录,不参与客户端安装镜像。平台引导、首次安装和后续更新都会在网络失败、超时或 404 时自动回退;任何来源一旦返回内容,Ed25519 签名或 SHA-256 不匹配都会立即终止。同一版本重跑会复用 Release Tag 的提交时间,测试包也使用归一化 tar 元数据,保证部署可安全重入。两个域名仍可能共享基础设施,如需机器级容灾,可再向 `release:build` 追加一个或多个 `--download-mirror-base-url` 指向独立的国内 OSS/COS。
|
||||
|
||||
公开 `install.md` 必须保留从平台检查到双来源验收的完整编号步骤,并分别提供 Windows PowerShell 与 Linux/macOS 的独立路径。三平台都必须支持系统没有 Node 的情况:发布流程固定 Node 精确版本及 Windows/Linux/macOS x64/ARM64 的官方 SHA-256;平台引导依次使用阿里云、npmmirror 和 Node 官方精确版本地址,网络失败才换源,哈希不符立即终止。禁止使用 `latest` 镜像别名、修改系统 PATH 或要求管理员权限。
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
- `npm ci`、`npm test`、`npm audit --omit=dev --audit-level=high` 全部通过。
|
||||
- `npm run runtime:verify` 确认 Node 官方精确版本源和至少一个国内源同时提供全部六个平台制品,且镜像 `SHASUMS256.txt` 与仓库固定哈希一致。
|
||||
- `release:build`、`test-bundle:build` 和 `release:verify` 成功,`SHA256SUMS` 校验通过,发行物不包含 `.secrets`、`.runtime`、数据库、私钥或机器私有路径。
|
||||
- 国内 Gitea 首选源与自建 HTTPS 备用目录的 `manifest.json`、`manifest.sig` 和全部附件逐文件哈希一致;模拟首选源网络失败/404 后能从备用源完成首次安装和更新,模拟首选源返回错误哈希或签名时必须立即停止而不换源。
|
||||
- 国内 Gitea 首选源与自建 HTTPS 备用目录的 `manifest.json`、`manifest.sig` 和全部安装附件逐文件哈希一致;黑盒测试包及动态提示词只要求自建目录可用。连续部署同一 Tag 必须复用既有 Release 和固定 `createdAt`,不得重复创建或因测试包时间戳产生哈希漂移。模拟首选源网络失败/404 后能从备用源完成首次安装和更新,模拟首选源返回错误哈希或签名时必须立即停止而不换源。
|
||||
- Nginx bootstrap 只开放 ACME challenge;正式站点启用 HSTS,mutable 发行文件返回 `no-cache`,客户端 tgz 才返回 immutable。连续两次部署时公开目录始终对应一个完整签名清单,旧版本保留数符合策略。
|
||||
- 从新生成的公开 `install.md` 在干净的用户级目录安装,不使用 `sudo`;安装结果主动返回 `postInstall`,可发现全部 13 个 MCP 工具。
|
||||
- 在未安装 Node 的干净 Windows x64/ARM64、Linux x64/ARM64、macOS Intel/Apple Silicon 用户环境验证平台引导。确认只使用固定精确版本、国内镜像失败会依次回退、任一哈希不符立即终止、私有 Node 不修改 PATH 且更新后仍可启动。至少在实际可用的每类操作系统完成一次端到端安装;其余架构用对应 CI 或发行制品校验覆盖。
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "dingtalk-approval-mcp",
|
||||
"version": "1.1.0-rc.11",
|
||||
"version": "1.1.0-rc.12",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "dingtalk-approval-mcp",
|
||||
"version": "1.1.0-rc.11",
|
||||
"version": "1.1.0-rc.12",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.30.0",
|
||||
"express": "^5.2.1",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dingtalk-approval-mcp",
|
||||
"version": "1.1.0-rc.11",
|
||||
"version": "1.1.0-rc.12",
|
||||
"description": "A lightweight Agent bridge for a unified DingTalk OA and XFT approval gateway.",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -25,7 +25,10 @@ if (gateway.protocol !== "https:" || gateway.pathname !== "/mcp" || gateway.sear
|
||||
}
|
||||
const artifactNames = [...new Set(Object.values(manifest.artifacts ?? {}).map((item) => basename(String(item.url))))];
|
||||
const members = ["SHA256SUMS", "install.md", "manifest.json", "manifest.sig", ...artifactNames];
|
||||
execFileSync("tar", ["-czf", bundle, "-C", releaseDir, ...members]);
|
||||
execFileSync("tar", [
|
||||
"--sort=name", "--mtime=@0", "--owner=0", "--group=0", "--numeric-owner", "--format=ustar",
|
||||
"-czf", bundle, "-C", releaseDir, ...members,
|
||||
]);
|
||||
const digest = createHash("sha256").update(readFileSync(bundle)).digest("hex");
|
||||
let prompt = readFileSync(resolve("distribution/agent-test.template.md"), "utf8");
|
||||
prompt = prompt.replaceAll("{{BUNDLE_URL}}", bundleUrl).replaceAll("{{BUNDLE_SHA256}}", digest);
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const tag = process.argv[2];
|
||||
if (!tag) {
|
||||
console.error("用法:release-tag-exists.mjs <tag>");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
let body = "";
|
||||
for await (const chunk of process.stdin) body += chunk;
|
||||
|
||||
let value;
|
||||
try {
|
||||
value = JSON.parse(body);
|
||||
} catch {
|
||||
console.error("无法解析 Gitea Release 列表 JSON");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const rows = Array.isArray(value)
|
||||
? value
|
||||
: Array.isArray(value?.data)
|
||||
? value.data
|
||||
: Array.isArray(value?.releases)
|
||||
? value.releases
|
||||
: undefined;
|
||||
if (!rows) {
|
||||
console.error("Gitea Release 列表 JSON 结构不受支持");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const found = rows.some((row) => row && typeof row === "object" && Object.entries(row).some(([key, entry]) => {
|
||||
const normalizedKey = key.toLowerCase().replace(/[^a-z]/g, "");
|
||||
return (normalizedKey === "tag" || normalizedKey === "tagname") && entry === tag;
|
||||
}));
|
||||
process.exit(found ? 0 : 1);
|
||||
@@ -86,6 +86,11 @@ test("customer distribution is a generic single-login gateway bridge", () => {
|
||||
|
||||
const deploy = readFileSync("deploy/install.sh", "utf8");
|
||||
assert.match(deploy, /--download-base-url "\$\{gitea_release_base_url\}"[\s\S]*--download-mirror-base-url "\$\{public_download_base_url\}"/s);
|
||||
assert.match(deploy, /refs\/tags\/\$\{release_tag\}[\s\S]*release_created_at_commit="\$\{release_tag_commit:-\$\{release_commit\}\}"/s);
|
||||
assert.match(deploy, /! -name 'agent-test-install\.md' ! -name 'dingmcp-test-bundle\.tar\.gz'/);
|
||||
|
||||
const testBundleBuilder = readFileSync("scripts/build-test-bundle.mjs", "utf8");
|
||||
assert.match(testBundleBuilder, /--sort=name.*--mtime=@0.*--owner=0.*--group=0.*--numeric-owner.*--format=ustar/s);
|
||||
});
|
||||
|
||||
test("public install page explicitly declares UTF-8 for browser rendering", () => {
|
||||
@@ -95,3 +100,18 @@ test("public install page explicitly declares UTF-8 for browser rendering", () =
|
||||
assert.match(installLocation, /charset utf-8;/);
|
||||
assert.match(installLocation, /charset_types text\/markdown;/);
|
||||
});
|
||||
|
||||
test("Gitea release lookup tolerates tea field-name corruption and fails closed", () => {
|
||||
const lookup = (body: string, tag = "v1.1.0-rc.11") => spawnSync(
|
||||
process.execPath,
|
||||
["scripts/release-tag-exists.mjs", tag],
|
||||
{ input: body, encoding: "utf8" },
|
||||
);
|
||||
assert.equal(lookup('[{"tag_name":"v1.1.0-rc.11"}]').status, 0);
|
||||
assert.equal(lookup('[{"tag-_name":"v1.1.0-rc.11"}]').status, 0);
|
||||
assert.equal(lookup('{"data":[{"Tag":"v1.1.0-rc.11"}]}').status, 0);
|
||||
assert.equal(lookup('[{"tag-_name":"v1.1.0-rc.10"}]').status, 1);
|
||||
const invalid = lookup("not json");
|
||||
assert.equal(invalid.status, 2);
|
||||
assert.match(invalid.stderr, /无法解析/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user