Fetch deployment dependencies over localhost SSH with GitHub fallback

This commit is contained in:
TheBadRoger committed 2026-10-15 20:55:25 +08:00
1 parent d4caf05370
commit 959dba988c
7 files changed
+130 -39

No files matched your search

+12
View File
@@ -0,0 +1,12 @@
#!/bin/sh
# Fetch/build external dependencies as deploy, then start Docker Compose as root.
set -eu
PROJECT_ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
if [ "$(id -u)" -ne 0 ]; then
echo 'Run with sudo; Git/compilation run as deploy, Docker runs as root.' >&2
exit 1
fi
sudo -H -u deploy python3 "$PROJECT_ROOT/scripts/init-deps.py"
sudo -H -u deploy python3 "$PROJECT_ROOT/scripts/prepare-deploy.py" --build-database
docker compose --env-file "$PROJECT_ROOT/.env" -f "$PROJECT_ROOT/deploy/compose.yaml" up -d --build
printf '%s\n' 'When the database is ready, run the documented db-init command once.'
+49 -26
View File
@@ -1,42 +1,65 @@
#!/usr/bin/env python3
"""Initialize pinned external submodules, preferring local repositories over SSH."""
"""Initialize pinned dependencies via localhost SSH, falling back to GitHub."""
import argparse
from pathlib import Path
import subprocess
import sys
ROOT = Path(__file__).resolve().parents[1]
PROJECTS = ['ChuSQL', 'ChuSQL-RustDriver']
PREFIX = 'ChuGit:/srv/git/'
def git(*args, **kwargs):
return subprocess.run(['git', *args], cwd=ROOT, **kwargs)
def initialize(name, override):
path = f'external/{name}'
checkout = ROOT / path
pin = git('ls-files', '--stage', '--', path, text=True, capture_output=True, check=True).stdout.split()
if len(pin) != 4 or pin[0] != '160000':
raise RuntimeError(f'{path} is missing its committed submodule pin')
if (checkout / '.git').exists():
dirty = git('-C', str(checkout), 'status', '--porcelain', text=True, capture_output=True, check=True).stdout
if dirty:
raise RuntimeError(f'{path} has local changes; refusing to overwrite them')
current = git('-C', str(checkout), 'rev-parse', '--verify', 'HEAD', text=True, capture_output=True, check=False)
if current.returncode == 0 and current.stdout.strip() == pin[1]:
print(f'{name}: pinned checkout already available', flush=True)
return
if override:
source = override.resolve()
if not source.is_dir():
raise RuntimeError(f'Local repository does not exist: {source}')
sources = [source.as_uri()]
else:
sources = [f'ssh://git@localhost/srv/git/{name}.git', f'git@github.com:TheBadRoger/{name}.git']
for url in sources:
print(f'{name}: trying {url}', flush=True)
git('config', '--local', f'submodule.{path}.url', url, check=True)
options = ['-c', 'protocol.file.allow=always'] if override else []
options += ['-c', 'core.sshCommand=ssh -o BatchMode=yes -o ConnectTimeout=10 -o ConnectionAttempts=1']
if (checkout / '.git').exists():
git('-C', str(checkout), 'remote', 'set-url', 'origin', url, check=True)
result = git(*options, '-C', str(checkout), 'fetch', '--no-tags', 'origin', pin[1], check=False)
if result.returncode == 0:
git('-C', str(checkout), 'checkout', '--detach', pin[1], check=True)
return
else:
result = git(*options, 'submodule', 'update', '--init', '--checkout', '--', path, check=False)
if result.returncode == 0:
return
print(f'{name}: source failed; trying next configured source', file=sys.stderr, flush=True)
raise RuntimeError(f'No source contains the pinned {name} commit {pin[1]}')
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--database-repo', type=Path, help='local clone/bare repository of ChuSQL')
parser.add_argument('--driver-repo', type=Path, help='local clone/bare repository of ChuSQL-RustDriver')
parser.add_argument('--database-repo', type=Path, help='explicit local development source for ChuSQL')
parser.add_argument('--driver-repo', type=Path, help='explicit local development source for the driver')
args = parser.parse_args()
git_args = []
local_config = ROOT / 'config/git-local.config'
if not args.database_repo and not args.driver_repo and all((Path('/srv/git') / f'{name}.git').is_dir() for name in PROJECTS):
current = subprocess.run(['git', 'config', '--local', '--get-all', 'include.path'], cwd=ROOT, text=True, capture_output=True, check=False)
if str(local_config) not in current.stdout.splitlines():
subprocess.run(['git', 'config', '--local', '--add', 'include.path', str(local_config)], cwd=ROOT, check=True)
git_args.extend(['-c', f'include.path={local_config}'])
for name, override in zip(PROJECTS, [args.database_repo, args.driver_repo]):
source = override or Path('/srv/git') / f'{name}.git'
if override and not source.is_dir():
parser.error(f'Local repository does not exist: {source}')
if source.is_dir():
source = source.resolve()
subprocess.run(['git', '-C', str(source), 'rev-parse', '--git-dir'], check=True, capture_output=True)
key = f'url.{source.as_uri()}.insteadOf'
url = f'{PREFIX}{name}.git'
subprocess.run(['git', 'config', '--local', '--replace-all', key, url], cwd=ROOT, check=True)
git_args.extend(['-c', f'{key}={url}'])
print(f'{name}: using local repository {source}', flush=True)
# The file protocol is enabled only for this explicit, fixed set of dependencies.
if git_args:
git_args.extend(['-c', 'protocol.file.allow=always'])
subprocess.run(['git', *git_args, 'submodule', 'update', '--init', '--', *[f'external/{name}' for name in PROJECTS]], cwd=ROOT, check=True)
initialize(name, override)
if __name__ == '__main__':
+33
View File
@@ -46,6 +46,39 @@ class InitializeDependencies(unittest.TestCase):
for name in ['ChuSQL', 'ChuSQL-RustDriver']:
self.assertEqual((project / f'external/{name}/source.txt').read_text(), f'{name} committed source')
self.assertTrue(all(line.startswith('160000') for line in git(project, 'ls-files', '--stage', 'external').splitlines()))
# A fresh clone must fall back when localhost SSH fails. Redirect only the test's
# GitHub remotes to local fixtures, so this verifies fallback without network access.
fresh = root / 'fresh'
git(project, '-c', 'user.name=Test', '-c', 'user.email=test@example.invalid', 'commit', '-m', 'Pinned dependencies')
git(root, 'clone', '--no-local', str(project), str(fresh))
(fresh / 'scripts').mkdir()
shutil.copy2(SCRIPT, fresh / 'scripts/init-deps.py')
fallback_env = {**env, 'GIT_ALLOW_PROTOCOL': 'file', 'GIT_CONFIG_COUNT': '2'}
for i, (name, source) in enumerate(zip(['ChuSQL', 'ChuSQL-RustDriver'], sources)):
fallback_env[f'GIT_CONFIG_KEY_{i}'] = f'url.{Path(source).as_uri()}.insteadOf'
fallback_env[f'GIT_CONFIG_VALUE_{i}'] = f'git@github.com:TheBadRoger/{name}.git'
subprocess.run(['python3', 'scripts/init-deps.py'], cwd=fresh, env=fallback_env, check=True, capture_output=True)
for name in ['ChuSQL', 'ChuSQL-RustDriver']:
self.assertEqual((fresh / f'external/{name}/source.txt').read_text(), f'{name} committed source')
self.assertEqual(git(fresh / f'external/{name}', 'config', '--get', 'remote.origin.url'), f'git@github.com:TheBadRoger/{name}.git')
# Also cover an accessible but empty localhost bare repository: the initial
# submodule clone succeeds, fetching the pinned commit fails, then GitHub recovers.
empty = root / 'empty.git'
git(root, 'init', '--bare', str(empty))
partial = root / 'partial'
git(root, 'clone', '--no-local', str(project), str(partial))
(partial / 'scripts').mkdir()
shutil.copy2(SCRIPT, partial / 'scripts/init-deps.py')
partial_env = {**fallback_env, 'GIT_CONFIG_COUNT': '4'}
for i, name in enumerate(['ChuSQL', 'ChuSQL-RustDriver'], start=2):
empty_source = root / f'empty-{name}.git'
git(root, 'clone', '--bare', str(empty), str(empty_source))
partial_env[f'GIT_CONFIG_KEY_{i}'] = f'url.{empty_source.as_uri()}.insteadOf'
partial_env[f'GIT_CONFIG_VALUE_{i}'] = f'ssh://git@localhost/srv/git/{name}.git'
result = subprocess.run(['python3', 'scripts/init-deps.py'], cwd=partial, env=partial_env, text=True, capture_output=True)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
for name in ['ChuSQL', 'ChuSQL-RustDriver']:
self.assertEqual((partial / f'external/{name}/source.txt').read_text(), f'{name} committed source')
# Running the initialization a second time must preserve the same pins.
subprocess.run(['python3', 'scripts/init-deps.py', '--database-repo', sources[0], '--driver-repo', sources[1]], cwd=project, env=env, check=True, capture_output=True)