| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328 |
- """Promote the frozen local r13 Skill to a pinned public release, without business writes."""
- import argparse
- import ast
- from concurrent.futures import ThreadPoolExecutor
- import hashlib
- import importlib.util
- import json
- from pathlib import Path
- import re
- import shutil
- import subprocess
- import tempfile
- import urllib.request
- import zipfile
- ROOT = Path(__file__).resolve().parents[1]
- SOURCE = ROOT / 'docs/workshop-quality-r13-20261110/installation'
- RELEASE = 'ksf-workshop-skill-20261110-r13'
- OUT = ROOT / 'docs' / RELEASE
- STAGING = ROOT / 'work' / (RELEASE + '-publication')
- BRANCH = 'codex/' + RELEASE
- REMOTE = 'https://git.fmode.cn/18307996893/kangshifu-board.git'
- PUBLIC = REMOTE[:-4]
- sha = lambda b: hashlib.sha256(b).hexdigest()
- encode = lambda x: (json.dumps(x, ensure_ascii=False, indent=2) + '\n').encode('utf-8')
- GUIDE = '''# 康师傅工作坊 Skill r13:安装与升级
- 本版是本地r13-local-quality的正式发行,研究规则与本地定稿一致,仅将入口版本标记改为r13。包含按问题选材、研究闭环、13类成果标准、HTML/图片/图表及可编辑文件的使用、七类看板导航、逐环节保存和编号回读规则。没有新增固定采集配额,也没有把本地模拟当成真实研究验收。企业账套继续后置。
- ## 固定资源
- 先读本说明同一固定Git提交目录的distribution.json,下载archive.path,核字节数与SHA-256。在独立新目录安全解包,拒绝重复路径、越界路径和链接;按FILES.sha256核全部49个声明文件。Skill本体45文件,入口name始终为ksf-workshop-runtime。不要改用历史r10资源或分支最新地址,不建立第二个康师傅技能入口。
- ## 已部署Agent:只升级工作坊Skill
- 在原Agent内,以Skill实际属主执行。结果目录是新的、Skill之外的私有目录。以下三个命令从核验后的解包目录运行,同轮共用结果目录:
- ```sh
- python3 -B upgrade.py plan --target-dir /opt/data/skills/ksf-workshop-runtime --out /opt/data/ksf-workshop-r13-release-results
- python3 -B upgrade.py apply --target-dir /opt/data/skills/ksf-workshop-runtime --out /opt/data/ksf-workshop-r13-release-results
- python3 -B upgrade.py verify --target-dir /opt/data/skills/ksf-workshop-runtime --out /opt/data/ksf-workshop-r13-release-results
- ```
- plan通过才apply。完整已知r6/r7/r8/r9/r10和封存的r11/r12/r13本地版可升级;精确指纹以release.json为准,同名版本或相似变更数量不算匹配。已是正式r13时只核验。未知改动停止,不绕过基线或强制覆盖;可执行upgrade.py inventory并只回传公开文件指纹和缺项。
- apply先整目录私有备份并逐项核对;只改清单内的说明和FILES.sha256。六个业务脚本与runtime-lock.json保持字节,credential-binding.json及额外本机文件保持字节和权限。出现并发变化或新增文档同路径冲突停止;升级失败恢复本次更改。备份不上传。
- 本升级入口不安装/改动VOC、不改研究运行组件或运行补丁、不改身份/授权/连接、不重跑迁移、不写业务记录。已退休ksf-voc-library不得恢复活动SKILL.md。
- ## 新员工电脑:由数字人完成安装
- 先通过当前宿主真实设置或受支持机制确认用户可持久安装的技能根目录,不猜别的客户端路径,不使用内置受管目录,不手写技能索引。由数字人替换下列绝对路径占位,不让学员填写技术路径:
- ```text
- python -B install.py --skills-dir <宿主真实用户Skills目录>
- python -B install.py --skills-dir <宿主真实用户Skills目录> --out <新的私有结果目录> --apply --upgrade --prepare-runtime
- ```
- 此入口安装工作坊Skill和配套官方VOC 0.3.42;VOC原包从官方npm取得并校验,不随本包分发。已有组件按实际清单核验复用;新安装仅将原包.mcp.json的args/cwd适配当前路径,保留差异回执。生成连接器片段不等于宿主原生注册成功。Python 3.9+、Node.js 18+和npm以实际环境为准。
- ## 发布收尾
- 回传plan/apply/verify、识别基线、变更数、45个公开文件指纹、7个运行文件不变和本机身份文件保留结果。已有企业通道时只读核默认研究contract的backend/mode/当前代际,以及workshop-contract。初始化不采集、不建会话/项目/报告、不触发发布或真实写库。
- 文件升级、宿主实际加载、VOC执行、企业资料读取和成果保存分别报告;磁盘文件更新不冒称新任务已加载。用户未安排额外工作坊试跑,本次不增加模拟或采集测试。下一次正常任务按宿主机制读取r13;若需要重开任务则说明具体状态,不要求为了验收新建业务记录。
- 本版只发布Skill。线上操作手册仍须按用户后续安排更新,不把历史手册中固定r10安装链接称为本次r13入口。HTML/图片本地交付与实际看板附件展示分别核实,不能承诺尚未支持的渲染功能。
- '''
- def need(ok, code):
- if not ok:
- raise RuntimeError(code)
- def write(path, value):
- path.parent.mkdir(parents=True, exist_ok=True)
- path.write_bytes(value)
- def git(*args, cwd=None):
- cwd = STAGING if cwd is None else cwd
- r = subprocess.run(['git', *args], cwd=cwd, capture_output=True, timeout=90)
- need(r.returncode == 0, 'GIT_OPERATION_FAILED')
- return r.stdout.decode('utf-8').strip()
- def verify_manifest(directory):
- entries = {}
- for line in (directory / 'FILES.sha256').read_text('utf-8').splitlines():
- h, name = line.split(' ', 1)
- need(name not in entries and sha((directory / name).read_bytes()) == h, 'SEALED_SOURCE_CHANGED')
- entries[name] = h
- return entries
- def build():
- need(not (OUT / 'publish' / (RELEASE + '.zip')).exists(), 'NEVER_OVERWRITE_SEALED_RELEASE')
- verify_manifest(SOURCE)
- meta = json.loads((SOURCE / 'release.json').read_bytes())
- old = meta['workshop']['files']
- content = {n: (SOURCE / 'skills/ksf-workshop-runtime' / n).read_bytes() for n in old}
- need(len(content) == 45 and all(sha(content[n]) == h for n, h in old.items()), 'LOCAL_R13_CHANGED')
- entry = content['SKILL.md'].decode('utf-8')
- need(entry.count('version: r13-local-quality') == 1, 'UNEXPECTED_SOURCE_VERSION')
- content['SKILL.md'] = entry.replace('version: r13-local-quality', 'version: r13', 1).encode('utf-8')
- content['FILES.sha256'] = ''.join(sha(b) + ' ' + n + '\n' for n, b in sorted(content.items()) if n != 'FILES.sha256').encode('utf-8')
- hashes = {n: sha(b) for n, b in content.items()}
- known = json.loads((ROOT / 'docs/ksf-workshop-r10-baseline-upgrade-20261110-r1/package/release.json').read_bytes())
- baseline = dict(known['baselines'])
- r10 = json.loads((ROOT / 'docs/workshop-onboarding-20261109-r5/package/release.json').read_bytes())
- baseline['r10'] = r10['workshop']['files']
- r12 = json.loads((ROOT / 'docs/workshop-validated-final-20261110/installation/release.json').read_bytes())
- baseline.update(r12['upgradeFrom'])
- baseline['r12-local-exact'] = r12['workshop']['files']
- baseline['r13-local-exact'] = old
- protected = meta['workshop']['protectedFiles']
- need(len(protected) == 7 and all(b[n] == hashes[n] for b in baseline.values() for n in protected), 'PROTECTED_RUNTIME_CHANGED')
- meta.update(releaseId=RELEASE, upgradeFrom=baseline)
- meta['localOnly'] = False
- meta['installation']['supportedUpgradeFrom'] = list(baseline)
- meta['workshop'].update(version='r13', files=hashes)
- meta['promotion'] = {'fromVersion': 'r13-local-quality', 'instructionBodiesUnchanged': True,
- 'versionAndManifestOnly': True, 'originalLocalReleasePreserved': True}
- meta['releaseScope'] = {'skillOnly': True, 'accountSetupIncluded': False, 'fixedCollectionQuotaAdded': False,
- 'productionDatabaseWrites': 0, 'manualPublished': False}
- # Reuse the existing full-backup platform upgrader, adapting only its pinned target/manifest layout.
- upgrade = (ROOT / 'scripts/workshop-onboarding/upgrade-platform.py').read_text('utf-8')
- upgrade = upgrade.replace("package / 'install-core.py'", "package / 'install.py'")
- upgrade = upgrade.replace("meta['schema'] == 'ksf.workshop-platform-upgrade.v1'", "meta['schema'] == 'ksf.workshop-onboarding.v1'")
- upgrade = upgrade.replace("candidate = meta['candidate']", "candidate = meta['workshop']['files']\n meta['candidate'] = candidate\n meta['baselines'] = meta['upgradeFrom']\n meta['protectedFiles'] = meta['workshop']['protectedFiles']")
- upgrade = upgrade.replace("len(candidate) == 43", "len(candidate) == 45")
- upgrade = upgrade.replace('8757e3b58b461deb70201469f2e3579a7f03e5e6204c94aa327e143aa9b44bb3', hashes['SKILL.md'])
- upgrade = upgrade.replace("package / 'skill' / name", "package / 'skills/ksf-workshop-runtime' / name")
- upgrade = upgrade.replace("'r10'", "'r13'").replace("'publicFilesVerified': 43", "'publicFilesVerified': 45")
- need('install-core.py' not in upgrade and "== 43" not in upgrade, 'UPGRADER_ADAPTATION_INCOMPLETE')
- package = {'skills/ksf-workshop-runtime/' + n: b for n, b in content.items()}
- package.update({'install.py': (SOURCE / 'install.py').read_bytes(), 'check-voc.cjs': (SOURCE / 'check-voc.cjs').read_bytes(),
- 'upgrade.py': upgrade.encode('utf-8'), 'release.json': encode(meta)})
- for name in ['install.py', 'upgrade.py']:
- ast.parse(package[name].decode('utf-8-sig'), feature_version=(3, 9))
- package['FILES.sha256'] = ''.join(sha(b) + ' ' + n + '\n' for n, b in sorted(package.items())).encode('utf-8')
- for n, b in package.items():
- write(OUT / 'package' / n, b)
- (OUT / 'publish').mkdir(parents=True, exist_ok=True)
- archive = OUT / 'publish' / (RELEASE + '.zip')
- with zipfile.ZipFile(archive, 'x', zipfile.ZIP_DEFLATED) as z:
- for n, b in sorted(package.items()):
- info = zipfile.ZipInfo(n, (2026, 11, 10, 0, 0, 0))
- info.external_attr = 0o100644 << 16
- z.writestr(info, b, compress_type=zipfile.ZIP_DEFLATED)
- with zipfile.ZipFile(archive) as z:
- need(z.testzip() is None and set(z.namelist()) == set(package), 'ARCHIVE_LAYOUT_CHANGED')
- need(all(z.read(n) == b for n, b in package.items()), 'ARCHIVE_CONTENT_CHANGED')
- distribution = {'schema': 'ksf.workshop-onboarding-distribution.v1', 'releaseId': RELEASE,
- 'archive': {'path': archive.name, 'bytes': archive.stat().st_size, 'sha256': sha(archive.read_bytes())},
- 'installerSha256': sha(package['install.py']), 'platformUpgraderSha256': sha(package['upgrade.py']),
- 'runtimeCheckSha256': sha(package['check-voc.cjs']),
- 'workshopSkill': {'version': 'r13', 'files': len(content), 'entrySha256': hashes['SKILL.md']},
- 'supportedUpgradeFrom': list(baseline), 'workshopEntrypoints': ['ksf-workshop-runtime'],
- 'voc': {k: meta['voc'][k] for k in ['name', 'version', 'url', 'bytes', 'sha256']},
- 'thirdPartyArtifactIncluded': False, 'credentialsIncluded': False, 'databaseIncluded': False,
- 'userAttachmentRequired': False, 'protectedFilesModified': 0, 'businessWrites': 0,
- 'productionAgentUpdated': False, 'hostLoadingVerified': False, 'manualPublished': False}
- write(OUT / 'publish/distribution.json', encode(distribution))
- write(OUT / 'publish/INSTALL.md', GUIDE.encode('utf-8'))
- write(OUT / 'build-receipt.json', encode(distribution))
- print(json.dumps({'ok': True, 'built': True, 'files': len(package), 'skillFiles': len(content),
- 'archive': distribution['archive'], 'supportedUpgradeFrom': list(baseline)}))
- def validate():
- """Release integrity and the existing installer's upgrade protection; no workshop trial."""
- package = OUT / 'package'
- entries = verify_manifest(package)
- spec = importlib.util.spec_from_file_location('r13_release_upgrade', package / 'upgrade.py')
- upgrade = importlib.util.module_from_spec(spec)
- spec.loader.exec_module(upgrade)
- core = upgrade.core_at(package)
- meta, content = upgrade.load(core, package)
- fixture = Path(tempfile.mkdtemp(prefix=RELEASE + '-packaging-', dir=ROOT / 'work'))
- target = fixture / 'skills/ksf-workshop-runtime'
- baseline_dir = ROOT / 'docs/workshop-onboarding-20261109-r5/package/skills/ksf-workshop-runtime'
- shutil.copytree(baseline_dir, target)
- # Synthetic local-only binding, never a real enterprise identity or credential.
- (target / 'credential-binding.json').write_text('{"syntheticPackagingFixture":true}\n', 'utf-8')
- (target / 'credential-binding.json').chmod(0o600)
- before = upgrade.snapshot(core, target)
- result_dir = fixture / 'results'
- plan = upgrade.run('plan', str(target.resolve()), str(result_dir.resolve()), package)
- need(plan['baselineId'] == 'r10', 'R10_BASELINE_NOT_RECOGNIZED')
- applied = upgrade.run('apply', str(target.resolve()), str(result_dir.resolve()), package)
- verified = upgrade.run('verify', str(target.resolve()), str(result_dir.resolve()), package)
- need(applied['ok'] and verified['ok'] and verified['publicFilesVerified'] == 45, 'UPGRADE_VERIFY_FAILED')
- after = upgrade.snapshot(core, target)
- need(all(after[n] == before[n] for n in ['credential-binding.json'] + meta['protectedFiles']), 'LOCAL_FILES_CHANGED')
- second = upgrade.run('plan', str(target.resolve()), str((fixture / 'same-version').resolve()), package)
- need(second['action'] == 'verify-only', 'SAME_VERSION_NOT_IDEMPOTENT')
- unknown = fixture / 'unknown/ksf-workshop-runtime'
- shutil.copytree(baseline_dir, unknown)
- (unknown / 'credential-binding.json').write_text('{"syntheticPackagingFixture":true}\n', 'utf-8')
- (unknown / 'credential-binding.json').chmod(0o600)
- (unknown / 'references/workshop-feedback.md').write_text('unknown local change\n', 'utf-8')
- preserved = upgrade.snapshot(core, unknown)
- try:
- upgrade.run('plan', str(unknown.resolve()), str((fixture / 'unknown-result').resolve()), package)
- except Exception as e:
- need(str(e) == 'UPGRADE_BASELINE_CHANGED', 'UNEXPECTED_REJECTION')
- else:
- raise RuntimeError('UNKNOWN_BASELINE_ACCEPTED')
- need(preserved == upgrade.snapshot(core, unknown), 'UNKNOWN_BASELINE_CHANGED')
- candidate = package / 'skills/ksf-workshop-runtime'
- for p in candidate.rglob('*.md'):
- for _, link in re.findall(r'\[([^\]]+)\]\(([^)]+)\)', p.read_text('utf-8')):
- if '://' not in link and not link.startswith('#'):
- need((p.parent / link.split('#')[0]).is_file(), 'BROKEN_REFERENCE')
- result = {'ok': True, 'releaseId': RELEASE, 'packageManifestEntries': len(entries), 'publicSkillFiles': len(content),
- 'r10PlanApplyVerify': True, 'sameVersionVerifyOnly': True, 'unknownBaselineRejectedWithoutWrites': True,
- 'fullBackupVerified': applied['fullBackupVerified'], 'protectedFilesAndFixtureBindingPreserved': True,
- 'python39SyntaxChecked': True, 'workshopTrialExecuted': False, 'productionWrites': 0,
- 'plan': plan, 'apply': applied, 'verify': verified}
- write(OUT / 'validation.json', encode(result))
- # Advance current source to the formal label; frozen local reports and archives stay untouched.
- for n, b in content.items():
- need((ROOT / 'skills/ksf-workshop-runtime' / n).is_file(), 'SOURCE_FILE_MISSING')
- if (ROOT / 'skills/ksf-workshop-runtime' / n).read_bytes() != b:
- need(n in ['SKILL.md', 'FILES.sha256'], 'INSTRUCTION_BODY_CHANGED')
- (ROOT / 'skills/ksf-workshop-runtime' / n).write_bytes(b)
- print(json.dumps({'ok': True, 'releaseIntegrityVerified': True, 'r10UpgradeVerified': True,
- 'newWorkshopTrial': False, 'productionWrites': 0}))
- def prepare():
- need(json.loads((OUT / 'validation.json').read_bytes())['ok'], 'RELEASE_NOT_VALIDATED')
- need(not (STAGING / '.git').exists(), 'NEW_PUBLICATION_DIRECTORY_REQUIRED')
- files = {p.relative_to(OUT / 'publish').as_posix(): p.read_bytes() for p in (OUT / 'publish').rglob('*') if p.is_file()}
- files.update({'source/' + p.relative_to(OUT / 'package').as_posix(): p.read_bytes()
- for p in (OUT / 'package').rglob('*') if p.is_file() and '__pycache__' not in p.parts})
- files['validation.json'] = (OUT / 'validation.json').read_bytes()
- files['.gitattributes'] = b'* -text\n'
- if STAGING.exists():
- prior = {p.relative_to(STAGING).as_posix(): p.read_bytes() for p in STAGING.rglob('*') if p.is_file()}
- need(all(n in files and files[n] == b for n, b in prior.items()), 'PARTIAL_STAGING_CHANGED')
- spec = importlib.util.spec_from_file_location('existing_public_release_checks', ROOT / 'scripts/publish-workshop-onboarding-r5.py')
- checks = importlib.util.module_from_spec(spec)
- spec.loader.exec_module(checks)
- scan = checks.secret_scan(files)
- write(OUT / 'public-secret-check.json', encode(scan))
- for n, b in files.items():
- write(STAGING / n, b)
- git('init', '--initial-branch=' + BRANCH)
- git('config', 'user.name', git('config', '--get', 'user.name', cwd=ROOT))
- git('config', 'user.email', git('config', '--get', 'user.email', cwd=ROOT))
- git('config', 'core.autocrlf', 'false')
- git('remote', 'add', 'origin', REMOTE)
- git('add', '--', *sorted(files))
- git('commit', '-m', 'release: workshop Skill r13 with report quality, file delivery and strict r10 upgrade')
- need(set(git('ls-files').splitlines()) == set(files), 'UNEXPECTED_PUBLIC_FILES')
- for n, b in files.items():
- oid = git('rev-parse', 'HEAD:' + n)
- blob = subprocess.run(['git', 'cat-file', 'blob', oid], cwd=STAGING, capture_output=True, check=True)
- need(blob.stdout == b, 'GIT_BLOB_CHANGED')
- plan = {'ok': True, 'commit': git('rev-parse', 'HEAD'), 'branch': BRANCH,
- 'files': {n: sha(b) for n, b in files.items()}, 'published': False, 'mainBranchModified': False}
- write(OUT / 'git-plan.json', encode(plan))
- print(json.dumps({'ok': True, 'prepared': True, 'commit': plan['commit'], 'files': len(files)}))
- def publish():
- plan = json.loads((OUT / 'git-plan.json').read_bytes())
- commit = plan['commit']
- need(git('rev-parse', 'HEAD') == commit and not git('status', '--porcelain'), 'PUBLICATION_STAGING_CHANGED')
- before = git('ls-remote', 'origin', 'refs/heads/' + BRANCH)
- need(not before or before.split()[0] == commit, 'REMOTE_BRANCH_ALREADY_DIFFERS')
- git('push', 'origin', 'HEAD:refs/heads/' + BRANCH)
- need(git('ls-remote', 'origin', 'refs/heads/' + BRANCH).split()[0] == commit, 'PUSH_NOT_CONFIRMED')
- def readback(item):
- name, expected = item
- url = PUBLIC + '/raw/' + commit + '/' + name
- request = urllib.request.Request(url, headers={'User-Agent': 'ksf-skill-release-verifier/1'})
- with urllib.request.urlopen(request, timeout=40) as response:
- body = response.read()
- need(response.status == 200 and sha(body) == expected, 'PUBLIC_READBACK_MISMATCH')
- return {'path': name, 'url': url, 'status': response.status, 'bytes': len(body), 'sha256': sha(body)}
- with ThreadPoolExecutor(max_workers=4) as pool:
- resources = list(pool.map(readback, plan['files'].items()))
- guide = PUBLIC + '/raw/' + commit + '/INSTALL.md'
- distribution = json.loads((OUT / 'publish/distribution.json').read_bytes())
- receipt = {'ok': True, 'releaseId': RELEASE, 'workshopVersion': 'r13', 'publicResourcesPublished': True,
- 'repository': PUBLIC, 'branch': BRANCH, 'commit': commit, 'guideUrl': guide,
- 'distributionUrl': PUBLIC + '/raw/' + commit + '/distribution.json',
- 'archiveUrl': PUBLIC + '/raw/' + commit + '/' + distribution['archive']['path'],
- 'archive': distribution['archive'], 'publicFilesReadBack': len(resources), 'resources': resources,
- 'productionAgentUpdated': False, 'hostLoadingVerified': False, 'manualPublished': False,
- 'productionWrites': 0, 'mainBranchModified': False}
- write(OUT / 'online-release-receipt.json', encode(receipt))
- prompt = ('请将原工作坊Agent的ksf-workshop-runtime升级到已正式发布的r13。固定执行单:\n' + guide + '\n\n'
- '按同一固定提交目录的distribution.json核包,使用upgrade.py完成plan/apply/verify。'
- '从已知完整基线严格升级;未知差异停止并回传公开文件指纹,不强制覆盖。'
- '先整目录备份,保留credential-binding.json、六个业务脚本、runtime-lock.json、身份、授权、连接及额外文件。'
- '不安装或改动VOC,不改研究运行补丁,不重跑迁移,不创建账套,不采集、不建会话/项目/报告。'
- '回传基线、变更数、45文件核验、运行与身份文件保留及已有通道的只读检查;'
- '磁盘升级和宿主实际加载分别报告。本次不增加工作坊试跑,不替换操作手册。\n')
- write(OUT / '运维升级正文.txt', prompt.encode('utf-8'))
- install_prompt = ('请安装或升级康师傅工作坊唯一内部技能ksf-workshop-runtime到正式r13。先读取固定版本说明:\n' + guide + '\n'
- '核同目录distribution.json与ZIP后,由你识别宿主实际支持的用户技能目录并按说明安装。'
- '保留本机身份、授权、连接及其他技能;已有VOC按官方0.3.42核验复用,不另建康师傅技能入口。'
- '仅初始化,不采集、不建业务记录;分别报告文件安装、宿主加载、VOC和企业通道实际状态。\n')
- write(OUT / '员工安装正文.txt', install_prompt.encode('utf-8'))
- print(json.dumps({k: v for k, v in receipt.items() if k != 'resources'}, ensure_ascii=False, indent=2))
- if __name__ == '__main__':
- parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument('command', choices=['build', 'validate', 'prepare', 'publish'])
- parser.add_argument('--revision', default='')
- args = parser.parse_args()
- if args.revision:
- need(re.fullmatch(r'r[2-9][0-9]*', args.revision) is not None, 'INVALID_RELEASE_REVISION')
- RELEASE += '-' + args.revision
- OUT = ROOT / 'docs' / RELEASE
- STAGING = ROOT / 'work' / (RELEASE + '-publication')
- BRANCH = 'codex/' + RELEASE
- command = args.command
- globals()[command]()
|