diff --git a/.claude/hooks/check-comment-line-breaks.sh b/.claude/hooks/check-comment-line-breaks.sh new file mode 100755 index 000000000..a59218ba3 --- /dev/null +++ b/.claude/hooks/check-comment-line-breaks.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# PostToolUse on Edit|Write|MultiEdit: reject hard-wrapped comments per CLAUDE.md. +# Blocks Claude (decision:block) when a single sentence is broken across multiple # lines so Claude rewrites before continuing. + +REPO_ROOT=$(cd "$(dirname "$0")/../.." && pwd) + +INPUT=$(cat) +FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""') + +case "$FILE" in + "$REPO_ROOT"/*.py) ;; + *) exit 0 ;; +esac + +case "$FILE" in + */.venv/*|*/venv/*) exit 0 ;; +esac + +OUTPUT=$(python3 "$REPO_ROOT/scripts/lint/check_comment_line_breaks.py" "$FILE" 2>&1) +STATUS=$? +if [ $STATUS -eq 0 ]; then + exit 0 +fi + +jq -n --arg reason "BLOCKED: Hard-wrapped comments per CLAUDE.md. Rewrite as one line per sentence (let the editor wrap visually): +$OUTPUT" '{decision: "block", reason: $reason}' +exit 2 diff --git a/.claude/settings.json b/.claude/settings.json index 3ff39188a..4b93273a1 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -32,6 +32,11 @@ "type": "command", "command": "/Users/rwest/Repositories/gitauto/.claude/hooks/check-logger-coverage.sh", "timeout": 15 + }, + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/check-comment-line-breaks.sh", + "timeout": 10 } ] } diff --git a/constants/mongoms.py b/constants/mongoms.py index 81ac7d032..fecb3b150 100644 --- a/constants/mongoms.py +++ b/constants/mongoms.py @@ -1,4 +1,5 @@ # Default MongoDB server versions that mongodb-memory-server downloads when no version is specified in package.json config. +# Must match upstream so GA tests against the same MongoDB the customer's CI does. # https://github.com/typegoose/mongodb-memory-server/blob/master/packages/mongodb-memory-server-core/src/util/resolve-config.ts MONGOMS_MAJOR_TO_MONGODB_VERSION: dict[int, str] = { 7: "6.0.9", diff --git a/pyproject.toml b/pyproject.toml index 737bad730..1995c7a9b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "GitAuto" -version = "1.55.3" +version = "1.55.4" requires-python = ">=3.14" dependencies = [ "annotated-doc==0.0.4", diff --git a/scripts/git/pre_commit_hook.sh b/scripts/git/pre_commit_hook.sh index 0baa1961d..39036b0e9 100755 --- a/scripts/git/pre_commit_hook.sh +++ b/scripts/git/pre_commit_hook.sh @@ -117,6 +117,17 @@ echo "--- cast usage check ---" scripts/lint/check_cast_usage.sh if [ $? -ne 0 ]; then exit 1; fi +# Comment line-break check (don't hard-wrap single sentences across # lines) +echo "--- comment line-break check ---" +if [ -n "$STAGED_PY_FILES" ]; then + # shellcheck disable=SC2086 + python3 scripts/lint/check_comment_line_breaks.py $STAGED_PY_FILES + if [ $? -ne 0 ]; then + echo "FAILED: Rewrite hard-wrapped comments as one line per sentence." + exit 1 + fi +fi + # Concurrent heavy checks (pylint, pyright, pytest) echo "--- pylint + pyright + pytest (concurrent) ---" scripts/lint/pre_commit_parallel_checks.sh diff --git a/services/mongoms/fixtures/foxden-version-controller-jest-mongodb-config.js b/services/mongoms/fixtures/foxden-version-controller-jest-mongodb-config.js new file mode 100644 index 000000000..5986102a9 --- /dev/null +++ b/services/mongoms/fixtures/foxden-version-controller-jest-mongodb-config.js @@ -0,0 +1,13 @@ +module.exports = { + mongodbMemoryServerOptions: { + binary: { + version: 'v8.0-latest', + skipMD5: true, + }, + instance: { + dbName: 'jest', + }, + autoStart: true, + }, + mongoURLEnvName: 'JEST_MONGODB_URI', +}; diff --git a/services/mongoms/get_distro_for_mongodb_server_version.py b/services/mongoms/get_distro_for_mongodb_server_version.py index 6621118ca..f1b9474a1 100644 --- a/services/mongoms/get_distro_for_mongodb_server_version.py +++ b/services/mongoms/get_distro_for_mongodb_server_version.py @@ -4,8 +4,11 @@ @handle_exceptions(default_return_value="amazon2023", raise_on_error=False) def get_distro_for_mongodb_server_version(mongodb_server_version: str): - """Return the correct Amazon Linux distro name for a MongoDB server version. - MongoDB 7.0+ has amazon2023 builds. 6.0.x and earlier only have amazon2.""" + """Return a distro name whose MongoDB binary runs on our AL2023 Lambda. + MongoDB 7.0+ has amazon2023 builds — use those. + MongoDB 6.0.x has no amazon2023 build, so fall back to rhel90 (RHEL 9 ships glibc 2.34 and OpenSSL 3, same ABI as AL2023). + Using `amazon2` for 6.x produces a binary linked against libcrypto.so.10 (OpenSSL 1.0.x) which AL2023 does not provide — Foxquilt PR #203 run (CloudWatch 2026-04-21 14:01:55) crashed with exactly this error. + """ # Extract major.minor from version strings like "v7.0-latest", "7.0.11", "6.0.14" cleaned = mongodb_server_version.lstrip("v") major_str = cleaned.split(".")[0] @@ -26,6 +29,7 @@ def get_distro_for_mongodb_server_version(mongodb_server_version: str): return "amazon2023" logger.info( - "get_distro_for_mongodb_server_version: MongoDB %s.x uses amazon2 distro", major + "get_distro_for_mongodb_server_version: MongoDB %s.x uses rhel90 distro (no amazon2023 build exists for 6.x; rhel90 shares glibc+OpenSSL ABI with AL2023)", + major, ) - return "amazon2" + return "rhel90" diff --git a/services/mongoms/get_mongodb_server_version.py b/services/mongoms/get_mongodb_server_version.py index 527b3a9e3..f952c67d6 100644 --- a/services/mongoms/get_mongodb_server_version.py +++ b/services/mongoms/get_mongodb_server_version.py @@ -5,10 +5,26 @@ from utils.files.read_local_file import read_local_file from utils.logging.logging_config import logger +# `@shelf/jest-mongodb` reads its settings from one of these files at the repo root. +# Order matters: the first one present wins (same order as the upstream library). +JEST_MONGODB_CONFIG_FILENAMES = ( + "jest-mongodb-config.js", + "jest-mongodb-config.cjs", + "jest-mongodb-config.ts", +) + +# Matches e.g. `binary: { version: 'v8.0-latest', ... }` or the same with double quotes. +# Kept permissive for whitespace/newlines inside the `binary` object so it tolerates typical formatter output. +_BINARY_VERSION_RE = re.compile( + r"binary\s*:\s*\{[^{}]*?version\s*:\s*['\"]([^'\"]+)['\"]", + re.DOTALL, +) + @handle_exceptions(default_return_value=None, raise_on_error=False) def get_mongodb_server_version(clone_dir: str): - """Detect MongoDB version from package.json config or scripts. Returns e.g. 'v7.0-latest' or None.""" + """Detect MongoDB version from package.json config, scripts, or jest-mongodb-config. + Returns e.g. 'v7.0-latest' or None.""" pkg_content = read_local_file("package.json", clone_dir) if not pkg_content: logger.info( @@ -24,8 +40,12 @@ def get_mongodb_server_version(clone_dir: str): # config.mongodbMemoryServer.version first config = pkg.get("config") if isinstance(config, dict): + logger.info("get_mongodb_server_version: checking config.mongodbMemoryServer") mongoms_config = config.get("mongodbMemoryServer") if isinstance(mongoms_config, dict): + logger.info( + "get_mongodb_server_version: checking config.mongodbMemoryServer.version" + ) version = mongoms_config.get("version") if isinstance(version, str): logger.info( @@ -40,6 +60,7 @@ def get_mongodb_server_version(clone_dir: str): ) scripts = pkg.get("scripts") if isinstance(scripts, dict): + logger.info("get_mongodb_server_version: scanning scripts for MONGOMS_VERSION") for script in scripts.values(): match = re.search(r"MONGOMS_VERSION=(\S+)", str(script)) if match: @@ -49,5 +70,29 @@ def get_mongodb_server_version(clone_dir: str): ) return match.group(1) + # Fall back to @shelf/jest-mongodb config file at the repo root. + # Example: `mongodbMemoryServerOptions: { binary: { version: 'v8.0-latest' } }` + logger.info( + "get_mongodb_server_version: no MONGOMS_VERSION in scripts, checking jest-mongodb config" + ) + for filename in JEST_MONGODB_CONFIG_FILENAMES: + config_content = read_local_file(filename, clone_dir) + if not config_content: + logger.info("get_mongodb_server_version: %s not found", filename) + continue + match = _BINARY_VERSION_RE.search(config_content) + if match: + logger.info( + "get_mongodb_server_version: found binary.version=%s in %s", + match.group(1), + filename, + ) + return match.group(1) + + logger.info( + "get_mongodb_server_version: %s present but no binary.version pattern matched", + filename, + ) + logger.info("get_mongodb_server_version: no MongoDB version detected") return None diff --git a/services/mongoms/test_get_archive_name.py b/services/mongoms/test_get_archive_name.py index d2b3e2bf8..1f39885a2 100644 --- a/services/mongoms/test_get_archive_name.py +++ b/services/mongoms/test_get_archive_name.py @@ -2,6 +2,8 @@ # pyright: reportUnusedVariable=false from unittest.mock import patch +import pytest + from services.mongoms.get_archive_name import get_mongoms_archive_name @@ -50,11 +52,13 @@ def test_mongoms_10x_with_explicit_7x_version(_mock_major, _mock_version): "services.mongoms.get_archive_name.get_mongodb_server_version", return_value=None ) @patch("services.mongoms.get_archive_name.get_dependency_major_version", return_value=9) -def test_mongoms_9x_no_explicit_version_uses_default(_mock_major, _mock_version): - """mongoms 9.x with no explicit version falls back to default 6.0.9 with amazon2 distro.""" +def test_mongoms_9x_no_explicit_version_uses_rhel90_distro(_mock_major, _mock_version): + """mongoms 9.x with no explicit version stays on upstream's MongoDB 6.0.9 default — GA must test the same version the customer's CI does. + The broken archive was `amazon2-6.0.9` (libcrypto.so.10, not on AL2023). `rhel90-6.0.9` shares AL2023's glibc + OpenSSL 3 ABI and actually runs on Lambda. + """ assert ( get_mongoms_archive_name("/tmp/clone") - == "mongodb-linux-x86_64-amazon2-6.0.9.tgz" + == "mongodb-linux-x86_64-rhel90-6.0.9.tgz" ) @@ -76,11 +80,11 @@ def test_mongoms_10x_no_explicit_version_uses_default(_mock_major, _mock_version "services.mongoms.get_archive_name.get_mongodb_server_version", return_value=None ) @patch("services.mongoms.get_archive_name.get_dependency_major_version", return_value=7) -def test_mongoms_7x_no_explicit_version_uses_default(_mock_major, _mock_version): - """mongoms 7.x with no explicit version falls back to default 6.0.9 with amazon2 distro.""" +def test_mongoms_7x_no_explicit_version_uses_rhel90_distro(_mock_major, _mock_version): + """mongoms 7.x with no explicit version: same rationale as the 9.x case — upstream default 6.0.9 with rhel90 distro so the binary actually runs on AL2023.""" assert ( get_mongoms_archive_name("/tmp/clone") - == "mongodb-linux-x86_64-amazon2-6.0.9.tgz" + == "mongodb-linux-x86_64-rhel90-6.0.9.tgz" ) @@ -113,3 +117,101 @@ def test_mongoms_old_version_returns_none(_mock_major, _mock_version): def test_no_mongoms_in_package_json(_mock_major): """mongodb-memory-server not in package.json.""" assert get_mongoms_archive_name("/tmp/clone") is None + + +# Snapshot of every Foxquilt repo at /Users/rwest/Repositories/Foxquilt on 2026-04-21. +# Columns: mongoms major (from direct dep or transitive via @shelf/jest-mongodb in yarn.lock), explicit MongoDB version detected by get_mongodb_server_version (package.json script MONGOMS_VERSION= OR jest-mongodb-config.js binary.version), and the archive name get_mongoms_archive_name must produce post-fix. +# `None` for mongoms_major means the repo doesn't use Mongo at all; the expected archive is `None` (no pre-cache needed). +# Every Mongo-using repo must resolve to a distro in AL2023_COMPATIBLE_DISTROS. `amazon2` would crash our Lambda with libcrypto.so.10 missing. +FOXQUILT_REPO_CASES = [ + ("foxcom-forms", None, None, None), + ( + "foxcom-forms-backend", + 7, + "v7.0-latest", + "mongodb-linux-x86_64-amazon2023-v7.0-latest.tgz", + ), + ( + "foxcom-payment-backend", + 7, + "v7.0-latest", + "mongodb-linux-x86_64-amazon2023-v7.0-latest.tgz", + ), + ("foxcom-payment-frontend", None, None, None), + ("foxden-admin-portal", None, None, None), + ( + "foxden-admin-portal-backend", + 9, + "v7.0-latest", + "mongodb-linux-x86_64-amazon2023-v7.0-latest.tgz", + ), + ( + "foxden-auth-service", + 9, + "v7.0-latest", + "mongodb-linux-x86_64-amazon2023-v7.0-latest.tgz", + ), + ("foxden-billing", 10, None, "mongodb-linux-x86_64-amazon2023-7.0.11.tgz"), + ( + "foxden-policy-document-backend", + 9, + "v7.0-latest", + "mongodb-linux-x86_64-amazon2023-v7.0-latest.tgz", + ), + ( + "foxden-rating-quoting-backend", + 9, + "v7.0-latest", + "mongodb-linux-x86_64-amazon2023-v7.0-latest.tgz", + ), + # Default path — upstream mongoms 9.x default is 6.0.9 (no amazon2023 build); must fall back to rhel90 which shares AL2023's glibc/OpenSSL ABI. + ("foxden-shared-lib", 9, None, "mongodb-linux-x86_64-rhel90-6.0.9.tgz"), + ( + "foxden-tools", + 9, + "v7.0-latest", + "mongodb-linux-x86_64-amazon2023-v7.0-latest.tgz", + ), + ( + "foxden-version-controller", + 9, + "v8.0-latest", + "mongodb-linux-x86_64-amazon2023-v8.0-latest.tgz", + ), + ("foxden-version-controller-client", None, None, None), +] + +# SONAMEs present on our AL2023 Lambda. Any distro whose binary dynamically links against these runs on Lambda. `amazon2` does not — it wants libcrypto.so.10 (OpenSSL 1.0.x) which AL2023 removed. +AL2023_COMPATIBLE_DISTROS = ("amazon2023", "rhel90") + + +def test_foxquilt_repo_cases_cover_all_14_repos(): + # If Foxquilt adds/removes a repo, update FOXQUILT_REPO_CASES and this number. + assert len(FOXQUILT_REPO_CASES) == 14 + + +@pytest.mark.parametrize( + "repo_name, mongoms_major, explicit_version, expected_archive", + FOXQUILT_REPO_CASES, + ids=[case[0] for case in FOXQUILT_REPO_CASES], +) +def test_every_foxquilt_repo_resolves_to_al2023_archive( + repo_name, mongoms_major, explicit_version, expected_archive +): + with patch( + "services.mongoms.get_archive_name.get_dependency_major_version", + return_value=mongoms_major, + ), patch( + "services.mongoms.get_archive_name.get_mongodb_server_version", + return_value=explicit_version, + ): + actual = get_mongoms_archive_name(f"/tmp/{repo_name}") + assert ( + actual == expected_archive + ), f"{repo_name} produced {actual!r}; expected {expected_archive!r}" + if expected_archive is None: + # Non-Mongo repo — nothing to pre-cache, nothing to check for ABI compat. + return + assert actual is not None and any( + d in actual for d in AL2023_COMPATIBLE_DISTROS + ), f"{repo_name} produced {actual!r}; expected a distro from {AL2023_COMPATIBLE_DISTROS} so the binary can actually run on AL2023 Lambda" diff --git a/services/mongoms/test_get_distro_for_mongodb_server_version.py b/services/mongoms/test_get_distro_for_mongodb_server_version.py index 58469fcb0..bdfa8306b 100644 --- a/services/mongoms/test_get_distro_for_mongodb_server_version.py +++ b/services/mongoms/test_get_distro_for_mongodb_server_version.py @@ -15,12 +15,13 @@ def test_mongodb_821_returns_amazon2023(): assert get_distro_for_mongodb_server_version("8.2.1") == "amazon2023" -def test_mongodb_609_returns_amazon2(): - assert get_distro_for_mongodb_server_version("6.0.9") == "amazon2" +def test_mongodb_609_returns_rhel90(): + # MongoDB 6.0.9 has no amazon2023 build. rhel90 shares the glibc 2.34 + OpenSSL 3 ABI with our AL2023 Lambda; amazon2 would pull in libcrypto.so.10 which AL2023 doesn't ship. + assert get_distro_for_mongodb_server_version("6.0.9") == "rhel90" -def test_mongodb_6014_returns_amazon2(): - assert get_distro_for_mongodb_server_version("6.0.14") == "amazon2" +def test_mongodb_6014_returns_rhel90(): + assert get_distro_for_mongodb_server_version("6.0.14") == "rhel90" def test_unparseable_version_defaults_to_amazon2023(): diff --git a/services/mongoms/test_get_mongodb_server_version.py b/services/mongoms/test_get_mongodb_server_version.py index 1f139fe56..6d358d004 100644 --- a/services/mongoms/test_get_mongodb_server_version.py +++ b/services/mongoms/test_get_mongodb_server_version.py @@ -45,3 +45,72 @@ def test_no_package_json(_mock_read): """No package.json found.""" _mock_read.return_value = None assert get_mongodb_server_version("/tmp/clone") is None + + +@patch("services.mongoms.get_mongodb_server_version.read_local_file") +def test_foxden_version_controller_version_from_jest_mongodb_config(_mock_read): + """foxden-version-controller: version lives in jest-mongodb-config.js. + + This is the real customer repo that caused Foxquilt PR #203 to fail: + GA's detection defaulted to 6.0.9 (amazon2 distro) because it never + looked at jest-mongodb-config.js, where `binary.version: 'v8.0-latest'` + is actually declared. + """ + package_json = _load_fixture("foxcom-forms-package.json") + jest_config = _load_fixture("foxden-version-controller-jest-mongodb-config.js") + + def _read_side_effect(path, _): + if path == "package.json": + return package_json + if path == "jest-mongodb-config.js": + return jest_config + return None + + _mock_read.side_effect = _read_side_effect + assert get_mongodb_server_version("/tmp/clone") == "v8.0-latest" + + +@patch("services.mongoms.get_mongodb_server_version.read_local_file") +def test_jest_mongodb_config_cjs_is_checked(_mock_read): + """`.cjs` variant is checked too (same precedence as the upstream library).""" + package_json = _load_fixture("foxcom-forms-package.json") + cjs_content = ( + "module.exports = {\n" + " mongodbMemoryServerOptions: {\n" + ' binary: { version: "v7.0.14" },\n' + " },\n" + "};\n" + ) + + def _read_side_effect(path, _): + if path == "package.json": + return package_json + if path == "jest-mongodb-config.cjs": + return cjs_content + return None + + _mock_read.side_effect = _read_side_effect + assert get_mongodb_server_version("/tmp/clone") == "v7.0.14" + + +@patch("services.mongoms.get_mongodb_server_version.read_local_file") +def test_jest_mongodb_config_without_binary_version(_mock_read): + """jest-mongodb-config.js present but without a binary.version key → still None.""" + package_json = _load_fixture("foxcom-forms-package.json") + jest_config_no_version = ( + "module.exports = {\n" + " mongodbMemoryServerOptions: {\n" + " instance: { dbName: 'jest' },\n" + " },\n" + "};\n" + ) + + def _read_side_effect(path, _): + if path == "package.json": + return package_json + if path == "jest-mongodb-config.js": + return jest_config_no_version + return None + + _mock_read.side_effect = _read_side_effect + assert get_mongodb_server_version("/tmp/clone") is None diff --git a/uv.lock b/uv.lock index 091c90d6e..214411680 100644 --- a/uv.lock +++ b/uv.lock @@ -596,7 +596,7 @@ wheels = [ [[package]] name = "gitauto" -version = "1.55.3" +version = "1.55.4" source = { virtual = "." } dependencies = [ { name = "annotated-doc" },