Repository: pwrdrvr/microapps-core (language: python)

Issue:
v0.3 - publish/deployer - Allow passing Lambda Version or Alias and skipping related steps
## Motivations

- Early versions created the version/alias in the deployer only because I did not know (or it maybe wasn't possible at the time) how to create a Version or Alias reliably in CDK
- Now it's quite easy to create a version in a CDK-deployed Lambda function and only when the code changes
- Most users should be creating their own versions
- Some users will also assign their own aliases
- The publish/deployer tools can just detect whether they were passed a version or alias and skip the related steps

## To-Do

- [x] microapps-publish - check for version or alias ARN
- [x] microapps-publish - if version, skip version step
- [x] microapps-publish - if alias, skip version and alias steps
- [x] publish tool - update documentation to indicate that this can be a version or an alias and the suggestion is for it to be a version (let microapps create the alias with the function url configured with IAM)


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index be2a58a7..ecd8d852 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -233,7 +233,17 @@ jobs:
           echo "DEPLOYER_LAMBDA_NAME="$(aws cloudformation list-exports --query "Exports[?Name==\`${{ matrix.deployName }}-ghpublic-${NODE_ENV}${{ needs.build.outputs.prSuffix }}-deployer-func-name\`].Value" --no-paginate --output text) >> $GITHUB_ENV
           echo "DEMO_APP_LAMBDA_NAME="$(aws cloudformation list-exports --query "Exports[?Name==\`${{ matrix.deployName }}-ghpublic-${NODE_ENV}${{ needs.build.outputs.prSuffix }}-demo-app-func-name\`].Value" --no-paginate --output text) >> $GITHUB_ENV
           echo "NEXTJS_DEMO_APP_LAMBDA_NAME="$(aws cloudformation list-exports --query "Exports[?Name==\`${{ matrix.deployName }}-ghpublic-${NODE_ENV}${{ needs.build.outputs.prSuffix }}-nextjs-demo-app-func-name\`].Value" --no-paginate --output text) >> $GITHUB_ENV
+          echo "NEXTJS_DEMO_APP_LAMBDA_VERSION_ARN="$(aws cloudformation list-exports --query "Exports[?Name==\`${{ matrix.deployName }}-ghpublic-${NODE_ENV}${{ needs.build.outputs.prSuffix }}-nextjs-demo-app-vers-arn\`].Value" --no-paginate --output text) >> $GITHUB_ENV
           echo "RELEASE_APP_LAMBDA_NAME="$(aws cloudformation list-exports --query "Exports[?Name==\`${{ matrix.deployName }}-ghpublic-${NODE_ENV}${{ needs.build.outputs.prSuffix }}-release-app-func-name\`].Value" --no-paginate --output text) >> $GITHUB_ENV
+          echo "RELEASE_APP_LAMBDA_VERSION_ARN="$(aws cloudformation list-exports --query "Exports[?Name==\`${{ matrix.deployName }}-ghpublic-${NODE_ENV}${{ needs.build.outputs.prSuffix }}-release-app-vers-arn\`].Value" --no-paginate --output text) >> $GITHUB_ENV
+
+      - name: Echo Exports
+        run: |
+          env | grep EDGE_DOMAIN
+          env | grep DEPLOYER_
+          env | grep DEMO_APP_
+          env | grep NEXTJS_DEMO_APP_
+          env | grep RELEASE_APP_
 
       - name: Publish Demo App to MicroApps
         run: |
@@ -327,7 +337,7 @@ jobs:
             -a ${NEXTJS_DEMO_APP_NAME} \
             -n ${{ needs.build.outputs.nextjsDemoAppPackageVersion }} \
             -d ${DEPLOYER_LAMBDA_NAME} \
-            -l ${NEXTJS_DEMO_APP_LAMBDA_NAME} \
+            -l ${NEXTJS_DEMO_APP_LAMBDA_VERSION_ARN} \
             -s packages/cdk/node_modules/@pwrdrvr/microapps-app-nextjs-demo-cdk/lib/microapps-app-nextjs-demo/.static_files/ \
             --overwrite --noCache
 
@@ -358,7 +368,7 @@ jobs:
             -a ${RELEASE_APP_NAME} \
             -n ${{ needs.build.outputs.releaseAppPackageVersion }} \
             -d ${DEPLOYER_LAMBDA_NAME} \
-            -l ${RELEASE_APP_LAMBDA_NAME} \
+            -l ${RELEASE_APP_LAMBDA_VERSION_ARN} \
             -s packages/cdk/node_modules/@pwrdrvr/microapps-app-release-cdk/lib/microapps-app-release/.static_files/ \
             --overwrite --noCache
 
diff --git a/packages/cdk/lib/MicroApps.ts b/packages/cdk/lib/MicroApps.ts
index 1022f0aa..4aee8d61 100644
--- a/packages/cdk/lib/MicroApps.ts
+++ b/packages/cdk/lib/MicroApps.ts
@@ -1,4 +1,4 @@
-import { Aws, CfnOutput, Duration, RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib';
+import { CfnOutput, RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib';
 import { Construct } from 'constructs';
 import * as acm from 'aws-cdk-lib/aws-certificatemanager';
 import * as lambda from 'aws-cdk-lib/aws-lambda';
@@ -265,10 +265,17 @@ export class MicroAppsStack extends Stack {
         removalPolicy,
       });
 
+      const appVersion = (app.lambdaFunction as lambda.Function).currentVersion;
+
       new CfnOutput(this, 'release-app-func-name', {
         value: `${app.lambdaFunction.functionName}`,
         exportName: `${this.stackName}-release-app-func-name`,
       });
+
+      new CfnOutput(this, 'release-app-vers-arn', {
+        value: `${appVersion.functionArn}`,
+        exportName: `${this.stackName}-release-app-vers-arn`,
+      });
     }
 
     if (deployNextjsDemoApp) {
@@ -280,10 +287,17 @@ export class MicroAppsStack extends Stack {
         removalPolicy,
       });
 
+      const appVersion = (app.lambdaFunction as lambda.Function).currentVersion;
+
       new CfnOutput(this, 'nextjs-demo-app-func-name', {
         value: `${app.lambdaFunction.functionName}`,
         exportName: `${this.stackName}-nextjs-demo-app-func-name`,
       });
+
+      new CfnOutput(this, 'nextjs-demo-app-vers-arn', {
+        value: `${appVersion.functionArn}`,
+        exportName: `${this.stackName}-nextjs-demo-app-vers-arn`,
+      });
     }
 
     // Exports
diff --git a/packages/microapps-cdk/src/MicroAppsSvcs.ts b/packages/microapps-cdk/src/MicroAppsSvcs.ts
index 61185b4c..7534d510 100644
--- a/packages/microapps-cdk/src/MicroAppsSvcs.ts
+++ b/packages/microapps-cdk/src/MicroAppsSvcs.ts
@@ -188,7 +188,7 @@ export interface IMicroAppsSvcs {
   /**
    * Lambda function for the Router
    */
-  readonly routerFunc: lambda.IFunction;
+  readonly routerFunc?: lambda.IFunction;
 }
 
 /**
@@ -207,8 +207,8 @@ export class MicroAppsSvcs extends Construct implements IMicroAppsSvcs {
     return this._deployerFunc;
   }
 
-  private _routerFunc: lambda.Function;
-  public get routerFunc(): lambda.IFunction {
+  private _routerFunc?: lambda.Function;
+  public get routerFunc(): lambda.IFunction | undefined {
     return this._routerFunc;
   }
 
@@ -268,78 +268,6 @@ export class MicroAppsSvcs extends Construct implements IMicroAppsSvcs {
       this._table = props.table;
     }
 
-    //
-    // Router Lambda Function
-    //
-
-    // Create Router Lambda Function
-    const routerFuncProps: Omit<lambda.FunctionProps, 'handler' | 'code'> = {
-      functionName: assetNameRoot ? `${assetNameRoot}-router${assetNameSuffix}` : undefined,
-      memorySize: 1769,
-      logRetention: logs.RetentionDays.ONE_MONTH,
-      runtime: lambda.Runtime.NODEJS_16_X,
-      timeout: Duration.seconds(15),
-      environment: {
-        NODE_ENV: appEnv,
-        DATABASE_TABLE_NAME: this._table.tableName,
-        AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1',
-        ROOT_PATH_PREFIX: rootPathPrefix,
-      },
-    };
-    if (
-      process.env.NODE_ENV === 'test' &&
-      existsSync(path.join(__dirname, '..', '..', 'microapps-router', 'dist', 'index.js'))
-    ) {
-      // This is for local dev
-      this._routerFunc = new lambda.Function(this, 'router-func', {
-        code: lambda.Code.fromAsset(path.join(__dirname, '..', '..', 'microapps-router', 'dist')),
-        handler: 'index.handler',
-        ...routerFuncProps,
-      });
-    } else if (existsSync(path.join(__dirname, 'microapps-router', 'index.js'))) {
-      // This is for built apps packaged with the CDK construct
-      this._routerFunc = new lambda.Function(this, 'router-func', {
-        code: lambda.Code.fromAsset(path.join(__dirname, 'microapps-router')),
-        handler: 'index.handler',
-        ...routerFuncProps,
-      });
-    } else {
-      // Create Router Lambda Layer
-      const routerDataFiles = new lambda.LayerVersion(this, 'router-templates', {
-        code: lambda.Code.fromAsset(
-          path.join(__dirname, '..', '..', 'microapps-router', 'templates'),
-        ),
-        removalPolicy,
-      });
-
-      this._routerFunc = new lambdaNodejs.NodejsFunction(this, 'router-func', {
-        entry: path.join(__dirname, '..', '..', 'microapps-router', 'src', 'index.ts'),
-        handler: 'handler',
-        bundling: {
-          minify: true,
-          sourceMap: true,
-        },
-        layers: [routerDataFiles],
-        ...routerFuncProps,
-      });
-    }
-    if (removalPolicy !== undefined) {
-      this._routerFunc.applyRemovalPolicy(removalPolicy);
-    }
-    const policyReadTarget = new iam.PolicyStatement({
-      effect: iam.Effect.ALLOW,
-      actions: ['s3:GetObject'],
-      resources: [`${bucketApps.bucketArn}/*`],
-    });
-    for (const router of [this._routerFunc]) {
-      router.addToRolePolicy(policyReadTarget);
-      // Give the Router access to DynamoDB table
-      this._table.grantReadData(router);
-      this._table.grant(router, 'dynamodb:DescribeTable');
-    }
-    // Create alias for Router
-    const routerAlias = this._routerFunc.addAlias('CurrentVersion');
-
     //
     // Deployer Lambda Function
     //
@@ -630,6 +558,78 @@ export class MicroAppsSvcs extends Construct implements IMicroAppsSvcs {
     this._deployerFunc.addToRolePolicy(policyAPIManageLambdas);
 
     if (httpApi) {
+      //
+      // Router Lambda Function
+      //
+
+      // Create Router Lambda Function
+      const routerFuncProps: Omit<lambda.FunctionProps, 'handler' | 'code'> = {
+        functionName: assetNameRoot ? `${assetNameRoot}-router${assetNameSuffix}` : undefined,
+        memorySize: 1769,
+        logRetention: logs.RetentionDays.ONE_MONTH,
+        runtime: lambda.Runtime.NODEJS_16_X,
+        timeout: Duration.seconds(15),
+        environment: {
+          NODE_ENV: appEnv,
+          DATABASE_TABLE_NAME: this._table.tableName,
+          AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1',
+          ROOT_PATH_PREFIX: rootPathPrefix,
+        },
+      };
+      if (
+        process.env.NODE_ENV === 'test' &&
+        existsSync(path.join(__dirname, '..', '..', 'microapps-router', 'dist', 'index.js'))
+      ) {
+        // This is for local dev
+        this._routerFunc = new lambda.Function(this, 'router-func', {
+          code: lambda.Code.fromAsset(path.join(__dirname, '..', '..', 'microapps-router', 'dist')),
+          handler: 'index.handler',
+          ...routerFuncProps,
+        });
+      } else if (existsSync(path.join(__dirname, 'microapps-router', 'index.js'))) {
+        // This is for built apps packaged with the CDK construct
+        this._routerFunc = new lambda.Function(this, 'router-func', {
+          code: lambda.Code.fromAsset(path.join(__dirname, 'microapps-router')),
+          handler: 'index.handler',
+          ...routerFuncProps,
+        });
+      } else {
+        // Create Router Lambda Layer
+        const routerDataFiles = new lambda.LayerVersion(this, 'router-templates', {
+          code: lambda.Code.fromAsset(
+            path.join(__dirname, '..', '..', 'microapps-router', 'templates'),
+          ),
+          removalPolicy,
+        });
+
+        this._routerFunc = new lambdaNodejs.NodejsFunction(this, 'router-func', {
+          entry: path.join(__dirname, '..', '..', 'microapps-router', 'src', 'index.ts'),
+          handler: 'handler',
+          bundling: {
+            minify: true,
+            sourceMap: true,
+          },
+          layers: [routerDataFiles],
+          ...routerFuncProps,
+        });
+      }
+      if (removalPolicy !== undefined) {
+        this._routerFunc.applyRemovalPolicy(removalPolicy);
+      }
+      const policyReadTarget = new iam.PolicyStatement({
+        effect: iam.Effect.ALLOW,
+        actions: ['s3:GetObject'],
+        resources: [`${bucketApps.bucketArn}/*`],
+      });
+      for (const router of [this._routerFunc]) {
+        router.addToRolePolicy(policyReadTarget);
+        // Give the Router access to DynamoDB table
+        this._table.grantReadData(router);
+        this._table.grant(router, 'dynamodb:DescribeTable');
+      }
+      // Create alias for Router
+      const routerAlias = this._routerFunc.addAlias('CurrentVersion');
+
       // This creates an integration and a router
       const route = new apigwy.HttpRoute(this, 'route-default', {
         httpApi,
diff --git a/packages/microapps-deployer/src/controllers/VersionController.spec.ts b/packages/microapps-deployer/src/controllers/VersionController.spec.ts
index 7a245a78..ff81aff0 100644
--- a/packages/microapps-deployer/src/controllers/VersionController.spec.ts
+++ b/packages/microapps-deployer/src/controllers/VersionController.spec.ts
@@ -894,6 +894,109 @@ describe('VersionController', () => {
       expect(updatedVersion.RouteIDAppVersionSplat).toBe('');
     });
 
+    it('should 201 version that does not exist - w/ full arn w/version', async () => {
+      const appName = 'newapp';
+      const semVer = '0.0.0';
+      const fakeLambdaVersion = '3';
+      const fakeLambdaARNBase = `arn:aws:lambda:${config.awsRegion}:${config.awsAccountID}:function:new-app-function`;
+      const fakeLambdaARN = `arn:aws:lambda:${config.awsRegion}:${config.awsAccountID}:function:new-app-function:${fakeLambdaVersion}`;
+
+      s3Client
+        .onAnyCommand()
+        .rejects()
+        // .callsFake((input) => {
+        //   console.log(`received input: ${JSON.stringify(input)}`);
+        //   console.log(
+        //     `our matcher: ${JSON.stringify({
+        //       Bucket: config.filestore.destinationBucket,
+        //       CopySource: `${config.filestore.stagingBucket}${pathPrefix}/${appName}/${semVer}/index.html`,
+        //       Key: `${pathPrefix}${appName}/${semVer}/index.html`,
+        //     })}`,
+        //   );
+        // })
+        // Mock S3 get for staging bucket - return one file name
+        .on(s3.ListObjectsV2Command, {
+          Bucket: config.filestore.stagingBucket,
+          Prefix: `${pathPrefix}${appName}/${semVer}/`,
+        })
+        .resolves({
+          IsTruncated: false,
+          Contents: [{ Key: `${pathPrefix}${appName}/${semVer}/index.html` }],
+        })
+        // Mock S3 copy to prod bucket
+        .on(s3.CopyObjectCommand, {
+          Bucket: config.filestore.destinationBucket,
+          CopySource: `${config.filestore.stagingBucket}/${pathPrefix}${appName}/${semVer}/index.html`,
+          Key: `${pathPrefix}${appName}/${semVer}/index.html`,
+        })
+        .resolves({})
+        .on(s3.DeleteObjectCommand, {
+          Bucket: config.filestore.stagingBucket,
+          Key: `${pathPrefix}${appName}/${semVer}/index.html`,
+        })
+        .resolves({});
+
+      lambdaClient
+        .onAnyCommand()
+        .rejects()
+        .on(lambda.ListTagsCommand, {
+          Resource: fakeLambdaARNBase,
+        })
+        .resolves({
+          Tags: {
+            'microapps-managed': 'true',
+          },
+        })
+        .on(lambda.TagResourceCommand, {
+          Resource: fakeLambdaARNBase,
+          Tags: {
+            'microapp-managed': 'true',
+          },
+        })
+        .resolves({})
+        .on(lambda.GetFunctionUrlConfigCommand, {
+          FunctionName: fakeLambdaARNBase,
+          Qualifier: fakeLambdaVersion,
+        })
+        .resolves({
+          FunctionUrl: fakeFunctionURLForAlias,
+        });
+      apigwyClient.onAnyCommand().rejects();
+
+      const response = await handler(
+        {
+          appName: appName,
+          semVer: semVer,
+          defaultFile: 'index.html',
+          lambdaARN: fakeLambdaARN,
+          type: 'deployVersion',
+          overwrite: true,
+          appType: 'lambda-url',
+          startupType: 'direct',
+          url: 'https://abc1234567.lambda-url.us-east-1.on.aws',
+        } as IDeployVersionRequest,
+        { awsRequestId: '123' } as lambdaTypes.Context,
+      );
+      expect(response.statusCode).toEqual(201);
+
+      const updatedVersion = await Version.LoadVersion({
+        dbManager,
+        key: { AppName: appName, SemVer: semVer },
+      });
+      // expect(updatedVersion).toEqual({});
+      expect(updatedVersion.AppName).toBe(appName);
+      expect(updatedVersion.SemVer).toBe(semVer);
+      expect(updatedVersion.DefaultFile).toBe('index.html');
+      expect(updatedVersion.LambdaARN).toBe(fakeLambdaARN);
+      expect(updatedVersion.URL).toBe(fakeFunctionURLForAlias);
+      expect(updatedVersion.StartupType).toBe('direct');
+      expect(updatedVersion.Status).toBe('routed');
+      expect(updatedVersion.Type).toBe('lambda-url');
+      expect(updatedVersion.IntegrationID).toBe('');
+      expect(updatedVersion.RouteIDAppVersion).toBe('');
+      expect(updatedVersion.RouteIDAppVersionSplat).toBe('');
+    });
+
     it('should 201 version that exists - overwrite true', async () => {
       const appName = 'newapp';
       const semVer = '0.0.0';
diff --git a/packages/microapps-publish/README.md b/packages/microapps-publish/README.md
index e905daec..d7fcf383 100644
--- a/packages/microapps-publish/README.md
+++ b/packages/microapps-publish/README.md
@@ -15,7 +15,6 @@
   - [Command - nextjs-version](#command---nextjs-version)
   - [Command - nextjs-version-restore](#command---nextjs-version-restore)
   - [Command - delete](#command---delete)
-  - [Command - nextjs-docker-auto](#command---nextjs-docker-auto)
 
 # Video Preview of Deploying an App
 
@@ -43,8 +42,6 @@ USAGE
 COMMANDS
   delete                  Delete app/version
   help                    display help for microapps-publish
-  nextjs-docker-auto      Fully automatic publishing of Docker-based Lambda
-                          function using Next.js and serverless-nextjs-router
   nextjs-version          Apply version to next.config.js overtop of 0.0.0
                           placeholder
   nextjs-version-restore  Restore next.config.js
@@ -247,73 +244,3 @@ EXAMPLE
   $ microapps-publish delete -d microapps-deployer-dev -a release -n 0.0.13
   ✔ App/Version deleted: release/0.0.13 [1.2s]
 ```
-
-## Command - nextjs-docker-auto
-
-Note: semi-deprecated as of 2022-01-27. This command may still work but it performs too many tasks that needed to be split out into individual commands to allow for integration into various build processes.
-
-`npx microapps-publish nextjs-docker-auto help`
-
-```
-Fully automatic publishing of Docker-based Lambda function using Next.js and serverless-nextjs-router
-
-USAGE
-  $ microapps-publish nextjs-docker-auto
-
-OPTIONS
-  -a, --appName=appName
-      MicroApps app name
-
-  -d, --deployerLambdaName=deployerLambdaName
-      (required) Name of the deployer lambda function
-
-  -f, --leaveCopy
-      Leave a copy of the modifed files as .modified
-
-  -i, --defaultFile=defaultFile
-      Default file to return when the app is loaded via the router without a
-      version (e.g. when app/ is requested).
-
-  -l, --appLambdaName=appLambdaName
-      Name of the application lambda function
-
-  -n, --newVersion=newVersion
-      (required) New semantic version to apply
-
-  -o, --overwrite
-      Allow overwrite - Warn but do not fail if version exists. Discouraged
-      outside of test envs if cacheable static files have changed.
-
-  -r, --repoName=repoName
-      (required) Name (not URI) of the Docker repo for the app
-
-  -s, --staticAssetsPath=staticAssetsPath
-      Path to files to be uploaded to S3 static bucket at app/version/ path.  Do
-      include app/version/ in path if files are already "rooted" under that path
-      locally.
-
-  -v, --version
-      show CLI version
-
-  --help
-      show CLI help
-
-  --noCache
-      Force revalidation of CloudFront and browser caching of static assets
-
-EXAMPLE
-  $ microapps-publish nextjs-docker-auto -d microapps-deployer-dev -r
-  microapps-app-release-dev-repo -n 0.0.14
-  ✔ Logging into ECR [2s]
-  ✔ Modifying Config Files [0.0s]
-  ✔ Preflight Version Check [1s]
-  ✔ Serverless Next.js Build [1m16s]
-  ✔ Publish to ECR [32s]
-  ✔ Deploy to Lambda [11s]
-  ✔ Confirm Static Assets Folder Exists [0.0s]
-  ✔ Copy Static Files to Local Upload Dir [0.0s]
-  ✔ Enumerate Files to Upload to S3 [0.0s]
-  ✔ Upload Static Files to S3 [1s]
-  ✔ Creating MicroApp Application: release [0.2s]
-  ✔ Creating MicroApp Version: 0.0.14 [1s]
-```
diff --git a/packages/microapps-publish/src/commands/nextjs-docker-auto.ts b/packages/microapps-publish/src/commands-deprecated/nextjs-docker-auto.ts
similarity index 99%
rename from packages/microapps-publish/src/commands/nextjs-docker-auto.ts
rename to packages/microapps-publish/src/commands-deprecated/nextjs-docker-auto.ts
index 3f12bab3..82a02c72 100755
--- a/packages/microapps-publish/src/commands/nextjs-docker-auto.ts
+++ b/packages/microapps-publish/src/commands-deprecated/nextjs-docker-auto.ts
@@ -447,7 +447,10 @@ export class DockerAutoCommand extends Command {
 
             // Call Deployer to Deploy AppName/Version
             await DeployClient.DeployVersion({
-              config,
+              appName: config.app.name,
+              semVer: config.app.semVer,
+              defaultFile: config.app.defaultFile,
+              deployerLambdaName: config.deployer.lambdaName,
               appType: 'lambda',
               overwrite,
               output: (message: string) => (task.output = message),
diff --git a/packages/microapps-publish/src/commands/publish-static.ts b/packages/microapps-publish/src/commands/publish-static.ts
index ab1ff43e..6d63b171 100644
--- a/packages/microapps-publish/src/commands/publish-static.ts
+++ b/packages/microapps-publish/src/commands/publish-static.ts
@@ -296,7 +296,10 @@ export class PublishCommand extends Command {
 
             // Call Deployer to Deploy AppName/Version
             await DeployClient.DeployVersion({
-              config,
+              appName: config.app.name,
+              semVer: config.app.semVer,
+              deployerLambdaName: config.deployer.lambdaName,
+              defaultFile: config.app.defaultFile,
               appType: 'static',
               overwrite,
               output: (message: string) => (task.output = message),
diff --git a/packages/microapps-publish/src/commands/publish.ts b/packages/microapps-publish/src/commands/publish.ts
index cfa5cadb..3cc5f723 100644
--- a/packages/microapps-publish/src/commands/publish.ts
+++ b/packages/microapps-publish/src/commands/publish.ts
@@ -25,6 +25,16 @@ const lambdaClient = new lambda.LambdaClient({
 interface IContext {
   preflightResult: IDeployVersionPreflightResult;
   files: string[];
+
+  /**
+   * The type of ARN passed in on the config or command line
+   */
+  configLambdaArnType: 'function' | 'alias' | 'version';
+
+  /**
+   * The ARN of the Lambda alias to use for the deploy
+   */
+  lambdaAliasArn: string;
 }
 
 export class PublishCommand extends Command {
@@ -65,7 +75,7 @@ export class PublishCommand extends Command {
       char: 'l',
       multiple: false,
       required: false,
-      description: 'Name of the application lambda function',
+      description: 'ARN of lambda version, alias, or function (name or ARN) to deploy',
     }),
     appName: flagsParser.string({
       char: 'a',
@@ -213,8 +223,27 @@ export class PublishCommand extends Command {
           },
         },
         {
-          // TODO: Disable this task if no Lambda function
-          title: 'Deploy to Lambda',
+          title: 'Check if Lambda ARN has Alias',
+          task: (ctx, task) => {
+            if (appLambdaName.match(/:/g)?.length === 7) {
+              if (/^[0-9]$/.test(appLambdaName.substring(appLambdaName.lastIndexOf(':') + 1))) {
+                ctx.configLambdaArnType = 'version';
+                task.output = `Lambda ARN has Version: ${config.app.lambdaName}`;
+              } else {
+                ctx.configLambdaArnType = 'alias';
+                task.output = `Lambda ARN has Alias: ${config.app.lambdaName}`;
+                ctx.lambdaAliasArn = config.app.lambdaName;
+              }
+            } else {
+              ctx.configLambdaArnType = 'function';
+              task.output = `Lambda ARN does not have Alias: ${config.app.lambdaName}`;
+            }
+          },
+        },
+        {
+          enabled: (ctx) =>
+            ctx.configLambdaArnType === 'function' || ctx.configLambdaArnType === 'version',
+          title: 'Create Lambda Alias and, optionally, Version',
           task: async (ctx, task) => {
             // Allow overwriting a non-overwritable app if the prior
             // publish was not completely successful - in that case
@@ -229,6 +258,7 @@ export class PublishCommand extends Command {
               versions: this.VersionAndAlias,
               overwrite: allowOverwrite,
               task,
+              ctx,
             });
 
             task.title = origTitle;
@@ -355,7 +385,11 @@ export class PublishCommand extends Command {
 
             // Call Deployer to Deploy AppName/Version
             await DeployClient.DeployVersion({
-              config,
+              appName: config.app.name,
+              semVer: config.app.semVer,
+              deployerLambdaName: config.deployer.lambdaName,
+              lambdaAliasArn: ctx.lambdaAliasArn,
+              defaultFile: config.app.defaultFile,
               appType,
               startupType: parsedFlags['startup-type'] as 'iframe' | 'direct',
               overwrite,
@@ -390,41 +424,51 @@ export class PublishCommand extends Command {
     versions: IVersions;
     overwrite: boolean;
     task: TaskWrapper<IContext, typeof DefaultRenderer>;
+    ctx: IContext;
   }): Promise<void> {
-    const { config, overwrite, versions, task } = opts;
+    const { config, overwrite, versions, task, ctx } = opts;
 
-    // Create Lambda version
-    task.output = 'Creating version for Lambda $LATEST';
-    const resultUpdate = await lambdaClient.send(
-      new lambda.PublishVersionCommand({
-        FunctionName: config.app.lambdaName,
-      }),
-    );
-    const lambdaVersion = resultUpdate.Version;
-    task.output = `Lambda version created: ${resultUpdate.Version}`;
-
-    let lastUpdateStatus = resultUpdate.LastUpdateStatus;
-    for (let i = 0; i < 5; i++) {
-      // When the function is created the status will be "Pending"
-      // and we have to wait until it's done creating
-      // before we can point an alias to it
-      if (lastUpdateStatus === 'Successful') {
-        task.output = `Lambda function updated, version: ${lambdaVersion}`;
-        break;
-      }
-
-      // If it didn't work, wait and try again
-      await asyncSetTimeout(1000 * i);
+    let lambdaVersion = '';
+    let lambdaArnBase = config.app.lambdaName;
 
-      const resultGet = await lambdaClient.send(
-        new lambda.GetFunctionCommand({
+    // Create Lambda version
+    if (ctx.configLambdaArnType === 'function') {
+      task.output = 'Creating version for Lambda $LATEST';
+      const resultUpdate = await lambdaClient.send(
+        new lambda.PublishVersionCommand({
           FunctionName: config.app.lambdaName,
-          Qualifier: lambdaVersion,
         }),
       );
+      lambdaVersion = resultUpdate.Version;
+      task.output = `Lambda version created: ${resultUpdate.Version}`;
+
+      let lastUpdateStatus = resultUpdate.LastUpdateStatus;
+      for (let i = 0; i < 5; i++) {
+        // When the function is created the status will be "Pending"
+        // and we have to wait until it's done creating
+        // before we can point an alias to it
+        if (lastUpdateStatus === 'Successful') {
+          task.output = `Lambda function updated, version: ${lambdaVersion}`;
+          break;
+        }
+
+        // If it didn't work, wait and try again
+        await asyncSetTimeout(1000 * i);
+
+        const resultGet = await lambdaClient.send(
+          new lambda.GetFunctionCommand({
+            FunctionName: config.app.lambdaName,
+            Qualifier: lambdaVersion,
+          }),
+        );
 
-      // Save the last update status so we can check on re-loop
-      lastUpdateStatus = resultGet?.Configuration?.LastUpdateStatus;
+        // Save the last update status so we can check on re-loop
+        lastUpdateStatus = resultGet?.Configuration?.LastUpdateStatus;
+      }
+    } else {
+      task.output = 'Lambda is already versioned, skipping version creation';
+      lambdaVersion = config.app.lambdaName.split(':')?.pop() || '';
+      lambdaArnBase = config.app.lambdaName.substring(0, config.app.lambdaName.lastIndexOf(':'));
     }
 
     // Create Lambda alias point
@@ -432,24 +476,26 @@ export class PublishCommand extends Command {
     try {
       const resultLambdaAlias = await lambdaClient.send(
         new lambda.CreateAliasCommand({
-          FunctionName: config.app.lambdaName,
+          FunctionName: lambdaArnBase,
           Name: versions.alias,
           FunctionVersion: lambdaVersion,
         }),
       );
       task.output = `Lambda alias created, name: ${resultLambdaAlias.Name}`;
+      ctx.lambdaAliasArn = resultLambdaAlias.AliasArn;
     } catch (error) {
       if (overwrite && error.name === 'ResourceConflictException') {
         task.output = `Alias exists, updating the lambda alias for version: ${lambdaVersion}`;
 
         const resultLambdaAlias = await lambdaClient.send(
           new lambda.UpdateAliasCommand({
-            FunctionName: config.app.lambdaName,
+            FunctionName: lambdaArnBase,
             Name: versions.alias,
             FunctionVersion: lambdaVersion,
           }),
         );
         task.output = `Lambda alias updated, name: ${resultLambdaAlias.Name}`;
+        ctx.lambdaAliasArn = resultLambdaAlias.AliasArn;
       } else {
         throw error;
       }
diff --git a/packages/microapps-publish/src/config/Application.ts b/packages/microapps-publish/src/config/Application.ts
index a1c01adc..1b0a7829 100644
--- a/packages/microapps-publish/src/config/Application.ts
+++ b/packages/microapps-publish/src/config/Application.ts
@@ -86,9 +86,11 @@ export class ApplicationConfig implements IApplicationConfig {
   })
   public lambdaName: string;
   public get lambdaARN(): string {
-    return `arn:aws:lambda:${this.awsRegion}:${this.awsAccountID}:function:${
-      this.lambdaName
-    }:v${this.semVer.replace(/\./g, '_')}`;
+    return this.lambdaName.includes(':')
+      ? this.lambdaName
+      : `arn:aws:lambda:${this.awsRegion}:${this.awsAccountID}:function:${
+          this.lambdaName
+        }:v${this.semVer.replace(/\./g, '_')}`;
   }
 
   @convict.Property({
diff --git a/packages/microapps-publish/src/lib/DeployClient.ts b/packages/microapps-publish/src/lib/DeployClient.ts
index f3cdeea5..083104df 100644
--- a/packages/microapps-publish/src/lib/DeployClient.ts
+++ b/packages/microapps-publish/src/lib/DeployClient.ts
@@ -147,26 +147,40 @@ export default class DeployClient {
    * @param task
    */
   public static async DeployVersion(opts: {
-    config: IConfig;
+    appName: string;
+    semVer: string;
+    defaultFile?: string;
+    lambdaAliasArn?: string;
+    deployerLambdaName: string;
     appType: 'lambda' | 'static' | 'lambda-url' | 'url';
     startupType?: 'iframe' | 'direct';
     overwrite: boolean;
     output: (message: string) => void;
   }): Promise<void> {
-    const { config, appType, startupType = 'iframe', overwrite, output } = opts;
+    const {
+      appName,
+      semVer,
+      defaultFile,
+      lambdaAliasArn,
+      deployerLambdaName,
+      appType,
+      startupType = 'iframe',
+      overwrite,
+      output,
+    } = opts;
     const request: IDeployVersionRequest = {
       type: 'deployVersion',
       appType,
       startupType,
-      appName: config.app.name,
-      semVer: config.app.semVer,
-      defaultFile: config.app.defaultFile,
+      appName: appName,
+      semVer: semVer,
+      defaultFile: defaultFile,
       overwrite,
-      ...(['lambda', 'lambda-url'].includes(appType) ? { lambdaARN: config.app.lambdaARN } : {}),
+      ...(['lambda', 'lambda-url'].includes(appType) ? { lambdaARN: lambdaAliasArn } : {}),
     };
     const response = await this._client.send(
       new lambda.InvokeCommand({
-        FunctionName: config.deployer.lambdaName,
+        FunctionName: deployerLambdaName,
         Payload: Buffer.from(JSON.stringify(request)),
       }),
     );
@@ -176,7 +190,7 @@ export default class DeployClient {
         Buffer.from(response.Payload).toString('utf-8'),
       ) as IDeployerResponse;
       if (dResponse.statusCode === 201) {
-        output(`Deploy succeeded: ${config.app.name}/${config.app.semVer}`);
+        output(`Deploy succeeded: ${appName}/${semVer}`);
       } else {
         output(`Deploy failed with: ${dResponse.statusCode}`);
         throw new Error(`Lambda call to DeployVersionfailed with: ${dResponse.statusCode}`);


Test patch:
diff --git a/packages/cdk/test/MicroApps.spec.ts b/packages/cdk/test/MicroApps.spec.ts
index 603f8bb3..13c8e75d 100644
--- a/packages/cdk/test/MicroApps.spec.ts
+++ b/packages/cdk/test/MicroApps.spec.ts
@@ -15,7 +15,7 @@ describe('MicroAppsStack', () => {
 
     expect(stack).toBeDefined();
     Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', {});
-    Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 4);
+    Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 3);
     Template.fromStack(stack).hasOutput('edgedomainname', {
       Value: { 'Fn::GetAtt': ['microappscft5FDF8AB8', 'DomainName'] },
       Export: { Name: `${stackName}-edge-domain-name` },
@@ -46,7 +46,7 @@ describe('MicroAppsStack', () => {
 
     expect(stack).toBeDefined();
     Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', {});
-    Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 4);
+    Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 3);
     Template.fromStack(stack).hasOutput('edgedomainname', {
       Value: 'appz.pwrdrvr.com',
       Export: { Name: `${stackName}-edge-domain-name` },
diff --git a/packages/microapps-cdk/test/MicroApps.test.ts b/packages/microapps-cdk/test/MicroApps.test.ts
index 168bdef4..2b113e27 100644
--- a/packages/microapps-cdk/test/MicroApps.test.ts
+++ b/packages/microapps-cdk/test/MicroApps.test.ts
@@ -28,7 +28,7 @@ describe('MicroApps', () => {
     expect(construct.node).toBeDefined();
     Template.fromStack(stack).resourceCountIs('AWS::DynamoDB::Table', 1);
     Template.fromStack(stack).resourceCountIs('AWS::CloudFront::Distribution', 1);
-    Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 4);
+    Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 3);
 
     // Confirm that logical IDs have not changed accidentally (causes delete/create)
     Template.fromStack(stack).templateMatches({
@@ -42,9 +42,9 @@ describe('MicroApps', () => {
         // constructedgeToOriginedgetoapigwyfuncFn10C0FCC9: {
         //   Type: 'AWS::Lambda::Function',
         // },
-        constructsvcsrouterfunc73102284: {
-          Type: 'AWS::Lambda::Function',
-        },
+        // constructsvcsrouterfunc73102284: {
+        //   Type: 'AWS::Lambda::Function',
+        // },
         constructs3apps91016270: {
           Type: 'AWS::S3::Bucket',
         },
@@ -90,7 +90,7 @@ describe('MicroApps', () => {
     expect(construct.node).toBeDefined();
     Template.fromStack(stack).resourceCountIs('AWS::DynamoDB::Table', 1);
     Template.fromStack(stack).resourceCountIs('AWS::CloudFront::Distribution', 1);
-    Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 4);
+    Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 3);
 
     // Confirm that logical IDs have not changed accidentally (causes delete/create)
     Template.fromStack(stack).templateMatches({
@@ -104,9 +104,9 @@ describe('MicroApps', () => {
         // constructedgeToOriginedgetoapigwyfuncFn10C0FCC9: {
         //   Type: 'AWS::Lambda::Function',
         // },
-        constructsvcsrouterfunc73102284: {
-          Type: 'AWS::Lambda::Function',
-        },
+        // constructsvcsrouterfunc73102284: {
+        //   Type: 'AWS::Lambda::Function',
+        // },
         constructs3apps91016270: {
           Type: 'AWS::S3::Bucket',
         },
@@ -153,10 +153,10 @@ describe('MicroApps', () => {
     expect(construct.node).toBeDefined();
     Template.fromStack(stack).resourceCountIs('AWS::DynamoDB::Table', 1);
     Template.fromStack(stack).resourceCountIs('AWS::CloudFront::Distribution', 1);
-    Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 4);
-    Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', {
-      FunctionName: 'my-asset-root-name-router-some-suffix',
-    });
+    Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 3);
+    // Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', {
+    //   FunctionName: 'my-asset-root-name-router-some-suffix',
+    // });
     Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', {
       FunctionName: 'my-asset-root-name-deployer-some-suffix',
     });
@@ -173,9 +173,9 @@ describe('MicroApps', () => {
         // constructedgeToOriginedgetoapigwyfuncFn10C0FCC9: {
         //   Type: 'AWS::Lambda::Function',
         // },
-        constructsvcsrouterfunc73102284: {
-          Type: 'AWS::Lambda::Function',
-        },
+        // constructsvcsrouterfunc73102284: {
+        //   Type: 'AWS::Lambda::Function',
+        // },
         constructs3apps91016270: {
           Type: 'AWS::S3::Bucket',
         },
@@ -223,10 +223,10 @@ describe('MicroApps', () => {
     expect(construct.node).toBeDefined();
     Template.fromStack(stack).resourceCountIs('AWS::DynamoDB::Table', 1);
     Template.fromStack(stack).resourceCountIs('AWS::CloudFront::Distribution', 1);
-    Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 4);
-    Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', {
-      FunctionName: 'my-asset-root-name-router-some-suffix',
-    });
+    Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 3);
+    // Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', {
+    //   FunctionName: 'my-asset-root-name-router-some-suffix',
+    // });
     Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', {
       FunctionName: 'my-asset-root-name-deployer-some-suffix',
     });
@@ -257,9 +257,9 @@ describe('MicroApps', () => {
         constructsvcsdeployerfunc88CC1526: {
           Type: 'AWS::Lambda::Function',
         },
-        constructsvcsrouterfunc73102284: {
-          Type: 'AWS::Lambda::Function',
-        },
+        // constructsvcsrouterfunc73102284: {
+        //   Type: 'AWS::Lambda::Function',
+        // },
         constructs3apps91016270: {
           Type: 'AWS::S3::Bucket',
         },
@@ -311,7 +311,7 @@ describe('MicroApps', () => {
     expect(construct.node).toBeDefined();
     Template.fromStack(stack).resourceCountIs('AWS::DynamoDB::Table', 1);
     Template.fromStack(stack).resourceCountIs('AWS::CloudFront::Distribution', 1);
-    Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 3);
+    Template.fromStack(stack).resourceCountIs('AWS::Lambda::Function', 2);
 
     // Confirm that logical IDs have not changed accidentally (causes delete/create)
     Template.fromStack(stack).templateMatches({
@@ -322,9 +322,6 @@ describe('MicroApps', () => {
         constructsvcstable0311CF05: {
           Type: 'AWS::DynamoDB::Table',
         },
-        constructsvcsrouterfunc73102284: {
-          Type: 'AWS::Lambda::Function',
-        },
         constructs3apps91016270: {
           Type: 'AWS::S3::Bucket',
         },