Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,9 @@ registry、模板和技能。原 `pre-push` 与测试流水线已经删除,当
文件名是安装脚本及升级器的兼容协议,保持原样。Unix 归档内的程序名为 `one`,
Windows 为 `one.exe`;每个归档包含 README 和 `third_party/mise/LICENSE`。
自动验证全部归档的校验值及 Go 构建信息,并运行本机平台的 `--version`、
中英文帮助、语言切换和模板列表。语言检查使用临时 XDG 目录,保护开发者设置。
中英文帮助、语言切换和模板列表。本机程序通过真实升级 worker 确认正式发布通道,
并用模拟 npx 记录工作区创建时生成的技能安装命令,确认技能来源提交。冒烟检查
在仓库外的临时目录执行,使用临时 XDG 目录,保护开发者设置。

### 发布及失败恢复

Expand Down Expand Up @@ -162,7 +164,9 @@ above and `checksums.txt` containing their SHA256 hashes. Archive names are the
installer/updater compatibility contract. Every archive contains `one` (`one.exe`
on Windows), README, and the mise license. Check all hashes and Go build settings;
run the host binary's version, both help languages, locale switching, and template
list with temporary XDG directories.
list. Check the release channel through the real upgrade worker and record the
skill-install command with a simulated npx to verify its source commit. Smoke
checks use a temporary directory outside the workspace and temporary XDG directories.

### Publishing and recovery

Expand Down
24 changes: 24 additions & 0 deletions scripts/release-rules.mts
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,27 @@ export function parseChecksums(text: string): Map<string, string> {
assertAssets(["checksums.txt", ...checksums.keys()]);
return checksums;
}

export function assertBuildSettings(info: string, sha: string, os: string, architecture: string) {
const settings = new Map(
info.split("\n").flatMap((line) => {
const match = /^\s*build\s+([^=]+)=(.*)$/.exec(line);
return match ? [[match[1], match[2]] as [string, string]] : [];
}),
);
// Go intentionally omits -ldflags from build info when -trimpath is used.
for (const [key, value] of Object.entries({
"-trimpath": "true",
CGO_ENABLED: "0",
GOOS: os,
GOARCH: architecture,
"vcs.revision": sha,
"vcs.modified": "false",
})) {
if (settings.get(key) !== value)
fail(
`Unexpected build setting ${key}: expected ${value}.`,
`构建设置 ${key} 不正确,应为 ${value}。`,
);
}
}
19 changes: 19 additions & 0 deletions scripts/release-rules.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import test from "node:test";
import {
assets,
assertAssets,
assertBuildSettings,
bumpTag,
compareTags,
decidePlan,
Expand Down Expand Up @@ -97,3 +98,21 @@ test("asset set and checksum manifest reject duplicates, extras, traversal and o
assert.throws(() => parseChecksums(`${checksums}\n${checksums.split("\n")[0]}`), /duplicate/);
assert.throws(() => parseChecksums(checksums.split("\n").slice(1).join("\n")), /Unexpected/);
});

test("trimpath builds verify source/platform metadata without expecting linker flags", () => {
const info =
"\tbuild\t-trimpath=true\n\tbuild\tCGO_ENABLED=0\n\tbuild\tGOOS=darwin\n\tbuild\tGOARCH=arm64\n\tbuild\tvcs.revision=source-sha\n\tbuild\tvcs.modified=false\n";
assertBuildSettings(info, "source-sha", "darwin", "arm64");
assert.throws(() => assertBuildSettings(info, "other-sha", "darwin", "arm64"), /vcs.revision/);
assert.throws(() => assertBuildSettings(info, "source-sha", "linux", "arm64"), /GOOS/);
assert.throws(
() =>
assertBuildSettings(
info.replace("CGO_ENABLED=0", "CGO_ENABLED=1"),
"source-sha",
"darwin",
"arm64",
),
/CGO_ENABLED/,
);
});
86 changes: 65 additions & 21 deletions scripts/release.mts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ import {
unlinkSync,
writeFileSync,
} from "node:fs";
import { arch, platform } from "node:os";
import { arch, platform, tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import {
assets,
assertAssets,
assertBuildSettings,
compareTags,
decidePlan,
fail,
Expand Down Expand Up @@ -181,9 +182,9 @@ function verifyArchives(build: Build) {
}
}

function smoke(build: Build) {
export function smoke(build: Build) {
const p = build.plan;
const verify = mkdtempSync(join(dirname(build.source), "verify-"));
const verify = mkdtempSync(join(tmpdir(), "one-cli-release-smoke-"));
const hostOS = platform() === "darwin" ? "darwin" : platform() === "linux" ? "linux" : "";
const hostArch = arch() === "x64" ? "amd64" : arch() === "arm64" ? "arm64" : "";
if (!hostOS || !hostArch)
Expand All @@ -210,18 +211,8 @@ function smoke(build: Build) {
);
}
const info = read("mise", ["exec", "--", "go", "version", "-m", exe], { cwd: build.source });
for (const value of [
`main.version=${p.version}`,
"updatecheck.buildChannel=release",
`skills.bundledSourceRef=${p.sourceSha}`,
"CGO_ENABLED=0",
]) {
if (!info.includes(value))
fail(
`Archive ${name} lacks build setting ${value}.`,
`归档 ${name} 缺少构建设置 ${value}。`,
);
}
const target = /^one-cli_(darwin|linux|windows)_(amd64|arm64)\./.exec(name)!;
assertBuildSettings(info, p.sourceSha, target[1], target[2]);
if (name === `one-cli_${hostOS}_${hostArch}.tar.gz`) binary = exe;
}
const config = join(verify, "config");
Expand All @@ -235,15 +226,68 @@ function smoke(build: Build) {
};
if (read(binary, ["--version"], { cwd: verify, env }) !== p.version)
fail("Packaged CLI version is incorrect.", "发布包中的 CLI 版本不正确。");
for (const [locale, helpText] of [
["zh-CN", "创建工作区"],
["en-US", "Create a workspace"],
]) {
read(binary, ["locale", locale, "-o", "json"], { cwd: verify, env });
if (!read(binary, ["--help"], { cwd: verify, env }).includes(helpText))
for (const locale of ["zh-CN", "en-US"]) {
const switched = JSON.parse(
read(binary, ["locale", locale, "-o", "json"], { cwd: verify, env }),
);
const dictionary = JSON.parse(
readFileSync(
join(build.source, "packages/cli/internal/platform/i18n/locales", `${locale}.json`),
"utf8",
),
);
if (
switched.resolved !== locale ||
read(binary, ["--help"], { cwd: verify, env }) !== dictionary["root.help"].trim()
)
fail(`CLI help did not switch to ${locale}.`, `CLI 帮助未切换到 ${locale}。`);
JSON.parse(read(binary, ["templates", "-o", "json"], { cwd: verify, env }));
}
// Validate the release channel through the real manual-upgrade worker.
// The worker and any replacement stay inside this temporary extraction.
const upgrade = JSON.parse(read(binary, ["upgrade", "-o", "json"], { cwd: verify, env }));
if (
upgrade.schema !== "one-cli/upgrade/v1" ||
upgrade.current_version !== p.version ||
upgrade.status !== "current"
) {
fail(
"The packaged CLI did not report a current release-channel installation.",
"发布包中的 CLI 未确认当前正式发布通道版本。",
);
}
// Record the external skills command instead of installing third-party skills.
// This checks the source SHA actually injected into the host executable.
const bin = join(verify, "bin");
mkdirSync(bin);
const calls = join(verify, "skills-calls.jsonl");
writeFileSync(
join(bin, "npx"),
`#!/usr/bin/env node
import { appendFileSync, mkdirSync, writeFileSync } from "node:fs";
const args = process.argv.slice(2);
appendFileSync(${JSON.stringify(calls)}, JSON.stringify(args) + "\\n");
for (const name of args.slice(args.indexOf("--skill") + 1, args.indexOf("--agent"))) {
mkdirSync(".agents/skills/" + name, { recursive: true });
writeFileSync(".agents/skills/" + name + "/SKILL.md", "---\\nname: " + name + "\\ndescription: Release smoke fixture\\n---\\n");
}
`,
{ mode: 0o755 },
);
read(binary, ["create", join(verify, "workspace"), "--yes", "-o", "json"], {
cwd: verify,
env: { ...env, PATH: `${bin}:${process.env.PATH ?? ""}` },
});
const source = `1cli-team/one-cli/packages/agent-skills#${p.sourceSha}`;
const commands = readFileSync(calls, "utf8")
.trim()
.split("\n")
.map((line) => JSON.parse(line));
if (!commands.some((args) => args.includes(source)))
fail(
"The packaged CLI did not use its exact skill-source commit.",
"发布包中的 CLI 未使用精确的技能来源提交。",
);
say(
"All five archives, build metadata, versions, and bilingual CLI smoke checks passed.",
"五个平台的归档、构建信息、版本及中英文 CLI 冒烟检查均通过。",
Expand Down
Loading