north-scape/relayboard
Relayboard: accessible team boards with audited actions, dependency maps, and shared web/MCP use cases.
A remix template · 0 remixes · Remix north-scape/relayboard
scripts/check_template.py
"""Check Relayboard's fresh-install contracts, not Dodil/cloud compatibility."""
from pathlib import Path
import re
import yaml
REQUIRED_COLUMNS = {
'boards': {'id': 'TEXT', 'title': 'TEXT', 'owner_email': 'TEXT',
'revision': 'INTEGER', 'created_at': 'TIMESTAMP'},
'memberships': {'id': 'TEXT', 'board_id': 'TEXT', 'email': 'TEXT', 'role': 'TEXT'},
'cards': {'id': 'TEXT', 'board_id': 'TEXT', 'list_name': 'TEXT', 'title': 'TEXT',
'assignee': 'TEXT', 'due_date': 'DATE', 'description': 'TEXT', 'labels': 'TEXT',
'blocked_by': 'TEXT', 'revision': 'INTEGER', 'checklist': 'TEXT', 'updated_at': 'TIMESTAMP'},
'activity': {'id': 'TEXT', 'board_id': 'TEXT', 'card_id': 'TEXT', 'actor': 'TEXT',
'action': 'TEXT', 'changes': 'TEXT', 'event_at': 'TIMESTAMP'},
}
def require(condition, message):
# Unlike assert, these checks still run under python -O.
if not condition:
raise ValueError(message)
def parse_manifest(text):
value = yaml.safe_load(text)
if not isinstance(value, dict):
raise ValueError('manifest must be a YAML mapping')
return value
def only(items, label):
require(isinstance(items, list) and len(items) == 1 and isinstance(items[0], dict),
f'{label}: Relayboard requires exactly one definition')
return items[0]
def check_schema(text):
# Deliberately narrow: plain CREATE TABLE statements used by this app.
# This catches omissions; it does not emulate DataK3 SQL/locking semantics.
text = re.sub(r'/\*.*?\*/|--[^\n]*', '', text, flags=re.S)
require(not re.search(r'\b(?:BIGSERIAL|SMALLSERIAL|SERIAL)\b', text, re.I),
'schema must use application-generated keys, not sequences')
definitions = re.findall(
r'CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(\w+)\s*\((.*?)\)\s*;',
text, re.I | re.S)
tables = {name.lower(): body for name, body in definitions}
require(len(tables) == len(definitions), 'schema contains duplicate table definitions')
for name, body in tables.items():
require(re.search(r'\bPRIMARY\s+KEY\b', body, re.I), f'{name}: table needs a primary key')
for name, expected in REQUIRED_COLUMNS.items():
require(name in tables, f'schema is missing table {name}')
body = tables[name]
columns = {column.lower(): kind.upper() for column, kind in re.findall(
r'(?:^|,)\s*(\w+)\s+([A-Za-z]\w*)\b', body, re.I)}
require(expected.keys() <= columns.keys(),
f'{name}: missing columns {sorted(expected.keys() - columns.keys())}')
for column, kind in expected.items():
require(columns[column] == kind, f'{name}.{column}: expected type {kind}, got {columns[column]}')
require(re.search(r'\bid\s+TEXT\b[^,]*\bPRIMARY\s+KEY\b', body, re.I)
or re.search(r'\bPRIMARY\s+KEY\s*\(\s*id\s*\)', body, re.I),
f'{name}: id must be the application-generated TEXT primary key')
require(re.search(r'\brevision\s+INTEGER\s+NOT\s+NULL\s+DEFAULT\s+0\b', tables['boards'], re.I),
'boards.revision must default to 0 for new boards')
require(re.search(r"\bchecklist\s+TEXT\s+NOT\s+NULL\s+DEFAULT\s+'\[\]'", tables['cards'], re.I),
'cards.checklist must default to an empty JSON list')
def validate_template(root):
root = Path(root)
for path in ('.dodil/remix.yaml', '.dodil/deploy.yaml', '.github/workflows/ci.yml', 'LICENSE'):
require((root / path).is_file(), f'missing {path}')
remix = parse_manifest((root / '.dodil/remix.yaml').read_text())
deploy = parse_manifest((root / '.dodil/deploy.yaml').read_text())
parse_manifest((root / '.github/workflows/ci.yml').read_text())
for label, manifest in [('remix', remix), ('deploy', deploy)]:
require(type(manifest.get('version')) is int and manifest['version'] == 1,
f'{label}: expected version 1')
require(not remix.get('required_secrets'), 'remix must not wait for user-supplied secrets')
provision = remix.get('provision', {})
require(isinstance(provision, dict), 'provision must be a mapping')
bucket = only(provision.get('buckets'), 'buckets')
pool = only(provision.get('user_pools'), 'user_pools')
service = only(provision.get('service_accounts'), 'service_accounts')
for resource, suffix in [(bucket, '-data'), (pool, '-users'), (service, '-app')]:
require(resource.get('name') == '${{ dodil.repo }}' + suffix,
f'{suffix}: resource must follow the final remixed repo name')
for resource in [bucket, pool]:
require(resource.get('on_conflict', 'fail') == 'fail',
'bucket/pool conflict policy must be fail; suffix/reuse breaks isolation or bindings')
require(bucket.get('schema') == 'schema.sql', 'bucket must provision the complete schema.sql')
require((root / 'schema.sql').is_file(), 'missing schema.sql')
check_schema((root / 'schema.sql').read_text())
secrets = service.get('secrets', {})
require(secrets == {'client_id': 'DODIL_SERVICE_ACCOUNT_ID',
'client_secret': 'DODIL_SERVICE_ACCOUNT_SECRET'},
'generated service-account CI secret names must match the app contract')
grants = service.get('grants', [])
require({'role': 'k3.editor', 'bucket': bucket['name']} in grants,
'service account must edit its provisioned bucket')
require({'role': 'ignite.model-user'} in grants, 'service account needs Dodil Models access')
require(deploy.get('create_if_missing') is True, 'fresh deploy must create the app')
app = only(deploy.get('apps'), 'apps')
require(app.get('name') == '${{ dodil.repo }}', 'app must use the remixed repo name')
require(app.get('image') == '{registry}/{org}/{repo}:{sha}', 'image must follow remixed org/repo/commit')
require(app.get('user_pool') == pool['name'], 'deploy must attach the provisioned pool')
require(app.get('public_invoke') is True, 'MCP OAuth discovery requires public_invoke: true')
require(app.get('port') == 8080 and app.get('health', {}).get('path') == '/healthz',
'deploy port/health must match the container')
env = app.get('env', {})
require(isinstance(env, dict), 'app env must be a mapping')
require(env.get('BUCKET') == bucket['name'], 'BUCKET must match the provisioned bucket')
require(env.get('DODIL_USER_POOL') == '${{ dodil.org }}/' + pool['name'],
'OAuth issuer must follow the remixed org and pool')
for name in secrets.values():
require(env.get(name) == '${{ secrets.' + name + ' }}',
f'{name} must reference its generated CI secret, not a literal credential')
if __name__ == '__main__':
validate_template(Path(__file__).resolve().parents[1])
print('Local fresh-install contract checks passed; cloud validity is NOT established.')