From 1b4f8be2bdca3696a9c69c6d56726bf96118d42b Mon Sep 17 00:00:00 2001 From: Rogee Date: Fri, 9 Oct 2026 11:52:34 +0800 Subject: [PATCH] Rename fixed deployment entry to production --- AGENTS.md | 6 +-- deploys/{preprod => production}/.gitignore | 0 deploys/{preprod => production}/README.md | 18 ++++--- deploys/{preprod => production}/probe.go | 0 deploys/{preprod => production}/probe_test.go | 0 .../preprod.py => production/production.py} | 14 +++--- deploys/{preprod => production}/remote.py | 12 ++--- .../{preprod => production}/test_lifecycle.py | 7 ++- .../test_production.py} | 50 ++++++++++++++++++- .../fixed-preprod-systemd-20261008.md | 6 ++- scripts/acceptance-local.sh | 4 +- 11 files changed, 87 insertions(+), 30 deletions(-) rename deploys/{preprod => production}/.gitignore (100%) rename deploys/{preprod => production}/README.md (82%) rename deploys/{preprod => production}/probe.go (100%) rename deploys/{preprod => production}/probe_test.go (100%) rename deploys/{preprod/preprod.py => production/production.py} (93%) rename deploys/{preprod => production}/remote.py (96%) rename deploys/{preprod => production}/test_lifecycle.py (92%) rename deploys/{preprod/test_preprod.py => production/test_production.py} (77%) diff --git a/AGENTS.md b/AGENTS.md index 5aa96f4..0d12bf4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -79,9 +79,9 @@ ## 当前范围与权威入口 -- **当前预生产目标授权(`muzr4gzv-jzx8pg`)**:先核验、合入 main 并推送,再完善一键部署脚本,在登记测试机 `server.sip` 使用根目录 `saas.env`、`rabbitmq.env`、`aliyun-oss.env` 和真实 SaaS 配置,以现有 `nonprod-real` 启动固定的 `go-sip-agent.service`、`go-sip-dispatcher.service`;原生 `go-sip-asterisk.service` 保留。配置固定在 `~/.config/go-sip/`,业务数据固定在 `~/.local/share/go-sip/`,制品在 `~/.local/opt/go-sip/releases/`,不再用每次试验的新业务目录或 Mock 代替预生产验证。下方日期记录是历史事实,其旧授权限制不覆盖本条新授权。 +- **当前正式部署目标授权(使用非正式配置,`muzr4gzv-jzx8pg`)**:先核验、合入 main 并推送,再完善一键部署脚本,在登记测试机 `server.sip` 使用根目录 `saas.env`、`rabbitmq.env`、`aliyun-oss.env` 和真实 SaaS 配置,以现有 `nonprod-real` 启动固定的 `go-sip-agent.service`、`go-sip-dispatcher.service`;原生 `go-sip-asterisk.service` 保留。配置固定在 `~/.config/go-sip/`,业务数据固定在 `~/.local/share/go-sip/`,制品在 `~/.local/opt/go-sip/releases/`,不再用每次试验的新业务目录或 Mock 代替正式部署验证。正式部署入口为 `deploys/production/production.py`;当前使用非正式配置,仍保留 `nonprod-real`、登记测试机及原有授权边界,命名不代表生产验收。下方日期记录是历史事实,其旧授权限制不覆盖本条新授权。 - **本轮外呼授权**:真实 SaaS 下发的全部获批外呼任务允许按正式规则执行,不再逐通询问;仍核对归属、号码、时段、额度、并发、有效授权与幂等,不自行生成外呼消息、不盲重拨或换线重拨。独立工具试拨及超出上述真实 SaaS 任务范围的操作仍需另行确认。没有任务或未接通时如实标记未验证,不以 Mock 代签真实通话或生产。 -- **部署与数据重置**:普通重部署不清数据;仅显式 `--reset-state` 可处理本项目非生产数据。本次首次部署已获准,后续重置须再次确认;必须先停止相关服务、核实零活动通话、完成私密备份与清单,再重置,不把重置当成代码问题修复,不伪造旧通话终结或结果,不清外部 SaaS/MQ/OSS。清理限于两端确认退役的部署制品、临时文件及自有 Mock 设施;不得直接删仍用凭据、SSH 登记、原始证据、录音或恢复,不删除整个用户 `~/.local` 或 Asterisk/其他项目依赖。退役数据按确认范围保留或备份后处理。SaaS 尚未启动不阻断部署:严格保留输入校验与关新准入,允许 Dispatcher 由 systemd 明确等待/重试,不用 Mock 或旧快照;SaaS/依赖业务队列/会话/SIP加载及通话验证延期,独立配置、MQ登录、证书或Asterisk故障仍须修复。2026-10-08 固定环境首次部署 `e5f46ba` 已完成:Asterisk/Agent enabled+active,Dispatcher enabled且因SaaS业务拓扑尚未供给而auto-restart,业务未ready。旧20个临时服务及自有本地RabbitMQ已私密归档退役;12份配置/证书0600,固定目录0700、linger已开启,但未实际重启验收。复核后5个旧unit drop-in目录/6份配置已私密归档移除,旧发布根目录原文件整体保留在go-sip-backups而非运行位置;证书/未知传输错误不归为SaaS等待,D须实际activating/auto-restart且有对应最新错误,failed不算完成。历史数据库/恢复事实保留于受控备份,不伪造旧终结;首次固定目录为空,已显式执行本次获批reset,之后普通重部署不reset。部署说明见 [`deploys/preprod/README.md`](deploys/preprod/README.md),证据见 [`docs/evidence/fixed-preprod-systemd-20261008.md`](docs/evidence/fixed-preprod-systemd-20261008.md)。 +- **部署与数据重置**:普通重部署不清数据;仅显式 `--reset-state` 可处理本项目非生产数据。本次首次部署已获准,后续重置须再次确认;必须先停止相关服务、核实零活动通话、完成私密备份与清单,再重置,不把重置当成代码问题修复,不伪造旧通话终结或结果,不清外部 SaaS/MQ/OSS。清理限于两端确认退役的部署制品、临时文件及自有 Mock 设施;不得直接删仍用凭据、SSH 登记、原始证据、录音或恢复,不删除整个用户 `~/.local` 或 Asterisk/其他项目依赖。退役数据按确认范围保留或备份后处理。SaaS 尚未启动不阻断部署:严格保留输入校验与关新准入,允许 Dispatcher 由 systemd 明确等待/重试,不用 Mock 或旧快照;SaaS/依赖业务队列/会话/SIP加载及通话验证延期,独立配置、MQ登录、证书或Asterisk故障仍须修复。2026-10-08 固定环境首次部署 `e5f46ba` 已完成:Asterisk/Agent enabled+active,Dispatcher enabled且因SaaS业务拓扑尚未供给而auto-restart,业务未ready。旧20个临时服务及自有本地RabbitMQ已私密归档退役;12份配置/证书0600,固定目录0700、linger已开启,但未实际重启验收。复核后5个旧unit drop-in目录/6份配置已私密归档移除,旧发布根目录原文件整体保留在go-sip-backups而非运行位置;证书/未知传输错误不归为SaaS等待,D须实际activating/auto-restart且有对应最新错误,failed不算完成。历史数据库/恢复事实保留于受控备份,不伪造旧终结;首次固定目录为空,已显式执行本次获批reset,之后普通重部署不reset。部署说明见 [`deploys/production/README.md`](deploys/production/README.md),证据见 [`docs/evidence/fixed-preprod-systemd-20261008.md`](docs/evidence/fixed-preprod-systemd-20261008.md)。 - P01–P08 及 K01–K16 已完成**项目内隔离 Mock** 核验;本轮把分散的人类可读契约、文档与第三方对接合为唯一当前规范,不重审已确认规则。若未来另获启动开发/审查子 Agent 授权,必须按使用者指定的 `gpt-5.6-luna`、`max` 思考和 `fast: true` 逐项核验并显式配置,不静默换模型、降档或关闭 fast。 - 唯一现行 SaaS↔Dispatcher 业务规范、Schema、拓扑和正常示例由共享仓库统一管理;现行合同只由 Git submodule 提交号固定,不维护重复逐文件或整包 hash;历史来源证据、验证脚本及错误示例放在 `contracts/`,不提交到共享仓库或嵌入运行契约。在本项目的共享入口为 [`contracts/schema/`](contracts/schema/README.md);[`docs/thirds/saas-dispatcher.md`](docs/thirds/saas-dispatcher.md) 只保留导航,不维护另一份规范。内部 Agent RPC 仍在 [`proto/agent/agent.proto`](proto/agent/agent.proto)。本地验收与外部缺口见 [`docs/evidence/saas-dispatcher-p08-acceptance.md`](docs/evidence/saas-dispatcher-p08-acceptance.md)。Markdown 不代替机器合同或外部签收,也不另外维护一份平行字段定义。 @@ -123,7 +123,7 @@ | 百应 | `160.202.254.79:5060` | `KQ91526` | `mka755108` | -- 业务全线路的原始被叫号码仅由校验归属及任务后的 SaaS `call.execute.payload.callee` 确定;不在 Dispatcher、SaaS Mock 或抓证脚本设置固定号码/日期特判,不向任务快照增设号码列表。只接受 1–32 位 ASCII 数字;格式校验不等于真实拨号授权。不再另设本地固定的 `09:00`–`20:00` 窗口或每线路每号码每日 3 次上限;任务、线路时段和额度以 SaaS 配置快照校验为准,不等候/自动延迟/自动重试/静默换线。独立工具或非上述真实 SaaS 获批任务的真实试拨仍须使用者明确安排;本轮预生产正式任务按当前授权执行。仅在 Agent 指定 `--debug/-D` 时,由专用主机脚本在拨号前启用抓包和 PJSIP logger、签发与该通 `event_id`/trunk/原始号码绑定的短时有效活跃抓包凭证,调试 Agent 拒绝缺失/失效/不匹配的凭证。默认 Agent 不依赖外部抓包或凭证;独立 sip-call 的号码与线路仅来自本次显式命令和 ENV,不冒充业务任务授权。SaaS Mock 投递和本地时段测试不构成真实拨号授权。 +- 业务全线路的原始被叫号码仅由校验归属及任务后的 SaaS `call.execute.payload.callee` 确定;不在 Dispatcher、SaaS Mock 或抓证脚本设置固定号码/日期特判,不向任务快照增设号码列表。只接受 1–32 位 ASCII 数字;格式校验不等于真实拨号授权。不再另设本地固定的 `09:00`–`20:00` 窗口或每线路每号码每日 3 次上限;任务、线路时段和额度以 SaaS 配置快照校验为准,不等候/自动延迟/自动重试/静默换线。独立工具或非上述真实 SaaS 获批任务的真实试拨仍须使用者明确安排;本轮使用非正式配置的正式部署任务按当前授权执行。仅在 Agent 指定 `--debug/-D` 时,由专用主机脚本在拨号前启用抓包和 PJSIP logger、签发与该通 `event_id`/trunk/原始号码绑定的短时有效活跃抓包凭证,调试 Agent 拒绝缺失/失效/不匹配的凭证。默认 Agent 不依赖外部抓包或凭证;独立 sip-call 的号码与线路仅来自本次显式命令和 ENV,不冒充业务任务授权。SaaS Mock 投递和本地时段测试不构成真实拨号授权。 - 任务按周一至周日多个时段与指定排除日期配置,线路只有每周允许时段(**没有线路排除日期**),Asia/Shanghai 左闭右开、跨日拆分;缺失或不确定 fail-closed,不自动重拨。由 Dispatcher 在持久接纳与实际发出指令前判定,并取任务/获批 AI 较小通话时限;Agent 仅校验会话和签发期限,不重算外呼策略。本地 SaaS 快照策略测试与主机抓证/逐次授权须分别报告。 - 当前修复仅在本地核验,未更新测试机上的旧多主叫 Mock 快照,也未部署或授权新呼叫。`BD` 等主叫原值不得清洗或当作 Digest 用户名;业务原始被叫号码不变,仅被选定数企 trunk 按规则构造 `7089<原号>`(其它线路使用自己的前缀),不重复加前缀。三条 trunk 独立,不能把同地址伪造为备用线路或换线重拨;服务商反馈 PCMA,对应 Asterisk `allow=alaw`,传输/注册/鉴权/并发仍待真实签收。不以 sipgo/diago 另造 Asterisk 替代架构。 diff --git a/deploys/preprod/.gitignore b/deploys/production/.gitignore similarity index 100% rename from deploys/preprod/.gitignore rename to deploys/production/.gitignore diff --git a/deploys/preprod/README.md b/deploys/production/README.md similarity index 82% rename from deploys/preprod/README.md rename to deploys/production/README.md index 2bf4e26..5136a21 100644 --- a/deploys/preprod/README.md +++ b/deploys/production/README.md @@ -1,6 +1,8 @@ -# 固定预生产部署 +# 正式部署 -目标是登记测试机 `server.sip`。服务使用 `nonprod-real`、真实 SaaS/RabbitMQ/AI/OSS 和原生 Asterisk;不安装 SaaS Mock 或本机 RabbitMQ,不以一次性运行目录代替正式运行。 +这套流程按正式部署实施,当前使用的是非正式配置文件,不是另一套“预生产”部署方式。目录为 `deploys/production/`,入口为 `production.py`。 + +当前获批目标仍是登记测试机 `server.sip`。服务使用 `nonprod-real`、真实 SaaS/RabbitMQ/AI/OSS 和原生 Asterisk;不安装 SaaS Mock 或本机 RabbitMQ,不以一次性运行目录代替正式运行。目录和脚本改名不改变运行方式、配置来源、数据处理范围或主机授权,也不表示生产验收已通过。 ## 固定位置 @@ -47,14 +49,14 @@ ## 一键入口 ```bash -python3 deploys/preprod/preprod.py deploy -python3 deploys/preprod/preprod.py status -python3 deploys/preprod/preprod.py stop -python3 deploys/preprod/preprod.py start +python3 deploys/production/production.py deploy +python3 deploys/production/production.py status +python3 deploys/production/production.py stop +python3 deploys/production/production.py start # 仅本次首次部署已获授权;之后先取得新的重置确认: -python3 deploys/preprod/preprod.py deploy --reset-state --confirm-reset +python3 deploys/production/production.py deploy --reset-state --confirm-reset ``` 部署要求本地main干净且已推送,源码固定为远程main,文件上传后再次校验SHA-256。`bootstrap` 仅导入原生Asterisk/TLS等主机基础元数据,并按根目录env覆盖业务来源;`retire-mocks` 在停服务、零活动通话和备份后退役已确认的本项目旧设施。普通重部署不重置数据。 -自动化测试可以使用短时夹具和资源,结束后清理;它们不是预生产的服务依赖。生产发布门禁仍独立,不能把此次预生产部署写成生产签收。 +自动化测试可以使用短时夹具和资源,结束后清理;它们不是正式部署的服务依赖。当前配置文件仍为非正式配置;正式部署方式与生产验收是两件事,生产发布门禁仍独立,不能把本次部署写成生产签收。 diff --git a/deploys/preprod/probe.go b/deploys/production/probe.go similarity index 100% rename from deploys/preprod/probe.go rename to deploys/production/probe.go diff --git a/deploys/preprod/probe_test.go b/deploys/production/probe_test.go similarity index 100% rename from deploys/preprod/probe_test.go rename to deploys/production/probe_test.go diff --git a/deploys/preprod/preprod.py b/deploys/production/production.py similarity index 93% rename from deploys/preprod/preprod.py rename to deploys/production/production.py index b4130b3..13b36ba 100644 --- a/deploys/preprod/preprod.py +++ b/deploys/production/production.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Deploy only the registered nonproduction node; never execute source env files.""" +"""Formal deployment on the registered node with nonproduction configuration; never execute source env files.""" import argparse import hashlib import json @@ -130,7 +130,7 @@ def main(): ssh = lambda cmd, **kw: checked(['ssh', *opts, args.host, cmd], timeout=180, **kw) tool_dir = '/home/rogee/.local/share/go-sip-tools' ssh("python3 -c \"from pathlib import Path;import os;p=Path('" + tool_dir + "');p.mkdir(mode=0o700,exist_ok=True);os.chmod(p,0o700)\"") - checked(['scp', '-q', *opts, str(HERE / 'remote.py'), args.host + ':' + tool_dir + '/preprod-remote.py'], timeout=30) + checked(['scp', '-q', *opts, str(HERE / 'remote.py'), args.host + ':' + tool_dir + '/production-remote.py'], timeout=30) payload = {'action': args.action, 'reset_state': args.reset_state, 'confirm_reset': args.confirm_reset} if args.action in ('deploy', 'bootstrap', 'retire-mocks'): payload['source'] = sources(args.source_dir) @@ -142,19 +142,19 @@ def main(): remote_commit = checked(['git', 'ls-remote', 'origin', 'refs/heads/main'], cwd=REPO).split()[0] if commit != remote_commit: raise ValueError('main is not pushed') - build_root = Path(tempfile.mkdtemp(prefix='go-sip-preprod-build-')) + build_root = Path(tempfile.mkdtemp(prefix='go-sip-production-build-')) try: - for name, source in (('sip-go-agent', './cmd/sip-go-agent'), ('preprod-probe', './deploys/preprod')): + for name, source in (('sip-go-agent', './cmd/sip-go-agent'), ('production-probe', './deploys/production')): env = dict(os.environ, CGO_ENABLED='0', GOOS='linux', GOARCH='amd64') checked(['go', 'build', '-trimpath', '-o', str(build_root / name), source], cwd=REPO, env=env, timeout=180) staging = tool_dir + '/' + commit ssh('mkdir -m 700 -p ' + shlex.quote(staging)) - for name in ('sip-go-agent', 'preprod-probe'): + for name in ('sip-go-agent', 'production-probe'): checked(['scp', '-q', *opts, str(build_root / name), args.host + ':' + staging + '/' + name], timeout=60) - payload.update({'commit': commit, 'staging': staging, 'hashes': {name: hashlib.sha256((build_root / name).read_bytes()).hexdigest() for name in ('sip-go-agent', 'preprod-probe')}}) + payload.update({'commit': commit, 'staging': staging, 'hashes': {name: hashlib.sha256((build_root / name).read_bytes()).hexdigest() for name in ('sip-go-agent', 'production-probe')}}) finally: __import__('shutil').rmtree(build_root) - raw = ssh('python3 ' + shlex.quote(tool_dir + '/preprod-remote.py'), input=json.dumps(payload)) + raw = ssh('python3 ' + shlex.quote(tool_dir + '/production-remote.py'), input=json.dumps(payload)) result = json.loads(raw) print(json.dumps(result, indent=2)) if not result.get('ok'): diff --git a/deploys/preprod/remote.py b/deploys/production/remote.py similarity index 96% rename from deploys/preprod/remote.py rename to deploys/production/remote.py index abbb6fd..7f8ab0d 100644 --- a/deploys/preprod/remote.py +++ b/deploys/production/remote.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Private-input remote operations for the single approved preproduction node.""" +"""Private-input formal deployment operations for the single approved node.""" import base64 import configparser import datetime @@ -310,7 +310,7 @@ def retire(p, payload): # addresses/NAT must not make us uninstall the configured real broker. marker = 'go-sip-retirement-check-' + uuid.uuid4().hex config = dict(payload['source']['probe'], connection_name=marker, hold_mq_ms=10000) - probe = p['home'] / '.local/share/go-sip-tools/preprod-probe' + probe = p['home'] / '.local/share/go-sip-tools/production-probe' child = subprocess.Popen([str(probe)], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) child.stdin.write(json.dumps(config)); child.stdin.close() time.sleep(1) @@ -386,14 +386,14 @@ def install(payload, p): if staging.parent != p['home'] / '.local/share/go-sip-tools' or staging.is_symlink(): raise ValueError('unexpected uploaded staging path') for name, expected in payload['hashes'].items(): - if name not in ('sip-go-agent', 'preprod-probe'): + if name not in ('sip-go-agent', 'production-probe'): raise ValueError('unexpected artifact') candidate = staging / name if candidate.is_symlink() or hashlib.sha256(candidate.read_bytes()).hexdigest() != expected: raise ValueError('uploaded artifact checksum mismatch') shutil.copy2(candidate, release / name) (release / name).chmod(0o700) - validate_environments(release / 'preprod-probe', roles) + validate_environments(release / 'production-probe', roles) write_private(release / 'manifest.json', json.dumps({'commit': payload['commit'], 'hashes': payload['hashes'], 'production_approval': False}, indent=2)) current = p['current'] if current.exists() and not current.is_symlink(): @@ -404,13 +404,13 @@ def install(payload, p): tmp.symlink_to(release) os.replace(tmp, current) for role in ('agent', 'dispatcher'): - unit = '[Unit]\nDescription=go-sip real preproduction ' + role + '\nAfter=go-sip-asterisk.service network-online.target\n\n[Service]\nType=simple\nEnvironmentFile=' + str(p['config'] / (role + '.env')) + '\nExecStart=' + str(current / 'sip-go-agent') + ' ' + role + ' --mode nonprod-real\nRestart=on-failure\nRestartSec=10\nUMask=0077\n\n[Install]\nWantedBy=default.target\n' + unit = '[Unit]\nDescription=go-sip production deployment ' + role + '\nAfter=go-sip-asterisk.service network-online.target\n\n[Service]\nType=simple\nEnvironmentFile=' + str(p['config'] / (role + '.env')) + '\nExecStart=' + str(current / 'sip-go-agent') + ' ' + role + ' --mode nonprod-real\nRestart=on-failure\nRestartSec=10\nUMask=0077\n\n[Install]\nWantedBy=default.target\n' write_private(p['units'] / ('go-sip-' + role + '.service'), unit) run(['systemctl', '--user', 'daemon-reload']) run(['sudo', '-n', 'loginctl', 'enable-linger', p['home'].name]) run(['systemctl', '--user', 'enable', *UNITS]) # MQ is checked independently even when real SaaS has not started. - probe = subprocess.run([str(release / 'preprod-probe')], input=json.dumps(source['probe']), text=True, capture_output=True, timeout=110) + probe = subprocess.run([str(release / 'production-probe')], input=json.dumps(source['probe']), text=True, capture_output=True, timeout=110) try: dependency = json.loads(probe.stdout) except json.JSONDecodeError: diff --git a/deploys/preprod/test_lifecycle.py b/deploys/production/test_lifecycle.py similarity index 92% rename from deploys/preprod/test_lifecycle.py rename to deploys/production/test_lifecycle.py index b188433..c94640f 100644 --- a/deploys/preprod/test_lifecycle.py +++ b/deploys/production/test_lifecycle.py @@ -25,7 +25,7 @@ class LifecycleTest(unittest.TestCase): self.staging = self.home / '.local/share/go-sip-tools' / ('a' * 40) remote.mkdir(self.staging) self.hashes = {} - for name in ('sip-go-agent', 'preprod-probe'): + for name in ('sip-go-agent', 'production-probe'): (self.staging / name).write_bytes(b'synthetic executable') self.hashes[name] = hashlib.sha256((self.staging / name).read_bytes()).hexdigest() self.payload = {'commit': 'a' * 40, 'staging': str(self.staging), 'hashes': self.hashes, 'source': {'dispatcher_id': 'synthetic', 'probe': {}}} @@ -61,8 +61,13 @@ class LifecycleTest(unittest.TestCase): self.assertTrue(self.p['current'].is_symlink()) for role in ('agent', 'dispatcher'): unit = (self.p['units'] / ('go-sip-' + role + '.service')).read_text() + self.assertIn('Description=go-sip production deployment ' + role, unit) self.assertIn('Restart=on-failure', unit) + self.assertIn('--mode nonprod-real', unit) self.assertNotIn('--mode mock', unit) + manifest = json.loads((self.p['current'] / 'manifest.json').read_text()) + self.assertFalse(manifest['production_approval']) + self.assertEqual(set(manifest['hashes']), {'sip-go-agent', 'production-probe'}) def test_explicit_reset_stops_and_archives_before_deleting(self): original = self.state.read_bytes() diff --git a/deploys/preprod/test_preprod.py b/deploys/production/test_production.py similarity index 77% rename from deploys/preprod/test_preprod.py rename to deploys/production/test_production.py index 55eecbc..91b8d17 100644 --- a/deploys/preprod/test_preprod.py +++ b/deploys/production/test_production.py @@ -1,8 +1,14 @@ +import contextlib import importlib.util +import io +import json import os from pathlib import Path +import subprocess +import sys import tempfile import unittest +from unittest.mock import patch HERE = Path(__file__).resolve().parent @@ -16,7 +22,7 @@ def module(name): class SourcesTest(unittest.TestCase): def setUp(self): - self.m = module('preprod') + self.m = module('production') self.root = Path(tempfile.mkdtemp()) self.addCleanup(lambda: __import__('shutil').rmtree(self.root)) @@ -59,6 +65,48 @@ class SourcesTest(unittest.TestCase): self.m.sources(self.root) +class EntryPointTest(unittest.TestCase): + def test_help_uses_production_entry_without_private_inputs(self): + result = subprocess.run([sys.executable, str(HERE / 'production.py'), '--help'], capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn('production.py', result.stdout) + self.assertNotIn('preprod', result.stdout) + + def test_deploy_builds_and_uploads_matching_production_artifacts(self): + m = module('production') + calls = [] + payloads = [] + + def checked(args, **kwargs): + calls.append(args) + if args[:3] == ['git', 'branch', '--show-current']: + return 'main' + if args[:3] == ['git', 'rev-parse', 'HEAD']: + return 'a' * 40 + if args[:2] == ['git', 'ls-remote']: + return 'a' * 40 + '\trefs/heads/main' + if args[:2] == ['go', 'build']: + Path(args[args.index('-o') + 1]).write_bytes(b'synthetic executable') + if args[0] == 'ssh' and args[-1].startswith('python3 /'): + payloads.append(json.loads(kwargs['input'])) + return json.dumps({'ok': True}) + return '' + + with tempfile.TemporaryDirectory() as tmp: + known_hosts = Path(tmp) / 'known_hosts' + known_hosts.touch() + argv = ['production.py', 'deploy', '--known-hosts', str(known_hosts)] + with patch.object(sys, 'argv', argv), patch.object(m, 'sources', return_value={'synthetic': True}), patch.object(m, 'checked', side_effect=checked), contextlib.redirect_stdout(io.StringIO()): + m.main() + + builds = [args for args in calls if args[:2] == ['go', 'build']] + self.assertEqual([args[-1] for args in builds], ['./cmd/sip-go-agent', './deploys/production']) + self.assertEqual([Path(args[args.index('-o') + 1]).name for args in builds], ['sip-go-agent', 'production-probe']) + self.assertEqual(set(payloads[0]['hashes']), {'sip-go-agent', 'production-probe'}) + self.assertTrue(any(args[0] == 'scp' and args[-1].endswith('/production-remote.py') for args in calls)) + self.assertFalse(any('preprod' in arg for args in calls for arg in args)) + + class SafetyTest(unittest.TestCase): def setUp(self): self.m = module('remote') diff --git a/docs/evidence/fixed-preprod-systemd-20261008.md b/docs/evidence/fixed-preprod-systemd-20261008.md index d3088cc..acf273b 100644 --- a/docs/evidence/fixed-preprod-systemd-20261008.md +++ b/docs/evidence/fixed-preprod-systemd-20261008.md @@ -1,4 +1,6 @@ -# 固定预生产部署:2026-10-08 +# 固定正式部署(使用非正式配置):2026-10-08 + +命名说明:现行目录与入口为 `deploys/production/production.py`;此历史记录的文件名和当时执行的旧命令保留,用于追溯,不代表另有一套预生产部署方式。改名不改变本记录的验收事实。 ## 范围与状态 @@ -8,7 +10,7 @@ SaaS 尚未启动,三个正式入口当前均为 HTTP 404;部署不等待 Sa ## 固定入口 -使用 `deploys/preprod/preprod.py` 的 `deploy/start/stop/status`,以及另外确认的 `deploy --reset-state --confirm-reset`。固定配置、数据库、恢复、发布版本、备份路径见 [部署说明](../../deploys/preprod/README.md)。首次重置已获用户授权,之后每次重置仍须新确认;普通重复部署无重置。旧 SQLite 不迁移、不以报错为由自动清理。 +当时使用旧入口 `deploys/preprod/preprod.py` 的 `deploy/start/stop/status`,以及另外确认的 `deploy --reset-state --confirm-reset`。固定配置、数据库、恢复、发布版本、备份路径见 [部署说明](../../deploys/production/README.md)。首次重置已获用户授权,之后每次重置仍须新确认;普通重复部署无重置。旧 SQLite 不迁移、不以报错为由自动清理。 根目录三份私有文件先检查所有者和 0600,按原格式解析,不执行 source。OSS 的 RAM 字段为不缩进的冒号格式,不是 YAML;生成当前配置的桶、区域、连接和命名环境变量引用。Agent/Dispatcher 的实际环境、单 Agent 清单、OSS 配置及双向 TLS 文件已在测试机通过**纯读取检查**,未打开数据库或网络。AI 连接和业务参数只由正式 SaaS 获取。 diff --git a/scripts/acceptance-local.sh b/scripts/acceptance-local.sh index 6377c3b..bca0ddd 100755 --- a/scripts/acceptance-local.sh +++ b/scripts/acceptance-local.sh @@ -4,7 +4,7 @@ set -euo pipefail ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) cd "$ROOT" -unformatted=$(find contracts internal cmd deploys/preprod -type f -name '*.go' -print0 | xargs -0 gofmt -l) +unformatted=$(find contracts internal cmd deploys/production -type f -name '*.go' -print0 | xargs -0 gofmt -l) if [[ -n "$unformatted" ]]; then printf 'Go files need gofmt:\n%s\n' "$unformatted" >&2 exit 1 @@ -19,7 +19,7 @@ if [[ -e scripts/mq-only-acceptance-local.sh ]]; then exit 1 fi -PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s deploys/preprod -p 'test_*.py' +PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s deploys/production -p 'test_*.py' ./scripts/check-contracts.sh bash ./scripts/check-install-diagnostics.sh go mod verify