pre-commit 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. #!/usr/bin/env bash
  2. set -euo pipefail
  3. # Pre-commit hook: keeps dist/ in sync with src/.
  4. #
  5. # Why: vendor/qmd/dist/ is tracked in git. Consumer machines fetch the
  6. # repo and use dist/ AS-IS without rebuilding (cli/build.sh and the fleet
  7. # pull_and_build_and_deploy.sh both consume the pre-built JS). A commit
  8. # that updates src/*.ts without committing the matching dist/ will ship a
  9. # CLI that crashes with MODULE_NOT_FOUND on any new import path.
  10. # Reference: i-9l2ueyyz (post-mortem of i-qkarfffa Stage-3 dist/embedding/
  11. # catch-up bug).
  12. #
  13. # Behaviour:
  14. # 1. If no staged file matches ^src/.*\.ts$ -> exit 0 (fast path for
  15. # docs/config-only commits).
  16. # 2. Otherwise run `npm run build` (which runs tsc + chmod the CLI shim).
  17. # 3. Then `git diff --quiet -- dist/` -- if dist/ changed as a result of
  18. # the rebuild AND those changes are not staged, FAIL LOUDLY with a
  19. # message naming the drifted files and the recovery command.
  20. #
  21. # Bypass: `git commit --no-verify` (standard git escape hatch).
  22. # --- collect staged src/*.ts paths -----------------------------------------
  23. mapfile -t STAGED_SRC < <(git diff --cached --name-only --diff-filter=ACMR | grep -E '^src/.*\.ts$' || true)
  24. if [[ "${#STAGED_SRC[@]}" -eq 0 ]]; then
  25. # Nothing under src/ in this commit -- no rebuild required.
  26. exit 0
  27. fi
  28. echo "pre-commit: ${#STAGED_SRC[@]} staged src/*.ts file(s) detected -- rebuilding dist/" >&2
  29. # --- snapshot dist/ before rebuild so we can detect rebuild-induced drift --
  30. # Use SHA-256 of git ls-tree-style content (working tree) for every dist/ file
  31. # we care about. Cheap enough for ~75 files.
  32. DIST_SHA_BEFORE=$(find dist -type f \( -name '*.js' -o -name '*.d.ts' -o -name '*.js.map' -o -name '*.d.ts.map' \) -print0 \
  33. | sort -z \
  34. | xargs -0 sha256sum \
  35. | sha256sum \
  36. | awk '{print $1}')
  37. # --- run build -- this also gates on tsc errors ----------------------------
  38. if ! npm run --silent build >/tmp/qmd-pre-commit-build.log 2>&1; then
  39. echo "" >&2
  40. echo "pre-commit: npm run build FAILED" >&2
  41. echo "" >&2
  42. echo "Build output (last 40 lines):" >&2
  43. tail -n 40 /tmp/qmd-pre-commit-build.log >&2
  44. echo "" >&2
  45. echo "Full log: /tmp/qmd-pre-commit-build.log" >&2
  46. exit 1
  47. fi
  48. DIST_SHA_AFTER=$(find dist -type f \( -name '*.js' -o -name '*.d.ts' -o -name '*.js.map' -o -name '*.d.ts.map' \) -print0 \
  49. | sort -z \
  50. | xargs -0 sha256sum \
  51. | sha256sum \
  52. | awk '{print $1}')
  53. # --- determine drift relative to git index ---------------------------------
  54. # `git diff --quiet -- dist/` exits 0 if working tree dist/ matches index.
  55. # Non-zero means dist/ has changes that are NOT staged.
  56. if git diff --quiet -- dist/; then
  57. # dist/ matches index -- everything is in sync, commit may proceed.
  58. exit 0
  59. fi
  60. # Drift detected. Print the offending files + recovery command.
  61. echo "" >&2
  62. echo "==========================================================================" >&2
  63. echo "ABORT: dist/ out of sync with src/" >&2
  64. echo "==========================================================================" >&2
  65. echo "" >&2
  66. echo "Your commit modifies src/*.ts but dist/ is not in sync after rebuild." >&2
  67. echo "Shipping this commit would push a broken @oivo/qmd to consumer machines" >&2
  68. echo "(see i-9l2ueyyz / i-qkarfffa Stage-3 retrospective)." >&2
  69. echo "" >&2
  70. echo "Drifted files:" >&2
  71. git diff --name-only -- dist/ | sed 's/^/ /' >&2
  72. echo "" >&2
  73. if [[ "$DIST_SHA_BEFORE" == "$DIST_SHA_AFTER" ]]; then
  74. # The rebuild produced the SAME dist/ as before, but git still reports
  75. # drift -- this means dist/ was already drifted before this commit
  76. # (someone hand-edited or forgot to stage a previous build). Tell them.
  77. echo "Note: rebuild produced no changes -- dist/ was already drifted before this" >&2
  78. echo "commit. Stage the drift OR revert dist/ to HEAD before retrying:" >&2
  79. echo "" >&2
  80. echo " # Option A: include the drift in this commit" >&2
  81. echo " git add dist/ && git commit # retry" >&2
  82. echo "" >&2
  83. echo " # Option B: discard the drift (use if dist/ was hand-edited)" >&2
  84. echo " git checkout HEAD -- dist/ && git commit # retry" >&2
  85. else
  86. # The rebuild itself produced new dist/ content. User just needs to stage
  87. # the freshly-built dist/.
  88. echo "Recovery (run, then retry the commit):" >&2
  89. echo "" >&2
  90. echo " git add dist/" >&2
  91. echo " git commit # retry" >&2
  92. fi
  93. echo "" >&2
  94. echo "Bypass (NOT recommended -- ships broken @oivo/qmd to fleet):" >&2
  95. echo " git commit --no-verify" >&2
  96. echo "" >&2
  97. exit 1