- 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.
304 lines
9.0 KiB
Bash
Executable File
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 ""
|