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:
@@ -12,8 +12,14 @@ set -euo pipefail
|
||||
# строке '/hui', а dead-route скан падал на router_test.go, который ПЕРЕЧИСЛЯЕТ
|
||||
# удалённые маршруты, чтобы доказать их отсутствие. Помощник существует, чтобы
|
||||
# третьего раза не было.
|
||||
#
|
||||
# Отбрасываются оба вида строк-комментариев: `//` для Go и TypeScript, `#` для
|
||||
# shell. Раньше стриплось только `//`, поэтому сканы по tools/build/*.sh не
|
||||
# работали вовсе — там комментарии начинаются с решётки, и объяснение «почему
|
||||
# этого больше нет» считалось за код. Ни один из языков не использует префикс
|
||||
# другого для чего-то иного, так что общий фильтр безопасен для обоих.
|
||||
code_without_comments() {
|
||||
grep -v '^[[:space:]]*//' "$1" 2>/dev/null || true
|
||||
grep -vE '^[[:space:]]*(//|#)' "$1" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# code_mentions_in отдаёт список файлов, где подстрока встречается В КОДЕ.
|
||||
@@ -551,11 +557,67 @@ run_dependency_hygiene_acceptance() {
|
||||
grep -q '^GOVULNCHECK_VERSION=' versions.env \
|
||||
|| fail "acceptance: версия govulncheck должна быть частью контракта сборки"
|
||||
|
||||
# По готовому tarball должно быть видно, проверялся он или собран с
|
||||
# пропущенной проверкой — ровно как hysteria_compat_gate.
|
||||
# По готовому tarball должно быть видно, что он проверялся — ровно как
|
||||
# hysteria_compat_gate.
|
||||
grep -q '^dependency_security_gate=true$' "$package_dir/metadata/package.env" \
|
||||
|| fail "acceptance: релизный пакет обязан собираться с включённой проверкой зависимостей"
|
||||
|
||||
# У шага не должно быть обходов — ни объявленных, ни забытых.
|
||||
#
|
||||
# Раньше их было два: ALLOW_VULNERABLE_DEPENDENCIES=true писал в metadata
|
||||
# `accepted-risk`, SKIP_SECURITY_SCAN=true — `skipped`. Оба документировались
|
||||
# README и docs/02 как способ выпустить релиз, зная об уязвимости, и оба
|
||||
# гарантированно падали здесь же, строкой выше: приёмка требует буквально
|
||||
# `true`. Сборка проходила весь цикл и умирала на последнем шаге ради
|
||||
# операции, которую продукт в итоге запрещает.
|
||||
#
|
||||
# Противоречие закрыто в пользу строгой политики, и теперь это проверяется, а
|
||||
# не только описано. Ищется КОД: комментарий выше объясняет, почему обходов
|
||||
# нет, и обязан называть их по имени.
|
||||
local bypass_var bypass_hits
|
||||
for bypass_var in ALLOW_VULNERABLE_DEPENDENCIES SKIP_SECURITY_SCAN; do
|
||||
bypass_hits="$(code_mentions_in "$bypass_var" tools/build)"
|
||||
[ -z "$bypass_hits" ] \
|
||||
|| fail "acceptance: обход проверки зависимостей $bypass_var вернулся в: $bypass_hits"
|
||||
done
|
||||
|
||||
# И в документации тоже: описанный, но нерабочий аварийный выход — хуже, чем
|
||||
# его отсутствие. Именно из-за такого описания противоречие и прожило до
|
||||
# приёмки.
|
||||
#
|
||||
# У markdown нет строк-комментариев, поэтому code_without_comments здесь не
|
||||
# применим и правило простое: место для истории — CHANGELOG.md, он под скан не
|
||||
# попадает. README и docs описывают текущую политику, а не отменённую.
|
||||
! grep -rq 'ALLOW_VULNERABLE_DEPENDENCIES\|SKIP_SECURITY_SCAN' README.md docs \
|
||||
|| fail "acceptance: README/docs снова описывают обход проверки зависимостей, которого нет в сборке; место для истории — CHANGELOG.md"
|
||||
|
||||
# Единственное значение поля — true. `accepted-risk`/`skipped` не должны
|
||||
# вернуться даже как строковые литералы: их некому произвести.
|
||||
local dead_gate_value
|
||||
for dead_gate_value in accepted-risk skipped; do
|
||||
! grep -rqF "DEPENDENCY_SECURITY_GATE=\"${dead_gate_value}\"" tools/build \
|
||||
|| fail "acceptance: у dependency_security_gate снова появилось значение ${dead_gate_value}"
|
||||
done
|
||||
|
||||
log_step "Acceptance: versions.env is a developer contract, not only a build contract"
|
||||
# Сборка соблюдала versions.env, а машина разработчика не проверялась никак:
|
||||
# локальный Go 1.25 собирал проект успешно, пока релизный бинарь собирался на
|
||||
# 1.26.7 и наследовал ЕЁ stdlib. Проверялся не тот код, который уезжает в
|
||||
# production.
|
||||
local doctor_script
|
||||
for doctor_script in tools/dev/doctor.sh tools/dev/doctor.ps1; do
|
||||
[ -f "$doctor_script" ] \
|
||||
|| fail "acceptance: проверка среды разработки $doctor_script отсутствует"
|
||||
grep -q 'versions.env' "$doctor_script" \
|
||||
|| fail "acceptance: $doctor_script обязан читать контракт из versions.env, а не носить свои значения"
|
||||
done
|
||||
[ -x tools/dev/doctor.sh ] \
|
||||
|| fail "acceptance: tools/dev/doctor.sh не исполняемый"
|
||||
# Собственных версий у doctor быть не должно: второй список версий неизбежно
|
||||
# разъедется с versions.env.
|
||||
! grep -qE '(GO|NODE|PNPM|BUN)_VERSION[[:space:]]*=[[:space:]]*[0-9]' tools/dev/doctor.sh \
|
||||
|| fail "acceptance: tools/dev/doctor.sh завёл собственные значения версий"
|
||||
|
||||
log_step "Acceptance: the Go toolchain is on a supported line"
|
||||
# Go компилирует hy2xs-admin, поэтому его stdlib уезжает в production-бинарь.
|
||||
# На 1.21 govulncheck находил 17 вызываемых уязвимостей в одной только stdlib.
|
||||
|
||||
+41
-26
@@ -27,24 +27,40 @@ set -euo pipefail
|
||||
# production-зависимостями и порогом PNPM_AUDIT_LEVEL:
|
||||
# devDependencies в артефакт не попадают.
|
||||
|
||||
# Аварийный выход существует по той же причине, что и ALLOW_DIRTY_BUILD: новое
|
||||
# advisory может выйти в неудачный момент, и решение «выпускаем, зная о нём»
|
||||
# должно приниматься человеком ЯВНО, а не пропуском шага по умолчанию.
|
||||
ALLOW_VULNERABLE_DEPENDENCIES="${ALLOW_VULNERABLE_DEPENDENCIES:-false}"
|
||||
|
||||
# Аварийного выхода у этого шага НЕТ, и это осознанное решение.
|
||||
#
|
||||
# Раньше существовали два: ALLOW_VULNERABLE_DEPENDENCIES=true записывал в
|
||||
# metadata `dependency_security_gate=accepted-risk`, SKIP_SECURITY_SCAN=true —
|
||||
# `skipped`. Оба были описаны в README и docs/02 как способ выпустить релиз,
|
||||
# зная об уязвимости.
|
||||
#
|
||||
# Способом они не были. Финальная приёмка архива требует буквально
|
||||
#
|
||||
# grep -q '^dependency_security_gate=true$' metadata/package.env
|
||||
#
|
||||
# то есть сборка с любым из этих значений доходила до самого конца — компиляция,
|
||||
# бандл, тесты, метаданные, tar — и падала на последнем шаге. Продукт
|
||||
# документировал операцию, которую сам же запрещал, а обнаруживалось это через
|
||||
# полный цикл сборки.
|
||||
#
|
||||
# Из двух непротиворечивых вариантов выбран строгий: гейт обязателен, значение
|
||||
# в metadata ровно одно. Контракт при этом читается однозначно:
|
||||
#
|
||||
# релизный артефакт HY2XS невозможно собрать с непройденной проверкой
|
||||
# зависимостей.
|
||||
#
|
||||
# Для локальной работы обходить нечего: `go test ./...`, `govulncheck ./...` и
|
||||
# `pnpm audit` запускаются напрямую и к созданию tarball отношения не имеют.
|
||||
security_gate_failed() {
|
||||
local scanner="$1"
|
||||
local details="$2"
|
||||
|
||||
if [ "$ALLOW_VULNERABLE_DEPENDENCIES" = "true" ]; then
|
||||
log_info "WARNING: $scanner нашёл уязвимости, но сборка продолжена по ALLOW_VULNERABLE_DEPENDENCIES=true"
|
||||
log_info "$details"
|
||||
DEPENDENCY_SECURITY_GATE="accepted-risk"
|
||||
export DEPENDENCY_SECURITY_GATE
|
||||
return 0
|
||||
fi
|
||||
fail "$scanner: найдены уязвимости в зависимостях.
|
||||
|
||||
Обойти этот шаг нельзя: релизный пакет HY2XS собирается только с пройденной
|
||||
проверкой. Обновите граф зависимостей (apps/go.sum, apps/frontend/pnpm-lock.yaml)
|
||||
или версию toolchain в versions.env.
|
||||
|
||||
fail "$scanner: найдены уязвимости в зависимостях. Обновите граф или, осознанно принимая риск, выставьте ALLOW_VULNERABLE_DEPENDENCIES=true.
|
||||
$details"
|
||||
}
|
||||
|
||||
@@ -106,25 +122,24 @@ $report"
|
||||
security_gate_failed "pnpm audit" "$report"
|
||||
}
|
||||
|
||||
# Результат шага уезжает в metadata/package.env: по готовому tarball должно
|
||||
# быть видно, проверялся он или собран с пропущенной проверкой. Ровно так же
|
||||
# устроен hysteria_compat_gate.
|
||||
# Результат шага уезжает в metadata/package.env — так же, как hysteria_compat_gate.
|
||||
#
|
||||
# Значение у поля теперь ровно одно: `true`. Пропущенного состояния не бывает,
|
||||
# потому что не бывает пакета, собранного с пропущенной проверкой; поле остаётся
|
||||
# в metadata как утверждение о готовом артефакте, а не как переключатель.
|
||||
run_dependency_security_gate() {
|
||||
if [ "${SKIP_SECURITY_SCAN:-false}" = "true" ]; then
|
||||
log_info "WARNING: проверка зависимостей на уязвимости пропущена (SKIP_SECURITY_SCAN=true)"
|
||||
DEPENDENCY_SECURITY_GATE="skipped"
|
||||
export DEPENDENCY_SECURITY_GATE
|
||||
return 0
|
||||
fi
|
||||
|
||||
[ -n "${GOVULNCHECK_VERSION:-}" ] \
|
||||
|| fail "security: GOVULNCHECK_VERSION не задан; load_versions_contract должен выполниться первым"
|
||||
[ -n "${PNPM_AUDIT_LEVEL:-}" ] \
|
||||
|| fail "security: PNPM_AUDIT_LEVEL не задан; load_versions_contract должен выполниться первым"
|
||||
|
||||
DEPENDENCY_SECURITY_GATE="true"
|
||||
export DEPENDENCY_SECURITY_GATE
|
||||
|
||||
run_go_vulnerability_gate
|
||||
run_frontend_vulnerability_gate
|
||||
|
||||
# Флаг выставляется ПОСЛЕ обеих проверок, а не до них. Разницы в поведении
|
||||
# сейчас нет — обе ветки отказа завершают сборку, — но «утверждение о
|
||||
# результате», записанное перед получением результата, рано или поздно
|
||||
# переживает свою причину.
|
||||
DEPENDENCY_SECURITY_GATE="true"
|
||||
export DEPENDENCY_SECURITY_GATE
|
||||
}
|
||||
|
||||
@@ -248,7 +248,7 @@ verify_api_namespace_contract() {
|
||||
}
|
||||
|
||||
verify_go_toolchain_contract() {
|
||||
local declared_go_line contract_minor go_minor
|
||||
local declared_go_line contract_minor go_minor declared_toolchain
|
||||
declared_go_line="$(grep -E '^go[[:space:]]+[0-9]' apps/go.mod | head -n1 | awk '{print $2}')"
|
||||
[ -n "$declared_go_line" ] || fail "versions contract: apps/go.mod has no 'go' directive"
|
||||
|
||||
@@ -259,6 +259,16 @@ verify_go_toolchain_contract() {
|
||||
|| fail "versions contract: apps/go.mod major version $declared_go_line is incompatible with GO_VERSION $GO_VERSION"
|
||||
[ "$go_minor" -le "$contract_minor" ] \
|
||||
|| fail "versions contract: apps/go.mod requires go $declared_go_line, but GO_VERSION is $GO_VERSION"
|
||||
|
||||
# Директива `go` задаёт языковой baseline и НЕ выбирает компилятор: с ней
|
||||
# одной локальная сборка на другой минорной линии проходит успешно, и
|
||||
# разработчик проверяет не ту stdlib, которая уедет в production-бинарь.
|
||||
# `toolchain` называет компилятор явно и обязан совпадать с versions.env
|
||||
# точно, а не «быть не выше».
|
||||
declared_toolchain="$(grep -E '^toolchain[[:space:]]+go[0-9]' apps/go.mod | head -n1 | awk '{print $2}')"
|
||||
[ -n "$declared_toolchain" ] \
|
||||
|| fail "versions contract: apps/go.mod has no 'toolchain' directive; add 'toolchain go${GO_VERSION}'"
|
||||
expect_equal "apps/go.mod toolchain" "$declared_toolchain" "go${GO_VERSION}"
|
||||
}
|
||||
|
||||
verify_versions_contract() {
|
||||
|
||||
@@ -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
|
||||
Executable
+127
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Проверка среды разработки против versions.env.
|
||||
#
|
||||
# Зачем.
|
||||
#
|
||||
# versions.env — единый контракт «продукт / платформа / toolchain», и сборка
|
||||
# релиза его соблюдает: она скачивает Go, Node и Bun ровно тех версий, что
|
||||
# записаны здесь, сверяя контрольные суммы. Машина разработчика при этом не
|
||||
# проверялась НИКАК, поэтому расхождение обнаруживалось только на Debian, внутри
|
||||
# release-сборки, и выглядело как «у меня работало».
|
||||
#
|
||||
# Расхождение не гипотетическое: локальный Go 1.25 собирал проект успешно (`go`
|
||||
# в go.mod — языковой baseline, а не выбор компилятора), а релизный бинарь
|
||||
# компилировался на 1.26.7 и наследовал ЕЁ stdlib. То есть проверялся не тот
|
||||
# код, который уезжает в production.
|
||||
#
|
||||
# Скрипт ничего не устанавливает и не меняет: он отвечает на один вопрос —
|
||||
# совпадает ли эта машина с контрактом сборки.
|
||||
#
|
||||
# Использование:
|
||||
# ./tools/dev/doctor.sh проверить и вернуть код выхода
|
||||
# ./tools/dev/doctor.sh --quiet только итог
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
QUIET=false
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--quiet) QUIET=true ;;
|
||||
-h|--help)
|
||||
printf 'usage: %s [--quiet]\n' "$0"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
printf 'unknown argument: %s\n' "$arg" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
[ -f versions.env ] || {
|
||||
printf 'versions.env не найден: doctor запускается из репозитория HY2XS\n' >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
# shellcheck disable=SC1091
|
||||
. ./versions.env
|
||||
|
||||
FAILURES=0
|
||||
|
||||
say() {
|
||||
[ "$QUIET" = true ] || printf '%s\n' "$1"
|
||||
}
|
||||
|
||||
# Версия извлекается из вывода самого инструмента: спрашивать `которая версия
|
||||
# записана в PATH` бессмысленно, отвечать должен тот бинарь, который реально
|
||||
# запустится.
|
||||
tool_version() {
|
||||
local command="$1"
|
||||
shift
|
||||
command -v "$command" >/dev/null 2>&1 || {
|
||||
printf 'not found'
|
||||
return 0
|
||||
}
|
||||
"$command" "$@" 2>/dev/null | head -n1 | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n1
|
||||
}
|
||||
|
||||
check() {
|
||||
local label="$1" required="$2" found="$3" hint="$4"
|
||||
|
||||
if [ -z "$found" ]; then
|
||||
found="unknown"
|
||||
fi
|
||||
|
||||
say ""
|
||||
say "$label:"
|
||||
say " required: $required"
|
||||
say " found: $found"
|
||||
|
||||
if [ "$found" = "$required" ]; then
|
||||
say " OK"
|
||||
return 0
|
||||
fi
|
||||
|
||||
say " FAIL — $hint"
|
||||
FAILURES=$((FAILURES + 1))
|
||||
}
|
||||
|
||||
say "HY2XS development environment"
|
||||
say "contract: versions.env (HY2XS $HY2XS_VERSION, release line $HY2XS_RELEASE_LINE)"
|
||||
|
||||
# Go попадает в production-артефакт целиком: его stdlib компилируется в
|
||||
# hy2xs-admin. Несовпадение здесь — не удобство, а разная поверхность атаки у
|
||||
# разработчика и у релиза.
|
||||
check "Go" "$GO_VERSION" "$(tool_version go version)" \
|
||||
"локальный Go собирает не ту stdlib, что уедет в релиз; поставьте $GO_VERSION"
|
||||
|
||||
# Node живёт только на build-хосте (сборка frontend-ассетов), но собирает код,
|
||||
# который уезжает в production.
|
||||
check "Node" "$NODE_VERSION" "$(tool_version node --version)" \
|
||||
"frontend-ассеты собираются другой версией Node; поставьте $NODE_VERSION"
|
||||
|
||||
check "pnpm" "$PNPM_VERSION" "$(tool_version pnpm --version)" \
|
||||
"разрешение зависимостей frontend может разойтись с lock-файлом; поставьте $PNPM_VERSION"
|
||||
|
||||
# Bun физически входит в скомпилированный оркестратор (`bun build --compile`),
|
||||
# поэтому его версия — часть артефакта, а не инструмента.
|
||||
check "Bun" "$BUN_VERSION" "$(tool_version bun --version)" \
|
||||
"runtime Bun входит в бинарь оркестратора; поставьте $BUN_VERSION"
|
||||
|
||||
# go.mod обязан называть компилятор явно: директива `go` этого не делает.
|
||||
DECLARED_TOOLCHAIN="$(grep -E '^toolchain[[:space:]]+go[0-9]' apps/go.mod | head -n1 | awk '{print $2}' || true)"
|
||||
check "apps/go.mod toolchain" "go$GO_VERSION" "${DECLARED_TOOLCHAIN:-отсутствует}" \
|
||||
"директива toolchain разошлась с versions.env; сборка отвергнет это в verify_go_toolchain_contract"
|
||||
|
||||
say ""
|
||||
if [ "$FAILURES" -eq 0 ]; then
|
||||
say "Итог: среда разработки совпадает с контрактом сборки."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
say "Итог: расхождений — $FAILURES. Локальные проверки идут не по тому toolchain,"
|
||||
say "которым собирается релиз."
|
||||
exit 1
|
||||
Reference in New Issue
Block a user