Merge pull request '部署:修复代码频道共享权限警告' (#6) from deploy/hlcc-native-shared-repository-20260806 into main

Reviewed-on: #6
This commit is contained in:
bingshuo 2026-08-06 15:45:12 +08:00
commit daa41e03bc
6 changed files with 179 additions and 44 deletions

View file

@ -0,0 +1,63 @@
{
"schema": "guanghu.architecture-provision-request/v1",
"request_id": "HLCC-NATIVE-SHARED-REPOSITORY-20260806",
"target_node": "JD-FD-PRIMARY",
"architecture_id": "GLS-HLCC-009",
"module": {
"code": "HLCC-JD-CANDIDATE-01",
"name": "光湖代码频道原生共享仓库权限修复",
"bind": "loopback:3340,3341",
"owner": "systemd",
"unit": "hlcc-jd-candidate.service",
"run_user": "guanghu",
"writable_paths": [
"/var/lib/guanghu/personas/guanghu/hlcc-v16.0.1"
],
"read_only_paths": []
},
"source_ref": "REPO-012:refs/heads/main",
"deployed_commit_policy": "use the exact human-merged commit and record it in the server-owned deployment receipt",
"source_paths": [
"server-tools/hololake-code-channel/jd-candidate/hlcc-bootstrap.py",
"server-tools/hololake-code-channel/jd-candidate/app.ini",
"server-tools/hololake-code-channel/jd-candidate/hlcc-jd-candidate.service"
],
"initial_provision": {
"kind": "new-architecture-unit",
"not_an_existing_action_bridge_extension": true,
"requires": [
"back up the currently installed hlcc-jd-candidate.service",
"install only the declared immutable source files",
"restart only hlcc-jd-candidate.service",
"set core.sharedRepository=group on the exact bingshuo/guanghu-ice-heart bare repository",
"remove only the identified obsolete guanghu-ice-heart sharing hook",
"preserve UMask=0077 for all other code-channel state",
"do not alter repository content, branches, owner identity or authentication"
]
},
"verification": [
"the candidate health endpoint returns ready=true after restart",
"the exact repository reports core.sharedRepository as group",
"the obsolete post-receive chmod hook is absent",
"a later public push no longer emits recursive chmod permission warnings",
"both code-channel and authorized direct-receiver repository checks remain valid"
],
"runtime_check": {
"url": "http://127.0.0.1:3341/health",
"expected": {
"ok": true,
"mode": "isolated-candidate",
"version": "16.0.1",
"code": "HLCC-JD-CANDIDATE-01",
"ready": true,
"stage": "ready"
}
},
"rollback": [
"restore the backed-up hlcc-jd-candidate.service",
"restart the previous immutable release",
"retain repository content and all authentication state",
"record any manual permission reconciliation separately"
],
"status": "ARCHITECTURE_PACKAGE_READY · INITIAL_PROVISION_PENDING"
}

View file

@ -26,6 +26,13 @@ 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 = "guanghu-ice-heart"
CHANNEL_REPOSITORY_PATH = (
STATE_ROOT
/ "data"
/ "repositories"
/ OWNER_NAME
/ f"{CHANNEL_REPOSITORY}.git"
)
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"
@ -473,6 +480,59 @@ def seed_fifth_domain_channel(binary: pathlib.Path) -> str:
delete_bootstrap_token(channel_db, token_name)
def configure_shared_channel_repository(
repository: pathlib.Path = CHANNEL_REPOSITORY_PATH,
) -> str:
"""Use Git's native shared-repository mode instead of a post-receive chmod."""
if not repository.is_dir():
raise RuntimeError("channel repository path unavailable")
subprocess.run(
[
"git",
"--git-dir",
str(repository),
"config",
"core.sharedRepository",
"group",
],
check=True,
capture_output=True,
text=True,
)
configured = subprocess.run(
[
"git",
"--git-dir",
str(repository),
"config",
"--get",
"core.sharedRepository",
],
check=True,
capture_output=True,
text=True,
).stdout.strip()
if configured not in {"1", "group"}:
raise RuntimeError("shared repository configuration verification failed")
obsolete_hook = (
repository
/ "hooks"
/ "post-receive.d"
/ "guanghu-ice-heart-share"
)
if obsolete_hook.exists():
if obsolete_hook.is_symlink() or not obsolete_hook.is_file():
raise RuntimeError("obsolete sharing hook path is unsafe")
hook_text = obsolete_hook.read_text(encoding="utf-8")
if "post_receive_permissions_reconciled" not in hook_text:
raise RuntimeError("obsolete sharing hook identity mismatch")
obsolete_hook.unlink()
return "configured"
def bootstrap() -> None:
process: subprocess.Popen[bytes] | None = None
try:
@ -514,6 +574,8 @@ def bootstrap() -> None:
migrate_owner_identity()
set_status(stage="seeding-fifth-domain-channel")
seed_fifth_domain_channel(binary)
set_status(stage="configuring-shared-repository")
configure_shared_channel_repository()
set_status(mode="isolated-candidate", ready=True, stage="ready")
return_code = process.wait()
raise RuntimeError(f"candidate stopped with code {return_code}")

View file

@ -47,6 +47,10 @@ 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.match(bootstrap, /core\.sharedRepository/);
assert.match(bootstrap, /"group"/);
assert.match(bootstrap, /obsolete sharing hook identity mismatch/);
assert.match(bootstrap, /configure_shared_channel_repository\(\)/);
assert.doesNotMatch(
bootstrap,
/print\s*\([^)]*token|stderr\.write\s*\([^)]*token|stdout\.write\s*\([^)]*token/,

View file

@ -6,6 +6,7 @@ import importlib.util
import json
import pathlib
import sqlite3
import subprocess
import tempfile
import unittest
@ -138,5 +139,50 @@ class OwnerMigrationTests(unittest.TestCase):
self.assertEqual(MODULE.migrate_owner_identity(self.old, self.new, self.receipt), "already-present")
class SharedRepositoryTests(unittest.TestCase):
def test_configures_native_group_sharing_and_removes_only_known_hook(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
repository = pathlib.Path(temporary) / "channel.git"
subprocess.run(["git", "init", "--bare", str(repository)], check=True, capture_output=True)
hook = repository / "hooks" / "post-receive.d" / "guanghu-ice-heart-share"
hook.parent.mkdir(parents=True)
hook.write_text(
"#!/bin/sh\nprintf '%s\\n' post_receive_permissions_reconciled\n",
encoding="utf-8",
)
self.assertEqual(
MODULE.configure_shared_channel_repository(repository),
"configured",
)
configured = subprocess.run(
[
"git",
"--git-dir",
str(repository),
"config",
"--get",
"core.sharedRepository",
],
check=True,
capture_output=True,
text=True,
).stdout.strip()
self.assertIn(configured, {"1", "group"})
self.assertFalse(hook.exists())
def test_refuses_unknown_hook_content(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
repository = pathlib.Path(temporary) / "channel.git"
subprocess.run(["git", "init", "--bare", str(repository)], check=True, capture_output=True)
hook = repository / "hooks" / "post-receive.d" / "guanghu-ice-heart-share"
hook.parent.mkdir(parents=True)
hook.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8")
with self.assertRaisesRegex(RuntimeError, "identity mismatch"):
MODULE.configure_shared_channel_repository(repository)
self.assertTrue(hook.exists())
if __name__ == "__main__":
unittest.main()

View file

@ -124,11 +124,10 @@ node server-tools/lake-lamp-authz/authorize-repo-push.js \
- 当裸仓库归属代码频道服务账户时,在系统 Git 配置中把该精确路径登记为
`safe.directory`,不得使用通配符。
光湖代码频道本身保留 `UMask=0077`,以免放宽数据库和其他状态目录。仅在
`guanghu-ice-heart.git/hooks/post-receive.d/guanghu-ice-heart-share` 安装仓库随附的
`hooks/guanghu-ice-heart-post-receive`,让成功的公共 Git 推送完成后校正
`objects``refs` 的共享组权限。不要为了共享一个裸仓库而修改整个代码频道服务的
UMask。
光湖代码频道本身保留 `UMask=0077`,以免放宽数据库和其他状态目录。对需要由直达
接收器共同读写的精确裸仓库设置 Git 原生 `core.sharedRepository=group`,让新建的
`objects``refs` 从写入时就继承专用共享组权限。不要安装推送后递归 `chmod`
钩子,也不要为了共享一个裸仓库而修改整个代码频道服务的 UMask。
部署后用当前主分支生成无变化验收 bundle`receiveBundle` 完整执行一次;验收前后
主分支 SHA 必须一致,并且代码频道账户与授权服务账户执行 `git fsck` 均通过。

View file

@ -1,39 +0,0 @@
#!/bin/sh
set -eu
# The code-channel service runs with UMask=0077. Keep that isolation for its
# database and other state, but make Git objects and refs readable/writable by
# the repository's dedicated shared group after an accepted public push.
cat >/dev/null
repo_dir=$(git rev-parse --absolute-git-dir)
expected_repo=/var/lib/guanghu/personas/guanghu/hlcc-v16.0.1/data/repositories/bingshuo/guanghu-ice-heart.git
[ "$repo_dir" = "$expected_repo" ] || exit 0
cd "$repo_dir"
owner_uid=$(id -u)
share_tree() {
# RestrictSUIDSGID forbids chmod calls that preserve a setgid directory.
# Existing shared directories are already traversable, so touch only new
# private directories and use a numeric mode that drops the special bit.
find "$1" -user "$owner_uid" -type d ! -perm -g=x -exec chmod 0770 {} +
find "$1" -user "$owner_uid" -type f ! -perm -g=r -exec chmod g+r {} +
}
share_tree "$repo_dir/objects"
share_tree "$repo_dir/refs"
# receive-pack may keep new objects in a quarantine directory until hooks have
# completed. Normalize that directory before Git moves the objects into place.
if [ -n "${GIT_OBJECT_DIRECTORY:-}" ] && [ -d "$GIT_OBJECT_DIRECTORY" ]; then
case "$GIT_OBJECT_DIRECTORY/" in
"$repo_dir/"*) share_tree "$GIT_OBJECT_DIRECTORY" ;;
esac
fi
for shared_file in HEAD packed-refs; do
if [ -f "$repo_dir/$shared_file" ]; then
find "$repo_dir/$shared_file" -user "$owner_uid" -exec chmod g+rw {} +
fi
done
printf '%s\n' "post_receive_permissions_reconciled" >"$repo_dir/hooks/post-receive-share.last"