Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,6 @@ PYTHON=../.venv/bin/python npm --prefix web run build

检查使用只读 GitHub 权限和临时构建目录;不上传网页、文档 ZIP 或记录正文,不配置飞书 Token,也不自动部署网站。新增资料仍需在 PR 中说明来源与公开范围,由资料负责人复核。需登录的飞书链接不做外链存活检查。

YAML 或日期元信息错误只报告文件位置和修正要求,不在日志中回显原文。测试和构建均使用 `PYTHON` 指定的解释器,未指定时使用 `python3`。

main 合并规则使用 `Community checks` 作为必需检查,要求分支与 main 同步并解决评审对话。维护者可在仓库 Settings → Branches 查看规则;首次贡献者的 fork 工作流仍遵循 GitHub 的运行审批设置。
2 changes: 1 addition & 1 deletion web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"build": "node build.mjs",
"test": "python3 tests/verify.py",
"test": "node tests/run.mjs",
"start": "python3 -m http.server 5274 --bind 127.0.0.1 --directory site"
},
"engines": {"node": ">=20"},
Expand Down
18 changes: 15 additions & 3 deletions web/read-records.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,14 @@
if meta[key] not in allowed:
raise ValueError(f'{key}: expected one of {sorted(allowed)}')
for key in ('date', 'updated'):
meta[key] = str(meta[key])
date.fromisoformat(meta[key])
value = str(meta[key])
if not re.fullmatch(r'\d{4}-\d{2}-\d{2}', value):
raise ValueError(f'{key} must be a YYYY-MM-DD date')
try:
date.fromisoformat(value)
except ValueError:
raise ValueError(f'{key} must be a valid calendar date') from None
meta[key] = value
if not isinstance(meta['example'], bool):
raise ValueError('example must be true or false')
if rel.parts[:2] == ('examples', 'records') and not meta['example']:
Expand All @@ -56,7 +62,13 @@
if key in meta and (not isinstance(meta[key], str) or not re.match(r'^https?://[^\s]+$', meta[key])):
raise ValueError(f'{key} must be an http(s) URL')
records.append(dict(meta, source_path='community/' + rel.as_posix(), body=text[match.end():].strip()))
except (ValueError, yaml.YAMLError) as exc:
except yaml.YAMLError as exc:
mark = getattr(exc, 'problem_mark', None)
location = f' at line {mark.line + 2}, column {mark.column + 1}' if mark else ''
errors.append(f'{rel}: invalid YAML metadata{location}; source text omitted')
except UnicodeError:
errors.append(f'{rel}: record must use UTF-8 encoding; source text omitted')
except ValueError as exc:
errors.append(f'{rel}: {exc}')
seen = set()
for r in records:
Expand Down
8 changes: 8 additions & 0 deletions web/tests/run.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {spawnSync} from 'node:child_process';
import {fileURLToPath} from 'node:url';

const result = spawnSync(process.env.PYTHON || 'python3',
[fileURLToPath(new URL('./verify.py', import.meta.url)), ...process.argv.slice(2)],
{stdio: 'inherit'});
if (result.error) console.error('Unable to start the configured Python interpreter. Check PYTHON.');
process.exit(result.status ?? 1);
41 changes: 40 additions & 1 deletion web/tests/verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,45 @@ def handle_starttag(self, tag, attrs):
self.urls += [v for k, v in attrs if k in ('href', 'src') and v]

class RecordWorkflow(unittest.TestCase):
def test_invalid_metadata_does_not_echo_source_text(self):
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
records = root / 'records'
records.mkdir()
file = records / 'invalid.md'
secret = 'SYNTHETIC-PRIVATE-METADATA-DO-NOT-LOG'
template = (BASE.parent / 'templates/meeting.md').read_text()
_, front, _ = template.split('---', 2)
meta = dict(yaml.safe_load(front), id='VERIFY-ERROR', title=secret,
summary=secret, reviewer='Test reviewer', source=secret,
source_revision='test-v1')
cases = ['---\nid: TEST\nsource: [' + secret + '\n---\n',
'---\n' + yaml.safe_dump(dict(meta, date=secret)) + '---\n',
'---\n' + yaml.safe_dump(dict(meta, updated='2026-02-30')) + '---\n']
for content in cases:
with self.subTest(content_type=cases.index(content)):
file.write_text(content + secret)
for command in ([sys.executable, str(BASE / 'check-public-records.py'), str(root)],
['node', str(BASE / 'build.mjs')]):
result = subprocess.run(command, capture_output=True, text=True,
env=dict(os.environ, CONTENT_ROOT=str(root),
SITE_OUTPUT=str(root / 'site'), PYTHON=sys.executable))
self.assertNotEqual(result.returncode, 0)
self.assertIn('invalid.md', result.stderr)
self.assertNotIn(secret, result.stdout + result.stderr)

def test_npm_test_uses_the_selected_python(self):
with tempfile.TemporaryDirectory() as temp:
interpreter = Path(temp) / 'python probe'
interpreter.write_text('#!' + sys.executable + '\n'
'import sys\nprint("SELECTED-PYTHON", sys.argv[1])\nsys.exit(7)\n')
interpreter.chmod(0o755)
result = subprocess.run(['npm', 'test'], cwd=BASE, capture_output=True, text=True,
env=dict(os.environ, PYTHON=str(interpreter)))
self.assertEqual(result.returncode, 7)
self.assertIn('SELECTED-PYTHON', result.stdout)
self.assertIn('verify.py', result.stdout)

def test_public_visibility_keeps_internal_originals_out_of_real_records(self):
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
Expand Down Expand Up @@ -52,7 +91,7 @@ def test_ingest_languages_metadata_and_links(self):
if (BASE/'drafts').is_dir():
shutil.copytree(BASE/'drafts', drafts)
else:
shutil.copytree(BASE.parent, drafts/'community', ignore=shutil.ignore_patterns('.git','web'))
shutil.copytree(BASE.parent, drafts/'community', ignore=shutil.ignore_patterns('.git','web','.venv','__pycache__'))
shutil.copytree(BASE/'organization-profile', drafts/'.github')
records = drafts/'community/records/meetings'
records.mkdir(parents=True, exist_ok=True)
Expand Down