diff --git a/template.yml b/.aws/cfn/app.yml similarity index 100% rename from template.yml rename to .aws/cfn/app.yml diff --git a/.aws/cfn/storybook.yml b/.aws/cfn/storybook.yml new file mode 100644 index 0000000..75c03f1 --- /dev/null +++ b/.aws/cfn/storybook.yml @@ -0,0 +1,152 @@ +Description: React Starter UI Storybook component resources + +Parameters: + EnvironmentCode: + Type: String + Description: Select an Environment + AllowedValues: + - dev + - qa + - prod + Default: dev + ConstraintDescription: Must select a valid environment + +Mappings: + EnvironmentAttributeMap: + dev: + CertificateArn: arn:aws:acm:us-east-1:988218269141:certificate/3d110b0f-8b3d-4ddc-bbd8-fab08ae6f038 + CloudFrontOAID: E2U9SKLVDD8TPN + HostedZone: dev.leanstacks.net + qa: + CertificateArn: arn:aws:acm:us-east-1:339939222800:certificate/5cd1bce7-1323-4625-a49e-5e72d1cff7ef + CloudFrontOAID: E322H9D7WOKWXW + HostedZone: qa.leanstacks.net + prod: + CertificateArn: arn:aws:acm:us-east-1:854599584783:certificate/fc25a13b-0c9f-4c79-a20f-a13f5d2245b3 + CloudFrontOAID: EVMQ2O0M1MS7S + HostedZone: leanstacks.net + +Resources: + ## + # S3 Bucket for the Storybook App + ## + BucketApp: + Type: AWS::S3::Bucket + Properties: + BucketName: !Sub + - 'reactstarter-ui-sb.${HostedZone}-${AWS::Region}-${AWS::AccountId}' + - HostedZone: !FindInMap [EnvironmentAttributeMap, !Ref EnvironmentCode, HostedZone] + + ## + # Bucket Policy allows access from AWS CloudFront + ## + BucketPolicyApp: + Type: AWS::S3::BucketPolicy + Properties: + Bucket: !Ref BucketApp + PolicyDocument: + Statement: + - Action: + - s3:GetObject + Effect: Allow + Resource: !Join + - '' + - - 'arn:aws:s3:::' + - !Ref BucketApp + - '/*' + Principal: + AWS: !Sub + - 'arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ${CloudFrontOAID}' + - CloudFrontOAID: + !FindInMap [EnvironmentAttributeMap, !Ref EnvironmentCode, CloudFrontOAID] + + ## + # CloudFront Distribution for the Storybook App - SPA errors and behaviors + ## + DistributionApp: + Type: AWS::CloudFront::Distribution + Properties: + DistributionConfig: + Comment: !Sub 'React Starter UI Storybook (${EnvironmentCode})' + Aliases: + - !Sub + - 'react-starter-storybook.${HostedZone}' + - HostedZone: !FindInMap [EnvironmentAttributeMap, !Ref EnvironmentCode, HostedZone] + CustomErrorResponses: + - ErrorCode: 404 + ResponsePagePath: '/index.html' + ResponseCode: 200 + - ErrorCode: 403 + ResponsePagePath: '/index.html' + ResponseCode: 200 + DefaultCacheBehavior: + AllowedMethods: + - GET + - HEAD + - OPTIONS + DefaultTTL: 60 + ForwardedValues: + Cookies: + Forward: none + QueryString: false + TargetOriginId: S3-APP + ViewerProtocolPolicy: redirect-to-https + DefaultRootObject: index.html + Enabled: true + HttpVersion: http2 + Origins: + - DomainName: !GetAtt BucketApp.DomainName + Id: S3-APP + S3OriginConfig: + OriginAccessIdentity: !Sub + - 'origin-access-identity/cloudfront/${CloudFrontOAID}' + - CloudFrontOAID: + !FindInMap [EnvironmentAttributeMap, !Ref EnvironmentCode, CloudFrontOAID] + PriceClass: PriceClass_100 + ViewerCertificate: + AcmCertificateArn: + !FindInMap [EnvironmentAttributeMap, !Ref EnvironmentCode, CertificateArn] + SslSupportMethod: sni-only + + ## + # Route53 'A' record for the Storybook CloudFront Distribution + ## + RecordSetAppA: + Type: AWS::Route53::RecordSet + Properties: + HostedZoneName: !Sub + - '${HostedZone}.' + - HostedZone: !FindInMap [EnvironmentAttributeMap, !Ref EnvironmentCode, HostedZone] + Name: !Sub + - 'react-starter-storybook.${HostedZone}' + - HostedZone: !FindInMap [EnvironmentAttributeMap, !Ref EnvironmentCode, HostedZone] + Type: A + AliasTarget: + HostedZoneId: Z2FDTNDATAQYW2 + DNSName: !GetAtt DistributionApp.DomainName + + ## + # Route53 'AAAA' record for the Storybook CloudFront Distribution + ## + RecordSetAppAAAA: + Type: AWS::Route53::RecordSet + Properties: + HostedZoneName: !Sub + - '${HostedZone}.' + - HostedZone: !FindInMap [EnvironmentAttributeMap, !Ref EnvironmentCode, HostedZone] + Name: !Sub + - 'react-starter-storybook.${HostedZone}' + - HostedZone: !FindInMap [EnvironmentAttributeMap, !Ref EnvironmentCode, HostedZone] + Type: AAAA + AliasTarget: + HostedZoneId: Z2FDTNDATAQYW2 + DNSName: !GetAtt DistributionApp.DomainName + +Outputs: + AppBucketName: + Description: The application S3 bucket name + Value: !Ref BucketApp + + DomainName: + Description: The application domain name + Value: !Ref RecordSetAppA diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 226f9a0..8c44ff7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,8 @@ concurrency: cancel-in-progress: true env: - AWS_CFN_TEMPLATE: template.yml + AWS_CFN_TEMPLATE_APP: .aws/cfn/app.yml + AWS_CFN_TEMPLATE_STORYBOOK: .aws/cfn/storybook.yml AWS_REGION: ${{ vars.AWS_REGION }} AWS_ROLE_ARN: ${{ vars.AWS_ROLE_ARN_DEV }} ENV_FILE: ${{ secrets.ENV_CI }} @@ -140,7 +141,12 @@ jobs: role-to-assume: ${{ env.AWS_ROLE_ARN }} aws-region: ${{ env.AWS_REGION }} - - name: Validate AWS CloudFormation Template + - name: Validate App Template run: |- aws cloudformation validate-template \ - --template-body file://${{ env.AWS_CFN_TEMPLATE }} + --template-body file://${{ env.AWS_CFN_TEMPLATE_APP }} + + - name: Validate Storybook Template + run: |- + aws cloudformation validate-template \ + --template-body file://${{ env.AWS_CFN_TEMPLATE_STORYBOOK }} diff --git a/.github/workflows/deploy-app-dev.yml b/.github/workflows/deploy-app-dev.yml new file mode 100644 index 0000000..7b69243 --- /dev/null +++ b/.github/workflows/deploy-app-dev.yml @@ -0,0 +1,22 @@ +name: Deploy App to Development + +on: + push: + branches: + - main + tags: + - dev + +concurrency: + group: ${{ github.workflow }} + +jobs: + deploy: + name: Deploy App DV + uses: ./.github/workflows/deploy-app.yml + with: + aws_cfn_stack_name: ls-ui-reactstarter-resources-dev + aws_role_arn: ${{ vars.AWS_ROLE_ARN_DEV }} + env: dev + secrets: + env_file: ${{ secrets.ENV_DEV }} diff --git a/.github/workflows/deploy-app-prod.yml b/.github/workflows/deploy-app-prod.yml new file mode 100644 index 0000000..c063459 --- /dev/null +++ b/.github/workflows/deploy-app-prod.yml @@ -0,0 +1,23 @@ +name: Deploy App to Production + +on: + release: + types: + - published + push: + tags: + - prod + +concurrency: + group: ${{ github.workflow }} + +jobs: + deploy: + name: Deploy App PR + uses: ./.github/workflows/deploy-app.yml + with: + aws_cfn_stack_name: ls-ui-reactstarter-resources-prod + aws_role_arn: ${{ vars.AWS_ROLE_ARN_PROD }} + env: prod + secrets: + env_file: ${{ secrets.ENV_PROD }} diff --git a/.github/workflows/deploy-app-qa.yml b/.github/workflows/deploy-app-qa.yml new file mode 100644 index 0000000..fdad2f8 --- /dev/null +++ b/.github/workflows/deploy-app-qa.yml @@ -0,0 +1,22 @@ +name: Deploy App to QA + +on: + push: + branches: + - release/* + tags: + - qa + +concurrency: + group: ${{ github.workflow }} + +jobs: + deploy: + name: Deploy App QA + uses: ./.github/workflows/deploy-app.yml + with: + aws_cfn_stack_name: ls-ui-reactstarter-resources-qa + aws_role_arn: ${{ vars.AWS_ROLE_ARN_QA }} + env: qa + secrets: + env_file: ${{ secrets.ENV_QA }} diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-app.yml similarity index 58% rename from .github/workflows/deploy-dev.yml rename to .github/workflows/deploy-app.yml index 4556ced..f1e4507 100644 --- a/.github/workflows/deploy-dev.yml +++ b/.github/workflows/deploy-app.yml @@ -1,23 +1,34 @@ -name: Deploy to Development +name: Deploy React App on: - push: - branches: - - main - tags: - - dev - -concurrency: - group: ${{ github.workflow }} - -env: - APP_NAME: react-starter.leanstacks.net - AWS_CFN_STACK_NAME: ls-ui-reactstarter-resources-dev - AWS_CFN_TEMPLATE: template.yml - AWS_ENV_CODE: dev - AWS_REGION: ${{ vars.AWS_REGION }} - AWS_ROLE_ARN: ${{ vars.AWS_ROLE_ARN_DEV }} - ENV_FILE: ${{ secrets.ENV_DEV }} + workflow_call: + inputs: + app_name: + required: false + type: string + default: 'react-starter.leanstacks.net' + aws_cfn_stack_name: + required: false + type: string + default: 'ls-ui-reactstarter-resources' + aws_cfn_template: + required: false + type: string + default: '.aws/cfn/app.yml' + aws_region: + required: false + type: string + default: 'us-east-1' + aws_role_arn: + required: true + type: string + env: + required: false + type: string + default: 'dev' + secrets: + env_file: + required: true jobs: deploy: @@ -45,12 +56,12 @@ jobs: - name: Create Environment Configuration run: | - echo "${{ env.ENV_FILE }}" > .env + echo "${{ secrets.ENV_FILE }}" > .env echo "VITE_BUILD_DATE=$(date +'%Y-%m-%d')" >> .env echo "VITE_BUILD_TIME=$(date +'%H:%M:%S%z')" >> .env echo "VITE_BUILD_TS=$(date +'%Y-%m-%dT%H:%M:%S%z')" >> .env echo "VITE_BUILD_COMMIT_SHA=${{ github.sha }}" >> .env - echo "VITE_BUILD_ENV_CODE=${{ env.AWS_ENV_CODE }}" >> .env + echo "VITE_BUILD_ENV_CODE=${{ inputs.env }}" >> .env echo "VITE_BUILD_WORKFLOW_NAME=${{ github.workflow }}" >> .env echo "VITE_BUILD_WORKFLOW_RUN_NUMBER=${{ github.run_number }}" >> .env echo "VITE_BUILD_WORKFLOW_RUN_ATTEMPT=${{ github.run_attempt }}" >> .env @@ -61,23 +72,23 @@ jobs: - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v4 with: - role-to-assume: ${{ env.AWS_ROLE_ARN }} - aws-region: ${{ env.AWS_REGION }} + role-to-assume: ${{ inputs.aws_role_arn }} + aws-region: ${{ inputs.aws_region }} - name: Deploy AWS CloudFormation Stack run: |- aws cloudformation deploy \ - --stack-name ${{ env.AWS_CFN_STACK_NAME }} \ - --template-file ${{ env.AWS_CFN_TEMPLATE }} \ - --parameter-overrides EnvironmentCode=${{ env.AWS_ENV_CODE }} \ - --tags App=${{ env.APP_NAME }} Env=${{ env.AWS_ENV_CODE }} OU=leanstacks Owner='Matthew Warman' + --stack-name ${{ inputs.aws_cfn_stack_name }} \ + --template-file ${{ inputs.aws_cfn_template }} \ + --parameter-overrides EnvironmentCode=${{ inputs.env }} \ + --tags App=${{ inputs.app_name }} Env=${{ inputs.env }} OU=leanstacks Owner='Matthew Warman' - name: Get CloudFormation Stack Outputs id: cloudformation run: |- APP_BUCKET_NAME=$( aws cloudformation describe-stacks \ - --stack-name ${{ env.AWS_CFN_STACK_NAME }} \ + --stack-name ${{ inputs.aws_cfn_stack_name }} \ --query "Stacks[0].Outputs[?OutputKey=='AppBucketName'].OutputValue | [0]" ) echo "APP_BUCKET_NAME=$APP_BUCKET_NAME" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/deploy-qa.yml b/.github/workflows/deploy-qa.yml deleted file mode 100644 index 666da4a..0000000 --- a/.github/workflows/deploy-qa.yml +++ /dev/null @@ -1,87 +0,0 @@ -name: Deploy to QA - -on: - push: - branches: - - release/* - tags: - - qa - -concurrency: - group: ${{ github.workflow }} - -env: - APP_NAME: react-starter.leanstacks.net - AWS_CFN_STACK_NAME: ls-ui-reactstarter-resources-qa - AWS_CFN_TEMPLATE: template.yml - AWS_ENV_CODE: qa - AWS_REGION: ${{ vars.AWS_REGION }} - AWS_ROLE_ARN: ${{ vars.AWS_ROLE_ARN_QA }} - ENV_FILE: ${{ secrets.ENV_QA }} - -jobs: - deploy: - name: Deploy - - runs-on: ubuntu-latest - timeout-minutes: 20 - - permissions: - id-token: write - contents: read - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Node.js Environment - uses: actions/setup-node@v4 - with: - node-version-file: '.nvmrc' - cache: npm - - - name: Install Dependencies - run: npm ci - - - name: Create Environment Configuration - run: | - echo "${{ env.ENV_FILE }}" > .env - echo "VITE_BUILD_DATE=$(date +'%Y-%m-%d')" >> .env - echo "VITE_BUILD_TIME=$(date +'%H:%M:%S%z')" >> .env - echo "VITE_BUILD_TS=$(date +'%Y-%m-%dT%H:%M:%S%z')" >> .env - echo "VITE_BUILD_COMMIT_SHA=${{ github.sha }}" >> .env - echo "VITE_BUILD_ENV_CODE=${{ env.AWS_ENV_CODE }}" >> .env - echo "VITE_BUILD_WORKFLOW_NAME=${{ github.workflow }}" >> .env - echo "VITE_BUILD_WORKFLOW_RUN_NUMBER=${{ github.run_number }}" >> .env - echo "VITE_BUILD_WORKFLOW_RUN_ATTEMPT=${{ github.run_attempt }}" >> .env - - - name: Build - run: npm run build - - - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - role-to-assume: ${{ env.AWS_ROLE_ARN }} - aws-region: ${{ env.AWS_REGION }} - - - name: Deploy AWS CloudFormation Stack - run: |- - aws cloudformation deploy \ - --stack-name ${{ env.AWS_CFN_STACK_NAME }} \ - --template-file ${{ env.AWS_CFN_TEMPLATE }} \ - --parameter-overrides EnvironmentCode=${{ env.AWS_ENV_CODE }} \ - --tags App=${{ env.APP_NAME }} Env=${{ env.AWS_ENV_CODE }} OU=leanstacks Owner='Matthew Warman' - - - name: Get CloudFormation Stack Outputs - id: cloudformation - run: |- - APP_BUCKET_NAME=$( - aws cloudformation describe-stacks \ - --stack-name ${{ env.AWS_CFN_STACK_NAME }} \ - --query "Stacks[0].Outputs[?OutputKey=='AppBucketName'].OutputValue | [0]" - ) - echo "APP_BUCKET_NAME=$APP_BUCKET_NAME" >> "$GITHUB_OUTPUT" - - - name: Deploy to AWS S3 - run: | - aws s3 sync dist s3://${{ steps.cloudformation.outputs.APP_BUCKET_NAME }} --delete diff --git a/.github/workflows/deploy-storybook-dev.yml b/.github/workflows/deploy-storybook-dev.yml new file mode 100644 index 0000000..1d2631c --- /dev/null +++ b/.github/workflows/deploy-storybook-dev.yml @@ -0,0 +1,22 @@ +name: Deploy Storybook to Development + +on: + push: + branches: + - main + tags: + - dev + +concurrency: + group: ${{ github.workflow }} + +jobs: + deploy: + name: Deploy SB DV + uses: ./.github/workflows/deploy-storybook.yml + with: + aws_cfn_stack_name: ls-ui-reactstarter-storybook-resources-dev + aws_role_arn: ${{ vars.AWS_ROLE_ARN_DEV }} + env: dev + secrets: + env_file: ${{ secrets.ENV_DEV }} diff --git a/.github/workflows/deploy-storybook-prod.yml b/.github/workflows/deploy-storybook-prod.yml new file mode 100644 index 0000000..9e3606b --- /dev/null +++ b/.github/workflows/deploy-storybook-prod.yml @@ -0,0 +1,23 @@ +name: Deploy Storybook to Production + +on: + release: + types: + - published + push: + tags: + - prod + +concurrency: + group: ${{ github.workflow }} + +jobs: + deploy: + name: Deploy SB PR + uses: ./.github/workflows/deploy-storybook.yml + with: + aws_cfn_stack_name: ls-ui-reactstarter-storybook-resources-prod + aws_role_arn: ${{ vars.AWS_ROLE_ARN_PROD }} + env: prod + secrets: + env_file: ${{ secrets.ENV_PROD }} diff --git a/.github/workflows/deploy-storybook-qa.yml b/.github/workflows/deploy-storybook-qa.yml new file mode 100644 index 0000000..6e07893 --- /dev/null +++ b/.github/workflows/deploy-storybook-qa.yml @@ -0,0 +1,22 @@ +name: Deploy Storybook to QA + +on: + push: + branches: + - release/* + tags: + - qa + +concurrency: + group: ${{ github.workflow }} + +jobs: + deploy: + name: Deploy SB QA + uses: ./.github/workflows/deploy-storybook.yml + with: + aws_cfn_stack_name: ls-ui-reactstarter-storybook-resources-qa + aws_role_arn: ${{ vars.AWS_ROLE_ARN_QA }} + env: qa + secrets: + env_file: ${{ secrets.ENV_QA }} diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-storybook.yml similarity index 54% rename from .github/workflows/deploy-prod.yml rename to .github/workflows/deploy-storybook.yml index af81041..27757d3 100644 --- a/.github/workflows/deploy-prod.yml +++ b/.github/workflows/deploy-storybook.yml @@ -1,24 +1,33 @@ -name: Deploy to Production +name: Deploy Storybook on: - release: - types: - - published - push: - tags: - - prod - -concurrency: - group: ${{ github.workflow }} - -env: - APP_NAME: react-starter.leanstacks.net - AWS_CFN_STACK_NAME: ls-ui-reactstarter-resources-prod - AWS_CFN_TEMPLATE: template.yml - AWS_ENV_CODE: prod - AWS_REGION: ${{ vars.AWS_REGION }} - AWS_ROLE_ARN: ${{ vars.AWS_ROLE_ARN_PROD }} - ENV_FILE: ${{ secrets.ENV_PROD }} + workflow_call: + inputs: + app_name: + required: false + type: string + default: 'react-starter-storybook.leanstacks.net' + aws_region: + required: false + type: string + default: 'us-east-1' + aws_role_arn: + required: true + type: string + aws_cfn_stack_name: + required: true + type: string + aws_cfn_template: + required: false + type: string + default: '.aws/cfn/storybook.yml' + env: + required: false + type: string + default: 'dev' + secrets: + env_file: + required: true jobs: deploy: @@ -46,43 +55,43 @@ jobs: - name: Create Environment Configuration run: | - echo "${{ env.ENV_FILE }}" > .env + echo "${{ secrets.env_file }}" > .env echo "VITE_BUILD_DATE=$(date +'%Y-%m-%d')" >> .env echo "VITE_BUILD_TIME=$(date +'%H:%M:%S%z')" >> .env echo "VITE_BUILD_TS=$(date +'%Y-%m-%dT%H:%M:%S%z')" >> .env echo "VITE_BUILD_COMMIT_SHA=${{ github.sha }}" >> .env - echo "VITE_BUILD_ENV_CODE=${{ env.AWS_ENV_CODE }}" >> .env + echo "VITE_BUILD_ENV_CODE=${{ inputs.env }}" >> .env echo "VITE_BUILD_WORKFLOW_NAME=${{ github.workflow }}" >> .env echo "VITE_BUILD_WORKFLOW_RUN_NUMBER=${{ github.run_number }}" >> .env echo "VITE_BUILD_WORKFLOW_RUN_ATTEMPT=${{ github.run_attempt }}" >> .env - name: Build - run: npm run build + run: npm run build:storybook - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v4 with: - role-to-assume: ${{ env.AWS_ROLE_ARN }} - aws-region: ${{ env.AWS_REGION }} + role-to-assume: ${{ inputs.aws_role_arn }} + aws-region: ${{ inputs.aws_region }} - name: Deploy AWS CloudFormation Stack run: |- aws cloudformation deploy \ - --stack-name ${{ env.AWS_CFN_STACK_NAME }} \ - --template-file ${{ env.AWS_CFN_TEMPLATE }} \ - --parameter-overrides EnvironmentCode=${{ env.AWS_ENV_CODE }} \ - --tags App=${{ env.APP_NAME }} Env=${{ env.AWS_ENV_CODE }} OU=leanstacks Owner='Matthew Warman' + --stack-name ${{ inputs.aws_cfn_stack_name }} \ + --template-file ${{ inputs.aws_cfn_template }} \ + --parameter-overrides EnvironmentCode=${{ inputs.env }} \ + --tags App=${{ inputs.app_name }} Env=${{ inputs.env }} OU=leanstacks Owner='Matthew Warman' - name: Get CloudFormation Stack Outputs id: cloudformation run: |- APP_BUCKET_NAME=$( aws cloudformation describe-stacks \ - --stack-name ${{ env.AWS_CFN_STACK_NAME }} \ + --stack-name ${{ inputs.aws_cfn_stack_name }} \ --query "Stacks[0].Outputs[?OutputKey=='AppBucketName'].OutputValue | [0]" ) echo "APP_BUCKET_NAME=$APP_BUCKET_NAME" >> "$GITHUB_OUTPUT" - name: Deploy to AWS S3 run: | - aws s3 sync dist s3://${{ steps.cloudformation.outputs.APP_BUCKET_NAME }} --delete + aws s3 sync storybook-static s3://${{ steps.cloudformation.outputs.APP_BUCKET_NAME }} --delete