fix(cv): preserve rewrite language and qualify responsive document controls
This commit is contained in:
@@ -0,0 +1,377 @@
|
||||
"""Offline current-source image preparation and disposable Linux PDF qualification.
|
||||
|
||||
Build and run are explicit, separate commands. No dependency download or production service.
|
||||
"""
|
||||
import argparse
|
||||
import datetime
|
||||
import hashlib
|
||||
import http.cookiejar
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
import uuid
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
BASE = 'sha256:0d1cb44ba8f71d54de77459915a7052b894191cfeb75aa32f0c1e7ffdc21fda6'
|
||||
PIN = 'mariadb:12.2.2@sha256:42a27baaba3e8f3f0159d16381d5a7156661aa91dcc65f663cabb6a6f9503c00'
|
||||
APP = '/app/cv-ux-current'
|
||||
CONFIG = '../docs/cv-redesign/linux/playwright.config.ts'
|
||||
PASSWORD = 'jt-integration-only'
|
||||
AUTH_PASSWORD = 'E2ePassword123!'
|
||||
JWT = 'e2e-only-key-at-least-thirty-two-characters-long'
|
||||
|
||||
|
||||
def utc():
|
||||
return datetime.datetime.now(datetime.timezone.utc).isoformat()
|
||||
|
||||
|
||||
def scrub(value):
|
||||
text = str(value)
|
||||
for secret in (PASSWORD, AUTH_PASSWORD, JWT):
|
||||
text = text.replace(secret, '[synthetic-test-secret]')
|
||||
return text
|
||||
|
||||
|
||||
def digest(value):
|
||||
return hashlib.sha256(value).hexdigest()
|
||||
|
||||
|
||||
def identity(files):
|
||||
return digest(json.dumps(files, sort_keys=True, separators=(',', ':')).encode())
|
||||
|
||||
|
||||
def write(path, value):
|
||||
path.write_text(json.dumps(value, indent=2) + '\n', encoding='utf-8')
|
||||
|
||||
|
||||
def fresh(path):
|
||||
path = Path(path).resolve()
|
||||
allowed = (ROOT / '.artifacts/cv-ux-01').resolve()
|
||||
if not path.is_relative_to(allowed) or path == allowed or path.exists():
|
||||
raise ValueError('Select a nonexistent child of .artifacts/cv-ux-01; previous attempts are immutable.')
|
||||
path.mkdir(parents=True)
|
||||
return path
|
||||
|
||||
|
||||
class Commands:
|
||||
def __init__(self, out):
|
||||
self.out = out
|
||||
self.records = []
|
||||
|
||||
def run(self, args, *, env=None, cwd=ROOT, check=True, timeout=600):
|
||||
item = {'startedUtc': utc(), 'argv': [scrub(a) for a in args], 'cwd': str(cwd)}
|
||||
self.records.append(item)
|
||||
try:
|
||||
result = subprocess.run(['rtk', 'proxy', *map(str, args)], cwd=cwd, env=env,
|
||||
capture_output=True, text=True, encoding='utf-8', errors='replace', timeout=timeout)
|
||||
item['exitCode'] = result.returncode
|
||||
output = result.stdout + result.stderr
|
||||
log = self.out / f'command-{len(self.records):03}.log'
|
||||
log.write_text(scrub(output), encoding='utf-8')
|
||||
item['log'] = str(log)
|
||||
item['logSha256'] = digest(log.read_bytes())
|
||||
if check and result.returncode:
|
||||
raise RuntimeError(f'Command {len(self.records)} failed with exit {result.returncode}; inspect {log.name}')
|
||||
return result.stdout.strip()
|
||||
finally:
|
||||
item['finishedUtc'] = utc()
|
||||
write(self.out / 'commands.json', self.records)
|
||||
|
||||
|
||||
def sources(commands):
|
||||
paths = commands.run(['git', '-c', 'core.quotePath=false', 'ls-files', '--cached', '--others', '--exclude-standard']).splitlines()
|
||||
prefixes = ('JobTrackerApi/', 'job-tracker-ui/src/', 'job-tracker-ui/e2e/')
|
||||
exact = {'global.json', '.dockerignore', 'docker-compose.database.yml', 'NuGet.Config', 'nuget.config', 'job-tracker-ui/package.json', 'job-tracker-ui/package-lock.json',
|
||||
'scripts/verify-cv-fidelity.py', 'docs/redesign/phase-9/p9-requal-01/pdf-proof.py'}
|
||||
selected = [name for name in paths if name.startswith(prefixes) or name in exact
|
||||
or name.startswith(('Directory.', 'job-tracker-ui/playwright', 'job-tracker-ui/scripts/'))
|
||||
or name.startswith('docs/cv-redesign/') and name.endswith(('.py', '.ts', '.json'))]
|
||||
return {name: digest((ROOT / name).read_bytes()) for name in sorted(set(selected)) if (ROOT / name).is_file()}
|
||||
|
||||
|
||||
def tree(path):
|
||||
return {file.relative_to(path).as_posix(): digest(file.read_bytes()) for file in sorted(path.rglob('*')) if file.is_file()}
|
||||
|
||||
|
||||
def image_id(value):
|
||||
if not re.fullmatch(r'sha256:[a-f0-9]{64}', value):
|
||||
raise ValueError('An immutable local image ID is required.')
|
||||
return value
|
||||
|
||||
|
||||
def schema_name(value):
|
||||
if not re.fullmatch(r'jt_it_[a-f0-9]{32}', value):
|
||||
raise ValueError('Only a generated disposable schema is allowed.')
|
||||
return value
|
||||
|
||||
|
||||
def guard_db(data, name, project, port):
|
||||
if not re.fullmatch(r'jobtracker-it-[a-f0-9]{32}', project) or name != project + '-mariadb-test-1':
|
||||
raise ValueError('Unexpected generated DB owner.')
|
||||
labels = data.get('Config', {}).get('Labels', {})
|
||||
host = data.get('HostConfig', {})
|
||||
if (data.get('Name') != '/' + name or data['Config']['Image'] != PIN
|
||||
or labels.get('com.docker.compose.project') != project
|
||||
or labels.get('com.docker.compose.service') != 'mariadb-test'
|
||||
or not data['State']['Running'] or data['State'].get('Health', {}).get('Status') != 'healthy'
|
||||
or host.get('Privileged') or host.get('NetworkMode') == 'host'
|
||||
or '/var/lib/mysql' not in host.get('Tmpfs', {})
|
||||
or host.get('PortBindings', {}).get('3306/tcp') != [{'HostIp': '127.0.0.1', 'HostPort': str(port)}]
|
||||
or any(mount.get('Destination') == '/var/lib/mysql' and mount.get('Type') != 'tmpfs' for mount in data.get('Mounts', []))):
|
||||
raise ValueError('DB must be the exact healthy pinned loopback tmpfs test service.')
|
||||
|
||||
|
||||
def verify_build(record, expected_image, current_sources):
|
||||
if (record.get('exitCode') != 0 or record.get('baseImageId') != BASE
|
||||
or record.get('imageId') != image_id(expected_image)
|
||||
or record.get('applicationPath') != APP
|
||||
or record.get('sourceSha256') != identity(current_sources)
|
||||
or record.get('sources') != current_sources):
|
||||
raise ValueError('Current source/build/image identity differs; rebuild in a fresh root.')
|
||||
|
||||
|
||||
def build(args):
|
||||
out = fresh(args.output)
|
||||
commands = Commands(out)
|
||||
record = {'startedUtc': utc(), 'exitCode': 1, 'baseImageId': BASE, 'applicationPath': APP}
|
||||
try:
|
||||
record['sources'] = sources(commands)
|
||||
record['sourceSha256'] = identity(record['sources'])
|
||||
record['commit'] = commands.run(['git', 'rev-parse', 'HEAD'])
|
||||
record['status'] = commands.run(['git', 'status', '--short'])
|
||||
record['dotnetInfo'] = commands.run([args.dotnet, '--info'])
|
||||
assert commands.run(['docker', 'image', 'inspect', '--format', '{{.Id}}', BASE]) == BASE
|
||||
record['baseMetadata'] = json.loads(commands.run(['docker', 'image', 'inspect', '--format', '{{json .Config.Labels}}', BASE]))
|
||||
native_tag = 'jobtracker-cv-ux-native:' + uuid.uuid4().hex
|
||||
commands.run(['docker', 'tag', BASE, native_tag])
|
||||
assert commands.run(['docker', 'image', 'inspect', '--format', '{{.Id}}', native_tag]) == BASE
|
||||
record['baseImageTag'] = native_tag
|
||||
publish = out / 'publish'
|
||||
env = {**os.environ, 'DOTNET_ROOT': str(Path(args.dotnet).parent)}
|
||||
record['publishEnvironment'] = {'DOTNET_ROOT': env['DOTNET_ROOT'], 'NuGetAudit': env.get('NuGetAudit')}
|
||||
commands.run([args.dotnet, 'publish', 'JobTrackerApi/JobTrackerApi.csproj', '--configuration', 'Release',
|
||||
'--no-restore', '--output', str(publish), '/p:UseAppHost=false', '--self-contained', 'false'], env=env)
|
||||
record['publish'] = tree(publish)
|
||||
record['publishSha256'] = identity(record['publish'])
|
||||
if 'JobTrackerApi.dll' not in record['publish'] or 'JobTrackerApi.deps.json' not in record['publish']:
|
||||
raise ValueError('Portable current API output is incomplete.')
|
||||
dockerfile = out / 'Dockerfile'
|
||||
dockerfile.write_text(f'FROM {native_tag}\nWORKDIR {APP}\nCOPY publish/ ./\n'
|
||||
+ f'LABEL io.jobjakt.cv-ux.source-sha256="{record["sourceSha256"]}" '
|
||||
+ f'io.jobjakt.cv-ux.publish-sha256="{record["publishSha256"]}"\n'
|
||||
+ 'ENTRYPOINT ["dotnet","JobTrackerApi.dll"]\n', encoding='utf-8')
|
||||
record['dockerfileSha256'] = digest(dockerfile.read_bytes())
|
||||
tag = 'jobtracker-cv-ux-api:' + uuid.uuid4().hex
|
||||
commands.run(['docker', 'build', '--network', 'none', '--pull=false', '--file', str(dockerfile), '--tag', tag, str(out)])
|
||||
record['imageTag'] = tag
|
||||
record['imageId'] = image_id(commands.run(['docker', 'image', 'inspect', '--format', '{{.Id}}', tag]))
|
||||
if record['sources'] != sources(commands) or record['publish'] != tree(publish):
|
||||
raise ValueError('Source or publish bytes changed during build.')
|
||||
record['exitCode'] = 0
|
||||
except Exception as error:
|
||||
record['error'] = scrub(error)
|
||||
finally:
|
||||
record['finishedUtc'] = utc()
|
||||
write(out / 'build.json', record)
|
||||
print(json.dumps({'exitCode': record['exitCode'], 'record': str(out / 'build.json')}))
|
||||
return record['exitCode']
|
||||
|
||||
|
||||
def free_port(port=0):
|
||||
with socket.socket() as probe:
|
||||
probe.bind(('127.0.0.1', port))
|
||||
return probe.getsockname()[1]
|
||||
|
||||
|
||||
def auth_identity(version, commit):
|
||||
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()))
|
||||
request = urllib.request.Request('http://localhost:5302/api/auth/login',
|
||||
data=json.dumps({'email': 'e2e@example.test', 'password': AUTH_PASSWORD}).encode(),
|
||||
headers={'Content-Type': 'application/json', 'Origin': 'http://localhost:3300'})
|
||||
with opener.open(request, timeout=10) as response:
|
||||
response.read()
|
||||
with opener.open('http://localhost:5302/api/auth/me', timeout=10) as response:
|
||||
me = json.load(response)
|
||||
if me.get('appVersion') != version or me.get('appCommitSha') != commit:
|
||||
raise ValueError('Authenticated API build metadata mismatch.')
|
||||
return {'appVersion': me['appVersion'], 'appCommitSha': me['appCommitSha']}
|
||||
|
||||
|
||||
def run(args):
|
||||
out = fresh(args.output)
|
||||
commands = Commands(out)
|
||||
record = {'startedUtc': utc(), 'exitCode': 1, 'cleanup': {}}
|
||||
suffix = uuid.uuid4().hex
|
||||
project = 'jobtracker-it-' + suffix
|
||||
db = project + '-mariadb-test-1'
|
||||
network = 'jobtracker-cv-ux-' + suffix
|
||||
api = network + '-api'
|
||||
schema = schema_name('jt_it_' + uuid.uuid4().hex)
|
||||
compose = ['docker', 'compose', '--project-name', project, '--project-directory', str(ROOT),
|
||||
'-f', str(ROOT / 'docker-compose.database.yml'), '--profile', 'integration']
|
||||
db_attempted = created = attached = network_created = api_created = False
|
||||
|
||||
def sql(query):
|
||||
return commands.run(['docker', 'exec', '-e', 'MYSQL_PWD=' + PASSWORD, db,
|
||||
'mariadb', '-uroot', '--batch', '--skip-column-names', '-e', query])
|
||||
|
||||
def clean(key, action):
|
||||
try:
|
||||
action()
|
||||
record['cleanup'][key] = True
|
||||
except Exception as error:
|
||||
record['cleanup'][key + 'Error'] = scrub(error)
|
||||
record['exitCode'] = 1
|
||||
|
||||
try:
|
||||
build_record = json.loads(Path(args.build_record).read_text(encoding='utf-8'))
|
||||
current_sources = sources(commands)
|
||||
verify_build(build_record, args.image, current_sources)
|
||||
publish = Path(args.build_record).resolve().parent / 'publish'
|
||||
if tree(publish) != build_record['publish']:
|
||||
raise ValueError('Published API output drifted after build.')
|
||||
static = ROOT / 'job-tracker-ui/out'
|
||||
record['frontend'] = tree(static)
|
||||
if not record['frontend']:
|
||||
raise ValueError('Prebuilt frontend output is required.')
|
||||
record.update({'buildRecord': str(Path(args.build_record).resolve()), 'buildRecordSha256': digest(Path(args.build_record).read_bytes()),
|
||||
'sourceSha256': build_record['sourceSha256'], 'imageId': args.image, 'commit': build_record['commit'],
|
||||
'schema': schema, 'dbContainer': db, 'dbProject': project, 'apiContainer': api, 'network': network})
|
||||
image = json.loads(commands.run(['docker', 'image', 'inspect', args.image]))[0]
|
||||
if (image['Id'] != args.image or image['Config']['WorkingDir'] != APP
|
||||
or image['Config'].get('Labels', {}).get('io.jobjakt.cv-ux.source-sha256') != record['sourceSha256']
|
||||
or image['Config'].get('Labels', {}).get('io.jobjakt.cv-ux.publish-sha256') != build_record['publishSha256']):
|
||||
raise ValueError('Image source/publish labels or current app directory mismatch.')
|
||||
free_port(5302)
|
||||
free_port(3300)
|
||||
record['chromium'] = commands.run(['docker', 'run', '--rm', '--network', 'none', '--entrypoint', '/usr/bin/chromium', args.image, '--version'])
|
||||
record['runtime'] = commands.run(['docker', 'run', '--rm', '--network', 'none', '--entrypoint', 'dotnet', args.image, '--info'])
|
||||
record['apiDllSha256'] = commands.run(['docker', 'run', '--rm', '--network', 'none', '--entrypoint', 'sha256sum', args.image, APP + '/JobTrackerApi.dll']).split()[0]
|
||||
if record['apiDllSha256'] != build_record['publish']['JobTrackerApi.dll']:
|
||||
raise ValueError('Image API DLL differs from current publish bytes.')
|
||||
port = free_port()
|
||||
record['dbPort'] = port
|
||||
environment = {**os.environ, 'MARIADB_TEST_PORT': str(port)}
|
||||
assert commands.run(['docker', 'image', 'inspect', '--format', '{{.Id}}', PIN])
|
||||
db_attempted = True
|
||||
commands.run(compose + ['up', '--pull', 'never', '-d', '--wait', '--wait-timeout', '120', 'mariadb-test'], env=environment)
|
||||
guard_db(json.loads(commands.run(['docker', 'inspect', db]))[0], db, project, port)
|
||||
record['serverVersion'] = sql('SELECT VERSION()')
|
||||
if not record['serverVersion'].startswith('12.2.2-MariaDB'):
|
||||
raise ValueError('Exact MariaDB 12.2.2 is required.')
|
||||
assert sql(f"SELECT COUNT(*) FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME='{schema}'") == '0'
|
||||
sql(f'CREATE DATABASE `{schema}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci')
|
||||
created = True
|
||||
commands.run(['docker', 'network', 'create', '--internal', '--label', 'task=cv-ux-01', network])
|
||||
network_created = True
|
||||
commands.run(['docker', 'network', 'connect', '--alias', 'mariadb', network, db])
|
||||
attached = True
|
||||
version = 'cv-ux-linux-' + record['sourceSha256'][:12]
|
||||
variables = {'ASPNETCORE_ENVIRONMENT': 'Development', 'Database__Provider': 'mariadb', 'Database__ServerVersion': '12.2.2',
|
||||
'ConnectionStrings__JobTracker': f'Server=mariadb;Database={schema};User ID=root;Password={PASSWORD};SslMode=None',
|
||||
'Auth__Require': 'true', 'Auth__JwtKey': JWT, 'Auth__AdminEmail': 'e2e@example.test', 'Auth__AdminPassword': AUTH_PASSWORD,
|
||||
'Auth__AllowRegistration': 'false', 'RateLimits__AuthLoginPermitLimit': '1000', 'Cors__Origins__0': 'http://localhost:3300',
|
||||
'Workers__AiOperationsEnabled': 'false', 'Workers__SavedJobSearchAlertsEnabled': 'false', 'HttpsRedirection__Enabled': 'false',
|
||||
'App__Version': version, 'App__CommitSha': record['commit'], 'Data__Root': '/data', 'Logging__LogLevel__Default': 'Warning'}
|
||||
create = ['docker', 'create', '--name', api, '--label', 'task=cv-ux-01', '--network', network,
|
||||
'-p', '127.0.0.1:5302:8080', '--tmpfs', '/data:rw,nosuid,size=512m']
|
||||
for key, value in variables.items():
|
||||
create += ['-e', key + '=' + value]
|
||||
api_created = True
|
||||
commands.run(create + [args.image])
|
||||
assert commands.run(['docker', 'inspect', '--format', '{{.Image}}', api]) == args.image
|
||||
commands.run(['docker', 'start', api])
|
||||
deadline = time.monotonic() + 120
|
||||
while True:
|
||||
try:
|
||||
with urllib.request.urlopen('http://localhost:5302/health', timeout=3) as response:
|
||||
health = json.load(response)
|
||||
if health.get('version') != version:
|
||||
raise ValueError('Linux API health version mismatch.')
|
||||
with urllib.request.urlopen('http://localhost:5302/ready', timeout=3) as response:
|
||||
assert json.load(response)['status'] == 'ready'
|
||||
record['health'] = health
|
||||
record['apiBuildMetadata'] = auth_identity(version, record['commit'])
|
||||
break
|
||||
except Exception:
|
||||
if time.monotonic() >= deadline:
|
||||
raise
|
||||
time.sleep(1)
|
||||
migration_sql = f'SELECT MigrationId FROM `{schema}`.`__EFMigrationsHistory` ORDER BY MigrationId'
|
||||
record['migrations'] = sql(migration_sql).splitlines()
|
||||
if '20261025090000_AddCoverLetterDocumentDesign' not in record['migrations']:
|
||||
raise ValueError('Current letter design migration was not applied.')
|
||||
env = {**os.environ, 'CV_UX_LINUX_READY': version, 'CV_UX_LINUX_SOURCE': record['sourceSha256'],
|
||||
'PLAYWRIGHT_STATIC_EXPORT': 'true', 'PLAYWRIGHT_PREBUILT_EXPORT': 'true', 'PLAYWRIGHT_REUSE_SERVERS': 'false',
|
||||
'NEXT_PUBLIC_API_BASE_URL': 'http://localhost:5302/api', 'CV_FIDELITY_OUTPUT': str(out / 'fidelity'),
|
||||
'CV_SETTINGS_FIDELITY_OUTPUT': str(out / 'settings-fidelity'), 'LETTER_FIDELITY_OUTPUT': str(out / 'letter-fidelity'),
|
||||
'PLAYWRIGHT_JSON_OUTPUT_NAME': str(out / 'linux-renderer.json')}
|
||||
commands.run(['node', 'node_modules/@playwright/test/cli.js', 'test', '--config=' + CONFIG, '--retries=0',
|
||||
'--reporter=list,json', '--output=' + str(out / 'test-results')], cwd=ROOT / 'job-tracker-ui', env=env, timeout=1800)
|
||||
native = json.loads((out / 'linux-renderer.json').read_text(encoding='utf-8'))
|
||||
stats = native.get('stats', {})
|
||||
if stats.get('expected') != 6 or any(stats.get(key) != 0 for key in ('unexpected', 'flaky', 'skipped')) or native.get('errors'):
|
||||
raise ValueError('Expected six passing browser cases with no skips, retries or errors.')
|
||||
record['browserStats'] = stats
|
||||
if sql(migration_sql).splitlines() != record['migrations'] or sources(commands) != current_sources or tree(static) != record['frontend']:
|
||||
raise ValueError('Source/runtime/migration drift during rendering.')
|
||||
for folder in ('fidelity', 'letter-fidelity'):
|
||||
if not (out / folder / 'measurements.json').is_file():
|
||||
raise ValueError('Native corpus measurements missing: ' + folder)
|
||||
cv = json.loads((out / 'fidelity/measurements.json').read_text(encoding='utf-8'))
|
||||
letter = json.loads((out / 'letter-fidelity/measurements.json').read_text(encoding='utf-8'))
|
||||
settings_files = list((out / 'settings-fidelity').glob('*-measurements.json'))
|
||||
if len(cv.get('fixtures', [])) != 16 or len(settings_files) != 4 or sum(len(json.loads(file.read_text(encoding='utf-8')).get('fixtures', [])) for file in settings_files) != 8:
|
||||
raise ValueError('The original CV16 + settings8 document membership is incomplete.')
|
||||
if {fixture.get('fixture') for fixture in letter.get('fixtures', [])} != {'light-en-a4', 'dark-nb-a4', 'light-nb-letter', 'dark-en-letter'}:
|
||||
raise ValueError('The independent letter4 document membership is incomplete.')
|
||||
record['exitCode'] = 0
|
||||
except Exception as error:
|
||||
record['error'] = scrub(error)
|
||||
finally:
|
||||
if api_created:
|
||||
clean('apiLogsCaptured', lambda: commands.run(['docker', 'logs', api]))
|
||||
clean('apiRemoved', lambda: commands.run(['docker', 'rm', '--force', api]))
|
||||
if attached:
|
||||
clean('databaseDisconnected', lambda: commands.run(['docker', 'network', 'disconnect', network, db]))
|
||||
if network_created:
|
||||
clean('networkRemoved', lambda: commands.run(['docker', 'network', 'rm', network]))
|
||||
if created and (not api_created or record['cleanup'].get('apiRemoved')):
|
||||
def remove_schema():
|
||||
sql(f'DROP DATABASE `{schema_name(schema)}`')
|
||||
assert sql(f"SELECT COUNT(*) FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME='{schema}'") == '0'
|
||||
clean('schemaRemoved', remove_schema)
|
||||
elif created:
|
||||
record['cleanup']['schemaRemovedError'] = 'API removal unconfirmed; schema retained.'
|
||||
record['exitCode'] = 1
|
||||
if db_attempted:
|
||||
clean('dbLogsCaptured', lambda: commands.run(compose + ['logs', '--no-color', 'mariadb-test']))
|
||||
clean('databaseRemoved', lambda: commands.run(compose + ['rm', '--stop', '--force', 'mariadb-test']))
|
||||
record['finishedUtc'] = utc()
|
||||
write(out / 'process.json', record)
|
||||
print(json.dumps({'exitCode': record['exitCode'], 'record': str(out / 'process.json')}))
|
||||
return record['exitCode']
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
modes = parser.add_subparsers(dest='mode', required=True)
|
||||
image = modes.add_parser('build')
|
||||
image.add_argument('--output', required=True)
|
||||
image.add_argument('--dotnet', default='C:/Users/Connor/.dotnet/dotnet.exe')
|
||||
renderer = modes.add_parser('run')
|
||||
renderer.add_argument('--output', required=True)
|
||||
renderer.add_argument('--build-record', required=True)
|
||||
renderer.add_argument('--image', required=True)
|
||||
args = parser.parse_args()
|
||||
return build(args) if args.mode == 'build' else run(args)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user