From 4d3382d024cda9f3c986aef0e808c8611e071533 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Jun 2026 11:49:40 +0000 Subject: [PATCH 1/5] Initial plan From 0a4aeeb2627bc36d16a635de6869efa52c6ae4ed Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Jun 2026 12:03:09 +0000 Subject: [PATCH 2/5] Migrate e2e test runner lambda to lambdas/ directory Co-authored-by: stephenhand <1694716+stephenhand@users.noreply.github.com> --- .github/workflows/e2e-lambda-deploy.yml | 2 +- lambdas/e2eTestRunner/Dockerfile | 112 +++++++++++++++ lambdas/e2eTestRunner/README.md | 36 +++++ lambdas/e2eTestRunner/docker-entrypoint.sh | 27 ++++ lambdas/e2eTestRunner/package.json | 23 +++ lambdas/e2eTestRunner/src/index.ts | 155 +++++++++++++++++++++ lambdas/e2eTestRunner/tsconfig.build.json | 8 ++ lambdas/e2eTestRunner/tsconfig.json | 7 + lambdas/package-lock.json | 93 +++++++++++++ lambdas/package.json | 1 + lambdas/tsconfig.json | 1 + 11 files changed, 464 insertions(+), 1 deletion(-) create mode 100644 lambdas/e2eTestRunner/Dockerfile create mode 100644 lambdas/e2eTestRunner/README.md create mode 100644 lambdas/e2eTestRunner/docker-entrypoint.sh create mode 100644 lambdas/e2eTestRunner/package.json create mode 100644 lambdas/e2eTestRunner/src/index.ts create mode 100644 lambdas/e2eTestRunner/tsconfig.build.json create mode 100644 lambdas/e2eTestRunner/tsconfig.json diff --git a/.github/workflows/e2e-lambda-deploy.yml b/.github/workflows/e2e-lambda-deploy.yml index aa18dcd3ce..ca46e2236f 100644 --- a/.github/workflows/e2e-lambda-deploy.yml +++ b/.github/workflows/e2e-lambda-deploy.yml @@ -75,7 +75,7 @@ jobs: uses: docker/build-push-action@v7 with: context: ./ - file: ./e2e-tests/Dockerfile + file: ./lambdas/e2eTestRunner/Dockerfile push: true tags: ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:live,${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ github.sha }} provenance: false diff --git a/lambdas/e2eTestRunner/Dockerfile b/lambdas/e2eTestRunner/Dockerfile new file mode 100644 index 0000000000..831abfadd2 --- /dev/null +++ b/lambdas/e2eTestRunner/Dockerfile @@ -0,0 +1,112 @@ +# Copyright (C) 2021-2023 Technology Matters +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see https://www.gnu.org/licenses/. + +# We start with a large official playwright base image to build the dependencies. +FROM mcr.microsoft.com/playwright:v1.30.0-jammy AS build + +WORKDIR /app + +ARG TARGETARCH + +RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \ + apt-get install -y \ + nodejs \ + autoconf \ + g++ \ + libtool \ + make \ + cmake \ + unzip \ + xz-utils \ + ca-certificates \ + libcurl4-openssl-dev && \ + # aws-lambda-ric provides the scaffolding to run the lambda in a container + npm install aws-lambda-ric -g && \ + #aws-lambda-rie provides emulation of the aws runtime interface for local development + if [ "$TARGETARCH" = "arm64" ]; then export REI_FILE="aws-lambda-rie-arm64"; else export REI_FILE="aws-lambda-rie"; fi && \ + curl -Lo /usr/local/bin/aws-lambda-rie https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/${REI_FILE} && \ + chmod +x /usr/local/bin/aws-lambda-rie + +# Install e2e-tests dependencies (includes Playwright and all test dependencies) +COPY lambdas/packages/hrm-form-definitions/package*.json /app/hrm-form-definitions/ +COPY e2e-tests/package*.json /app/e2e-tests/ + +RUN cd hrm-form-definitions && \ + npm ci && \ + cd ../e2e-tests && \ + npm ci && \ + # We clean the base image browsers and only install the one we need to reduce the final image size + rm -rf /ms-playwright/* && \ + npm run postinstall + +# Install and build the lambda handler from the lambdas workspace +COPY lambdas/e2eTestRunner/package*.json /app/lambdas/e2eTestRunner/ +COPY lambdas/packages /app/lambdas/packages/ +COPY lambdas/tsconfig.base.json /app/lambdas/ +COPY lambdas/e2eTestRunner/tsconfig.json /app/lambdas/e2eTestRunner/ + +RUN cd /app/lambdas && npm ci -w e2eTestRunner -w packages/* --verbose + +COPY lambdas/e2eTestRunner/src /app/lambdas/e2eTestRunner/src/ +COPY lambdas/e2eTestRunner/tsconfig.build.json /app/lambdas/ + +RUN cd /app/lambdas && npx tsc -b e2eTestRunner/tsconfig.build.json --verbose + +# After initial build, we switch to a smaller base image and only copy the necessary files to reduce the final image size. +FROM ubuntu:jammy + +WORKDIR /app + +RUN apt-get update && \ + apt-get install -y curl wget gpg && \ + curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \ + apt-get install -y nodejs && \ + # move npm cache to /tmp for use in lambda + npm config set cache /tmp --global && \ + rm -rf /root/.npm/* && \ + apt-get install -y --no-install-recommends git openssh-client && \ + npm install -g yarn && \ + # clean apt cache + rm -rf /var/lib/apt/lists/* + +COPY --from=build /usr/local/bin/aws-lambda-rie /usr/local/bin/aws-lambda-rie +COPY --from=build /usr/bin /usr/bin +COPY --from=build /usr/lib/node_modules /usr/lib/node_modules +COPY --from=build /app/hrm-form-definitions /app/hrm-form-definitions +COPY --from=build /app/e2e-tests /app/e2e-tests +COPY --from=build /ms-playwright /ms-playwright + +WORKDIR /app/e2e-tests + +RUN npx playwright install-deps + +COPY lambdas/packages/hrm-form-definitions /app/hrm-form-definitions +COPY e2e-tests /app/e2e-tests + +# Copy the compiled lambda handler and its dependencies +COPY --from=build /app/lambdas/e2eTestRunner/dist /app/e2eTestRunner +COPY --from=build /app/lambdas/node_modules /app/e2eTestRunner/node_modules + +COPY --chmod=0755 lambdas/e2eTestRunner/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh + +# AWS Lambda always sets LAMBDA_TASK_ROOT=/var/task at runtime, overriding any Dockerfile ENV. +# Symlinking /var/task -> /app/e2eTestRunner ensures aws-lambda-ric finds index.js at the expected path. +RUN ln -sf /app/e2eTestRunner /var/task + +ENV TEST_IN_LAMBDA=true +ENV XDG_CONFIG_HOME=/tmp/.config +ENV PLAYWRIGHT_BROWSERS_PATH=/tmp + +ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] +CMD ["index.handler"] diff --git a/lambdas/e2eTestRunner/README.md b/lambdas/e2eTestRunner/README.md new file mode 100644 index 0000000000..ee41dd9f5d --- /dev/null +++ b/lambdas/e2eTestRunner/README.md @@ -0,0 +1,36 @@ +# E2E Test Runner Lambda + +This lambda runs the Playwright E2E tests defined in `/e2e-tests` via an AWS Lambda function. + +## Overview + +The handler spawns `npm run test` (or a custom npm script) in the `/app/e2e-tests` directory +inside the container, then uploads the test artifacts (screenshots, videos, junit results) to S3. + +## Event Parameters + +| Parameter | Type | Required | Description | +|-------------|--------|----------|----------------------------------------------------------------| +| `testName` | string | No | Name of a specific test to run (sets `TEST_NAME` env var) | +| `npmScript` | string | No | npm script to run (defaults to `test`) | + +## Docker + +The lambda uses a custom Dockerfile (`Dockerfile`) that is based on the Microsoft Playwright +base image in order to include the Playwright browser dependencies. The E2E test code from +`/e2e-tests` is pulled into the container at build time. + +To build the Docker image locally from the repository root: + +```bash +cd lambdas/e2eTestRunner +npm run docker:build +``` + +## Environment Variables + +The following environment variables are set in the container: + +- `TEST_IN_LAMBDA=true` — indicates the tests are running inside a Lambda +- `XDG_CONFIG_HOME=/tmp/.config` — redirects browser config to writable `/tmp` +- `PLAYWRIGHT_BROWSERS_PATH=/tmp` — redirects Playwright browser binaries to writable `/tmp` diff --git a/lambdas/e2eTestRunner/docker-entrypoint.sh b/lambdas/e2eTestRunner/docker-entrypoint.sh new file mode 100644 index 0000000000..1fe95993b0 --- /dev/null +++ b/lambdas/e2eTestRunner/docker-entrypoint.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env sh +# Copyright (C) 2021-2023 Technology Matters +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see https://www.gnu.org/licenses/. + +# Nothing is writable on a lambda except /tmp. We need to copy the browser there and set it as our home directory +# so that chromium can write to it. +cp -r /ms-playwright/* /tmp/ + +# Some chromeium startup writes to ~/some/directory. We need to set the home directory to /tmp so that it can write +export HOME=/tmp + +if [ -z "${AWS_LAMBDA_RUNTIME_API}" ]; then + exec /usr/local/bin/aws-lambda-rie /usr/bin/aws-lambda-ric $@ +else + exec /usr/bin/aws-lambda-ric $@ +fi diff --git a/lambdas/e2eTestRunner/package.json b/lambdas/e2eTestRunner/package.json new file mode 100644 index 0000000000..f8d1a4ff9b --- /dev/null +++ b/lambdas/e2eTestRunner/package.json @@ -0,0 +1,23 @@ +{ + "name": "@tech-matters/e2e-test-runner", + "version": "1.0.0", + "description": "Lambda for running Playwright E2E tests", + "main": "index.js", + "scripts": { + "docker:build": "docker build -t e2etestrunner -f Dockerfile ../../../" + }, + "author": "Tech Matters", + "license": "AGPL", + "devDependencies": { + "@tsconfig/node22": "^22.0.0", + "@types/aws-lambda": "^8.10.108", + "@types/node": "^22.18.0", + "ts-node": "^10.9.1", + "typescript": "^5.8.2" + }, + "dependencies": { + "@aws-sdk/client-s3": "^3.993.0", + "@aws-sdk/client-ssm": "^3.993.0", + "date-fns": "^2.28.0" + } +} diff --git a/lambdas/e2eTestRunner/src/index.ts b/lambdas/e2eTestRunner/src/index.ts new file mode 100644 index 0000000000..3fe5722de3 --- /dev/null +++ b/lambdas/e2eTestRunner/src/index.ts @@ -0,0 +1,155 @@ +/** + * Copyright (C) 2021-2023 Technology Matters + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { spawn } from 'child_process'; +import path from 'path'; +import { promises as fs, createReadStream } from 'fs'; +import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; +import { SSMClient, GetParameterCommand } from '@aws-sdk/client-ssm'; +import { format } from 'date-fns'; + +const SSM_REGION = 'us-east-1'; // All our parameters are in this region, regardless of where the actual helpline is deployed to + +type HandlerEnv = Record; + +type E2ETestEvent = { + testName?: string; + npmScript?: string; +}; + +const getParameterValue = async (name: string): Promise => { + const ssm = new SSMClient({ region: SSM_REGION }); + const command = new GetParameterCommand({ + Name: name, + WithDecryption: true, + }); + const { + Parameter: { Value }, + } = await ssm.send(command); + console.debug(`SSM ${name} = ${Value}`); + return Value as string; +}; + +// https://stackoverflow.com/a/65862128/30481093 +const uploadDir = async ( + dirPath: string, + bucketName: string, + s3BasePath: string, + options: { region: string }, +): Promise => { + const s3 = new S3Client(options); + + // Recursive getFiles from https://stackoverflow.com/a/45130990/831465 + async function getFiles(dir: string): Promise { + const dirents = await fs.readdir(dir, { withFileTypes: true }); + const files = await Promise.all( + dirents.map(dirent => { + const res = path.resolve(dir, dirent.name); + return dirent.isDirectory() ? getFiles(res) : [res]; + }), + ); + return ([] as string[]).concat(...files); + } + + const files = await getFiles(dirPath); + console.debug('Uploading files:', files); + const uploads = files.map(filePath => + s3.send( + new PutObjectCommand({ + Key: path.join(s3BasePath, path.relative(dirPath, filePath)).replace('\\', '/'), + Bucket: bucketName, + Body: createReadStream(filePath), + }), + ), + ); + await Promise.all(uploads); +}; + +const uploadTestArtifactsToS3 = async (env: HandlerEnv): Promise => { + const accountSid = await getParameterValue( + `/${env.HL_ENV}/twilio/${(env.HL as string).toUpperCase()}/account_sid`, + ); + const region = await getParameterValue(`/${env.HL_ENV}/aws/${accountSid}/region`); + const bucket = await getParameterValue( + `/${env.HL_ENV}/s3/${accountSid}/docs_bucket_name`, + ); + + const now = new Date(); + const formattedDate = format(now, 'yyyy-MM-dd-HH-mm-ss-SSSS'); + const s3KeyRoot = `e2e-tests/${env.TEST_NAME ?? 'all_test'}/${formattedDate}`; + console.info(`Uploading test artifacts to ${s3KeyRoot}`); + await uploadDir(path.resolve('/tmp/storage'), bucket, s3KeyRoot, { region }); + await uploadDir( + path.resolve('/tmp/test-results'), + bucket, + `${s3KeyRoot}/test-results`, + { + region, + }, + ); +}; + +export const handler = async (event: E2ETestEvent): Promise => { + const env: HandlerEnv = { ...process.env }; + + const { testName, npmScript } = event; + if (testName) { + env.TEST_NAME = testName; + } + + const cmd = spawn( + /^win/.test(process.platform) ? 'npm.cmd' : 'npm', + ['-loglevel silent', 'run', npmScript || 'test'], + { + stdio: 'inherit', + cwd: '/app/e2e-tests', + env: env as NodeJS.ProcessEnv, + }, + ); + + let result: unknown; + let isError = false; + try { + result = await new Promise((resolve, reject) => { + cmd.on('exit', code => { + if (code !== 0) { + reject(`Execution error: ${code}`); + } else { + resolve(`Exited with code: ${code}`); + } + }); + + cmd.on('error', error => { + reject(`Execution error: ${error}`); + }); + }); + } catch (error) { + result = error; + isError = true; + } + + try { + console.info('Attempting artifact uploads...'); + await uploadTestArtifactsToS3(env); + } catch (err) { + console.error('Error uploading test artifacts:', err); + } + + if (isError) { + throw result; + } + console.info(`Test run (${env.TEST_NAME}) result: `, result); +}; diff --git a/lambdas/e2eTestRunner/tsconfig.build.json b/lambdas/e2eTestRunner/tsconfig.build.json new file mode 100644 index 0000000000..69aedecf49 --- /dev/null +++ b/lambdas/e2eTestRunner/tsconfig.build.json @@ -0,0 +1,8 @@ +// This file is copied to the lambda root at build time, so all paths are relative to the root +{ + "extends": "./tsconfig.base.json", + "files": [], + "references": [ + { "path": "e2eTestRunner" } + ] +} diff --git a/lambdas/e2eTestRunner/tsconfig.json b/lambdas/e2eTestRunner/tsconfig.json new file mode 100644 index 0000000000..a170f2fdaa --- /dev/null +++ b/lambdas/e2eTestRunner/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.base.json", + "include": ["src/**/*.ts"], + "compilerOptions": { + "outDir": "./dist" + } +} diff --git a/lambdas/package-lock.json b/lambdas/package-lock.json index 5bed4a36d7..b2f1bbeee2 100644 --- a/lambdas/package-lock.json +++ b/lambdas/package-lock.json @@ -10,6 +10,7 @@ "license": "AGPL", "workspaces": [ "account-scoped", + "e2eTestRunner", "facebookCallback", "facebookSignin", "instagramWebhook", @@ -427,6 +428,56 @@ "ts-node": "^10.9.1" } }, + "e2eTestRunner": { + "name": "@tech-matters/e2e-test-runner", + "version": "1.0.0", + "license": "AGPL", + "dependencies": { + "@aws-sdk/client-s3": "^3.993.0", + "@aws-sdk/client-ssm": "^3.993.0", + "date-fns": "^2.28.0" + }, + "devDependencies": { + "@tsconfig/node22": "^22.0.0", + "@types/aws-lambda": "^8.10.108", + "@types/node": "^22.18.0", + "ts-node": "^10.9.1", + "typescript": "^5.8.2" + } + }, + "e2eTestRunner/node_modules/@types/node": { + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "e2eTestRunner/node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "e2eTestRunner/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, "facebook-callback": { "name": "@tech-matters/facebookcallback", "version": "1.0.0", @@ -6153,6 +6204,10 @@ "resolved": "packages/configuration", "link": true }, + "node_modules/@tech-matters/e2e-test-runner": { + "resolved": "e2eTestRunner", + "link": true + }, "node_modules/@tech-matters/facebookcallback": { "resolved": "facebookCallback", "link": true @@ -22701,6 +22756,44 @@ "typescript": "^5.9.2" } }, + "@tech-matters/e2e-test-runner": { + "version": "file:e2eTestRunner", + "requires": { + "@aws-sdk/client-s3": "^3.993.0", + "@aws-sdk/client-ssm": "^3.993.0", + "@tsconfig/node22": "^22.0.0", + "@types/aws-lambda": "^8.10.108", + "@types/node": "^22.18.0", + "date-fns": "^2.28.0", + "ts-node": "^10.9.1", + "typescript": "^5.8.2" + }, + "dependencies": { + "@types/node": { + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "dev": true, + "requires": { + "undici-types": "~6.21.0" + } + }, + "date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "requires": { + "@babel/runtime": "^7.21.0" + } + }, + "undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true + } + } + }, "@tech-matters/facebookcallback": { "version": "file:facebookCallback", "requires": { diff --git a/lambdas/package.json b/lambdas/package.json index 86184780ae..9aa27bc3c3 100644 --- a/lambdas/package.json +++ b/lambdas/package.json @@ -6,6 +6,7 @@ "license": "AGPL", "workspaces": [ "account-scoped", + "e2eTestRunner", "facebookCallback", "facebookSignin", "instagramWebhook", diff --git a/lambdas/tsconfig.json b/lambdas/tsconfig.json index b140bb2e26..275902ed3c 100644 --- a/lambdas/tsconfig.json +++ b/lambdas/tsconfig.json @@ -11,6 +11,7 @@ { "path": "packages/twilio-types" }, { "path": "packages/twilio-configuration" }, { "path": "account-scoped" }, + { "path": "e2eTestRunner" }, { "path": "facebookCallback" }, { "path": "facebookSignin" }, { "path": "instagramWebhook" }, From cf2c909e9e3c85e4c21e6792b8d7f6914cc39139 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Jun 2026 12:06:12 +0000 Subject: [PATCH 3/5] Fix typo in entrypoint comment and use replaceAll for path backslash replacement Co-authored-by: stephenhand <1694716+stephenhand@users.noreply.github.com> --- lambdas/e2eTestRunner/docker-entrypoint.sh | 2 +- lambdas/e2eTestRunner/src/index.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lambdas/e2eTestRunner/docker-entrypoint.sh b/lambdas/e2eTestRunner/docker-entrypoint.sh index 1fe95993b0..f116ea4d49 100644 --- a/lambdas/e2eTestRunner/docker-entrypoint.sh +++ b/lambdas/e2eTestRunner/docker-entrypoint.sh @@ -17,7 +17,7 @@ # so that chromium can write to it. cp -r /ms-playwright/* /tmp/ -# Some chromeium startup writes to ~/some/directory. We need to set the home directory to /tmp so that it can write +# Some chromium startup writes to ~/some/directory. We need to set the home directory to /tmp so that it can write export HOME=/tmp if [ -z "${AWS_LAMBDA_RUNTIME_API}" ]; then diff --git a/lambdas/e2eTestRunner/src/index.ts b/lambdas/e2eTestRunner/src/index.ts index 3fe5722de3..002f49beb2 100644 --- a/lambdas/e2eTestRunner/src/index.ts +++ b/lambdas/e2eTestRunner/src/index.ts @@ -69,7 +69,9 @@ const uploadDir = async ( const uploads = files.map(filePath => s3.send( new PutObjectCommand({ - Key: path.join(s3BasePath, path.relative(dirPath, filePath)).replace('\\', '/'), + Key: path + .join(s3BasePath, path.relative(dirPath, filePath)) + .replaceAll('\\', '/'), Bucket: bucketName, Body: createReadStream(filePath), }), From 1d427fa255fe7fdb2ab2b41ad9da228e7f62ddca Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Jun 2026 12:08:50 +0000 Subject: [PATCH 4/5] Fix SSM null safety and split npm loglevel arg correctly Co-authored-by: stephenhand <1694716+stephenhand@users.noreply.github.com> --- lambdas/e2eTestRunner/src/index.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lambdas/e2eTestRunner/src/index.ts b/lambdas/e2eTestRunner/src/index.ts index 002f49beb2..e2d66c0514 100644 --- a/lambdas/e2eTestRunner/src/index.ts +++ b/lambdas/e2eTestRunner/src/index.ts @@ -36,11 +36,12 @@ const getParameterValue = async (name: string): Promise => { Name: name, WithDecryption: true, }); - const { - Parameter: { Value }, - } = await ssm.send(command); - console.debug(`SSM ${name} = ${Value}`); - return Value as string; + const { Parameter } = await ssm.send(command); + if (!Parameter?.Value) { + throw new Error(`SSM parameter not found: ${name}`); + } + console.debug(`SSM ${name} = ${Parameter.Value}`); + return Parameter.Value; }; // https://stackoverflow.com/a/65862128/30481093 @@ -114,7 +115,7 @@ export const handler = async (event: E2ETestEvent): Promise => { const cmd = spawn( /^win/.test(process.platform) ? 'npm.cmd' : 'npm', - ['-loglevel silent', 'run', npmScript || 'test'], + ['-loglevel', 'silent', 'run', npmScript || 'test'], { stdio: 'inherit', cwd: '/app/e2e-tests', From 82a95685f8ec0957356f1d6f71de89c190dfb0df Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Jun 2026 14:47:14 +0000 Subject: [PATCH 5/5] Deprecate e2e-lambda-deploy; wire e2eTestRunner into standard lambda workflows - Add e2eTestRunner to twilio-lambda-ci.yml build matrix with conditional build steps that use its custom Dockerfile (lambdas/e2eTestRunner/Dockerfile) and repo-root build context instead of the shared lambdas/Dockerfile - Add e2eTestRunner to lambda-environment-region-map.json with same regions as the previous e2e-lambda-deploy (dev/staging/prod all regions) - Add e2eTestRunner to Deploy Twilio Lambda workflow options - Add e2eTestRunner to Deploy All Twilio Lambdas workflow matrix - Deprecate e2e-lambda-deploy.yml: remove workflow_dispatch trigger and add deprecation notice pointing to the standard deploy workflows Co-authored-by: stephenhand <1694716+stephenhand@users.noreply.github.com> --- .../config/lambda-environment-region-map.json | 1 + .github/workflows/e2e-lambda-deploy.yml | 17 ++++------------- .github/workflows/twilio-lambda-ci.yml | 12 ++++++++++++ .github/workflows/twilio-lambda-deploy-all.yml | 1 + .github/workflows/twilio-lambda-deploy.yml | 1 + 5 files changed, 19 insertions(+), 13 deletions(-) diff --git a/.github/workflows/config/lambda-environment-region-map.json b/.github/workflows/config/lambda-environment-region-map.json index d24579aca0..829c738046 100644 --- a/.github/workflows/config/lambda-environment-region-map.json +++ b/.github/workflows/config/lambda-environment-region-map.json @@ -1,5 +1,6 @@ { "account-scoped": { "development": ["us-east-1"], "staging": ["us-east-1", "eu-west-1"], "production": ["us-east-1", "eu-west-1", "ca-central-1"] }, + "e2eTestRunner": { "development": ["us-east-1"], "staging": ["us-east-1", "eu-west-1"], "production": ["us-east-1", "eu-west-1", "ca-central-1"] }, "facebookCallback": { "development": ["us-east-1"], "staging": [], "production": ["us-east-1"] }, "facebookSignin": { "development": ["us-east-1"], "staging": [], "production": ["us-east-1"] }, "instagramWebhook": { "development": ["us-east-1"], "staging": [], "production": ["us-east-1"] }, diff --git a/.github/workflows/e2e-lambda-deploy.yml b/.github/workflows/e2e-lambda-deploy.yml index ca46e2236f..b81382845c 100644 --- a/.github/workflows/e2e-lambda-deploy.yml +++ b/.github/workflows/e2e-lambda-deploy.yml @@ -12,20 +12,11 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see https://www.gnu.org/licenses/. -name: 'Deploy E2E Lambda' +name: 'Deploy E2E Lambda [DEPRECATED]' +# DEPRECATED: The e2e lambda is now deployed via the 'Deploy Twilio Lambda' and +# 'Deploy All Twilio Lambdas' workflows using the standard lambda deployment pipeline. +# This workflow is kept for reference only and will be removed in a future cleanup. on: - workflow_dispatch: - inputs: - environment: - description: Environment to deploy. - default: development - required: true - type: choice - options: - - development - - staging - - production - workflow_call: secrets: AWS_ACCESS_KEY_ID: diff --git a/.github/workflows/twilio-lambda-ci.yml b/.github/workflows/twilio-lambda-ci.yml index 7c22deedf3..929d2d0ea8 100644 --- a/.github/workflows/twilio-lambda-ci.yml +++ b/.github/workflows/twilio-lambda-ci.yml @@ -75,6 +75,7 @@ jobs: matrix: lambda_path: - account-scoped + - e2eTestRunner - facebookCallback - facebookSignin - instagramWebhook @@ -122,6 +123,7 @@ jobs: echo "ref_name_for_docker=${ref_name//\//_-}" >> $GITHUB_ENV shell: bash - name: Build and Push Docker Image + if: ${{ matrix.lambda_path != 'e2eTestRunner' }} uses: docker/build-push-action@v7 with: context: ./lambdas @@ -133,3 +135,13 @@ jobs: # 'latest' is never used, but it keeps terraform happy tags: ${{ env.ECR_URL }}:${{ github.ref_type }}.${{ env.ref_name_for_docker }},${{ env.ECR_URL }}:${{ github.sha }},${{ env.ECR_URL }}:latest provenance: false + - name: Build and Push Docker Image (e2eTestRunner) + if: ${{ matrix.lambda_path == 'e2eTestRunner' }} + uses: docker/build-push-action@v7 + with: + context: ./ + file: ./lambdas/e2eTestRunner/Dockerfile + push: true + # 'latest' is never used, but it keeps terraform happy + tags: ${{ env.ECR_URL }}:${{ github.ref_type }}.${{ env.ref_name_for_docker }},${{ env.ECR_URL }}:${{ github.sha }},${{ env.ECR_URL }}:latest + provenance: false diff --git a/.github/workflows/twilio-lambda-deploy-all.yml b/.github/workflows/twilio-lambda-deploy-all.yml index 2082072dc3..2167261c11 100644 --- a/.github/workflows/twilio-lambda-deploy-all.yml +++ b/.github/workflows/twilio-lambda-deploy-all.yml @@ -54,6 +54,7 @@ jobs: matrix: lambda_path: - account-scoped + - e2eTestRunner - facebookCallback - facebookSignin - instagramWebhook diff --git a/.github/workflows/twilio-lambda-deploy.yml b/.github/workflows/twilio-lambda-deploy.yml index b00b491592..ba62913671 100644 --- a/.github/workflows/twilio-lambda-deploy.yml +++ b/.github/workflows/twilio-lambda-deploy.yml @@ -31,6 +31,7 @@ on: type: choice options: - account-scoped + - e2eTestRunner - facebookCallback - facebookSignin - instagramWebhook