From 1e535c83eb4e9beff95d320053508e3ce052f36d Mon Sep 17 00:00:00 2001 From: TheBadRoger Date: Thu, 15 Oct 2026 21:12:36 +0800 Subject: [PATCH] Default dependencies to HTTPS with opt-in SSH fallback --- .gitmodules | 4 +-- README.md | 33 ++++++++------------ TODO.md | 2 +- config/git-local.config | 9 ------ docs/decisions/0002-external-dependencies.md | 4 +-- scripts/deploy.sh | 10 ++---- scripts/init-deps.py | 21 ++++++++----- scripts/test-init-deps.py | 21 ++++++++----- 8 files changed, 49 insertions(+), 55 deletions(-) delete mode 100644 config/git-local.config diff --git a/.gitmodules b/.gitmodules index a605b83..5062799 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ [submodule "external/ChuSQL"] path = external/ChuSQL - url = ChuGit:/srv/git/ChuSQL.git + url = https://github.com/TheBadRoger/ChuSQL.git [submodule "external/ChuSQL-RustDriver"] path = external/ChuSQL-RustDriver - url = ChuGit:/srv/git/ChuSQL-RustDriver.git + url = https://github.com/TheBadRoger/ChuSQL-RustDriver.git diff --git a/README.md b/README.md index 05f2240..d89a8b6 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,6 @@ ChuBlog/ ├── external/ │ ├── ChuSQL/ # Git 子模块,主仓库只记录版本指针 │ └── ChuSQL-RustDriver/ # Git 子模块 -├── config/git-local.config └── scripts/init-deps.py ``` @@ -22,12 +21,18 @@ ChuBlog/ python3 scripts/init-deps.py ``` -子模块登记地址为 `ChuGit:/srv/git/ChuSQL.git` 和 `ChuGit:/srv/git/ChuSQL-RustDriver.git`。初始化脚本按以下顺序获取**固定提交**: +默认依赖地址为 GitHub HTTPS,无需服务器 SSH 公钥或 `ChuGit` 别名: -1. 本机 SSH:`ssh://git@localhost/srv/git/ChuSQL.git`、`ssh://git@localhost/srv/git/ChuSQL-RustDriver.git`。 -2. 本机连接、访问或获取固定提交失败,回退到 `git@github.com:TheBadRoger/ChuSQL.git`、`git@github.com:TheBadRoger/ChuSQL-RustDriver.git`。 +- `https://github.com/TheBadRoger/ChuSQL.git` +- `https://github.com/TheBadRoger/ChuSQL-RustDriver.git` -已正确初始化的固定版本直接复用;有未提交修改时拒绝覆盖。不会因回退而改为任意最新分支。`config/git-local.config` 提供 `ChuGit`/自建服务器地址到 localhost SSH 的可选 `insteadOf` 映射;刻意不改写 GitHub 地址,以免回退又被映射回本机。脚本不启用全局 `file` 协议,也不递归初始化未知子模块。 +已授权设备可显式优先使用自建服务器;SSH 连接、访问或固定提交获取失败时,自动回退到上面的 GitHub HTTPS 地址: + +```sh +python3 scripts/init-deps.py --prefer-ssh +``` + +该选项先尝试 `ChuGit:/srv/git/ChuSQL.git` 和 `ChuGit:/srv/git/ChuSQL-RustDriver.git`,需要设备已配置对应 SSH 身份。已正确初始化的固定版本直接复用,有未提交修改时拒绝覆盖;回退不改为任意最新分支。服务器已有的 Git 地址覆盖由服务器自行管理,项目不设置全局 `insteadOf`、localhost 映射、OS 用户或服务器 SSH 配置。 开发电脑仍可显式选择已有本地来源(仅开发覆盖参数,不是服务器默认流程);只读取已提交版本: @@ -46,25 +51,13 @@ cargo fmt --check cargo clippy --all-targets -- -D warnings ``` -部署基于 Debian 13。项目和依赖工作目录应归 `deploy` 用户所有;为该用户配置可访问 `git@localhost` 与 GitHub 的 SSH 密钥,并预先核对两端主机密钥。私钥留在主机,不能放进镜像。 - -推荐入口(以 root 调 Docker,Git 与编译均以 deploy 运行): +部署基于 Debian 13。`scripts/deploy.sh` 会使用同一个依赖初始化脚本,再构建外部数据库与 Docker 镜像;沿用运行者现有的 Git 配置,不在项目内覆盖服务器环境。 ```sh -sudo /srv/apps/ChuBlog/scripts/deploy.sh +scripts/deploy.sh ``` -脚本先以 `sudo -H -u deploy` 运行 `init-deps.py`,再构建外部项目与 Docker 镜像。默认依赖工作树位于 `/srv/apps/ChuBlog/external/`。其数据库拉取等价于你指定的方式: - -```sh -sudo -u deploy git clone ssh://git@localhost/srv/git/ChuSQL.git /srv/apps/chusql -# 本机来源失败时: -sudo -u deploy git clone git@github.com:TheBadRoger/ChuSQL.git /srv/apps/chusql -``` - -如选择这个独立数据库目录,可用 `prepare-deploy.py --chusql-source /srv/apps/chusql --build-database` 构建;默认部署入口使用子模块固定版本。不要将数据库单独目录的源代码或产物加入博客提交。 - -首次部署先准备 `.env`,再运行入口。也可以手动执行以下同架构构建步骤;远程服务器上的 Git/编译命令应以 `deploy` 身份执行,Docker 命令由 root 执行: +首次部署先准备 `.env`;也可以手动执行以下同架构构建步骤: ```sh # ChuSQL 尚未编译时,先按其项目说明构建 Rust 存储库、server 与 bootstrap。 diff --git a/TODO.md b/TODO.md index b0a1470..8698f63 100644 --- a/TODO.md +++ b/TODO.md @@ -13,7 +13,7 @@ - [x] pre-receive 校验 Git quarantine 内的候选提交:单次一个线性提交、仅 main、禁止删除分支、强推、标签及其他分支。 - [x] Markdown Frontmatter、路径、文件模式、大小配额、图片类型/内容/尺寸与链接安全校验;拒绝原始 HTML、隐藏控制符、符号链接、可执行文件及额外文件。 - [x] 独立 SSH 接收仓库与 API 正式仓库;从 Git 恢复公开快照,草稿不出现在公开 API/资源接口。 -- [x] 外部 `ChuSQL`/`ChuSQL-RustDriver` 子模块配置、本机 SSH 优先拉取与 GitHub 回退;ChuSQL 公开元数据/标签索引及失败自动重建,数据库不承担唯一内容或账户存储。 +- [x] 外部 `ChuSQL`/`ChuSQL-RustDriver` 子模块配置、GitHub HTTPS 默认来源及授权设备 SSH 优先/HTTPS 回退;ChuSQL 公开元数据/标签索引及失败自动重建,数据库不承担唯一内容或账户存储。 - [x] Docker Compose、Caddy 反向代理、独立持久卷、容器资源限制、最小进程身份与网络隔离。 - [x] 后端/驱动自动测试、真实容器 SSH 联调脚本、运行/API/架构说明。 diff --git a/config/git-local.config b/config/git-local.config deleted file mode 100644 index d47e4c9..0000000 --- a/config/git-local.config +++ /dev/null @@ -1,9 +0,0 @@ -# Optional project-local aliases. GitHub URLs deliberately remain untouched for fallback. -[url "ssh://git@localhost/srv/git/ChuSQL.git"] - insteadOf = ChuGit:/srv/git/ChuSQL.git - insteadOf = ssh://git@chunana.cn/srv/git/ChuSQL.git - insteadOf = git@chunana.cn:/srv/git/ChuSQL.git -[url "ssh://git@localhost/srv/git/ChuSQL-RustDriver.git"] - insteadOf = ChuGit:/srv/git/ChuSQL-RustDriver.git - insteadOf = ssh://git@chunana.cn/srv/git/ChuSQL-RustDriver.git - insteadOf = git@chunana.cn:/srv/git/ChuSQL-RustDriver.git diff --git a/docs/decisions/0002-external-dependencies.md b/docs/decisions/0002-external-dependencies.md index 542943d..3ca91ce 100644 --- a/docs/decisions/0002-external-dependencies.md +++ b/docs/decisions/0002-external-dependencies.md @@ -8,8 +8,8 @@ 数据库服务与 Rust 驱动作为两个独立 Git 子模块维护。主仓库只提交远程地址、固定版本指针、初始化/构建配置,不提交依赖源码或编译产物。Cargo 引用初始化后的驱动路径,数据库从外部 checkout 构建并暂存到被忽略的 Docker 构建目录。 -子模块登记来源使用 `ChuGit:/srv/git/ChuSQL.git` 与 `ChuGit:/srv/git/ChuSQL-RustDriver.git`。服务器初始化由 `deploy` 用户执行,优先连接 `ssh://git@localhost/srv/git/…`;本机不可用或不含固定提交时,改用对应 GitHub 仓库。支持空仓库留下的半初始化状态恢复。回退仍保持固定提交,不自动跟随远程最新分支。 +子模块默认使用 `https://github.com/TheBadRoger/ChuSQL.git` 与 `https://github.com/TheBadRoger/ChuSQL-RustDriver.git`,公开依赖不要求读者配置服务器 SSH 密钥。已授权设备可用 `--prefer-ssh` 先访问 `ChuGit:/srv/git/…`,失败后回退 GitHub HTTPS;空仓库留下的半初始化状态也可恢复。回退仍保持固定提交,不自动跟随远程最新分支。 -可选项目级 `insteadOf` 只将自建服务器别名转到 localhost SSH,不重写 GitHub,以保证回退独立。开发者可以显式使用已有文件路径;文件协议只对这种开发覆盖参数的一次命令启用。Docker 构建前通过同一个初始化脚本取依赖,Git 密钥不进入构建上下文。 +服务器上的 Git 地址覆盖、操作系统账户和 SSH 配置由服务器自己管理,项目不配置 localhost 映射或固定 deploy 用户。Docker 构建前使用同一初始化脚本,继承运行者现有 Git 配置。开发者仍可以显式使用已有文件路径,此时文件协议仅对该次初始化命令启用,不设置全局许可。 已有驱动副本从主仓库的历史树中删除。外部项目仍各自保留完整历史;主仓库不能替代依赖仓库分发其对象,源仓库必须先具有固定指针所引用的提交。 diff --git a/scripts/deploy.sh b/scripts/deploy.sh index fb857f1..72ca6e9 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -1,12 +1,8 @@ #!/bin/sh -# Fetch/build external dependencies as deploy, then start Docker Compose as root. +# Prepare configured external dependencies, then build and start Docker Compose. set -eu PROJECT_ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) -if [ "$(id -u)" -ne 0 ]; then - echo 'Run with sudo; Git/compilation run as deploy, Docker runs as root.' >&2 - exit 1 -fi -sudo -H -u deploy python3 "$PROJECT_ROOT/scripts/init-deps.py" -sudo -H -u deploy python3 "$PROJECT_ROOT/scripts/prepare-deploy.py" --build-database +python3 "$PROJECT_ROOT/scripts/init-deps.py" +python3 "$PROJECT_ROOT/scripts/prepare-deploy.py" --build-database docker compose --env-file "$PROJECT_ROOT/.env" -f "$PROJECT_ROOT/deploy/compose.yaml" up -d --build printf '%s\n' 'When the database is ready, run the documented db-init command once.' diff --git a/scripts/init-deps.py b/scripts/init-deps.py index 369cd2f..ea6bca9 100755 --- a/scripts/init-deps.py +++ b/scripts/init-deps.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 -"""Initialize pinned dependencies via localhost SSH, falling back to GitHub.""" +"""Initialize pinned dependencies over GitHub HTTPS; optionally prefer authorized SSH.""" import argparse +import os from pathlib import Path import subprocess import sys @@ -13,7 +14,7 @@ def git(*args, **kwargs): return subprocess.run(['git', *args], cwd=ROOT, **kwargs) -def initialize(name, override): +def initialize(name, override, prefer_ssh=False): path = f'external/{name}' checkout = ROOT / path pin = git('ls-files', '--stage', '--', path, text=True, capture_output=True, check=True).stdout.split() @@ -33,20 +34,25 @@ def initialize(name, override): raise RuntimeError(f'Local repository does not exist: {source}') sources = [source.as_uri()] else: - sources = [f'ssh://git@localhost/srv/git/{name}.git', f'git@github.com:TheBadRoger/{name}.git'] + sources = [f'https://github.com/TheBadRoger/{name}.git'] + if prefer_ssh: + sources.insert(0, f'ChuGit:/srv/git/{name}.git') + transport_env = {**os.environ, 'GIT_TERMINAL_PROMPT': '0'} + configured_ssh = git('config', '--get', 'core.sshCommand', capture_output=True, check=False) + if configured_ssh.returncode != 0 and not any(name in transport_env for name in ['GIT_SSH', 'GIT_SSH_COMMAND']): + transport_env['GIT_SSH_COMMAND'] = 'ssh -o BatchMode=yes -o ConnectTimeout=10 -o ConnectionAttempts=1' for url in sources: print(f'{name}: trying {url}', flush=True) git('config', '--local', f'submodule.{path}.url', url, check=True) options = ['-c', 'protocol.file.allow=always'] if override else [] - options += ['-c', 'core.sshCommand=ssh -o BatchMode=yes -o ConnectTimeout=10 -o ConnectionAttempts=1'] if (checkout / '.git').exists(): git('-C', str(checkout), 'remote', 'set-url', 'origin', url, check=True) - result = git(*options, '-C', str(checkout), 'fetch', '--no-tags', 'origin', pin[1], check=False) + result = git(*options, '-C', str(checkout), 'fetch', '--no-tags', 'origin', pin[1], env=transport_env, check=False) if result.returncode == 0: git('-C', str(checkout), 'checkout', '--detach', pin[1], check=True) return else: - result = git(*options, 'submodule', 'update', '--init', '--checkout', '--', path, check=False) + result = git(*options, 'submodule', 'update', '--init', '--checkout', '--', path, env=transport_env, check=False) if result.returncode == 0: return print(f'{name}: source failed; trying next configured source', file=sys.stderr, flush=True) @@ -57,9 +63,10 @@ def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--database-repo', type=Path, help='explicit local development source for ChuSQL') parser.add_argument('--driver-repo', type=Path, help='explicit local development source for the driver') + parser.add_argument('--prefer-ssh', action='store_true', help='authorized devices: try ChuGit SSH first, then GitHub HTTPS') args = parser.parse_args() for name, override in zip(PROJECTS, [args.database_repo, args.driver_repo]): - initialize(name, override) + initialize(name, override, args.prefer_ssh) if __name__ == '__main__': diff --git a/scripts/test-init-deps.py b/scripts/test-init-deps.py index 796b705..d01a349 100644 --- a/scripts/test-init-deps.py +++ b/scripts/test-init-deps.py @@ -11,7 +11,7 @@ SCRIPT = Path(__file__).with_name('init-deps.py') class InitializeDependencies(unittest.TestCase): - def test_local_rewrite_fetches_only_pinned_dependency_commits(self): + def test_https_default_and_authorized_ssh_fallback_preserve_pins(self): with tempfile.TemporaryDirectory(prefix='chublog deps ') as temporary: root = Path(temporary) project = root / 'blog' @@ -46,7 +46,7 @@ class InitializeDependencies(unittest.TestCase): for name in ['ChuSQL', 'ChuSQL-RustDriver']: self.assertEqual((project / f'external/{name}/source.txt').read_text(), f'{name} committed source') self.assertTrue(all(line.startswith('160000') for line in git(project, 'ls-files', '--stage', 'external').splitlines())) - # A fresh clone must fall back when localhost SSH fails. Redirect only the test's + # A fresh clone must use HTTPS without attempting SSH. Redirect only the test's # GitHub remotes to local fixtures, so this verifies fallback without network access. fresh = root / 'fresh' git(project, '-c', 'user.name=Test', '-c', 'user.email=test@example.invalid', 'commit', '-m', 'Pinned dependencies') @@ -56,12 +56,19 @@ class InitializeDependencies(unittest.TestCase): fallback_env = {**env, 'GIT_ALLOW_PROTOCOL': 'file', 'GIT_CONFIG_COUNT': '2'} for i, (name, source) in enumerate(zip(['ChuSQL', 'ChuSQL-RustDriver'], sources)): fallback_env[f'GIT_CONFIG_KEY_{i}'] = f'url.{Path(source).as_uri()}.insteadOf' - fallback_env[f'GIT_CONFIG_VALUE_{i}'] = f'git@github.com:TheBadRoger/{name}.git' + fallback_env[f'GIT_CONFIG_VALUE_{i}'] = f'https://github.com/TheBadRoger/{name}.git' subprocess.run(['python3', 'scripts/init-deps.py'], cwd=fresh, env=fallback_env, check=True, capture_output=True) for name in ['ChuSQL', 'ChuSQL-RustDriver']: self.assertEqual((fresh / f'external/{name}/source.txt').read_text(), f'{name} committed source') - self.assertEqual(git(fresh / f'external/{name}', 'config', '--get', 'remote.origin.url'), f'git@github.com:TheBadRoger/{name}.git') - # Also cover an accessible but empty localhost bare repository: the initial + self.assertEqual(git(fresh / f'external/{name}', 'config', '--get', 'remote.origin.url'), f'https://github.com/TheBadRoger/{name}.git') + unavailable = root / 'unavailable' + git(root, 'clone', '--no-local', str(project), str(unavailable)) + (unavailable / 'scripts').mkdir() + shutil.copy2(SCRIPT, unavailable / 'scripts/init-deps.py') + subprocess.run(['python3', 'scripts/init-deps.py', '--prefer-ssh'], cwd=unavailable, env=fallback_env, check=True, capture_output=True) + for name in ['ChuSQL', 'ChuSQL-RustDriver']: + self.assertEqual((unavailable / f'external/{name}/source.txt').read_text(), f'{name} committed source') + # Also cover an accessible but empty authorized SSH bare repository: the initial # submodule clone succeeds, fetching the pinned commit fails, then GitHub recovers. empty = root / 'empty.git' git(root, 'init', '--bare', str(empty)) @@ -74,8 +81,8 @@ class InitializeDependencies(unittest.TestCase): empty_source = root / f'empty-{name}.git' git(root, 'clone', '--bare', str(empty), str(empty_source)) partial_env[f'GIT_CONFIG_KEY_{i}'] = f'url.{empty_source.as_uri()}.insteadOf' - partial_env[f'GIT_CONFIG_VALUE_{i}'] = f'ssh://git@localhost/srv/git/{name}.git' - result = subprocess.run(['python3', 'scripts/init-deps.py'], cwd=partial, env=partial_env, text=True, capture_output=True) + partial_env[f'GIT_CONFIG_VALUE_{i}'] = f'ChuGit:/srv/git/{name}.git' + result = subprocess.run(['python3', 'scripts/init-deps.py', '--prefer-ssh'], cwd=partial, env=partial_env, text=True, capture_output=True) self.assertEqual(result.returncode, 0, result.stdout + result.stderr) for name in ['ChuSQL', 'ChuSQL-RustDriver']: self.assertEqual((partial / f'external/{name}/source.txt').read_text(), f'{name} committed source')