admin/scripts/pre-push-checks.sh
alisaza f9a9afc697 feat: add layout components for various admin dashboard pages
- Introduced layout components for multiple admin dashboard sections including events, audit logs, bank accounts, blog articles, bookings, chat oversight, cities, contact messages, dashboard, discount codes, event categories, guest lists, identity verifications, manage events, manual notifications, notification rules, payments, provinces, push deliveries, reviews, settlements, sms messages, support tickets, user reports, and users.
- Updated metadata for the auth layout to include the admin page title.
- Modified branch protection documentation to clarify deployment notifications.
2026-09-05 14:29:43 +03:30

304 lines
9.0 KiB
Bash
Executable File

#!/usr/bin/env bash
# Mirrors .github/workflows/frontend-quality.yml (verify job).
# Intentionally not mirrored locally: OSV scan, gitleaks (PR-only on GitHub).
#
# Resume: after a failed push, re-push skips steps whose inputs are unchanged
# (checkpoint in .git/ghabilee-prepush-checkpoint). Force full rerun:
# PREPUSH_NO_CACHE=1 git push
set -euo pipefail
ROOT="$(git rev-parse --show-toplevel)"
cd "$ROOT"
ZERO="0000000000000000000000000000000000000000"
CHECKPOINT_FILE="$ROOT/.git/ghabilee-prepush-checkpoint"
# Same idea as CI path filters — docs/ops-only pushes skip the heavy suite.
FRONTEND_PATH_RE='^(app/|components/|context/|features/|hooks/|services/|lib/|helpers/|validation/|types/|queries/|texts/|constants/|config/|api/|e2e/|public/|styles/|messages/|middleware\.|next\.config|orval\.config|vitest|playwright|openapi\.json$|package\.json$|pnpm-lock\.yaml$|tsconfig|eslint\.config|postcss\.config|tailwind\.config|knip\.json$|\.github/workflows/frontend-quality\.yml$|scripts/)'
PUSH_LOCAL_SHA=""
PUSH_REMOTE_SHA=""
CHECKPOINT_REMOTE_SHA=""
if [[ "${SKIP_PREPUSH_CHECKS:-}" == "1" ]]; then
echo "⚠ SKIP_PREPUSH_CHECKS=1 — skipping pre-push checks"
exit 0
fi
run_step() {
local title="$1"
shift
echo ""
echo "==> $title"
local start
start="$(date +%s)"
"$@"
echo " ($(( $(date +%s) - start ))s)"
}
hash_paths() {
local files=""
local spec
for spec in "$@"; do
local chunk
chunk="$(git ls-files -co --exclude-standard -- "$spec" 2>/dev/null || true)"
[[ -n "$chunk" ]] && files+="${chunk}"$'\n'
done
files="$(printf '%s' "$files" | LC_ALL=C sort -u | sed '/^$/d')"
if [[ -z "$files" ]]; then
printf '%s' "empty"
return 0
fi
printf '%s\0' "$files" | LC_ALL=C sort -z | xargs -0 git hash-object 2>/dev/null | shasum -a 256 | awk '{print $1}'
}
checkpoint_clear() {
rm -f "$CHECKPOINT_FILE"
}
checkpoint_get_remote_sha() {
[[ -f "$CHECKPOINT_FILE" ]] || return 0
grep '^remote_sha=' "$CHECKPOINT_FILE" | head -1 | cut -d= -f2-
}
checkpoint_get_step_hash() {
local step_id="$1"
[[ -f "$CHECKPOINT_FILE" ]] || return 0
grep "^step:${step_id}=" "$CHECKPOINT_FILE" | head -1 | cut -d= -f2- || true
}
checkpoint_mark_step() {
local step_id="$1"
local step_hash="$2"
local remote_line="remote_sha=${PUSH_REMOTE_SHA}"
local tmp
tmp="$(mktemp)"
if [[ -f "$CHECKPOINT_FILE" ]]; then
grep -v "^step:${step_id}=" "$CHECKPOINT_FILE" | grep -v '^remote_sha=' >"$tmp" || true
fi
{
echo "$remote_line"
if [[ -s "$tmp" ]]; then
cat "$tmp"
fi
echo "step:${step_id}=${step_hash}"
} >"$CHECKPOINT_FILE"
rm -f "$tmp"
}
checkpoint_init_run() {
CHECKPOINT_REMOTE_SHA="$(checkpoint_get_remote_sha)"
if [[ "$CHECKPOINT_REMOTE_SHA" != "$PUSH_REMOTE_SHA" ]]; then
checkpoint_clear
echo "remote_sha=${PUSH_REMOTE_SHA}" >"$CHECKPOINT_FILE"
elif [[ ! -f "$CHECKPOINT_FILE" ]]; then
echo "remote_sha=${PUSH_REMOTE_SHA}" >"$CHECKPOINT_FILE"
fi
}
checkpoint_step_hash_matches() {
local step_id="$1"
local current_hash="$2"
[[ "${PREPUSH_NO_CACHE:-}" == "1" ]] && return 1
[[ "$(checkpoint_get_remote_sha)" == "$PUSH_REMOTE_SHA" ]] || return 1
[[ "$(checkpoint_get_step_hash "$step_id")" == "$current_hash" ]]
}
run_cached_step() {
local step_id="$1"
local title="$2"
local input_hash="$3"
shift 3
if checkpoint_step_hash_matches "$step_id" "$input_hash"; then
echo ""
echo "==> $title (skipped — inputs unchanged since last run)"
return 0
fi
run_step "$title" "$@"
checkpoint_mark_step "$step_id" "$input_hash"
}
frontend_orval_drift() {
pnpm exec orval --config orval.config.ts
git diff --exit-code -- api/generated
}
frontend_build() {
NEXT_PUBLIC_API_URL="${NEXT_PUBLIC_API_URL:-http://127.0.0.1:3000}" \
pnpm exec next build
pnpm bundle:check
}
run_frontend_quality() {
checkpoint_init_run
echo ""
echo "── Frontend quality (frontend-quality.yml) ──"
if [[ "${PREPUSH_NO_CACHE:-}" != "1" && -s "$CHECKPOINT_FILE" ]] && grep -q '^step:' "$CHECKPOINT_FILE" 2>/dev/null; then
echo " Resume enabled — unchanged steps are skipped (PREPUSH_NO_CACHE=1 to rerun all)."
fi
run_cached_step generate_api "Generate API client from committed OpenAPI" \
"$(hash_paths openapi.json orval.config.ts scripts/generate-api.mjs)" \
pnpm generate:api
# Always run full typecheck (no checkpoint skip) — same surface as CI `pnpm typecheck`.
run_step "TypeScript" pnpm typecheck
run_cached_step lint "ESLint" \
"$(hash_paths '*.ts' '*.tsx' '*.js' '*.jsx' eslint.config.mjs)" \
pnpm lint
run_cached_step build "Production build and bundle budget" \
"$(hash_paths app components context features hooks services lib helpers validation types queries texts constants content config api public styles messages scripts '*.ts' '*.tsx' next.config.js postcss.config.mjs tailwind.config.ts package.json pnpm-lock.yaml)" \
frontend_build
run_cached_step orval_drift "Verify generated API client" \
"$(hash_paths openapi.json orval.config.ts api scripts/generate-api.mjs)" \
frontend_orval_drift
run_cached_step test "Unit tests" \
"$(hash_paths '*.ts' '*.tsx' vitest.config.ts vitest.consumer.config.ts package.json pnpm-lock.yaml)" \
pnpm test
run_cached_step architecture "Architecture (no circular dependencies)" \
"$(hash_paths app components context hooks services lib config '*.ts' '*.tsx')" \
pnpm architecture:check
run_cached_step pwa "PWA policy" \
"$(hash_paths scripts/check-service-worker-policy.mjs public package.json '*.ts' '*.tsx')" \
pnpm pwa:check
run_cached_step knip "Unused code and dependencies" \
"$(hash_paths knip.json package.json pnpm-lock.yaml '*.ts' '*.tsx' '*.js' '*.jsx' '*.mjs')" \
pnpm unused:check
checkpoint_clear
}
echo ""
echo "Pre-push CI guard — mirrors frontend-quality.yml on matching paths."
echo "Pre-commit already auto-fixes staged files (ESLint --fix + Prettier)."
echo "Skip once: HUSKY=0 git push or SKIP_PREPUSH_CHECKS=1 git push"
run_frontend=0
changed_files=""
compared=0
version_checked=0
compare_and_collect() {
local local_sha="$1"
local remote_sha="$2"
[[ "$local_sha" == "$ZERO" ]] && return 0
if [[ "$remote_sha" == "$ZERO" ]]; then
if git rev-parse --verify --quiet origin/main >/dev/null; then
remote_sha="$(git rev-parse origin/main)"
elif git rev-parse --verify --quiet main >/dev/null; then
remote_sha="$(git rev-parse main)"
else
return 0
fi
fi
[[ "$local_sha" == "$remote_sha" ]] && return 0
PUSH_LOCAL_SHA="$local_sha"
PUSH_REMOTE_SHA="$remote_sha"
compared=1
changed_files+="$(git diff --name-only "$remote_sha" "$local_sha")"$'\n'
}
check_version_bump() {
local local_sha="$1"
local remote_sha="$2"
[[ "$local_sha" == "$ZERO" || "$local_sha" == "$remote_sha" ]] && return 0
if [[ "$remote_sha" == "$ZERO" ]]; then
if git rev-parse --verify --quiet origin/main >/dev/null; then
remote_sha="$(git rev-parse origin/main)"
elif git rev-parse --verify --quiet main >/dev/null; then
remote_sha="$(git rev-parse main)"
else
return 0
fi
fi
[[ "$local_sha" == "$remote_sha" ]] && return 0
node "$ROOT/scripts/check-version-bump.mjs" "$remote_sha" "$local_sha"
version_checked=1
}
if [[ -t 0 ]]; then
echo " (no git refs on stdin — comparing HEAD to origin/main, running full suite)"
PUSH_LOCAL_SHA="$(git rev-parse HEAD)"
if git rev-parse --verify --quiet origin/main >/dev/null; then
PUSH_REMOTE_SHA="$(git rev-parse origin/main)"
else
PUSH_REMOTE_SHA="$ZERO"
fi
compare_and_collect "$PUSH_LOCAL_SHA" "$PUSH_REMOTE_SHA"
compared=0
else
push_refs="$(mktemp)"
trap 'rm -f "$push_refs"' EXIT
cat >"$push_refs"
while read -r _local_ref local_sha _remote_ref remote_sha; do
compare_and_collect "$local_sha" "$remote_sha"
done <"$push_refs"
fi
if [[ -z "$PUSH_LOCAL_SHA" ]]; then
PUSH_LOCAL_SHA="$(git rev-parse HEAD)"
fi
if [[ -z "$PUSH_REMOTE_SHA" || "$PUSH_REMOTE_SHA" == "$ZERO" ]]; then
if git rev-parse --verify --quiet origin/main >/dev/null; then
PUSH_REMOTE_SHA="$(git rev-parse origin/main)"
else
PUSH_REMOTE_SHA="$ZERO"
fi
fi
if [[ "$compared" -eq 0 ]]; then
run_frontend=1
elif echo "$changed_files" | grep -qE "$FRONTEND_PATH_RE"; then
run_frontend=1
fi
if [[ "$run_frontend" -eq 1 ]]; then
echo ""
echo "==> Frontend version bump"
if [[ -t 0 ]]; then
check_version_bump "$PUSH_LOCAL_SHA" "$PUSH_REMOTE_SHA"
else
while read -r _local_ref local_sha _remote_ref remote_sha; do
if [[ "$local_sha" != "$ZERO" && "$remote_sha" != "$ZERO" && "$local_sha" != "$remote_sha" ]]; then
check_version_bump "$local_sha" "$remote_sha"
fi
done <"$push_refs"
fi
if [[ "$version_checked" -eq 0 ]]; then
echo " (no refs to compare — skipping version check)"
fi
fi
if [[ "$run_frontend" -eq 0 ]]; then
echo ""
echo "No frontend code paths in this push — skipping quality suite (same as CI path filters)."
checkpoint_clear
echo "✓ Pre-push checks passed"
echo ""
exit 0
fi
run_frontend_quality
echo ""
echo "✓ Pre-push checks passed"
echo ""