create-mr.sh 5.9 KB
#!/usr/bin/env bash
# create-mr.sh — mr-create 主流程:渲染 description、查已有 MR / 创建新 MR
#
# 用法:
#   bash create-mr.sh <phase_id> <current_branch> <date>
#
#   <phase_id> = 后端模块 id(如 module_sys)或前端常量 "frontend-phase"
#
# 输出(stdout,单行,由调用方读取):
#   <MR_IID> <MR_URL>
#
# 失败:诊断写 stderr,exit 1。
#
# 设计要点:
# - 报告整文只经 sed + awk 管道流入 description 与 GitLab API(curl --rawfile),
#   全程不进 LLM 上下文。
# - 幂等:同一 source_branch 已有 opened MR 时,复用其 iid/url,不再创建。
# - phase 自动:phase_id == "frontend-phase" 时跳过 docs/08 § 二 lookup,使用常量名。

set -euo pipefail

MODULE_ID="${1:?usage: create-mr.sh <phase_id> <current_branch> <date>}"
CURRENT_BRANCH="${2:?missing current_branch}"
DATE="${3:?missing date (YYYY-MM-DD)}"

SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
TPL_DIR="$SCRIPT_DIR/../templates"
DESC_TPL="$TPL_DIR/mr-description-template.md"
TITLE_TPL="$TPL_DIR/mr-title-template.md"

REPORT="docs/superpowers/module-reports/${DATE}-${MODULE_ID}.md"
TEST_GATE="docs/superpowers/module-reports/${MODULE_ID}-test-gate.md"

[ -f "$REPORT" ]    || { echo "[create-mr] ⚠️ 完成报告不存在: $REPORT" >&2; exit 1; }
[ -f "$TEST_GATE" ] || { echo "[create-mr] ⚠️ test-gate evidence 不存在: $TEST_GATE" >&2; exit 1; }

# 1. 加载凭据
[ -f .env.local ] || { echo "[create-mr] ⚠️ .env.local 不存在" >&2; exit 1; }
set -a; . ./.env.local; set +a
for v in GITLAB_API_URL GITLAB_TOKEN GITLAB_PROJECT_ID; do
  eval "val=\${$v:-}"
  [ -n "$val" ] || { echo "[create-mr] ⚠️ .env.local 缺少 $v" >&2; exit 1; }
done

# 2. 探测目标分支(origin/HEAD → main → master)
TARGET_BRANCH=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/origin/||' || true)
[ -n "$TARGET_BRANCH" ] || TARGET_BRANCH=$(git branch -r --format='%(refname:short)' | grep -E '^origin/(main|master)$' | head -1 | sed 's|^origin/||' || true)
[ -n "$TARGET_BRANCH" ] || { echo "[create-mr] ⚠️ 无法探测默认分支(origin/main 或 origin/master)" >&2; exit 1; }

# 3. 取 module_name
if [ "$MODULE_ID" = "frontend-phase" ]; then
  MODULE_NAME="前端阶段(整体)"
else
  # 从 docs/08 § 二 提取后端模块 module_name
  MODULE_NAME=$(awk -v mid="$MODULE_ID" '
    $0 ~ "^- "mid" " { sub("^- "mid" ", ""); print; exit }
  ' docs/08-模块任务管理.md)
  [ -n "$MODULE_NAME" ] || { echo "[create-mr] ⚠️ docs/08 § 二 找不到模块 $MODULE_ID" >&2; exit 1; }
fi

# 4. 从 test-gate evidence 提取 conclusion + subagent_id
TEST_SUBAGENT_ID=$(awk '/^- 子会话: / { sub("^- 子会话: ", ""); print; exit }' "$TEST_GATE")
TEST_GATE_CONCLUSION=$(awk '/^结论: / { sub("^结论: ", ""); print; exit }' "$TEST_GATE" | awk '{print $1}')

# 5. 渲染 MR 标题(单行,可进 LLM 上下文)
TITLE=$(cat "$TITLE_TPL")
TITLE="${TITLE//\{\{module_id\}\}/$MODULE_ID}"
TITLE="${TITLE//\{\{module_name\}\}/$MODULE_NAME}"

# 6. 渲染 description(整篇模块报告,全程不进 LLM 上下文)
mkdir -p .tmp
DESC_FILE=.tmp/mr-desc.md

sed -e "s|{{test_gate_conclusion}}|$TEST_GATE_CONCLUSION|g" \
    -e "s|{{test_subagent_id}}|$TEST_SUBAGENT_ID|g" \
    -e "s|{{module_id}}|$MODULE_ID|g" \
    -e "s|{{date}}|$DATE|g" \
    "$DESC_TPL" > "$DESC_FILE"

awk -v report="$REPORT" '
  /\{\{module_report_contents\}\}/ { while ((getline line < report) > 0) print line; close(report); next }
  { print }
' "$DESC_FILE" > .tmp/mr-desc.final
mv .tmp/mr-desc.final "$DESC_FILE"

# 7. 幂等守门:查已有 opened MR
CURL_META=$(curl -sS -o .tmp/existing.json -w '%{http_code}|%{url_effective}' \
  --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \
  "${GITLAB_API_URL}/projects/${GITLAB_PROJECT_ID}/merge_requests?source_branch=${CURRENT_BRANCH}&state=opened")

HTTP_CODE=${CURL_META%%|*}
EFFECTIVE_URL=${CURL_META#*|}

if [ "$HTTP_CODE" != "200" ]; then
  echo "[create-mr] ⚠️ 查询已有 MR 失败 (HTTP $HTTP_CODE)" >&2
  echo "  effective URL: $EFFECTIVE_URL" >&2
  FIRST=$(head -c 1 .tmp/existing.json 2>/dev/null || echo "")
  case "$FIRST" in
    '{'|'[')
      echo "  响应(JSON):" >&2
      jq -r '.message // .error // .' .tmp/existing.json 2>/dev/null | head -c 200 >&2
      echo >&2
      ;;
    '<')
      echo "  响应是 HTML(很可能反代/路由失配,把 API 请求送进了 web 处理链):" >&2
      head -c 200 .tmp/existing.json >&2
      echo >&2
      echo "  常见原因:GITLAB_API_URL 错 / GITLAB_PROJECT_ID 不是数字 ID / 反代规范化路径" >&2
      ;;
    *)
      echo "  响应非 JSON 非 HTML(前 200 bytes):" >&2
      head -c 200 .tmp/existing.json >&2
      echo >&2
      ;;
  esac
  rm -f .tmp/existing.json "$DESC_FILE"
  exit 1
fi

EXISTING_IID=$(jq -r '.[0].iid // empty' .tmp/existing.json)
EXISTING_URL=$(jq -r '.[0].web_url // empty' .tmp/existing.json)
rm -f .tmp/existing.json

if [ -n "$EXISTING_IID" ]; then
  echo "[create-mr] 复用已有 opened MR: !$EXISTING_IID" >&2
  rm -f "$DESC_FILE"
  echo "$EXISTING_IID $EXISTING_URL"
  exit 0
fi

# 8. 创建新 MR(--rawfile desc 把 description 文件流入 jq → curl,不进 LLM 上下文)
CREATE_RESP=$(curl -sS -X POST \
  --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \
  --header "Content-Type: application/json" \
  --data "$(jq -n \
    --arg src "$CURRENT_BRANCH" \
    --arg tgt "$TARGET_BRANCH" \
    --arg title "$TITLE" \
    --rawfile desc "$DESC_FILE" \
    '{source_branch: $src, target_branch: $tgt, title: $title, description: $desc, remove_source_branch: false}')" \
  "${GITLAB_API_URL}/projects/${GITLAB_PROJECT_ID}/merge_requests")

MR_IID=$(echo "$CREATE_RESP" | jq -r '.iid // empty')
MR_URL=$(echo "$CREATE_RESP" | jq -r '.web_url // empty')

if [ -z "$MR_IID" ]; then
  echo "[create-mr] ⚠️ MR 创建失败:" >&2
  echo "$CREATE_RESP" | jq . >&2
  rm -f "$DESC_FILE"
  exit 1
fi

rm -f "$DESC_FILE"
echo "$MR_IID $MR_URL"