Files
skills/skills/app-dev/references/publish-deploy.md
T
sunjunzhao 3022d74a92 feat(app-dev): wasm CSP baseline and per-app CSP directives
- contract/checklist/AGENTS template cover connect/script/worker/img/media
- check-app.sh validates directives and cross-origin loads against declarations
- selftest covers external worker/script and blob worker cases
2026-10-26 00:58:37 +08:00

10 KiB
Raw Blame History

发布、部署与验收(release-cli / 商店 / quickstart)

1. release-cli(aigrego-release)

零依赖单文件 ESM(Node ≥18),不上 npm。获取:

# 一行安装(分发仓随平台发版同步)
curl -sSL https://code.innev.cn/images/tickshield-dist/raw/branch/main/cli/aigrego-release.mjs \
  -o /usr/local/bin/aigrego-release && chmod +x /usr/local/bin/aigrego-release
# 或 vendored 进应用仓库:node ./tools/aigrego-release.mjs ...
# CI runner 无 node 时:docker run --rm -v "$PWD:/src" node:20-alpine node /src/cli/aigrego-release.mjs ...

命令集(仅四个)

命令 作用
whoami 用应用凭证换一次服务态 TDT 验凭证——CI 里先跑这个(放在构建之前)
publish 四个可组合子动作,固定顺序:--manifest → --dist → --image → --store
register-app --manifest m.json [--host --port] 调 POST /api/register,网关 ≤2s 热载 /svc/<key>/
status 查状态机/生效版本/在审条目

publish 子动作:

  1. --manifest app.manifest.json → POST /api/apps/submit。治理字段有 diff ⇒ 202 进审核队列,批准前不影响已安装租户;无 diff ⇒ 200 当场生效;首个版本直接生效。版本号以 manifest.version 为准(semver)。
  2. --dist <dir|tgz> → POST /api/apps/:key/artifacts。产物目录根的 index.html 必须存在(tar czf -C <dir> .,套一层目录会被拒);tgz 请求体 ≤64 MiB(网关同值,超过直接 413)、路径穿越/symlink 被拒、同卷 rename 原子替换;上传后 /apps/<key>/* 托管立即可读(2026-10 起拍平,旧 /_/ 子路径产物必须重新构建上传)。超限走下方「大产物外置」。
  3. --image <ref> → POST /api/apps/:key/deploy。只写 deployDescriptor.image 并滚动重启;镜像必须先由应用方自己 push 到平台白名单 registry;docker.sock 不可用时降级为仅记录。
  4. --store → POST /api/apps/:key/publish 自行上架。前置:平台已给该应用开 auto_publish,否则 403;已上架返回 409 视为跳过。

大产物外置(dist tgz > 64 MiB)

  1. dist 只保留前端壳(index.html + 它直接引用的 JS/CSS),核心资源放 assets/ 目录(不进 dist)。
  2. CI 用钉版 minio/mc 镜像 mirror:apps/<key>/<version>/,版本化路径 + immutable 缓存;旧版本保留供回滚。
  3. 桶配 CORS:GET/HEAD/OPTIONS、Allow-Headers: Range、Expose-Headers: Content-Length,Content-Range,ETag;CI 做匿名读自检。
  4. manifest 按指令声明(仅 micro,治理字段 → 202 审核):
    "embedCsp": {
      "connectSrc": ["https://static.example.cn"],
      "scriptSrc": ["blob:", "https://core.example.cn"],
      "workerSrc": ["blob:"],
      "imgSrc": ["blob:"]
    }
    
    来源只能是 blob: 或 scheme://host[:port];platform 基线已固定 script-src 'self' 'wasm-unsafe-eval'(wasm 编译必需)。
  5. 指令对应关系:核心用 fetch 加载 → connectSrc;外链/动态脚本与 Worker 内 importScripts → scriptSrc;new Worker(含 blob)→ workerSrc;blob 图片/音视频 → imgSrc/mediaSrc。外链 stylesheet 仍未放开。
  6. 模板见 references/templates/gitea-app-release.yaml 的 assets job(未配 TS_ASSETS_BUCKET 时自动跳过)。

凭证与配置

复用应用服务凭证(X-App-Key/X-App-Secret),与容器自注册、/oauth/token 同一套,无独立发布令牌。来源优先级:参数 > 环境变量 > 配置文件。

  • AIGREGO_PORTAL_URL / --portal:门户地址(随环境变)
  • APP_KEY / --app-key:就是 listingKey 本身
  • APP_SECRET / --app-secret:平台签发,仅此一次明文可见。CI 里优先环境变量——命令行参数会进 ps 和构建日志
  • 配置文件(--config / $AIGREGO_REGISTRY_CONFIG / ./.aigrego-registry.env)只读 LISTING_KEY;出现 APP_SECRET 会告警并忽略(密钥不落盘;文件进 .gitignore)

越权语义:应用只能操作自己(--key 与凭证 listingKey 不一致 → 403);CLI 能写的 deployDescriptor 字段只有 image。

退出码与门禁

0 = 全部提交被接受(含治理变更 202 待审,非阻塞成功);1 = 认证/越权/校验拒绝/--wait 超时被驳回。--wait [秒](默认 1800)轮询等审批落地,把"审批通过且生效"纳入 CI 门禁。

CI 模板

照 references/templates/gitea-app-release.yaml:推 tag v* → buildx 多架构推镜像(tag = 去 v 版本号 + latest)→ publish --manifest --image --store --wait 600。仓库 secrets:APP_KEY/APP_SECRET/REGISTRY_USER/REGISTRY_TOKEN;manifest.version 与 tag 对齐。

2. 商店部署链路(APPSTORE-RUNTIME)

状态机 draft → published → installed → active。开发者交付物 = 镜像 + manifest;注册/上架/授权/安装/启停/卸载全部由平台在 console「应用市场」完成(租户侧注册/安装/卸载端点已下线)。

  • manifest 进平台双通道:publish --manifest(CLI/CI);或平台侧 console「应用中心」按向导导入 app.manifest.json——同一条 submit 管道:同一套校验、治理字段变更同样 202 进审核队列;两条路消费的都是应用仓库里这份文件
  • 上架双通道:平台管理员 console 操作;或应用凭证 + auto_publish(publish --store)
  • 授权即安装:授权变更自动建/停 app_installs 行;收权停用(数据保留);卸载回 published,安装行保留
  • FR-101 registries 表即白名单:安装按镜像 host 精确匹配 registries 行,未登记一律 403;空表 = 全拒(安全默认)。host 归一对齐 docker 语义:首段含 ./: 或为 localhost → 首段即 host;否则归一 docker.io
  • 安装 API 收窄:只收 {image, port, env}(全可选;image 回退 deployDescriptor.image,port 默认 8080);容器 spec 固定模板渲染,不暴露卷挂载/privileged/cap-add/network-mode
  • 安装等待:容器起后 30s 内必须完成自注册,否则 op failed(容器保留)——镜像必须基于 tickshield-node-runtime:base 内置注册循环
  • 启停 = 容器层 + 路由层墓碑/恢复;卸载 = 摘流 → Stop → Remove(可选 removeImage)→ 回 published
  • 全部操作审计留痕;docker.sock 缺失时运行时端点 503

平台侧待办清单(Agent 做不到,交付给用户/平台管理员):

  1. console「应用市场」新建应用 → 签发 APP_KEY/APP_SECRET(仅此一次明文)
  2. 应用详情开 auto_publish(要走 --store 自助上架时)
  3. console「仓库管理」登记镜像 host(私有仓库配凭证,可「测试登录」)
  4. 治理字段变更后的审核批准
  5. 租户授权(默认无 grant 行 = 全量授予)

3. 接入门验收清单(NFR-5 六项)

平台出口验收基准(scripts/verify-onboarding.sh,按其样式改写演练路由):

# 项 通过标准
1 Manifest 完整性 缺字段 manifest → 400;真实 manifest submit → 200 且 effective:true
2 四道闸 匿名 → 401;跨 aud 经网关 → 401;跨 aud 直连 → 403 gate=audience;无 scope → 403 gate=scope;member 调 admin → 403 gate=role;无页面权限 → 403 gate=acl;合法 → 200
3 租户隔离 跨租户读 = 空集;本租户写入后他租户不可见
4 SDK 握手 micro 应用宿主桥全流程(托管/host 页/握手/acl 显隐/callService)
5 制品交付 dist 上传/manifest 提交/register/镜像发布全链路
6 健康检查 /health 恰为 {"status","db"} 两键且均 "ok"(直连与经网关均 200)

4. 上线前验收矩阵(分层)

参照平台既有应用(secret-manager)的出口门:

  • 接入层:注册生效且心跳鲜活;/health 规范;manifest 逐字一致且不含 deployDescriptor;四道闸矩阵全绿(含伪造签名 401、错 aud、服务态写 403、无租户上下文 403);跨租户空集语义
  • 功能/安全层:业务 CRUD 与错误码;密文/敏感值零命中日志与审计;trace_id 端到端串联
  • 生态互操作层:跨应用 token-exchange(未声明 exchangeTargets → 拒绝;声明 + consent 后仅本租户可取)
  • 发布演练层:publish --dist 真实产物上传回读托管;--manifest 非治理字段直接生效 / scopes 变更进审核队列;演练幂等可重跑
  • 商店全链路:建应用签凭证 → 凭证 submit(draft 对租户不可见)→ auto_publish 关时凭证上架 403 / 开后直上 / 重复上架 409 → 授权语义(关授权即隐藏)→ 运行时安装(容器在跑 + 实例 active + /svc/<key>/health 200 + 授权租户自动有安装行)→ stop 摘流 / start 恢复 → 收权 disabled → 卸载回 published + 容器删除

5. quickstart catalog 物料(进平台服务目录时)

向平台仓 deploy/quickstart/catalog/apps/ 提交一对同名文件:

  1. <key>.yaml(compose 片段):必须含顶层 services: 与 networks:;container_name == listingKey;networks 挂平台网且 alias == listingKey;image 用 ${TS_IMAGE_PREFIX:-...}/tickshield-<key>:${TS_<KEY>_IMAGE:-${TS_VERSION}} 形态;environment 至少 PORT/PORTAL_API_URL/APP_KEY/APP_SECRET(secret 由 install.sh 统一生成落 .env,幂等复用)+ 业务 env 复用平台账本项;micro 应用挂门户托管产物卷(只读);禁止出现 docker.sock(静态门禁)
  2. <key>.json(app manifest):listingKey/name/version 必填,内容照 manifest 契约;安装时拷入 data/manifests/ 由 portal ScanManifests 建应用行(凭证经 APP_SECRET_LINES 注入),容器自注册建实例

6. 版本/tag 约定

git tag v<x.y.z> 触发 CI;镜像 tag = 去 v 的版本号 + 另挂 latest;manifest.version 与 tag 同步且 semver。latest_version 只在 semver 不减时拨动——回滚 = 显式发新版本号,不能靠重启旧容器。