diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..b82c365d --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,133 @@ +name: Build + +on: + push: + branches: ["main", "development"] + pull_request: + branches: ["main", "development"] + +permissions: + 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: + fail-fast: false + 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 + permissions: + contents: read + id-token: write + strategy: + fail-fast: false + 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 + permissions: + 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 + fail-fast: false + 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..02bd1885 100644 --- a/.github/workflows/cleanup-pr-logs.yml +++ b/.github/workflows/cleanup-pr-logs.yml @@ -7,16 +7,15 @@ on: - development 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 runs-on: ubuntu-latest + permissions: + contents: read + id-token: write # Required for AWS credentials steps: - name: Configure AWS credentials @@ -24,7 +23,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 +31,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..8637c090 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -1,86 +1,66 @@ 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: + workflow_call: + inputs: + node-version: + required: true + type: string -env: - AWS_REGION: ${{ vars.AWS_REGION }} - -# permission can be added at job level or workflow level -permissions: - id-token: write # This is required for requesting the JWT - contents: read # This is 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: + permissions: + contents: read + id-token: write # Required for AWS credentials 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 + permissions: + contents: read + id-token: write # Required for AWS credentials steps: - uses: actions/checkout@v4 @@ -88,16 +68,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 +93,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, ); }