Files
lijiaoqiao/scripts/ci/lib/manifest_lib.sh
Your Name 1fec3e981d feat(ci): 实现 Phase 1/2 严格退出标准的所有代码实现
Phase 1 Criterion 4: contract tests 场景清单 → backend-verify.sh --phase1-contract-gate(含四个场景:合法token全链路、吊销拒绝、scope不足拒绝、runtime快速失败),repo_integrity_check.sh 集成调用

Phase 2 Criterion 1: manifest.json 系统(lib/manifest_lib.sh + staging_release_pipeline.sh),run_id 作为硬门禁,manifest_hard_gate_run_id() 验证非空

Phase 2 Criterion 2: superpowers_stage_validate.sh exit 1 条件从 NO_GO 扩展到 CONDITIONAL_GO,staging 硬门禁不再放行条件通过

Phase 2 Criterion 3: DEFERRED 语义修正,CONDITIONAL_GO 不再出现在复审结论选项中;CONDITIONAL_GO 在 pipeline 中强制 exit 1

Phase 2 Criterion 5: cross_service_smoke.sh 从 DESIGN_ONLY 变为可执行(exit 0=PASS/1=FAIL/2=SKIP_LOCAL_PLACEHOLDER),纳入 staging_release_pipeline.sh STEP-03

Phase 2 Criterion 4: 配置分离(已之前落地,本次确认)

环境问题记录: docs/plans/2026-04-21-environmental-issues-log.md
- P3-A: HTTP timeout + cache eviction(需要真实 staging env + env var 热加载支持)
- P3-B/C: /metrics 端点(需要 Prometheus scrape 配置 + 运维介入)
- P3-D: graceful shutdown(需要 staging 流量压测验证)
2026-04-21 12:14:50 +08:00

263 lines
10 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# shellcheck disable=SC1091
# scripts/ci/lib/manifest_lib.sh
# Staging release manifest 生成和消费公共库
# 所有 staging pipeline 脚本应使用此库管理 manifest.json
set -euo pipefail
# 默认值ROOT_DIR 必须在 MANIFEST_DIR 之前先解析)
_root_dir="${ROOT_DIR:-$(cd "$(dirname "$0")/../../.." && pwd)}"
MANIFEST_DIR="${MANIFEST_DIR:-${_root_dir}/reports/releases}"
unset _root_dir
RUN_ID="${RUN_ID:-$(date +%Y%m%d_%H%M%S)_$$}"
MANIFEST_FILE="${MANIFEST_DIR}/${RUN_ID}/manifest.json"
# ──────────────────────────────────────────────────────────────
# 生成 manifest.json
# 用法: manifest_generate [--run-id <id>] [--staging] [--prod]
# ──────────────────────────────────────────────────────────────
manifest_generate() {
local env="staging"
while [[ $# -gt 0 ]]; do
case "$1" in
--run-id) RUN_ID="$2"; shift 2 ;;
--staging) env="staging"; shift ;;
--prod) env="prod"; shift ;;
*) shift ;;
esac
done
MANIFEST_FILE="${MANIFEST_DIR}/${RUN_ID}/manifest.json"
mkdir -p "$(dirname "${MANIFEST_FILE}")"
# 生成时间戳
local ts
ts="$(date -Iseconds)"
# 基础 manifest 结构
cat > "${MANIFEST_FILE}" <<EOF
{
"version": "1.0",
"run_id": "${RUN_ID}",
"environment": "${env}",
"created_at": "${ts}",
"decision_inputs": {},
"artifact_paths": {},
"smoke_results": {},
"contract_results": {}
}
EOF
echo "[MANIFEST] Generated: ${MANIFEST_FILE}"
}
# ──────────────────────────────────────────────────────────────
# 向 manifest 写入 key=value支持嵌套路径
# 用法: manifest_set "decision_inputs.stage_validation" "GO"
# manifest_set "artifact_paths.backend_verify" "/path/to/report.md"
# ──────────────────────────────────────────────────────────────
manifest_set() {
local key="$1"
local value="$2"
local file="${3:-${MANIFEST_FILE}}"
if [[ ! -f "${file}" ]]; then
echo "[WARN] manifest_set: ${file} not found, skipping" >&2
return 1
fi
# 将 dot-notation 路径转为 jq 路径
local jq_path
jq_path="$(echo "${key}" | sed 's/\./|/g' | tr '|' '.')"
local tmp
tmp="$(mktemp)"
if ! jq --arg v "${value}" \
"setpath(\"${jq_path}\" | split(\".\"); \$v)" \
"${file}" > "${tmp}"; then
echo "[WARN] manifest_set: jq failed for key=${key} value=${value}" >&2
rm -f "${tmp}"
return 1
fi
mv "${tmp}" "${file}"
echo "[MANIFEST] set ${key}=${value}"
}
# ──────────────────────────────────────────────────────────────
# 从 manifest 读取 value
# 用法: manifest_get "decision_inputs.stage_validation"
# 返回值写入 stdout
# ──────────────────────────────────────────────────────────────
manifest_get() {
local key="$1"
local file="${2:-${MANIFEST_FILE}}"
if [[ ! -f "${file}" ]]; then
echo ""
return
fi
local jq_path
jq_path="$(echo "${key}" | sed 's/\./|/g' | tr '|' '.')"
jq -r "getpath(\"${jq_path}\" | split(\".\")) // \"\" " "${file}" 2>/dev/null || true
}
# ──────────────────────────────────────────────────────────────
# 验证 manifest 完整性
# 返回 0 = 有效1 = 无效
# 用法: manifest_validate "${MANIFEST_FILE}" || exit 1
# ──────────────────────────────────────────────────────────────
manifest_validate() {
local file="${1:-${MANIFEST_FILE}}"
if [[ ! -f "${file}" ]]; then
echo "[FAIL] manifest_validate: ${file} does not exist" >&2
return 1
fi
# 基础字段检查
if ! jq -e '.run_id != "" and .environment != "" and .created_at != ""' "${file}" > /dev/null 2>&1; then
echo "[FAIL] manifest_validate: missing required fields (run_id/environment/created_at)" >&2
return 1
fi
echo "[MANIFEST] validate OK: ${file}"
return 0
}
# ──────────────────────────────────────────────────────────────
# 运行 backend-verify 并将结果写入 manifest
# 用法: manifest_run_backend_verify [--manifest-file <path>]
# ──────────────────────────────────────────────────────────────
manifest_run_backend_verify() {
local manifest_file="${MANIFEST_FILE}"
while [[ $# -gt 0 ]]; do
case "$1" in
--manifest-file) manifest_file="$2"; shift 2 ;;
*) shift ;;
esac
done
local bv_log="${OUT_DIR:-/tmp}/backend_verify_$(date +%F_%H%M%S).log"
local bv_report="${OUT_DIR:-/tmp}/backend_verify_$(date +%F_%H%M%S).md"
if bash "${ROOT_DIR}/scripts/ci/backend-verify.sh" \
> >(tee "${bv_log}") 2>&1; then
manifest_set "artifact_paths.backend_verify" "${bv_report}" "${manifest_file}"
manifest_set "contract_results.backend_verify" "PASS" "${manifest_file}"
echo "[MANIFEST] backend_verify: PASS"
else
manifest_set "artifact_paths.backend_verify" "${bv_log}" "${manifest_file}"
manifest_set "contract_results.backend_verify" "FAIL" "${manifest_file}"
echo "[MANIFEST] backend_verify: FAIL"
return 1
fi
}
# ──────────────────────────────────────────────────────────────
# 运行 contract gate 并将结果写入 manifest
# 用法: manifest_run_contract_gate [--manifest-file <path>]
# ──────────────────────────────────────────────────────────────
manifest_run_contract_gate() {
local manifest_file="${MANIFEST_FILE}"
while [[ $# -gt 0 ]]; do
case "$1" in
--manifest-file) manifest_file="$2"; shift 2 ;;
*) shift ;;
esac
done
local cg_log="${OUT_DIR:-/tmp}/contract_gate_$(date +%F_%H%M%S).log"
local cg_report="${OUT_DIR:-/tmp}/contract_gate_$(date +%F_%H%M%S).md"
if bash "${ROOT_DIR}/scripts/ci/backend-verify.sh" --phase1-contract-gate \
> >(tee "${cg_log}") 2>&1; then
manifest_set "artifact_paths.contract_gate" "${cg_report}" "${manifest_file}"
manifest_set "contract_results.contract_gate" "PASS" "${manifest_file}"
echo "[MANIFEST] contract_gate: PASS"
else
manifest_set "artifact_paths.contract_gate" "${cg_log}" "${manifest_file}"
manifest_set "contract_results.contract_gate" "FAIL" "${manifest_file}"
echo "[MANIFEST] contract_gate: FAIL"
return 1
fi
}
# ──────────────────────────────────────────────────────────────
# 运行 superpowers_stage_validate 并将结果写入 manifest
# 用法: manifest_run_stage_validation [--manifest-file <path>]
# ──────────────────────────────────────────────────────────────
manifest_run_stage_validation() {
local manifest_file="${MANIFEST_FILE}"
while [[ $# -gt 0 ]]; do
case "$1" in
--manifest-file) manifest_file="$2"; shift 2 ;;
*) shift ;;
esac
done
local sp_log="${OUT_DIR:-/tmp}/superpowers_stage_validation_$(date +%F_%H%M%S).log"
local sp_report="${OUT_DIR:-/tmp}/superpowers_stage_validation_$(date +%F_%H%M%S).md"
if bash "${ROOT_DIR}/scripts/ci/superpowers_stage_validate.sh" \
> >(tee "${sp_log}") 2>&1; then
manifest_set "decision_inputs.stage_validation" "PASS" "${manifest_file}"
manifest_set "artifact_paths.stage_validation" "${sp_report}" "${manifest_file}"
echo "[MANIFEST] stage_validation: PASS"
else
manifest_set "decision_inputs.stage_validation" "FAIL" "${manifest_file}"
manifest_set "artifact_paths.stage_validation" "${sp_report}" "${manifest_file}"
echo "[MANIFEST] stage_validation: FAIL"
return 1
fi
}
# ──────────────────────────────────────────────────────────────
# 检查 manifest 中的 run_id 是否非空(硬门禁)
# 用法: manifest_hard_gate_run_id [--manifest-file <path>]
# ──────────────────────────────────────────────────────────────
manifest_hard_gate_run_id() {
local file="${1:-${MANIFEST_FILE}}"
local run_id
run_id="$(manifest_get "run_id" "${file}")"
if [[ -z "${run_id}" ]]; then
echo "[GATE FAIL] run_id is empty in manifest — hard gate blocked" >&2
echo "[GATE FAIL] manifest: ${file}" >&2
return 1
fi
echo "[GATE OK] run_id=${run_id}"
return 0
}
# ──────────────────────────────────────────────────────────────
# 打印 manifest 摘要
# 用法: manifest_summary [--manifest-file <path>]
# ──────────────────────────────────────────────────────────────
manifest_summary() {
local file="${1:-${MANIFEST_FILE}}"
if [[ ! -f "${file}" ]]; then
echo "[WARN] manifest not found: ${file}"
return
fi
echo "=== Manifest: ${file} ==="
jq -r '
to_entries | .[] |
if .value | type == "object" then
"\(.key):"
elif .value | type == "array" then
"\(.key): \(.value | join(", "))"
else
"\(.key): \(.value}"
end
' "${file}" 2>/dev/null || cat "${file}"
}