feat: закрыть остаточный backlog
- keyset-пагинация аудита с курсором (occurred_at, id) и UI «Показать ещё» - пакетный /api/v1/labs/details и переход progress на один запрос - абсолютный потолок сессии 30 дней (identity V12 + web), очистка refresh - админская инвентаризация legacy-активов (/admin/legacy-assets, скрипт) - E2E в Chromium/Firefox/WebKit (18/18), CI preflight - LDD: 51 claim, 346 тестов, docs и handoff обновлены
This commit is contained in:
111
scripts/ci-preflight.sh
Executable file
111
scripts/ci-preflight.sh
Executable file
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env bash
|
||||
# Проверка окружения shell-runner перед smoke/release. Ничего не устанавливает.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
release=0
|
||||
case "${1:-}" in
|
||||
"") ;;
|
||||
--release) release=1 ;;
|
||||
-h | --help)
|
||||
echo "Usage: $0 [--release]"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 [--release]" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
failures=()
|
||||
|
||||
ok() { printf 'OK %s\n' "$*"; }
|
||||
|
||||
fail() {
|
||||
printf 'FAIL %s\n' "$*"
|
||||
failures+=("$*")
|
||||
}
|
||||
|
||||
if [[ -n "${BASH_VERSION:-}" ]]; then
|
||||
ok "bash ${BASH_VERSION}"
|
||||
else
|
||||
fail "bash: не найден"
|
||||
fi
|
||||
|
||||
if command -v java >/dev/null 2>&1; then
|
||||
java_line=$(java -version 2>&1 | head -n 1 || true)
|
||||
if [[ "$java_line" =~ version\ \"21([.\"-]|$) ]]; then
|
||||
ok "java 21: ${java_line:-java}"
|
||||
else
|
||||
fail "java: нужна версия 21, найдено: ${java_line:-неизвестно}"
|
||||
fi
|
||||
else
|
||||
fail "java: не найден в PATH"
|
||||
fi
|
||||
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
docker_line=$(docker --version 2>/dev/null | head -n 1 || true)
|
||||
ok "docker: ${docker_line:-команда доступна}"
|
||||
else
|
||||
fail "docker: не найден в PATH"
|
||||
fi
|
||||
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
compose_short=$(docker compose version --short 2>/dev/null | head -n 1 || true)
|
||||
if [[ -z "$compose_short" ]]; then
|
||||
compose_short=$(docker compose version 2>/dev/null | head -n 1 || true)
|
||||
compose_short=${compose_short#Docker Compose version }
|
||||
compose_short=${compose_short#v}
|
||||
fi
|
||||
compose_short=${compose_short#v}
|
||||
compose_major=${compose_short%%.*}
|
||||
if [[ "$compose_major" =~ ^[0-9]+$ ]] && ((compose_major >= 2)); then
|
||||
ok "docker compose v2+: ${compose_short:-неизвестно}"
|
||||
else
|
||||
fail "docker compose: нужен v2+, найдено: ${compose_short:-версия не определена}"
|
||||
fi
|
||||
else
|
||||
fail "docker compose: docker не найден"
|
||||
fi
|
||||
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
ok "python3: $(python3 --version 2>&1 | head -n 1)"
|
||||
else
|
||||
fail "python3: не найден в PATH"
|
||||
fi
|
||||
|
||||
if ((release)); then
|
||||
if command -v node >/dev/null 2>&1; then
|
||||
ok "node: $(node --version 2>&1 | head -n 1)"
|
||||
else
|
||||
fail "node: не найден в PATH"
|
||||
fi
|
||||
|
||||
if command -v npx >/dev/null 2>&1; then
|
||||
ok "npx: $(npx --version 2>&1 | head -n 1)"
|
||||
else
|
||||
fail "npx: не найден в PATH"
|
||||
fi
|
||||
|
||||
if [[ -d e2e/node_modules ]]; then
|
||||
ok "e2e/node_modules: найден"
|
||||
playwright_line=$(cd e2e && npx playwright --version 2>/dev/null | head -n 1 || true)
|
||||
if [[ -n "$playwright_line" ]]; then
|
||||
ok "playwright: $playwright_line"
|
||||
else
|
||||
fail "npx playwright --version: не удалось получить версию"
|
||||
fi
|
||||
else
|
||||
fail "e2e/node_modules: не найден (выполните: cd e2e && npm ci)"
|
||||
fail "npx playwright --version: пропущено без e2e/node_modules"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo
|
||||
if ((${#failures[@]} > 0)); then
|
||||
echo "Preflight не пройден, ошибок: ${#failures[@]}."
|
||||
printf ' - %s\n' "${failures[@]}"
|
||||
exit 1
|
||||
fi
|
||||
echo "Preflight пройден."
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# LDD browser E2E: полный стек + Playwright (Chromium) на одноразовой БД.
|
||||
# LDD browser E2E: полный стек + Playwright (Chromium/Firefox/WebKit) на одноразовой БД.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
@@ -11,6 +11,47 @@ E2E_STATUS=1
|
||||
|
||||
log() { echo ">> $*"; }
|
||||
|
||||
playwright_version() {
|
||||
(cd e2e && node -p "require('@playwright/test/package.json').version" 2>/dev/null || true)
|
||||
}
|
||||
|
||||
host_playwright_usable() {
|
||||
# WebKit на Fedora часто не запускается из-за Ubuntu-специфичных libicu/libjpeg;
|
||||
# проверяем все три браузера, чтобы fallback сработал и при нехватке lib для Firefox.
|
||||
(cd e2e && node -e '
|
||||
const { chromium, firefox, webkit } = require("playwright");
|
||||
(async () => {
|
||||
for (const type of [chromium, firefox, webkit]) {
|
||||
const browser = await type.launch();
|
||||
await browser.close();
|
||||
}
|
||||
})().catch(() => process.exit(1));
|
||||
') >/dev/null 2>&1
|
||||
}
|
||||
|
||||
ensure_playwright_browsers() {
|
||||
local browser detected missing=()
|
||||
# Проверяем точные пути браузеров текущей версии Playwright, а не кэш целиком.
|
||||
if ! detected=$(cd e2e && node -e '
|
||||
const fs = require("fs");
|
||||
const { chromium, firefox, webkit } = require("playwright");
|
||||
for (const [name, type] of [["chromium", chromium], ["firefox", firefox], ["webkit", webkit]]) {
|
||||
if (!fs.existsSync(type.executablePath())) console.log(name);
|
||||
}
|
||||
' 2>/dev/null); then
|
||||
detected=$'chromium\nfirefox\nwebkit'
|
||||
fi
|
||||
while IFS= read -r browser; do
|
||||
[[ -n "$browser" ]] && missing+=("$browser")
|
||||
done <<<"$detected"
|
||||
if ((${#missing[@]} == 0)); then
|
||||
log "Браузеры Playwright уже установлены (Chromium/Firefox/WebKit)."
|
||||
return
|
||||
fi
|
||||
log "Устанавливаю браузеры Playwright: ${missing[*]}; системные зависимости ставит CI."
|
||||
(cd e2e && npx playwright install --no-progress "${missing[@]}")
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
local code=$?
|
||||
if ((STACK_STARTED)); then
|
||||
@@ -68,15 +109,34 @@ STACK_STARTED=1
|
||||
./run.sh start
|
||||
|
||||
mkdir -p build/evidence
|
||||
log "Запускаю Playwright..."
|
||||
ensure_playwright_browsers
|
||||
set +e
|
||||
(
|
||||
cd e2e
|
||||
LDD_WEB_URL="http://localhost:8090" \
|
||||
LDD_API_URL="http://localhost:8080" \
|
||||
npx playwright test --config=playwright.config.ts
|
||||
)
|
||||
E2E_STATUS=$?
|
||||
if host_playwright_usable; then
|
||||
log "Запускаю Playwright на хосте (Chromium/Firefox/WebKit)..."
|
||||
(
|
||||
cd e2e
|
||||
LDD_WEB_URL="http://localhost:8090" \
|
||||
LDD_API_URL="http://localhost:8080" \
|
||||
npx playwright test --config=playwright.config.ts
|
||||
)
|
||||
E2E_STATUS=$?
|
||||
else
|
||||
version=$(playwright_version)
|
||||
if [[ -z "$version" ]]; then
|
||||
echo "WebKit не запускается на хосте, а версию Playwright определить не удалось." >&2
|
||||
E2E_STATUS=1
|
||||
else
|
||||
log "WebKit не запускается на хосте; запускаю Chromium/Firefox/WebKit в Docker-образе Playwright ${version}."
|
||||
docker run --rm --network host \
|
||||
-v "$PWD:/work" \
|
||||
-w /work/e2e \
|
||||
-e LDD_WEB_URL="http://localhost:8090" \
|
||||
-e LDD_API_URL="http://localhost:8080" \
|
||||
"mcr.microsoft.com/playwright:v${version}-noble" \
|
||||
npx playwright test --config=playwright.config.ts
|
||||
E2E_STATUS=$?
|
||||
fi
|
||||
fi
|
||||
set -e
|
||||
|
||||
python3 - "$E2E_STATUS" <<'PY'
|
||||
|
||||
@@ -103,6 +103,8 @@ log "Применяю миграции ветки..."
|
||||
cat services/identity-service/src/main/resources/db/migration/V9__account_statuses.sql
|
||||
cat services/identity-service/src/main/resources/db/migration/V10__audit_events.sql
|
||||
cat services/identity-service/src/main/resources/db/migration/V11__refresh_tokens.sql
|
||||
cat services/identity-service/src/main/resources/db/migration/V12__refresh_absolute.sql
|
||||
cat services/identity-service/src/main/resources/db/migration/V13__audit_keyset_indexes.sql
|
||||
echo 'SET search_path TO academic;'
|
||||
cat services/academic-service/src/main/resources/db/migration/V6__courses.sql
|
||||
cat services/academic-service/src/main/resources/db/migration/V7__labs_course.sql
|
||||
@@ -122,6 +124,10 @@ SELECT 'audit_events=' || count(*) FROM information_schema.tables
|
||||
WHERE table_schema='identity' AND table_name='audit_events';
|
||||
SELECT 'refresh_tokens=' || count(*) FROM information_schema.tables
|
||||
WHERE table_schema='identity' AND table_name='refresh_tokens';
|
||||
SELECT 'refresh_absolute=' || count(*) FROM information_schema.columns
|
||||
WHERE table_schema='identity' AND table_name='refresh_tokens' AND column_name='absolute_expires_at';
|
||||
SELECT 'audit_keyset_idx=' || count(*) FROM pg_indexes
|
||||
WHERE schemaname='identity' AND indexname='audit_events_occurred_id_idx';
|
||||
SELECT 'students=' || count(*) FROM students;
|
||||
SELECT 'groups=' || count(*) FROM groups;
|
||||
SELECT 'history=' || count(*) FROM membership_history;
|
||||
@@ -163,6 +169,8 @@ out, result, v13 = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
required = {
|
||||
"audit_events=1",
|
||||
"refresh_tokens=1",
|
||||
"refresh_absolute=1",
|
||||
"audit_keyset_idx=1",
|
||||
"students=1",
|
||||
"groups=1",
|
||||
"history=1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# Полный LDD release gate: тесты, линтеры, upgrade, mutation, runtime и browser E2E.
|
||||
# Требует Docker, Java 21, Node.js/npm и установленный Chromium Playwright.
|
||||
# Требует Docker, Java 21, Node.js/npm и Chromium/Firefox/WebKit Playwright.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
81
scripts/legacy-assets.sh
Executable file
81
scripts/legacy-assets.sh
Executable file
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bash
|
||||
# Инвентаризация legacy-активов (файлов без связи со статьёй) на работающем стенде.
|
||||
# Пример:
|
||||
# LMS_BASE_URL=https://lms.example.ru LMS_ADMIN_LOGIN=admin LMS_ADMIN_PASSWORD=... \
|
||||
# ./scripts/legacy-assets.sh
|
||||
set -euo pipefail
|
||||
|
||||
base_url="${LMS_BASE_URL:-http://localhost:8080}"
|
||||
admin_login="${LMS_ADMIN_LOGIN:-}"
|
||||
admin_password="${LMS_ADMIN_PASSWORD:-}"
|
||||
|
||||
if [[ -z "$admin_login" || -z "$admin_password" ]]; then
|
||||
echo "Задайте LMS_ADMIN_LOGIN и LMS_ADMIN_PASSWORD." >&2
|
||||
echo "Пример: LMS_BASE_URL=https://lms.example.ru LMS_ADMIN_LOGIN=admin LMS_ADMIN_PASSWORD=... $0" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
LMS_BASE_URL="${base_url%/}" \
|
||||
LMS_ADMIN_LOGIN="$admin_login" \
|
||||
LMS_ADMIN_PASSWORD="$admin_password" \
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
base_url = os.environ["LMS_BASE_URL"]
|
||||
admin_login = os.environ["LMS_ADMIN_LOGIN"]
|
||||
admin_password = os.environ["LMS_ADMIN_PASSWORD"]
|
||||
|
||||
|
||||
def request(method, path, payload=None, token=None):
|
||||
body = None if payload is None else json.dumps(payload).encode("utf-8")
|
||||
headers = {"Accept": "application/json"}
|
||||
if body is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
if token:
|
||||
headers["Authorization"] = "Bearer " + token
|
||||
req = urllib.request.Request(base_url + path, data=body, headers=headers, method=method)
|
||||
try:
|
||||
return urllib.request.urlopen(req, timeout=30)
|
||||
except urllib.error.HTTPError as exc:
|
||||
detail = exc.read().decode("utf-8", "replace").strip()
|
||||
message = f"{method} {path}: HTTP {exc.code}"
|
||||
if detail:
|
||||
message += f": {detail}"
|
||||
print(message, file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
except urllib.error.URLError as exc:
|
||||
print(f"Не удалось подключиться к {base_url}: {exc.reason}", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
with request("POST", "/api/v1/auth/login", {"login": admin_login, "password": admin_password}) as response:
|
||||
login_body = response.read().decode("utf-8")
|
||||
|
||||
try:
|
||||
token = json.loads(login_body)["token"]
|
||||
except (ValueError, KeyError, TypeError):
|
||||
print("Ответ /api/v1/auth/login не содержит token.", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
|
||||
with request("GET", "/api/v1/content/legacy-assets", token=token) as response:
|
||||
try:
|
||||
assets = json.load(response)
|
||||
except ValueError:
|
||||
print("Ответ /api/v1/content/legacy-assets не является JSON.", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
|
||||
if not isinstance(assets, list):
|
||||
print("Неожиданный формат /api/v1/content/legacy-assets: ожидался список.", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
|
||||
for asset in assets:
|
||||
if not isinstance(asset, str):
|
||||
print(f"Пропущен нестроковый элемент: {asset!r}", file=sys.stderr)
|
||||
continue
|
||||
print(asset)
|
||||
print(f"Всего legacy-активов: {len(assets)}", file=sys.stderr)
|
||||
PY
|
||||
Reference in New Issue
Block a user