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
2 changes: 1 addition & 1 deletion README-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<h1>SubBoost</h1>
<p>
<img src="https://img.shields.io/badge/platform-Linux%20%2B%20Docker-lightgrey.svg" alt="平台:Linux + Docker">
<img src="https://img.shields.io/badge/version-2.8.0-green.svg" alt="版本 2.8.0">
<img src="https://img.shields.io/badge/version-2.8.1-green.svg" alt="版本 2.8.1">
<a href="https://subboost.org"><img src="https://img.shields.io/badge/app-subboost.org-brightgreen.svg" alt="在线入口"></a>
<a href="https://docs.subboost.org"><img src="https://img.shields.io/badge/docs-subboost.org-blue.svg" alt="文档"></a>
<img src="https://img.shields.io/badge/image-GHCR-blue.svg" alt="GHCR 镜像">
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<h1>SubBoost</h1>
<p>
<img src="https://img.shields.io/badge/platform-Linux%20%2B%20Docker-lightgrey.svg" alt="Platform: Linux + Docker">
<img src="https://img.shields.io/badge/version-2.8.0-green.svg" alt="Version 2.8.0">
<img src="https://img.shields.io/badge/version-2.8.1-green.svg" alt="Version 2.8.1">
<a href="https://subboost.org"><img src="https://img.shields.io/badge/app-subboost.org-brightgreen.svg" alt="Online app"></a>
<a href="https://docs.subboost.org"><img src="https://img.shields.io/badge/docs-subboost.org-blue.svg" alt="Documentation"></a>
<img src="https://img.shields.io/badge/image-GHCR-blue.svg" alt="GHCR image">
Expand Down
28 changes: 7 additions & 21 deletions docs/release-notes.md
Original file line number Diff line number Diff line change
@@ -1,35 +1,21 @@
# SubBoost v2.8.0
# SubBoost v2.8.1

## 中文

### 更新重点
### 修复

SubBoost v2.8.0 改进 VLESS、TLS、ECH 与 xHTTP 分享链接兼容性,并提升规则管理、预览和复制操作的一致性。本版本同时修复多项第三方依赖安全公告。

### 主要变化

- 改进 Shadowrocket 等来源的 VLESS TLS/ECH/xHTTP 参数解析,从有效复合 ECH 值中保留查询域名,不写入 Mihomo 不支持的解析器 URI,并继续拒绝格式错误的值。
- 优化规则目标、规则类型徽标、自定义代理组和规则集路径显示;禁用自定义代理组后会同步清理无效的规则顺序引用。
- 增强复制订阅链接的兼容性:现代剪贴板接口不可用时使用安全回退,并在复制失败时给出明确提示。
- 更新受安全公告影响的 `deepmerge-ts`、`nanoid`、`brace-expansion`、`fast-uri` 和 `js-yaml` 依赖。
- 修复自部署实例从旧版本更新时,首次执行 `subboost update` 可能已显示新版本配置、但应用仍运行旧镜像的问题。升级成功后现在会在一次更新中实际切换到新版本镜像。

### 升级说明

- 升级前仍建议备份 `/opt/subboost/.env` 和数据库
- 现有自部署用户可继续运行 `subboost update`。升级器仍会按原流程创建并验证数据库备份;本版本不需要新增环境变量或人工迁移

## English

### Highlights

SubBoost v2.8.0 improves VLESS, TLS, ECH, and xHTTP share-link compatibility and refines rule management, previews, and copy interactions. It also fixes several third-party dependency advisories.

### Main Changes
### Fixes

- Improved parsing of VLESS TLS/ECH/xHTTP parameters from clients such as Shadowrocket, preserving the query name from valid compound ECH values without emitting unsupported resolver URIs, while continuing to reject malformed values.
- Refined rule targets, rule-type badges, custom proxy-group behavior, and rule-set path display. Disabling a custom proxy group now removes invalid rule-order references consistently.
- Improved subscription-link copy compatibility with a safe fallback when the modern Clipboard API is unavailable and clear feedback when copying fails.
- Updated `deepmerge-ts`, `nanoid`, `brace-expansion`, `fast-uri`, and `js-yaml` versions affected by security advisories.
- Fixed an issue where the first `subboost update` from an older self-hosted version could activate new release metadata while the application continued running the old image. A successful update now switches the running application to the new image in a single update.

### Upgrade Notes

- Back up `/opt/subboost/.env` and the database before upgrading.
- Existing self-hosted users can continue to run `subboost update`. The updater still creates and verifies a database backup as before; this release requires no new environment variables or manual migration.
2 changes: 1 addition & 1 deletion local/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@subboost/local",
"version": "2.8.0",
"version": "2.8.1",
"license": "AGPL-3.0-only",
"private": true,
"type": "module",
Expand Down
1 change: 1 addition & 0 deletions local/scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,7 @@ main() {
[ -s "$candidate_manager" ] && bash -n "$candidate_manager" || die "Candidate manager is invalid."

set_env_value SUBBOOST_IMAGE "$image"
set_env_value SUBBOOST_CANDIDATE_IMAGE "$image"
set_env_value SUBBOOST_RELEASE_URL "$SUBBOOST_UPDATE_RELEASE_URL"
set_env_value SUBBOOST_COMPOSE_URL "$compose_url"
set_env_value SUBBOOST_MANAGER_URL "$manager_url"
Expand Down
43 changes: 37 additions & 6 deletions local/scripts/selfhost-shell.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,9 +322,18 @@ ENV
set -Eeuo pipefail
home="$(mktemp -d)"
trap 'rm -rf "$home"' EXIT
mkdir -p "$home/bin"
cat > "$home/.env" <<'ENV'
SUBBOOST_IMAGE=image
release_dir="$home/release"
mkdir -p "$release_dir" "$home/bin"
cat > "$release_dir/release.json" <<'JSON'
{"image":"new-image","composeUrl":"docker-compose.image.yml","managerUrl":"subboost-manager"}
JSON
printf 'services:\n app:\n image: \${SUBBOOST_IMAGE}\n' > "$release_dir/docker-compose.image.yml"
printf '#!/usr/bin/env bash\necho new-manager\n' > "$release_dir/subboost-manager"
printf '#!/usr/bin/env bash\necho old-manager\n' > "$home/bin/subboost"
cat > "$home/.env" <<ENV
SUBBOOST_RELEASE_URL=file://$release_dir/release.json
SUBBOOST_IMAGE=old-image
SUBBOOST_CANDIDATE_IMAGE=old-candidate-image
POSTGRES_DB=subboost
POSTGRES_USER=subboost
POSTGRES_PASSWORD=password
Expand All @@ -348,6 +357,7 @@ ENV
if [ "$1" = "info" ]; then return 0; fi
if [ "$1" = "compose" ]; then
printf '%s\\n' "$*" >> "$docker_calls_file"
printf 'image=%s candidate_image=%s command=%s\\n' "\${SUBBOOST_IMAGE:-}" "\${SUBBOOST_CANDIDATE_IMAGE:-}" "$*" >> "$docker_calls_file"
case "$*" in
"compose version"*) return 0 ;;
*" config --services") printf 'app\\ndb\\ncron\\n'; return 0 ;;
Expand All @@ -371,7 +381,7 @@ ENV
else
update_status=$?
fi
printf 'update_status=%s\\n' "$update_status"
printf 'update_status=%s parent_image=%s parent_candidate_image=%s\\n' "$update_status" "$SUBBOOST_IMAGE" "$SUBBOOST_CANDIDATE_IMAGE"
cat "$docker_calls_file"
[ "$update_status" -eq 1 ]
`;
Expand All @@ -381,8 +391,14 @@ ENV
expect(result.status, `stdout:\n${result.stdout}\nstderr:\n${result.stderr}`).toBe(0);
expect(result.stdout).toContain("Candidate update failed: candidate cron startup failed");
expect(result.stdout).toContain("Previous version restored successfully.");
expect(result.stdout).toMatch(/candidate-compose\.yml.*up -d --no-deps cron/);
expect(result.stdout).toMatch(/old-compose\.yml.*up -d --no-deps cron/);
expect(result.stdout).toContain(
"update_status=1 parent_image=old-image parent_candidate_image=old-candidate-image",
);
expect(result.stdout).toMatch(/image=new-image candidate_image=new-image command=compose.*candidate-compose\.yml up -d --no-deps cron$/m);
expect(result.stdout).toMatch(/image=new-image candidate_image=new-image command=compose.*candidate-compose\.yml stop cron app$/m);
expect(result.stdout).toMatch(/image=old-image candidate_image=old-candidate-image command=compose.*old-compose\.yml up -d db$/m);
expect(result.stdout).toMatch(/image=old-image candidate_image=old-candidate-image command=compose.*old-compose\.yml up -d app$/m);
expect(result.stdout).toMatch(/image=old-image candidate_image=old-candidate-image command=compose.*old-compose\.yml up -d --no-deps cron$/m);
expect(result.stdout).not.toMatch(/old-compose\.yml.*up -d cron(?:\s|$)/);
}, 10_000);

Expand All @@ -401,6 +417,7 @@ JSON
cat > "$home/.env" <<ENV
SUBBOOST_RELEASE_URL=file://$release_dir/release.json
SUBBOOST_IMAGE=old-image
SUBBOOST_CANDIDATE_IMAGE=old-candidate-image
POSTGRES_DB=subboost
POSTGRES_USER=subboost
POSTGRES_PASSWORD=password
Expand All @@ -427,6 +444,11 @@ ENV
printf 'command=%s\n' "$*" >> "$docker_log"
if [ "$1" = "info" ]; then return 0; fi
if [ "$1" = "compose" ]; then
case "$*" in
*"candidate-compose.yml"*)
printf 'candidate_image=%s candidate_release_image=%s command=%s\n' "\${SUBBOOST_IMAGE:-}" "\${SUBBOOST_CANDIDATE_IMAGE:-}" "$*" >> "$docker_log"
;;
esac
case "$*" in
"compose version"*) return 0 ;;
*" config --services") printf 'app\\ndb\\ncron\\n'; return 0 ;;
Expand Down Expand Up @@ -463,7 +485,16 @@ ENV

expect(result.status).toBe(0);
expect(result.stdout).toContain("pull_image=new-image");
expect(result.stdout).not.toContain("candidate_image=old-image");
expect(result.stdout).not.toContain("candidate_release_image=old-candidate-image");
expect(result.stdout).toMatch(/candidate_image=new-image candidate_release_image=new-image command=compose.*candidate-compose\.yml config$/m);
expect(result.stdout).toMatch(/candidate_image=new-image candidate_release_image=new-image command=compose.*candidate-compose\.yml config --services$/m);
expect(result.stdout).toMatch(/candidate_image=new-image candidate_release_image=new-image command=compose.*candidate-compose\.yml pull$/m);
expect(result.stdout).toMatch(/candidate_image=new-image candidate_release_image=new-image command=compose.*candidate-compose\.yml up -d db$/m);
expect(result.stdout).toMatch(/candidate_image=new-image candidate_release_image=new-image command=compose.*candidate-compose\.yml up -d --no-deps app$/m);
expect(result.stdout).toMatch(/candidate_image=new-image candidate_release_image=new-image command=compose.*candidate-compose\.yml up -d --no-deps cron$/m);
expect(result.stdout).toContain("SUBBOOST_IMAGE=new-image");
expect(result.stdout).toContain("SUBBOOST_CANDIDATE_IMAGE=new-image");
expect(result.stdout).toContain("SUBBOOST_COMPOSE_URL=file://");
expect(result.stdout).toContain("SUBBOOST_MANAGER_URL=file://");
const pullIndex = result.stdout.search(/^command=compose.* pull$/m);
Expand Down
21 changes: 14 additions & 7 deletions local/scripts/subboost.sh
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ compose_files() {
(cd "$SUBBOOST_HOME" && docker_cmd compose --project-directory "$SUBBOOST_HOME" --env-file "$env_file" -f "$compose_file" "$@")
}

compose_files_with_image() {
local image="$1"
shift
SUBBOOST_IMAGE="$image" SUBBOOST_CANDIDATE_IMAGE="$image" compose_files "$@"
}

load_env() {
[ -f "$ENV_FILE" ] || die "Missing $ENV_FILE"
set -a
Expand Down Expand Up @@ -423,19 +429,20 @@ update_cmd() {
old_manager_present=1
fi
set_file_env_value "$candidate_env" SUBBOOST_IMAGE "$image"
set_file_env_value "$candidate_env" SUBBOOST_CANDIDATE_IMAGE "$image"
set_file_env_value "$candidate_env" SUBBOOST_RELEASE_URL "$release_url"
[ -n "$compose_url" ] && set_file_env_value "$candidate_env" SUBBOOST_COMPOSE_URL "$compose_url"
[ -n "$manager_url" ] && set_file_env_value "$candidate_env" SUBBOOST_MANAGER_URL "$manager_url"
if ! grep -q '^LOCAL_SETUP_TOKEN=.' "$candidate_env"; then
set_file_env_value "$candidate_env" LOCAL_SETUP_TOKEN "$(random_hex 32)"
fi
compose_files "$candidate_env" "$candidate_compose" config >/dev/null
services="$(compose_files "$candidate_env" "$candidate_compose" config --services)"
compose_files_with_image "$image" "$candidate_env" "$candidate_compose" config >/dev/null
services="$(compose_files_with_image "$image" "$candidate_env" "$candidate_compose" config --services)"
for service in app db cron; do
printf '%s\n' "$services" | grep -Fxq "$service" || die "Candidate Compose is missing service: $service"
done
say "Pulling candidate image before the maintenance window..."
SUBBOOST_IMAGE="$image" compose_files "$candidate_env" "$candidate_compose" pull
compose_files_with_image "$image" "$candidate_env" "$candidate_compose" pull

app_id="$(service_container_id app)"
[ -n "$app_id" ] || die "Cannot identify the current app container for rollback."
Expand Down Expand Up @@ -469,9 +476,9 @@ update_cmd() {
fi

update_error=""
compose_files "$candidate_env" "$candidate_compose" up -d db || update_error="candidate database startup failed"
compose_files_with_image "$image" "$candidate_env" "$candidate_compose" up -d db || update_error="candidate database startup failed"
if [ -z "$update_error" ]; then
compose_files "$candidate_env" "$candidate_compose" up -d --no-deps app || update_error="candidate app startup or migration failed"
compose_files_with_image "$image" "$candidate_env" "$candidate_compose" up -d --no-deps app || update_error="candidate app startup or migration failed"
fi
if [ -z "$update_error" ] && ! wait_for_health; then
update_error="candidate health check failed"
Expand All @@ -486,12 +493,12 @@ update_cmd() {
activate_staged_file "${SUBBOOST_BIN:-/usr/local/bin/subboost}" || update_error="candidate manager activation failed"
fi
if [ -z "$update_error" ]; then
compose_files "$candidate_env" "$candidate_compose" up -d --no-deps cron || update_error="candidate cron startup failed"
compose_files_with_image "$image" "$candidate_env" "$candidate_compose" up -d --no-deps cron || update_error="candidate cron startup failed"
fi

if [ -n "$update_error" ]; then
say "Candidate update failed: $update_error"
compose_files "$candidate_env" "$candidate_compose" stop cron app >/dev/null 2>&1 || true
compose_files_with_image "$image" "$candidate_env" "$candidate_compose" stop cron app >/dev/null 2>&1 || true
docker_cmd tag "$rollback_tag" "$old_image_ref" || true
sudo_do rm -f "${ENV_FILE}.candidate.$$" "${COMPOSE_FILE}.candidate.$$" "${SUBBOOST_BIN:-/usr/local/bin/subboost}.candidate.$$"
restore_error=""
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "subboost",
"version": "2.8.0",
"version": "2.8.1",
"license": "AGPL-3.0-only",
"private": true,
"engines": {
Expand Down
16 changes: 15 additions & 1 deletion scripts/selfhost-release-assets.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -224,14 +224,27 @@ function copyInstallScript(publicRoot, to, args) {
fs.writeFileSync(to, rewriteInstallerDefaults(content, args), "utf8");
}

function writeReleaseCompose(publicRoot, to, image) {
fs.mkdirSync(path.dirname(to), { recursive: true });
const source = path.join(publicRoot, "local/docker-compose.image.yml");
const content = fs.readFileSync(source, "utf8").replace(/\r\n/g, "\n");
const marker = "image: ${SUBBOOST_IMAGE:?set SUBBOOST_IMAGE}";
const count = content.split(marker).length - 1;
if (count !== 1) {
throw new Error(`Expected exactly one SubBoost image marker in docker-compose.image.yml, found ${count}.`);
}
const resolvedImage = `image: ${"${SUBBOOST_CANDIDATE_IMAGE:-"}${image}}`;
fs.writeFileSync(to, content.replace(marker, resolvedImage), "utf8");
}

function createBundle(publicRoot, args) {
const output = path.resolve(publicRoot, args.output);
const manifest = buildManifest(publicRoot, args);
if (args.dryRun) return { manifest, output };

fs.rmSync(output, { force: true, recursive: true });
fs.mkdirSync(output, { recursive: true });
copyFile(publicRoot, "local/docker-compose.image.yml", path.join(output, "docker-compose.image.yml"));
writeReleaseCompose(publicRoot, path.join(output, "docker-compose.image.yml"), manifest.image);
copyInstallScript(publicRoot, path.join(output, "install.sh"), args);
copyFile(publicRoot, "local/scripts/subboost.sh", path.join(output, MANAGER_ASSET_NAME), { normalizeLineEndings: true });
fs.chmodSync(path.join(output, "install.sh"), 0o755);
Expand Down Expand Up @@ -272,4 +285,5 @@ module.exports = {
parseArgs,
rewriteInstallerDefaults,
usage,
writeReleaseCompose,
};
22 changes: 20 additions & 2 deletions test/regression/tooling/selfhost-release-assets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,11 @@ function withTempVersionRepo(run: (root: string) => void): void {
const root = mkdtempSync(join(tmpdir(), "subboost-public-release-assets-"));
try {
writeText(root, "public/package.json", JSON.stringify({ version: "9.8.7" }));
writeText(root, "public/local/docker-compose.image.yml", "services: {}\n");
writeText(
root,
"public/local/docker-compose.image.yml",
"services:\n app:\n image: ${SUBBOOST_IMAGE:?set SUBBOOST_IMAGE}\n",
);
writeText(
root,
"public/local/scripts/install.sh",
Expand Down Expand Up @@ -129,9 +133,19 @@ describe("public selfhost release assets script", () => {
'DEFAULT_COMPOSE_URL="https://github.com/SubBoost/subboost/releases/latest/download/docker-compose.image.yml"'
);
expect(readFileSync(join(bundle.output, "subboost-manager"), "utf8")).not.toContain("\r");
const releaseCompose = readFileSync(join(bundle.output, "docker-compose.image.yml"), "utf8");
expect(releaseCompose).toContain(
"image: ${SUBBOOST_CANDIDATE_IMAGE:-ghcr.io/subboost/subboost@sha256:abc123}",
);
expect(releaseCompose).not.toContain("${SUBBOOST_IMAGE:?set SUBBOOST_IMAGE}");
expectExecutable(join(bundle.output, "install.sh"));
expectExecutable(join(bundle.output, "subboost-manager"));
expect(JSON.parse(readFileSync(join(bundle.output, "release.json"), "utf8"))).toMatchObject(manifest);

writeText(publicRoot, "local/docker-compose.image.yml", "services: {}\n");
expect(() => publicReleaseAssets.createBundle(publicRoot, args)).toThrow(
"Expected exactly one SubBoost image marker in docker-compose.image.yml, found 0.",
);
});
}, 10_000);

Expand Down Expand Up @@ -289,7 +303,11 @@ describe("public selfhost release assets script", () => {
try {
writeText(root, "package.json", JSON.stringify({ version: "1.2.3" }));
writeText(root, "local/Dockerfile", "FROM node:24-alpine\n");
writeText(root, "local/docker-compose.image.yml", "services: {}\n");
writeText(
root,
"local/docker-compose.image.yml",
"services:\n app:\n image: ${SUBBOOST_IMAGE:?set SUBBOOST_IMAGE}\n",
);
writeText(root, "local/scripts/install.sh", "#!/usr/bin/env bash\necho install\n");
writeText(root, "local/scripts/subboost.sh", "#!/usr/bin/env bash\necho subboost\n");

Expand Down
1 change: 1 addition & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export default defineConfig({
"packages/server-core/src/**/*.test.ts",
"packages/ui/src/**/*.test.ts",
"local/**/*.test.ts",
"test/regression/tooling/**/*.test.ts",
],
restoreMocks: true,
},
Expand Down
Loading