fix: make council source checksum reproducible across platforms
CI / test (pull_request) Failing after 4m47s
CI / Fingerprint diagnostics (production deployment temporarily disabled) (pull_request) Has been skipped

This commit is contained in:
codex committed 2026-10-08 14:52:12 +03:00
1 parent 74f32a3e67
commit 76bcb0db35
10 files changed
+43 -13

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
app/data/council_articles.csv text eol=lf
app/data/council_articles.json text eol=lf
+5 -2
View File
@@ -155,7 +155,7 @@ node --check app/static/admin.js
Тесты с данными используют временную SQLite; отдельные API smoke-тесты запускают приложение с его текущей конфигурацией. Браузерная проверка: `pip install playwright`, `python -m playwright install chromium`, затем `python tests/browser_admin.py`. Она проверяет реальный рендеринг страниц, клавиатуру, диалог, фильтры и восстановление прогресса после ошибки. Скриншоты сохраняются во временную папку; путь выводится в конце.
Версия сервиса: `0.15.2`. Админка всегда показывает версии backend и frontend в footer.
Версия сервиса: `0.15.3`. Админка всегда показывает версии backend и frontend в footer.
## Разовый импорт публикаций
@@ -219,6 +219,9 @@ python scripts/prepare_council_articles.py app/data/council_articles.csv
```
Нужна только стандартная библиотека Python. Скрипт проверяет структуру CSV и индекс,
сохраняет число записей и SHA-256 источника и заменяет JSON атомарно после проверки.
сохраняет число записей и SHA-256 источника с окончаниями строк LF и заменяет JSON
атомарно после проверки. CRLF нормализуются в LF только при чтении; исходный CSV
не перезаписывается. JSON и хеш одинаковы на Windows и Linux; Git фиксирует эти
файлы с LF.
После замены файла перезапустите процессы API, чтобы сбросить их кэш. Обновления
публикаций из БД продолжают отображаться без перезапуска.
+2 -1
View File
@@ -1,7 +1,8 @@
{
"schema_version": 1,
"source_file": "council_articles.csv",
"source_sha256": "7729d06503de8197c25e61f6e653a342390f274f31fd6bed43496c4cc21b8177",
"source_sha256": "f9e6126efcb144042e89bcce284deb3e5b26dd76b1be0741962626d2b140c043",
"source_hash_eol": "LF",
"source_rows": 1797,
"publications": [
{
+4 -2
View File
@@ -41,7 +41,9 @@ def _journal_names(data: dict) -> list[str]:
def prepare_reference(source: Path) -> dict:
publications = []
by_author = defaultdict(list)
source_bytes = source.read_bytes()
# Git stores this text export with LF; canonicalize Windows CRLF before
# parsing and hashing so the prepared artifact is identical on both hosts.
source_bytes = source.read_bytes().replace(b"\r\n", b"\n")
with StringIO(source_bytes.decode("utf-8-sig"), newline="") as stream:
reader = csv.DictReader(stream, delimiter=";")
required = {"Название публикации", "Год", "DOI", "Авторы НИУ ВШЭ", "Квартиль scopus", "Квартиль wos"}
@@ -65,7 +67,7 @@ def prepare_reference(source: Path) -> dict:
for author in sorted({_name(value) for value in authors} - {""}):
by_author[author].append(index)
return {"schema_version": 1, "source_file": source.name,
"source_sha256": hashlib.sha256(source_bytes).hexdigest(),
"source_sha256": hashlib.sha256(source_bytes).hexdigest(), "source_hash_eol": "LF",
"source_rows": len(publications), "publications": publications, "by_author": dict(by_author)}
+3 -3
View File
@@ -1,3 +1,3 @@
APP_VERSION = "0.15.2"
FRONTEND_VERSION = "0.15.2"
BACKEND_VERSION = "0.15.2"
APP_VERSION = "0.15.3"
FRONTEND_VERSION = "0.15.3"
BACKEND_VERSION = "0.15.3"
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "miem-workers"
version = "0.15.2"
version = "0.15.3"
description = "MIEM employees parser, admin API, and web admin"
requires-python = ">=3.11"
dependencies = [
+1 -1
View File
@@ -21,7 +21,7 @@ if __name__ == "__main__":
parser.error("Output must not overwrite the source CSV")
result = prepare_reference(args.source)
text = json.dumps(result, ensure_ascii=False, indent=2) + "\n"
with NamedTemporaryFile(mode="w", encoding="utf-8", dir=args.output.parent,
with NamedTemporaryFile(mode="w", encoding="utf-8", newline="\n", dir=args.output.parent,
prefix=args.output.name + ".", suffix=".tmp", delete=False) as stream:
stream.write(text)
temporary = Path(stream.name)
+1 -1
View File
@@ -18,7 +18,7 @@ def test_health_returns_versions():
response = TestClient(app).get("/api/health")
assert response.status_code == 200
assert response.json()["backend_version"] == "0.15.2"
assert response.json()["backend_version"] == "0.15.3"
def test_api_employees_and_stats_require_admin_session():
+1 -1
View File
@@ -215,7 +215,7 @@ def test_public_page_data_minimization_and_safe_links(db_session, export):
assert "&lt;script&gt;" in response.text and "<script>" not in response.text
assert "javascript:" not in response.text and "private@example.org" not in response.text
assert "/admin/employees/" not in response.text and "/admin/logout" not in response.text
assert "Backend 0.15.2 · Frontend 0.15.2" in response.text
assert "Backend 0.15.3 · Frontend 0.15.3" in response.text
assert "Вход в админку" in response.text
assert client.get("/admin/directory", follow_redirects=False).status_code in (302, 303, 401, 403)
assert "Сотрудники не найдены" in client.get("/admin/council?q=missing").text
+23 -1
View File
@@ -30,7 +30,7 @@ def test_prepared_reference_preserves_quoted_fields_metadata_and_matching(tmp_pa
data = prepare_reference(source)
assert source.read_bytes() == before
assert data['source_rows'] == 3
assert data['source_sha256'] == hashlib.sha256(before).hexdigest()
assert data['source_sha256'] == hashlib.sha256(before.replace(b'\r\n', b'\n')).hexdigest()
assert data['by_author'] == {'другой автор': [0], 'елкин петр': [0]}
record = data['publications'][0]
assert record['title'] == 'Quoted; title\ncontinuation'
@@ -100,3 +100,25 @@ def test_converter_does_not_replace_output_when_input_is_invalid(tmp_path):
same = subprocess.run([sys.executable, 'scripts/prepare_council_articles.py', str(source), '--output', str(source)],
capture_output=True, text=True)
assert same.returncode != 0 and source.read_text(encoding='utf8') == 'Wrong header\nvalue\n'
def test_preparation_is_identical_for_lf_and_crlf_sources(tmp_path):
lf_dir = tmp_path / 'lf'
crlf_dir = tmp_path / 'crlf'
lf_dir.mkdir()
crlf_dir.mkdir()
lf = lf_dir / 'source.csv'
crlf = crlf_dir / 'source.csv'
write_csv(crlf, [{'Название публикации': 'Multiline\npublication', 'Год': '2024',
'Авторы НИУ ВШЭ': 'Author; Coauthor', 'Квартиль scopus': 'Q1'}])
canonical = crlf.read_bytes().replace(b'\r\n', b'\n')
lf.write_bytes(canonical)
crlf.write_bytes(canonical.replace(b'\n', b'\r\n'))
before = crlf.read_bytes()
from_lf = prepare_reference(lf)
from_crlf = prepare_reference(crlf)
assert from_lf == from_crlf
assert from_crlf['source_sha256'] == hashlib.sha256(canonical).hexdigest()
assert from_crlf['source_hash_eol'] == 'LF'
assert from_crlf['publications'][0]['title'] == 'Multiline\npublication'
assert crlf.read_bytes() == before