From d2f4bd8373f0cf72212734f54e03304fe7973284 Mon Sep 17 00:00:00 2001 From: Anthony Ting Date: Tue, 4 Nov 2025 19:19:15 -0800 Subject: [PATCH 1/4] ci: run unit and integration tests aginst more node versions --- .github/workflows/build.yml | 120 ++++++++++++++ .github/workflows/cleanup-pr-logs.yml | 9 +- .github/workflows/integration-tests.yml | 133 ++++----------- .../integration-test/integration-test.js | 154 +++++++++--------- .github/workflows/unit-tests.yml | 57 +++---- package-lock.json | 11 ++ package.json | 2 + .../package.json | 2 + .../scripts/deploy-lambda.ts | 100 +++++++++--- 9 files changed, 343 insertions(+), 245 deletions(-) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..1caabcc0 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,120 @@ +name: Build + +on: + push: + branches: ["main", "development"] + pull_request: + branches: ["main", "development"] + +permissions: + id-token: write + contents: read # This is required for actions/checkout + +# Cancel when pull request is updated +concurrency: + group: ${{ github.head_ref }}-${{ github.run_id}} + cancel-in-progress: true + +jobs: + lint-commits: + # Note: To re-run `lint-commits` after fixing the PR title, close-and-reopen the PR. + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Use Node.js 22.x + uses: actions/setup-node@v4 + with: + node-version: 22.x + cache: "npm" + - name: Check PR title + run: | + node "$GITHUB_WORKSPACE/.github/workflows/lintcommit.js" + + build: + needs: lint-commits + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Use Node.js 22.x + uses: actions/setup-node@v4 + with: + node-version: 22.x + cache: "npm" + - name: Install dependencies + run: npm run install-all + - name: Build project + run: npm run build + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: built-artifacts + path: | + packages/**/dist*/* + package.json + package-lock.json + + unit-tests: + needs: build + strategy: + matrix: + node-version: ["20.x", "22.x", "24.x"] + uses: "./.github/workflows/unit-tests.yml" + with: + node-version: ${{ matrix.node-version }} + + integration-tests: + needs: build + strategy: + matrix: + node-version: ["20.x", "22.x", "24.x"] + uses: "./.github/workflows/integration-tests.yml" + with: + node-version: ${{ matrix.node-version }} + secrets: inherit + + cleanup-integration-tests: + needs: [integration-tests] + runs-on: ubuntu-latest + + strategy: + max-parallel: 1 + matrix: + node-version: ["20.x", "22.x", "24.x"] + + if: ${{ !failure() }} + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: "npm" + + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: built-artifacts + path: . + + - name: Install dependencies + run: | + npm run install-all + + - name: Get AWS Credentials + id: credentials + if: github.actor != 'nektos/act' + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: "${{ secrets.ACTIONS_INTEGRATION_ROLE_NAME }}" + role-session-name: githubIntegrationTest + aws-region: ${{ vars.AWS_REGION }} + + - name: Cleanup Lambda functions + env: + LAMBDA_ENDPOINT: ${{ secrets.LAMBDA_ENDPOINT }} + GITHUB_EVENT_NAME: ${{ github.event_name }} + GITHUB_EVENT_NUMBER: ${{ github.event.number }} + run: | + node .github/workflows/scripts/integration-test/integration-test.js --cleanup-only --runtime ${{ matrix.node-version }} diff --git a/.github/workflows/cleanup-pr-logs.yml b/.github/workflows/cleanup-pr-logs.yml index 97f09054..0c79f933 100644 --- a/.github/workflows/cleanup-pr-logs.yml +++ b/.github/workflows/cleanup-pr-logs.yml @@ -10,9 +10,6 @@ permissions: id-token: write # This is required for requesting the JWT contents: read # This is required for actions/checkout -env: - AWS_REGION: ${{ vars.AWS_REGION }} - jobs: cleanup-logs: if: github.event.pull_request.merged == true @@ -24,7 +21,7 @@ jobs: with: role-to-assume: "${{ secrets.ACTIONS_WEBHOOK_ROLE_ARN }}" role-session-name: "GitHub-PR-${{ github.event.pull_request.number }}-Cleanup" - aws-region: ${{ env.AWS_REGION }} + aws-region: ${{ vars.AWS_REGION }} - name: Install awscurl run: pip install awscurl @@ -32,8 +29,8 @@ jobs: - name: Cleanup PR log group run: | awscurl --service execute-api \ - --region ${{ env.AWS_REGION }} \ + --region ${{ vars.AWS_REGION }} \ -X POST \ -H "Content-Type: application/json" \ - -d '{"logGroupSuffix": "-TypeScript-PR-${{ github.event.pull_request.number }}"}' \ + -d '{"logGroupSuffix": "-NodeJS-PR-${{ github.event.pull_request.number }}"}' \ "${{ secrets.LOG_GROUP_CLEANUP_WEBHOOK }}" diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 51938266..1c061a17 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -1,84 +1,62 @@ name: Integration Tests on: - push: - branches: ["main", "development"] - paths: - - "packages/aws-durable-execution-**" - - "packages/client-lambda/**" - - ".github/workflows/integration-tests.yml" - - ".github/workflows/scripts/integration-test/**" - - ".github/model/**" - pull_request: - branches: ["main", "development"] - paths: - - "packages/aws-durable-execution-**" - - "packages/client-lambda/**" - - ".github/workflows/integration-tests.yml" - - ".github/workflows/scripts/integration-test/**" - - ".github/model/**" - workflow_dispatch: - -env: - AWS_REGION: ${{ vars.AWS_REGION }} - -# permission can be added at job level or workflow level + workflow_call: + inputs: + node-version: + required: true + type: string + permissions: - id-token: write # This is required for requesting the JWT - contents: read # This is required for actions/checkout + id-token: write # Required for AWS credentials + contents: read # Required for actions/checkout + +# Cancel when pull request is updated for this node version +concurrency: + group: ${{ github.head_ref }}-${{ github.run_id }}-${{ inputs.node-version }}-integ + cancel-in-progress: true jobs: - setup: + deploy: runs-on: ubuntu-latest - outputs: - function-name-map: ${{ steps.deploy-functions.outputs.function-name-map }} steps: - uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: "22.x" + node-version: ${{ inputs.node-version }} cache: "npm" - - name: Configure AWS credentials (OIDC for main workflow) - if: github.event_name != 'workflow_dispatch' || github.actor != 'nektos/act' + - name: Get AWS Credentials + id: credentials + if: github.actor != 'nektos/act' uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: "${{ secrets.ACTIONS_INTEGRATION_ROLE_NAME }}" role-session-name: githubIntegrationTest - aws-region: ${{ env.AWS_REGION }} + aws-region: ${{ vars.AWS_REGION }} - - name: Install custom Lambda model - run: | - aws configure add-model --service-model file://.github/model/lambda.json --service-name lambda + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: built-artifacts + path: . - name: Install dependencies run: npm run install-all - - name: Build project - run: npm run build - - - name: Upload build artifacts - uses: actions/upload-artifact@v4 - with: - name: built-artifacts - path: | - packages/**/dist*/* - package.json - - name: Deploy functions + id: deploy-functions env: - AWS_ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }} LAMBDA_ENDPOINT: ${{ secrets.LAMBDA_ENDPOINT }} - INVOKE_ACCOUNT_ID: ${{ secrets.INVOKE_ACCOUNT_ID_BETA }} - KMS_KEY_ARN: ${{ secrets.KMS_KEY_ARN }} GITHUB_EVENT_NAME: ${{ github.event_name }} GITHUB_EVENT_NUMBER: ${{ github.event.number }} - run: node .github/workflows/scripts/integration-test/integration-test.js --deploy-only + AWS_ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }} + run: node .github/workflows/scripts/integration-test/integration-test.js --deploy-only --runtime ${{ inputs.node-version }} jest-integration-test: - needs: setup + needs: deploy runs-on: ubuntu-latest name: Jest Integration Tests @@ -88,16 +66,17 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: "22.x" + node-version: ${{ inputs.node-version }} cache: "npm" - - name: Configure AWS credentials (OIDC for main workflow) - if: github.event_name != 'workflow_dispatch' || github.actor != 'nektos/act' + - name: Get AWS Credentials + id: credentials + if: github.actor != 'nektos/act' uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: "${{ secrets.ACTIONS_INTEGRATION_ROLE_NAME }}" role-session-name: githubIntegrationTest - aws-region: ${{ env.AWS_REGION }} + aws-region: ${{ vars.AWS_REGION }} - name: Download build artifacts uses: actions/download-artifact@v4 @@ -112,51 +91,7 @@ jobs: - name: Run Jest integration tests env: LAMBDA_ENDPOINT: ${{ secrets.LAMBDA_ENDPOINT }} - FUNCTION_NAME_MAP: ${{ needs.setup.outputs.function-name-map }} GITHUB_EVENT_NAME: ${{ github.event_name }} GITHUB_EVENT_NUMBER: ${{ github.event.number }} - AWS_ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }} - run: | - node .github/workflows/scripts/integration-test/integration-test.js --test-only - - cleanup: - needs: [setup, jest-integration-test] - runs-on: ubuntu-latest - if: ${{ !failure() }} - - steps: - - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: "22.x" - cache: "npm" - - - name: Download build artifacts - uses: actions/download-artifact@v4 - with: - name: built-artifacts - path: . - - - name: Install dependencies - run: | - npm run install-all - - - name: Configure AWS credentials (OIDC for main workflow) - if: github.event_name != 'workflow_dispatch' || github.actor != 'nektos/act' - uses: aws-actions/configure-aws-credentials@v4 - with: - role-to-assume: "${{ secrets.ACTIONS_INTEGRATION_ROLE_NAME }}" - role-session-name: githubIntegrationTest - aws-region: ${{ env.AWS_REGION }} - - - name: Cleanup Lambda functions - env: - LAMBDA_ENDPOINT: ${{ secrets.LAMBDA_ENDPOINT }} - FUNCTION_NAME_MAP: ${{ needs.setup.outputs.function-name-map }} - GITHUB_EVENT_NAME: ${{ github.event_name }} - GITHUB_EVENT_NUMBER: ${{ github.event.number }} - AWS_ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }} run: | - node .github/workflows/scripts/integration-test/integration-test.js --cleanup-only + node .github/workflows/scripts/integration-test/integration-test.js --test-only --runtime ${{ inputs.node-version }} diff --git a/.github/workflows/scripts/integration-test/integration-test.js b/.github/workflows/scripts/integration-test/integration-test.js index a2bd1392..8e954d5a 100644 --- a/.github/workflows/scripts/integration-test/integration-test.js +++ b/.github/workflows/scripts/integration-test/integration-test.js @@ -4,6 +4,7 @@ import { execSync } from "child_process"; import { appendFileSync } from "fs"; import { join, dirname } from "path"; import { fileURLToPath } from "url"; +import { ArgumentParser } from "argparse"; import examplesCatalog from "@aws/durable-execution-sdk-js-examples/catalog"; import { @@ -49,20 +50,17 @@ const CONFIG = { __dirname, "../../../../packages/aws-durable-execution-sdk-js-examples", ), - AWS_ACCOUNT_ID: process.env.AWS_ACCOUNT_ID, }; -if (!CONFIG.AWS_ACCOUNT_ID) { - throw new Error("AWS_ACCOUNT_ID environment variable must be set."); -} - class IntegrationTestRunner { /** * @param {Object} options * @param {boolean} [options.cleanupOnExit] + * @param {string} options.runtime */ - constructor(options = {}) { + constructor(options) { this.cleanupOnExit = options.cleanupOnExit !== false; + this.runtime = options.runtime; this.isGitHubActions = !!process.env.GITHUB_ACTIONS; /** @type {Record | undefined} */ this.functionNameMap = undefined; @@ -145,27 +143,33 @@ class IntegrationTestRunner { /** @type {Record} */ const functionNameMap = {}; + // Get runtime suffix from argument or environment variable + const lambdaRuntime = this.runtime.replace(".", ""); + for (const example of examples) { const exampleName = example.name; const exampleHandler = example.handler; - // Build function name + // Build function name with runtime suffix let functionName; if (this.isGitHubActions) { - const baseName = exampleName.replace(/\s/g, "") + "-TypeScript"; + // Functions are named with the runtime first since the log scrubber cleans logs by the NodeJS- suffix + const baseName = + exampleName.replace(/\s/g, "") + `-${lambdaRuntime}-NodeJS`; if (process.env.GITHUB_EVENT_NAME === "pull_request") { if (!process.env.GITHUB_EVENT_NUMBER) { throw new Error( "Could not find GITHUB_EVENT_NUMBER environment variable", ); } - functionName = `arn:aws:lambda:${CONFIG.AWS_REGION}:${CONFIG.AWS_ACCOUNT_ID}:${baseName}-PR-${process.env.GITHUB_EVENT_NUMBER}`; + functionName = `${baseName}-PR-${process.env.GITHUB_EVENT_NUMBER}`; } else { - functionName = `arn:aws:lambda:${CONFIG.AWS_REGION}:${CONFIG.AWS_ACCOUNT_ID}:${baseName}`; + functionName = baseName; } } else { - const name = exampleName.replace(/\s/g, "") + "-TypeScript-Local"; - functionName = `arn:aws:lambda:${CONFIG.AWS_REGION}:${CONFIG.AWS_ACCOUNT_ID}:${name}`; + const name = + exampleName.replace(/\s/g, "") + `-${lambdaRuntime}-NodeJS-Local`; + functionName = name; } const handlerFile = exampleHandler.replace(/\.handler$/, ""); @@ -180,6 +184,10 @@ class IntegrationTestRunner { async deployFunctions() { log.info("Deploying Lambda functions..."); + if (!process.env.AWS_ACCOUNT_ID) { + throw new Error("Missing required AWS_ACCOUNT_ID for deployment"); + } + const examples = this.getIntegrationExamples(); const examplesDir = CONFIG.EXAMPLES_PACKAGE_PATH; @@ -199,8 +207,10 @@ class IntegrationTestRunner { cwd: examplesDir, }); - // Deploy using npm script - this.execCommand(`npm run deploy -- "${handlerFile}" '${functionName}'`, { + // Deploy using npm script with runtime parameter + const deployCommand = `npm run deploy -- "${handlerFile}" '${functionName}' --runtime ${this.runtime}`; + + this.execCommand(deployCommand, { cwd: examplesDir, }); log.success(`Deployed function: ${functionName}`); @@ -228,21 +238,20 @@ class IntegrationTestRunner { const examplesDir = CONFIG.EXAMPLES_PACKAGE_PATH; - const functionsWithLatestArn = Object.fromEntries( + const functionsWithQualifier = Object.fromEntries( Object.entries(this.getFunctionNameMap()).map(([key, value]) => { return [key, `${value}:$LATEST`]; }), ); - // Set environment variables + // Set additional environment variables const env = { - ...process.env, - FUNCTION_NAME_MAP: JSON.stringify(functionsWithLatestArn), + FUNCTION_NAME_MAP: JSON.stringify(functionsWithQualifier), LAMBDA_ENDPOINT: CONFIG.LAMBDA_ENDPOINT, }; log.info("Running Jest integration tests with function map:"); - console.log(JSON.stringify(functionsWithLatestArn, null, 2)); + console.log(JSON.stringify(functionsWithQualifier, null, 2)); log.info(`Lambda Endpoint: ${CONFIG.LAMBDA_ENDPOINT}`); // Build test command with optional pattern @@ -337,72 +346,61 @@ class IntegrationTestRunner { } } -// CLI interface -function showUsage() { - console.log("Usage: node integration-test.js [OPTIONS] [TEST_PATTERN]"); - console.log(""); - console.log("Options:"); - console.log(" --deploy-only Only deploy functions, don't run tests"); - console.log( - " --test-only [pattern] Only run tests (assumes functions are already deployed)", - ); - console.log( - " Optional test pattern to filter specific tests", - ); - console.log(" --cleanup-only Only cleanup existing functions"); - console.log(" --help Show this help message"); - console.log(""); - console.log("Examples:"); - console.log(" node integration-test.js --test-only my-test-name"); - console.log(" node integration-test.js --test-only"); - console.log(""); - console.log("Environment Variables:"); - console.log(" AWS_REGION AWS region (default: us-west-2)"); - console.log(" LAMBDA_ENDPOINT Custom Lambda endpoint URL"); - console.log(""); -} - async function main() { - const args = process.argv.slice(2); + // Set up argument parser + const parser = new ArgumentParser({ + description: "Integration test runner for Lambda Durable Functions SDK", + epilog: `Environment Variables: + AWS_REGION AWS region (default: us-east-1) + LAMBDA_ENDPOINT Custom Lambda endpoint URL`, + }); + + // Add mutually exclusive group for operation modes + const group = parser.add_mutually_exclusive_group(); + + group.add_argument("--deploy-only", { + action: "store_true", + help: "Only deploy functions, don't run tests", + }); + + group.add_argument("--test-only", { + action: "store_true", + help: "Only run tests (assumes functions are already deployed)", + }); + + group.add_argument("--cleanup-only", { + action: "store_true", + help: "Only cleanup existing functions", + }); + + // Add test pattern argument + parser.add_argument("--test-pattern", { + help: "Optional test pattern to filter specific tests (used with --test-only)", + }); + + // Add runtime argument + parser.add_argument("--runtime", { + help: "Node runtime version (e.g., 20.x, 22.x, 24.x)", + default: "22.x", + required: true, + }); // Parse command line arguments - /** @type {{ cleanupOnExit: boolean, deployOnly: boolean, testOnly: boolean, cleanupOnly: boolean, testPattern?: string }} */ + const args = parser.parse_args(); + + // Configure options based on parsed arguments const options = { cleanupOnExit: true, - deployOnly: false, - testOnly: false, - cleanupOnly: false, - testPattern: undefined, + deployOnly: args.deploy_only || false, + testOnly: args.test_only || false, + cleanupOnly: args.cleanup_only || false, + testPattern: args.test_pattern, + runtime: args.runtime, }; - for (let i = 0; i < args.length; i++) { - const arg = args[i]; - - switch (arg) { - case "--deploy-only": - options.deployOnly = true; - options.cleanupOnExit = false; - break; - case "--test-only": - options.testOnly = true; - options.cleanupOnExit = false; - // Check if the next argument is a test pattern (not another flag) - if (i + 1 < args.length && !args[i + 1].startsWith("--")) { - options.testPattern = args[i + 1]; - i++; // Skip the next argument since we consumed it as test pattern - } - break; - case "--cleanup-only": - options.cleanupOnly = true; - break; - case "--help": - showUsage(); - process.exit(0); - default: - log.error(`Unknown option: ${arg}`); - showUsage(); - process.exit(1); - } + // Disable cleanup on exit for deploy-only and test-only modes + if (options.deployOnly || options.testOnly) { + options.cleanupOnExit = false; } const runner = new IntegrationTestRunner(options); diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index e6ab3131..60143ff5 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -1,55 +1,36 @@ -# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs - name: Unit Tests on: - push: - branches: ["main", "development"] - pull_request: - branches: ["main", "development"] + workflow_call: + inputs: + node-version: + required: true + type: string -# permission can be added at job level or workflow level permissions: - contents: read # This is required for actions/checkout + contents: read # Required for actions/checkout -# Cancel old jobs when a pull request is updated. +# Cancel when pull request is updated for this node version concurrency: - group: ${{ github.head_ref || github.run_id }} + group: ${{ github.head_ref }}-${{ github.run_id}}-${{ inputs.node-version }}-unit cancel-in-progress: true jobs: - lint-commits: - # Note: To re-run `lint-commits` after fixing the PR title, close-and-reopen the PR. + test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Use Node.js ${{ matrix.node-version }} + - name: Use Node.js ${{ inputs.node-version }} uses: actions/setup-node@v4 with: - node-version: ${{ matrix.node-version }} + node-version: ${{ inputs.node-version }} cache: "npm" - - name: Check PR title - run: | - node "$GITHUB_WORKSPACE/.github/workflows/lintcommit.js" - - build: - needs: lint-commits - - runs-on: ubuntu-latest - - strategy: - matrix: - node-version: [22.x] - # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ - - steps: - - uses: actions/checkout@v4 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 + - name: Download build artifacts + uses: actions/download-artifact@v4 with: - node-version: ${{ matrix.node-version }} - cache: "npm" - - run: npm run install-all - - run: npm run build - - run: npm run test + name: built-artifacts + path: . + - name: Install dependencies + run: npm run install-all + - name: Run tests + run: npm run test diff --git a/package-lock.json b/package-lock.json index 20ae2ddd..c6fee1b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,12 +17,14 @@ "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-typescript": "^12.1.4", "@tsconfig/node20": "^20.1.5", + "@types/argparse": "^2.0.17", "@types/aws-lambda": "^8.10.150", "@types/express": "^5.0.2", "@types/jest": "^30.0.0", "@types/morgan": "^1.9.10", "@types/supertest": "^6.0.3", "@typescript-eslint/parser": "^8.44.0", + "argparse": "^2.0.1", "concurrently": "^9.2.1", "eslint": "^9.29.0", "eslint-config-prettier": "^10.1.5", @@ -3862,6 +3864,13 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/argparse": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-2.0.17.tgz", + "integrity": "sha512-fueJssTf+4dW4HODshEGkIZbkLKHzgu1FvCI4cTc/MKum/534Euo3SrN+ilq8xgyHnOjtmg33/hee8iXLRg1XA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/aws-lambda": { "version": "8.10.152", "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.152.tgz", @@ -13812,10 +13821,12 @@ "@rollup/plugin-commonjs": "^28.0.6", "@rollup/plugin-node-resolve": "^16.0.2", "@tsconfig/node22": "^22.0.1", + "@types/argparse": "^2.0.17", "@types/aws-lambda": "^8.10.145", "@types/jest": "^29.5.14", "@types/js-yaml": "^4.0.9", "@types/node": "^22.13.5", + "argparse": "^2.0.1", "eslint": "^9.23.0", "jest": "^29.7.0", "js-yaml": "^4.1.0", diff --git a/package.json b/package.json index 084f116b..b1f4a99d 100644 --- a/package.json +++ b/package.json @@ -36,12 +36,14 @@ "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-typescript": "^12.1.4", "@tsconfig/node20": "^20.1.5", + "@types/argparse": "^2.0.17", "@types/aws-lambda": "^8.10.150", "@types/express": "^5.0.2", "@types/jest": "^30.0.0", "@types/morgan": "^1.9.10", "@types/supertest": "^6.0.3", "@typescript-eslint/parser": "^8.44.0", + "argparse": "^2.0.1", "concurrently": "^9.2.1", "eslint": "^9.29.0", "eslint-config-prettier": "^10.1.5", diff --git a/packages/aws-durable-execution-sdk-js-examples/package.json b/packages/aws-durable-execution-sdk-js-examples/package.json index 03bbc7ad..b3bff113 100644 --- a/packages/aws-durable-execution-sdk-js-examples/package.json +++ b/packages/aws-durable-execution-sdk-js-examples/package.json @@ -54,10 +54,12 @@ "@rollup/plugin-commonjs": "^28.0.6", "@rollup/plugin-node-resolve": "^16.0.2", "@tsconfig/node22": "^22.0.1", + "@types/argparse": "^2.0.17", "@types/aws-lambda": "^8.10.145", "@types/jest": "^29.5.14", "@types/js-yaml": "^4.0.9", "@types/node": "^22.13.5", + "argparse": "^2.0.1", "eslint": "^9.23.0", "jest": "^29.7.0", "js-yaml": "^4.1.0", diff --git a/packages/aws-durable-execution-sdk-js-examples/scripts/deploy-lambda.ts b/packages/aws-durable-execution-sdk-js-examples/scripts/deploy-lambda.ts index d4a4dd99..4ab6bed3 100644 --- a/packages/aws-durable-execution-sdk-js-examples/scripts/deploy-lambda.ts +++ b/packages/aws-durable-execution-sdk-js-examples/scripts/deploy-lambda.ts @@ -3,6 +3,7 @@ import { readFileSync, existsSync } from "fs"; import { resolve } from "path"; import { config as dotenvConfig } from "dotenv"; +import { ArgumentParser } from "argparse"; import { LambdaClient, GetFunctionCommand, @@ -12,9 +13,12 @@ import { UpdateFunctionConfigurationCommand, Runtime, GetFunctionConfigurationCommandOutput, + ResourceNotFoundException, + ResourceConflictException, + UpdateFunctionConfigurationCommandInput, } from "@aws-sdk/client-lambda"; import { ExamplesWithConfig } from "../src/types"; -import catalog from "../src/utils/examples-catalog"; +import catalog from "@aws/durable-execution-sdk-js-examples/catalog"; // Types interface EnvironmentVariables { @@ -26,20 +30,37 @@ interface EnvironmentVariables { } // Configuration and validation -function validateArgs(): { example: string; functionName: string } { - const args = process.argv.slice(2); +function parseArgs(): { + example: string; + functionName: string; + runtime?: string; +} { + const parser = new ArgumentParser({ + description: "Deploy Lambda function with AWS Durable Execution SDK", + add_help: true, + }); - if (args.length < 1 || args.length > 2) { - console.error("Usage: deploy-lambda.ts [function-name]"); - console.error("Example: deploy-lambda.ts hello-world"); - console.error("Example: deploy-lambda.ts hello-world custom-function-name"); - process.exit(1); - } + parser.add_argument("example", { + help: "Example name to deploy (e.g., hello-world)", + }); + + parser.add_argument("function_name", { + nargs: "?", + help: "Custom function name (defaults to example name)", + }); + + parser.add_argument("--runtime", { + choices: ["20.x", "22.x", "24.x"], + help: "Lambda nodejs runtime version (default: 24.x)", + }); - const example = args[0]; - const functionName = args[1] || example; + const args = parser.parse_args(); - return { example, functionName }; + return { + example: args.example, + functionName: args.function_name || args.example, + runtime: args.runtime, + }; } function loadEnvironmentVariables(): EnvironmentVariables { @@ -103,6 +124,25 @@ function validateZipFile(exampleName: string): void { } } +function mapRuntimeToEnum(runtimeString?: string): Runtime { + if (!runtimeString) { + return Runtime.nodejs22x; // Default runtime + } + + switch (runtimeString) { + case "20.x": + return Runtime.nodejs20x; + case "22.x": + return Runtime.nodejs22x; + case "24.x": + return "nodejs24.x" as Runtime; + default: + console.error(`Invalid runtime: ${runtimeString}`); + console.error("Available runtimes: 20x, 22x, 24x"); + process.exit(1); + } +} + // Lambda operations async function checkFunctionExists( lambdaClient: LambdaClient, @@ -113,8 +153,8 @@ async function checkFunctionExists( new GetFunctionCommand({ FunctionName: functionName }), ); return true; - } catch (error: any) { - if (error.name === "ResourceNotFoundException") { + } catch (error: unknown) { + if (error instanceof ResourceNotFoundException) { return false; } throw error; @@ -128,9 +168,9 @@ async function retryOnConflict( for (let attempt = 0; attempt < maxRetries; attempt++) { try { return await operation(); - } catch (error: any) { + } catch (error: unknown) { if ( - error.name === "ResourceConflictException" && + error instanceof ResourceConflictException && attempt < maxRetries - 1 ) { await new Promise((resolve) => setTimeout(resolve, 1000)); @@ -158,15 +198,18 @@ async function createFunction( exampleConfig: ExamplesWithConfig, zipFile: string, env: EnvironmentVariables, + runtime?: Runtime, ): Promise { - console.log(`Deploying function: ${functionName} (creating new)`); + console.log( + `Deploying function: ${functionName} (creating new) with runtime: ${runtime}`, + ); const zipBuffer = readFileSync(zipFile); const roleArn = `arn:aws:iam::${env.AWS_ACCOUNT_ID}:role/DurableFunctionsIntegrationTestRole`; - const createParams: any = { + const createParams = { FunctionName: functionName, - Runtime: Runtime.nodejs22x, + Runtime: runtime, Role: roleArn, Handler: exampleConfig.handler, Description: exampleConfig.description, @@ -182,7 +225,7 @@ async function createFunction( AWS_ENDPOINT_URL_LAMBDA: env.LAMBDA_ENDPOINT, }, }, - }; + } as const; const command = new CreateFunctionCommand(createParams); await lambdaClient.send(command); @@ -195,7 +238,8 @@ async function updateFunction( exampleConfig: ExamplesWithConfig, zipFile: string, env: EnvironmentVariables, - currentConfig: any, + currentConfig: GetFunctionConfigurationCommandOutput, + runtime?: Runtime, ): Promise { console.log(`Deploying function: ${functionName} (updating existing)`); @@ -219,12 +263,13 @@ async function updateFunction( FunctionName: functionName, ZipFile: zipBuffer, }); - await lambdaClient.send(updateCodeCommand); + await retryOnConflict(() => lambdaClient.send(updateCodeCommand)); // Update environment variables console.log("Updating environment variables..."); - const updateEnvParams: any = { + const updateEnvParams: UpdateFunctionConfigurationCommandInput = { FunctionName: functionName, + Runtime: runtime, Environment: { Variables: { AWS_ENDPOINT_URL_LAMBDA: env.LAMBDA_ENDPOINT, @@ -272,7 +317,7 @@ async function showFinalConfiguration( async function main(): Promise { try { // Parse arguments and load configuration - const { example, functionName } = validateArgs(); + const { example, functionName, runtime } = parseArgs(); const env = loadEnvironmentVariables(); const exampleConfig = loadExampleConfiguration(example); @@ -281,6 +326,9 @@ async function main(): Promise { console.log(` Function Name: ${functionName}`); console.log(` Handler: ${exampleConfig.handler}`); console.log(` Description: ${exampleConfig.description}`); + if (runtime) { + console.log(` Runtime: ${runtime}`); + } console.log( ` Retention: ${exampleConfig.durableConfig.RetentionPeriodInDays} days`, ); @@ -305,6 +353,8 @@ async function main(): Promise { const zipFile = `${example}.zip`; + const selectedRuntime = mapRuntimeToEnum(runtime); + if (functionExists) { const currentConfig = await getCurrentConfiguration( lambdaClient, @@ -317,6 +367,7 @@ async function main(): Promise { zipFile, env, currentConfig, + selectedRuntime, ); } else { console.log("Function does not exist"); @@ -326,6 +377,7 @@ async function main(): Promise { exampleConfig, zipFile, env, + selectedRuntime, ); } From 451697fa51607fe2dc186f03e1317d103b4d1564 Mon Sep 17 00:00:00 2001 From: Anthony Ting Date: Wed, 12 Nov 2025 16:16:47 -0800 Subject: [PATCH 2/4] scope down id-token: write permissions to only jobs that need it --- .github/workflows/build.yml | 7 ++++++- .github/workflows/cleanup-pr-logs.yml | 4 +++- .github/workflows/integration-tests.yml | 10 ++++++---- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1caabcc0..f08fd2bc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,6 @@ on: branches: ["main", "development"] permissions: - id-token: write contents: read # This is required for actions/checkout # Cancel when pull request is updated @@ -64,6 +63,9 @@ jobs: integration-tests: needs: build + permissions: + contents: read + id-token: write strategy: matrix: node-version: ["20.x", "22.x", "24.x"] @@ -75,6 +77,9 @@ jobs: cleanup-integration-tests: needs: [integration-tests] runs-on: ubuntu-latest + permissions: + contents: read + id-token: write strategy: max-parallel: 1 diff --git a/.github/workflows/cleanup-pr-logs.yml b/.github/workflows/cleanup-pr-logs.yml index 0c79f933..02bd1885 100644 --- a/.github/workflows/cleanup-pr-logs.yml +++ b/.github/workflows/cleanup-pr-logs.yml @@ -7,13 +7,15 @@ on: - development permissions: - id-token: write # This is required for requesting the JWT contents: read # This is required for actions/checkout jobs: cleanup-logs: if: github.event.pull_request.merged == true runs-on: ubuntu-latest + permissions: + contents: read + id-token: write # Required for AWS credentials steps: - name: Configure AWS credentials diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 1c061a17..8637c090 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -7,10 +7,6 @@ on: required: true type: string -permissions: - id-token: write # Required for AWS credentials - contents: read # Required for actions/checkout - # Cancel when pull request is updated for this node version concurrency: group: ${{ github.head_ref }}-${{ github.run_id }}-${{ inputs.node-version }}-integ @@ -18,6 +14,9 @@ concurrency: jobs: deploy: + permissions: + contents: read + id-token: write # Required for AWS credentials runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -59,6 +58,9 @@ jobs: needs: deploy runs-on: ubuntu-latest name: Jest Integration Tests + permissions: + contents: read + id-token: write # Required for AWS credentials steps: - uses: actions/checkout@v4 From f1e63ff537901497fff7d4011b08cdfb56beef67 Mon Sep 17 00:00:00 2001 From: Anthony Ting Date: Wed, 12 Nov 2025 16:47:31 -0800 Subject: [PATCH 3/4] disable failing fast for matrix configurations --- .github/workflows/build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f08fd2bc..b6a2d34d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -55,6 +55,7 @@ jobs: unit-tests: needs: build strategy: + fail-fast: false matrix: node-version: ["20.x", "22.x", "24.x"] uses: "./.github/workflows/unit-tests.yml" @@ -67,6 +68,7 @@ jobs: contents: read id-token: write strategy: + fail-fast: false matrix: node-version: ["20.x", "22.x", "24.x"] uses: "./.github/workflows/integration-tests.yml" @@ -82,7 +84,9 @@ jobs: id-token: write strategy: + # Clean up functions sequentially to avoid throttling max-parallel: 1 + fail-fast: false matrix: node-version: ["20.x", "22.x", "24.x"] From 2a54f0b0cb438bd5fb803efa989931073b27b692 Mon Sep 17 00:00:00 2001 From: Anthony Ting Date: Wed, 12 Nov 2025 17:22:17 -0800 Subject: [PATCH 4/4] use same concurrency group as integ tests for cleanup script --- .github/workflows/build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b6a2d34d..b82c365d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -83,6 +83,10 @@ jobs: contents: read id-token: write + concurrency: + group: ${{ github.head_ref }}-${{ github.run_id }}-${{ matrix.node-version }}-integ + cancel-in-progress: true + strategy: # Clean up functions sequentially to avoid throttling max-parallel: 1