Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
4f0a8f2
chore(tests): add e2e test for logger InvokeStore isolation on Lambda…
svozza Jul 8, 2026
a308964
chore(tests): capture LMI e2e logs via stdout interception instead of…
svozza Jul 8, 2026
cae8d13
chore(tests): run LMI e2e suite unconditionally and filter captured l…
svozza Jul 9, 2026
f54675d
chore(tests): raise vitest worker cap for logger e2e runs
svozza Jul 9, 2026
9970bc1
Revert "chore(tests): raise vitest worker cap for logger e2e runs"
svozza Jul 9, 2026
87d71f4
chore(tests): add run-scoped shared LMI capacity provider CLI
svozza Jul 10, 2026
6783759
fix(tests): validate LMI CLI run-id to prevent path traversal
svozza Jul 13, 2026
b5e9d4d
fix(tests): validate constructed assembly path stays within tmpdir
svozza Jul 13, 2026
e70faea
refactor(tests): extract LMI capacity-provider attachment helpers
svozza Jul 13, 2026
d4d4306
refactor(tests): replace LMI CLI with TestStack-based workflow scripts
svozza Jul 20, 2026
813e50e
refactor(tests): run LMI e2e suites in a dedicated job gated on the s…
svozza Jul 20, 2026
d8199a3
refactor(tests): deploy and destroy LMI capacity provider stacks conc…
svozza Jul 20, 2026
b87de6a
refactor(tests): pass architecture explicitly instead of mutating pro…
svozza Jul 20, 2026
3ba1f85
fix(tests): disambiguate architectures in shared capacity provider pr…
svozza Jul 20, 2026
90752a7
chore(tests): add phase markers to LMI e2e suite output
svozza Jul 20, 2026
f2b3c0f
docs(tests): clarify retry semantics for LMI vs non-LMI e2e jobs
svozza Jul 20, 2026
ed214b5
chore(tests): invoke LMI capacity provider scripts via npm scripts
svozza Jul 20, 2026
f7ca73d
fix(tests): opt out of clobberEnv to make concurrent CDK synths safe
svozza Aug 6, 2026
ba28594
fix(tests): sweep orphaned LMI function stacks before deleting providers
svozza Aug 6, 2026
c07bee8
fix(tests): set publishToLatestPublished on the construct attach path
svozza Aug 6, 2026
b40b226
refactor(tests): drop unused includeTailLogs invoke option
svozza Aug 6, 2026
7894356
chore(tests): fix logger e2e SDK dependencies
svozza Aug 6, 2026
e0d809c
fix(tests): log the real stack name in deploy/destroy progress
svozza Aug 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 154 additions & 1 deletion .github/workflows/run-e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,50 @@ permissions:
contents: read

jobs:
# Deploys one run-scoped shared LMI capacity provider stack per architecture.
# EC2-backed capacity providers are the slowest resources in the LMI e2e
# suites and are subject to account vCPU quotas, so the suites share one per
# architecture instead of provisioning their own (see
# packages/testing/src/lmi/sharedCapacityProviderStack.ts).
setup-lmi-capacity-providers:
Comment thread
dreamorosi marked this conversation as resolved.
runs-on: ubuntu-latest
timeout-minutes: 60
env:
NODE_ENV: dev
environment: e2e-tests
permissions:
id-token: write # needed to interact with GitHub's OIDC Token endpoint.
contents: read
steps:
- name: Checkout Repo
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
- name: Setup dependencies
uses: aws-powertools/actions/.github/actions/cached-node-modules@3b5b8e2e58b7af07994be982e83584a94e8c76c5 # v1.5.0
with:
node-version: 24
- name: Setup AWS credentials
uses: aws-actions/configure-aws-credentials@517a711dbcd0e402f90c77e7e2f81e849156e31d # v6.2.2
with:
role-to-assume: ${{ secrets.E2E_IAM_ROLE_ARN }}
aws-region: eu-west-1
mask-aws-account-id: true
# The capacity provider ARNs are NOT passed to the test jobs as job
# outputs: they contain the AWS account id, which is masked, and GitHub
# silently drops job outputs containing masked values. The stack names
# are deterministic (LmiShared-<runId>-<arch>), so each test job
# resolves the ARN itself from the stack outputs.
- name: Deploy shared LMI capacity providers (all architectures)
run: npm run lmi:deploy -w packages/testing

# Runs every package's e2e suites EXCEPT the LMI ones (each package's
# `test:e2e` script excludes `tests/e2e/lmi.*`). Deliberately does NOT
# depend on the LMI setup job: these suites don't use the capacity
# provider, so they fan out immediately and its EC2-backed provisioning
# time is amortized behind them.
run-e2e-tests-on-utils:
runs-on: ubuntu-latest
env:
Expand Down Expand Up @@ -58,4 +102,113 @@ jobs:
ARCH: ${{ matrix.arch }}
JSII_SILENCE_WARNING_DEPRECATED_NODE_VERSION: true
RUNNER_DEBUG: ${{ env.RUNNER_DEBUG }}
run: npm run test:e2e -w ${{ matrix.package }}
run: npm run test:e2e -w ${{ matrix.package }}

# Runs only the LMI suites (`tests/e2e/lmi.*`, via each package's
# `test:e2e:lmi` script) for the packages that have them. Gated on the
# shared capacity provider being ready.
run-e2e-tests-lmi:
runs-on: ubuntu-latest
timeout-minutes: 60
needs: setup-lmi-capacity-providers
env:
NODE_ENV: dev
environment: e2e-tests
permissions:
id-token: write # needed to interact with GitHub's OIDC Token endpoint.
contents: read
strategy:
matrix:
package: [packages/logger]
version: [22, 24]
arch: [x86_64, arm64]
fail-fast: false
steps:
- name: Checkout Repo
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
- name: Setup dependencies
uses: aws-powertools/actions/.github/actions/cached-node-modules@3b5b8e2e58b7af07994be982e83584a94e8c76c5 # v1.5.0
with:
node-version: 24
- name: Setup AWS credentials
uses: aws-actions/configure-aws-credentials@517a711dbcd0e402f90c77e7e2f81e849156e31d # v6.2.2
with:
role-to-assume: ${{ secrets.E2E_IAM_ROLE_ARN }}
aws-region: eu-west-1
mask-aws-account-id: true
# The setup job's stack name is deterministic for a given run id and
# architecture, so the ARN is resolved from CloudFormation instead of a
# job output (job outputs containing the masked account id are silently
# dropped by GitHub). `>> $GITHUB_ENV` is safe: masking only redacts
# logs, not environment values.
- name: Resolve shared LMI capacity provider ARN
env:
ARCH: ${{ matrix.arch }}
run: |
arn=$(aws cloudformation describe-stacks \
--stack-name "LmiShared-${GITHUB_RUN_ID}-${ARCH//_/-}" \
--query "Stacks[0].Outputs[?OutputKey=='CapacityProviderArn'].OutputValue" \
--output text)
Comment thread
dreamorosi marked this conversation as resolved.
# `--output text` prints an empty string (exit 0) when the query
# matches nothing and `None` when the output is null. Either would be
# silently treated as "no shared provider", making every cell fall
# back to provisioning its own VPC + capacity provider and defeating
# the shared-provider design, so fail loudly instead.
if [[ -z "$arn" || "$arn" == "None" ]]; then
echo "::error::Could not resolve shared LMI capacity provider ARN for LmiShared-${GITHUB_RUN_ID}-${ARCH//_/-}"
exit 1
fi
echo "LMI_CAPACITY_PROVIDER_ARN=$arn" >> "$GITHUB_ENV"
- name: Run LMI e2e ${{ matrix.package }}-${{ matrix.version }}-${{ matrix.arch }}
env:
RUNTIME: nodejs${{ matrix.version }}x
CI: true
ARCH: ${{ matrix.arch }}
JSII_SILENCE_WARNING_DEPRECATED_NODE_VERSION: true
RUNNER_DEBUG: ${{ env.RUNNER_DEBUG }}
run: npm run test:e2e:lmi -w ${{ matrix.package }}

# Tears down the shared capacity provider stacks as soon as the LMI suites
# finish: the non-LMI matrix doesn't use them, so it doesn't hold them up.
# Retries: non-LMI job failures can be recovered with "re-run failed jobs"
# (those cells never touch the shared stacks). LMI job failures need
# "re-run all jobs": this teardown destroys the stacks at the end of each
# attempt, so a re-run LMI cell fails fast at ARN resolution ("stack does
# not exist") until the setup job re-runs and redeploys them.
teardown-lmi-capacity-providers:
Comment thread
dreamorosi marked this conversation as resolved.
runs-on: ubuntu-latest
timeout-minutes: 60
needs: [setup-lmi-capacity-providers, run-e2e-tests-lmi]
if: always()
env:
NODE_ENV: dev
environment: e2e-tests
permissions:
id-token: write # needed to interact with GitHub's OIDC Token endpoint.
contents: read
steps:
- name: Checkout Repo
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
- name: Setup dependencies
uses: aws-powertools/actions/.github/actions/cached-node-modules@3b5b8e2e58b7af07994be982e83584a94e8c76c5 # v1.5.0
with:
node-version: 24
- name: Setup AWS credentials
uses: aws-actions/configure-aws-credentials@517a711dbcd0e402f90c77e7e2f81e849156e31d # v6.2.2
with:
role-to-assume: ${{ secrets.E2E_IAM_ROLE_ARN }}
aws-region: eu-west-1
mask-aws-account-id: true
# The script destroys all architectures' stacks concurrently and
# attempts every teardown even if one fails; destroying a non-existent
# stack is a no-op
- name: Destroy shared LMI capacity providers (all architectures)
run: npm run lmi:destroy -w packages/testing
6 changes: 5 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 8 additions & 4 deletions packages/logger/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@
"test:unit:coverage": "vitest --run tests/unit --coverage.enabled --coverage.thresholds.100 --coverage.include='src/**'",
"test:unit:types": "echo 'Not Implemented'",
"test:unit:watch": "vitest tests/unit",
"test:e2e:nodejs22x": "RUNTIME=nodejs22x vitest --run tests/e2e",
"test:e2e:nodejs24x": "RUNTIME=nodejs24x vitest --run tests/e2e",
"test:e2e": "vitest --run tests/e2e",
"test:e2e:nodejs22x": "RUNTIME=nodejs22x vitest --run tests/e2e --exclude '**/tests/e2e/lmi.*'",
"test:e2e:nodejs24x": "RUNTIME=nodejs24x vitest --run tests/e2e --exclude '**/tests/e2e/lmi.*'",
"test:e2e": "vitest --run tests/e2e --exclude '**/tests/e2e/lmi.*'",
"test:e2e:lmi": "vitest --run tests/e2e/lmi.",
"build:cjs": "tsc --build tsconfig.cjs.json && echo '{ \"type\": \"commonjs\" }' > lib/cjs/package.json",
"build:esm": "tsc --build tsconfig.json && echo '{ \"type\": \"module\" }' > lib/esm/package.json",
"build:tests": "tsc --noEmit -p tests/tsconfig.json",
Expand Down Expand Up @@ -73,7 +74,10 @@
"types": "./lib/cjs/index.d.ts",
"main": "./lib/cjs/index.js",
"devDependencies": {
"@aws-lambda-powertools/testing-utils": "file:../testing"
"@aws-lambda-powertools/testing-utils": "file:../testing",
"@aws-sdk/client-lambda": "^3.1079.0",
"@types/promise-retry": "^1.1.3",
"promise-retry": "^2.0.1"
},
"peerDependencies": {
"@aws-lambda-powertools/jmespath": "2.34.0",
Expand Down
79 changes: 79 additions & 0 deletions packages/logger/tests/e2e/lmi.test.FunctionCode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { randomUUID } from 'node:crypto';
import { setTimeout } from 'node:timers/promises';
import { Logger } from '@aws-lambda-powertools/logger';
import type { Context } from 'aws-lambda';

// Module scope: identifies the execution environment across invocations
const executionEnvId = randomUUID();

// Capture the log lines the Logger emits so they can be returned in the
// response payload: on LMI the Invoke API does not support Tail logs and
// CloudWatch delivery is asynchronous, so returning the logs is the only
// fully deterministic way for the test to read them. In production mode the
// Logger writes each log line as a single atomic write to process.stdout
// (via its own Console instance, bypassing Lambda's patched global console),
// so intercepting the stream captures the real production write path.
const capturedLogs: Array<Record<string, unknown>> = [];
const originalWrite = process.stdout.write.bind(process.stdout);
process.stdout.write = ((chunk: string | Uint8Array, ...rest: unknown[]) => {
try {
capturedLogs.push(JSON.parse(chunk.toString()));
} catch {
// not a JSON log line, ignore
}
// @ts-expect-error - passing through the remaining overloaded args as-is
return originalWrite(chunk, ...rest);
}) as typeof process.stdout.write;

const logger = new Logger();

// Invocations multiplexed into the same execution environment share this
// module-scoped state, which lets us prove a genuine overlap: every
// invocation blocks until a second invocation is in flight in the same
// environment (or times out reporting that it stayed alone)
let inFlight = 0;
let barrier = Promise.withResolvers<void>();

export const handler = async (
event: { invocationId: string; role: 'warmup' | 'test' },
context: Context
) => {
logger.addContext(context);
logger.appendKeys({ invocationKey: event.invocationId });

let sawPeer = false;
if (event.role === 'test') {
inFlight++;
if (inFlight >= 2) {
barrier.resolve();
}
sawPeer = await Promise.race([
barrier.promise.then(() => true),
setTimeout(15_000, false),
]);
inFlight--;
if (inFlight === 0) {
barrier = Promise.withResolvers<void>();
}
}

logger.info('LMI isolation test');
logger.resetKeys();

return {
invocationId: event.invocationId,
executionEnvId,
sawPeer,
initializationType: process.env.AWS_LAMBDA_INITIALIZATION_TYPE ?? 'unset',
maxConcurrency: process.env.AWS_LAMBDA_MAX_CONCURRENCY ?? 'unset',
// Only the lines this invocation emitted, selected by the request id
// stamped on them. Under LMI multiplexing this only works because
// addContext scopes the lambda context per invocation via the
// InvokeStore (#5430) — an empty logs array here is the signature of
// that scoping regressing. The invocationKey assertion in the test
// then verifies appendKeys isolation on independently-selected lines.
logs: capturedLogs.filter(
(log) => log.function_request_id === context.awsRequestId
),
};
};
Loading
Loading