프로젝트 소스 공개
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
"""Machine-specific release configuration, kept outside the public source tree."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import shlex
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class ConfigurationError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ReleaseConfiguration:
|
||||
stable_app: Path
|
||||
releases_root: Path
|
||||
deploy_host: str
|
||||
ssh_key: Path | None
|
||||
remote_source: str
|
||||
|
||||
def require_deployment(self) -> None:
|
||||
if not self.deploy_host:
|
||||
raise ConfigurationError('Set PROJECTQUINT_DEPLOY_HOST or deploy_host in .release.local.json')
|
||||
if self.deploy_host.startswith('-') or any(character.isspace() for character in self.deploy_host):
|
||||
raise ConfigurationError('The deployment host must be one SSH destination')
|
||||
if self.ssh_key is None or not self.ssh_key.is_file():
|
||||
raise ConfigurationError('PROJECTQUINT_DEPLOY_KEY must name an existing SSH identity file')
|
||||
# These paths are passed to a remote shell by the deployment tools.
|
||||
if not self.remote_source.startswith('/') or any(
|
||||
character not in '/._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
||||
for character in self.remote_source
|
||||
):
|
||||
raise ConfigurationError('The remote source must be an absolute path without shell metacharacters')
|
||||
|
||||
def environment(self) -> dict[str, str]:
|
||||
self.require_deployment()
|
||||
return {
|
||||
'PROJECTQUINT_STABLE_APP': str(self.stable_app),
|
||||
'PROJECTQUINT_RELEASES_ROOT': str(self.releases_root),
|
||||
'PROJECTQUINT_DEPLOY_HOST': self.deploy_host,
|
||||
'PROJECTQUINT_DEPLOY_KEY': str(self.ssh_key),
|
||||
'PROJECTQUINT_REMOTE_SOURCE': self.remote_source,
|
||||
}
|
||||
|
||||
def shell_exports(self) -> str:
|
||||
return '\n'.join(f'export {key}={shlex.quote(value)}' for key, value in self.environment().items())
|
||||
|
||||
|
||||
def load_configuration(root: Path) -> ReleaseConfiguration:
|
||||
path = root / '.release.local.json'
|
||||
values = {}
|
||||
if path.exists():
|
||||
try:
|
||||
values = json.loads(path.read_text(encoding='utf-8'))
|
||||
except (OSError, ValueError) as error:
|
||||
raise ConfigurationError('Cannot read .release.local.json') from error
|
||||
allowed = {'stable_app', 'releases_root', 'deploy_host', 'ssh_key', 'remote_source'}
|
||||
if not isinstance(values, dict) or values.keys() - allowed:
|
||||
raise ConfigurationError('Unknown release configuration fields')
|
||||
if any(not isinstance(value, str) or not value for value in values.values()):
|
||||
raise ConfigurationError('Release configuration values must be non-empty strings')
|
||||
|
||||
def value(key: str, environment: str, default: str) -> str:
|
||||
return os.environ.get(environment, values.get(key, default))
|
||||
|
||||
def absolute_path(key: str, environment: str, default: Path | None) -> Path | None:
|
||||
raw = value(key, environment, str(default) if default else '')
|
||||
if not raw:
|
||||
return None
|
||||
result = Path(raw)
|
||||
if not result.is_absolute():
|
||||
raise ConfigurationError(f'{environment} must be an absolute path')
|
||||
return result
|
||||
|
||||
stable_app = absolute_path('stable_app', 'PROJECTQUINT_STABLE_APP', root / '_workspace/builds/Stable/ProjectQuint.app')
|
||||
releases_root = absolute_path('releases_root', 'PROJECTQUINT_RELEASES_ROOT', root / '_workspace/releases')
|
||||
if stable_app is None or stable_app.suffix != '.app' or releases_root is None:
|
||||
raise ConfigurationError('Release output paths must include an absolute .app path and releases directory')
|
||||
return ReleaseConfiguration(
|
||||
stable_app=stable_app,
|
||||
releases_root=releases_root,
|
||||
deploy_host=value('deploy_host', 'PROJECTQUINT_DEPLOY_HOST', ''),
|
||||
ssh_key=absolute_path('ssh_key', 'PROJECTQUINT_DEPLOY_KEY', None),
|
||||
remote_source=value('remote_source', 'PROJECTQUINT_REMOTE_SOURCE', '/srv/projectquint/site-source'),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--shell', action='store_true', required=True)
|
||||
parser.parse_args()
|
||||
try:
|
||||
print(load_configuration(Path(__file__).resolve().parents[1]).shell_exports())
|
||||
except ConfigurationError as error:
|
||||
parser.exit(2, f'Release configuration: {error}\n')
|
||||
Reference in New Issue
Block a user