#!/usr/bin/env bash # create-mr.sh — mr-create 主流程:渲染 description、查已有 MR / 创建新 MR # # 用法: # bash create-mr.sh # # 输出(stdout,单行,由调用方读取): # # # 失败:诊断写 stderr,exit 1。 # # 设计要点: # - 模块报告整文只经 sed + awk 管道流入 description 与 GitLab API(curl --rawfile), # 全程不进 LLM 上下文。 # - 幂等:同一 source_branch 已有 opened MR 时,复用其 iid/url,不再创建。 set -euo pipefail MODULE_ID="${1:?usage: create-mr.sh }" 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. 从 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; } # 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"