fix(v1): сделать отзыв доступа, бэкап и диагностику соответствующими своим именам
Проход по операциям, которые делают не то, что обещает их имя. P0. Удаление bootstrap-admin-peer не было отзывом доступа. Признаком «создавать пир или нет» служило наличие строки в таблице, а HY2XS_ADMIN_CON_PASS продолжает жить в /etc/hy2xs/hy2xs.env — его читает systemd-юнит. Оператор удалял пира, доступ исчезал, и ближайший restart возвращал того же пира с тем же секретом. Молча. Признаком стала отметка BOOTSTRAP_PEER_SEEDED в таблице config: «создавался когда-либо», а не «существует сейчас». Отметка и пир пишутся одной транзакцией. P1. Резервная копия с includeSecrets=true проглатывала и ошибку расшифровки, и отсутствие шифртекста, отдавая пира с пустым secret и успешный ответ. Теперь недоступный секрет любого пира отклоняет весь запрос с указанием имени. P1. DecryptPeerSecret возвращала содержимое колонки как расшифрованный секрет, если оно не начиналось с v1: — остаток поколения с открытыми секретами. P1. doctor перезапускал hysteria-server и hy2xs-admin: диагностика подозрения на проблему обрывала все живые соединения. P1. Админка сама генерировала HYSTERIA2_TRAFFIC_STATS_SECRET, записать который в /etc/hysteria/config.yaml она не может. Сервис объявлял себя здоровым, а machine auth переставал совпадать. P1. Обходы проверки зависимостей (accepted-risk/skipped) не могли произвести артефакт: приёмка требует dependency_security_gate=true. Удалены из сборки и документации, отсутствие проверяется приёмкой. P2. UPDATE по отсутствующей строке config считался успехом, и cron перепланировался при несохранённом значении. Решение по RowsAffected. P2. Слой данных не отличал «записи нет» от «база не ответила»: sentinel-значения ErrPeerNotFound / ErrAdminUserNotFound / ErrConfigNotFound / ErrStorage. P2. Удалены алиасы /:id/client-url и /:id/qr. Контракт разработки: apps/go.mod объявляет toolchain go1.26.7 (директива go — языковой baseline, а не выбор компилятора), tools/dev/doctor.sh|.ps1 сверяют среду с versions.env.
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
#Requires -Version 7.0
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Проверка среды разработки против versions.env (Windows).
|
||||
|
||||
.DESCRIPTION
|
||||
Точный аналог tools/dev/doctor.sh: сборка релиза скачивает Go, Node и Bun
|
||||
ровно тех версий, что записаны в versions.env, а машина разработчика не
|
||||
проверялась никак — расхождение обнаруживалось только на Debian, внутри
|
||||
release-сборки.
|
||||
|
||||
Расхождение не гипотетическое: локальный Go 1.25 собирал проект успешно
|
||||
(директива `go` в go.mod — языковой baseline, а не выбор компилятора), а
|
||||
релизный бинарь компилировался на 1.26.7 и наследовал ЕЁ stdlib. То есть
|
||||
проверялся не тот код, который уезжает в production.
|
||||
|
||||
Скрипт ничего не устанавливает и не меняет.
|
||||
|
||||
.PARAMETER Quiet
|
||||
Печатать только итог.
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[switch]$Quiet
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$rootDir = Resolve-Path (Join-Path $PSScriptRoot '..' '..')
|
||||
Set-Location $rootDir
|
||||
|
||||
$versionsPath = Join-Path $rootDir 'versions.env'
|
||||
if (-not (Test-Path $versionsPath)) {
|
||||
Write-Error 'versions.env не найден: doctor запускается из репозитория HY2XS'
|
||||
}
|
||||
|
||||
# versions.env — POSIX shell, но по построению содержит только присваивания
|
||||
# KEY=value и комментарии: это его объявленный контракт (см. шапку файла).
|
||||
$contract = @{}
|
||||
foreach ($line in Get-Content $versionsPath) {
|
||||
$trimmed = $line.Trim()
|
||||
if (-not $trimmed -or $trimmed.StartsWith('#')) { continue }
|
||||
$separator = $trimmed.IndexOf('=')
|
||||
if ($separator -lt 1) { continue }
|
||||
$contract[$trimmed.Substring(0, $separator).Trim()] = $trimmed.Substring($separator + 1).Trim()
|
||||
}
|
||||
|
||||
$failures = 0
|
||||
|
||||
function Say([string]$Text) {
|
||||
if (-not $Quiet) { Write-Host $Text }
|
||||
}
|
||||
|
||||
# Версия спрашивается у самого бинаря: что записано в PATH — не тот вопрос,
|
||||
# отвечать должен тот инструмент, который реально запустится.
|
||||
function Get-ToolVersion {
|
||||
param([string]$Command, [string[]]$Arguments)
|
||||
|
||||
$resolved = Get-Command $Command -ErrorAction SilentlyContinue
|
||||
if (-not $resolved) { return 'not found' }
|
||||
|
||||
try {
|
||||
$output = & $resolved.Source @Arguments 2>$null | Select-Object -First 1
|
||||
} catch {
|
||||
return 'unknown'
|
||||
}
|
||||
$match = [regex]::Match([string]$output, '\d+\.\d+\.\d+')
|
||||
if (-not $match.Success) { return 'unknown' }
|
||||
return $match.Value
|
||||
}
|
||||
|
||||
function Test-Contract {
|
||||
param([string]$Label, [string]$Required, [string]$Found, [string]$Hint)
|
||||
|
||||
if (-not $Found) { $Found = 'unknown' }
|
||||
|
||||
Say ''
|
||||
Say "${Label}:"
|
||||
Say " required: $Required"
|
||||
Say " found: $Found"
|
||||
|
||||
if ($Found -eq $Required) {
|
||||
Say ' OK'
|
||||
return
|
||||
}
|
||||
|
||||
Say " FAIL — $Hint"
|
||||
$script:failures++
|
||||
}
|
||||
|
||||
Say 'HY2XS development environment'
|
||||
Say "contract: versions.env (HY2XS $($contract.HY2XS_VERSION), release line $($contract.HY2XS_RELEASE_LINE))"
|
||||
|
||||
# Go попадает в production-артефакт целиком: его stdlib компилируется в
|
||||
# hy2xs-admin. Несовпадение здесь — не удобство, а разная поверхность атаки у
|
||||
# разработчика и у релиза.
|
||||
Test-Contract 'Go' $contract.GO_VERSION (Get-ToolVersion 'go' @('version')) `
|
||||
"локальный Go собирает не ту stdlib, что уедет в релиз; поставьте $($contract.GO_VERSION)"
|
||||
|
||||
# Node живёт только на build-хосте, но собирает код, который уезжает в production.
|
||||
Test-Contract 'Node' $contract.NODE_VERSION (Get-ToolVersion 'node' @('--version')) `
|
||||
"frontend-ассеты собираются другой версией Node; поставьте $($contract.NODE_VERSION)"
|
||||
|
||||
Test-Contract 'pnpm' $contract.PNPM_VERSION (Get-ToolVersion 'pnpm' @('--version')) `
|
||||
"разрешение зависимостей frontend может разойтись с lock-файлом; поставьте $($contract.PNPM_VERSION)"
|
||||
|
||||
# Bun физически входит в скомпилированный оркестратор (`bun build --compile`),
|
||||
# поэтому его версия — часть артефакта, а не инструмента.
|
||||
Test-Contract 'Bun' $contract.BUN_VERSION (Get-ToolVersion 'bun' @('--version')) `
|
||||
"runtime Bun входит в бинарь оркестратора; поставьте $($contract.BUN_VERSION)"
|
||||
|
||||
# go.mod обязан называть компилятор явно: директива `go` этого не делает.
|
||||
$declaredToolchain = (Select-String -Path (Join-Path $rootDir 'apps' 'go.mod') -Pattern '^toolchain\s+(go[0-9][^\s]*)' |
|
||||
Select-Object -First 1).Matches.Groups[1].Value
|
||||
if (-not $declaredToolchain) { $declaredToolchain = 'отсутствует' }
|
||||
|
||||
Test-Contract 'apps/go.mod toolchain' "go$($contract.GO_VERSION)" $declaredToolchain `
|
||||
'директива toolchain разошлась с versions.env; сборка отвергнет это в verify_go_toolchain_contract'
|
||||
|
||||
Say ''
|
||||
if ($failures -eq 0) {
|
||||
Say 'Итог: среда разработки совпадает с контрактом сборки.'
|
||||
exit 0
|
||||
}
|
||||
|
||||
Say "Итог: расхождений — $failures. Локальные проверки идут не по тому toolchain,"
|
||||
Say 'которым собирается релиз.'
|
||||
exit 1
|
||||
Reference in New Issue
Block a user