mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
- .githooks/pre-push + scripts/pre-push-gate.mjs --hook: gate:small runs by default for issue/* branches with an executable diff; C/D-only diffs and non-issue refs are skipped, HP_PREPUSH_GATE=0 turns it off, =1 forces it. A non-HEAD push with an executable diff is rejected (the gate checks the working tree). The gate runs with every GIT_* variable removed: git gives hooks GIT_DIR (and GIT_CONFIG_PARAMETERS with the pusher's -c), and unit tests that `git init` temporary repositories otherwise write into the real one — seen on the first live run. The manual #343 mode is unchanged. - scripts/sandbox-bootstrap.sh: idempotent worktree / deps / chromium (npm @sparticuz/chromium@152.0.0 + Playwright shims) / bundle / check steps, no owner paths or credentials. - scripts/test-chunk.mjs, npm run test:chunk -- N/M: round-robin over test/*.test.mjs sorted by code point. - Tests, seven mutants, docs/DEVELOPMENT.md «Локальный контур за 5 минут», docs/TESTING.md. package.json changed → bundle rebuilt (fingerprint input). Issue: #633 User-Visible: no
192 lines
10 KiB
Bash
Executable File
192 lines
10 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# Локальный контур в Linux-песочнице агента одной командой (#633).
|
||
#
|
||
# bash scripts/sandbox-bootstrap.sh # всё по порядку
|
||
# bash scripts/sandbox-bootstrap.sh <шаг> # worktree | deps | chromium | bundle | check
|
||
# HP_BRANCH=issue/633-local-contour bash scripts/sandbox-bootstrap.sh
|
||
#
|
||
# Идемпотентен: готовый шаг узнаётся и пропускается, поэтому при обрыве (лимит
|
||
# одной команды песочницы ≈ 3 мин, перезапуск песочницы стирает /tmp) скрипт
|
||
# просто запускают ещё раз. Каждый шаг по отдельности укладывается в 3 минуты;
|
||
# `all` на холодной песочнице может не уложиться — тогда повторить, сделанное
|
||
# не переделывается. Скрипт не знает ни путей владельца, ни учётных данных: всё
|
||
# машинно-специфичное приходит переменными окружения.
|
||
#
|
||
# HP_CLONE клон, из которого заводится worktree (по умолчанию —
|
||
# репозиторий, где лежит этот скрипт)
|
||
# HP_WORKTREE куда положить worktree (/tmp/hp-wt)
|
||
# HP_BRANCH ветка worktree; если есть на origin — берётся оттуда,
|
||
# иначе создаётся от HP_REF. Без неё — detached HEAD
|
||
# HP_REF от чего заводить (origin/dev)
|
||
# HP_SHARED_NODE_MODULES готовый node_modules с тем же package-lock.json:
|
||
# ссылка на него вместо npm ci (параллельные сессии)
|
||
# HP_CHROMIUM_DIR куда распаковать Chromium (/tmp/chr)
|
||
# HP_CHROMIUM_PKG npm-пакет с Chromium (@sparticuz/chromium@152.0.0)
|
||
#
|
||
# Почему Chromium из npm, а не `npx playwright install`: CDN Playwright
|
||
# (cdn.playwright.dev) закрыт allowlist'ом песочницы, а реестр npm открыт.
|
||
# Пакет содержит brotli-архив бинаря и его библиотек; Playwright получает шим по
|
||
# своему ожидаемому пути, который запускает этот бинарь. Существующий настоящий
|
||
# браузер по этому пути не трогается — шим узнаётся по маркеру.
|
||
#
|
||
# Golden этим браузером не снимаются (#455, только Linux CI); смоки и
|
||
# `golden:verify` как совет — да.
|
||
set -euo pipefail
|
||
|
||
STEP="${1:-all}"
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
CLONE="${HP_CLONE:-$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel)}"
|
||
WT="${HP_WORKTREE:-/tmp/hp-wt}"
|
||
REF="${HP_REF:-origin/dev}"
|
||
BRANCH="${HP_BRANCH:-}"
|
||
CHR="${HP_CHROMIUM_DIR:-/tmp/chr}"
|
||
CHR_PKG="${HP_CHROMIUM_PKG:-@sparticuz/chromium@152.0.0}"
|
||
SHIM_MARK="# hp-sandbox-bootstrap chromium shim"
|
||
|
||
say() { printf '%s\n' "$*"; }
|
||
die() { printf 'sandbox-bootstrap: %s\n' "$*" >&2; exit 1; }
|
||
|
||
step_worktree() {
|
||
if git -C "$WT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||
say "worktree: $WT уже есть ($(git -C "$WT" rev-parse --abbrev-ref HEAD) @ $(git -C "$WT" rev-parse --short HEAD)) — не трогаю"
|
||
return 0
|
||
fi
|
||
[ -e "$WT" ] && die "$WT существует, но это не рабочее дерево git — уберите его или задайте HP_WORKTREE"
|
||
git -C "$CLONE" fetch -q origin
|
||
if [ -n "$BRANCH" ]; then
|
||
local from="$REF"
|
||
if [ -z "${HP_REF:-}" ] && git -C "$CLONE" rev-parse -q --verify "refs/remotes/origin/$BRANCH" >/dev/null; then
|
||
from="origin/$BRANCH"
|
||
fi
|
||
git -C "$CLONE" worktree add -q -B "$BRANCH" "$WT" "$from"
|
||
say "worktree: $WT на $BRANCH от $from"
|
||
else
|
||
git -C "$CLONE" worktree add -q --detach "$WT" "$REF"
|
||
say "worktree: $WT, detached на $REF"
|
||
fi
|
||
}
|
||
|
||
lock_digest() { sha256sum "$WT/package-lock.json" | cut -d' ' -f1; }
|
||
|
||
step_deps() {
|
||
[ -f "$WT/package-lock.json" ] || die "нет $WT/package-lock.json — сначала шаг worktree"
|
||
local stamp="$WT/node_modules/.hp-bootstrap-lock"
|
||
if [ -n "${HP_SHARED_NODE_MODULES:-}" ]; then
|
||
[ -x "$HP_SHARED_NODE_MODULES/.bin/tsc" ] || die "HP_SHARED_NODE_MODULES=$HP_SHARED_NODE_MODULES без .bin/tsc"
|
||
if [ ! -e "$WT/node_modules" ]; then ln -s "$HP_SHARED_NODE_MODULES" "$WT/node_modules"; fi
|
||
say "deps: node_modules -> $HP_SHARED_NODE_MODULES (общий; package-lock обязан совпадать)"
|
||
elif [ -f "$stamp" ] && [ "$(cat "$stamp")" = "$(lock_digest)" ] && [ -x "$WT/node_modules/.bin/tsc" ]; then
|
||
say "deps: node_modules соответствует package-lock.json — npm ci не нужен"
|
||
else
|
||
# --ignore-scripts: postinstall Playwright полез бы на закрытый CDN, а
|
||
# prepare (установка хуков) выполняется ниже явно и только при нужде.
|
||
(cd "$WT" && npm ci --ignore-scripts --no-audit --no-fund >/tmp/hp-bootstrap-npm-ci.log 2>&1) \
|
||
|| { tail -20 /tmp/hp-bootstrap-npm-ci.log >&2; die "npm ci не удался (лог: /tmp/hp-bootstrap-npm-ci.log)"; }
|
||
lock_digest >"$stamp"
|
||
say "deps: npm ci — ok"
|
||
fi
|
||
if [ "$(git -C "$WT" config --get core.hooksPath || true)" != ".githooks" ]; then
|
||
(cd "$WT" && node scripts/install-hooks.mjs)
|
||
fi
|
||
}
|
||
|
||
# Пути, по которым установленный Playwright ищет свои браузеры.
|
||
playwright_targets() {
|
||
(cd "$WT" && node --input-type=module -e "
|
||
import { readFileSync } from 'node:fs';
|
||
import { createRequire } from 'node:module';
|
||
import { dirname, join } from 'node:path';
|
||
const require = createRequire(join(process.cwd(), 'package.json'));
|
||
const { chromium } = require('playwright');
|
||
const full = chromium.executablePath();
|
||
const root = dirname(dirname(dirname(full)));
|
||
// browsers.json не экспортирован пакетом — читается по пути от его корня.
|
||
const core = dirname(require.resolve('playwright-core'));
|
||
const { browsers } = JSON.parse(readFileSync(join(core, 'browsers.json'), 'utf8'));
|
||
const rev = browsers.find((b) => b.name === 'chromium-headless-shell').revision;
|
||
const shell = join(root, 'chromium_headless_shell-' + rev);
|
||
console.log([full,
|
||
join(shell, 'chrome-linux64', 'chrome-headless-shell'),
|
||
join(shell, 'chrome-headless-shell-linux64', 'chrome-headless-shell')].join('\n'));
|
||
")
|
||
}
|
||
|
||
brotli_out() { node -e "const z=require('zlib'),fs=require('fs');fs.writeFileSync(process.argv[2],z.brotliDecompressSync(fs.readFileSync(process.argv[1])));" "$1" "$2"; }
|
||
|
||
step_chromium() {
|
||
[ -x "$WT/node_modules/.bin/tsc" ] || die "нет node_modules — сначала шаг deps"
|
||
if [ ! -x "$CHR/bin/chromium" ]; then
|
||
mkdir -p "$CHR/bin"
|
||
local tgz
|
||
tgz="$(ls "$CHR"/*.tgz 2>/dev/null | head -1 || true)"
|
||
if [ -z "$tgz" ]; then
|
||
(cd "$CHR" && npm pack --silent "$CHR_PKG" >/dev/null) || die "npm pack $CHR_PKG не удался"
|
||
tgz="$(ls "$CHR"/*.tgz | head -1)"
|
||
fi
|
||
tar xzf "$tgz" -C "$CHR"
|
||
brotli_out "$CHR/package/bin/chromium.br" "$CHR/bin/chromium.tmp"
|
||
for archive in "$CHR"/package/bin/*.tar.br; do
|
||
[ -f "$archive" ] || continue
|
||
brotli_out "$archive" "$CHR/x.tar"
|
||
tar xf "$CHR/x.tar" -C "$CHR/bin" 2>/dev/null || true
|
||
rm -f "$CHR/x.tar"
|
||
done
|
||
chmod +x "$CHR/bin/chromium.tmp"
|
||
mv "$CHR/bin/chromium.tmp" "$CHR/bin/chromium" # последним: наличие = распаковка завершена
|
||
say "chromium: распакован $CHR_PKG в $CHR/bin"
|
||
else
|
||
say "chromium: $CHR/bin/chromium уже есть"
|
||
fi
|
||
local targets target
|
||
targets="$(playwright_targets)" || die "не удалось узнать пути браузеров Playwright"
|
||
[ -n "$targets" ] || die "Playwright не назвал ни одного пути браузера"
|
||
while IFS= read -r target; do
|
||
[ -n "$target" ] || continue
|
||
if [ -e "$target" ] && ! grep -q "$SHIM_MARK" "$target" 2>/dev/null; then
|
||
say "chromium: $target — настоящий браузер, шим не ставлю"
|
||
continue
|
||
fi
|
||
mkdir -p "$(dirname "$target")"
|
||
printf '#!/bin/bash\n%s\nexport LD_LIBRARY_PATH="%s/bin/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"\nexec "%s/bin/chromium" --headless=new "$@"\n' \
|
||
"$SHIM_MARK" "$CHR" "$CHR" >"$target"
|
||
chmod +x "$target"
|
||
done <<<"$targets"
|
||
say "chromium: шимы Playwright на месте"
|
||
}
|
||
|
||
step_bundle() {
|
||
[ -x "$WT/node_modules/.bin/tsc" ] || die "нет node_modules — сначала шаг deps"
|
||
(cd "$WT" && npm run --silent bundle:sync >/tmp/hp-bootstrap-bundle.log 2>&1) \
|
||
|| { tail -30 /tmp/hp-bootstrap-bundle.log >&2; die "npm run bundle:sync не удался (лог: /tmp/hp-bootstrap-bundle.log)"; }
|
||
say "bundle: npm run bundle:sync — ok (dist, custom_components/houseplan/frontend, demo/srv/assets)"
|
||
}
|
||
|
||
step_check() {
|
||
(cd "$WT" && node --input-type=module -e "
|
||
import { createRequire } from 'node:module';
|
||
import { join } from 'node:path';
|
||
const require = createRequire(join(process.cwd(), 'package.json'));
|
||
const { chromium } = require('playwright');
|
||
const browser = await chromium.launch();
|
||
const page = await browser.newPage();
|
||
await page.setContent('<p id=ok>houseplan</p>');
|
||
const text = await page.textContent('#ok');
|
||
const version = browser.version();
|
||
await browser.close();
|
||
if (text !== 'houseplan') { console.error('playwright: страница не отрисовалась'); process.exit(1); }
|
||
console.log('check: playwright открыл страницу, Chromium ' + version);
|
||
") || die "проверка Playwright не прошла — повторите шаг chromium"
|
||
say "готово: cd $WT && node demo/smoke_edge_cases.mjs"
|
||
}
|
||
|
||
case "$STEP" in
|
||
worktree) step_worktree ;;
|
||
deps) step_deps ;;
|
||
chromium) step_chromium ;;
|
||
bundle) step_bundle ;;
|
||
check) step_check ;;
|
||
all) step_worktree; step_deps; step_chromium; step_bundle; step_check ;;
|
||
-h|--help|help) sed -n '2,33p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' ;;
|
||
*) die "неизвестный шаг «$STEP»: worktree | deps | chromium | bundle | check | all" ;;
|
||
esac
|