[HLCC-ICE-000001][ZY-CONTRIB-20260723-001] feat: 以来光者贡献链启用冰朔第五域个人子频道
This commit is contained in:
commit
5615453e4e
660 changed files with 122355 additions and 0 deletions
57
server-tools/hololake-code-channel/README.md
Normal file
57
server-tools/hololake-code-channel/README.md
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# HoloLake Code Channel · 部署与更新边界
|
||||
|
||||
本目录保存光湖代码频道(HLCC)的部署约束与机器可检验配置。
|
||||
|
||||
当前状态:
|
||||
|
||||
- 上游源码镜像与光湖自主源码基线已在 `BS-SG-003` 建立;
|
||||
- 固定源码基线为 Forgejo `v16.0.1` / `b3d7e4ac3cbccc220703097a51fa4c16bf302579`;
|
||||
- 国内旧第五域仍由 Gitea 1.23.7 原地承载并作为历史事实源;
|
||||
- `GLS-0239` 已登记京东个人子频道、`/code/` 正式入口、公开 AI 读取、
|
||||
`bingshuo` 单身份密码沿用和 `HLCC-ICE-000001` 新 Git 根提交;
|
||||
- 新频道部署与低权限激活清单已经就绪,正式切换前不覆盖旧第五域;
|
||||
- `update-policy.json` 中的光湖更新清单地址仍是规划地址,尚未上线。
|
||||
|
||||
`jd-candidate/` 是绑定京东主节点固定架构预置器的候选服务包。它只从临时新加坡
|
||||
中继读取 Forgejo 官方公开二进制、签名、公钥和哈希清单,不公开或下载光湖产品
|
||||
Git bundle。服务先在 `127.0.0.1:3341` 暴露结构化安装状态;只有固定 SHA-256、
|
||||
官方发布密钥指纹和 GPG 签名全部通过,且 `127.0.0.1:3340/api/v1/version`
|
||||
确认版本、单身份迁移和新频道根提交后,状态才会变为 `ready=true`。候选数据与
|
||||
旧第五域 Gitea 完全隔离;旧访问令牌、仓库、活动和 Git 历史不会迁入。
|
||||
|
||||
必须同时满足:
|
||||
|
||||
1. Forgejo 内置更新检查器关闭;
|
||||
2. 官方上游不能自动合并、自动构建或自动部署;
|
||||
3. 生产更新只认光湖签名清单;
|
||||
4. 光湖清单不可用时停留在人工发布,不回退官方接口;
|
||||
5. Gitea 迁移必须并行演练、可恢复、可回滚。
|
||||
|
||||
本目录不是服务器凭据库,不保存密码、Token、SSH 私钥或数据库转储。
|
||||
|
||||
## 隔离候选
|
||||
|
||||
`candidate-app.jd.ini`、`candidate-app.enterprise.ini`、`install-candidate.sh`、
|
||||
`start-candidate.sh` 和
|
||||
`stop-candidate.sh` 用于 `JD-FD-PRIMARY` 与 `AW-GZ-001` 上各自独立的隔离候选:
|
||||
|
||||
- 只绑定 `127.0.0.1:3340`;
|
||||
- 使用独立 SQLite 和仓库目录;
|
||||
- 禁止注册、Actions、镜像和 Forgejo 内置更新检查;
|
||||
- 只接收由 `BS-SG-003` 准备的固定 v16.0.1 二进制、签名与官方公钥材料;
|
||||
- 在两个国内节点分别再次核验 Forgejo 官方 GPG 指纹和二进制签名;
|
||||
- 安装和启动分离,安装脚本不注册 systemd、cron 或其他自动任务。
|
||||
|
||||
同一份离线包还要在 `AW-GZ-001` 企业服务器使用
|
||||
`candidate-app.enterprise.ini` 独立部署。京东与企业实例的数据库、仓库、用户、
|
||||
权限、密钥和回滚目录完全分离;它们只共享经过验签的源码与版本基线。
|
||||
|
||||
`BS-SG-003` 只负责海外源码和发布材料下载,不启动代码仓库服务。京东和企业节点都
|
||||
必须保留 `forgejo-upstream-all.bundle`、`guanghu-code-channel.bundle`、官方签名、
|
||||
公钥和 `MANIFEST.sha256`,从而在国内断开海外下载时仍可离线恢复与重装。候选验证
|
||||
位于各自独立目录,不读取、不迁移、不修改国内现役 Gitea 数据。
|
||||
|
||||
`prepare-release-on-sg.sh` 是新加坡中继的唯一准备脚本:它从 Forgejo 官方镜像
|
||||
下载固定版本二进制和签名、拉取并核对官方 GPG 指纹,并从现有上游镜像和光湖
|
||||
产品仓生成两个 Git bundle。全部文件写入 `MANIFEST.sha256`。它不会启动服务,
|
||||
也不会自行向京东或企业服务器传输;两次传输都必须绑定后续受控工单。
|
||||
5
server-tools/hololake-code-channel/app.ini.fragment
Normal file
5
server-tools/hololake-code-channel/app.ini.fragment
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
; HoloLake Code Channel mandatory Forgejo-base runtime fragment.
|
||||
; The upstream Forgejo release checker must never control Guanghu production updates.
|
||||
|
||||
[cron.update_checker]
|
||||
ENABLED = false
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
APP_NAME = HoloLake Code Channel · Enterprise
|
||||
RUN_USER = guanghu
|
||||
RUN_MODE = prod
|
||||
|
||||
[database]
|
||||
DB_TYPE = sqlite3
|
||||
PATH = /var/lib/guanghu/code-channel/candidates/hlcc-v16.0.1/data/hlcc.db
|
||||
|
||||
[repository]
|
||||
ROOT = /var/lib/guanghu/code-channel/candidates/hlcc-v16.0.1/data/repositories
|
||||
|
||||
[server]
|
||||
DOMAIN = 127.0.0.1
|
||||
HTTP_ADDR = 127.0.0.1
|
||||
HTTP_PORT = 3340
|
||||
ROOT_URL = http://127.0.0.1:3340/
|
||||
DISABLE_SSH = true
|
||||
LFS_START_SERVER = true
|
||||
OFFLINE_MODE = true
|
||||
|
||||
[service]
|
||||
DISABLE_REGISTRATION = true
|
||||
REQUIRE_SIGNIN_VIEW = true
|
||||
|
||||
[security]
|
||||
INSTALL_LOCK = true
|
||||
|
||||
[actions]
|
||||
ENABLED = false
|
||||
|
||||
[mirror]
|
||||
ENABLED = false
|
||||
|
||||
[cron.update_checker]
|
||||
ENABLED = false
|
||||
35
server-tools/hololake-code-channel/candidate-app.jd.ini
Normal file
35
server-tools/hololake-code-channel/candidate-app.jd.ini
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
APP_NAME = HoloLake Code Channel · Fifth Domain
|
||||
RUN_USER = guanghu
|
||||
RUN_MODE = prod
|
||||
|
||||
[database]
|
||||
DB_TYPE = sqlite3
|
||||
PATH = /var/lib/guanghu/code-channel/candidates/hlcc-v16.0.1/data/hlcc.db
|
||||
|
||||
[repository]
|
||||
ROOT = /var/lib/guanghu/code-channel/candidates/hlcc-v16.0.1/data/repositories
|
||||
|
||||
[server]
|
||||
DOMAIN = 127.0.0.1
|
||||
HTTP_ADDR = 127.0.0.1
|
||||
HTTP_PORT = 3340
|
||||
ROOT_URL = http://127.0.0.1:3340/
|
||||
DISABLE_SSH = true
|
||||
LFS_START_SERVER = true
|
||||
OFFLINE_MODE = true
|
||||
|
||||
[service]
|
||||
DISABLE_REGISTRATION = true
|
||||
REQUIRE_SIGNIN_VIEW = true
|
||||
|
||||
[security]
|
||||
INSTALL_LOCK = true
|
||||
|
||||
[actions]
|
||||
ENABLED = false
|
||||
|
||||
[mirror]
|
||||
ENABLED = false
|
||||
|
||||
[cron.update_checker]
|
||||
ENABLED = false
|
||||
95
server-tools/hololake-code-channel/install-candidate.sh
Executable file
95
server-tools/hololake-code-channel/install-candidate.sh
Executable file
|
|
@ -0,0 +1,95 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
readonly HLCC_VERSION="16.0.1"
|
||||
readonly HLCC_ROOT="/var/lib/guanghu/code-channel/candidates/hlcc-v${HLCC_VERSION}"
|
||||
readonly HLCC_SOURCE_ARCHIVE="/var/lib/guanghu/code-channel/offline-source/hlcc-v${HLCC_VERSION}"
|
||||
readonly HLCC_RELEASE_KEY="EB114F5E6C0DC2BCDD183550A4B61A2DC5923710"
|
||||
readonly HLCC_PACKAGE_SOURCE="${1:-}"
|
||||
readonly HLCC_CONFIG_SOURCE="${2:-}"
|
||||
readonly HLCC_BINARY_NAME="forgejo-${HLCC_VERSION}-linux-amd64"
|
||||
readonly HLCC_BINARY_SOURCE="${HLCC_PACKAGE_SOURCE}/${HLCC_BINARY_NAME}"
|
||||
readonly HLCC_SIGNATURE_SOURCE="${HLCC_BINARY_SOURCE}.asc"
|
||||
readonly HLCC_PUBLIC_KEY_SOURCE="${HLCC_PACKAGE_SOURCE}/forgejo-release-key.asc"
|
||||
readonly HLCC_MANIFEST_SOURCE="${HLCC_PACKAGE_SOURCE}/MANIFEST.sha256"
|
||||
readonly HLCC_UPSTREAM_BUNDLE_SOURCE="${HLCC_PACKAGE_SOURCE}/forgejo-upstream-all.bundle"
|
||||
readonly HLCC_PRODUCT_BUNDLE_SOURCE="${HLCC_PACKAGE_SOURCE}/guanghu-code-channel.bundle"
|
||||
|
||||
if [[ "$(id -un)" != "guanghu" ]]; then
|
||||
echo "Refusing to install a domestic isolated candidate as any user other than guanghu." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ \
|
||||
-z "${HLCC_PACKAGE_SOURCE}" || ! -d "${HLCC_PACKAGE_SOURCE}" || \
|
||||
! -f "${HLCC_BINARY_SOURCE}" || \
|
||||
! -f "${HLCC_SIGNATURE_SOURCE}" || \
|
||||
! -f "${HLCC_PUBLIC_KEY_SOURCE}" || \
|
||||
! -f "${HLCC_MANIFEST_SOURCE}" || \
|
||||
! -f "${HLCC_UPSTREAM_BUNDLE_SOURCE}" || \
|
||||
! -f "${HLCC_PRODUCT_BUNDLE_SOURCE}" || \
|
||||
-z "${HLCC_CONFIG_SOURCE}" || ! -f "${HLCC_CONFIG_SOURCE}" \
|
||||
]]; then
|
||||
echo "Usage: $0 /absolute/path/to/offline-package candidate-app.<target>.ini" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
for command_name in gpg sha256sum git; do
|
||||
if ! command -v "${command_name}" >/dev/null 2>&1; then
|
||||
echo "Missing required command: ${command_name}" >&2
|
||||
exit 3
|
||||
fi
|
||||
done
|
||||
|
||||
umask 077
|
||||
mkdir -p \
|
||||
"${HLCC_ROOT}/bin" \
|
||||
"${HLCC_ROOT}/config" \
|
||||
"${HLCC_ROOT}/data/repositories" \
|
||||
"${HLCC_ROOT}/logs" \
|
||||
"${HLCC_ROOT}/tmp" \
|
||||
"${HLCC_SOURCE_ARCHIVE}"
|
||||
|
||||
readonly gpg_home="${HLCC_ROOT}/tmp/gpg"
|
||||
mkdir -p "${gpg_home}"
|
||||
chmod 700 "${gpg_home}"
|
||||
|
||||
(
|
||||
cd "${HLCC_PACKAGE_SOURCE}"
|
||||
sha256sum --check MANIFEST.sha256
|
||||
)
|
||||
|
||||
GNUPGHOME="${gpg_home}" gpg --batch --import "${HLCC_PUBLIC_KEY_SOURCE}"
|
||||
|
||||
if ! GNUPGHOME="${gpg_home}" gpg \
|
||||
--batch \
|
||||
--with-colons \
|
||||
--fingerprint "${HLCC_RELEASE_KEY}" \
|
||||
| grep -Fq "fpr:::::::::${HLCC_RELEASE_KEY}:"; then
|
||||
echo "Forgejo release key fingerprint mismatch." >&2
|
||||
exit 4
|
||||
fi
|
||||
|
||||
GNUPGHOME="${gpg_home}" gpg \
|
||||
--batch \
|
||||
--verify "${HLCC_SIGNATURE_SOURCE}" "${HLCC_BINARY_SOURCE}"
|
||||
|
||||
install -m 0755 "${HLCC_BINARY_SOURCE}" "${HLCC_ROOT}/bin/hlcc"
|
||||
install -m 0600 "${HLCC_CONFIG_SOURCE}" "${HLCC_ROOT}/config/app.ini"
|
||||
install -m 0600 "${HLCC_MANIFEST_SOURCE}" "${HLCC_SOURCE_ARCHIVE}/MANIFEST.sha256"
|
||||
install -m 0755 "${HLCC_BINARY_SOURCE}" "${HLCC_SOURCE_ARCHIVE}/${HLCC_BINARY_NAME}"
|
||||
install -m 0600 "${HLCC_UPSTREAM_BUNDLE_SOURCE}" "${HLCC_SOURCE_ARCHIVE}/forgejo-upstream-all.bundle"
|
||||
install -m 0600 "${HLCC_PRODUCT_BUNDLE_SOURCE}" "${HLCC_SOURCE_ARCHIVE}/guanghu-code-channel.bundle"
|
||||
install -m 0600 "${HLCC_PUBLIC_KEY_SOURCE}" "${HLCC_SOURCE_ARCHIVE}/forgejo-release-key.asc"
|
||||
install -m 0600 "${HLCC_SIGNATURE_SOURCE}" "${HLCC_SOURCE_ARCHIVE}/${HLCC_BINARY_NAME}.asc"
|
||||
install -m 0600 "${HLCC_CONFIG_SOURCE}" "${HLCC_SOURCE_ARCHIVE}/candidate-app.ini"
|
||||
(
|
||||
cd "${HLCC_SOURCE_ARCHIVE}"
|
||||
sha256sum --check MANIFEST.sha256
|
||||
)
|
||||
sha256sum "${HLCC_ROOT}/bin/hlcc" > "${HLCC_ROOT}/RELEASE.sha256"
|
||||
|
||||
"${HLCC_ROOT}/bin/hlcc" --version
|
||||
echo "HLCC isolated candidate installed at ${HLCC_ROOT}"
|
||||
echo "Offline source and release material retained at ${HLCC_SOURCE_ARCHIVE}"
|
||||
echo "No service was started and no automatic update job was installed."
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Activate the reviewed full-offline HLCC candidate unit without root shell."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import signal
|
||||
import subprocess
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
|
||||
SERVICE = "hlcc-jd-candidate.service"
|
||||
HEALTH_URL = "http://127.0.0.1:3341/health"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
result = subprocess.run(
|
||||
["/usr/bin/systemctl", "show", "--property=MainPID", "--value", SERVICE],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
pid = int(result.stdout.strip())
|
||||
if pid <= 1:
|
||||
raise RuntimeError("HLCC candidate bootstrap has no active main process")
|
||||
|
||||
process_root = pathlib.Path("/proc") / str(pid)
|
||||
if process_root.stat().st_uid != os.getuid():
|
||||
raise RuntimeError("refusing to signal a process owned by another user")
|
||||
command = (process_root / "cmdline").read_bytes().replace(b"\0", b" ").decode("utf-8", "replace")
|
||||
if "/hololake-code-channel/jd-candidate/hlcc-bootstrap.py" not in command:
|
||||
raise RuntimeError("refusing to signal an unexpected process")
|
||||
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
for _attempt in range(40):
|
||||
try:
|
||||
with urllib.request.urlopen(HEALTH_URL, timeout=2) as response:
|
||||
payload = json.load(response)
|
||||
if (
|
||||
payload.get("ok") is True
|
||||
and payload.get("mode") == "isolated-candidate"
|
||||
and payload.get("version") == "16.0.1"
|
||||
and payload.get("package_profile") == "full-offline-v16.0.1"
|
||||
and payload.get("ready") is True
|
||||
and payload.get("stage") == "ready"
|
||||
):
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(1)
|
||||
raise RuntimeError("full-offline HLCC personal channel did not become ready")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const root = __dirname;
|
||||
const script = fs.readFileSync(path.join(root, "activate-staged-candidate.py"), "utf8");
|
||||
const unit = fs.readFileSync(path.join(root, "hlcc-jd-candidate-activator.service"), "utf8");
|
||||
const bootstrap = fs.readFileSync(path.join(root, "hlcc-bootstrap.py"), "utf8");
|
||||
|
||||
assert.match(bootstrap, /hlcc-offline\/16\.0\.1/);
|
||||
assert.match(bootstrap, /forgejo-upstream-all\.bundle/);
|
||||
assert.match(bootstrap, /guanghu-code-channel\.bundle/);
|
||||
assert.match(bootstrap, /full-offline-v16\.0\.1/);
|
||||
assert.match(script, /systemctl", "show", "--property=MainPID"/);
|
||||
assert.match(script, /process_root\.stat\(\)\.st_uid != os\.getuid\(\)/);
|
||||
assert.match(script, /"\/hololake-code-channel\/jd-candidate\/hlcc-bootstrap\.py" not in command/);
|
||||
assert.match(script, /os\.kill\(pid, signal\.SIGTERM\)/);
|
||||
assert.match(script, /payload\.get\("mode"\) == "isolated-candidate"/);
|
||||
assert.match(script, /payload\.get\("ready"\) is True/);
|
||||
assert.match(script, /payload\.get\("stage"\) == "ready"/);
|
||||
assert.doesNotMatch(script, /shell=True|systemctl", "(?:restart|stop|start)"/);
|
||||
assert.match(unit, /^User=guanghu$/m);
|
||||
assert.match(unit, /^NoNewPrivileges=true$/m);
|
||||
assert.match(unit, /^ProtectSystem=strict$/m);
|
||||
|
||||
console.log("HLCC full-offline candidate activator: PASS");
|
||||
40
server-tools/hololake-code-channel/jd-candidate/app.ini
Normal file
40
server-tools/hololake-code-channel/jd-candidate/app.ini
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
APP_NAME = 光湖代码频道
|
||||
RUN_USER = guanghu
|
||||
RUN_MODE = prod
|
||||
|
||||
[database]
|
||||
DB_TYPE = sqlite3
|
||||
PATH = /var/lib/guanghu/personas/guanghu/hlcc-v16.0.1/data/hlcc.db
|
||||
|
||||
[repository]
|
||||
ROOT = /var/lib/guanghu/personas/guanghu/hlcc-v16.0.1/data/repositories
|
||||
|
||||
[server]
|
||||
DOMAIN = guanghulab.com
|
||||
HTTP_ADDR = 127.0.0.1
|
||||
HTTP_PORT = 3340
|
||||
ROOT_URL = https://guanghulab.com/code/
|
||||
DISABLE_SSH = true
|
||||
LFS_START_SERVER = true
|
||||
OFFLINE_MODE = true
|
||||
|
||||
[service]
|
||||
DISABLE_REGISTRATION = true
|
||||
REQUIRE_SIGNIN_VIEW = false
|
||||
|
||||
[security]
|
||||
INSTALL_LOCK = true
|
||||
|
||||
[actions]
|
||||
ENABLED = false
|
||||
|
||||
[mirror]
|
||||
ENABLED = false
|
||||
|
||||
[other]
|
||||
SHOW_FOOTER_BRANDING = false
|
||||
SHOW_FOOTER_VERSION = false
|
||||
SHOW_FOOTER_TEMPLATE_LOAD_TIME = false
|
||||
|
||||
[cron.update_checker]
|
||||
ENABLED = false
|
||||
|
|
@ -0,0 +1,528 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Verify and start the isolated JD HoloLake Code Channel candidate."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import http.server
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import shutil
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
|
||||
VERSION = "16.0.1"
|
||||
UPSTREAM_FINGERPRINT = "EB114F5E6C0DC2BCDD183550A4B61A2DC5923710"
|
||||
STATE_ROOT = pathlib.Path("/var/lib/guanghu/personas/guanghu/hlcc-v16.0.1")
|
||||
LEGACY_DB = STATE_ROOT / "data" / "owner-identity-source.db"
|
||||
OWNER_NAME = "bingshuo"
|
||||
CHANNEL_REPOSITORY = "fifth-domain"
|
||||
LEGACY_REPOSITORY_URL = "https://guanghulab.com/fifth-domain/bingshuo/fifth-domain.git"
|
||||
SEED_COMMIT_NUMBER = "HLCC-ICE-000001"
|
||||
SEED_CONTRIBUTION_NUMBER = "ZY-CONTRIB-20260723-001"
|
||||
RELAY_ROOT = "https://guanghubingshuo.com/hlcc-offline/16.0.1"
|
||||
BINARY_NAME = f"forgejo-{VERSION}-linux-amd64"
|
||||
EXPECTED = {
|
||||
BINARY_NAME: "7a4c568136650c10498a9d3d62c7fd630a0cf09c166293ebd78708248f6398fc",
|
||||
f"{BINARY_NAME}.asc": "1c0ca36df3adb0a7692b6bdc84d7886001ca0c6d0408e67c9d232d2f33cecc71",
|
||||
"forgejo-release-key.asc": "6fae8894c671ce2397cb35fe40c324f73deade6b4cb3cd6cedd1d2b248e0e3ea",
|
||||
"forgejo-upstream-all.bundle": "c33bd074d9b2896259e86ebe03ad31ccdd8ff71897beed4320081fa03b15381f",
|
||||
"guanghu-code-channel.bundle": "fc53740259d108128e69f5a809cec438ecf3158175617574ba55b8612c5eaa6c",
|
||||
"MANIFEST.sha256": "d564c3b600d4b7a199d8a04ce505ceabf81993ca74fa440805601d55e550f185",
|
||||
}
|
||||
|
||||
STATUS = {
|
||||
"ok": True,
|
||||
"mode": "bootstrap",
|
||||
"version": VERSION,
|
||||
"code": "HLCC-JD-CANDIDATE-01",
|
||||
"ready": False,
|
||||
"stage": "starting",
|
||||
"package_profile": "full-offline-v16.0.1",
|
||||
}
|
||||
STATUS_LOCK = threading.Lock()
|
||||
|
||||
|
||||
def set_status(**values: object) -> None:
|
||||
with STATUS_LOCK:
|
||||
STATUS.update(values)
|
||||
|
||||
|
||||
class HealthHandler(http.server.BaseHTTPRequestHandler):
|
||||
def do_GET(self) -> None: # noqa: N802 - BaseHTTPRequestHandler API
|
||||
if self.path != "/health":
|
||||
self.send_error(404)
|
||||
return
|
||||
with STATUS_LOCK:
|
||||
payload = json.dumps(STATUS, ensure_ascii=False).encode("utf-8")
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
self.send_header("Content-Length", str(len(payload)))
|
||||
self.end_headers()
|
||||
self.wfile.write(payload)
|
||||
|
||||
def log_message(self, _format: str, *_args: object) -> None:
|
||||
return
|
||||
|
||||
|
||||
def sha256(path: pathlib.Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as handle:
|
||||
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def download_verified(name: str) -> pathlib.Path:
|
||||
destination = STATE_ROOT / "release" / name
|
||||
if destination.is_file() and sha256(destination) == EXPECTED[name]:
|
||||
return destination
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
temporary = destination.with_suffix(destination.suffix + ".partial")
|
||||
temporary.unlink(missing_ok=True)
|
||||
request = urllib.request.Request(
|
||||
f"{RELAY_ROOT}/{name}",
|
||||
headers={"User-Agent": "HoloLake-Code-Channel/16.0.1"},
|
||||
)
|
||||
with urllib.request.urlopen(request, timeout=45) as response, temporary.open("wb") as output:
|
||||
shutil.copyfileobj(response, output, length=1024 * 1024)
|
||||
if sha256(temporary) != EXPECTED[name]:
|
||||
temporary.unlink(missing_ok=True)
|
||||
raise RuntimeError(f"sha256 mismatch: {name}")
|
||||
temporary.replace(destination)
|
||||
return destination
|
||||
|
||||
|
||||
def verify_release(files: dict[str, pathlib.Path]) -> None:
|
||||
manifest = files["MANIFEST.sha256"].read_text(encoding="utf-8")
|
||||
for name in (
|
||||
BINARY_NAME,
|
||||
f"{BINARY_NAME}.asc",
|
||||
"forgejo-release-key.asc",
|
||||
"forgejo-upstream-all.bundle",
|
||||
"guanghu-code-channel.bundle",
|
||||
):
|
||||
expected_line = f"{EXPECTED[name]} {name}"
|
||||
if expected_line not in manifest.splitlines():
|
||||
raise RuntimeError(f"manifest entry mismatch: {name}")
|
||||
|
||||
gpg_home = STATE_ROOT / "gpg"
|
||||
gpg_home.mkdir(parents=True, exist_ok=True)
|
||||
gpg_home.chmod(0o700)
|
||||
environment = {**os.environ, "GNUPGHOME": str(gpg_home)}
|
||||
subprocess.run(
|
||||
["gpg", "--batch", "--import", str(files["forgejo-release-key.asc"])],
|
||||
check=True,
|
||||
env=environment,
|
||||
stdout=subprocess.DEVNULL,
|
||||
)
|
||||
fingerprint = subprocess.run(
|
||||
["gpg", "--batch", "--with-colons", "--fingerprint", UPSTREAM_FINGERPRINT],
|
||||
check=True,
|
||||
env=environment,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
).stdout
|
||||
if f"fpr:::::::::{UPSTREAM_FINGERPRINT}:" not in fingerprint:
|
||||
raise RuntimeError("release key fingerprint mismatch")
|
||||
subprocess.run(
|
||||
[
|
||||
"gpg",
|
||||
"--batch",
|
||||
"--verify",
|
||||
str(files[f"{BINARY_NAME}.asc"]),
|
||||
str(files[BINARY_NAME]),
|
||||
],
|
||||
check=True,
|
||||
env=environment,
|
||||
stdout=subprocess.DEVNULL,
|
||||
)
|
||||
|
||||
|
||||
def wait_for_candidate(process: subprocess.Popen[bytes]) -> None:
|
||||
url = "http://127.0.0.1:3340/api/healthz"
|
||||
for _attempt in range(90):
|
||||
if process.poll() is not None:
|
||||
raise RuntimeError(f"candidate exited with code {process.returncode}")
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=2) as response:
|
||||
payload = json.load(response)
|
||||
if payload.get("status") == "pass":
|
||||
return
|
||||
except Exception: # Candidate is still starting.
|
||||
pass
|
||||
time.sleep(1)
|
||||
raise RuntimeError("candidate readiness timeout")
|
||||
|
||||
|
||||
def immutable_source_commit(script_path: pathlib.Path | None = None) -> str:
|
||||
source = (script_path or pathlib.Path(__file__)).resolve()
|
||||
matches = [part for part in source.parts if re.fullmatch(r"[0-9a-f]{40}", part)]
|
||||
if len(matches) != 1:
|
||||
raise RuntimeError("immutable release commit unavailable")
|
||||
return matches[0]
|
||||
|
||||
|
||||
def owner_identity_columns(connection: sqlite3.Connection) -> list[str]:
|
||||
return [row[1] for row in connection.execute("pragma table_info(user)")]
|
||||
|
||||
|
||||
def migrate_owner_identity(
|
||||
legacy_db: pathlib.Path = LEGACY_DB,
|
||||
channel_db: pathlib.Path | None = None,
|
||||
receipt_path: pathlib.Path | None = None,
|
||||
) -> str:
|
||||
target = channel_db or STATE_ROOT / "data" / "hlcc.db"
|
||||
receipt = receipt_path or STATE_ROOT / "data" / "owner-migration-receipt.json"
|
||||
if not legacy_db.is_file() or not target.is_file():
|
||||
raise RuntimeError("owner migration database unavailable")
|
||||
|
||||
legacy = sqlite3.connect(f"file:{legacy_db}?mode=ro", uri=True, timeout=15)
|
||||
channel = sqlite3.connect(target, timeout=15)
|
||||
try:
|
||||
existing = channel.execute(
|
||||
"select is_active, is_admin from user where lower_name = ?",
|
||||
(OWNER_NAME,),
|
||||
).fetchall()
|
||||
if existing:
|
||||
if len(existing) != 1 or existing[0] != (1, 1):
|
||||
raise RuntimeError("channel owner identity mismatch")
|
||||
return "already-present"
|
||||
|
||||
source_rows = legacy.execute(
|
||||
"select * from user where lower_name = ?",
|
||||
(OWNER_NAME,),
|
||||
).fetchall()
|
||||
if len(source_rows) != 1:
|
||||
raise RuntimeError("legacy owner identity mismatch")
|
||||
|
||||
old_columns = owner_identity_columns(legacy)
|
||||
new_info = list(channel.execute("pragma table_info(user)"))
|
||||
new_columns = {row[1] for row in new_info}
|
||||
missing = [
|
||||
row[1]
|
||||
for row in new_info
|
||||
if row[3] and row[4] is None and row[1] != "id" and row[1] not in old_columns
|
||||
]
|
||||
if missing:
|
||||
raise RuntimeError("channel owner schema has unsupported required columns")
|
||||
|
||||
copied_columns = [name for name in old_columns if name in new_columns and name != "id"]
|
||||
values = dict(zip(old_columns, source_rows[0]))
|
||||
for counter in ("num_repos", "num_stars", "num_followers", "num_following"):
|
||||
if counter in values:
|
||||
values[counter] = 0
|
||||
if "use_custom_avatar" in values:
|
||||
values["use_custom_avatar"] = 0
|
||||
if "prohibit_login" in values:
|
||||
values["prohibit_login"] = 0
|
||||
|
||||
backup_dir = target.parent / "backups"
|
||||
backup_dir.mkdir(parents=True, exist_ok=True)
|
||||
backup_path = backup_dir / "hlcc-before-owner-migration.db"
|
||||
backup = sqlite3.connect(backup_path)
|
||||
try:
|
||||
channel.backup(backup)
|
||||
finally:
|
||||
backup.close()
|
||||
backup_path.chmod(0o600)
|
||||
|
||||
placeholders = ",".join("?" for _name in copied_columns)
|
||||
column_sql = ",".join(f'"{name}"' for name in copied_columns)
|
||||
with channel:
|
||||
channel.execute(
|
||||
f"insert into user ({column_sql}) values ({placeholders})",
|
||||
[values[name] for name in copied_columns],
|
||||
)
|
||||
verified = channel.execute(
|
||||
"select id, is_active, is_admin from user where lower_name = ?",
|
||||
(OWNER_NAME,),
|
||||
).fetchall()
|
||||
if len(verified) != 1 or verified[0][1:] != (1, 1):
|
||||
raise RuntimeError("channel owner migration verification failed")
|
||||
|
||||
receipt.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"schema": "guanghu.hlcc-owner-migration/v1",
|
||||
"owner": OWNER_NAME,
|
||||
"identity_only": True,
|
||||
"password_hash_preserved": True,
|
||||
"access_tokens_migrated": False,
|
||||
"repositories_migrated": False,
|
||||
"result": "VERIFIED",
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
)
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
receipt.chmod(0o600)
|
||||
return "migrated"
|
||||
finally:
|
||||
channel.close()
|
||||
legacy.close()
|
||||
|
||||
|
||||
def api_json(
|
||||
method: str,
|
||||
path: str,
|
||||
token: str = "",
|
||||
body: dict[str, object] | None = None,
|
||||
) -> tuple[int, dict[str, object]]:
|
||||
headers = {"Accept": "application/json"}
|
||||
data = None
|
||||
if token:
|
||||
headers["Authorization"] = f"token {token}"
|
||||
if body is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
data = json.dumps(body).encode("utf-8")
|
||||
request = urllib.request.Request(
|
||||
f"http://127.0.0.1:3340{path}",
|
||||
data=data,
|
||||
headers=headers,
|
||||
method=method,
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=15) as response:
|
||||
raw = response.read()
|
||||
return response.status, json.loads(raw or b"{}")
|
||||
except urllib.error.HTTPError as error:
|
||||
raw = error.read()
|
||||
try:
|
||||
payload = json.loads(raw or b"{}")
|
||||
except json.JSONDecodeError:
|
||||
payload = {}
|
||||
return error.code, payload
|
||||
|
||||
|
||||
def delete_bootstrap_token(channel_db: pathlib.Path, token_name: str) -> None:
|
||||
connection = sqlite3.connect(channel_db, timeout=15)
|
||||
try:
|
||||
with connection:
|
||||
owner = connection.execute(
|
||||
"select id from user where lower_name = ?",
|
||||
(OWNER_NAME,),
|
||||
).fetchone()
|
||||
if owner:
|
||||
connection.execute(
|
||||
"delete from access_token where uid = ? and name = ?",
|
||||
(owner[0], token_name),
|
||||
)
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
|
||||
def generate_bootstrap_token(binary: pathlib.Path, token_name: str) -> str:
|
||||
channel_db = STATE_ROOT / "data" / "hlcc.db"
|
||||
delete_bootstrap_token(channel_db, token_name)
|
||||
result = subprocess.run(
|
||||
[
|
||||
str(binary),
|
||||
"admin",
|
||||
"user",
|
||||
"generate-access-token",
|
||||
"--username",
|
||||
OWNER_NAME,
|
||||
"--token-name",
|
||||
token_name,
|
||||
"--scopes",
|
||||
"write:repository",
|
||||
"--raw",
|
||||
"--config",
|
||||
str(STATE_ROOT / "config" / "app.ini"),
|
||||
"--work-path",
|
||||
str(STATE_ROOT / "data"),
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
token = result.stdout.strip().splitlines()[-1]
|
||||
if not re.fullmatch(r"[A-Za-z0-9_-]{32,160}", token):
|
||||
raise RuntimeError("bootstrap access token format invalid")
|
||||
return token
|
||||
|
||||
|
||||
def seed_fifth_domain_channel(binary: pathlib.Path) -> str:
|
||||
status, repository = api_json("GET", f"/api/v1/repos/{OWNER_NAME}/{CHANNEL_REPOSITORY}")
|
||||
if status == 200:
|
||||
if repository.get("private") is not False:
|
||||
raise RuntimeError("existing channel repository is not public")
|
||||
if repository.get("empty") is False and repository.get("default_branch") == "main":
|
||||
return "already-present"
|
||||
if repository.get("empty") is not True:
|
||||
raise RuntimeError("existing channel repository state invalid")
|
||||
elif status != 404:
|
||||
raise RuntimeError("channel repository lookup failed")
|
||||
|
||||
token_name = "hlcc-fifth-domain-bootstrap"
|
||||
token = generate_bootstrap_token(binary, token_name)
|
||||
channel_db = STATE_ROOT / "data" / "hlcc.db"
|
||||
try:
|
||||
if status == 404:
|
||||
created_status, _created = api_json(
|
||||
"POST",
|
||||
"/api/v1/user/repos",
|
||||
token,
|
||||
{
|
||||
"name": CHANNEL_REPOSITORY,
|
||||
"description": "光湖代码频道 · 冰朔第五域个人子频道 · 2026-07-23 新起点",
|
||||
"private": False,
|
||||
"auto_init": False,
|
||||
"default_branch": "main",
|
||||
},
|
||||
)
|
||||
if created_status != 201:
|
||||
raise RuntimeError("channel repository creation failed")
|
||||
|
||||
source_commit = immutable_source_commit()
|
||||
with tempfile.TemporaryDirectory(prefix="hlcc-seed-") as temporary:
|
||||
root = pathlib.Path(temporary)
|
||||
snapshot = root / "snapshot"
|
||||
subprocess.run(
|
||||
["git", "clone", "--depth=1", "--branch", "main", LEGACY_REPOSITORY_URL, str(snapshot)],
|
||||
check=True,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
actual_commit = subprocess.run(
|
||||
["git", "-C", str(snapshot), "rev-parse", "HEAD"],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
).stdout.strip()
|
||||
if actual_commit != source_commit:
|
||||
raise RuntimeError("legacy snapshot commit mismatch")
|
||||
|
||||
shutil.rmtree(snapshot / ".git")
|
||||
subprocess.run(["git", "-C", str(snapshot), "init", "-b", "main"], check=True, capture_output=True)
|
||||
subprocess.run(
|
||||
["git", "-C", str(snapshot), "config", "user.name", "光湖代码频道 · 铸渊"],
|
||||
check=True,
|
||||
)
|
||||
subprocess.run(
|
||||
["git", "-C", str(snapshot), "config", "user.email", "hlcc@guanghulab.invalid"],
|
||||
check=True,
|
||||
)
|
||||
subprocess.run(["git", "-C", str(snapshot), "add", "-A"], check=True)
|
||||
subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"-C",
|
||||
str(snapshot),
|
||||
"commit",
|
||||
"-m",
|
||||
(
|
||||
f"[{SEED_COMMIT_NUMBER}][{SEED_CONTRIBUTION_NUMBER}] "
|
||||
"feat: 以来光者贡献链启用冰朔第五域个人子频道"
|
||||
),
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
credential = root / "credentials"
|
||||
encoded = urllib.parse.quote(token, safe="")
|
||||
credential.write_text(
|
||||
f"http://{OWNER_NAME}:{encoded}@127.0.0.1:3340\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
credential.chmod(0o600)
|
||||
subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"-C",
|
||||
str(snapshot),
|
||||
"-c",
|
||||
f"credential.helper=store --file {credential}",
|
||||
"push",
|
||||
"http://127.0.0.1:3340/bingshuo/fifth-domain.git",
|
||||
"main:main",
|
||||
],
|
||||
check=True,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
credential.unlink(missing_ok=True)
|
||||
|
||||
verified_status, verified = api_json(
|
||||
"GET",
|
||||
f"/api/v1/repos/{OWNER_NAME}/{CHANNEL_REPOSITORY}",
|
||||
)
|
||||
if (
|
||||
verified_status != 200
|
||||
or verified.get("private") is not False
|
||||
or verified.get("empty") is not False
|
||||
or verified.get("default_branch") != "main"
|
||||
):
|
||||
raise RuntimeError("channel repository verification failed")
|
||||
return "seeded"
|
||||
finally:
|
||||
delete_bootstrap_token(channel_db, token_name)
|
||||
|
||||
|
||||
def bootstrap() -> None:
|
||||
process: subprocess.Popen[bytes] | None = None
|
||||
try:
|
||||
for directory in ("config", "data", "logs", "release", "tmp"):
|
||||
(STATE_ROOT / directory).mkdir(parents=True, exist_ok=True)
|
||||
set_status(stage="downloading")
|
||||
files = {name: download_verified(name) for name in EXPECTED}
|
||||
set_status(stage="verifying")
|
||||
verify_release(files)
|
||||
binary = files[BINARY_NAME]
|
||||
binary.chmod(0o755)
|
||||
config_source = pathlib.Path(__file__).with_name("app.ini")
|
||||
config_target = STATE_ROOT / "config" / "app.ini"
|
||||
shutil.copyfile(config_source, config_target)
|
||||
config_target.chmod(0o600)
|
||||
set_status(stage="launching")
|
||||
log_path = STATE_ROOT / "logs" / "hlcc.log"
|
||||
log_handle = log_path.open("ab", buffering=0)
|
||||
process = subprocess.Popen(
|
||||
[
|
||||
str(binary),
|
||||
"web",
|
||||
"--work-path",
|
||||
str(STATE_ROOT / "data"),
|
||||
"--config",
|
||||
str(config_target),
|
||||
],
|
||||
stdout=log_handle,
|
||||
stderr=subprocess.STDOUT,
|
||||
)
|
||||
wait_for_candidate(process)
|
||||
set_status(stage="migrating-owner")
|
||||
migrate_owner_identity()
|
||||
set_status(stage="seeding-fifth-domain-channel")
|
||||
seed_fifth_domain_channel(binary)
|
||||
set_status(mode="isolated-candidate", ready=True, stage="ready")
|
||||
return_code = process.wait()
|
||||
raise RuntimeError(f"candidate stopped with code {return_code}")
|
||||
except Exception as error:
|
||||
if process and process.poll() is None:
|
||||
process.terminate()
|
||||
set_status(ok=False, ready=False, stage="failed", error=str(error)[:180])
|
||||
|
||||
|
||||
def main() -> None:
|
||||
thread = threading.Thread(target=bootstrap, name="hlcc-bootstrap", daemon=True)
|
||||
thread.start()
|
||||
server = http.server.ThreadingHTTPServer(("127.0.0.1", 3341), HealthHandler)
|
||||
server.serve_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
[Unit]
|
||||
Description=Activate the staged full-offline HoloLake Code Channel JD candidate
|
||||
After=hlcc-jd-candidate.service
|
||||
Requires=hlcc-jd-candidate.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=guanghu
|
||||
Group=guanghu
|
||||
ExecStart=/usr/bin/python3 __RELEASE_ROOT__/server-tools/hololake-code-channel/jd-candidate/activate-staged-candidate.py
|
||||
RemainAfterExit=true
|
||||
TimeoutStartSec=60
|
||||
NoNewPrivileges=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
RestrictSUIDSGID=true
|
||||
LockPersonality=true
|
||||
ReadOnlyPaths=__RELEASE_ROOT__
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
[Unit]
|
||||
Description=HoloLake Code Channel isolated JD candidate
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=guanghu
|
||||
Group=guanghu
|
||||
UMask=0077
|
||||
StateDirectory=guanghu/personas/guanghu/hlcc-v16.0.1
|
||||
ExecStart=/usr/bin/python3 __RELEASE_ROOT__/server-tools/hololake-code-channel/jd-candidate/hlcc-bootstrap.py
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
NoNewPrivileges=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
RestrictSUIDSGID=true
|
||||
LockPersonality=true
|
||||
ReadOnlyPaths=__RELEASE_ROOT__
|
||||
ReadWritePaths=/var/lib/guanghu/personas/guanghu/hlcc-v16.0.1
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
[Unit]
|
||||
Description=Activate the HoloLake Code Channel Fifth Domain personal subchannel
|
||||
After=hlcc-jd-candidate.service
|
||||
Requires=hlcc-jd-candidate.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=guanghu
|
||||
Group=guanghu
|
||||
ExecStart=/usr/bin/python3 __RELEASE_ROOT__/server-tools/hololake-code-channel/jd-candidate/activate-staged-candidate.py
|
||||
RemainAfterExit=true
|
||||
TimeoutStartSec=180
|
||||
NoNewPrivileges=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
RestrictSUIDSGID=true
|
||||
LockPersonality=true
|
||||
ReadOnlyPaths=__RELEASE_ROOT__
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const root = __dirname;
|
||||
const bootstrap = fs.readFileSync(path.join(root, "hlcc-bootstrap.py"), "utf8");
|
||||
const ini = fs.readFileSync(path.join(root, "app.ini"), "utf8");
|
||||
const unit = fs.readFileSync(path.join(root, "hlcc-jd-candidate.service"), "utf8");
|
||||
const manifest = JSON.parse(
|
||||
fs.readFileSync(
|
||||
path.join(
|
||||
root,
|
||||
"../../../deployment/requests/HLCC-JD-CANDIDATE-INITIAL-PROVISION-20260723.json",
|
||||
),
|
||||
"utf8",
|
||||
),
|
||||
);
|
||||
const offlineReceipt = JSON.parse(
|
||||
fs.readFileSync(
|
||||
path.join(
|
||||
root,
|
||||
"../../../deployment/receipts/HLCC-BS-SG-003-OFFLINE-PACK-20260723.json",
|
||||
),
|
||||
"utf8",
|
||||
),
|
||||
);
|
||||
|
||||
assert.match(bootstrap, /VERSION = "16\.0\.1"/);
|
||||
assert.match(bootstrap, /EB114F5E6C0DC2BCDD183550A4B61A2DC5923710/);
|
||||
assert.match(bootstrap, /MANIFEST\.sha256/);
|
||||
assert.match(bootstrap, /gpg"[\s\S]*"--verify"/);
|
||||
assert.match(bootstrap, /127\.0\.0\.1", 3341/);
|
||||
assert.match(bootstrap, /127\.0\.0\.1:3340\/api\/healthz/);
|
||||
assert.match(bootstrap, /payload\.get\("status"\) == "pass"/);
|
||||
assert.doesNotMatch(bootstrap, /127\.0\.0\.1:3340\/api\/v1\/version/);
|
||||
assert.match(bootstrap, /forgejo-upstream-all\.bundle/);
|
||||
assert.match(bootstrap, /guanghu-code-channel\.bundle/);
|
||||
assert.match(bootstrap, /package_profile": "full-offline-v16\.0\.1"/);
|
||||
assert.match(bootstrap, /OWNER_NAME = "bingshuo"/);
|
||||
assert.match(bootstrap, /SEED_COMMIT_NUMBER = "HLCC-ICE-000001"/);
|
||||
assert.match(bootstrap, /SEED_CONTRIBUTION_NUMBER = "ZY-CONTRIB-20260723-001"/);
|
||||
assert.match(bootstrap, /以来光者贡献链启用冰朔第五域个人子频道/);
|
||||
assert.match(bootstrap, /owner-identity-source\.db/);
|
||||
assert.match(bootstrap, /password_hash_preserved/);
|
||||
assert.match(bootstrap, /access_tokens_migrated": False/);
|
||||
assert.match(bootstrap, /repositories_migrated": False/);
|
||||
assert.match(bootstrap, /delete from access_token/);
|
||||
assert.doesNotMatch(
|
||||
bootstrap,
|
||||
/print\s*\([^)]*token|stderr\.write\s*\([^)]*token|stdout\.write\s*\([^)]*token/,
|
||||
);
|
||||
for (const artifact of offlineReceipt.artifacts) {
|
||||
assert.match(artifact.sha256, /^[0-9a-f]{64}$/);
|
||||
assert.match(
|
||||
bootstrap,
|
||||
new RegExp(`: "${artifact.sha256}"`),
|
||||
);
|
||||
}
|
||||
assert.match(offlineReceipt.release.manifest_sha256, /^[0-9a-f]{64}$/);
|
||||
assert.match(
|
||||
bootstrap,
|
||||
new RegExp(`"MANIFEST\\.sha256": "${offlineReceipt.release.manifest_sha256}"`),
|
||||
);
|
||||
assert.match(ini, /APP_NAME = 光湖代码频道/);
|
||||
assert.match(ini, /ROOT_URL = https:\/\/guanghulab\.com\/code\//);
|
||||
assert.match(ini, /REQUIRE_SIGNIN_VIEW = false/);
|
||||
assert.match(ini, /SHOW_FOOTER_BRANDING = false/);
|
||||
assert.match(ini, /SHOW_FOOTER_VERSION = false/);
|
||||
assert.match(ini, /\[cron\.update_checker\][\s\S]*ENABLED = false/);
|
||||
assert.match(unit, /^User=guanghu$/m);
|
||||
assert.match(unit, /^ProtectSystem=strict$/m);
|
||||
assert.match(unit, /^ReadOnlyPaths=__RELEASE_ROOT__$/m);
|
||||
assert.match(
|
||||
unit,
|
||||
/^ReadWritePaths=\/var\/lib\/guanghu\/personas\/guanghu\/hlcc-v16\.0\.1$/m,
|
||||
);
|
||||
assert.equal(manifest.target_node, "JD-FD-PRIMARY");
|
||||
assert.equal(manifest.runtime_check.url, "http://127.0.0.1:3341/health");
|
||||
assert.equal(manifest.module.unit, "hlcc-jd-candidate.service");
|
||||
assert.equal(manifest.module.run_user, "guanghu");
|
||||
|
||||
console.log("HoloLake Code Channel JD candidate package: PASS");
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Create a one-user SQLite handoff without exposing the legacy database to HLCC."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import pwd
|
||||
import sqlite3
|
||||
import tempfile
|
||||
|
||||
|
||||
OWNER_NAME = "bingshuo"
|
||||
|
||||
|
||||
def prepare(source: pathlib.Path, destination: pathlib.Path, owner: str) -> None:
|
||||
if not source.is_file():
|
||||
raise RuntimeError("legacy database unavailable")
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
legacy = sqlite3.connect(f"file:{source}?mode=ro", uri=True, timeout=15)
|
||||
try:
|
||||
schema = legacy.execute(
|
||||
"select sql from sqlite_master where type = 'table' and name = 'user'"
|
||||
).fetchone()
|
||||
row = legacy.execute(
|
||||
"select * from user where lower_name = ?",
|
||||
(OWNER_NAME,),
|
||||
).fetchall()
|
||||
if not schema or not schema[0] or len(row) != 1:
|
||||
raise RuntimeError("legacy owner identity mismatch")
|
||||
|
||||
file_descriptor, temporary_name = tempfile.mkstemp(
|
||||
prefix=".owner-identity-source.",
|
||||
suffix=".db",
|
||||
dir=destination.parent,
|
||||
)
|
||||
os.close(file_descriptor)
|
||||
temporary = pathlib.Path(temporary_name)
|
||||
try:
|
||||
handoff = sqlite3.connect(temporary)
|
||||
try:
|
||||
handoff.execute(schema[0])
|
||||
columns = [item[1] for item in legacy.execute("pragma table_info(user)")]
|
||||
placeholders = ",".join("?" for _column in columns)
|
||||
column_sql = ",".join(f'"{column}"' for column in columns)
|
||||
handoff.execute(
|
||||
f"insert into user ({column_sql}) values ({placeholders})",
|
||||
row[0],
|
||||
)
|
||||
handoff.commit()
|
||||
finally:
|
||||
handoff.close()
|
||||
temporary.chmod(0o600)
|
||||
identity = pwd.getpwnam(owner)
|
||||
os.chown(temporary, identity.pw_uid, identity.pw_gid)
|
||||
temporary.replace(destination)
|
||||
finally:
|
||||
temporary.unlink(missing_ok=True)
|
||||
finally:
|
||||
legacy.close()
|
||||
|
||||
receipt = destination.with_suffix(".receipt.json")
|
||||
receipt.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"schema": "guanghu.hlcc-owner-identity-handoff/v1",
|
||||
"owner": OWNER_NAME,
|
||||
"rows": 1,
|
||||
"contains_repository_data": False,
|
||||
"contains_access_tokens": False,
|
||||
"result": "PREPARED",
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
)
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
identity = pwd.getpwnam(owner)
|
||||
os.chown(receipt, identity.pw_uid, identity.pw_gid)
|
||||
receipt.chmod(0o600)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--source", required=True, type=pathlib.Path)
|
||||
parser.add_argument("--destination", required=True, type=pathlib.Path)
|
||||
parser.add_argument("--owner", default="guanghu")
|
||||
arguments = parser.parse_args()
|
||||
prepare(arguments.source, arguments.destination, arguments.owner)
|
||||
print("OWNER_IDENTITY_SOURCE_PREPARED rows=1 tokens=0 repositories=0")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import pathlib
|
||||
import sqlite3
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
MODULE_PATH = pathlib.Path(__file__).with_name("hlcc-bootstrap.py")
|
||||
SPEC = importlib.util.spec_from_file_location("hlcc_bootstrap", MODULE_PATH)
|
||||
assert SPEC and SPEC.loader
|
||||
MODULE = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(MODULE)
|
||||
|
||||
|
||||
SCHEMA = """
|
||||
create table user (
|
||||
id integer primary key autoincrement,
|
||||
lower_name text not null,
|
||||
name text not null,
|
||||
email text not null,
|
||||
passwd text not null,
|
||||
salt text,
|
||||
passwd_hash_algo text,
|
||||
avatar text not null,
|
||||
avatar_email text not null,
|
||||
type integer default 0,
|
||||
is_active integer default 1,
|
||||
is_admin integer default 0,
|
||||
num_repos integer default 0,
|
||||
num_stars integer default 0,
|
||||
num_followers integer default 0,
|
||||
num_following integer default 0,
|
||||
use_custom_avatar integer default 0,
|
||||
prohibit_login integer default 0
|
||||
);
|
||||
create table repository (
|
||||
id integer primary key autoincrement,
|
||||
owner_id integer not null,
|
||||
name text not null
|
||||
);
|
||||
create table access_token (
|
||||
id integer primary key autoincrement,
|
||||
uid integer not null,
|
||||
name text not null,
|
||||
token_hash text not null
|
||||
);
|
||||
"""
|
||||
|
||||
|
||||
class OwnerMigrationTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.temporary = tempfile.TemporaryDirectory()
|
||||
root = pathlib.Path(self.temporary.name)
|
||||
self.old = root / "old.db"
|
||||
self.new = root / "new.db"
|
||||
self.receipt = root / "receipt.json"
|
||||
for database in (self.old, self.new):
|
||||
connection = sqlite3.connect(database)
|
||||
connection.executescript(SCHEMA)
|
||||
connection.commit()
|
||||
connection.close()
|
||||
|
||||
connection = sqlite3.connect(self.old)
|
||||
connection.execute(
|
||||
"""
|
||||
insert into user (
|
||||
lower_name, name, email, passwd, salt, passwd_hash_algo,
|
||||
avatar, avatar_email, is_active, is_admin, num_repos,
|
||||
num_stars, num_followers, num_following, use_custom_avatar
|
||||
) values (?, ?, ?, ?, ?, ?, ?, ?, 1, 1, 12, 4, 3, 2, 1)
|
||||
""",
|
||||
(
|
||||
"bingshuo",
|
||||
"bingshuo",
|
||||
"owner@example.invalid",
|
||||
"preserved-password-hash",
|
||||
"preserved-salt",
|
||||
"pbkdf2$50000$50",
|
||||
"legacy-avatar",
|
||||
"avatar@example.invalid",
|
||||
),
|
||||
)
|
||||
connection.execute(
|
||||
"insert into repository (owner_id, name) values (1, 'legacy-repo')"
|
||||
)
|
||||
connection.execute(
|
||||
"insert into access_token (uid, name, token_hash) values (1, 'legacy-token', 'secret-hash')"
|
||||
)
|
||||
connection.commit()
|
||||
connection.close()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.temporary.cleanup()
|
||||
|
||||
def test_migrates_only_owner_identity_and_preserves_password_hash(self) -> None:
|
||||
result = MODULE.migrate_owner_identity(self.old, self.new, self.receipt)
|
||||
self.assertEqual(result, "migrated")
|
||||
connection = sqlite3.connect(self.new)
|
||||
owner = connection.execute(
|
||||
"""
|
||||
select lower_name, passwd, salt, passwd_hash_algo, is_active,
|
||||
is_admin, num_repos, num_stars, num_followers,
|
||||
num_following, use_custom_avatar
|
||||
from user
|
||||
"""
|
||||
).fetchone()
|
||||
self.assertEqual(
|
||||
owner,
|
||||
(
|
||||
"bingshuo",
|
||||
"preserved-password-hash",
|
||||
"preserved-salt",
|
||||
"pbkdf2$50000$50",
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
),
|
||||
)
|
||||
self.assertEqual(connection.execute("select count(*) from repository").fetchone()[0], 0)
|
||||
self.assertEqual(connection.execute("select count(*) from access_token").fetchone()[0], 0)
|
||||
connection.close()
|
||||
receipt = json.loads(self.receipt.read_text(encoding="utf-8"))
|
||||
self.assertTrue(receipt["identity_only"])
|
||||
self.assertFalse(receipt["access_tokens_migrated"])
|
||||
self.assertFalse(receipt["repositories_migrated"])
|
||||
|
||||
def test_is_idempotent(self) -> None:
|
||||
self.assertEqual(MODULE.migrate_owner_identity(self.old, self.new, self.receipt), "migrated")
|
||||
self.assertEqual(MODULE.migrate_owner_identity(self.old, self.new, self.receipt), "already-present")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
107
server-tools/hololake-code-channel/native.test.js
Normal file
107
server-tools/hololake-code-channel/native.test.js
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const assert = require("node:assert/strict");
|
||||
|
||||
const root = __dirname;
|
||||
const ini = fs.readFileSync(path.join(root, "app.ini.fragment"), "utf8");
|
||||
const jdCandidateIni = fs.readFileSync(
|
||||
path.join(root, "candidate-app.jd.ini"),
|
||||
"utf8",
|
||||
);
|
||||
const enterpriseCandidateIni = fs.readFileSync(
|
||||
path.join(root, "candidate-app.enterprise.ini"),
|
||||
"utf8",
|
||||
);
|
||||
const installer = fs.readFileSync(
|
||||
path.join(root, "install-candidate.sh"),
|
||||
"utf8",
|
||||
);
|
||||
const relayPreparer = fs.readFileSync(
|
||||
path.join(root, "prepare-release-on-sg.sh"),
|
||||
"utf8",
|
||||
);
|
||||
const policy = JSON.parse(
|
||||
fs.readFileSync(path.join(root, "update-policy.json"), "utf8"),
|
||||
);
|
||||
|
||||
assert.match(ini, /\[cron\.update_checker\][\s\S]*ENABLED\s*=\s*false/i);
|
||||
assert.match(jdCandidateIni, /HoloLake Code Channel · Fifth Domain/);
|
||||
assert.match(enterpriseCandidateIni, /HoloLake Code Channel · Enterprise/);
|
||||
for (const candidateIni of [jdCandidateIni, enterpriseCandidateIni]) {
|
||||
assert.match(
|
||||
candidateIni,
|
||||
/\[server\][\s\S]*HTTP_ADDR\s*=\s*127\.0\.0\.1[\s\S]*HTTP_PORT\s*=\s*3340/i,
|
||||
);
|
||||
assert.match(candidateIni, /RUN_USER\s*=\s*guanghu/i);
|
||||
assert.match(
|
||||
candidateIni,
|
||||
/\/var\/lib\/guanghu\/code-channel\/candidates\/hlcc-v16\.0\.1/,
|
||||
);
|
||||
assert.match(
|
||||
candidateIni,
|
||||
/\[cron\.update_checker\][\s\S]*ENABLED\s*=\s*false/i,
|
||||
);
|
||||
assert.match(candidateIni, /\[actions\][\s\S]*ENABLED\s*=\s*false/i);
|
||||
assert.match(candidateIni, /\[mirror\][\s\S]*ENABLED\s*=\s*false/i);
|
||||
}
|
||||
assert.match(
|
||||
installer,
|
||||
/\/var\/lib\/guanghu\/code-channel\/offline-source\/hlcc-v/,
|
||||
);
|
||||
assert.match(installer, /HLCC_VERSION="16\.0\.1"/);
|
||||
assert.doesNotMatch(installer, /curl|wget|keyserver/);
|
||||
assert.match(
|
||||
installer,
|
||||
/HLCC_RELEASE_KEY="EB114F5E6C0DC2BCDD183550A4B61A2DC5923710"/,
|
||||
);
|
||||
assert.match(installer, /gpg[\s\S]*--verify/);
|
||||
assert.match(installer, /HLCC_SOURCE_ARCHIVE[\s\S]*sha256sum --check MANIFEST\.sha256/);
|
||||
assert.match(installer, /candidate-app\.ini/);
|
||||
assert.match(installer, /No service was started/);
|
||||
assert.match(relayPreparer, /RELEASE_URL="https:\/\/code\.forgejo\.org\//);
|
||||
assert.match(relayPreparer, /id -un\)" != "ubuntu"/);
|
||||
assert.match(relayPreparer, /forgejo-upstream-all\.bundle/);
|
||||
assert.match(relayPreparer, /guanghu-code-channel\.bundle/);
|
||||
assert.match(relayPreparer, /MANIFEST\.sha256/);
|
||||
assert.match(
|
||||
relayPreparer,
|
||||
/No service was started and no transfer to a domestic node was attempted/,
|
||||
);
|
||||
assert.equal(policy.product_id, "HLP-MOD-CODE-CHANNEL");
|
||||
assert.equal(policy.official_upstream.automatic_fetch, false);
|
||||
assert.equal(policy.official_upstream.automatic_merge, false);
|
||||
assert.equal(policy.official_upstream.automatic_build, false);
|
||||
assert.equal(policy.official_upstream.automatic_deploy, false);
|
||||
assert.equal(policy.official_upstream.push_enabled, false);
|
||||
assert.equal(policy.forgejo_builtin_update_checker.enabled, false);
|
||||
assert.equal(
|
||||
policy.guanghu_update_channel.manifest_url,
|
||||
"https://guanghulab.com/api/code-channel/updates/v1/manifest.json",
|
||||
);
|
||||
assert.equal(policy.guanghu_update_channel.require_signature, true);
|
||||
assert.equal(policy.guanghu_update_channel.require_artifact_sha256, true);
|
||||
assert.equal(policy.guanghu_update_channel.require_rollback_release, true);
|
||||
assert.equal(
|
||||
policy.guanghu_update_channel.fallback_when_unavailable,
|
||||
"MANUAL_RELEASE_ONLY",
|
||||
);
|
||||
assert.equal(
|
||||
policy.source_baseline.upstream_commit,
|
||||
"b3d7e4ac3cbccc220703097a51fa4c16bf302579",
|
||||
);
|
||||
assert.deepEqual(policy.offline_distribution.domestic_archive_nodes, [
|
||||
"JD-FD-PRIMARY",
|
||||
"AW-GZ-001",
|
||||
]);
|
||||
assert.deepEqual(policy.offline_distribution.runtime_nodes, [
|
||||
"JD-FD-PRIMARY",
|
||||
"AW-GZ-001",
|
||||
]);
|
||||
assert.equal(policy.offline_distribution.require_manifest_sha256, true);
|
||||
assert.equal(policy.offline_distribution.require_domestic_gpg_reverification, true);
|
||||
assert.equal(policy.offline_distribution.shared_runtime_data, false);
|
||||
assert.equal(policy.offline_distribution.automatic_transfer, false);
|
||||
|
||||
console.log("HoloLake Code Channel update boundary: PASS");
|
||||
87
server-tools/hololake-code-channel/prepare-release-on-sg.sh
Executable file
87
server-tools/hololake-code-channel/prepare-release-on-sg.sh
Executable file
|
|
@ -0,0 +1,87 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
readonly HLCC_VERSION="16.0.1"
|
||||
readonly RELAY_ROOT="/home/ubuntu/guanghu/release-relay/hlcc-v${HLCC_VERSION}"
|
||||
readonly BINARY_NAME="forgejo-${HLCC_VERSION}-linux-amd64"
|
||||
readonly RELEASE_URL="https://code.forgejo.org/forgejo/forgejo/releases/download/v${HLCC_VERSION}/${BINARY_NAME}"
|
||||
readonly RELEASE_KEY="EB114F5E6C0DC2BCDD183550A4B61A2DC5923710"
|
||||
readonly GPG_HOME="${RELAY_ROOT}/gpg"
|
||||
readonly UPSTREAM_REPOSITORY="/home/ubuntu/guanghu/upstream-parts/forgejo-official.git"
|
||||
readonly PRODUCT_REPOSITORY="/home/ubuntu/guanghu/products/guanghu-code-channel"
|
||||
|
||||
if [[ "$(id -un)" != "ubuntu" ]]; then
|
||||
echo "Refusing to prepare overseas release material as any user other than ubuntu." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
for command_name in curl git gpg sha256sum; do
|
||||
if ! command -v "${command_name}" >/dev/null 2>&1; then
|
||||
echo "Missing required command: ${command_name}" >&2
|
||||
exit 3
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ! -d "${UPSTREAM_REPOSITORY}" || ! -d "${PRODUCT_REPOSITORY}/.git" ]]; then
|
||||
echo "Required Forgejo upstream mirror or Guanghu product worktree is missing." >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
umask 077
|
||||
mkdir -p "${RELAY_ROOT}" "${GPG_HOME}"
|
||||
chmod 700 "${GPG_HOME}"
|
||||
|
||||
curl --fail --location --proto '=https' --tlsv1.2 \
|
||||
--output "${RELAY_ROOT}/${BINARY_NAME}" \
|
||||
"${RELEASE_URL}"
|
||||
curl --fail --location --proto '=https' --tlsv1.2 \
|
||||
--output "${RELAY_ROOT}/${BINARY_NAME}.asc" \
|
||||
"${RELEASE_URL}.asc"
|
||||
|
||||
GNUPGHOME="${GPG_HOME}" gpg \
|
||||
--batch \
|
||||
--keyserver hkps://keys.openpgp.org \
|
||||
--recv-keys "${RELEASE_KEY}"
|
||||
|
||||
if ! GNUPGHOME="${GPG_HOME}" gpg \
|
||||
--batch \
|
||||
--with-colons \
|
||||
--fingerprint "${RELEASE_KEY}" \
|
||||
| grep -Fq "fpr:::::::::${RELEASE_KEY}:"; then
|
||||
echo "Forgejo release key fingerprint mismatch." >&2
|
||||
exit 4
|
||||
fi
|
||||
|
||||
GNUPGHOME="${GPG_HOME}" gpg \
|
||||
--batch \
|
||||
--verify "${RELAY_ROOT}/${BINARY_NAME}.asc" "${RELAY_ROOT}/${BINARY_NAME}"
|
||||
GNUPGHOME="${GPG_HOME}" gpg \
|
||||
--batch \
|
||||
--armor \
|
||||
--export "${RELEASE_KEY}" > "${RELAY_ROOT}/forgejo-release-key.asc"
|
||||
|
||||
git -C "${UPSTREAM_REPOSITORY}" bundle create \
|
||||
"${RELAY_ROOT}/forgejo-upstream-all.bundle" \
|
||||
--all
|
||||
git -C "${PRODUCT_REPOSITORY}" bundle create \
|
||||
"${RELAY_ROOT}/guanghu-code-channel.bundle" \
|
||||
guanghu/main
|
||||
git -C "${UPSTREAM_REPOSITORY}" bundle verify \
|
||||
"${RELAY_ROOT}/forgejo-upstream-all.bundle"
|
||||
git -C "${PRODUCT_REPOSITORY}" bundle verify \
|
||||
"${RELAY_ROOT}/guanghu-code-channel.bundle"
|
||||
|
||||
(
|
||||
cd "${RELAY_ROOT}"
|
||||
sha256sum \
|
||||
"${BINARY_NAME}" \
|
||||
"${BINARY_NAME}.asc" \
|
||||
forgejo-release-key.asc \
|
||||
forgejo-upstream-all.bundle \
|
||||
guanghu-code-channel.bundle \
|
||||
> MANIFEST.sha256
|
||||
)
|
||||
|
||||
echo "Verified HLCC release material prepared at ${RELAY_ROOT}"
|
||||
echo "The offline package contains full upstream and Guanghu product Git bundles."
|
||||
echo "No service was started and no transfer to a domestic node was attempted."
|
||||
44
server-tools/hololake-code-channel/start-candidate.sh
Executable file
44
server-tools/hololake-code-channel/start-candidate.sh
Executable file
|
|
@ -0,0 +1,44 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
readonly HLCC_ROOT="/var/lib/guanghu/code-channel/candidates/hlcc-v16.0.1"
|
||||
readonly HLCC_PID_FILE="${HLCC_ROOT}/hlcc.pid"
|
||||
readonly HLCC_LOG_FILE="${HLCC_ROOT}/logs/hlcc.log"
|
||||
|
||||
if [[ "$(id -un)" != "guanghu" ]]; then
|
||||
echo "Refusing to start a domestic isolated candidate as any user other than guanghu." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if ! command -v curl >/dev/null 2>&1; then
|
||||
echo "Missing required command: curl" >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
if [[ -f "${HLCC_PID_FILE}" ]]; then
|
||||
readonly existing_pid="$(cat "${HLCC_PID_FILE}")"
|
||||
if kill -0 "${existing_pid}" 2>/dev/null; then
|
||||
echo "HLCC candidate is already running as PID ${existing_pid}."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
nohup "${HLCC_ROOT}/bin/hlcc" web \
|
||||
--work-path "${HLCC_ROOT}/data" \
|
||||
--config "${HLCC_ROOT}/config/app.ini" \
|
||||
>>"${HLCC_LOG_FILE}" 2>&1 &
|
||||
|
||||
readonly candidate_pid="$!"
|
||||
echo "${candidate_pid}" > "${HLCC_PID_FILE}"
|
||||
sleep 2
|
||||
|
||||
if ! kill -0 "${candidate_pid}" 2>/dev/null; then
|
||||
echo "HLCC candidate failed to remain running." >&2
|
||||
tail -n 80 "${HLCC_LOG_FILE}" >&2
|
||||
exit 5
|
||||
fi
|
||||
|
||||
curl --fail --silent --show-error \
|
||||
"http://127.0.0.1:3340/api/v1/version"
|
||||
echo
|
||||
echo "HLCC isolated candidate is running on loopback only."
|
||||
23
server-tools/hololake-code-channel/stop-candidate.sh
Executable file
23
server-tools/hololake-code-channel/stop-candidate.sh
Executable file
|
|
@ -0,0 +1,23 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
readonly HLCC_ROOT="/var/lib/guanghu/code-channel/candidates/hlcc-v16.0.1"
|
||||
readonly HLCC_PID_FILE="${HLCC_ROOT}/hlcc.pid"
|
||||
|
||||
if [[ "$(id -un)" != "guanghu" ]]; then
|
||||
echo "Refusing to stop a domestic isolated candidate as any user other than guanghu." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ ! -f "${HLCC_PID_FILE}" ]]; then
|
||||
echo "HLCC candidate PID file does not exist."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
readonly candidate_pid="$(cat "${HLCC_PID_FILE}")"
|
||||
if kill -0 "${candidate_pid}" 2>/dev/null; then
|
||||
kill "${candidate_pid}"
|
||||
fi
|
||||
|
||||
rm "${HLCC_PID_FILE}"
|
||||
echo "HLCC isolated candidate stopped."
|
||||
47
server-tools/hololake-code-channel/update-policy.json
Normal file
47
server-tools/hololake-code-channel/update-policy.json
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
"schema": "guanghu.code-channel.update-policy/v1",
|
||||
"product_id": "HLP-MOD-CODE-CHANNEL",
|
||||
"name_zh": "光湖代码频道",
|
||||
"name_en": "HoloLake Code Channel",
|
||||
"official_upstream": {
|
||||
"source": "https://code.forgejo.org/forgejo/forgejo.git",
|
||||
"role": "review-only-parts-source",
|
||||
"automatic_fetch": false,
|
||||
"automatic_merge": false,
|
||||
"automatic_build": false,
|
||||
"automatic_deploy": false,
|
||||
"push_enabled": false
|
||||
},
|
||||
"forgejo_builtin_update_checker": {
|
||||
"enabled": false,
|
||||
"forbidden_endpoint": "release.forgejo.org"
|
||||
},
|
||||
"guanghu_update_channel": {
|
||||
"manifest_url": "https://guanghulab.com/api/code-channel/updates/v1/manifest.json",
|
||||
"state": "PLANNED_NOT_LIVE",
|
||||
"require_signature": true,
|
||||
"require_artifact_sha256": true,
|
||||
"require_rollback_release": true,
|
||||
"fallback_when_unavailable": "MANUAL_RELEASE_ONLY"
|
||||
},
|
||||
"source_baseline": {
|
||||
"upstream_tag": "v16.0.1",
|
||||
"upstream_commit": "b3d7e4ac3cbccc220703097a51fa4c16bf302579",
|
||||
"guanghu_branch": "guanghu/main"
|
||||
},
|
||||
"offline_distribution": {
|
||||
"download_and_bundle_node": "BS-SG-003",
|
||||
"domestic_archive_nodes": [
|
||||
"JD-FD-PRIMARY",
|
||||
"AW-GZ-001"
|
||||
],
|
||||
"runtime_nodes": [
|
||||
"JD-FD-PRIMARY",
|
||||
"AW-GZ-001"
|
||||
],
|
||||
"require_manifest_sha256": true,
|
||||
"require_domestic_gpg_reverification": true,
|
||||
"shared_runtime_data": false,
|
||||
"automatic_transfer": false
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue