feat: use gitea as source authority

This commit is contained in:
Codex
2026-07-05 09:46:05 +00:00
parent 345b1cb7bb
commit d5a97318a3
5 changed files with 1253 additions and 17 deletions
+124 -3
View File
@@ -4,10 +4,11 @@ kind: platform-infra-gitea
metadata:
id: gitea-internal-mirror
owner: unidesk
spec: GH-1548
spec: GH-1548/GH-1550
relatedIssues:
- 1548
- 1549
- 1550
defaults:
targetId: JD01
@@ -20,6 +21,100 @@ migration:
buildPlane: controlled-docker-or-buildkit-outside-runtime
runtimePlane: k3s-gitea-service-zero-docker
sourceAuthority:
enabled: true
stage: gitea-controlled-mirror-source-authority
statusAuthority: gitea-repository-refs-plus-snapshot-ref
firstCiConsumer: agentrun-jd01-v02
credentials:
sourceRoot: /root/unidesk
admin:
sourceRef: .env/gitea.auth
format: line-pair
usernameLine: 1
passwordLine: 2
requiredFor:
- repo-bootstrap
- mirror-sync
- snapshot-create
github:
transport: https-token
sourceRef: /root/.config/unidesk/github.env
sourceKey: GH_TOKEN
requiredFor:
- upstream-mirror
- mirror-sync
githubProxy:
enabled: true
url: http://127.0.0.1:10808
noProxy:
- 127.0.0.1
- localhost
- .svc
- .svc.cluster.local
responsibilities:
- name: source-read
current: legacy-git-mirror
target: gitea
disposition: replaced-by-gitea
- name: branch-observation
current: branch-follower
target: gitea-mirror
disposition: replaced-by-gitea
- name: immutable-snapshot-ref
current: legacy-git-mirror-cache
target: gitea-ref
disposition: replaced-by-gitea
- name: gitops-flush
current: legacy-git-mirror
target: github-direct-or-gitea-writeback
disposition: retained-for-gitops-flush
- name: legacy-status
current: branch-follower-status
target: gitea-mirror-status
disposition: migration-readonly
repositories:
- key: agentrun-jd01-v02
targetId: JD01
upstream:
repository: pikasTech/agentrun
cloneUrl: https://github.com/pikasTech/agentrun.git
branch: v0.2
gitea:
owner: mirrors
name: pikasTech-agentrun
mirrorMode: controlled-push
readUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-agentrun.git
gitops:
branch: jd01-v0.2-gitops
flushDisposition: retained-for-gitops-flush
snapshot:
prefix: refs/unidesk/snapshots/gitea-actions/agentrun-v0.2
legacyGitMirror:
readUrl: http://git-mirror-http.devops-infra.svc.cluster.local:8080/pikasTech/agentrun.git
configRef: config/cicd-branch-followers.yaml#followers.agentrun-jd01-v02.nativeStatus.source.gitMirrorReadUrl
disposition: replaced-by-gitea
- key: unidesk-master
targetId: JD01
upstream:
repository: pikasTech/unidesk
cloneUrl: https://github.com/pikasTech/unidesk.git
branch: master
gitea:
owner: mirrors
name: pikasTech-unidesk
mirrorMode: controlled-push
readUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-unidesk.git
gitops:
branch: master
flushDisposition: not-a-gitops-branch
snapshot:
prefix: refs/unidesk/snapshots/gitea-actions/unidesk-master
legacyGitMirror:
readUrl: http://git-mirror-http.devops-infra.svc.cluster.local:8080/pikasTech/unidesk.git
configRef: config/cicd-branch-followers.yaml#controller.source.gitMirrorReadUrl
disposition: replaced-by-gitea
targets:
- id: JD01
route: JD01:k3s
@@ -43,11 +138,37 @@ app:
httpPort: 3000
sshPort: 2222
server:
domain: gitea-http.devops-infra.svc.cluster.local
rootUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/
domain: gitea.pikapython.com
rootUrl: https://gitea.pikapython.com/
sshDomain: gitea-http.devops-infra.svc.cluster.local
protocol: http
startSshServer: true
publicExposure:
enabled: true
publicBaseUrl: https://gitea.pikapython.com
secretRoot: /root/unidesk/.state/secrets
dns:
hostname: gitea.pikapython.com
expectedA: 82.156.23.220
resolvers: [1.1.1.1, 8.8.8.8, 223.5.5.5, 114.114.114.114]
frpc:
deploymentName: gitea-frpc
secretName: gitea-frpc-secrets
secretKey: frpc.toml
image: fatedier/frpc:v0.68.1
serverAddr: 82.156.23.220
serverPort: 22000
proxyName: platform-infra-gitea-jd01-web
remotePort: 22080
localIP: gitea-http.devops-infra.svc.cluster.local
localPort: 3000
tokenSourceRef: platform-infra/pk01-frp.env
tokenSourceKey: FRP_TOKEN
pk01:
route: PK01
caddyConfigPath: /etc/caddy/Caddyfile
caddyServiceName: caddy
responseHeaderTimeoutSeconds: 600
database:
type: sqlite3
path: /var/lib/gitea/gitea.db
+3
View File
@@ -693,6 +693,9 @@ function platformInfraHelpSummary(): unknown {
"bun scripts/cli.ts platform-infra gitea apply --target JD01 --confirm",
"bun scripts/cli.ts platform-infra gitea status --target JD01",
"bun scripts/cli.ts platform-infra gitea validate --target JD01",
"bun scripts/cli.ts platform-infra gitea mirror plan --target JD01",
"bun scripts/cli.ts platform-infra gitea mirror sync --target JD01 --confirm",
"bun scripts/cli.ts platform-infra gitea mirror status --target JD01",
],
description: "Operate platform-infra services such as Sub2API, shared egress-proxy benchmarks, LangBot, n8n, WeChat archive workflows, the YAML-controlled Codex pool, and internal Gitea for the GH-1548/GH-1549 CI/CD migration.",
};
+334 -7
View File
@@ -2,6 +2,7 @@
set -u
tmp="$(mktemp -d)"
export tmp
trap 'rm -rf "$tmp"' EXIT
json_tail() {
@@ -29,12 +30,33 @@ capture_json() {
run_apply() {
manifest="$tmp/gitea.k8s.yaml"
printf '%s' "$UNIDESK_GITEA_MANIFEST_B64" | base64 -d >"$manifest"
if [ "$UNIDESK_GITEA_FRPC_ENABLED" = "1" ] && [ "$UNIDESK_GITEA_DRY_RUN" != "1" ]; then
printf '%s' "$UNIDESK_GITEA_FRPC_TOML_B64" | base64 -d >"$tmp/frpc.toml"
kubectl -n "$UNIDESK_GITEA_NAMESPACE" create secret generic "$UNIDESK_GITEA_FRPC_SECRET_NAME" \
--from-file="$UNIDESK_GITEA_FRPC_SECRET_KEY=$tmp/frpc.toml" \
--dry-run=client -o yaml | kubectl apply --server-side --force-conflicts --field-manager="$UNIDESK_GITEA_FIELD_MANAGER" -f - >"$tmp/frpc-secret.out" 2>"$tmp/frpc-secret.err"
frpc_rc=$?
else
frpc_rc=0
: >"$tmp/frpc-secret.out"
if [ "$UNIDESK_GITEA_FRPC_ENABLED" = "1" ]; then
printf '%s\n' "dry-run: frpc secret apply skipped" >"$tmp/frpc-secret.err"
else
printf '%s\n' "frpc disabled" >"$tmp/frpc-secret.err"
fi
fi
dry_arg=""
if [ "$UNIDESK_GITEA_DRY_RUN" = "1" ]; then
dry_arg="--dry-run=server"
fi
timeout "$UNIDESK_GITEA_WAIT_TIMEOUT_SECONDS" kubectl apply --server-side $dry_arg --force-conflicts --field-manager="$UNIDESK_GITEA_FIELD_MANAGER" -f "$manifest" >"$tmp/apply.out" 2>"$tmp/apply.err"
apply_rc=$?
if [ "$frpc_rc" -eq 0 ]; then
timeout "$UNIDESK_GITEA_WAIT_TIMEOUT_SECONDS" kubectl apply --server-side $dry_arg --force-conflicts --field-manager="$UNIDESK_GITEA_FIELD_MANAGER" -f "$manifest" >"$tmp/apply.out" 2>"$tmp/apply.err"
apply_rc=$?
else
apply_rc=1
: >"$tmp/apply.out"
printf '%s\n' "frpc secret apply failed; manifest apply skipped" >"$tmp/apply.err"
fi
if [ "$apply_rc" -eq 0 ] && [ "$UNIDESK_GITEA_WAIT" = "1" ] && [ "$UNIDESK_GITEA_DRY_RUN" != "1" ]; then
timeout "$UNIDESK_GITEA_WAIT_TIMEOUT_SECONDS" kubectl -n "$UNIDESK_GITEA_NAMESPACE" rollout status "statefulset/$UNIDESK_GITEA_STATEFULSET_NAME" --timeout="${UNIDESK_GITEA_WAIT_TIMEOUT_SECONDS}s" >"$tmp/rollout.out" 2>"$tmp/rollout.err"
rollout_rc=$?
@@ -47,9 +69,9 @@ run_apply() {
printf '%s\n' "rollout wait not requested" >"$tmp/rollout.err"
fi
fi
python3 - "$apply_rc" "$rollout_rc" "$tmp/apply.out" "$tmp/apply.err" "$tmp/rollout.out" "$tmp/rollout.err" <<'PY'
python3 - "$frpc_rc" "$apply_rc" "$rollout_rc" "$tmp/frpc-secret.out" "$tmp/frpc-secret.err" "$tmp/apply.out" "$tmp/apply.err" "$tmp/rollout.out" "$tmp/rollout.err" <<'PY'
import json, os, sys
apply_rc, rollout_rc = int(sys.argv[1]), int(sys.argv[2])
frpc_rc, apply_rc, rollout_rc = int(sys.argv[1]), int(sys.argv[2]), int(sys.argv[3])
def text(path, limit=3000):
try:
return open(path, encoding="utf-8", errors="replace").read()[-limit:]
@@ -57,7 +79,7 @@ def text(path, limit=3000):
return ""
dry = os.environ.get("UNIDESK_GITEA_DRY_RUN") == "1"
payload = {
"ok": apply_rc == 0 and rollout_rc == 0,
"ok": frpc_rc == 0 and apply_rc == 0 and rollout_rc == 0,
"target": os.environ.get("UNIDESK_GITEA_TARGET_ID"),
"route": os.environ.get("UNIDESK_GITEA_ROUTE"),
"namespace": os.environ.get("UNIDESK_GITEA_NAMESPACE"),
@@ -70,8 +92,9 @@ payload = {
"networkPolicy": "allow-all",
},
"steps": {
"apply": {"exitCode": apply_rc, "stdoutTail": text(sys.argv[3]), "stderrTail": text(sys.argv[4])},
"rollout": {"exitCode": rollout_rc, "stdoutTail": text(sys.argv[5]), "stderrTail": text(sys.argv[6])},
"frpcSecret": {"exitCode": frpc_rc, "stdoutTail": text(sys.argv[4]), "stderrTail": text(sys.argv[5])},
"apply": {"exitCode": apply_rc, "stdoutTail": text(sys.argv[6]), "stderrTail": text(sys.argv[7])},
"rollout": {"exitCode": rollout_rc, "stdoutTail": text(sys.argv[8]), "stderrTail": text(sys.argv[9])},
},
"valuesPrinted": False,
}
@@ -238,9 +261,313 @@ sys.exit(0 if ok else 1)
PY
}
service_base_url() {
kubectl -n "$UNIDESK_GITEA_NAMESPACE" get svc "$UNIDESK_GITEA_SERVICE_NAME" -o json >"$tmp/gitea-svc.json" 2>"$tmp/gitea-svc.err"
python3 - "$tmp/gitea-svc.json" "$UNIDESK_GITEA_HTTP_PORT" <<'PY'
import json, sys
data = json.load(open(sys.argv[1], encoding="utf-8"))
print(f"http://{data['spec']['clusterIP']}:{sys.argv[2]}")
PY
}
mirror_repos_json() {
printf '%s' "$UNIDESK_GITEA_REPOS_B64" | base64 -d >"$tmp/repos.json"
}
admin_basic_b64() {
python3 - <<'PY'
import base64, os
login = os.environ["UNIDESK_GITEA_ADMIN_USERNAME"]
username = login.split("@", 1)[0] if "@" in login else login
raw = f"{username}:{os.environ['UNIDESK_GITEA_ADMIN_PASSWORD']}".encode()
print(base64.b64encode(raw).decode())
PY
}
admin_cli_username() {
python3 - <<'PY'
import os
login = os.environ["UNIDESK_GITEA_ADMIN_USERNAME"]
print(login.split("@", 1)[0] if "@" in login else login)
PY
}
run_mirror_bootstrap() {
mirror_repos_json
pod="$(kubectl -n "$UNIDESK_GITEA_NAMESPACE" get pod -l "app.kubernetes.io/name=$UNIDESK_GITEA_APP_NAME,app.kubernetes.io/component=gitea" -o jsonpath='{.items[0].metadata.name}' 2>"$tmp/pod.err")"
create_rc=1
change_rc=1
unset_rc=1
if [ -n "$pod" ]; then
admin_username="$(admin_cli_username)"
admin_email="$UNIDESK_GITEA_ADMIN_USERNAME"
case "$admin_email" in
*@*) ;;
*) admin_email="$admin_username@local" ;;
esac
kubectl -n "$UNIDESK_GITEA_NAMESPACE" exec "$pod" -- gitea --work-path /var/lib/gitea --config /etc/gitea/app.ini admin user create --username "$admin_username" --password "$UNIDESK_GITEA_ADMIN_PASSWORD" --email "$admin_email" --admin --must-change-password=false >"$tmp/admin-create.out" 2>"$tmp/admin-create.err"
create_rc=$?
if [ "$create_rc" -ne 0 ] && grep -qi 'user already exists' "$tmp/admin-create.err"; then
create_rc=0
fi
kubectl -n "$UNIDESK_GITEA_NAMESPACE" exec "$pod" -- gitea --work-path /var/lib/gitea --config /etc/gitea/app.ini admin user change-password --username "$admin_username" --password "$UNIDESK_GITEA_ADMIN_PASSWORD" --must-change-password=false >"$tmp/admin-pass.out" 2>"$tmp/admin-pass.err"
change_rc=$?
kubectl -n "$UNIDESK_GITEA_NAMESPACE" exec "$pod" -- gitea --work-path /var/lib/gitea --config /etc/gitea/app.ini admin user must-change-password --unset "$admin_username" >"$tmp/admin-unset.out" 2>"$tmp/admin-unset.err"
unset_rc=$?
else
printf '%s\n' "gitea pod not found" >"$tmp/admin-create.err"
: >"$tmp/admin-create.out"
: >"$tmp/admin-pass.out"
: >"$tmp/admin-pass.err"
: >"$tmp/admin-unset.out"
: >"$tmp/admin-unset.err"
fi
base_url="$(service_base_url)"
basic="$(admin_basic_b64)"
python3 - "$base_url" "$basic" "$tmp/repos.json" "$tmp/api.json" <<'PY'
import json, sys, traceback, urllib.error, urllib.request
base_url, basic, repos_path, out_path = sys.argv[1:5]
def request(method, path, payload=None, expected=(200, 201, 204), tolerate=()):
data = None if payload is None else json.dumps(payload).encode()
req = urllib.request.Request(base_url + path, data=data, method=method)
req.add_header("Authorization", "Basic " + basic)
if payload is not None:
req.add_header("Content-Type", "application/json")
try:
with urllib.request.urlopen(req, timeout=20) as resp:
body = resp.read().decode("utf-8", errors="replace")
return {"ok": resp.status in expected, "status": resp.status, "body": body[:1000]}
except urllib.error.HTTPError as exc:
body = exc.read().decode("utf-8", errors="replace")
return {"ok": exc.code in tolerate, "status": exc.code, "body": body[:1000]}
try:
repos = json.load(open(repos_path, encoding="utf-8"))
orgs = {}
repositories = []
auth = request("GET", "/api/v1/user")
for repo in repos:
owner = repo["gitea"]["owner"]
if owner not in orgs:
orgs[owner] = request("POST", "/api/v1/orgs", {"username": owner, "full_name": "UniDesk internal mirrors", "visibility": "private"}, expected=(201,), tolerate=(409, 422))
repositories.append({
"key": repo["key"],
"owner": owner,
"name": repo["gitea"]["name"],
"create": request("POST", f"/api/v1/orgs/{owner}/repos", {"name": repo["gitea"]["name"], "private": False, "auto_init": False, "description": f"UniDesk controlled mirror for {repo['upstream']['repository']}"}, expected=(201,), tolerate=(409, 422)),
})
payload = {"auth": auth, "orgs": orgs, "repositories": repositories}
ok = auth.get("ok") and all(v.get("ok") for v in orgs.values()) and all(item["create"].get("ok") for item in repositories)
except Exception as exc:
payload = {
"auth": {"ok": False},
"orgs": {},
"repositories": [],
"error": str(exc),
"traceTail": traceback.format_exc()[-1600:],
}
ok = False
json.dump(payload, open(out_path, "w", encoding="utf-8"), ensure_ascii=False, indent=2)
sys.exit(0 if ok else 1)
PY
api_rc=$?
python3 - "$create_rc" "$change_rc" "$unset_rc" "$api_rc" "$tmp/admin-create.out" "$tmp/admin-create.err" "$tmp/admin-pass.out" "$tmp/admin-pass.err" "$tmp/admin-unset.out" "$tmp/admin-unset.err" "$tmp/api.json" <<'PY'
import json, sys
def text(path, limit=1600):
try:
return open(path, encoding="utf-8", errors="replace").read()[-limit:]
except FileNotFoundError:
return ""
create_rc, change_rc, unset_rc, api_rc = map(int, sys.argv[1:5])
try:
api = json.load(open(sys.argv[11], encoding="utf-8"))
except Exception:
api = {}
payload = {
"ok": create_rc == 0 and change_rc == 0 and unset_rc == 0 and api_rc == 0,
"target": __import__("os").environ.get("UNIDESK_GITEA_TARGET_ID"),
"namespace": __import__("os").environ.get("UNIDESK_GITEA_NAMESPACE"),
"steps": {
"adminCreate": {"exitCode": create_rc, "stdoutTail": text(sys.argv[5]), "stderrTail": text(sys.argv[6])},
"adminPassword": {"exitCode": change_rc, "stdoutTail": text(sys.argv[7]), "stderrTail": text(sys.argv[8])},
"adminMustChangePasswordUnset": {"exitCode": unset_rc, "stdoutTail": text(sys.argv[9]), "stderrTail": text(sys.argv[10])},
"apiBootstrap": {"exitCode": api_rc},
},
"api": api,
"repositories": api.get("repositories", []),
"valuesPrinted": False,
}
print(json.dumps(payload, ensure_ascii=False, indent=2))
sys.exit(0 if payload["ok"] else 1)
PY
}
run_mirror_sync() {
mirror_repos_json
base_url="$(service_base_url)"
basic="$(admin_basic_b64)"
python3 - "$tmp/repos.json" "$tmp/sync-plan.sh" "$tmp/repo-meta.json" "$base_url" <<'PY'
import json, shlex, sys, urllib.parse
repos = json.load(open(sys.argv[1], encoding="utf-8"))
base_url = sys.argv[4].rstrip("/")
script = []
meta = []
for i, repo in enumerate(repos):
key = repo["key"]
source_branch = repo["upstream"]["branch"]
gitops_branch = repo["gitops"]["branch"]
branches = [source_branch] + ([] if gitops_branch == source_branch or gitops_branch == "not-a-gitops-branch" else [gitops_branch])
work = f"$tmp/repo-{i}.git"
gitea_write_url = base_url + urllib.parse.urlparse(repo["gitea"]["readUrl"]).path
script += [
f"mkdir -p {shlex.quote(work)}",
f"git init --bare {shlex.quote(work)} >$tmp/{key}.init.out 2>$tmp/{key}.init.err; printf '%s' \"$?\" >$tmp/{key}.init.rc",
f"git -C {shlex.quote(work)} -c http.extraHeader=\"$GITHUB_AUTH_HEADER\" fetch --prune {shlex.quote(repo['upstream']['cloneUrl'])} " + " ".join(shlex.quote(f"+refs/heads/{b}:refs/remotes/github/{b}") for b in branches) + f" >$tmp/{key}.fetch.out 2>$tmp/{key}.fetch.err; fetch_rc=$?; printf '%s' \"$fetch_rc\" >$tmp/{key}.fetch.rc",
f"if [ \"$fetch_rc\" -eq 0 ]; then sha=$(git -C {shlex.quote(work)} rev-parse refs/remotes/github/{shlex.quote(source_branch)} 2>$tmp/{key}.revparse.err); revparse_rc=$?; printf '%s\\n' \"$sha\" >$tmp/{key}.revparse.out; else sha=''; revparse_rc=1; : >$tmp/{key}.revparse.out; printf '%s\\n' 'fetch failed; revparse skipped' >$tmp/{key}.revparse.err; fi; printf '%s' \"$revparse_rc\" >$tmp/{key}.revparse.rc",
f"if [ \"$revparse_rc\" -eq 0 ]; then snapshot_ref={shlex.quote(repo['snapshot']['prefix'])}/$sha; git -C {shlex.quote(work)} -c http.extraHeader=\"$GITEA_AUTH_HEADER\" push --force {shlex.quote(gitea_write_url)} $sha:refs/heads/{shlex.quote(source_branch)} $sha:$snapshot_ref >$tmp/{key}.push.out 2>$tmp/{key}.push.err; push_rc=$?; else snapshot_ref=''; push_rc=1; : >$tmp/{key}.push.out; printf '%s\\n' 'revparse failed; push skipped' >$tmp/{key}.push.err; fi; printf '%s' \"$push_rc\" >$tmp/{key}.push.rc",
]
if gitops_branch != source_branch and gitops_branch != "not-a-gitops-branch":
script.append(f"if [ \"$fetch_rc\" -eq 0 ]; then gitops_sha=$(git -C {shlex.quote(work)} rev-parse refs/remotes/github/{shlex.quote(gitops_branch)} 2>/dev/null || true); else gitops_sha=''; fi")
script.append(f"if [ -n \"$gitops_sha\" ] && [ \"$push_rc\" -eq 0 ]; then git -C {shlex.quote(work)} -c http.extraHeader=\"$GITEA_AUTH_HEADER\" push --force {shlex.quote(gitea_write_url)} $gitops_sha:refs/heads/{shlex.quote(gitops_branch)} >>$tmp/{key}.push.out 2>>$tmp/{key}.push.err; gitops_push_rc=$?; else gitops_push_rc=0; fi; printf '%s' \"$gitops_push_rc\" >$tmp/{key}.gitops-push.rc")
else:
script.append(f"printf '%s' '0' >$tmp/{key}.gitops-push.rc")
script.append(f"if [ \"$push_rc\" -eq 0 ]; then printf '%s %s\\n' \"$sha\" \"$snapshot_ref\" >$tmp/{key}.bundle; fi")
meta.append({"key": key, "sourceBranch": source_branch, "gitopsBranch": gitops_branch, "readUrl": repo["gitea"]["readUrl"], "writeUrlHost": urllib.parse.urlparse(gitea_write_url).netloc, "snapshotPrefix": repo["snapshot"]["prefix"], "legacyReadUrl": repo["legacyGitMirror"]["readUrl"]})
open(sys.argv[2], "w", encoding="utf-8").write("\n".join(script) + "\n")
json.dump(meta, open(sys.argv[3], "w", encoding="utf-8"), ensure_ascii=False, indent=2)
PY
github_basic="$(python3 - <<'PY'
import base64, os
raw = f"x-access-token:{os.environ['UNIDESK_GITEA_GITHUB_TOKEN']}".encode()
print(base64.b64encode(raw).decode())
PY
)"
GITHUB_AUTH_HEADER="Authorization: Basic $github_basic"
GITEA_AUTH_HEADER="Authorization: Basic $basic"
export GITHUB_AUTH_HEADER GITEA_AUTH_HEADER
if [ "$UNIDESK_GITEA_GITHUB_PROXY_ENABLED" = "1" ]; then
export https_proxy="$UNIDESK_GITEA_GITHUB_PROXY_URL"
export http_proxy="$UNIDESK_GITEA_GITHUB_PROXY_URL"
export no_proxy="$UNIDESK_GITEA_NO_PROXY"
fi
sh "$tmp/sync-plan.sh" >"$tmp/sync.out" 2>"$tmp/sync.err"
sync_rc=$?
python3 - "$sync_rc" "$tmp/repo-meta.json" "$tmp" <<'PY'
import json, os, sys
sync_rc = int(sys.argv[1])
meta = json.load(open(sys.argv[2], encoding="utf-8"))
tmp = sys.argv[3]
def text(path, limit=1200):
try:
return open(path, encoding="utf-8", errors="replace").read()[-limit:]
except FileNotFoundError:
return ""
def rc(path):
try:
return int(open(path, encoding="utf-8").read() or "1")
except Exception:
return 1
repos = []
bundles = []
for repo in meta:
bundle_path = os.path.join(tmp, f"{repo['key']}.bundle")
source_commit = None
snapshot_ref = None
try:
parts = open(bundle_path, encoding="utf-8").read().strip().split()
source_commit, snapshot_ref = parts[0], parts[1]
except Exception:
pass
repos.append({
**repo,
"sourceCommit": source_commit,
"snapshotRef": snapshot_ref,
"syncOk": source_commit is not None and snapshot_ref is not None and rc(os.path.join(tmp, f"{repo['key']}.gitops-push.rc")) == 0,
"initRc": rc(os.path.join(tmp, f"{repo['key']}.init.rc")),
"fetchRc": rc(os.path.join(tmp, f"{repo['key']}.fetch.rc")),
"revparseRc": rc(os.path.join(tmp, f"{repo['key']}.revparse.rc")),
"pushRc": rc(os.path.join(tmp, f"{repo['key']}.push.rc")),
"gitopsPushRc": rc(os.path.join(tmp, f"{repo['key']}.gitops-push.rc")),
"fetchStdoutTail": text(os.path.join(tmp, f"{repo['key']}.fetch.out")),
"fetchTail": text(os.path.join(tmp, f"{repo['key']}.fetch.err")),
"pushStdoutTail": text(os.path.join(tmp, f"{repo['key']}.push.out")),
"pushTail": text(os.path.join(tmp, f"{repo['key']}.push.err")),
"revparseTail": text(os.path.join(tmp, f"{repo['key']}.revparse.err")),
})
if source_commit and snapshot_ref:
bundles.append({"key": repo["key"], "sourceCommit": source_commit, "snapshotRef": snapshot_ref, "readUrl": repo["readUrl"], "sourceAuthority": "gitea"})
payload = {"ok": sync_rc == 0 and all(r["syncOk"] for r in repos), "repositories": repos, "sourceBundles": bundles, "valuesPrinted": False}
print(json.dumps(payload, ensure_ascii=False, indent=2))
sys.exit(0 if payload["ok"] else 1)
PY
}
run_mirror_status() {
mirror_repos_json
base_url="$(service_base_url)"
basic="$(admin_basic_b64)"
python3 - "$tmp/repos.json" "$tmp/status-plan.sh" "$tmp/repo-meta.json" "$base_url" <<'PY'
import json, shlex, sys, urllib.parse
repos = json.load(open(sys.argv[1], encoding="utf-8"))
base_url = sys.argv[4].rstrip("/")
script = []
meta = []
for repo in repos:
key = repo["key"]
branch = repo["upstream"]["branch"]
gitops = repo["gitops"]["branch"]
gitea_probe_url = base_url + urllib.parse.urlparse(repo["gitea"]["readUrl"]).path
script.append(f"git -c http.extraHeader=\"$GITEA_AUTH_HEADER\" ls-remote {shlex.quote(gitea_probe_url)} refs/heads/{shlex.quote(branch)} '{shlex.quote(repo['snapshot']['prefix'])}/*' refs/heads/{shlex.quote(gitops)} >$tmp/{key}.ls.out 2>$tmp/{key}.ls.err")
meta.append({"key": key, "branch": branch, "gitopsBranch": gitops, "readUrl": repo["gitea"]["readUrl"], "probeUrlHost": urllib.parse.urlparse(gitea_probe_url).netloc, "snapshotPrefix": repo["snapshot"]["prefix"], "legacyReadUrl": repo["legacyGitMirror"]["readUrl"], "legacyDisposition": repo["legacyGitMirror"]["disposition"]})
open(sys.argv[2], "w", encoding="utf-8").write("\n".join(script) + "\n")
json.dump(meta, open(sys.argv[3], "w", encoding="utf-8"), ensure_ascii=False)
PY
GITEA_AUTH_HEADER="Authorization: Basic $basic"
export GITEA_AUTH_HEADER
sh "$tmp/status-plan.sh" >"$tmp/status.out" 2>"$tmp/status.err"
status_rc=$?
python3 - "$status_rc" "$tmp/repo-meta.json" "$tmp" <<'PY'
import json, os, sys
status_rc = int(sys.argv[1])
meta = json.load(open(sys.argv[2], encoding="utf-8"))
tmp = sys.argv[3]
def text(path, limit=1200):
try:
return open(path, encoding="utf-8", errors="replace").read()[-limit:]
except FileNotFoundError:
return ""
repos = []
for repo in meta:
out = text(os.path.join(tmp, f"{repo['key']}.ls.out"), 20000)
refs = {}
for line in out.splitlines():
parts = line.split()
if len(parts) == 2:
refs[parts[1]] = parts[0]
branch_ref = f"refs/heads/{repo['branch']}"
snapshots = sorted((ref, sha) for ref, sha in refs.items() if ref.startswith(repo["snapshotPrefix"] + "/"))
latest_snapshot = snapshots[-1] if snapshots else None
repos.append({
**repo,
"branchCommit": refs.get(branch_ref),
"snapshotRef": latest_snapshot[0] if latest_snapshot else None,
"snapshotCommit": latest_snapshot[1] if latest_snapshot else None,
"sourceBundleReady": bool(refs.get(branch_ref) and latest_snapshot),
"mirrorState": "ready" if refs.get(branch_ref) and latest_snapshot else "missing-ref",
"errorTail": text(os.path.join(tmp, f"{repo['key']}.ls.err")),
})
payload = {"ok": status_rc == 0 and all(r["sourceBundleReady"] for r in repos), "repositories": repos, "valuesPrinted": False}
print(json.dumps(payload, ensure_ascii=False, indent=2))
sys.exit(0 if payload["ok"] else 1)
PY
}
case "$UNIDESK_GITEA_ACTION" in
apply) run_apply ;;
status) run_status ;;
validate) run_validate ;;
mirror-bootstrap) run_mirror_bootstrap ;;
mirror-sync) run_mirror_sync ;;
mirror-status) run_mirror_status ;;
*) printf '{"ok":false,"error":"unsupported-gitea-remote-action"}\n'; exit 2 ;;
esac
File diff suppressed because it is too large Load Diff
+4
View File
@@ -435,6 +435,10 @@ export function platformInfraHelp(): unknown {
"bun scripts/cli.ts platform-infra gitea apply --target JD01 --confirm",
"bun scripts/cli.ts platform-infra gitea status --target JD01",
"bun scripts/cli.ts platform-infra gitea validate --target JD01",
"bun scripts/cli.ts platform-infra gitea mirror plan --target JD01",
"bun scripts/cli.ts platform-infra gitea mirror bootstrap --target JD01 --confirm",
"bun scripts/cli.ts platform-infra gitea mirror sync --target JD01 --confirm",
"bun scripts/cli.ts platform-infra gitea mirror status --target JD01",
],
description: "Operate YAML-controlled platform-infra services such as Sub2API, LangBot, n8n, WeChat archive workflows, OpenTelemetry tracing, the independent target-scoped secret plane, the D518 Kafka event bus, and the internal Gitea source-authority service for GH-1548/GH-1549. Public services use PK01 Caddy+FRP rather than Kubernetes Ingress, NodePort, or LoadBalancer.",
target,