fix: derive deployment version without env persistence
This commit is contained in:
1 parent
6dc6839ff9
commit
665354ca49
7 files changed
+77
-42
No files matched your search
@@ -23,11 +23,6 @@ CORS_ORIGIN=https://cloud.example.com
|
||||
# 例如:python3 -c "import secrets; print(secrets.token_hex(32))"
|
||||
DB_PASSWORD=
|
||||
|
||||
# 自动生成:镜像版本、源码版本与当前 Git 提交。
|
||||
IMAGE_VERSION=
|
||||
SOURCE_REVISION=
|
||||
SOURCE_VERSION=
|
||||
|
||||
# 高级覆盖:OAuth 默认分别继承 VITE_API_URL 与 CORS_ORIGIN。
|
||||
# 仅多入口或特殊反代部署需要显式填写。
|
||||
OAUTH_CALLBACK_BASE_URL=
|
||||
|
||||
@@ -68,7 +68,7 @@ cd tg-vault
|
||||
./deploy/install.sh
|
||||
```
|
||||
|
||||
安装向导会先检测 Docker Engine、Docker Compose 插件、Python 3 和 Git;缺少组件时,由你选择自动补全、查看手动提示或退出。环境通过后,依次输入 Web 前端 URL 和后端 API URL,确认后会在同一次运行中生成 `.env`、数据库密码、应用密钥和构建元数据,并执行 `docker compose up -d --build`。
|
||||
安装向导会先检测 Docker Engine、Docker Compose 插件、Python 3 和 Git;缺少组件时,由你选择自动补全、查看手动提示或退出。环境通过后,依次输入 Web 前端 URL 和后端 API URL,确认后会在同一次运行中生成 `.env`、数据库密码和应用密钥;版本号从 `backend/package.json` 读取,并只在本次构建命令中临时注入,不写入 `.env`。升级时只重建并替换前后端,不重建 PostgreSQL。
|
||||
|
||||
- **基础 Web 部署**
|
||||
只需输入 Web 前端 URL 与后端 API URL;地址必须是完整的 `http(s)` origin。
|
||||
@@ -101,8 +101,8 @@ cd tg-vault
|
||||
|
||||
- **`DB_PASSWORD`** — 自动生成 64 位十六进制 PostgreSQL 密码
|
||||
- **`SESSION_SECRET`** / **`STORAGE_CREDENTIALS_SECRET`** — 新安装时自动生成并保留;升级旧部署时不会覆盖 `/data/secrets` 中已有的持久密钥
|
||||
- **`SOURCE_REVISION`** — 自动读取当前 `git rev-parse HEAD`
|
||||
- **`SOURCE_VERSION`** / **`IMAGE_VERSION`** — 优先读取当前 Git tag,否则读取项目版本
|
||||
|
||||
版本号不保存在 `.env`。安装时会直接从 `backend/package.json` 读取应用版本,并从当前 Git 提交读取源码修订号,只把它们临时传给 Docker 构建。
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
- 改进任务轮询串行化、任务快速筛选及按账户清理任务的准确性。
|
||||
- 改进移动端和键盘操作、对话框异步失败反馈、上传失败原因展示及中英文界面一致性。
|
||||
- 修复构建产物数据库 schema 同步,确保源码、初始化脚本与发布镜像一致。
|
||||
- `.env.example` 不再包含 `IMAGE_VERSION`、`SOURCE_VERSION`、`SOURCE_REVISION`。应用版本统一从 `backend/package.json` 读取,Git 修订号在部署时读取,构建元数据只临时传给 Docker,不再持久化到 `.env`。
|
||||
- `deploy/install.sh` 升级旧部署时会清理遗留版本字段,避免旧 `.env` 覆盖新版本;同时只重建和替换前后端,不重建 PostgreSQL。
|
||||
|
||||
### 升级
|
||||
|
||||
|
||||
@@ -20,9 +20,9 @@ function assertBeginnerFriendlyInstall(source: string): void {
|
||||
assert.match(source, /prompt_origin '请输入后端 API URL'/);
|
||||
assert.match(source, /ensure_generated_secret DB_PASSWORD/);
|
||||
assert.match(source, /if \[\[ "\$created_env" == true \]\]; then[\s\S]*ensure_generated_secret SESSION_SECRET[\s\S]*ensure_generated_secret STORAGE_CREDENTIALS_SECRET/);
|
||||
assert.match(source, /SOURCE_REVISION=.*git rev-parse HEAD/);
|
||||
assert.match(source, /SOURCE_VERSION=.*git describe/);
|
||||
assert.match(source, /IMAGE_VERSION=.*SOURCE_VERSION/);
|
||||
assert.match(source, /RELEASE_REVISION=.*git rev-parse HEAD/);
|
||||
assert.match(source, /RELEASE_VERSION=.*python3[\s\S]*backend\/package\.json/);
|
||||
assert.match(source, /env IMAGE_VERSION="\$RELEASE_VERSION"/);
|
||||
}
|
||||
|
||||
test('release images use locked dependencies, pinned bases, verified yt-dlp and source labels', () => {
|
||||
@@ -69,5 +69,16 @@ test('installer keeps beginner input to two public origins and derives the rest'
|
||||
assert.match(deployGuide, /只需填写以下 2 项/);
|
||||
assert.match(deployGuide, /docker inspect/);
|
||||
assert.match(deployGuide, /assets\//);
|
||||
assert.match(deployGuide, /IMAGE_VERSION.*source/);
|
||||
assert.match(deployGuide, /镜像名称会使用 `source`/);
|
||||
});
|
||||
|
||||
test('release metadata is derived per deployment and never persisted in user env files', () => {
|
||||
for (const key of ['IMAGE_VERSION', 'SOURCE_REVISION', 'SOURCE_VERSION']) {
|
||||
assert.doesNotMatch(envExample, new RegExp(`^${key}=`, 'm'));
|
||||
assert.doesNotMatch(installScript, new RegExp(`upsert_env ${key}\\b`));
|
||||
}
|
||||
assert.match(installScript, /remove_env_keys IMAGE_VERSION SOURCE_REVISION SOURCE_VERSION/);
|
||||
assert.match(installScript, /env IMAGE_VERSION="\$RELEASE_VERSION" SOURCE_REVISION="\$RELEASE_REVISION" SOURCE_VERSION="\$RELEASE_VERSION" docker compose config --quiet/);
|
||||
assert.match(installScript, /env IMAGE_VERSION="\$RELEASE_VERSION" SOURCE_REVISION="\$RELEASE_REVISION" SOURCE_VERSION="\$RELEASE_VERSION" docker compose build backend frontend/);
|
||||
assert.match(installScript, /env IMAGE_VERSION="\$RELEASE_VERSION" SOURCE_REVISION="\$RELEASE_REVISION" SOURCE_VERSION="\$RELEASE_VERSION" docker compose up -d --no-build --no-deps backend frontend/);
|
||||
});
|
||||
+8
-10
@@ -11,7 +11,7 @@ TG Vault 当前采用 **Docker Compose + 宿主机 Nginx/面板反向代理**。
|
||||
|
||||
## 2. 创建环境变量
|
||||
|
||||
推荐先运行一次安装脚本,它会创建 `.env`、生成数据库密码和应用密钥,并写入当前源码版本信息:
|
||||
推荐先运行一次安装脚本,它会创建 `.env`、生成数据库密码和应用密钥;当前源码版本只在构建命令中临时注入,不写入 `.env`:
|
||||
|
||||
```bash
|
||||
./deploy/install.sh
|
||||
@@ -32,24 +32,22 @@ CORS_ORIGIN=https://cloud.example.com
|
||||
DB_PASSWORD=随机生成的64位十六进制密码
|
||||
SESSION_SECRET=随机生成的64位十六进制密钥
|
||||
STORAGE_CREDENTIALS_SECRET=随机生成的64位十六进制密钥
|
||||
SOURCE_REVISION=当前部署提交的完整 SHA
|
||||
SOURCE_VERSION=当前 Git tag 或项目版本
|
||||
IMAGE_VERSION=与 SOURCE_VERSION 相同
|
||||
```
|
||||
|
||||
升级已有部署时,如果 `.env` 没有 `SESSION_SECRET` 或 `STORAGE_CREDENTIALS_SECRET`,脚本不会突然写入新值覆盖 `/data/secrets` 中已有的持久密钥,避免现有 2FA 与存储凭证失效。
|
||||
|
||||
OAuth 默认使用 `VITE_API_URL` 作为回调来源,并使用 `CORS_ORIGIN` 的第一个地址作为前端通知来源。只有多入口或特殊反代部署才需显式设置 `OAUTH_CALLBACK_BASE_URL`、`OAUTH_FRONTEND_ORIGIN`。
|
||||
|
||||
如果不使用安装脚本而直接运行 Compose,至少需要手动生成 `DB_PASSWORD`;`IMAGE_VERSION` / `SOURCE_REVISION` / `SOURCE_VERSION` 会分别回退为 `source` / `unknown` / `worktree`。
|
||||
如果不使用安装脚本而直接运行 Compose,至少需要手动生成 `DB_PASSWORD`;镜像名称会使用 `source`,镜像标签中的源码版本元数据会分别回退为 `unknown` / `worktree`。
|
||||
|
||||
## 3. 构建并启动
|
||||
|
||||
直接运行 Compose 时,缺失的 `IMAGE_VERSION` / `SOURCE_REVISION` / `SOURCE_VERSION` 会分别回退为 `source` / `unknown` / `worktree`。使用 `deploy/install.sh` 时,这些构建元数据会从当前 Git 工作树自动刷新。手动发布时也可自行更新:
|
||||
版本号不保存在 `.env`。使用 `deploy/install.sh` 时,应用版本直接读取 `backend/package.json`,源码修订号读取当前 Git 提交,并只在本次 Compose 调用中传入。直接运行 Compose 时,镜像名称和标签元数据会回退为 `source` / `unknown` / `worktree`。手动构建时可临时传入:
|
||||
|
||||
```bash
|
||||
revision=$(git rev-parse HEAD)
|
||||
# 将 .env 中 SOURCE_REVISION 更新为 $revision;IMAGE_VERSION 与 SOURCE_VERSION 必须等于发布版本。
|
||||
version="v$(node -p "require('./backend/package.json').version")"
|
||||
IMAGE_VERSION="$version" SOURCE_REVISION="$revision" SOURCE_VERSION="$version" docker compose up -d --build
|
||||
```
|
||||
|
||||
```bash
|
||||
@@ -58,7 +56,7 @@ docker compose up -d --build
|
||||
docker compose ps
|
||||
```
|
||||
|
||||
数据库 schema 会在后端启动时自动检查/迁移。`postgres`、`backend`、`frontend` 都配置了健康检查;只有 `/readyz` 通过后 backend 才为 healthy。
|
||||
安装脚本先构建 `backend` 和 `frontend`,再通过 `--no-deps` 只替换这两个服务,不会重建 PostgreSQL。数据库 schema 会在后端启动时自动检查/迁移。`postgres`、`backend`、`frontend` 都配置了健康检查;只有 `/readyz` 通过后 backend 才为 healthy。
|
||||
|
||||
## 4. 配置宿主机反向代理
|
||||
|
||||
@@ -77,7 +75,7 @@ docker compose ps
|
||||
git fetch origin
|
||||
git status --short
|
||||
git pull --ff-only origin main
|
||||
docker compose up -d --build
|
||||
./deploy/install.sh
|
||||
```
|
||||
|
||||
如果 `git status --short` 显示本地改动,先人工确认,不要强制覆盖。
|
||||
@@ -91,7 +89,7 @@ curl -fsS http://127.0.0.1:51947/readyz
|
||||
docker compose logs --tail=100 backend frontend postgres
|
||||
|
||||
expected_revision=$(git rev-parse HEAD)
|
||||
expected_version=$(grep '^SOURCE_VERSION=' .env | cut -d= -f2-)
|
||||
expected_version="v$(node -p "require('./backend/package.json').version")"
|
||||
docker inspect --format='{{index .Config.Labels "org.opencontainers.image.revision"}} {{index .Config.Labels "org.opencontainers.image.version"}}' tg-vault-backend tg-vault-frontend
|
||||
# 两个容器都必须输出 "$expected_revision $expected_version"。
|
||||
|
||||
|
||||
+29
-8
@@ -264,6 +264,22 @@ PY
|
||||
fi
|
||||
}
|
||||
|
||||
remove_env_keys() {
|
||||
local temp
|
||||
temp="$(mktemp)"
|
||||
python3 - .env "$temp" "$@" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
source, target, *keys = sys.argv[1:]
|
||||
blocked = set(keys)
|
||||
lines = Path(source).read_text().splitlines() if Path(source).exists() else []
|
||||
kept = [line for line in lines if line.split('=', 1)[0] not in blocked]
|
||||
Path(target).write_text('\n'.join(kept) + ('\n' if kept else ''))
|
||||
PY
|
||||
chmod 600 "$temp"
|
||||
mv "$temp" .env
|
||||
}
|
||||
|
||||
created_env=false
|
||||
CURRENT_CORS_ORIGIN=""
|
||||
CURRENT_VITE_API_URL=""
|
||||
@@ -321,15 +337,20 @@ if [[ "$created_env" == true ]]; then
|
||||
ensure_generated_secret STORAGE_CREDENTIALS_SECRET
|
||||
fi
|
||||
|
||||
SOURCE_REVISION="$(git rev-parse HEAD 2>/dev/null || printf unknown)"
|
||||
SOURCE_VERSION="$(git describe --tags --exact-match 2>/dev/null || node -p "'v' + require('./backend/package.json').version" 2>/dev/null || printf worktree)"
|
||||
IMAGE_VERSION="$SOURCE_VERSION"
|
||||
upsert_env SOURCE_REVISION "$SOURCE_REVISION"
|
||||
upsert_env SOURCE_VERSION "$SOURCE_VERSION"
|
||||
upsert_env IMAGE_VERSION "$IMAGE_VERSION"
|
||||
# v2.2.0 及更早版本曾把构建元数据写入 .env,升级时主动清理,避免旧值覆盖新版本。
|
||||
remove_env_keys IMAGE_VERSION SOURCE_REVISION SOURCE_VERSION
|
||||
|
||||
docker compose config --quiet
|
||||
docker compose up -d --build
|
||||
RELEASE_REVISION="$(git rev-parse HEAD 2>/dev/null || printf unknown)"
|
||||
RELEASE_VERSION="v$(python3 - <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
print(json.loads(Path('backend/package.json').read_text())['version'])
|
||||
PY
|
||||
)"
|
||||
|
||||
env IMAGE_VERSION="$RELEASE_VERSION" SOURCE_REVISION="$RELEASE_REVISION" SOURCE_VERSION="$RELEASE_VERSION" docker compose config --quiet
|
||||
env IMAGE_VERSION="$RELEASE_VERSION" SOURCE_REVISION="$RELEASE_REVISION" SOURCE_VERSION="$RELEASE_VERSION" docker compose build backend frontend
|
||||
env IMAGE_VERSION="$RELEASE_VERSION" SOURCE_REVISION="$RELEASE_REVISION" SOURCE_VERSION="$RELEASE_VERSION" docker compose up -d --no-build --no-deps backend frontend
|
||||
docker compose ps
|
||||
|
||||
echo
|
||||
|
||||
+20
-12
@@ -60,7 +60,8 @@ test_interactive_new_install_collects_urls_and_starts_compose() {
|
||||
assert_contains "$FIXTURE/output.log" '请输入后端 API URL'
|
||||
assert_contains "$FIXTURE/output.log" '按 Enter 保存配置并开始安装'
|
||||
assert_contains "$FIXTURE/docker.log" 'compose config --quiet'
|
||||
assert_contains "$FIXTURE/docker.log" 'compose up -d --build'
|
||||
assert_contains "$FIXTURE/docker.log" 'compose build backend frontend'
|
||||
assert_contains "$FIXTURE/docker.log" 'compose up -d --no-build --no-deps backend frontend'
|
||||
assert_contains "$FIXTURE/docker.log" 'compose ps'
|
||||
}
|
||||
|
||||
@@ -86,7 +87,8 @@ test_non_interactive_uses_environment_without_waiting() {
|
||||
|
||||
assert_contains "$FIXTURE/.env" 'CORS_ORIGIN=https://cloud.example.net'
|
||||
assert_contains "$FIXTURE/.env" 'VITE_API_URL=https://api.example.net'
|
||||
assert_contains "$FIXTURE/docker.log" 'compose up -d --build'
|
||||
assert_contains "$FIXTURE/docker.log" 'compose build backend frontend'
|
||||
assert_contains "$FIXTURE/docker.log" 'compose up -d --no-build --no-deps backend frontend'
|
||||
}
|
||||
|
||||
test_existing_install_keeps_urls_on_enter() {
|
||||
@@ -97,6 +99,9 @@ VITE_API_URL=https://existing-api.example.net
|
||||
DB_PASSWORD=keep-this-password
|
||||
SESSION_SECRET=keep-this-session
|
||||
STORAGE_CREDENTIALS_SECRET=keep-this-storage-secret
|
||||
IMAGE_VERSION=v1.0.0
|
||||
SOURCE_REVISION=stale-revision
|
||||
SOURCE_VERSION=v1.0.0
|
||||
EOF
|
||||
run_in_tty '\n\n\n' "$FIXTURE/output.log"
|
||||
|
||||
@@ -105,6 +110,9 @@ EOF
|
||||
assert_contains "$FIXTURE/.env" 'DB_PASSWORD=keep-this-password'
|
||||
assert_contains "$FIXTURE/.env" 'SESSION_SECRET=keep-this-session'
|
||||
assert_contains "$FIXTURE/.env" 'STORAGE_CREDENTIALS_SECRET=keep-this-storage-secret'
|
||||
if grep -Eq '^(IMAGE_VERSION|SOURCE_REVISION|SOURCE_VERSION)=' "$FIXTURE/.env"; then
|
||||
fail "升级后应清理旧版持久化版本元数据"
|
||||
fi
|
||||
assert_contains "$FIXTURE/output.log" '直接按 Enter 保留当前值'
|
||||
}
|
||||
|
||||
@@ -138,24 +146,24 @@ SH
|
||||
|
||||
test_secret_generation_does_not_require_openssl() {
|
||||
make_fixture
|
||||
hidden="$FIXTURE/hidden-bin"
|
||||
mkdir -p "$hidden"
|
||||
for cmd in python3 git node mktemp chmod mv bash dirname id touch; do
|
||||
target="$(command -v "$cmd")"
|
||||
ln -s "$target" "$hidden/$cmd"
|
||||
done
|
||||
ln -s "$FIXTURE/fake-bin/docker" "$hidden/docker"
|
||||
openssl_stub="$FIXTURE/fake-bin/openssl"
|
||||
cat > "$openssl_stub" <<'SH'
|
||||
#!/usr/bin/env bash
|
||||
echo "openssl 不应被调用" >&2
|
||||
exit 99
|
||||
SH
|
||||
chmod +x "$openssl_stub"
|
||||
(
|
||||
cd "$FIXTURE"
|
||||
env PATH="$hidden" \
|
||||
env PATH="$FIXTURE/fake-bin:$PATH" \
|
||||
INSTALL_TEST_DOCKER_LOG="$FIXTURE/docker.log" \
|
||||
CORS_ORIGIN='https://cloud.example.net' \
|
||||
VITE_API_URL='https://api.example.net' \
|
||||
bash deploy/install.sh --non-interactive > "$FIXTURE/output.log" 2>&1
|
||||
)
|
||||
assert_contains "$FIXTURE/.env" 'DB_PASSWORD='
|
||||
if grep -Fq '未找到 openssl' "$FIXTURE/output.log"; then
|
||||
fail "安装脚本不应再要求 openssl"
|
||||
if grep -Fq 'openssl 不应被调用' "$FIXTURE/output.log"; then
|
||||
fail "安装脚本不应调用 openssl"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user