Rename fixed deployment entry to production
This commit is contained in:
@@ -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 替代架构。
|
||||
|
||||
|
||||
@@ -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` 在停服务、零活动通话和备份后退役已确认的本项目旧设施。普通重部署不重置数据。
|
||||
|
||||
自动化测试可以使用短时夹具和资源,结束后清理;它们不是预生产的服务依赖。生产发布门禁仍独立,不能把此次预生产部署写成生产签收。
|
||||
自动化测试可以使用短时夹具和资源,结束后清理;它们不是正式部署的服务依赖。当前配置文件仍为非正式配置;正式部署方式与生产验收是两件事,生产发布门禁仍独立,不能把本次部署写成生产签收。
|
||||
@@ -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'):
|
||||
@@ -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:
|
||||
@@ -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()
|
||||
@@ -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')
|
||||
@@ -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 获取。
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user