-
Notifications
You must be signed in to change notification settings - Fork 0
feat: conductor 状态机骨架(W0-C3 #132,ADR-0049) #140
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,221 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: conductor | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| # 状态机路由器(IR-0001 W0-C3 / ADR-0049)。W0 事件面=本仓(issues.labeled + | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| # issue_comment);跨仓扩展随产品仓接入。全部状态标签写操作以 cloudbrid-agent | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| # App 令牌执行(INV-02:GITHUB_TOKEN 身份不持有状态写权)。 | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| on: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| issues: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| types: [labeled] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| issue_comment: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| types: [created] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| # INV-09:每 issue 一个 concurrency group、cancel-in-progress=false——重复投递 | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| # 排队串行而非并发竞态;幂等由 from_state 匹配承担(重复事件=当前态已变=no-op) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| concurrency: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| group: conductor-issue-${{ github.event.issue.number }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| cancel-in-progress: false | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| permissions: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| contents: read # checkout 本仓(transitions.yaml + gh-app-token.sh) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| jobs: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| route: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if: github.repository == 'Cloudbird-Software/.github' | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| runs-on: ubuntu-latest | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| timeout-minutes: 5 | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| outputs: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| invoke: ${{ steps.route.outputs.invoke }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| issue: ${{ steps.route.outputs.issue }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ir_ref: ${{ steps.route.outputs.ir_ref }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| steps: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| with: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| persist-credentials: false | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| - name: 铸 App 令牌(AG-2:本仓单仓作用域) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| env: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| CB_APP_ID: ${{ secrets.CB_APP_ID }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| AGENT_APP_SECRET: ${{ secrets.AGENT_APP_SECRET }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| run: | | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| TOKEN=$(REPO=.github CB_APP_ID="$CB_APP_ID" AGENT_APP_SECRET="$AGENT_APP_SECRET" \ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| bash scripts/gh-app-token.sh) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| echo "APP_TOKEN=$TOKEN" >>"$GITHUB_ENV" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+33
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win 令牌铸造代码块在两个 job 中重复,且都缺少失败即停与日志脱敏。 同一段
📍 Affects 1 file
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
| # 事件路由与守卫:transitions.yaml 是唯一转移定义;guard 受限求值 | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| # (变量白名单注入、无内建);非授权=静默丢弃(回退标签、不评论、审计进 | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| # run 日志——AC-11)。注释/标签正文绝不进入任何求值(命令白名单精确匹配)。 | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| - name: route(INV-02/09) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| id: route | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| env: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| APP_TOKEN: ${{ env.APP_TOKEN }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| GOV_TOKEN: ${{ secrets.GOVERNANCE_TOKEN }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+46
to
+48
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 1. gov_token bypasses approved scripts The workflow uses secrets.GOVERNANCE_TOKEN directly for GitHub API calls, instead of obtaining tokens via scripts/ghcb or scripts/gh-app-token.sh with single-repo scope. This can introduce over-privileged/org-scoped credentials into automation flows. Agent Prompt
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| EVENT_NAME: ${{ github.event_name }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ACTION: ${{ github.event.action }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| LABEL_NAME: ${{ github.event.label.name }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| COMMENT_BODY: ${{ github.event.comment.body }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| COMMENT_ASSOC: ${{ github.event.comment.author_association }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ACTOR: ${{ github.actor }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ISSUE_NUMBER: ${{ github.event.issue.number }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ISSUE_TITLE: ${{ github.event.issue.title }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| REPO: ${{ github.repository }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| run: | | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| python3 - <<'PYEOF' | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import json, os, re, urllib.parse, urllib.request, urllib.error, yaml | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+59
to
+60
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🌐 Web query:
💡 Result: No, PyYAML is not preinstalled on the GitHub Actions ubuntu-24.04 runner image's system Python [1][2]. GitHub maintains an official list of installed software for its runner images, and PyYAML is not included in the preinstalled packages for the Ubuntu 24.04 runner [1][2]. Users requiring PyYAML must install it during their workflow execution, typically by using pip, such as running Citations:
🏁 Script executed: #!/bin/bash
set -eu
file=".github/workflows/conductor.yml"
printf '%s\n' '--- workflow context ---'
cat -n "$file" | sed -n '1,130p'
printf '%s\n' '--- dependency references ---'
rg -n -i 'pyyaml|python.*(pip|yaml)|pip(3)?[[:space:]].*(yaml|pyyaml)|setup-python|runs-on|import yaml' .github/workflows "$file" 2>/dev/null || true
printf '%s\n' '--- all workflow files ---'
git ls-files '.github/workflows/**'Repository: Cloudbird-Software/.github Length of output: 7911 🏁 Script executed: #!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
import re
workflow = Path(".github/workflows/conductor.yml").read_text()
transitions = Path("governance/transitions.yaml").read_text()
print("--- workflow dependency invariant ---")
route = workflow.split(" route:", 1)[1].split("\n ", 1)[0] if " route:" in workflow else ""
print("route_has_ubuntu_latest:", "runs-on: ubuntu-latest" in workflow)
print("route_imports_yaml:", "import json, os, re, urllib.parse, urllib.request, urllib.error, yaml" in workflow)
print("route_calls_safe_load:", "yaml.safe_load" in workflow)
print("explicit_yaml_install:", bool(re.search(r"(?i)(pip3?|python3?\\s+-m\\s+pip)\\s+.*(pyyaml|yaml)", workflow)))
print("setup_python_present_in_conductor:", "actions/setup-python@" in workflow)
print("yaml_import_before_safe_load:",
workflow.find("import json, os, re, urllib.parse, urllib.request, urllib.error, yaml")
< workflow.find("yaml.safe_load"))
print("--- transitions.yaml shape ---")
lines = [line for line in transitions.splitlines() if line.strip() and not line.lstrip().startswith("#")]
print("non_comment_lines:", len(lines))
print("uses_yaml_features:", any(re.search(r"(^|\\s)(\\-|[A-Za-z_][A-Za-z0-9_]*:|[>|&*!])", line) for line in lines))
print("first_lines:")
for line in transitions.splitlines()[:80]:
print(line)
PY
printf '%s\n' '--- conductor tail ---'
cat -n .github/workflows/conductor.yml | sed -n '125,240p'Repository: Cloudbird-Software/.github Length of output: 7333 🏁 Script executed: #!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
import re
workflow = Path(".github/workflows/conductor.yml").read_text()
transitions = Path("governance/transitions.yaml").read_text()
print("--- workflow dependency invariant ---")
print("route_has_ubuntu_latest:", "runs-on: ubuntu-latest" in workflow)
print("route_imports_yaml:", "import json, os, re, urllib.parse, urllib.request, urllib.error, yaml" in workflow)
print("route_calls_safe_load:", "yaml.safe_load" in workflow)
print("explicit_yaml_install:", bool(re.search(r"(?i)(pip3?|python3?\s+-m\s+pip)\s+.*(pyyaml|yaml)", workflow)))
print("setup_python_present_in_conductor:", "actions/setup-python@" in workflow)
print("yaml_import_before_safe_load:",
workflow.find("import json, os, re, urllib.parse, urllib.request, urllib.error, yaml")
< workflow.find("yaml.safe_load"))
print("--- transitions.yaml shape ---")
for line in transitions.splitlines()[:80]:
print(line)
PY
cat -n .github/workflows/conductor.yml | sed -n '125,240p'Repository: Cloudbird-Software/.github Length of output: 7250 为
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+59
to
+61
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 3. Missing pyyaml dependency The router step imports yaml and calls yaml.safe_load, but this workflow never installs PyYAML (no pip install/setup-python), so it can fail at runtime with `ModuleNotFoundError: No module named 'yaml'` depending on the runner image. This would break all routing and downstream state transitions. Agent Prompt
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| E = os.environ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ORG, REPO = "Cloudbird-Software", E["REPO"] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ISSUE = E["ISSUE_NUMBER"] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| def api(token, path, method="GET", body=None): | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| data = json.dumps(body).encode() if body is not None else None | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| req = urllib.request.Request(f"https://api.github.com{path}", data=data, method=method, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers={"Authorization": f"Bearer {token}", "Accept": "application/vnd.github+json", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "User-Agent": "conductor"}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| with urllib.request.urlopen(req) as r: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return r.status, json.load(r) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| except urllib.error.HTTPError as e: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return e.code, {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+65
to
+74
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🛡️ 建议修复:加超时并保留错误正文 try:
- with urllib.request.urlopen(req) as r:
+ with urllib.request.urlopen(req, timeout=15) as r:
return r.status, json.load(r)
except urllib.error.HTTPError as e:
- return e.code, {}
+ detail = e.read(2048).decode("utf-8", "replace")
+ print(f"AUDIT | api {method} {path} -> HTTP {e.code}: {detail}", flush=True)
+ return e.code, {}
+ except urllib.error.URLError as e:
+ print(f"AUDIT | api {method} {path} -> 网络错误: {e}", flush=True)
+ return 0, {}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| def audit(msg): | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| # 审计面 = 本 run 日志(AC-11;不评论、不写 issue——防评论轰炸) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| print(f"AUDIT | issue=#{ISSUE} | actor={E.get('ACTOR')} | {msg}", flush=True) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| # ---- 事件规范化(白名单精确匹配,正文不进任何求值)---- | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ev = None | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if E["EVENT_NAME"] == "issues" and E["ACTION"] == "labeled": | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ln = E.get("LABEL_NAME") or "" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if ln.startswith("state:"): | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ev = f"label:{ln}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| audit(f"event=label(non-state:{ln}) verdict=noop"); raise SystemExit(0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| elif E["EVENT_NAME"] == "issue_comment" and E["ACTION"] == "created": | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| head = (E.get("COMMENT_BODY") or "").strip().split() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| token = head[0] if head else "" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if token in ("/start", "/claim", "/retry"): | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ev = f"comment:{token}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise SystemExit(0) # 普通评论:无审计面(噪音) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if ev is None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| audit("event=unrecognized verdict=noop"); raise SystemExit(0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| # ---- sender_role(INV-02:API 判定,不硬编码用户名)---- | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| actor = E.get("ACTOR") or "" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| role = "none" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if actor == "cloudbrid-agent[bot]": | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| role = "agent" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| st, m = api(E["GOV_TOKEN"], f"/orgs/{ORG}/memberships/{actor}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if st == 200 and m.get("role") == "admin": | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| role = "owner" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| assoc = E.get("COMMENT_ASSOC") or "NONE" # label 事件无此字段→NONE(guard 用 role) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| # ---- 当前状态与标签集 ---- | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| st, iss = api(E["APP_TOKEN"], f"/repos/{REPO}/issues/{ISSUE}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if st != 200: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| audit(f"verdict=abort 读取 issue 失败 HTTP {st}"); raise SystemExit(1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| labels = {l["name"] for l in iss.get("labels", [])} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| states = [n[len("state:"):] for n in labels if n.startswith("state:")] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| current = states[0] if states else "ir-draft" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if len(states) > 1: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| audit(f"verdict=abort 多状态标签并存: {states}"); raise SystemExit(1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| # ---- 转移表匹配(幂等:from_state 不符=no-op)---- | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| table = yaml.safe_load(open("governance/transitions.yaml", encoding="utf-8")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| cands = [t for t in table["transitions"] if t["event"] == ev] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| t = next((x for x in cands if x["from_state"] == current), None) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if t is None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| audit(f"event={ev} from={current} verdict=noop(无匹配转移——跳态/重复/未列组合)") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise SystemExit(0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| # ---- guard 受限求值 ---- | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| env_vars = {"sender_role": role, "author_association": assoc, "label_set": labels} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ok = False | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ok = bool(eval(t["guard"], {"__builtins__": {}}, dict(env_vars))) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+129
to
+132
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 4. Eval sandbox bypass risk The workflow executes transition guards using Python eval(...) with user-defined expressions from governance/transitions.yaml, which is not a safe sandbox even with __builtins__ cleared. A malicious or mistakenly-expanded guard can escape and execute arbitrary code on the runner, potentially exposing secrets (App token, governance token). Agent Prompt
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| audit(f"verdict=abort guard 求值失败 {t['id']}: {e}"); raise SystemExit(1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| def swap_state(frm, to): | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| enc_from = urllib.parse.quote(f"state:{frm}", safe="") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| api(E["APP_TOKEN"], f"/repos/{REPO}/issues/{ISSUE}/labels/state%3A{frm}", "DELETE") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| api(E["APP_TOKEN"], f"/repos/{REPO}/issues/{ISSUE}/labels", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "POST", {"labels": [f"state:{to}"]}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+135
to
+139
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 5. State label swap inconsistent swap_state() ignores HTTP results for both deleting the old state label and adding the new one, so partial failures can leave an issue with multiple state:* labels or none; the next run hard-aborts when multiple state labels exist. This can wedge issues into an unrecoverable state without manual cleanup. Agent Prompt
Comment on lines
+135
to
+139
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win 换签失败被静默忽略,状态标签与下游执行会不一致。
结果:issue 停留在 另外第 136 行的 🐛 建议修复:校验换签状态码并统一编码 def swap_state(frm, to):
- enc_from = urllib.parse.quote(f"state:{frm}", safe="")
- api(E["APP_TOKEN"], f"/repos/{REPO}/issues/{ISSUE}/labels/state%3A{frm}", "DELETE")
- api(E["APP_TOKEN"], f"/repos/{REPO}/issues/{ISSUE}/labels",
- "POST", {"labels": [f"state:{to}"]})
+ enc_from = urllib.parse.quote(f"state:{frm}", safe="")
+ st_del, _ = api(E["APP_TOKEN"], f"/repos/{REPO}/issues/{ISSUE}/labels/{enc_from}", "DELETE")
+ if st_del not in (200, 204, 404): # 404 = 标签本就不在
+ audit(f"verdict=abort 移除 state:{frm} 失败 HTTP {st_del}"); raise SystemExit(1)
+ st_add, _ = api(E["APP_TOKEN"], f"/repos/{REPO}/issues/{ISSUE}/labels",
+ "POST", {"labels": [f"state:{to}"]})
+ if st_add not in (200, 201):
+ audit(f"verdict=abort 置上 state:{to} 失败 HTTP {st_add}"); raise SystemExit(1)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not ok: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| # 静默丢弃(AC-11):回退标签、不评论、不启动 | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if ev.startswith("label:"): | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| api(E["APP_TOKEN"], f"/repos/{REPO}/issues/{ISSUE}/labels/" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| + urllib.parse.quote(ev[len("label:"):], safe=""), "DELETE") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| audit(f"event={ev} transition={t['id']} sender_role={role} assoc={assoc} " | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| f"verdict=DENIED-silent-drop(标签已回退,无评论,无阶段启动)") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise SystemExit(0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| # ---- 执行转移(状态标签写=App 身份,INV-02)---- | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| out = open(E["GITHUB_OUTPUT"], "a", encoding="utf-8") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| action = t["action"] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if action == "invoke:spec-author": | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| m = re.match(r"(IR-\d+)", iss.get("title") or "") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| task_id = m.group(1) if m else f"ISSUE-{ISSUE}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| swap_state(t["from_state"], t["to_state"]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| out.write(f"invoke=spec-author\nissue={ISSUE}\nir_ref={task_id} {REPO}#{ISSUE}\n") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| audit(f"event={ev} transition={t['id']} sender_role={role} verdict=ALLOWED " | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| f"{t['from_state']}->{t['to_state']} action=invoke:spec-author") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| elif action == "claim": | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| swap_state(t["from_state"], t["to_state"]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| api(E["APP_TOKEN"], f"/repos/{REPO}/issues/{ISSUE}/assignees", "POST", {"assignees": [actor]}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| out.write("invoke=none\n") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| audit(f"event={ev} transition={t['id']} sender_role={role} verdict=ALLOWED " | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| f"claim->in-progress assignee={actor}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| swap_state(t["from_state"], t["to_state"]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| out.write("invoke=none\n") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| audit(f"event={ev} transition={t['id']} sender_role={role} verdict=ALLOWED " | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| f"{t['from_state']}->{t['to_state']} action=noop") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| out.close() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| PYEOF | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| - name: 幂等键落盘({issue, from, to}——重复投递复核凭据) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if: steps.route.outputs.invoke == 'spec-author' | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| run: | | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| echo "transition-key: {issue: ${{ steps.route.outputs.issue }}, to: spec} @ $(date -u +%FT%TZ)" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| spec: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| # spec-author 钉发布 tag 的 commit SHA(供应链;ADR-0049/0050) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| needs: route | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if: needs.route.outputs.invoke == 'spec-author' | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| uses: Cloudbird-Software/CI-Workflows/.github/workflows/spec-author.yml@b89d88696a5b184447c317851e9bcbebb733a439 | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| with: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| issue_number: ${{ fromJson(needs.route.outputs.issue) }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| target_repo: 'Cloudbird-Software/.github' | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ir_ref: ${{ needs.route.outputs.ir_ref }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| # 显式传递(zizmor secrets-inherit:调用方 secrets 必须显式点名) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| secrets: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| LLM_API_KEY: ${{ secrets.LLM_API_KEY }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| CB_APP_ID: ${{ secrets.CB_APP_ID }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| AGENT_APP_SECRET: ${{ secrets.AGENT_APP_SECRET }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| on-failure: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| # BEH-01:启动失败在原 issue 评论原因(人类只看 issue 就能知道卡在哪) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| needs: [route, spec] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if: always() && (needs.route.result == 'failure' || needs.spec.result == 'failure') | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| runs-on: ubuntu-latest | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| timeout-minutes: 5 | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| permissions: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| contents: read | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| steps: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| with: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| persist-credentials: false | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| - name: 评论失败原因 | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| env: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| CB_APP_ID: ${{ secrets.CB_APP_ID }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| AGENT_APP_SECRET: ${{ secrets.AGENT_APP_SECRET }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ISSUE: ${{ needs.route.outputs.issue }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win route 失败时
此时 🐛 建议修复:用事件 issue 号兜底- ISSUE: ${{ needs.route.outputs.issue }}
+ ISSUE: ${{ needs.route.outputs.issue || github.event.issue.number }}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
| ROUTE_RC: ${{ needs.route.result }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| SPEC_RC: ${{ needs.spec.result }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| run: | | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| TOKEN=$(REPO=.github CB_APP_ID="$CB_APP_ID" AGENT_APP_SECRET="$AGENT_APP_SECRET" \ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| bash scripts/gh-app-token.sh) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| gh api "repos/Cloudbird-Software/.github/issues/$ISSUE/comments" \ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| -f body="**conductor:spec 阶段启动失败**(BEH-01,须人类介入) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+214
to
+217
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 2. gh api missing app token The on-failure notification step mints a GitHub App token but does not export it as GH_TOKEN/GITHUB_TOKEN for the gh api call, so it may fall back to the restricted default runner token (or fail) instead of using an approved-script token as required. Additionally, the failure comment can break because ISSUE is sourced from needs.route.outputs.issue, which is not set on many failure paths, potentially producing an invalid .../issues//comments endpoint. Agent Prompt
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| route=${ROUTE_RC} spec=${SPEC_RC} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| run:${RUN_URL} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| 状态已停于当前态;修复后 owner 评论 /start 或重打 state:ir-signed 重试。" \ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| && echo "AUDIT | issue=#$ISSUE | verdict=failure-notified" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+213
to
+221
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win 严重(Critical): 第 210 行把令牌赋给 当前 job 的 🐛 建议修复:显式传入 GH_TOKEN TOKEN=$(REPO=.github CB_APP_ID="$CB_APP_ID" AGENT_APP_SECRET="$AGENT_APP_SECRET" \
bash scripts/gh-app-token.sh)
- gh api "repos/Cloudbird-Software/.github/issues/$ISSUE/comments" \
+ GH_TOKEN="$TOKEN" gh api "repos/Cloudbird-Software/.github/issues/$ISSUE/comments" \
-f body="**conductor:spec 阶段启动失败**(BEH-01,须人类介入)📝 Committable suggestion
Suggested change
🧰 Tools🪛 actionlint (1.7.12)[error] 209-209: shellcheck reported issue in this script: SC2034:warning:1:1: TOKEN appears unused. Verify use (or export if used externally) (shellcheck) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| version: 1 | ||
| # 状态机转移表(IR-0001 IFACE-03 / ADR-0049)——conductor 唯一定义源, | ||
| # 只解释不内嵌逻辑。本文件是 C1 资产:改动走 PR+ADR。 | ||
| # | ||
| # schema: | ||
| # from_state = 转移前状态(当前 issue 无 state:* 标签时视为 ir-draft) | ||
| # event = label:state:<name> | comment:/start | comment:/claim | comment:/retry | ||
| # to_state = 转移后状态(conductor 以 App 身份换标签:移除 from、置上 to) | ||
| # action = invoke:spec-author | claim | noop(W0 全集) | ||
| # guard = 布尔表达式;变量白名单:sender_role(owner|agent|none)、 | ||
| # author_association(OWNER|MEMBER|COLLABORATOR|CONTRIBUTOR|NONE)、 | ||
| # label_set(当前标签名集合) | ||
| # 未列出的 (from_state, event) 组合 = 禁止转移(conductor no-op;重复投递/ | ||
| # 跳态/反向天然被 from_state 匹配拒绝——幂等键 {issue, from, to} 的落盘形态)。 | ||
| states: [ir-draft, ir-signed, spec, redteam, wave-planned, ready, in-progress, quarantine, needs-human, done] | ||
|
|
||
| transitions: | ||
| - id: T1 # 主通路:owner/agent 打签署标签 → 进 spec 阶段(BEH-01) | ||
| from_state: ir-draft | ||
| event: label:state:ir-signed | ||
| to_state: spec | ||
| action: invoke:spec-author | ||
| guard: "sender_role in ['owner', 'agent'] and 'type:intent' in label_set" | ||
| - id: T2 # owner 评论 /start 等价签署(AC-1 的 comment 路径) | ||
| from_state: ir-draft | ||
| event: comment:/start | ||
| to_state: spec | ||
| action: invoke:spec-author | ||
| guard: "sender_role == 'owner' and 'type:intent' in label_set" | ||
| - id: T3 # 认领:先到先得(当前态必须 ready,否则 no-op)(BEH-08) | ||
| from_state: ready | ||
| event: comment:/claim | ||
| to_state: in-progress | ||
| action: claim | ||
| guard: "sender_role == 'agent' or author_association in ['OWNER', 'MEMBER', 'COLLABORATOR']" | ||
| - id: T4 # 隔离重判(BEH-07 的 /retry 回流) | ||
| from_state: quarantine | ||
| event: comment:/retry | ||
| to_state: ready | ||
| action: noop | ||
| guard: "sender_role in ['owner', 'agent']" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
issue_comment也会在 PR 评论上触发,请过滤 PR。issue_comment对 issue 和 PR 的评论都会触发,github.event.issue在两种情形下都存在。PR 允许打标签,因此一个带state:ready标签的 PR 收到/claim评论会命中 T3,conductor 会对该 PR 换签并指派。issues.labeled不受影响(PR 打标签走pull_request事件)。🛡️ 建议修复:在 route job 的 if 中排除 PR
🤖 Prompt for AI Agents