diff --git a/README.md b/README.md index c0be9da..cb624e2 100644 --- a/README.md +++ b/README.md @@ -264,7 +264,7 @@ npm run prisma:studio ```powershell .\scripts\git-incremental-deploy.ps1 -DryRun -.\scripts\git-incremental-deploy.ps1 +.\scripts\git-incremental-deploy.ps1 -Commit '<40位小写提交SHA>' ``` 部署链路是:源码推送到 Git -> 构建机增量拉取并构建镜像 -> 生产机拉取镜像并替换应用容器。旧的 `deploy-remote-build.ps1` 属于本地历史脚本名,只作为识别应急资料使用,不是仓库交付入口,也不再作为默认推荐方式。 diff --git a/docs/Git增量部署方案.md b/docs/Git增量部署方案.md index 94ad935..27239aa 100644 --- a/docs/Git增量部署方案.md +++ b/docs/Git增量部署方案.md @@ -25,7 +25,7 @@ 默认部署: -- Git 仓库:`https://git.1080p.hk/WuYongJie/gongxiangp2.git`(证书必须有效,禁止跳过 TLS 校验) +- Git 仓库:`http://git.1080p.hk/WuYongJie/gongxiangp2.git`(现场 Gitea 当前仅提供 HTTP;脚本只允许这一条精确地址) - 分支:`main` - 构建机:`192.168.40.206` - 生产机:`192.168.58.20` @@ -45,7 +45,8 @@ - 构建机本地 Registry 端口 `5000` 可用。 - 已通过环境变量或参数提供构建机密码:`BUILD_SERVER_PASSWORD` 或 `-BuildServerPassword`。 - 已分别固定构建机和生产机的 SSH SHA256 主机指纹;OpenSSH 模式还必须使用预置、只读的 `known_hosts` 文件。 -- 私有 Git 仓库认证必须预先配置在维护机和构建机的只读 credential helper 中。脚本拒绝 `GIT_DEPLOY_PASSWORD`、`GIT_AUTH_HEADER`、明文 HTTP 和 URL 内嵌凭据。 +- 私有 Git 仓库认证必须预先配置在维护机和构建机的只读、最小权限 credential helper 中。脚本拒绝 `GIT_DEPLOY_PASSWORD`、`GIT_AUTH_HEADER` 和 URL 内嵌凭据。 +- 由于 Gitea 使用 HTTP,正式构建或发布必须显式传入维护机已核对的 40 位小写 `-Commit`;脚本不允许把 HTTP 返回的分支最新提交直接当作发布依据。 ## 安全边界 @@ -53,7 +54,7 @@ - 不在生产机上执行 `npm install` 或 `next build`。 - 不改生产机的 `.env` 和 uploads 目录。 - 不把 Git 密码写入仓库或远端 URL。 -- Git HTTPS 证书失效、SSH 指纹不匹配或仓库地址偏离权威地址时,发布必须停止;不得使用 `-k` 或关闭主机校验绕过。 +- Gitea HTTP 不提供传输加密或服务器证书身份保证,因此部署账号必须只读、最小权限并定期轮换;仓库地址偏离固定值、未给精确提交 SHA、SSH 指纹不匹配、构建镜像 ID/digest 不一致时,发布必须停止。 - 生产容器启动失败时,默认尝试回滚到旧镜像。 - 清理策略只清理无用镜像/容器,默认保留 Docker build cache。 @@ -65,6 +66,12 @@ .\scripts\git-incremental-deploy.ps1 -DryRun ``` +正式构建或部署必须显式指定完整提交: + +```powershell +.\scripts\git-incremental-deploy.ps1 -Commit '<40位小写提交SHA>' +``` + DryRun 只验证: - 构建机可连接 diff --git a/docs/部署规范.md b/docs/部署规范.md index 59d0e70..0004d01 100644 --- a/docs/部署规范.md +++ b/docs/部署规范.md @@ -25,7 +25,7 @@ 默认参数: -- Git 仓库:`https://git.1080p.hk/WuYongJie/gongxiangp2.git`(只允许有效 TLS,不得降级到 HTTP) +- Git 仓库:`http://git.1080p.hk/WuYongJie/gongxiangp2.git`(现场 Gitea 仅提供 HTTP;脚本只允许这一条精确地址) - 分支:`main` - 构建机:`192.168.40.206` - 生产机:`192.168.58.20` @@ -49,7 +49,7 @@ - 构建机、生产机的固定 SSH SHA256 主机指纹;OpenSSH 模式还要提供受控 `known_hosts` 文件。 - 维护机和构建机已配置只读 Git credential helper。部署脚本禁止把 Git 密码/Header 写入参数、临时脚本或进程命令行。 -不要把 Git 密码写进仓库 URL,也不要把临时部署密码写入文档或提交到仓库。HTTPS 证书过期、主机指纹不匹配或权威仓库不可验证时,必须先修基础设施,不能关闭校验强行发布。 +不要把 Git 密码写进仓库 URL,也不要把临时部署密码写入文档或提交到仓库。Gitea 的 HTTP 通道不具备传输加密与证书身份保证,因此只能使用只读、最小权限 credential helper;正式构建/部署必须显式指定维护机已核对的完整 40 位小写提交 SHA,并继续核对 SSH 主机指纹、镜像 ID 与 manifest digest。 ### 2.2 部署命令 @@ -62,7 +62,7 @@ 部署正式环境: ```powershell -.\scripts\git-incremental-deploy.ps1 +.\scripts\git-incremental-deploy.ps1 -Commit '<40位小写提交SHA>' ``` 影子环境(当前禁止通过通用脚本执行;下面命令会明确失败): @@ -74,13 +74,13 @@ 部署指定提交: ```powershell -.\scripts\git-incremental-deploy.ps1 -Commit +.\scripts\git-incremental-deploy.ps1 -Commit '<40位小写提交SHA>' ``` 必要时彻底重新构建: ```powershell -.\scripts\git-incremental-deploy.ps1 -NoCache +.\scripts\git-incremental-deploy.ps1 -Commit '<40位小写提交SHA>' -NoCache ``` ## 3. 参数说明 @@ -88,7 +88,7 @@ - `-Target`:当前只允许 `production`;`shadow` 会在任何 Git/远程操作前 fail-closed。 - `-RepoUrl`:Git 仓库地址,默认使用 `WuYongJie/gongxiangp2`。 - `-Branch`:部署分支,默认 `main`。 -- `-Commit`:指定提交;为空时部署目标分支最新提交。 +- `-Commit`:正式构建/部署必填,必须是维护机已核对的 40 位小写提交 SHA;仅 `-DryRun` 可不填。 - `-DryRun`:只做连通性与依赖预检,不构建、不推镜像、不重启容器。 - `-NoCache`:Docker 构建不使用缓存。 - `-KeepOldImages`:跳过自动 Docker 镜像清理。 diff --git a/lib/__tests__/video-transcode-deploy-drain.test.ts b/lib/__tests__/video-transcode-deploy-drain.test.ts index 77616b4..f2b6f1a 100644 --- a/lib/__tests__/video-transcode-deploy-drain.test.ts +++ b/lib/__tests__/video-transcode-deploy-drain.test.ts @@ -403,10 +403,14 @@ describe('production video transcode drain gate', () => { expect(dockerfile).toContain('chmod 0444 /app/DEPLOYMENT_COMMIT') }) - test('Git 与 SSH 发布通道禁止明文凭据、盲信主机和宽泛临时脚本权限', () => { - expect(script).toContain("[string]$RepoUrl = 'https://git.1080p.hk/WuYongJie/gongxiangp2.git'") + test('HTTP Gitea 只允许固定仓库和精确提交,SSH 通道禁止明文凭据与盲信主机', () => { + expect(script).toContain("[string]$RepoUrl = 'http://git.1080p.hk/WuYongJie/gongxiangp2.git'") expect(script).toContain('Inline Git credentials are forbidden') - expect(script).toContain("$trustedRepoUrl = 'https://git.1080p.hk/WuYongJie/gongxiangp2.git'") + expect(script).toContain("$trustedRepoUrl = 'http://git.1080p.hk/WuYongJie/gongxiangp2.git'") + expect(script).toContain("if (-not $DryRun -and $Commit -cnotmatch '^[0-9a-f]{40}$')") + expect(script).toContain('HTTP Git transport requires -Commit with the exact 40-character lowercase SHA') + expect(script).toContain("[ \"$REPO_URL\" = 'http://git.1080p.hk/WuYongJie/gongxiangp2.git' ]") + expect(script).not.toContain('https://git.1080p.hk') expect(script).toContain('StrictHostKeyChecking=yes') expect(script).toContain('-hostkey $BuildServerHostKey') expect(script).toContain('Production host key fingerprint does not match the pinned value') @@ -490,6 +494,32 @@ describe('production video transcode drain gate', () => { 35_000, ) + ;(process.platform === 'win32' ? test : test.skip)( + 'HTTP Gitea 正式发布未提供完整提交 SHA 时在任何远程操作前 fail-closed', + () => { + const deployScript = path.join(process.cwd(), 'scripts', 'git-incremental-deploy.ps1') + const powershell = path.join( + process.env.SystemRoot || 'C:\\Windows', + 'System32', + 'WindowsPowerShell', + 'v1.0', + 'powershell.exe', + ) + const deploy = spawnSync(powershell, [ + '-NoProfile', + '-ExecutionPolicy', 'Bypass', + '-File', deployScript, + ], { cwd: process.cwd(), encoding: 'utf8', timeout: 30_000 }) + const output = `${deploy.stdout}\n${deploy.stderr}` + + expect(deploy.status).not.toBe(0) + expect(output).toContain('HTTP Git transport requires -Commit with the exact 40-character lowercase SHA') + expect(output).not.toContain('Resolving and scanning the exact Git tree') + expect(output).not.toContain('Connecting to build server') + }, + 35_000, + ) + test('旧镜像不能证明协议能力时 fail-closed,候选容器不会启动', () => { expect(script).toContain("grep -Fqx -- 'VIDEO_TRANSCODE_DRAIN_PROTOCOL_V1'") expect(script).toContain('-e "VIDEO_TRANSCODE_INSTANCE_ID=$transcode_instance_id"') diff --git a/scripts/git-incremental-deploy.ps1 b/scripts/git-incremental-deploy.ps1 index 69aa8f0..e0e9341 100644 --- a/scripts/git-incremental-deploy.ps1 +++ b/scripts/git-incremental-deploy.ps1 @@ -17,7 +17,7 @@ param( [ValidateSet('production', 'shadow')] [string]$Target = 'production', - [string]$RepoUrl = 'https://git.1080p.hk/WuYongJie/gongxiangp2.git', + [string]$RepoUrl = 'http://git.1080p.hk/WuYongJie/gongxiangp2.git', [string]$Branch = 'main', [string]$Commit = '', @@ -71,6 +71,10 @@ if (-not $SkipDeploy -and ($SkipBuild -or $SkipPush)) { throw 'Production deployment requires a freshly built and pushed exact-SHA image; -SkipBuild and -SkipPush are only allowed together with -SkipDeploy.' } +if (-not $DryRun -and $Commit -cnotmatch '^[0-9a-f]{40}$') { + throw 'HTTP Git transport requires -Commit with the exact 40-character lowercase SHA selected on the trusted workstation.' +} + if (-not $DryRun -and -not $SkipDeploy) { if ($MigrationAuditFile -cnotmatch '^/root/gongxiangp2-migration-audit/[A-Za-z0-9._-]+/applied-[0-9]{8}T[0-9]{6}Z\.json$') { throw 'Production deployment requires the immutable apply audit file under /root/gongxiangp2-migration-audit//applied-.json.' @@ -93,13 +97,13 @@ if ( throw 'Inline Git credentials are forbidden. Configure a read-only credential helper on the workstation and build server instead.' } -$trustedRepoUrl = 'https://git.1080p.hk/WuYongJie/gongxiangp2.git' +$trustedRepoUrl = 'http://git.1080p.hk/WuYongJie/gongxiangp2.git' $localDryRunRepository = $false if ($DryRun -and [System.IO.Path]::IsPathRooted($RepoUrl)) { $localDryRunRepository = Test-Path -LiteralPath $RepoUrl -PathType Container } if (-not $localDryRunRepository -and $RepoUrl -cne $trustedRepoUrl) { - throw "Production Git repository must be the pinned HTTPS repository $trustedRepoUrl. Plain HTTP, inline credentials, and arbitrary repositories are forbidden." + throw "Production Git repository must be the exact pinned HTTP repository $trustedRepoUrl. URL credentials and arbitrary repositories are forbidden." } # Production evidence from 2026-08-02 identifies the dedicated ZFS dataset as @@ -254,8 +258,8 @@ function Invoke-LegacyStorageBoundaryPreflight { $targetCommit = $branchCommit if (-not [string]::IsNullOrWhiteSpace($RequestedCommit)) { - if ($RequestedCommit -notmatch '^[0-9a-fA-F]{7,40}$') { - throw 'Commit override must be a 7-40 character hexadecimal Git commit ID.' + if ($RequestedCommit -cnotmatch '^[0-9a-f]{40}$') { + throw 'Commit override must be the exact 40-character lowercase Git commit SHA.' } $targetCommit = Invoke-PreflightGit -GitExecutable $gitCommand.Source -CaptureOutput -Arguments @( "--git-dir=$bareRepository", 'rev-parse', '--verify', "${RequestedCommit}^{commit}" @@ -697,7 +701,7 @@ require_cmd curl require_cmd node require_cmd ssh require_cmd ssh-keygen -[ "$REPO_URL" = 'https://git.1080p.hk/WuYongJie/gongxiangp2.git' ] || fail "Build server received an untrusted Git repository URL" +[ "$REPO_URL" = 'http://git.1080p.hk/WuYongJie/gongxiangp2.git' ] || fail "Build server received an untrusted Git repository URL" [ "$REMOTE_BUILD_DIR" = '/root/youyidian-3d-git-build' ] || fail "Build directory is not the pinned dedicated worktree" test -f "$PROD_KNOWN_HOSTS_FILE" || fail "Pinned production known_hosts file is missing on build server" test ! -L "$PROD_KNOWN_HOSTS_FILE" || fail "Pinned production known_hosts file must not be a symlink"