From df8f5398b7211b424f06639858af06834d0dac78 Mon Sep 17 00:00:00 2001 From: Devin Matte Date: Sun, 4 Feb 2024 17:40:34 -0500 Subject: [PATCH] Set up deployment of service (#7) --- .github/CODEOWNERS | 1 + .github/PULL_REQUEST_TEMPLATE.md | 11 +++ .github/labeler.yml | 11 +++ .github/workflows/deploy.yml | 54 ++++++++++++ cloudformation.json | 145 +++++++++++++++++++++++++++++++ deploy.sh | 36 ++++++++ package-lock.json | 83 +++++++++++++----- package.json | 11 +-- 8 files changed, 325 insertions(+), 27 deletions(-) create mode 100644 .github/CODEOWNERS create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/labeler.yml create mode 100644 .github/workflows/deploy.yml create mode 100644 cloudformation.json create mode 100755 deploy.sh diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..1e6cfa5 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @skaplan-dev diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..98321bf --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,11 @@ +## Motivation + + + +## Changes + + + +## Testing Instructions + + diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000..d2132c2 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,11 @@ +ci/cd: +- .github/**/* +- deploy.sh + +dependencies: +- package.json +- package-lock.json + +documentation: +- README.md +- LICENSE diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..b3d8700 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,54 @@ +name: deploy + +on: + workflow_dispatch: + push: + branches: + - main + +jobs: + deploy: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: ['20'] + env: + AWS_PROFILE: transitmatters + AWS_DEFAULT_REGION: us-east-1 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + TM_LABS_WILDCARD_CERT_ARN: ${{ secrets.TM_LABS_WILDCARD_CERT_ARN }} + DD_API_KEY: ${{ secrets.DD_API_KEY }} + + steps: + - name: Checkout repo + uses: actions/checkout@v3 + - name: Set up Node ${{ matrix.node-version }}.x + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + - name: Set up CI Cache + uses: actions/cache@v3 + with: + path: | + ~/.npm + ${{ github.workspace }}/.next/cache + key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }} + restore-keys: | + ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}- + - name: Check if package-lock.json is up to date + run: | + npx --yes package-lock-utd@1.1.0 + - name: Generate AWS profile + run: | + mkdir ~/.aws + cat >> ~/.aws/credentials << EOF + [$AWS_PROFILE] + aws_access_key_id = $AWS_ACCESS_KEY_ID + aws_secret_access_key = $AWS_SECRET_ACCESS_KEY + EOF + - name: Run deploy shell script + run: | + npm ci + npm run build + bash deploy.sh diff --git a/cloudformation.json b/cloudformation.json new file mode 100644 index 0000000..bbb5ba8 --- /dev/null +++ b/cloudformation.json @@ -0,0 +1,145 @@ +{ + "Parameters": { + "TMFrontendHostname": { + "Type": "String", + "Default": "shutdowns.labs.transitmatters.org", + "AllowedValues": ["shutdowns.labs.transitmatters.org"], + "Description": "The frontend hostname for the shutdown tracker" + }, + "TMFrontendZone": { + "Type": "String", + "Default": "labs.transitmatters.org", + "AllowedPattern": "^labs\\.transitmatters\\.org$", + "Description": "The frontend's DNS zone file name. Most likely labs.transitmatters.org." + }, + "TMFrontendCertArn": { + "Type": "String", + "Description": "The ACM ARN of the frontend certificate." + }, + "DDApiKey": { + "Type": "String", + "Description": "Datadog API key." + }, + "DDTags": { + "Type": "String", + "Description": "Additional Datadog Tags" + }, + "GitVersion": { + "Type": "String", + "Description": "Current Git Id" + } + }, + "Resources": { + "FrontendDNSRecordSet": { + "Type": "AWS::Route53::RecordSet", + "Properties": { + "Name": { "Ref": "TMFrontendHostname" }, + "HostedZoneName": { "Fn::Sub": "${TMFrontendZone}." }, + "AliasTarget": { + "HostedZoneId": "Z2FDTNDATAQYW2", + "DNSName": { + "Fn::GetAtt": ["FrontendCloudFront", "DomainName"] + } + }, + "Type": "A" + } + }, + "FrontendBucket": { + "Type": "AWS::S3::Bucket", + "Properties": { + "PublicAccessBlockConfiguration": { + "BlockPublicPolicy": false, + "RestrictPublicBuckets": false + }, + "BucketName": { "Ref": "TMFrontendHostname" }, + "WebsiteConfiguration": { + "IndexDocument": "index.html" + }, + "Tags": [ + { + "Key": "service", + "Value": "shutdown-tracker" + } + ] + } + }, + "FrontendBucketPolicy": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { "Ref": "FrontendBucket" }, + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "PublicReadForGetBucketObjects", + "Effect": "Allow", + "Principal": "*", + "Action": "s3:GetObject", + "Resource": { "Fn::Join": ["", [{ "Fn::GetAtt": ["FrontendBucket", "Arn"] }, "/*"]] } + } + ] + } + } + }, + "FrontendCloudFront": { + "Type": "AWS::CloudFront::Distribution", + "Properties": { + "DistributionConfig": { + "Aliases": [{ "Ref": "TMFrontendHostname" }], + "Enabled": "true", + "DefaultCacheBehavior": { + "Compress": true, + "ForwardedValues": { + "QueryString": "false" + }, + "TargetOriginId": "only-origin", + "ViewerProtocolPolicy": "redirect-to-https" + }, + "DefaultRootObject": "index.html", + "Origins": [ + { + "CustomOriginConfig": { + "HTTPPort": "80", + "HTTPSPort": "443", + "OriginProtocolPolicy": "http-only" + }, + "DomainName": { + "Fn::Join": [ + "", + [{ "Ref": "TMFrontendHostname" }, ".s3-website-us-east-1.amazonaws.com"] + ] + }, + "Id": "only-origin" + } + ], + "CustomErrorResponses": [ + { + "ErrorCode": "404", + "ResponsePagePath": "/404.html", + "ResponseCode": "404", + "ErrorCachingMinTTL": "86400" + } + ], + "PriceClass": "PriceClass_100", + "ViewerCertificate": { + "MinimumProtocolVersion": "TLSv1.2_2018", + "AcmCertificateArn": { "Ref": "TMFrontendCertArn" }, + "SslSupportMethod": "sni-only" + } + }, + "Tags": [ + { + "Key": "service", + "Value": "shutdown-tracker" + } + ] + } + } + }, + "Outputs": { + "WebsiteURL": { + "Value": "FrontendBucket.WebsiteURL", + "Description": "URL for website hosted on S3" + } + } +} diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..7fa2110 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,36 @@ +#!/bin/bash -x + +if [[ -z "$DD_API_KEY" || -z "$TM_LABS_WILDCARD_CERT_ARN" ]]; then + echo "Must provide DD_API_KEY and TM_LABS_WILDCARD_CERT_ARN in environment" 1>&2 + exit 1 +fi + +STACK_NAME=shutdown-tracker +FRONTEND_HOSTNAME="shutdowns.labs.transitmatters.org" +FRONTEND_ZONE="labs.transitmatters.org" +BUCKET="$FRONTEND_HOSTNAME" +FRONTEND_CERT_ARN="$TM_LABS_WILDCARD_CERT_ARN" + +# Identify the version and commit of the current deploy +GIT_VERSION=`git describe --tags --always` +GIT_SHA=`git rev-parse HEAD` +echo "Deploying version $GIT_VERSION | $GIT_SHA" + +# Adding some datadog tags to get better data +DD_TAGS="git.commit.sha:$GIT_SHA,git.repository_url:github.com/transitmatters/shutdown-tracker" + +npm run build + +# Deploy to cloudformation +aws cloudformation deploy --template-file cloudformation.json --stack-name $STACK_NAME --capabilities CAPABILITY_IAM --no-fail-on-empty-changeset --parameter-overrides \ + TMFrontendHostname=$FRONTEND_HOSTNAME \ + TMFrontendZone=$FRONTEND_ZONE \ + TMFrontendCertArn=$FRONTEND_CERT_ARN \ + DDApiKey=$DD_API_KEY \ + GitVersion=$GIT_VERSION \ + DDTags=$DD_TAGS +aws s3 sync dist/ s3://$BUCKET + +# Grab the cloudfront ID and invalidate its cache +CLOUDFRONT_ID=$(aws cloudfront list-distributions --query "DistributionList.Items[?Aliases.Items!=null] | [?contains(Aliases.Items, '$FRONTEND_HOSTNAME')].Id | [0]" --output text) +aws cloudfront create-invalidation --distribution-id $CLOUDFRONT_ID --paths "/*" diff --git a/package-lock.json b/package-lock.json index 06321d5..6fb74eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,27 +10,28 @@ "dependencies": { "@headlessui/react": "^1.7.18", "@heroicons/react": "^2.1.1", - "@tanstack/react-query": "^5.17.15", + "@tanstack/react-query": "^5.18.1", "bezier-js": "^6.1.4", "chart.js": "^4.4.1", "chartjs-adapter-date-fns": "^3.0.0", "chartjs-plugin-annotation": "^3.0.1", "chartjs-plugin-watermark": "^2.0.2", "classnames": "^2.5.1", - "date-fns": "^3.2.0", + "date-fns": "^3.3.1", "dayjs": "^1.11.10", "react": "^18.2.0", "react-chartjs-2": "^5.2.0", "react-dom": "^18.2.0", "react-responsive": "^9.0.2", + "react-router-dom": "^6.22.0", "react-scroll": "^1.9.0", "react-toggle-dark-mode": "^1.1.1", - "zustand": "^4.4.7" + "zustand": "^4.5.0" }, "devDependencies": { "@types/bezier-js": "^4.1.3", - "@types/react": "^18.2.43", - "@types/react-dom": "^18.2.17", + "@types/react": "^18.2.52", + "@types/react-dom": "^18.2.18", "@types/react-scroll": "^1.8.10", "@typescript-eslint/eslint-plugin": "^6.14.0", "@typescript-eslint/parser": "^6.14.0", @@ -4348,6 +4349,14 @@ } } }, + "node_modules/@remix-run/router": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.0.tgz", + "integrity": "sha512-HOil5aFtme37dVQTB6M34G95kPM3MMuqSmIRVCC52eKV+Y/tGSqw9P3rWhlAx6A+mz+MoX+XxsGsNJbaI5qCgQ==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.9.5", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.5.tgz", @@ -4563,20 +4572,20 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.17.15", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.17.15.tgz", - "integrity": "sha512-QURxpu77/ICA4d61aPvV7EcJ2MwmksxUejKBaq/xLcO2TUJAlXf4PFKHC/WxnVFI/7F1jeLx85AO3Vpk0+uBXw==", + "version": "5.18.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.18.1.tgz", + "integrity": "sha512-fYhrG7bHgSNbnkIJF2R4VUXb4lF7EBiQjKkDc5wOlB7usdQOIN4LxxHpDxyE3qjqIst1WBGvDtL48T0sHJGKCw==", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" } }, "node_modules/@tanstack/react-query": { - "version": "5.17.15", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.17.15.tgz", - "integrity": "sha512-9qur91mOihaUN7pXm6ioDtS+4qgkBcCiIaZyvi3lZNcQZsrMGCYZ+eP3hiFrV4khoJyJrFUX1W0NcCVlgwNZxQ==", + "version": "5.18.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.18.1.tgz", + "integrity": "sha512-PdI07BbsahZ+04PxSuDQsQvBWe008eWFk/YYWzt8fvzt2sALUM0TpAJa/DFpqa7+SSo7j1EQR6Jx6znXNHyaXw==", "dependencies": { - "@tanstack/query-core": "5.17.15" + "@tanstack/query-core": "5.18.1" }, "funding": { "type": "github", @@ -4715,9 +4724,9 @@ "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" }, "node_modules/@types/react": { - "version": "18.2.47", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.47.tgz", - "integrity": "sha512-xquNkkOirwyCgoClNk85BjP+aqnIS+ckAJ8i37gAbDs14jfW/J23f2GItAf33oiUPQnqNMALiFeoM9Y5mbjpVQ==", + "version": "18.2.52", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.52.tgz", + "integrity": "sha512-E/YjWh3tH+qsLKaUzgpZb5AY0ChVa+ZJzF7ogehVILrFpdQk6nC/WXOv0bfFEABbXbgNxLBGU7IIZByPKb6eBw==", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -6269,9 +6278,9 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/date-fns": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.2.0.tgz", - "integrity": "sha512-E4KWKavANzeuusPi0jUjpuI22SURAznGkx7eZV+4i6x2A+IZxAMcajgkvuDAU1bg40+xuhW1zRdVIIM/4khuIg==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.3.1.tgz", + "integrity": "sha512-y8e109LYGgoQDveiEBD3DYXKba1jWf5BA8YU1FL5Tvm0BTdEfy54WLCwnuYWZNnzzvALy/QQ4Hov+Q9RVRv+Zw==", "funding": { "type": "github", "url": "https://github.com/sponsors/kossnocorp" @@ -11534,6 +11543,36 @@ "react": ">=16.8.0" } }, + "node_modules/react-router": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.0.tgz", + "integrity": "sha512-q2yemJeg6gw/YixRlRnVx6IRJWZD6fonnfZhN1JIOhV2iJCPeRNSH3V1ISwHf+JWcESzLC3BOLD1T07tmO5dmg==", + "dependencies": { + "@remix-run/router": "1.15.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.0.tgz", + "integrity": "sha512-z2w+M4tH5wlcLmH3BMMOMdrtrJ9T3oJJNsAlBJbwk+8Syxd5WFJ7J5dxMEW0/GEXD1BBis4uXRrNIz3mORr0ag==", + "dependencies": { + "@remix-run/router": "1.15.0", + "react-router": "6.22.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-scroll": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/react-scroll/-/react-scroll-1.9.0.tgz", @@ -13640,9 +13679,9 @@ "peer": true }, "node_modules/zustand": { - "version": "4.4.7", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.4.7.tgz", - "integrity": "sha512-QFJWJMdlETcI69paJwhSMJz7PPWjVP8Sjhclxmxmxv/RYI7ZOvR5BHX+ktH0we9gTWQMxcne8q1OY8xxz604gw==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.0.tgz", + "integrity": "sha512-zlVFqS5TQ21nwijjhJlx4f9iGrXSL0o/+Dpy4txAP22miJ8Ti6c1Ol1RLNN98BMib83lmDH/2KmLwaNXpjrO1A==", "dependencies": { "use-sync-external-store": "1.2.0" }, @@ -13651,7 +13690,7 @@ }, "peerDependencies": { "@types/react": ">=16.8", - "immer": ">=9.0", + "immer": ">=9.0.6", "react": ">=16.8" }, "peerDependenciesMeta": { diff --git a/package.json b/package.json index cb27a25..b405110 100644 --- a/package.json +++ b/package.json @@ -16,27 +16,28 @@ "dependencies": { "@headlessui/react": "^1.7.18", "@heroicons/react": "^2.1.1", - "@tanstack/react-query": "^5.17.15", + "@tanstack/react-query": "^5.18.1", "bezier-js": "^6.1.4", "chart.js": "^4.4.1", "chartjs-adapter-date-fns": "^3.0.0", "chartjs-plugin-annotation": "^3.0.1", "chartjs-plugin-watermark": "^2.0.2", "classnames": "^2.5.1", - "date-fns": "^3.2.0", + "date-fns": "^3.3.1", "dayjs": "^1.11.10", "react": "^18.2.0", "react-chartjs-2": "^5.2.0", "react-dom": "^18.2.0", "react-responsive": "^9.0.2", + "react-router-dom": "^6.22.0", "react-scroll": "^1.9.0", "react-toggle-dark-mode": "^1.1.1", - "zustand": "^4.4.7" + "zustand": "^4.5.0" }, "devDependencies": { "@types/bezier-js": "^4.1.3", - "@types/react": "^18.2.43", - "@types/react-dom": "^18.2.17", + "@types/react": "^18.2.52", + "@types/react-dom": "^18.2.18", "@types/react-scroll": "^1.8.10", "@typescript-eslint/eslint-plugin": "^6.14.0", "@typescript-eslint/parser": "^6.14.0",