Fetch deployment dependencies over localhost SSH with GitHub fallback
This commit is contained in:
1 parent
d4caf05370
commit
959dba988c
7 files changed
+130
-39
No files matched your search
Executable
+12
@@ -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
@@ -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__':
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in new issue
Block a user