Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d4016a517 | ||
|
|
ba35d0ccfe | ||
|
|
da7518be54 | ||
|
|
3a8377f799 | ||
|
|
ecdcb671de | ||
|
|
6c912f5e18 | ||
|
|
015f5bcfd3 | ||
|
|
1de57bcf48 | ||
|
|
091538eaa5 | ||
|
|
c38c575d8e | ||
|
|
b1c1e49cc5 |
@@ -185,15 +185,15 @@ class H(BaseHTTPRequestHandler):
|
||||
if path in ('/api/openapi.yaml','/api/openapi-chatgpt-actions.yaml','/api/schema'):
|
||||
return self.sendt(OPENAPI.read_text(encoding='utf-8'), 'application/yaml; charset=utf-8') if OPENAPI.exists() else self.sendj({'error':'schema_missing'},404)
|
||||
if path in ('/api','/api/status'): return self.sendj(api_spec())
|
||||
if path in ('/api/base','/base'):
|
||||
if path in ('/api/base','/api/baseline','/base'):
|
||||
data=jload(WS/'baseline'/'current.json', {}); data.setdefault('git', sh(['rev-parse','--short','HEAD'])['stdout']); return self.sendj(data)
|
||||
if path in ('/api/ver','/ver'):
|
||||
if path in ('/api/ver','/api/versions','/ver'):
|
||||
return self.sendj({'tags':sh(['tag','--list','V*'])['stdout'].splitlines(),'versions_dir':[p.name for p in (WS/'versions').glob('*')],'head':sh(['rev-parse','--short','HEAD'])['stdout']})
|
||||
if path in ('/api/diff','/diff'):
|
||||
frm=(q.get('from') or ['HEAD~1'])[0]; to=(q.get('to') or ['HEAD'])[0]; return self.sendj({'from':frm,'to':to,'diff':sh(['diff',frm,to])})
|
||||
if path in ('/api/patch','/patch'): return self.sendj({'patches':patches(),'branches':sh(['branch','--format','%(refname:short)'])['stdout'].splitlines()})
|
||||
if path in ('/api/patch','/api/patches','/api/branches','/patch'): return self.sendj({'patches':patches(),'branches':sh(['branch','--format','%(refname:short)'])['stdout'].splitlines()})
|
||||
if path in ('/api/model','/model'): return self.sendj({'base':BASE_MODELS,'custom':model_docs(),'priority':[jload(p,{}) for p in sorted((WS/'config'/'model_priority').glob('*.json'))]})
|
||||
if path in ('/api/run','/run'):
|
||||
if path in ('/api/run','/api/runtime-status','/run'):
|
||||
svc=subprocess.run(['systemctl','is-active','aios-gitops-model-api'], text=True, capture_output=True)
|
||||
return self.sendj({'service':'aios-gitops-model-api','status':svc.stdout.strip() or svc.stderr.strip(),'workspace_branch':sh(['rev-parse','--abbrev-ref','HEAD'])['stdout']})
|
||||
if path in ('/api/action-monitor','/action-monitor'):
|
||||
@@ -211,11 +211,11 @@ class H(BaseHTTPRequestHandler):
|
||||
def do_POST(self):
|
||||
path=urllib.parse.urlparse(self.path).path.rstrip('/'); data=self.body()
|
||||
try:
|
||||
if path == '/api/branch': return self.branch(data)
|
||||
if path == '/api/file': return self.file(data)
|
||||
if path in ('/api/branch','/api/create-branch'): return self.branch(data)
|
||||
if path in ('/api/file','/api/commit-file'): return self.file(data)
|
||||
if path == '/api/commit': return self.commit(data)
|
||||
if path == '/api/patch': return self.patch(data)
|
||||
if path == '/api/tag': return self.tag(data)
|
||||
if path in ('/api/patch','/api/create-patch'): return self.patch(data)
|
||||
if path in ('/api/tag','/api/create-tag'): return self.tag(data)
|
||||
if path == '/api/rollback': return self.rollback(data)
|
||||
if path == '/api/model': return self.model(data)
|
||||
if path == '/api/model-priority': return self.model_priority(data)
|
||||
@@ -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
|
||||
|
||||
+10
-20
@@ -1,22 +1,12 @@
|
||||
{
|
||||
"version": "V1R1C0",
|
||||
"architecture_version": "V1",
|
||||
"release_version": "R1",
|
||||
"change_version": "C0",
|
||||
"components": [
|
||||
"webops",
|
||||
"owner-admin-user",
|
||||
"npm",
|
||||
"casdoor",
|
||||
"openclaw",
|
||||
"hermes",
|
||||
"one-api",
|
||||
"gitops-control",
|
||||
"baseline-registry",
|
||||
"custom-llm-registry",
|
||||
"runtime-selector"
|
||||
],
|
||||
"entry": "https://cnnetmkt.top",
|
||||
"proxy": "npm",
|
||||
"runtime_sync": "openclaw"
|
||||
"active_version": "V1R2C3",
|
||||
"active_tag": "V1R2C3",
|
||||
"previous_version": "V1R2C2",
|
||||
"rollback_version": "V1R2C2",
|
||||
"mode": "gitops-online",
|
||||
"update_mode": "git_patch_only",
|
||||
"zip_update_deprecated": true,
|
||||
"chatgpt_access": true,
|
||||
"codex_access": true,
|
||||
"updated_at": "2026-07-08T00:00:00Z"
|
||||
}
|
||||
|
||||
+7
-16
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"active_version": "V1R2C2",
|
||||
"active_tag": "V1R2C2",
|
||||
"active_version": "V1R2C3",
|
||||
"active_tag": "V1R2C3",
|
||||
"mode": "gitops-online",
|
||||
"version_policy": "VxRxCx",
|
||||
"update_mode": "git_patch_only",
|
||||
@@ -28,8 +28,8 @@
|
||||
"human_shortlinks_login_required": true,
|
||||
"autonomous_recovery_loop": true
|
||||
},
|
||||
"version": "V1R2C2",
|
||||
"baseline_tag": "V1R2C2-auto-recovery-baseline",
|
||||
"version": "V1R2C3",
|
||||
"baseline_tag": "V1R2C3",
|
||||
"source": "AIOS_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_PROD_FULL_DEPLOY_20260706.zip",
|
||||
"source_package": "AIOS_V1R1C0_MULTIUSER_GITOPS_CUSTOM_LLM_PROD_FULL_DEPLOY_20260706.zip",
|
||||
"zip_update_mode": "deprecated",
|
||||
@@ -47,7 +47,7 @@
|
||||
"/api",
|
||||
"/hook"
|
||||
],
|
||||
"updated_at": "2026-07-07T19:45:00Z",
|
||||
"updated_at": "2026-07-08T00:00:00Z",
|
||||
"auth": {
|
||||
"primary_domain": "https://auth.cnnetmkt.top",
|
||||
"backup_domain": "https://auth.h.imruyi.top:8443",
|
||||
@@ -56,7 +56,7 @@
|
||||
"casdoor_application": "admin/aios-portal",
|
||||
"backup_domain_role": "standby-only"
|
||||
},
|
||||
"previous_version": "V1R2C1",
|
||||
"previous_version": "V1R2C2",
|
||||
"codex_gitops": {
|
||||
"config": "config/codex/gitops-online.json",
|
||||
"token_account": "codex-gitops-bot",
|
||||
@@ -96,14 +96,5 @@
|
||||
"owner_password_sync": "aios-gitea-owner-sync.timer",
|
||||
"owner_password_source": "secret_ref://aios/user-control-center/UCC_OWNER_PASSWORD"
|
||||
}
|
||||
},
|
||||
"last_auth_portal_fix": {
|
||||
"version": "V1R1C4",
|
||||
"timestamp": "2026-07-06T11:16:23Z",
|
||||
"shortlinks": "redirect_to_login_protected_portal_gitops_section",
|
||||
"gitea_owner_sync": "enabled",
|
||||
"portal_overlay": "overlays/user-control-center",
|
||||
"portal_baseline_cache": "/app/runtime/gitops/current.json",
|
||||
"sync_log_secret_hygiene": "runuser_without_sudo_command_logging"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
gitea_branch_sync:
|
||||
purpose: keep Gitea branch index aligned with Git refs
|
||||
mode: reconcile_report_first
|
||||
automatic_database_write: false
|
||||
checks:
|
||||
- compare_git_refs_with_branch_index
|
||||
- verify_post_receive_hook
|
||||
- verify_push_path
|
||||
preferred_write_path:
|
||||
method: gitea_ssh_or_http_push
|
||||
reason: trigger_native_gitea_hooks
|
||||
fallback:
|
||||
method: approved_operator_reconciliation
|
||||
requires_owner_approval: true
|
||||
security:
|
||||
secrets_in_git: false
|
||||
direct_main_push: false
|
||||
@@ -0,0 +1,103 @@
|
||||
"""Gitea branch index reconciliation helper.
|
||||
|
||||
Purpose:
|
||||
- compare Git refs in a bare repository with Gitea visible branch index
|
||||
- generate a reconciliation report
|
||||
- provide safe operator guidance
|
||||
|
||||
This tool defaults to dry-run. It does not store credentials, push main, or
|
||||
modify production databases automatically.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
from typing import Any
|
||||
|
||||
|
||||
def utc_now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BranchRef:
|
||||
name: str
|
||||
commit_id: str
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
@dataclass
|
||||
class BranchIndexReport:
|
||||
git_branch_count: int
|
||||
indexed_branch_count: int
|
||||
missing_branches: list[BranchRef] = field(default_factory=list)
|
||||
generated_at: str = field(default_factory=utc_now)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"git_branch_count": self.git_branch_count,
|
||||
"indexed_branch_count": self.indexed_branch_count,
|
||||
"missing_branches": [b.to_dict() for b in self.missing_branches],
|
||||
"generated_at": self.generated_at,
|
||||
}
|
||||
|
||||
|
||||
class GitBranchReader:
|
||||
def __init__(self, bare_repo: str):
|
||||
self.bare_repo = Path(bare_repo)
|
||||
|
||||
def read(self) -> list[BranchRef]:
|
||||
result = subprocess.run(
|
||||
[
|
||||
"git",
|
||||
f"--git-dir={self.bare_repo}",
|
||||
"for-each-ref",
|
||||
"--format=%(refname:short)|%(objectname)",
|
||||
"refs/heads",
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
branches = []
|
||||
for line in result.stdout.splitlines():
|
||||
if "|" not in line:
|
||||
continue
|
||||
name, commit_id = line.split("|", 1)
|
||||
branches.append(BranchRef(name, commit_id))
|
||||
return branches
|
||||
|
||||
|
||||
class GiteaBranchIndexReconciler:
|
||||
def __init__(self, git_reader: GitBranchReader):
|
||||
self.git_reader = git_reader
|
||||
|
||||
def compare(self, indexed_branches: dict[str, str]) -> BranchIndexReport:
|
||||
refs = self.git_reader.read()
|
||||
missing = [branch for branch in refs if branch.name not in indexed_branches]
|
||||
return BranchIndexReport(
|
||||
git_branch_count=len(refs),
|
||||
indexed_branch_count=len(indexed_branches),
|
||||
missing_branches=missing,
|
||||
)
|
||||
|
||||
def generate_operator_plan(self, report: BranchIndexReport) -> dict[str, Any]:
|
||||
return {
|
||||
"action": "reconcile_gitea_branch_index",
|
||||
"mode": "dry_run",
|
||||
"missing": [branch.to_dict() for branch in report.missing_branches],
|
||||
"recommendation": [
|
||||
"verify Gitea post-receive hook execution",
|
||||
"prefer Gitea SSH/HTTP push path for future writes",
|
||||
"apply DB/index repair only through approved operator procedure",
|
||||
],
|
||||
"created_at": utc_now(),
|
||||
}
|
||||
|
||||
|
||||
__all__ = ["BranchRef", "BranchIndexReport", "GitBranchReader", "GiteaBranchIndexReconciler"]
|
||||
@@ -0,0 +1,51 @@
|
||||
"""Supervisor for Gitea branch index drift detection."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
from .gitea_branch_index_sync import BranchIndexReport
|
||||
|
||||
|
||||
def utc_now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
@dataclass
|
||||
class BranchSyncState:
|
||||
checks: int = 0
|
||||
drift_detected: int = 0
|
||||
last_status: str = "idle"
|
||||
last_run: str | None = None
|
||||
alerts: list[str] = field(default_factory=list)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
class GiteaBranchSyncSupervisor:
|
||||
def __init__(self):
|
||||
self.state = BranchSyncState()
|
||||
|
||||
def evaluate(self, report: BranchIndexReport) -> dict[str, Any]:
|
||||
self.state.checks += 1
|
||||
self.state.last_run = utc_now()
|
||||
if report.missing_branches:
|
||||
self.state.drift_detected += 1
|
||||
self.state.last_status = "drift_detected"
|
||||
self.state.alerts.append(
|
||||
f"{len(report.missing_branches)} git refs missing from Gitea branch index"
|
||||
)
|
||||
else:
|
||||
self.state.last_status = "healthy"
|
||||
return {
|
||||
"status": self.state.last_status,
|
||||
"state": self.state.to_dict(),
|
||||
"report": report.to_dict(),
|
||||
"action": "repair_required" if report.missing_branches else "none",
|
||||
}
|
||||
|
||||
|
||||
__all__ = ["BranchSyncState", "GiteaBranchSyncSupervisor"]
|
||||
@@ -0,0 +1,73 @@
|
||||
"""Gitea push adapter plan for GitOps writes.
|
||||
|
||||
The adapter does not execute network pushes by itself. It builds a safe push plan
|
||||
that routes future GitOps writes through Gitea SSH/HTTP instead of direct bare
|
||||
repo writes, so Gitea native hooks can update branch indexes.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
|
||||
def utc_now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GiteaPushPlan:
|
||||
remote_url: str
|
||||
branch: str
|
||||
refspec: str
|
||||
push_method: str
|
||||
direct_bare_write: bool
|
||||
expected_hook: str
|
||||
owner_approval_required: bool
|
||||
created_at: str = field(default_factory=utc_now)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
class GiteaPushAdapter:
|
||||
"""Builds governance-safe plans for future GitOps push execution."""
|
||||
|
||||
ALLOWED_METHODS = {"ssh", "https"}
|
||||
|
||||
def __init__(self, remote_url: str, push_method: str = "ssh"):
|
||||
if push_method not in self.ALLOWED_METHODS:
|
||||
raise ValueError("push_method must be ssh or https")
|
||||
self.remote_url = remote_url
|
||||
self.push_method = push_method
|
||||
|
||||
def build_plan(self, branch: str, owner_approval_required: bool = True) -> GiteaPushPlan:
|
||||
if not branch.startswith(("patch/", "request/", "test/")):
|
||||
raise ValueError("only patch/request/test branches may be pushed by GitOps")
|
||||
return GiteaPushPlan(
|
||||
remote_url=self.remote_url,
|
||||
branch=branch,
|
||||
refspec=f"HEAD:refs/heads/{branch}",
|
||||
push_method=self.push_method,
|
||||
direct_bare_write=False,
|
||||
expected_hook="post-receive",
|
||||
owner_approval_required=owner_approval_required,
|
||||
)
|
||||
|
||||
def validate_plan(self, plan: GiteaPushPlan) -> dict[str, Any]:
|
||||
return {
|
||||
"valid": (
|
||||
plan.direct_bare_write is False
|
||||
and plan.push_method in self.ALLOWED_METHODS
|
||||
and plan.branch.startswith(("patch/", "request/", "test/"))
|
||||
),
|
||||
"remote_url": plan.remote_url,
|
||||
"branch": plan.branch,
|
||||
"expected_hook": plan.expected_hook,
|
||||
"direct_bare_write": plan.direct_bare_write,
|
||||
"owner_approval_required": plan.owner_approval_required,
|
||||
}
|
||||
|
||||
|
||||
__all__ = ["GiteaPushPlan", "GiteaPushAdapter"]
|
||||
@@ -0,0 +1,51 @@
|
||||
# Gitea Branch Index Repair Runbook
|
||||
|
||||
## Goal
|
||||
|
||||
Repair Gitea Web branch visibility when Git refs exist in the bare repository but
|
||||
Gitea's branch index/table is stale.
|
||||
|
||||
## Confirmed root cause
|
||||
|
||||
- Gitea repository ROOT matches the GitOps bare repo path.
|
||||
- Branch refs exist in the same bare repo.
|
||||
- Gitea Web does not show some branches because the Gitea branch index/table is stale.
|
||||
- Older GitOps writes likely bypassed Gitea hooks; newer writes through the Gitea HTTP gateway are visible.
|
||||
|
||||
## Important warning
|
||||
|
||||
A no-op push of an existing ref at the same commit may not trigger Gitea
|
||||
post-receive hooks because Git can return `Everything up-to-date`. Do not rely on
|
||||
a no-op push as the only repair mechanism.
|
||||
|
||||
## Preferred repair order
|
||||
|
||||
1. Back up the Gitea database.
|
||||
2. Try Gitea official repository sync/rebuild/admin command if available.
|
||||
3. If no official command is available, reconcile branch index records from Git refs:
|
||||
- insert missing branch records only;
|
||||
- do not delete existing records;
|
||||
- do not change main;
|
||||
- do not create tags;
|
||||
- preserve current branch commit IDs.
|
||||
4. Verify target branches are visible in Gitea Web.
|
||||
5. Verify main HEAD remains unchanged.
|
||||
|
||||
## Target branches to verify
|
||||
|
||||
- `patch/user-portal-rbac-ops-dashboard`
|
||||
- `request/tag-V1R3C0-self-loop-autonomous-engine`
|
||||
|
||||
## Long-term fix
|
||||
|
||||
Future GitOps writes must use the Gitea SSH/HTTP push path so Gitea native hooks
|
||||
update branch indexes automatically. Direct bare repository writes should be
|
||||
restricted to emergency/operator procedures.
|
||||
|
||||
## Forbidden actions
|
||||
|
||||
- Do not push main.
|
||||
- Do not create release tags during this repair.
|
||||
- Do not store secrets in Git.
|
||||
- Do not rewrite branch history unless owner explicitly approves a separate plan.
|
||||
- Do not change authentication/SSO configuration as part of this repair.
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"type": "branch",
|
||||
"branch": "patch/gitea-branch-index-sync",
|
||||
"base": "main",
|
||||
"created_by": "chatgpt-gitops-bot",
|
||||
"created_at": "2026-07-08T05:19:40Z",
|
||||
"direct_main_push": false,
|
||||
"owner_approval_required": true
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
from ops.gitea_branch_sync_supervisor import GiteaBranchSyncSupervisor
|
||||
from ops.gitea_branch_index_sync import BranchIndexReport, BranchRef
|
||||
from ops.gitea_push_adapter import GiteaPushAdapter
|
||||
|
||||
|
||||
def test_branch_drift_detection():
|
||||
report = BranchIndexReport(
|
||||
git_branch_count=49,
|
||||
indexed_branch_count=11,
|
||||
missing_branches=[BranchRef("patch/user-portal-rbac-ops-dashboard", "ac5518588f98")],
|
||||
)
|
||||
|
||||
result = GiteaBranchSyncSupervisor().evaluate(report)
|
||||
assert result["status"] == "drift_detected"
|
||||
|
||||
|
||||
def test_gitea_push_plan_uses_hooks():
|
||||
plan = GiteaPushAdapter("ssh://g.cnnetmkt.top/aios/aios-v23.git").build_plan(
|
||||
"patch/test-sync"
|
||||
)
|
||||
assert plan.direct_bare_write is False
|
||||
assert plan.expected_hook == "post-receive"
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"version": "V1R2C3",
|
||||
"tag": "V1R2C3",
|
||||
"previous_version": "V1R2C2",
|
||||
"rollback_version": "V1R2C2",
|
||||
"release_reason": "GPT Actions ClientResponseError compatibility recovery",
|
||||
"validated_actions": [
|
||||
"getRuntimeStatus",
|
||||
"getBaseline",
|
||||
"listVersions",
|
||||
"listPatches",
|
||||
"getDiff",
|
||||
"createBranch",
|
||||
"createPatch",
|
||||
"commitFile"
|
||||
],
|
||||
"secret_plaintext_written": false,
|
||||
"created_at": "2026-07-08T00:00:00Z",
|
||||
"author": "openclaw"
|
||||
}
|
||||
Reference in New Issue
Block a user