feat: use gitea as source authority
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.",
|
||||
};
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user