feat(platform): add platform blueprint scaffolding
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
---
|
||||
name: Platform Execution
|
||||
about: Track platform delivery work with explicit planning, evidence, and rollback fields
|
||||
title: "[Platform] "
|
||||
labels: platform
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
## Summary
|
||||
<!-- Describe the scope, expected outcome, and impacted behavior. -->
|
||||
|
||||
## Type
|
||||
- [ ] Epic
|
||||
- [ ] Issue
|
||||
- [ ] Follow-up
|
||||
|
||||
## Priority
|
||||
- [ ] P0
|
||||
- [ ] P1
|
||||
- [ ] P2
|
||||
- [ ] P3
|
||||
|
||||
## Week
|
||||
<!-- Use YYYY-Www, for example 2026-W10. -->
|
||||
|
||||
## Repos touched
|
||||
- `ljwx-deploy`
|
||||
|
||||
## Files / Paths expected to change
|
||||
- `platform/...`
|
||||
|
||||
## Dependencies
|
||||
- None
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ]
|
||||
|
||||
## Evidence Required
|
||||
- [ ] PR link
|
||||
- [ ] Manifest or config diff
|
||||
- [ ] Runtime verification output
|
||||
- [ ] Docs updated if contract or behavior changed
|
||||
|
||||
## Rollback Plan
|
||||
- Revert target:
|
||||
- Re-apply or resync step:
|
||||
- Verification after rollback:
|
||||
|
||||
## Links
|
||||
<!-- Optional milestone, epic, dashboard, ArgoCD, or runbook links. -->
|
||||
@@ -7,6 +7,14 @@ GitOps 配置仓库,使用 ArgoCD 管理 Kubernetes 集群。
|
||||
- 新进程/新项目/老项目统一接入入口:`docs/START-HERE-GITOPS-ONBOARDING.md`
|
||||
- 自动扫描当前仓库是否已接入:`bash scripts/ops/scan-gitops-context.sh --repo "$PWD"`
|
||||
|
||||
## Platform Blueprint
|
||||
|
||||
- 平台装配蓝图入口:`docs/platform/README.md`
|
||||
- 平台配置与契约:`platform/`
|
||||
- 集成版本清单:`release/platform-version.yaml`
|
||||
- 平台 runbooks:`runbooks/`
|
||||
- 新集群初始化脚本骨架:`scripts/bootstrap/cluster-init.sh`
|
||||
|
||||
## Bid-MVP Factory Control Plane
|
||||
|
||||
- 运行验证:`bash scripts/verify.sh`
|
||||
@@ -30,6 +38,8 @@ GitOps 配置仓库,使用 ArgoCD 管理 Kubernetes 集群。
|
||||
- `apps/` - 应用配置
|
||||
- `cluster/` - 本地/dev 集群级别配置
|
||||
- `cluster-prod/` - 生产集群级别配置(prod app + prod promoter + registry secret sync)
|
||||
- `platform/` - 平台装配、契约、路由、知识治理配置
|
||||
- `runbooks/` - 平台级操作手册
|
||||
|
||||
## 生产发布闭环(当前)
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# Platform Blueprint
|
||||
|
||||
本目录用于沉淀 `ljwx-deploy` 作为平台集成仓时的装配规则、边界和执行方式。
|
||||
|
||||
原则:
|
||||
|
||||
1. `ljwx-deploy` 仍然是唯一部署真相源。
|
||||
2. `apps/`、`argocd-apps/`、`cluster/` 负责实际交付对象。
|
||||
3. `platform/` 负责平台级配置、契约、路由、知识治理和环境矩阵。
|
||||
4. 运行时行为必须能回溯到 Git 中的版本、策略、知识和能力契约。
|
||||
|
||||
建议阅读顺序:
|
||||
|
||||
1. `architecture-overview.md`
|
||||
2. `component-responsibilities.md`
|
||||
3. `platform-repo-design.md`
|
||||
4. `routing-strategy.md`
|
||||
5. `knowledge-pipeline.md`
|
||||
6. `deployment-environments.md`
|
||||
7. `bootstrap-and-migration.md`
|
||||
8. `engineering-execution-guide.md`
|
||||
9. `github-project-plan.md`
|
||||
@@ -0,0 +1,40 @@
|
||||
# Architecture Overview
|
||||
|
||||
## Goal
|
||||
|
||||
建立一个以客户产品为中心、可版本化、可回滚、可迁移的 AI 平台装配结构。
|
||||
|
||||
## Layers
|
||||
|
||||
| Layer | Components | Responsibility |
|
||||
| --- | --- | --- |
|
||||
| Product entry | `ljwx-chat`, website embeds | Conversation UI, customer-facing experience, tenant-aware entry |
|
||||
| Orchestration | `OpenClaw`, routing policy | Request classification, model selection, tool orchestration, execution audit |
|
||||
| Capability | `ljwx-core-api`, `n8n`, adapters | Deterministic business actions, authz, audit, external integrations |
|
||||
| Knowledge | knowledge pipeline, retrieval, Dify datasets | Normalize, classify, publish, invalidate, retrieve |
|
||||
| Platform | `ljwx-deploy`, ArgoCD, k3s, GitHub Actions | GitOps, overlays, release evidence, bootstrap, rollback |
|
||||
|
||||
## Source-of-Truth Rules
|
||||
|
||||
1. Deployment truth lives in `apps/`, `cluster/`, `argocd-apps/`, and `release/`.
|
||||
2. Platform behavior truth lives in `platform/`.
|
||||
3. Dify consumes processed knowledge; it is not the raw knowledge source.
|
||||
4. `n8n` executes workflows; it is not the business system of record.
|
||||
|
||||
## Request Flows
|
||||
|
||||
### General chat
|
||||
|
||||
`ljwx-chat -> OpenClaw -> route policy -> GPT -> response`
|
||||
|
||||
### Knowledge Q&A
|
||||
|
||||
`ljwx-chat -> OpenClaw -> retrieval/Dify dataset -> GPT/Claude -> cited response`
|
||||
|
||||
### Deterministic business action
|
||||
|
||||
`ljwx-chat -> OpenClaw -> capability registry -> core-api/n8n -> audited result -> response`
|
||||
|
||||
### Multi-step request
|
||||
|
||||
`ljwx-chat -> OpenClaw -> plan -> retrieval + tools + business action -> summarized response`
|
||||
@@ -0,0 +1,27 @@
|
||||
# Bootstrap And Migration
|
||||
|
||||
## Bootstrap Goal
|
||||
|
||||
Bring up a new server or cluster by replaying Git-managed baselines instead of rebuilding state manually.
|
||||
|
||||
## Bootstrap Sequence
|
||||
|
||||
1. prepare host and k3s
|
||||
2. prepare registry pull and secret management prerequisites
|
||||
3. run `scripts/bootstrap/cluster-init.sh`
|
||||
4. apply Argo bootstrap apps
|
||||
5. sync app overlays
|
||||
6. run smoke and evidence checks
|
||||
|
||||
## Migration Goal
|
||||
|
||||
Migrate workloads or contracts from legacy directories into Git-managed platform structure without changing the runtime truth boundary.
|
||||
|
||||
## Migration Rule
|
||||
|
||||
When moving legacy assets:
|
||||
|
||||
1. export contracts and examples first
|
||||
2. add platform config references
|
||||
3. validate Argo and smoke targets
|
||||
4. only then retire legacy entrypoints
|
||||
@@ -0,0 +1,20 @@
|
||||
# Component Responsibilities
|
||||
|
||||
| Component | Owns | Must Not Own |
|
||||
| --- | --- | --- |
|
||||
| `ljwx-chat` | UI, session presentation, channel entry, user-facing configuration | Orchestration policy, business workflows, knowledge source-of-truth |
|
||||
| `OpenClaw` | Routing, tool orchestration, execution traces, fallback policy | Persistent business state, raw knowledge governance, product backend ownership |
|
||||
| `GPT` | Default generation, tool-capable responses, fast general tasks | Workflow state, knowledge publishing, business truth |
|
||||
| `Claude` | Long-form analysis, design reasoning, complementary fallback | Default full traffic routing, workflow state |
|
||||
| `Dify` | Dataset-backed RAG consumption, agent UX where appropriate | Raw content source, core business logic, system-wide orchestration |
|
||||
| `n8n` | Deterministic workflow execution, retries, notifications, approvals | Product backend, complex reasoning, knowledge governance |
|
||||
| `ljwx-core-api` | Authn/authz, audit, capability gateway, business APIs | Frontend rendering, workflow editing |
|
||||
| `knowledge-processor` | Collection, normalization, classification, publish/invalidate | User-facing conversation UI |
|
||||
| `ljwx-deploy` | Integrated assembly, deployment structure, env overlays, runbooks | Product feature logic |
|
||||
|
||||
## Guardrails
|
||||
|
||||
1. Core business rules go to `ljwx-core-api`, not Dify or `n8n`.
|
||||
2. Routing logic goes to `platform/routing/`, not `ljwx-chat`.
|
||||
3. Capability ownership is explicit in `platform/assembly/capabilities.yaml`.
|
||||
4. Knowledge publication is reversible and auditable.
|
||||
@@ -0,0 +1,22 @@
|
||||
# Deployment Environments
|
||||
|
||||
## Current Profiles
|
||||
|
||||
`platform/assembly/env-matrix.yaml` is the platform-level matrix for environment and cluster behavior.
|
||||
|
||||
Initial profiles:
|
||||
|
||||
1. `local-k3s`
|
||||
2. `orbstack-k3s-cn`
|
||||
3. `prod-planned`
|
||||
|
||||
## Rules
|
||||
|
||||
1. Environment differences belong in profile files, not duplicated scripts.
|
||||
2. Dev can auto-sync; production should remain review-gated.
|
||||
3. Service map, smoke targets, public ingress, and release policy must be profile-driven.
|
||||
|
||||
## Secrets
|
||||
|
||||
Secrets are still externalized and must not be stored in this repository.
|
||||
This directory only records references and expected runtime contracts.
|
||||
@@ -0,0 +1,26 @@
|
||||
# Engineering Execution Guide
|
||||
|
||||
## Change Types
|
||||
|
||||
| Change type | Primary files |
|
||||
| --- | --- |
|
||||
| routing behavior | `platform/routing/`, `platform/contracts/`, `release/platform-version.yaml` |
|
||||
| knowledge publication | `platform/knowledge/`, knowledge source repos, publish workflow |
|
||||
| capability ownership | `platform/assembly/capabilities.yaml`, `platform/contracts/service-capability-map.yaml` |
|
||||
| deployment topology | `apps/`, `cluster/`, `argocd-apps/`, `release/` |
|
||||
|
||||
## Required PR Contents
|
||||
|
||||
Every platform PR should include:
|
||||
|
||||
1. scope and impacted environments
|
||||
2. changed source-of-truth files
|
||||
3. rollback method
|
||||
4. verification evidence
|
||||
5. follow-up tasks if the change is staged
|
||||
|
||||
## Minimum Review Standard
|
||||
|
||||
1. platform config and runtime behavior must match
|
||||
2. no component should silently gain a new responsibility boundary
|
||||
3. route and capability changes must remain explainable from Git
|
||||
@@ -0,0 +1,68 @@
|
||||
# GitHub Project Plan
|
||||
|
||||
## Purpose
|
||||
|
||||
Use GitHub Milestones, Epics, and Issues to plan platform work with clear ownership, scheduling, evidence, and rollback.
|
||||
|
||||
## Milestones
|
||||
|
||||
- Define a dated delivery checkpoint or platform outcome.
|
||||
- Group epics that must be completed together.
|
||||
- Carry exit criteria that can be verified from GitHub, manifests, and runtime evidence.
|
||||
|
||||
## Epics
|
||||
|
||||
- Represent one cross-repo capability slice under a milestone.
|
||||
- Link the affected repos, contracts, and operating procedures.
|
||||
- Decompose into issues that can be reviewed and rolled back independently.
|
||||
|
||||
## Issues
|
||||
|
||||
- Represent the smallest reviewable execution unit.
|
||||
- Change one coherent behavior, contract, or operational path.
|
||||
- Always include acceptance, evidence, and rollback before moving to done.
|
||||
|
||||
## Priority
|
||||
|
||||
- `P0`: production blocker, security issue, or same-day delivery requirement.
|
||||
- `P1`: current-week delivery target with direct dependency on active milestone work.
|
||||
- `P2`: planned follow-up work that should land in the next scheduled wave.
|
||||
- `P3`: backlog or optimization work without immediate release pressure.
|
||||
|
||||
## Week
|
||||
|
||||
- Use `YYYY-Www` format, for example `2026-W10`.
|
||||
- Re-evaluate the week whenever dependencies or scope change.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- List blocking issues, PRs, repos, manifests, or environment gates.
|
||||
- Make blockers explicit enough that another engineer can trace them without oral context.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Define observable done conditions, not implementation intent.
|
||||
- Prefer checks such as manifest diff merged, Argo sync healthy, smoke test pass, or contract updated.
|
||||
|
||||
## Evidence
|
||||
|
||||
- Attach the proof required to close the item.
|
||||
- Typical evidence includes PR links, screenshots, command output, ArgoCD state, and smoke-test results.
|
||||
|
||||
## Rollback
|
||||
|
||||
- Document the exact revert path before execution starts.
|
||||
- Include the repo or manifest to revert and the verification step after rollback.
|
||||
|
||||
## Recommended Planning Table
|
||||
|
||||
| Milestone | Epic | Issue | Priority | Week | Dependencies | Acceptance | Evidence | Rollback |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| Platform routing baseline | Shared ingress behavior | Add platform redirect middleware to affected app overlay | P1 | 2026-W10 | ingress profile update, service PR | dev ingress smoke passes and docs updated | PR link, sync result, curl output | revert PR, resync app, rerun smoke |
|
||||
|
||||
## Operating Rules
|
||||
|
||||
1. Every execution issue should belong to one epic and one milestone.
|
||||
2. Priority and week should be reviewed together; stale scheduling is a planning bug.
|
||||
3. Acceptance and evidence should be specific enough for async review.
|
||||
4. Rollback should be executable by an on-call engineer without extra explanation.
|
||||
@@ -0,0 +1,37 @@
|
||||
# Knowledge Pipeline
|
||||
|
||||
## Positioning
|
||||
|
||||
The raw source of truth stays in Git-managed repositories and approved source systems.
|
||||
Dify only consumes processed outputs.
|
||||
|
||||
## Pipeline
|
||||
|
||||
1. collect
|
||||
2. normalize
|
||||
3. classify
|
||||
4. deduplicate
|
||||
5. chunk
|
||||
6. tag
|
||||
7. apply ACL
|
||||
8. review
|
||||
9. publish
|
||||
10. invalidate or unpublish
|
||||
|
||||
## Publish Targets
|
||||
|
||||
- Dify datasets for product-facing retrieval
|
||||
- internal retrieval collections for platform and support use cases
|
||||
- customer-scoped retrieval collections when tenant isolation is required
|
||||
|
||||
## Invalidate Triggers
|
||||
|
||||
Publishing is not append-only. A document or chunk must be invalidated when:
|
||||
|
||||
1. source content is deleted
|
||||
2. ACL changes
|
||||
3. `review_status` moves out of `approved`
|
||||
4. `expires_at` is reached
|
||||
5. customer scope changes
|
||||
|
||||
The target configuration lives in `platform/knowledge/publish-targets.yaml`.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Platform Repo Design
|
||||
|
||||
## Purpose
|
||||
|
||||
`ljwx-deploy` is both:
|
||||
|
||||
1. the only deployment source of truth
|
||||
2. the integrated platform assembly repository
|
||||
|
||||
## Directory Roles
|
||||
|
||||
| Directory | Role |
|
||||
| --- | --- |
|
||||
| `apps/` | deployable application manifests and overlays |
|
||||
| `cluster/` | namespace and cluster baselines |
|
||||
| `argocd-apps/` | ArgoCD bootstrap and application ownership |
|
||||
| `release/` | queue, service maps, integrated platform versions |
|
||||
| `platform/` | contracts, routing, knowledge governance, env matrix, capability registry |
|
||||
| `runbooks/` | operational procedures for rollback, rebuild, republish |
|
||||
| `scripts/bootstrap/` | cluster bootstrap entrypoints |
|
||||
| `scripts/migrate/` | migration/export helpers |
|
||||
|
||||
## Change Rule
|
||||
|
||||
If a change affects runtime behavior but not image code, it should still be represented in this repository through:
|
||||
|
||||
- route config
|
||||
- capability registry
|
||||
- knowledge publish config
|
||||
- env profile
|
||||
- platform version
|
||||
@@ -0,0 +1,38 @@
|
||||
# Routing Strategy
|
||||
|
||||
## Design Split
|
||||
|
||||
Routing must be split into three layers:
|
||||
|
||||
1. `platform/routing/routes.*.yaml`
|
||||
Business-intent to route-policy mapping.
|
||||
2. `platform/routing/model-catalog.yaml`
|
||||
Provider and model capability metadata.
|
||||
3. `platform/assembly/capabilities.yaml`
|
||||
Tool and service dispatch registry.
|
||||
|
||||
This keeps strategy, model facts, and execution targets separate.
|
||||
|
||||
## Target Categories
|
||||
|
||||
| Category | Primary | Fallback | Tool orchestration |
|
||||
| --- | --- | --- | --- |
|
||||
| `general_chat` | GPT | Claude | no |
|
||||
| `long_analysis` | Claude | GPT | no |
|
||||
| `solution_design` | Claude | GPT | optional |
|
||||
| `tool_execution` | GPT | Claude | yes |
|
||||
| `knowledge_qa` | GPT | Claude | yes |
|
||||
| `multi_step` | GPT | Claude | yes |
|
||||
|
||||
## Decision Evidence
|
||||
|
||||
Every route decision must record:
|
||||
|
||||
1. matched rule IDs
|
||||
2. chosen provider and model
|
||||
3. enabled or skipped tools
|
||||
4. selected dataset scope
|
||||
5. fallback reason when triggered
|
||||
6. final cited documents or chunks for retrieval-backed answers
|
||||
|
||||
These fields are represented by `platform/contracts/router-request.schema.json` and `platform/contracts/router-decision.schema.json`.
|
||||
@@ -0,0 +1,10 @@
|
||||
# Platform Config
|
||||
|
||||
This directory stores platform-level assembly artifacts that are not application source code:
|
||||
|
||||
- `assembly/`: environment matrix and capability registry
|
||||
- `contracts/`: request, decision, and knowledge contracts
|
||||
- `routing/`: route policies and model catalog
|
||||
- `knowledge/`: source registry, taxonomy, ACL, publish targets
|
||||
|
||||
Deployment truth is still executed from `apps/`, `cluster/`, `argocd-apps/`, and `release/`.
|
||||
@@ -0,0 +1,33 @@
|
||||
version: 0.1.0
|
||||
|
||||
capabilities:
|
||||
kb.search:
|
||||
executor: retrieval
|
||||
owner_service: knowledge-processor
|
||||
auth: scoped
|
||||
audit: summary
|
||||
timeout_ms: 3000
|
||||
customer.lookup:
|
||||
executor: core-api
|
||||
owner_service: ljwx-core-api
|
||||
auth: required
|
||||
audit: full
|
||||
timeout_ms: 5000
|
||||
ticket.create:
|
||||
executor: n8n
|
||||
owner_service: n8n
|
||||
auth: required
|
||||
audit: full
|
||||
timeout_ms: 8000
|
||||
conversation.audit.write:
|
||||
executor: core-api
|
||||
owner_service: ljwx-core-api
|
||||
auth: internal
|
||||
audit: full
|
||||
timeout_ms: 2000
|
||||
route.execute:
|
||||
executor: openclaw
|
||||
owner_service: openclaw
|
||||
auth: internal
|
||||
audit: full
|
||||
timeout_ms: 15000
|
||||
@@ -0,0 +1,34 @@
|
||||
version: 0.1.0
|
||||
|
||||
profiles:
|
||||
local-k3s:
|
||||
cluster_class: k3s
|
||||
service_map_path: release/services.local-k3s.yaml
|
||||
smoke_targets_path: scripts/smoke/targets.local-k3s.json
|
||||
argocd_auto_sync: true
|
||||
public_https: true
|
||||
release_policy: auto-promote
|
||||
orbstack-k3s-cn:
|
||||
cluster_class: k3s
|
||||
service_map_path: release/services.orbstack-k3s-cn.yaml
|
||||
smoke_targets_path: scripts/smoke/targets.orbstack-k3s-cn.json
|
||||
argocd_auto_sync: true
|
||||
public_https: true
|
||||
release_policy: auto-promote
|
||||
prod-planned:
|
||||
cluster_class: k3s
|
||||
service_map_path: TBD
|
||||
smoke_targets_path: TBD
|
||||
argocd_auto_sync: false
|
||||
public_https: true
|
||||
release_policy: review-gated
|
||||
|
||||
environments:
|
||||
dev:
|
||||
default_profile: local-k3s
|
||||
sync_policy: automated
|
||||
approval_required: false
|
||||
prod:
|
||||
default_profile: prod-planned
|
||||
sync_policy: manual
|
||||
approval_required: true
|
||||
@@ -0,0 +1,50 @@
|
||||
$schema: https://json-schema.org/draft/2020-12/schema
|
||||
$id: https://lingjingwanxiang.cn/schemas/knowledge-chunk.schema.yaml
|
||||
title: KnowledgeChunk
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- chunk_id
|
||||
- document_id
|
||||
- chunk_index
|
||||
- visibility
|
||||
- token_count
|
||||
- text_hash
|
||||
properties:
|
||||
chunk_id:
|
||||
type: string
|
||||
minLength: 1
|
||||
document_id:
|
||||
type: string
|
||||
minLength: 1
|
||||
chunk_index:
|
||||
type: integer
|
||||
minimum: 0
|
||||
heading_path:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
visibility:
|
||||
type: string
|
||||
enum:
|
||||
- public
|
||||
- internal
|
||||
- customer
|
||||
customer_id:
|
||||
type:
|
||||
- string
|
||||
- "null"
|
||||
token_count:
|
||||
type: integer
|
||||
minimum: 1
|
||||
embedding_model:
|
||||
type:
|
||||
- string
|
||||
- "null"
|
||||
text_hash:
|
||||
type: string
|
||||
minLength: 1
|
||||
published_targets:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
@@ -0,0 +1,79 @@
|
||||
$schema: https://json-schema.org/draft/2020-12/schema
|
||||
$id: https://lingjingwanxiang.cn/schemas/knowledge-document.schema.yaml
|
||||
title: KnowledgeDocument
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- document_id
|
||||
- title
|
||||
- source_type
|
||||
- source_ref
|
||||
- visibility
|
||||
- doc_type
|
||||
- owner
|
||||
- version
|
||||
- review_status
|
||||
properties:
|
||||
document_id:
|
||||
type: string
|
||||
minLength: 1
|
||||
title:
|
||||
type: string
|
||||
minLength: 1
|
||||
source_type:
|
||||
type: string
|
||||
enum:
|
||||
- repo_doc
|
||||
- faq
|
||||
- delivery_doc
|
||||
- deployment_doc
|
||||
- support_record
|
||||
- partner_reference
|
||||
source_ref:
|
||||
type: string
|
||||
minLength: 1
|
||||
visibility:
|
||||
type: string
|
||||
enum:
|
||||
- public
|
||||
- internal
|
||||
- customer
|
||||
customer_id:
|
||||
type:
|
||||
- string
|
||||
- "null"
|
||||
doc_type:
|
||||
type: string
|
||||
owner:
|
||||
type: string
|
||||
version:
|
||||
type: string
|
||||
checksum:
|
||||
type:
|
||||
- string
|
||||
- "null"
|
||||
review_status:
|
||||
type: string
|
||||
enum:
|
||||
- draft
|
||||
- reviewing
|
||||
- approved
|
||||
- rejected
|
||||
approved_by:
|
||||
type:
|
||||
- string
|
||||
- "null"
|
||||
effective_from:
|
||||
type:
|
||||
- string
|
||||
- "null"
|
||||
format: date-time
|
||||
expires_at:
|
||||
type:
|
||||
- string
|
||||
- "null"
|
||||
format: date-time
|
||||
tags:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://lingjingwanxiang.cn/schemas/router-decision.schema.json",
|
||||
"title": "RouterDecision",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"route_id",
|
||||
"selected_provider",
|
||||
"selected_model",
|
||||
"tool_plan",
|
||||
"decision_basis"
|
||||
],
|
||||
"properties": {
|
||||
"route_id": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"selected_provider": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"selected_model": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"tool_plan": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"dataset_scopes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"fallback": {
|
||||
"type": ["object", "null"],
|
||||
"additionalProperties": false,
|
||||
"required": ["provider", "model", "condition"],
|
||||
"properties": {
|
||||
"provider": {
|
||||
"type": "string"
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"condition": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"decision_basis": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["matched_rules", "reasons"],
|
||||
"properties": {
|
||||
"matched_rules": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"reasons": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"skipped_tools": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://lingjingwanxiang.cn/schemas/router-request.schema.json",
|
||||
"title": "RouterRequest",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"trace_id",
|
||||
"channel",
|
||||
"task_type",
|
||||
"requires_tools",
|
||||
"requires_retrieval",
|
||||
"messages"
|
||||
],
|
||||
"properties": {
|
||||
"trace_id": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"tenant_id": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"channel": {
|
||||
"type": "string",
|
||||
"enum": ["web", "api", "im", "system"]
|
||||
},
|
||||
"task_type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"general_chat",
|
||||
"long_analysis",
|
||||
"solution_design",
|
||||
"tool_execution",
|
||||
"knowledge_qa",
|
||||
"multi_step"
|
||||
]
|
||||
},
|
||||
"requires_tools": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"requires_retrieval": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"latency_budget_ms": {
|
||||
"type": ["integer", "null"],
|
||||
"minimum": 1
|
||||
},
|
||||
"messages": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["role", "content"],
|
||||
"properties": {
|
||||
"role": {
|
||||
"type": "string",
|
||||
"enum": ["system", "user", "assistant", "tool"]
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"context_meta": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
version: 0.1.0
|
||||
|
||||
services:
|
||||
ljwx-chat:
|
||||
provides:
|
||||
- conversation.ui
|
||||
- session.preferences
|
||||
consumes:
|
||||
- route.execute
|
||||
openclaw:
|
||||
provides:
|
||||
- route.execute
|
||||
- response.stream
|
||||
- tool.orchestrate
|
||||
consumes:
|
||||
- kb.search
|
||||
- customer.lookup
|
||||
- ticket.create
|
||||
- conversation.audit.write
|
||||
ljwx-core-api:
|
||||
provides:
|
||||
- customer.lookup
|
||||
- conversation.audit.write
|
||||
- auth.check
|
||||
- tool.gateway
|
||||
consumes: []
|
||||
n8n:
|
||||
provides:
|
||||
- ticket.create
|
||||
- notification.dispatch
|
||||
- approval.flow
|
||||
consumes:
|
||||
- auth.check
|
||||
ljwx-dify:
|
||||
provides:
|
||||
- rag.answer
|
||||
- dataset.retrieve
|
||||
consumes:
|
||||
- kb.search
|
||||
knowledge-processor:
|
||||
provides:
|
||||
- kb.search
|
||||
- knowledge.publish
|
||||
- knowledge.invalidate
|
||||
consumes: []
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://lingjingwanxiang.cn/schemas/tool-result.schema.json",
|
||||
"title": "ToolResult",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["tool_name", "success"],
|
||||
"properties": {
|
||||
"tool_name": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"data": {
|
||||
"type": ["object", "array", "string", "number", "boolean", "null"]
|
||||
},
|
||||
"error": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"meta": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
"mock": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"reason": {
|
||||
"type": "string"
|
||||
},
|
||||
"audit_event_id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
version: 0.1.0
|
||||
|
||||
policies:
|
||||
public:
|
||||
allowed_targets:
|
||||
- dify-public
|
||||
- retrieval-public
|
||||
review_required: true
|
||||
internal:
|
||||
allowed_targets:
|
||||
- dify-internal
|
||||
- retrieval-internal
|
||||
review_required: true
|
||||
customer:
|
||||
allowed_targets:
|
||||
- retrieval-customer-template
|
||||
review_required: true
|
||||
requires_customer_id: true
|
||||
@@ -0,0 +1,49 @@
|
||||
version: 0.1.0
|
||||
|
||||
targets:
|
||||
dify-public:
|
||||
type: dify_dataset
|
||||
visibility: public
|
||||
dataset_name: public-knowledge
|
||||
invalidate_on:
|
||||
- source_deleted
|
||||
- acl_changed
|
||||
- review_revoked
|
||||
- expired
|
||||
dify-internal:
|
||||
type: dify_dataset
|
||||
visibility: internal
|
||||
dataset_name: internal-knowledge
|
||||
invalidate_on:
|
||||
- source_deleted
|
||||
- acl_changed
|
||||
- review_revoked
|
||||
- expired
|
||||
retrieval-public:
|
||||
type: retrieval_collection
|
||||
visibility: public
|
||||
collection_name: public-knowledge
|
||||
invalidate_on:
|
||||
- source_deleted
|
||||
- acl_changed
|
||||
- review_revoked
|
||||
- expired
|
||||
retrieval-internal:
|
||||
type: retrieval_collection
|
||||
visibility: internal
|
||||
collection_name: internal-knowledge
|
||||
invalidate_on:
|
||||
- source_deleted
|
||||
- acl_changed
|
||||
- review_revoked
|
||||
- expired
|
||||
retrieval-customer-template:
|
||||
type: retrieval_collection
|
||||
visibility: customer
|
||||
collection_name_template: customer-{customer_id}
|
||||
invalidate_on:
|
||||
- source_deleted
|
||||
- acl_changed
|
||||
- review_revoked
|
||||
- expired
|
||||
- customer_scope_changed
|
||||
@@ -0,0 +1,34 @@
|
||||
version: 0.1.0
|
||||
|
||||
sources:
|
||||
- id: ljwx-chat-docs
|
||||
type: git_repo
|
||||
repo: ljwx-chat
|
||||
include:
|
||||
- README.md
|
||||
- docs/**
|
||||
visibility: internal
|
||||
owner: team-ai
|
||||
- id: ljwx-deploy-docs
|
||||
type: git_repo
|
||||
repo: ljwx-deploy
|
||||
include:
|
||||
- docs/**
|
||||
- runbooks/**
|
||||
visibility: internal
|
||||
owner: team-platform
|
||||
- id: website-public-faq
|
||||
type: git_repo
|
||||
repo: ljwx-website
|
||||
include:
|
||||
- docs/**
|
||||
- content/faq/**
|
||||
visibility: public
|
||||
owner: team-product
|
||||
- id: customer-delivery-docs
|
||||
type: managed_upload
|
||||
repo: ljwx-knowledge
|
||||
include:
|
||||
- content/customers/**
|
||||
visibility: customer
|
||||
owner: team-delivery
|
||||
@@ -0,0 +1,25 @@
|
||||
version: 0.1.0
|
||||
|
||||
doc_types:
|
||||
- project_doc
|
||||
- solution_doc
|
||||
- delivery_doc
|
||||
- deployment_doc
|
||||
- readme
|
||||
- faq
|
||||
- support_record
|
||||
- ops_doc
|
||||
- partner_reference
|
||||
|
||||
product_areas:
|
||||
- chat
|
||||
- openclaw
|
||||
- dify
|
||||
- n8n
|
||||
- platform
|
||||
- knowledge
|
||||
|
||||
visibility_scopes:
|
||||
- public
|
||||
- internal
|
||||
- customer
|
||||
@@ -0,0 +1,25 @@
|
||||
version: 0.1.0
|
||||
|
||||
providers:
|
||||
openai:
|
||||
auth_secret_ref: runtime-llm/openai
|
||||
supports_streaming: true
|
||||
anthropic:
|
||||
auth_secret_ref: runtime-llm/anthropic
|
||||
supports_streaming: true
|
||||
|
||||
models:
|
||||
gpt-5.4:
|
||||
provider: openai
|
||||
tier: primary
|
||||
supports_tools: true
|
||||
supports_json_mode: true
|
||||
max_context_tokens: 200000
|
||||
cost_profile: standard
|
||||
claude-sonnet-4.6:
|
||||
provider: anthropic
|
||||
tier: complementary
|
||||
supports_tools: true
|
||||
supports_json_mode: true
|
||||
max_context_tokens: 200000
|
||||
cost_profile: analysis
|
||||
@@ -0,0 +1,66 @@
|
||||
version: 0.1.0
|
||||
default_route: general_chat
|
||||
|
||||
routes:
|
||||
- id: general_chat
|
||||
match:
|
||||
task_type: general_chat
|
||||
primary:
|
||||
provider: openai
|
||||
model: gpt-5.4
|
||||
fallback:
|
||||
provider: anthropic
|
||||
model: claude-sonnet-4.6
|
||||
condition: provider_error_or_timeout
|
||||
tool_policy: none
|
||||
|
||||
- id: long_analysis
|
||||
match:
|
||||
task_type: long_analysis
|
||||
primary:
|
||||
provider: anthropic
|
||||
model: claude-sonnet-4.6
|
||||
fallback:
|
||||
provider: openai
|
||||
model: gpt-5.4
|
||||
condition: provider_error_or_timeout
|
||||
tool_policy: none
|
||||
|
||||
- id: knowledge_qa
|
||||
match:
|
||||
task_type: knowledge_qa
|
||||
primary:
|
||||
provider: openai
|
||||
model: gpt-5.4
|
||||
fallback:
|
||||
provider: anthropic
|
||||
model: claude-sonnet-4.6
|
||||
condition: provider_error_or_timeout
|
||||
tool_policy: retrieval_only
|
||||
dataset_scopes:
|
||||
- public
|
||||
- internal
|
||||
|
||||
- id: tool_execution
|
||||
match:
|
||||
task_type: tool_execution
|
||||
primary:
|
||||
provider: openai
|
||||
model: gpt-5.4
|
||||
fallback:
|
||||
provider: anthropic
|
||||
model: claude-sonnet-4.6
|
||||
condition: provider_error_or_timeout
|
||||
tool_policy: capability_registry
|
||||
|
||||
- id: multi_step
|
||||
match:
|
||||
task_type: multi_step
|
||||
primary:
|
||||
provider: openai
|
||||
model: gpt-5.4
|
||||
fallback:
|
||||
provider: anthropic
|
||||
model: claude-sonnet-4.6
|
||||
condition: provider_error_or_timeout
|
||||
tool_policy: capability_registry
|
||||
@@ -0,0 +1,54 @@
|
||||
version: 0.1.0
|
||||
default_route: general_chat
|
||||
|
||||
routes:
|
||||
- id: general_chat
|
||||
match:
|
||||
task_type: general_chat
|
||||
primary:
|
||||
provider: openai
|
||||
model: gpt-5.4
|
||||
fallback:
|
||||
provider: anthropic
|
||||
model: claude-sonnet-4.6
|
||||
condition: provider_error_or_timeout
|
||||
tool_policy: none
|
||||
|
||||
- id: long_analysis
|
||||
match:
|
||||
task_type: long_analysis
|
||||
primary:
|
||||
provider: anthropic
|
||||
model: claude-sonnet-4.6
|
||||
fallback:
|
||||
provider: openai
|
||||
model: gpt-5.4
|
||||
condition: provider_error_or_timeout
|
||||
tool_policy: none
|
||||
|
||||
- id: knowledge_qa
|
||||
match:
|
||||
task_type: knowledge_qa
|
||||
primary:
|
||||
provider: openai
|
||||
model: gpt-5.4
|
||||
fallback:
|
||||
provider: anthropic
|
||||
model: claude-sonnet-4.6
|
||||
condition: provider_error_or_timeout
|
||||
tool_policy: retrieval_only
|
||||
dataset_scopes:
|
||||
- public
|
||||
|
||||
- id: tool_execution
|
||||
match:
|
||||
task_type: tool_execution
|
||||
primary:
|
||||
provider: openai
|
||||
model: gpt-5.4
|
||||
fallback:
|
||||
provider: anthropic
|
||||
model: claude-sonnet-4.6
|
||||
condition: provider_error_or_timeout
|
||||
tool_policy: capability_registry
|
||||
approval_gate: required_for_high_risk_actions
|
||||
@@ -0,0 +1,44 @@
|
||||
platform_version: 0.1.0-draft
|
||||
release_channel: dev
|
||||
released_at: null
|
||||
|
||||
components:
|
||||
ljwx-chat:
|
||||
delivery_ref: release/services.yaml
|
||||
config_ref: apps/ljwx-chat/overlays/ljwx-chat-dev/kustomization.yaml
|
||||
image: TBD
|
||||
openclaw:
|
||||
delivery_ref: planned-import
|
||||
config_ref: planned-import
|
||||
image: TBD
|
||||
ljwx-core-api:
|
||||
delivery_ref: planned
|
||||
config_ref: planned
|
||||
image: TBD
|
||||
ljwx-dify:
|
||||
delivery_ref: release/services.yaml
|
||||
config_ref: apps/ljwx-dify
|
||||
image: TBD
|
||||
n8n:
|
||||
delivery_ref: planned-import
|
||||
config_ref: planned-import
|
||||
image: TBD
|
||||
knowledge-processor:
|
||||
delivery_ref: planned
|
||||
config_ref: planned
|
||||
image: TBD
|
||||
|
||||
config_versions:
|
||||
routing_rules: platform/routing/routes.dev.yaml@0.1.0
|
||||
model_catalog: platform/routing/model-catalog.yaml@0.1.0
|
||||
capability_registry: platform/assembly/capabilities.yaml@0.1.0
|
||||
service_capability_map: platform/contracts/service-capability-map.yaml@0.1.0
|
||||
knowledge_document_schema: platform/contracts/knowledge-document.schema.yaml@0.1.0
|
||||
knowledge_chunk_schema: platform/contracts/knowledge-chunk.schema.yaml@0.1.0
|
||||
workflow_template_version: tracked-in-ljwx-workflow-templates
|
||||
core_contract_version: 0.1.0
|
||||
env_profile: platform/assembly/env-matrix.yaml#local-k3s
|
||||
|
||||
notes:
|
||||
- This file versions integrated behavior, not only image references.
|
||||
- Update this file whenever routing, capability, or knowledge behavior changes.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Knowledge Republish
|
||||
|
||||
## Use Cases
|
||||
|
||||
Use this runbook when processed knowledge must be republished or invalidated without changing product code.
|
||||
|
||||
## Triggers
|
||||
|
||||
1. source file deleted
|
||||
2. ACL changed
|
||||
3. review revoked
|
||||
4. document expired
|
||||
5. customer scope changed
|
||||
|
||||
## Steps
|
||||
|
||||
1. confirm source registry and publish target entries
|
||||
2. rebuild normalized and chunk manifests
|
||||
3. invalidate target datasets or collections
|
||||
4. republish approved content
|
||||
5. verify retrieval scope and sample queries
|
||||
|
||||
## Evidence
|
||||
|
||||
- publish job run ID
|
||||
- invalidated dataset or collection IDs
|
||||
- sample retrieval verification
|
||||
@@ -0,0 +1,20 @@
|
||||
# Node Rebuild
|
||||
|
||||
## Goal
|
||||
|
||||
Rebuild a cluster node or a small cluster using Git-managed bootstrap and application baselines.
|
||||
|
||||
## Steps
|
||||
|
||||
1. prepare host
|
||||
2. install k3s
|
||||
3. restore pull secrets and external secrets prerequisites
|
||||
4. run `scripts/bootstrap/cluster-init.sh --env dev --profile <profile> --apply`
|
||||
5. verify Argo apps
|
||||
6. verify ingress and smoke targets
|
||||
|
||||
## Exit Criteria
|
||||
|
||||
- Argo apps are healthy
|
||||
- promoted images match overlays
|
||||
- smoke checks pass
|
||||
@@ -0,0 +1,28 @@
|
||||
# Platform Rollback
|
||||
|
||||
## Scope
|
||||
|
||||
Use this runbook when a platform release changes runtime behavior and must be reverted.
|
||||
|
||||
## Inputs
|
||||
|
||||
1. target `platform_version`
|
||||
2. affected services
|
||||
3. reason for rollback
|
||||
4. verification owner
|
||||
|
||||
## Steps
|
||||
|
||||
1. identify the last known-good `release/platform-version.yaml`
|
||||
2. re-enqueue last known-good image digests where required
|
||||
3. restore route, capability, and knowledge config refs
|
||||
4. merge or revert via PR
|
||||
5. wait for Argo `Synced/Healthy`
|
||||
6. run smoke and record evidence
|
||||
|
||||
## Evidence
|
||||
|
||||
- rollback PR
|
||||
- Argo health snapshot
|
||||
- smoke result
|
||||
- updated evidence record
|
||||
Executable
+111
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# This script bootstraps a cluster using Git-managed Argo applications.
|
||||
|
||||
ENV_NAME=""
|
||||
PROFILE_NAME=""
|
||||
APPLY_MODE="false"
|
||||
SKIP_TLS="false"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
用法:
|
||||
bash scripts/bootstrap/cluster-init.sh --env <env> --profile <profile> [--apply] [--skip-tls]
|
||||
|
||||
示例:
|
||||
bash scripts/bootstrap/cluster-init.sh --env dev --profile local-k3s
|
||||
bash scripts/bootstrap/cluster-init.sh --env dev --profile local-k3s --apply
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--env)
|
||||
ENV_NAME="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--profile)
|
||||
PROFILE_NAME="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--apply)
|
||||
APPLY_MODE="true"
|
||||
shift
|
||||
;;
|
||||
--skip-tls)
|
||||
SKIP_TLS="true"
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "未知参数: $1" >&2
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$ENV_NAME" || -z "$PROFILE_NAME" ]]; then
|
||||
echo "必须同时提供 --env 和 --profile。" >&2
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
ENV_MATRIX="$REPO_ROOT/platform/assembly/env-matrix.yaml"
|
||||
ARGO_CLUSTER_BOOTSTRAP="$REPO_ROOT/argocd-apps/00-cluster-bootstrap.yaml"
|
||||
ARGO_APPS_BOOTSTRAP="$REPO_ROOT/argocd-apps/01-apps-bootstrap.yaml"
|
||||
TLS_APP="$REPO_ROOT/argocd-apps/03-cert-manager-dev.yaml"
|
||||
TLS_CONFIG_APP="$REPO_ROOT/argocd-apps/04-cert-manager-config-dev.yaml"
|
||||
|
||||
for required_file in \
|
||||
"$ENV_MATRIX" \
|
||||
"$ARGO_CLUSTER_BOOTSTRAP" \
|
||||
"$ARGO_APPS_BOOTSTRAP"; do
|
||||
if [[ ! -f "$required_file" ]]; then
|
||||
echo "缺少必需文件: $required_file" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "准备初始化集群。"
|
||||
echo "环境: $ENV_NAME"
|
||||
echo "Profile: $PROFILE_NAME"
|
||||
echo "应用模式: $APPLY_MODE"
|
||||
echo "跳过 TLS: $SKIP_TLS"
|
||||
|
||||
echo "校验完成,计划执行以下步骤:"
|
||||
echo "1. 应用 cluster bootstrap Argo app"
|
||||
echo "2. 应用 apps bootstrap Argo app"
|
||||
if [[ "$SKIP_TLS" != "true" ]]; then
|
||||
echo "3. 应用 cert-manager 与 TLS 配置"
|
||||
fi
|
||||
echo "4. 等待 Argo 接管后续资源"
|
||||
echo "5. 运行 smoke 和证据校验"
|
||||
|
||||
if [[ "$APPLY_MODE" != "true" ]]; then
|
||||
echo "当前为预览模式,未执行 kubectl apply。"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! command -v kubectl >/dev/null 2>&1; then
|
||||
echo "未找到 kubectl,无法执行 apply。" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "开始应用 Argo bootstrap 资源。"
|
||||
kubectl apply -f "$ARGO_CLUSTER_BOOTSTRAP"
|
||||
kubectl apply -f "$ARGO_APPS_BOOTSTRAP"
|
||||
|
||||
if [[ "$SKIP_TLS" != "true" ]]; then
|
||||
kubectl apply -f "$TLS_APP"
|
||||
kubectl apply -f "$TLS_CONFIG_APP"
|
||||
fi
|
||||
|
||||
echo "初始化命令已执行。请随后检查 Argo、Smoke 和 Evidence 状态。"
|
||||
Executable
+56
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# This script exports runtime-contract directories for migration review.
|
||||
|
||||
OUT_DIR=""
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
用法:
|
||||
bash scripts/migrate/export-runtime-contract.sh --out <output-dir>
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--out)
|
||||
OUT_DIR="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "未知参数: $1" >&2
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$OUT_DIR" ]]; then
|
||||
echo "必须提供导出目录。" >&2
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
DEST_DIR="$(cd "$REPO_ROOT" && mkdir -p "$OUT_DIR" && cd "$OUT_DIR" && pwd)"
|
||||
MANIFEST_FILE="$DEST_DIR/exported-runtime-contracts.txt"
|
||||
|
||||
echo "开始导出 runtime-contract 目录到: $DEST_DIR"
|
||||
: > "$MANIFEST_FILE"
|
||||
|
||||
while IFS= read -r contract_dir; do
|
||||
rel_path="${contract_dir#"$REPO_ROOT"/}"
|
||||
target_dir="$DEST_DIR/$rel_path"
|
||||
mkdir -p "$(dirname "$target_dir")"
|
||||
cp -R "$contract_dir" "$target_dir"
|
||||
echo "$rel_path" >> "$MANIFEST_FILE"
|
||||
done < <(find "$REPO_ROOT/apps" -type d -name runtime-contract | sort)
|
||||
|
||||
echo "导出完成,清单文件: $MANIFEST_FILE"
|
||||
Executable
+519
@@ -0,0 +1,519 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
ENV_NAME="dev"
|
||||
SERVICES_CSV="ljwx-website,ljwx-dify,ljwx-chat"
|
||||
TARGETS_FILE="scripts/smoke/targets.local-k3s.json"
|
||||
WORKFLOW_TIMEOUT_SEC=3600
|
||||
QUEUE_TIMEOUT_SEC=1800
|
||||
PROMOTE_TIMEOUT_SEC=1800
|
||||
POLL_INTERVAL_SEC=20
|
||||
SKIP_TRIGGER="false"
|
||||
SKIP_SMOKE="false"
|
||||
SMOKE_DRY_RUN="false"
|
||||
ALLOW_SMOKE_FAILURES="false"
|
||||
ALLOW_PROMOTE_PENDING="false"
|
||||
|
||||
log() {
|
||||
echo "[smart-cs-e2e] $*"
|
||||
}
|
||||
|
||||
err() {
|
||||
echo "[smart-cs-e2e][错误] $*" >&2
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
用途:
|
||||
一键执行 Smart CS 端到端验证:
|
||||
Gate -> 三仓 build-and-enqueue -> queue/promote -> ArgoCD -> smoke -> evidence
|
||||
|
||||
用法:
|
||||
bash scripts/ops/e2e-smart-cs.sh [options]
|
||||
|
||||
选项:
|
||||
--env <dev|demo|prod> 目标环境(默认: dev)
|
||||
--services <csv> 服务列表(默认: ljwx-website,ljwx-dify,ljwx-chat)
|
||||
--targets <path> smoke targets 文件(默认: scripts/smoke/targets.local-k3s.json)
|
||||
--workflow-timeout-sec <n> 每个 workflow 等待秒数(默认: 3600)
|
||||
--queue-timeout-sec <n> 等待 queue 入列秒数(默认: 1800)
|
||||
--promote-timeout-sec <n> 等待 promoted 秒数(默认: 1800)
|
||||
--poll-interval-sec <n> 轮询间隔秒数(默认: 20)
|
||||
--skip-trigger 跳过触发三仓 workflow(只做后半段验证)
|
||||
--skip-smoke 跳过 smoke 执行
|
||||
--smoke-dry-run smoke 使用 --dry-run
|
||||
--allow-smoke-failures smoke 使用 --allow-failures
|
||||
--allow-promote-pending promoted 超时不失败(仅告警)
|
||||
-h, --help 显示帮助
|
||||
USAGE
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--env)
|
||||
ENV_NAME="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--services)
|
||||
SERVICES_CSV="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--targets)
|
||||
TARGETS_FILE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--workflow-timeout-sec)
|
||||
WORKFLOW_TIMEOUT_SEC="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--queue-timeout-sec)
|
||||
QUEUE_TIMEOUT_SEC="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--promote-timeout-sec)
|
||||
PROMOTE_TIMEOUT_SEC="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--poll-interval-sec)
|
||||
POLL_INTERVAL_SEC="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--skip-trigger)
|
||||
SKIP_TRIGGER="true"
|
||||
shift
|
||||
;;
|
||||
--skip-smoke)
|
||||
SKIP_SMOKE="true"
|
||||
shift
|
||||
;;
|
||||
--smoke-dry-run)
|
||||
SMOKE_DRY_RUN="true"
|
||||
shift
|
||||
;;
|
||||
--allow-smoke-failures)
|
||||
ALLOW_SMOKE_FAILURES="true"
|
||||
shift
|
||||
;;
|
||||
--allow-promote-pending)
|
||||
ALLOW_PROMOTE_PENDING="true"
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
err "未知参数: $1"
|
||||
usage
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
required_cmds=(jq kubectl python3 uvx)
|
||||
if [[ "$SKIP_TRIGGER" != "true" ]]; then
|
||||
required_cmds+=(gh)
|
||||
fi
|
||||
|
||||
for cmd in "${required_cmds[@]}"; do
|
||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||
err "缺少命令: $cmd"
|
||||
exit 2
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$SKIP_TRIGGER" != "true" ]]; then
|
||||
if ! gh auth status >/dev/null 2>&1; then
|
||||
err "gh 未登录,请先执行: gh auth login"
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ ! -f "release/queue.yaml" ]]; then
|
||||
err "当前目录不是 ljwx-deploy 仓库根目录"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ ! -f "$TARGETS_FILE" ]]; then
|
||||
err "smoke targets 文件不存在: $TARGETS_FILE"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
declare -A REPO_BY_SERVICE=(
|
||||
["ljwx-website"]="BrunoGao/ljwx-website"
|
||||
["ljwx-dify"]="BrunoGao/ljwx-dify"
|
||||
["ljwx-chat"]="BrunoGao/ljwx-chat"
|
||||
)
|
||||
|
||||
IFS=',' read -r -a SERVICES <<< "$SERVICES_CSV"
|
||||
if [[ "${#SERVICES[@]}" -eq 0 ]]; then
|
||||
err "--services 不能为空"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
for service in "${SERVICES[@]}"; do
|
||||
if [[ -z "${REPO_BY_SERVICE[$service]:-}" ]]; then
|
||||
err "未配置仓库映射的服务: $service"
|
||||
exit 2
|
||||
fi
|
||||
done
|
||||
|
||||
resolve_run_id() {
|
||||
local repo="$1"
|
||||
local since_ts="$2"
|
||||
local run_id=""
|
||||
local deadline=$((SECONDS + 180))
|
||||
|
||||
while (( SECONDS < deadline )); do
|
||||
run_id="$(gh run list -R "$repo" -w build-and-enqueue --event workflow_dispatch --json databaseId,createdAt -L 30 \
|
||||
| jq -r --arg since "$since_ts" '[.[] | select(.createdAt >= $since)] | sort_by(.createdAt) | reverse | .[0].databaseId // empty')"
|
||||
if [[ -n "$run_id" ]]; then
|
||||
echo "$run_id"
|
||||
return 0
|
||||
fi
|
||||
sleep 3
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
wait_run_success() {
|
||||
local repo="$1"
|
||||
local run_id="$2"
|
||||
local deadline=$((SECONDS + WORKFLOW_TIMEOUT_SEC))
|
||||
|
||||
while (( SECONDS < deadline )); do
|
||||
local info
|
||||
info="$(gh run view "$run_id" -R "$repo" --json status,conclusion,url 2>/dev/null || true)"
|
||||
local status conclusion url
|
||||
status="$(jq -r '.status // empty' <<<"$info")"
|
||||
conclusion="$(jq -r '.conclusion // empty' <<<"$info")"
|
||||
url="$(jq -r '.url // empty' <<<"$info")"
|
||||
|
||||
if [[ "$status" == "completed" ]]; then
|
||||
if [[ "$conclusion" == "success" ]]; then
|
||||
log "workflow 成功: $repo run_id=$run_id"
|
||||
return 0
|
||||
fi
|
||||
err "workflow 失败: $repo run_id=$run_id conclusion=$conclusion url=$url"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log "等待 workflow 完成: $repo run_id=$run_id status=${status:-unknown}"
|
||||
sleep "$POLL_INTERVAL_SEC"
|
||||
done
|
||||
|
||||
err "workflow 等待超时: $repo run_id=$run_id"
|
||||
return 1
|
||||
}
|
||||
|
||||
wait_queue_new_entries() {
|
||||
local since_ts="$1"
|
||||
local deadline=$((SECONDS + QUEUE_TIMEOUT_SEC))
|
||||
|
||||
while (( SECONDS < deadline )); do
|
||||
if uvx --with pyyaml python - "$since_ts" "$ENV_NAME" "$SERVICES_CSV" <<'PY'
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
import yaml
|
||||
|
||||
since = datetime.fromisoformat(sys.argv[1].replace("Z", "+00:00")).astimezone(timezone.utc)
|
||||
env = sys.argv[2]
|
||||
services = [x for x in sys.argv[3].split(",") if x]
|
||||
|
||||
with open("release/queue.yaml", "r", encoding="utf-8") as f:
|
||||
q = yaml.safe_load(f) or {}
|
||||
|
||||
states = ["pending", "promoted", "failed", "superseded"]
|
||||
missing = []
|
||||
for svc in services:
|
||||
latest = None
|
||||
for st in states:
|
||||
for item in q.get(st, []) or []:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
if item.get("service") != svc or item.get("env") != env:
|
||||
continue
|
||||
created_at = item.get("createdAt") or ""
|
||||
try:
|
||||
ts = datetime.fromisoformat(str(created_at).replace("Z", "+00:00")).astimezone(timezone.utc)
|
||||
except ValueError:
|
||||
continue
|
||||
if ts < since:
|
||||
continue
|
||||
if latest is None or ts > latest[0]:
|
||||
latest = (ts, st, item.get("id", ""))
|
||||
if latest is None:
|
||||
missing.append(svc)
|
||||
else:
|
||||
print(f"QUEUE_OK {svc} state={latest[1]} id={latest[2]}")
|
||||
|
||||
if missing:
|
||||
print("QUEUE_MISSING " + ",".join(missing), file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
PY
|
||||
then
|
||||
return 0
|
||||
fi
|
||||
|
||||
log "等待 queue 入列..."
|
||||
sleep "$POLL_INTERVAL_SEC"
|
||||
done
|
||||
|
||||
err "queue 入列等待超时"
|
||||
return 1
|
||||
}
|
||||
|
||||
wait_promoted_entries() {
|
||||
local since_ts="$1"
|
||||
local deadline=$((SECONDS + PROMOTE_TIMEOUT_SEC))
|
||||
|
||||
while (( SECONDS < deadline )); do
|
||||
if uvx --with pyyaml python - "$since_ts" "$ENV_NAME" "$SERVICES_CSV" <<'PY'
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
import yaml
|
||||
|
||||
since = datetime.fromisoformat(sys.argv[1].replace("Z", "+00:00")).astimezone(timezone.utc)
|
||||
env = sys.argv[2]
|
||||
services = [x for x in sys.argv[3].split(",") if x]
|
||||
|
||||
with open("release/queue.yaml", "r", encoding="utf-8") as f:
|
||||
q = yaml.safe_load(f) or {}
|
||||
|
||||
missing = []
|
||||
for svc in services:
|
||||
latest = None
|
||||
for item in q.get("promoted", []) or []:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
if item.get("service") != svc or item.get("env") != env:
|
||||
continue
|
||||
created_at = item.get("createdAt") or ""
|
||||
try:
|
||||
ts = datetime.fromisoformat(str(created_at).replace("Z", "+00:00")).astimezone(timezone.utc)
|
||||
except ValueError:
|
||||
continue
|
||||
if ts < since:
|
||||
continue
|
||||
if latest is None or ts > latest[0]:
|
||||
latest = (ts, item.get("id", ""), item.get("promotedAt", ""))
|
||||
|
||||
if latest is None:
|
||||
missing.append(svc)
|
||||
else:
|
||||
print(f"PROMOTED_OK {svc} id={latest[1]} promotedAt={latest[2]}")
|
||||
|
||||
if missing:
|
||||
print("PROMOTED_MISSING " + ",".join(missing), file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
PY
|
||||
then
|
||||
return 0
|
||||
fi
|
||||
|
||||
log "等待 promoter 出列..."
|
||||
sleep "$POLL_INTERVAL_SEC"
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
check_argocd_apps() {
|
||||
log "检查 ArgoCD 应用健康状态"
|
||||
|
||||
local app_json
|
||||
app_json="$(uvx --with pyyaml python - "$ENV_NAME" "$SERVICES_CSV" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
import yaml
|
||||
|
||||
env = sys.argv[1]
|
||||
services = [x for x in sys.argv[2].split(',') if x]
|
||||
|
||||
with open("release/services.yaml", "r", encoding="utf-8") as f:
|
||||
data = yaml.safe_load(f) or {}
|
||||
|
||||
apps = []
|
||||
for svc in services:
|
||||
app = (
|
||||
data.get("services", {})
|
||||
.get(svc, {})
|
||||
.get("envs", {})
|
||||
.get(env, {})
|
||||
.get("argocdApp")
|
||||
)
|
||||
if not app:
|
||||
raise SystemExit(f"missing argocdApp mapping for service={svc} env={env}")
|
||||
apps.append(app)
|
||||
|
||||
print(json.dumps(apps))
|
||||
PY
|
||||
)"
|
||||
|
||||
mapfile -t app_names < <(jq -r '.[]' <<<"$app_json")
|
||||
for app in "${app_names[@]}"; do
|
||||
local sync health
|
||||
sync="$(kubectl -n argocd get applications.argoproj.io "$app" -o jsonpath='{.status.sync.status}')"
|
||||
health="$(kubectl -n argocd get applications.argoproj.io "$app" -o jsonpath='{.status.health.status}')"
|
||||
log "argocd app=$app sync=$sync health=$health"
|
||||
if [[ "$sync" != "Synced" || "$health" != "Healthy" ]]; then
|
||||
err "ArgoCD 状态异常: app=$app sync=$sync health=$health"
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
run_smoke_subset() {
|
||||
local tmp_targets
|
||||
tmp_targets="$(mktemp /tmp/smart-cs-smoke-targets.XXXXXX.json)"
|
||||
|
||||
local services_json
|
||||
services_json="$(printf '%s\n' "${SERVICES[@]}" | jq -R . | jq -s .)"
|
||||
|
||||
jq --argjson services "$services_json" '{profile: .profile, targets: [ .targets[] | select(.service as $s | $services | index($s)) ]}' "$TARGETS_FILE" > "$tmp_targets"
|
||||
|
||||
local target_count
|
||||
target_count="$(jq '.targets | length' "$tmp_targets")"
|
||||
if [[ "$target_count" -lt 1 ]]; then
|
||||
err "过滤后的 smoke targets 为空: $tmp_targets"
|
||||
rm -f "$tmp_targets"
|
||||
return 1
|
||||
fi
|
||||
|
||||
for service in "${SERVICES[@]}"; do
|
||||
if ! jq -e --arg svc "$service" '.targets | any(.service == $svc)' "$tmp_targets" >/dev/null; then
|
||||
err "smoke targets 缺少服务: $service"
|
||||
rm -f "$tmp_targets"
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
local args=(python3 scripts/smoke/run_smoke.py --targets "$tmp_targets" --queue release/queue.yaml)
|
||||
if [[ "$SMOKE_DRY_RUN" == "true" ]]; then
|
||||
args+=(--dry-run)
|
||||
fi
|
||||
if [[ "$ALLOW_SMOKE_FAILURES" == "true" ]]; then
|
||||
args+=(--allow-failures)
|
||||
fi
|
||||
|
||||
log "执行 smoke: ${args[*]}"
|
||||
"${args[@]}"
|
||||
|
||||
rm -f "$tmp_targets"
|
||||
return 0
|
||||
}
|
||||
|
||||
print_evidence_summary() {
|
||||
uvx --with pyyaml python - "$ENV_NAME" "$SERVICES_CSV" <<'PY'
|
||||
import glob
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
import yaml
|
||||
|
||||
env = sys.argv[1]
|
||||
services = [x for x in sys.argv[2].split(',') if x]
|
||||
|
||||
records = []
|
||||
for path in glob.glob("evidence/records/*.yaml"):
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
data = yaml.safe_load(f) or {}
|
||||
if data.get("service") in services and data.get("env") == env:
|
||||
ts_text = (data.get("tests", {}).get("smoke", {}).get("checkedAt")
|
||||
or data.get("deploy", {}).get("syncedAt")
|
||||
or data.get("promotedAt")
|
||||
or "1970-01-01T00:00:00Z")
|
||||
try:
|
||||
ts = datetime.fromisoformat(str(ts_text).replace("Z", "+00:00")).astimezone(timezone.utc)
|
||||
except ValueError:
|
||||
ts = datetime(1970, 1, 1, tzinfo=timezone.utc)
|
||||
records.append((data.get("service"), ts, data))
|
||||
|
||||
print("EVIDENCE_SUMMARY")
|
||||
for svc in services:
|
||||
cand = [item for item in records if item[0] == svc]
|
||||
if not cand:
|
||||
print(f"- {svc}: NO_RECORD")
|
||||
continue
|
||||
_, _, latest = sorted(cand, key=lambda x: x[1], reverse=True)[0]
|
||||
smoke = (((latest.get("tests") or {}).get("smoke") or {}).get("status") or "unknown")
|
||||
queue_id = (((latest.get("deploy") or {}).get("queueId")) or "")
|
||||
checked = (((latest.get("tests") or {}).get("smoke") or {}).get("checkedAt") or "")
|
||||
print(f"- {svc}: smoke={smoke} queue_id={queue_id} checked_at={checked}")
|
||||
PY
|
||||
}
|
||||
|
||||
START_TS="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
||||
QUEUE_REFERENCE_TS="$START_TS"
|
||||
log "开始执行 E2E,env=$ENV_NAME services=$SERVICES_CSV start=$START_TS"
|
||||
|
||||
log "步骤 1/7: 执行 deploy gate 本地校验"
|
||||
bash scripts/verify.sh
|
||||
|
||||
log "步骤 2/7: 校验三仓 n8n 模板已回写"
|
||||
for service in "${SERVICES[@]}"; do
|
||||
local_repo="/root/codes/$service"
|
||||
manifest_path="$local_repo/config/n8n/workflows/smart-cs/manifest.yaml"
|
||||
if [[ ! -f "$manifest_path" ]]; then
|
||||
err "缺少模板清单: $manifest_path"
|
||||
exit 1
|
||||
fi
|
||||
log "模板存在: $manifest_path"
|
||||
done
|
||||
|
||||
if [[ "$SKIP_TRIGGER" != "true" ]]; then
|
||||
log "步骤 3/7: 触发三仓 build-and-enqueue"
|
||||
for service in "${SERVICES[@]}"; do
|
||||
repo="${REPO_BY_SERVICE[$service]}"
|
||||
trigger_ts="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
||||
log "触发 workflow: repo=$repo env=$ENV_NAME"
|
||||
gh workflow run build-and-enqueue.yml -R "$repo" --ref main -f target_env="$ENV_NAME"
|
||||
|
||||
run_id="$(resolve_run_id "$repo" "$trigger_ts" || true)"
|
||||
if [[ -z "$run_id" ]]; then
|
||||
err "无法解析 workflow run id: repo=$repo"
|
||||
exit 1
|
||||
fi
|
||||
log "捕获 run_id: repo=$repo run_id=$run_id"
|
||||
|
||||
wait_run_success "$repo" "$run_id"
|
||||
done
|
||||
else
|
||||
log "步骤 3/7: 已跳过 workflow 触发(--skip-trigger)"
|
||||
QUEUE_REFERENCE_TS="1970-01-01T00:00:00Z"
|
||||
log "跳过触发模式下,queue/promoted 校验将使用全量历史记录"
|
||||
fi
|
||||
|
||||
log "步骤 4/7: 等待 queue 入列"
|
||||
wait_queue_new_entries "$QUEUE_REFERENCE_TS"
|
||||
|
||||
log "步骤 5/7: 等待 promoter 出列"
|
||||
if ! wait_promoted_entries "$QUEUE_REFERENCE_TS"; then
|
||||
if [[ "$ALLOW_PROMOTE_PENDING" == "true" ]]; then
|
||||
log "告警: promoter 出列超时,但已允许继续(--allow-promote-pending)"
|
||||
else
|
||||
err "promoter 出列超时"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
log "步骤 6/7: 检查 ArgoCD 健康状态"
|
||||
check_argocd_apps
|
||||
|
||||
if [[ "$SKIP_SMOKE" != "true" ]]; then
|
||||
log "步骤 7/7: 执行 smoke"
|
||||
run_smoke_subset
|
||||
else
|
||||
log "步骤 7/7: 已跳过 smoke(--skip-smoke)"
|
||||
fi
|
||||
|
||||
log "输出 evidence 摘要"
|
||||
print_evidence_summary
|
||||
|
||||
log "E2E 验证完成"
|
||||
Reference in New Issue
Block a user