Skip to content

Commit 077b5ba

Browse files
committed
Validate ClickHouse env in greenlight Lambda
- Require CLICKHOUSE_USERNAME and CLICKHOUSE_HOST/ENDPOINT before the scan - Fail fast in handler() instead of deep inside review.run() - Gate greenlight-lambda-release.yml to run only on refs/heads/main - Add tests for the missing-username and missing-host/endpoint cases The handler already mints the App token and injects CLICKHOUSE_PASSWORD from Secrets Manager, but the ClickHouse connection settings that come from plain env vars were unchecked — a missing host or username surfaced only as an opaque failure once the scan tried to query. Validating them up front turns a misconfigured Lambda into a clear error at entry. The release-workflow branch guard keeps feature branches from publishing a release artifact. Signed-off-by: Jean Schmidt <contato@jschmidt.me>
1 parent 5fc13df commit 077b5ba

3 files changed

Lines changed: 43 additions & 0 deletions

File tree

.github/workflows/greenlight-lambda-release.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ defaults:
1313
jobs:
1414
release:
1515
name: Build and publish greenlight lambda release
16+
if: ${{ github.ref == 'refs/heads/main' }}
1617
runs-on: ubuntu-latest
1718
steps:
1819
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0

greenlight/src/greenlight/lambda_handler.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ def _require_key(mapping: dict[str, str], key: str, source: str) -> str:
4646
return mapping[key]
4747

4848

49+
def _require_clickhouse_host() -> None:
50+
if not (os.environ.get("CLICKHOUSE_HOST") or os.environ.get("CLICKHOUSE_ENDPOINT")):
51+
raise ValueError("CLICKHOUSE_HOST or CLICKHOUSE_ENDPOINT is required for the greenlight Lambda handler")
52+
53+
4954
def _load_secret(secret_store_name: str) -> dict[str, str]:
5055
import boto3 # lazy: keeps this module importable without the AWS SDK
5156

@@ -74,6 +79,8 @@ def handler(event: dict[str, object], context: object) -> dict[str, str]: # noq
7479
secret_store_name = _require_env("SECRET_STORE_NAME")
7580
app_id = _require_env("GITHUB_APP_ID")
7681
installation_id = int(_require_env("GITHUB_INSTALLATION_ID"))
82+
_require_env("CLICKHOUSE_USERNAME")
83+
_require_clickhouse_host()
7784

7885
secret = _load_secret(secret_store_name)
7986
secret_source = f"secret {secret_store_name!r}"
@@ -86,6 +93,9 @@ def handler(event: dict[str, object], context: object) -> dict[str, str]: # noq
8693
# SIGALRM soft timeout and the hard watchdog (whose os._exit would abort the runtime uncleanly).
8794
os.environ["PYTORCH_GREENLIGHT_MAX_RUNTIME_SECONDS"] = "0"
8895

96+
# No PYTORCH_GREENLIGHT_LOCK_PATH is set: Lambda's reserved_concurrent_executions = 1 already
97+
# guarantees single-flight, so the in-process fcntl lock is intentionally absent and the
98+
# EXIT_ALREADY_RUNNING branch below is only defensive/forward-compat.
8999
rc = cli.main(["review", "--ref", "main"])
90100
if rc == EXIT_OK:
91101
return {"status": "ok"}

greenlight/tests/test_lambda_handler.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
_PEM = "-----BEGIN RSA PRIVATE KEY-----\nline-one\nline-two\n-----END RSA PRIVATE KEY-----\n"
1414
_PEM_B64 = base64.b64encode(_PEM.encode("utf-8")).decode("ascii")
1515
_CH_PASSWORD = "clickhouse-secret-pw"
16+
_CH_USERNAME = "greenlight-ro"
17+
_CH_HOST = "greenlight.clickhouse.cloud"
1618
_TOKEN = "ghs_minted_installation_token"
1719
_SECRET_STORE = "greenlight/prod"
1820
_APP_ID = "123456"
@@ -41,6 +43,9 @@ def fakes(monkeypatch):
4143
monkeypatch.setenv("SECRET_STORE_NAME", _SECRET_STORE)
4244
monkeypatch.setenv("GITHUB_APP_ID", _APP_ID)
4345
monkeypatch.setenv("GITHUB_INSTALLATION_ID", str(_INSTALLATION_ID))
46+
monkeypatch.setenv("CLICKHOUSE_USERNAME", _CH_USERNAME)
47+
monkeypatch.setenv("CLICKHOUSE_HOST", _CH_HOST)
48+
monkeypatch.delenv("CLICKHOUSE_ENDPOINT", raising=False)
4449

4550
secret_json = json.dumps({"GITHUB_APP_SECRET": _PEM_B64, "CLICKHOUSE_PASSWORD": _CH_PASSWORD})
4651
fake_boto3 = MagicMock()
@@ -125,6 +130,33 @@ def test_handler_missing_env_raises(monkeypatch, fakes, missing):
125130
main_mock.assert_not_called()
126131

127132

133+
def test_handler_missing_clickhouse_username_raises(monkeypatch, fakes):
134+
_fake_boto3, fake_github = fakes
135+
monkeypatch.delenv("CLICKHOUSE_USERNAME", raising=False)
136+
main_mock = Mock()
137+
monkeypatch.setattr(cli, "main", main_mock)
138+
139+
with pytest.raises(ValueError, match="CLICKHOUSE_USERNAME"):
140+
lambda_handler.handler({}, object())
141+
142+
fake_github.GithubIntegration.assert_not_called()
143+
main_mock.assert_not_called()
144+
145+
146+
def test_handler_missing_clickhouse_host_and_endpoint_raises(monkeypatch, fakes):
147+
_fake_boto3, fake_github = fakes
148+
monkeypatch.delenv("CLICKHOUSE_HOST", raising=False)
149+
monkeypatch.delenv("CLICKHOUSE_ENDPOINT", raising=False)
150+
main_mock = Mock()
151+
monkeypatch.setattr(cli, "main", main_mock)
152+
153+
with pytest.raises(ValueError, match="CLICKHOUSE_HOST or CLICKHOUSE_ENDPOINT"):
154+
lambda_handler.handler({}, object())
155+
156+
fake_github.GithubIntegration.assert_not_called()
157+
main_mock.assert_not_called()
158+
159+
128160
def test_handler_non_numeric_installation_id_raises(monkeypatch, fakes):
129161
monkeypatch.setenv("GITHUB_INSTALLATION_ID", "not-a-number")
130162
main_mock = Mock()

0 commit comments

Comments
 (0)