From 1b9b2cb9918b83a348eaec8ed0187bb05938b957 Mon Sep 17 00:00:00 2001 From: eipasteur <85672662+eipasteur@users.noreply.github.com> Date: Thu, 21 May 2026 18:44:05 -0700 Subject: [PATCH] fix(github-lambda): bundle via esbuild + inline resolveGitToken Two layered runtime regressions blocked /api/github/* on staging: 1. PR #176 migrated lambda/github from CommonJS to ESM but the Terraform packaging used npm_requirements + prefix_in_zip='shared'. The ESM resolver dereferenced '../shared/response.js' to /var/shared/... (one level above /var/task/), which doesn't exist, throwing ERR_MODULE_NOT_FOUND on every cold start. 2. shared/git-token.js is CommonJS and does require('@aws-sdk/client-ssm') at the module top level. esbuild bundles a CJS shim around it, but the Node.js ESM runtime rejects the resulting dynamic require: 'Dynamic require of @aws-sdk/client-ssm is not supported'. Fix: - terraform/modules/api/lambda/main.tf: switch github_lambda from npm_requirements to the build/:zip pattern (mirroring github_issues per PR #180), bumping runtime to nodejs24.x to match the esbuild --target=node24 the workspace already uses. - lambda/github/index.js: inline resolveGitToken locally, mirroring the exact pattern adopted by lambda/github-issues. shared/response.js remains imported because esbuild can statically bundle it without hitting a dynamic require (it has no top-level require of an external package). Validated end-to-end on staging: - terraform apply succeeded (no drift) - aws lambda invoke returns statusCode 200 with valid JSON body ({connected: false}) for /api/github/status - 153 unit tests across the repo continue to pass Closes the runtime regression introduced in #176. --- lambda/github/index.js | 27 +++++++++++++++++++++++++-- terraform/modules/api/lambda/main.tf | 13 ++++++------- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/lambda/github/index.js b/lambda/github/index.js index d9f8125..e6eaeb3 100644 --- a/lambda/github/index.js +++ b/lambda/github/index.js @@ -6,15 +6,38 @@ import { DeleteCommand, } from '@aws-sdk/lib-dynamodb'; import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager'; -import { SSMClient, PutParameterCommand, DeleteParameterCommand } from '@aws-sdk/client-ssm'; +import { + SSMClient, + PutParameterCommand, + DeleteParameterCommand, + GetParameterCommand, +} from '@aws-sdk/client-ssm'; import crypto from 'crypto'; import { buildResponse } from '../shared/response.js'; -import { resolveGitToken } from '../shared/git-token.js'; const ddb = DynamoDBDocumentClient.from(new DynamoDBClient({})); const secrets = new SecretsManagerClient({}); const ssm = new SSMClient({}); +const GIT_TOKEN_PARAM_PATTERN = /^\/[\w-]+\/[\w-]+\/[\w-]+\/[\w-]+$/; + +// Inlined from shared/git-token.js — esbuild cannot bundle the CJS module +// because it does `require('@aws-sdk/client-ssm')` which becomes a dynamic +// require not supported in the ESM runtime. Mirrors the pattern adopted by +// lambda/github-issues (see PR #180). +const resolveGitToken = async (ssmClient, item) => { + if (item?.parameterName) { + if (!GIT_TOKEN_PARAM_PATTERN.test(item.parameterName)) { + throw new Error('Invalid SSM parameter name format'); + } + const param = await ssmClient.send( + new GetParameterCommand({ Name: item.parameterName, WithDecryption: true }), + ); + return JSON.parse(param.Parameter.Value).accessToken; + } + throw new Error('No SSM parameter name set'); +}; + class OAuthNotConfiguredError extends Error { constructor() { super( diff --git a/terraform/modules/api/lambda/main.tf b/terraform/modules/api/lambda/main.tf index 27f5d9d..83ddc17 100644 --- a/terraform/modules/api/lambda/main.tf +++ b/terraform/modules/api/lambda/main.tf @@ -668,17 +668,16 @@ module "github_lambda" { function_name = "${var.project_name}-github-${var.environment}" handler = "index.handler" - runtime = "nodejs18.x" + runtime = "nodejs24.x" timeout = 30 source_path = [ { - path = "${path.module}/../../../../lambda/github" - npm_requirements = true - }, - { - path = "${path.module}/../../../../lambda/shared" - prefix_in_zip = "shared" + path = "${path.module}/../../../../lambda/github" + commands = [ + "cd ../.. && npm run build -w github-lambda", + ":zip lambda/github/.build", + ] } ]