diff --git a/packages/@aws-cdk/alexa-ask/README.md b/packages/@aws-cdk/alexa-ask/README.md index d9b65d34f8459..9e3754166cf5d 100644 --- a/packages/@aws-cdk/alexa-ask/README.md +++ b/packages/@aws-cdk/alexa-ask/README.md @@ -3,3 +3,51 @@ ```ts const alexaAsk = require('@aws-cdk/alexa-ask'); ``` + +### Alexa as deploy target for CodePipeline + +You can deploy to Alexa using CodePipeline with the following DeployAction. + +```ts +// Read the secrets from ParameterStore +const clientId = new cdk.SecretParameter(stack, 'AlexaClientId', {ssmParameter: '/Alexa/ClientId'}); +const clientSecret = new cdk.SecretParameter(stack, 'AlexaClientSecret', {ssmParameter: '/Alexa/ClientSecret'}); +const refreshToken = new cdk.SecretParameter(stack, 'AlexaRefreshToken', {ssmParameter: '/Alexa/RefreshToken'}); + +// Add deploy action +new alexa.AlexaSkillDeployAction(stack, 'DeploySkill', { + stage: deployStage, + runOrder: 1, + inputArtifact: sourceAction.outputArtifact, + clientId: clientId.value, + clientSecret: clientSecret.value, + refreshToken: refreshToken.value, + skillId: 'amzn1.ask.skill.12345678-1234-1234-1234-123456789012', +}); +``` + +If you need manifest overrides you can specify them as `overrideArtifact` in the action. + +```ts +// Deploy some CFN change set and store output +const executeChangeSetAction = new PipelineExecuteChangeSetAction(this, 'ExecuteChangesTest', { + stage: deployStage, + runOrder: 2, + stackName, + changeSetName, + outputFileName: 'overrides.json', + outputArtifactName: 'CloudFormation', +}); + +// Provide CFN output as manifest overrides +new AlexaSkillDeployAction(this, 'DeploySkill', { + stage: deployStage, + runOrder: 1, + inputArtifact: sourceAction.outputArtifact, + parameterOverridesArtifact: executeChangeSetAction.outputArtifact, + clientId: clientId.value, + clientSecret: clientSecret.value, + refreshToken: refreshToken.value, + skillId: 'amzn1.ask.skill.12345678-1234-1234-1234-123456789012', +}); +``` \ No newline at end of file diff --git a/packages/@aws-cdk/alexa-ask/lib/index.ts b/packages/@aws-cdk/alexa-ask/lib/index.ts index 59194e547cc5a..a691e2ef6e3cb 100644 --- a/packages/@aws-cdk/alexa-ask/lib/index.ts +++ b/packages/@aws-cdk/alexa-ask/lib/index.ts @@ -1,2 +1,3 @@ // Alexa::ASK CloudFormation Resources: export * from './ask.generated'; +export * from './pipeline-actions'; diff --git a/packages/@aws-cdk/alexa-ask/lib/pipeline-actions.ts b/packages/@aws-cdk/alexa-ask/lib/pipeline-actions.ts new file mode 100644 index 0000000000000..e74b267d75221 --- /dev/null +++ b/packages/@aws-cdk/alexa-ask/lib/pipeline-actions.ts @@ -0,0 +1,67 @@ +import codepipeline = require('@aws-cdk/aws-codepipeline-api'); +import cdk = require('@aws-cdk/cdk'); + +/** + * Construction properties of the {@link AlexaSkillDeployAction Alexa deploy Action}. + */ +export interface AlexaSkillDeployActionProps extends codepipeline.CommonActionProps, + codepipeline.CommonActionConstructProps { + + /** + * The client id of the developer console token + */ + clientId: cdk.Secret; + + /** + * The client secret of the developer console token + */ + clientSecret: cdk.Secret; + + /** + * The refresh token of the developer console token + */ + refreshToken: cdk.Secret; + + /** + * The Alexa skill id + */ + skillId: string; + + /** + * The source artifact containing the voice model and skill manifest + */ + inputArtifact?: codepipeline.Artifact; + + /** + * An optional artifact containing overrides for the skill manifest + */ + parameterOverridesArtifact?: codepipeline.Artifact; +} + +/** + * Deploys the skill to Alexa + */ +export class AlexaSkillDeployAction extends codepipeline.DeployAction { + constructor(scope: cdk.Construct, id: string, props: AlexaSkillDeployActionProps) { + super(scope, id, { + ...props, + artifactBounds: { + minInputs: 1, + maxInputs: 2, + minOutputs: 0, + maxOutputs: 0, + }, + owner: 'ThirdParty', + provider: 'AlexaSkillsKit', + configuration: { + ClientId: props.clientId, + ClientSecret: props.clientSecret, + RefreshToken: props.refreshToken, + SkillId: props.skillId, + }, + }); + if (props.parameterOverridesArtifact) { + this.addInputArtifact(props.parameterOverridesArtifact); + } + } +} diff --git a/packages/@aws-cdk/alexa-ask/package.json b/packages/@aws-cdk/alexa-ask/package.json index bb90ccb028d26..6453f3fee4230 100644 --- a/packages/@aws-cdk/alexa-ask/package.json +++ b/packages/@aws-cdk/alexa-ask/package.json @@ -61,12 +61,14 @@ "pkglint": "^0.22.0" }, "dependencies": { + "@aws-cdk/aws-codepipeline-api": "^0.22.0", "@aws-cdk/cdk": "^0.22.0" }, "peerDependencies": { + "@aws-cdk/aws-codepipeline-api": "^0.22.0", "@aws-cdk/cdk": "^0.22.0" }, "engines": { "node": ">= 8.10.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-codepipeline/package.json b/packages/@aws-cdk/aws-codepipeline/package.json index a797d2cb9f7c1..6b1a4d7473858 100644 --- a/packages/@aws-cdk/aws-codepipeline/package.json +++ b/packages/@aws-cdk/aws-codepipeline/package.json @@ -60,6 +60,7 @@ }, "license": "Apache-2.0", "devDependencies": { + "@aws-cdk/alexa-ask": "^0.22.0", "@aws-cdk/assert": "^0.22.0", "@aws-cdk/aws-cloudformation": "^0.22.0", "@aws-cdk/aws-cloudtrail": "^0.22.0", diff --git a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-alexa-deploy.expected.json b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-alexa-deploy.expected.json new file mode 100644 index 0000000000000..f65ac6af5d5f4 --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-alexa-deploy.expected.json @@ -0,0 +1,189 @@ +{ + "Resources": { + "PipelineArtifactsBucket22248F97": { + "Type": "AWS::S3::Bucket", + "DeletionPolicy": "Retain" + }, + "PipelineRoleD68726F7": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codepipeline.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineRoleDefaultPolicyC7A05455": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject*", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineArtifactsBucket22248F97", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineBucketB967BD35", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineBucketB967BD35", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineRoleDefaultPolicyC7A05455", + "Roles": [ + { + "Ref": "PipelineRoleD68726F7" + } + ] + } + }, + "PipelineC660917D": { + "Type": "AWS::CodePipeline::Pipeline", + "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "PipelineRoleD68726F7", + "Arn" + ] + }, + "Stages": [ + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Source", + "Owner": "AWS", + "Provider": "S3", + "Version": "1" + }, + "Configuration": { + "S3Bucket": { + "Ref": "PipelineBucketB967BD35" + }, + "S3ObjectKey": "key" + }, + "InputArtifacts": [], + "Name": "Source", + "OutputArtifacts": [ + { + "Name": "SourceArtifact" + } + ], + "RunOrder": 1 + } + ], + "Name": "Source" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "ThirdParty", + "Provider": "AlexaSkillsKit", + "Version": "1" + }, + "Configuration": { + "ClientId": "clientId", + "ClientSecret": "clientSecret", + "RefreshToken": "refreshToken", + "SkillId": "amzn1.ask.skill.12345678-1234-1234-1234-123456789012" + }, + "InputArtifacts": [ + { + "Name": "SourceArtifact" + } + ], + "Name": "DeploySkill", + "OutputArtifacts": [], + "RunOrder": 1 + } + ], + "Name": "Deploy" + } + ], + "ArtifactStore": { + "Location": { + "Ref": "PipelineArtifactsBucket22248F97" + }, + "Type": "S3" + } + }, + "DependsOn": [ + "PipelineRoleD68726F7", + "PipelineRoleDefaultPolicyC7A05455" + ] + }, + "PipelineBucketB967BD35": { + "Type": "AWS::S3::Bucket", + "Properties": { + "VersioningConfiguration": { + "Status": "Enabled" + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-alexa-deploy.ts b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-alexa-deploy.ts new file mode 100644 index 0000000000000..d01f6147d57e7 --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-alexa-deploy.ts @@ -0,0 +1,36 @@ +import alexa = require('@aws-cdk/alexa-ask'); +import s3 = require('@aws-cdk/aws-s3'); +import cdk = require('@aws-cdk/cdk'); +import codepipeline = require('../lib'); + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'aws-cdk-codepipeline-alexa-deploy'); + +const pipeline = new codepipeline.Pipeline(stack, 'Pipeline'); + +const sourceStage = new codepipeline.Stage(pipeline, 'Source', { pipeline }); +const bucket = new s3.Bucket(stack, 'PipelineBucket', { + versioned: true, + removalPolicy: cdk.RemovalPolicy.Destroy, +}); +const sourceAction = new s3.PipelineSourceAction(stack, 'Source', { + stage: sourceStage, + outputArtifactName: 'SourceArtifact', + bucket, + bucketKey: 'key', +}); + +const deployStage = new codepipeline.Stage(pipeline, 'Deploy', { pipeline }); + +new alexa.AlexaSkillDeployAction(stack, 'DeploySkill', { + stage: deployStage, + runOrder: 1, + inputArtifact: sourceAction.outputArtifact, + clientId: new cdk.Secret('clientId'), + clientSecret: new cdk.Secret('clientSecret'), + refreshToken: new cdk.Secret('refreshToken'), + skillId: 'amzn1.ask.skill.12345678-1234-1234-1234-123456789012', +}); + +app.run();