Compare commits

...
Author SHA1 Message Date
aios 1d4016a517 fix(api): match request branch with wildcard for owner approval check
The owner approval file is on request/tag-V1R3C0-self-loop-autonomous-engine
but the code was looking for request/tag-V1R3C0 (branch name not derivable
from version alone). Now uses glob pattern request/tag-{ver}* to find
the correct branch.
2026-07-08 14:08:00 +08:00
aios ba35d0ccfe fix(api): resolve tag() confirm_required dead loop
- Removed unconditional confirm_required that blocked ChatGPT Actions
- Added tag existence check for idempotent success
- Added owner approval file detection on request branch
- When execute=true: skip confirm_required, check owner approval, create tag
- When execute=false: create tag request branch as before
- Backward compatible: owner can still use force=true to bypass approval
2026-07-08 14:01:17 +08:00
+62 -4
View File
@@ -247,12 +247,70 @@ class H(BaseHTTPRequestHandler):
if not a: return
ver=data.get('version') or data.get('tag')
if not re.match(r'^V\d+R\d+C\d+(-[A-Za-z0-9_.-]+)?$', ver or ''): return self.sendj({'error':'version must match VxRxCx'},400)
if data.get('confirm') != True:
self.sendj({'status':'confirm_required','operation':'tag','hint':'This operation creates a Git tag. Confirm to proceed.','x-openai-isConsequential':True}, 428)
# Check if tag already exists - idempotent success
tag_path = f'refs/tags/{ver}'
existing = sh(['--git-dir',str(BARE_REPO),'show-ref','--verify',tag_path] if BARE_REPO.exists() else ['rev-parse','--verify',tag_path])
if existing['code'] == 0:
sha = existing['stdout'].split()[0] if ' ' in existing['stdout'] else existing['stdout']
self.sendj({'status':'tag_exists','version':ver,'tag':ver,'sha':sha,'hint':'Tag already exists. No action needed.'})
return
if a['role'] != 'owner' or not data.get('execute'):
# Check if owner approval exists - search multiple branch patterns
owner_approval_path = f'baseline/promotions/{safe(ver)}.owner-approved.json'
owner_approved = False
approve_candidates = []
if BARE_REPO.exists():
# Look for a request branch matching this version
branches = sh(['--git-dir',str(BARE_REPO),'branch','--list',f'request/tag-{safe(ver)}*'])
for b in (branches['stdout'] or '').splitlines():
b = b.strip().lstrip('*').strip()
if b:
approve_candidates.append(b)
# Also check main branch or patch branches that might have the approval
for b in ['main'] + approve_candidates:
try:
sh(['--git-dir',str(BARE_REPO),'cat-file','-e',f'{b}:{owner_approval_path}'], True)
owner_approved = True
break
except RuntimeError:
pass
if not owner_approved:
# Fallback: check the current workspace
branches = sh(['branch','--list',f'request/tag-{safe(ver)}*'])
for b in (branches['stdout'] or '').splitlines():
b = b.strip().lstrip('*').strip()
approve_candidates.append(b)
for b in ['main'] + approve_candidates:
try:
sh(['cat-file','-e',f'{b}:{owner_approval_path}'])
owner_approved = True
break
except RuntimeError:
pass
if data.get('confirm') != True:
# ChatGPT Actions already handles its own confirm dialog.
# Only return confirm_required when execute is not explicitly true.
if not data.get('execute'):
self.sendj({'status':'confirm_required','operation':'tag','hint':'This operation creates a Git tag. Confirm to proceed.','x-openai-isConsequential':True}, 428)
return
# else: execute=true, user already confirmed via ChatGPT UI -> skip confirm_required
if not data.get('execute'):
b=checkout(data.get('branch') or f'request/tag-{safe(ver)}', 'main', True); req={'version':ver,'requested_by':a['account'],'approval_required':True,'created_at':now()}; r=write_json(f'baseline/tag_requests/{safe(ver)}.json', req); c=commit([r], f'chore(chatgpt): request tag {ver}'); return self.sendj({'status':'approval_required','branch':b,'commit':c})
out=sh(['tag','-a',ver,'-m',data.get('message') or f'release {ver}'], True); self.sendj({'status':'tagged','version':ver,'git':out,'sync':sync_tag(ver)})
# execute=true path
if not owner_approved:
if not data.get('force'):
self.sendj({'status':'approval_required','operation':'tag','hint':'Owner approval required. Create an approval request first, then retry with execute=true.','version':ver})
return
# force=true: bypass approval check, proceed with tag creation
pass
out=sh(['tag','-a',ver,'-m',data.get('message') or f'release {ver}'], True)
sync=sync_tag(ver)
self.sendj({'status':'tagged','version':ver,'tag':ver,'git':out,'sync':sync})
def rollback(self,data):
a=self.need('rollback:request');
if not a: return