diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1f03d13d6e5..f92d8108e62 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,4 +1,20 @@ name: Deploy to Novu Cloud +run-name: > + Deploying to + ${{ + github.event.inputs.deploy_api == 'true' && 'api, ' || '' + }}${{ + github.event.inputs.deploy_worker == 'true' && 'worker, ' || '' + }}${{ + github.event.inputs.deploy_ws == 'true' && 'ws, ' || '' + }}${{ + github.event.inputs.deploy_webhook == 'true' && 'webhook ' || '' + }}on ${{ github.event.inputs.environment }} +description: | + This workflow deploys the Novu Cloud application to different environments and services based on the selected options. + It builds Docker images, pushes them to Amazon ECR, and deploys them to Amazon ECS. + Additionally, it creates Sentry releases and New Relic deployment markers. + on: workflow_dispatch: inputs: @@ -6,30 +22,30 @@ on: description: 'Environment to deploy to' required: true type: choice - default: development + default: staging options: - - development + - staging - production-us - production-eu - production-both deploy_api: - description: 'Deploy API?' + description: 'Deploy API' required: true type: boolean default: true deploy_worker: - description: 'Deploy Worker?' + description: 'Deploy Worker' required: true type: boolean default: false deploy_ws: - description: 'Deploy WS?' + description: 'Deploy WS' required: true type: boolean default: false deploy_webhook: - description: 'Deploy Webhook?' + description: 'Deploy Webhook' required: true type: boolean default: false @@ -40,34 +56,52 @@ jobs: outputs: env_matrix: ${{ steps.set-matrix.outputs.env_matrix }} service_matrix: ${{ steps.set-matrix.outputs.service_matrix }} + deploy_matrix: ${{ steps.set-matrix.outputs.deploy_matrix }} + nr_matrix: ${{ steps.set-matrix.outputs.nr_matrix }} steps: - - name: Generate Environment & Service Matrices + - name: Validate Selected Services + run: | + if [ "${{ github.event.inputs.deploy_api }}" != "true" ] && \ + [ "${{ github.event.inputs.deploy_worker }}" != "true" ] && \ + [ "${{ github.event.inputs.deploy_ws }}" != "true" ] && \ + [ "${{ github.event.inputs.deploy_webhook }}" != "true" ]; then + echo "Error: At least one service must be selected for deployment." + exit 1 + fi + + - name: Generate Environment, Service, and Deploy Matrices id: set-matrix + env: + WORKER_SERVICE: ${{ vars.WORKER_SERVICE }} run: | envs=() services=() + deploy_matrix=() + nr=() # Collect selected environments - if [ "${{ github.event.inputs.environment }}" == "development" ]; then - envs+=("\"development\"") + if [ "${{ github.event.inputs.environment }}" == "staging" ]; then + envs+=("\"staging-eu\"") fi if [ "${{ github.event.inputs.environment }}" == "production-us" ]; then - envs+=("\"production-us\"") + envs+=("\"prod-us\"") fi if [ "${{ github.event.inputs.environment }}" == "production-eu" ]; then - envs+=("\"production-eu\"") + envs+=("\"prod-eu\"") fi if [ "${{ github.event.inputs.environment }}" == "production-both" ]; then - envs+=("\"production-us\"") - envs+=("\"production-eu\"") + envs+=("\"prod-us\"") + envs+=("\"prod-eu\"") fi # Collect selected services if [ "${{ github.event.inputs.deploy_api }}" == "true" ]; then services+=("\"api\"") + nr+=("\"api\"") fi if [ "${{ github.event.inputs.deploy_worker }}" == "true" ]; then services+=("\"worker\"") + nr+=("\"worker\"") fi if [ "${{ github.event.inputs.deploy_ws }}" == "true" ]; then services+=("\"ws\"") @@ -76,26 +110,68 @@ jobs: services+=("\"webhook\"") fi + # Parse service secrets and generate deploy_matrix + for service in "${services[@]}"; do + if [ "$service" == "\"worker\"" ]; then + IFS=',' read -r -a worker_services <<< "$WORKER_SERVICE" + for worker_service in $(echo "$WORKER_SERVICE" | jq -c '.[]'); do + cluster_name=$(echo "$worker_service" | jq -r '.cluster_name') + container_name=$(echo "$worker_service" | jq -r '.container_name') + service_name=$(echo "$worker_service" | jq -r '.service') + task_name=$(echo "$worker_service" | jq -r '.task_name') + image=$(echo "$worker_service" | jq -r '.image') + deploy_matrix+=("{\"cluster_name\": \"$cluster_name\", \"container_name\": \"$container_name\", \"service_name\": \"$service_name\", \"task_name\": \"$task_name\", \"image\": \"$image\"}") + done + elif [ "$service" == "\"api\"" ]; then + cluster_name=api-cluster + container_name=api-container + service_name=api-service + task_name=api-task + image=api + deploy_matrix+=("{\"cluster_name\": \"$cluster_name\", \"container_name\": \"$container_name\", \"service_name\": \"$service_name\", \"task_name\": \"$task_name\", \"image\": \"$image\"}") + elif [ "$service" == "\"ws\"" ]; then + cluster_name=ws-cluster + container_name=ws-container + service_name=ws-service + task_name=ws-task + image=ws + deploy_matrix+=("{\"cluster_name\": \"$cluster_name\", \"container_name\": \"$container_name\", \"service_name\": \"$service_name\", \"task_name\": \"$task_name\", \"image\": \"$image\"}") + elif [ "$service" == "\"webhook\"" ]; then + cluster_name=webhook-cluster + container_name=webhook-container + service_name=webhook-service + task_name=webhook-task + image=webhook + deploy_matrix+=("{\"cluster_name\": \"$cluster_name\", \"container_name\": \"$container_name\", \"service_name\": \"$service_name\", \"task_name\": \"$task_name\", \"image\": \"$image\"}") + fi + done + env_matrix="{\"environment\": [$( IFS=','; echo "${envs[*]}" )]}" service_matrix="{\"service\": [$( IFS=','; echo "${services[*]}" )]}" - + deploy_matrix="[$( + IFS=','; echo "${deploy_matrix[*]}" + )]" + nr_matrix="[$( + IFS=','; echo "${nr[*]}" + )]" echo "env_matrix=$env_matrix" >> $GITHUB_OUTPUT echo "service_matrix=$service_matrix" >> $GITHUB_OUTPUT + echo "deploy_matrix=$deploy_matrix" >> $GITHUB_OUTPUT + echo "nr_matrix=$nr_matrix" >> $GITHUB_OUTPUT build: needs: prepare-matrix timeout-minutes: 60 runs-on: ubuntu-latest - outputs: - docker_image: ${{ steps.build-image.outputs.IMAGE }} + environment: ${{ fromJson(needs.prepare-matrix.outputs.env_matrix).environment[0] }} strategy: matrix: service: ${{ fromJson(needs.prepare-matrix.outputs.service_matrix).service }} - + steps: - name: Checkout uses: actions/checkout@v4 @@ -124,24 +200,17 @@ jobs: uses: docker/setup-buildx-action@v3 with: driver-opts: 'image=moby/buildkit:v0.13.1' - + - name: Prepare Variables - run: | - set -e - if [[ "$(echo ${{ fromJson(needs.prepare-matrix.outputs.env_matrix).environment }})" == "development" ]]; then - echo "AWS_REGION=eu-west-2" >> $GITHUB_ENV - else - echo "AWS_REGION=us-east-1" >> $GITHUB_ENV - fi - echo "BULL_MQ_PRO_NPM_TOKEN=${{ secrets.BULL_MQ_PRO_NPM_TOKEN }}" >> $GITHUB_ENV + run: echo "BULL_MQ_PRO_NPM_TOKEN=${{ secrets.BULL_MQ_PRO_NPM_TOKEN }}" >> $GITHUB_ENV - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID}} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: ${{ env.AWS_REGION }} - + aws-region: ${{ vars.AWS_REGION }} + - name: Login to Amazon ECR id: login-ecr uses: aws-actions/amazon-ecr-login@v2 @@ -150,23 +219,19 @@ jobs: id: build-image env: REGISTRY: ${{ steps.login-ecr.outputs.registry }} - REPOSITORY: novu/${{ matrix.service }} + REPOSITORY: ${{ vars.ECR_PREFIX }} SERVICE: ${{ matrix.service }} IMAGE_TAG: ${{ github.sha }} DOCKER_BUILD_ARGUMENTS: > --platform=linux/amd64 - --output=type=image,name=$REGISTRY/$REPOSITORY,push-by-digest=true,name-canonical=true + --output=type=image,name=$REGISTRY/$REPOSITORY/$SERVICE,push-by-digest=true,name-canonical=true run: | cp scripts/dotenvcreate.mjs apps/$SERVICE/src/dotenvcreate.mjs - cd apps/$SERVICE && pnpm --silent --workspace-root pnpm-context -- apps/$SERVICE/Dockerfile | BULL_MQ_PRO_NPM_TOKEN=${BULL_MQ_PRO_NPM_TOKEN} docker buildx build --secret id=BULL_MQ_PRO_NPM_TOKEN --build-arg PACKAGE_PATH=apps/$SERVICE - -t novu-$SERVICE --load $DOCKER_BUILD_ARGUMENTS - docker tag novu-$SERVICE $REGISTRY/$REPOSITORY:latest - docker tag novu-$SERVICE $REGISTRY/$REPOSITORY:prod - docker tag novu-$SERVICE $REGISTRY/$REPOSITORY:$IMAGE_TAG - - docker push $REGISTRY/$REPOSITORY:prod - docker push $REGISTRY/$REPOSITORY:latest - docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG - echo "IMAGE=$REGISTRY/$REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT + cd apps/$SERVICE && pnpm run docker:build + docker tag novu-$SERVICE $REGISTRY/$REPOSITORY/$SERVICE:latest + docker tag novu-$SERVICE $REGISTRY/$REPOSITORY/$SERVICE:$IMAGE_TAG + docker push $REGISTRY/$REPOSITORY/$SERVICE:latest + docker push $REGISTRY/$REPOSITORY/$SERVICE:$IMAGE_TAG deploy: needs: [build, prepare-matrix] @@ -174,9 +239,103 @@ jobs: strategy: matrix: env: ${{ fromJson(needs.prepare-matrix.outputs.env_matrix).environment }} - service: ${{ fromJson(needs.prepare-matrix.outputs.service_matrix).service }} + service: ${{ fromJson(needs.prepare-matrix.outputs.deploy_matrix) }} + + environment: ${{ matrix.env }} + steps: - - name: Print Important Info + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ vars.AWS_REGION }} + + - name: Download task definition + env: + ECS_PREFIX: ${{ vars.ECS_PREFIX }} + TASK_NAME: ${{ matrix.service.task_name }} run: | - echo "Deploying ${{ matrix.service }} to ${{ matrix.env }}" - echo "Docker Image: ${{ needs.build.outputs.docker_image }}" + aws ecs describe-task-definition --task-definition ${ECS_PREFIX}-${TASK_NAME} \ + --query taskDefinition > task-definition.json + + - name: Render Amazon ECS task definition + id: render-web-container + uses: aws-actions/amazon-ecs-render-task-definition@39c13cf530718ffeb524ec8ee0c15882bcb13842 + with: + task-definition: task-definition.json + container-name: ${{ vars.ECS_PREFIX }}-${{ matrix.service.container_name }} + image: ${{secrets.ECR_URI}}/${{ vars.ECR_PREFIX }}/${{ matrix.service.image }}:${{ github.sha }} + + - name: Deploy to Amazon ECS service + uses: aws-actions/amazon-ecs-deploy-task-definition@3e7310352de91b71a906e60c22af629577546002 + with: + task-definition: ${{ steps.render-web-container.outputs.task-definition }} + service: ${{ vars.ECS_PREFIX }}-${{ matrix.service.service_name }} + cluster: ${{ vars.ECS_PREFIX }}-${{ matrix.service.cluster_name }} + wait-for-service-stability: true + + + sentry_release: + needs: [deploy, prepare-matrix] + runs-on: ubuntu-latest + strategy: + matrix: + service: ${{ fromJson(needs.prepare-matrix.outputs.service_matrix).service }} + environment: ${{ fromJson(needs.prepare-matrix.outputs.env_matrix).environment[0] }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Get NPM Version + id: package-version + uses: martinbeentjes/npm-get-version-action@main + with: + path: apps/${{ matrix.service }} + + - name: Create Sentry release + uses: getsentry/action-release@v1 + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_ORG: ${{ vars.SENTRY_ORG }} + SENTRY_PROJECT: ${{ matrix.service }} + with: + version: ${{ steps.package-version.outputs.current-version}} + version_prefix: v + environment: ${{vars.SENTRY_ENV}} + ignore_empty: true + ignore_missing: true + + new_relic_release: + needs: [deploy, prepare-matrix] + if: ${{ fromJson(needs.prepare-matrix.outputs.nr_matrix) != '[]' }} + runs-on: ubuntu-latest + strategy: + matrix: + env: ${{ fromJson(needs.prepare-matrix.outputs.env_matrix).environment }} + nr: ${{ fromJson(needs.prepare-matrix.outputs.nr_matrix) }} + environment: ${{ matrix.env }} + + steps: + - name: New Relic Application Deployment Marker + uses: newrelic/deployment-marker-action@v2.3.0 + with: + region: EU + apiKey: ${{ secrets.NEW_RELIC_API_KEY }} + guid: ${{ matrix.nr == 'api' && secrets.NEW_RELIC_API_GUID || matrix.nr == 'worker' && secrets.NEW_RELIC_Worker_GUID }} + version: '${{ github.sha }}' + user: '${{ github.actor }}' + description: 'Novu Cloud Deployment' + + sync_novu_state: + needs: [deploy, prepare-matrix] + runs-on: ubuntu-latest + if: github.event.inputs.deploy_api == 'true' + environment: ${{ fromJson(needs.prepare-matrix.outputs.env_matrix).environment[0] }} + steps: + - name: Sync State to Novu + uses: novuhq/actions-novu-sync@v2 + with: + secret-key: ${{ secrets.NOVU_INTERNAL_SECRET_KEY }} + bridge-url: ${{ vars.NOVU_BRIDGE_URL }} diff --git a/.github/workflows/rollback.yml b/.github/workflows/rollback.yml index f504ad0b270..7e5ee1fa397 100644 --- a/.github/workflows/rollback.yml +++ b/.github/workflows/rollback.yml @@ -1,288 +1,245 @@ -name: Rollback -run-name: Rollback the ${{ inputs.service }} service in the ${{ inputs.environment }} environment - -env: - NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} +name: Rollback Deployment +run-name: > + Rollback + ${{ + github.event.inputs.rollback_api == 'true' && 'api, ' || '' + }}${{ + github.event.inputs.rollback_worker == 'true' && 'worker, ' || '' + }}${{ + github.event.inputs.rollback_ws == 'true' && 'ws, ' || '' + }}${{ + github.event.inputs.rollback_webhook == 'true' && 'webhook ' || '' + }}on ${{ github.event.inputs.environment }} +description: Rollback deployment to the previous task definition for selected services in the specified environment. +concurrency: + group: "rollback-${{ github.event.inputs.environment }}" on: workflow_dispatch: inputs: - service: - type: choice - description: Select service to rollback. - options: - - inbound_mail - - api - - web - - webhook - - widget - - worker - - ws environment: + description: 'Environment to rollback' + required: true type: choice - description: Select the environment + default: staging options: - - Production - - Development - region: + - staging + - production-us + - production-eu + - production-both + + rollback_api: + description: 'Rollback API' + required: true + type: boolean + default: true + + rollback_worker: + description: 'Rollback Worker' + required: true + type: boolean + default: true + + rollback_ws: + description: 'Rollback WS' + required: true + type: boolean + default: true + + rollback_webhook: + description: 'Rollback Webhook' + required: true + type: boolean + default: true + + rollback_signoff: + description: "This will rollback the selected services to the previous task definition. This won't rollback any database migration or environment changes. Do you agree?" + required: true type: choice - description: Select the environment region. Required only in production. + default: 'I do not agree' options: - - [EU, US] - - [EU] - - [US] - mode: - type: choice - description: The Rollback mode. You can roll back to the previously deployed version or to the version that has the current commit hash of this branch in an image tag name or a deployment info. - options: - - Previous Version - - Commit Hash + - 'I agree' + - 'I do not agree' + jobs: - ecs: - if: contains(fromJson('["api", "inbound_mail", "webhook", "worker", "ws"]'), github.event.inputs.service) + prepare-matrix: runs-on: ubuntu-latest - strategy: - matrix: - region: ${{ fromJSON(github.event.inputs.region) }} - timeout-minutes: 60 - environment: ${{ github.event.inputs.environment }} - permissions: - contents: read - packages: write - deployments: write + if : "${{ github.event.inputs.rollback_signoff == 'I agree' }}" + outputs: + env_matrix: ${{ steps.set-matrix.outputs.env_matrix }} + service_matrix: ${{ steps.set-matrix.outputs.service_matrix }} + rollback_matrix: ${{ steps.set-matrix.outputs.rollback_matrix }} steps: - - run: echo "Rolling back ${{ github.event.inputs.service }} in ${{ github.event.inputs.environment }}" - - - id: commit - if: contains(fromJson('["Commit Hash"]'), github.event.inputs.mode) - uses: prompt/actions-commit-hash@v3 - - - name: Prepare variables - id: variables + - name: Validate Selected Services run: | - if [[ "${{ matrix.region }}" == "EU" && "${{ github.event.inputs.environment }}" == "Production" ]]; then - echo "Using Terraform Workspace: novu-prod-eu" - echo "TF_WORKSPACE=novu-prod-eu" >> $GITHUB_ENV - elif [[ "${{ matrix.region }}" == "US" && "${{ github.event.inputs.environment }}" == "Production" ]]; then - echo "Using Terraform Workspace: novu-prod" - echo "TF_WORKSPACE=novu-prod" >> $GITHUB_ENV - elif [[ "${{ matrix.region }}" == "EU" && "${{ github.event.inputs.environment }}" == "Development" ]]; then - echo "Using Terraform Workspace: novu-dev" - echo "TF_WORKSPACE=novu-dev" >> $GITHUB_ENV - elif [[ "${{ matrix.region }}" == "US" && "${{ github.event.inputs.environment }}" == "Development" ]]; then - echo "Using Terraform Workspace: novu-dev" - echo "TF_WORKSPACE=novu-dev" >> $GITHUB_ENV - echo "Error: Development environment doesn't exist in the US region." >&2 + if [ "${{ github.event.inputs.rollback_api }}" != "true" ] && \ + [ "${{ github.event.inputs.rollback_worker }}" != "true" ] && \ + [ "${{ github.event.inputs.rollback_ws }}" != "true" ] && \ + [ "${{ github.event.inputs.rollback_webhook }}" != "true" ]; then + echo "Error: At least one service must be selected for rollback." exit 1 - else - echo "Using Terraform Workspace: novu-dev" - echo "TF_WORKSPACE=novu-dev" >> $GITHUB_ENV fi - - - name: Checkout cloud infra - uses: actions/checkout@master - with: - repository: novuhq/cloud-infra - token: ${{ secrets.GH_PACKAGES }} - path: cloud-infra - - - name: Terraform setup - uses: hashicorp/setup-terraform@v3 - with: - cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }} - terraform_version: 1.5.5 - terraform_wrapper: false - - - name: Terraform Init - working-directory: cloud-infra/terraform/novu/aws - run: terraform init - - - name: Terraform get output - working-directory: cloud-infra/terraform/novu/aws - id: terraform - env: - SERVICE_NAME: ${{ github.event.inputs.service }} + + - name: Generate Environment, Service, and Rollback Matrices + id: set-matrix + env: + WORKER_SERVICE: ${{ vars.WORKER_SERVICE }} run: | - echo "ecs_container_name=$(terraform output -json ${{ env.SERVICE_NAME }}_ecs_container_name | jq -r .)" >> $GITHUB_ENV - echo "ecs_service=$(terraform output -json ${{ env.SERVICE_NAME }}_ecs_service | jq -r .)" >> $GITHUB_ENV - echo "ecs_cluster=$(terraform output -json ${{ env.SERVICE_NAME }}_ecs_cluster | jq -r .)" >> $GITHUB_ENV - echo "task_name=$(terraform output -json ${{ env.SERVICE_NAME }}_task_name | jq -r .)" >> $GITHUB_ENV - echo "aws_region=$(terraform output -json aws_region | jq -r .)" >> $GITHUB_ENV - - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: ${{ env.aws_region }} + envs=() + services=() + rollback_matrix=() - - name: ECS get output - if: contains(fromJson('["Previous Version"]'), github.event.inputs.mode) - id: ecs-output - run: | - echo "Retrieving current_task_definition_arn..." - current_task_definition_arn=$(aws ecs describe-services --cluster ${{ env.ecs_cluster }} --services ${{ env.ecs_service }} --query 'services[0].taskDefinition' --output text) - echo "current_task_definition_arn=$current_task_definition_arn" >> $GITHUB_ENV - - echo "Retrieving task_definition_family..." - task_definition_family=$(aws ecs describe-task-definition --task-definition ${{ env.task_name }} --query 'taskDefinition.family' --output text) - echo "task_definition_family=$task_definition_family" >> $GITHUB_ENV - - echo "Retrieving task_definition_list..." - task_definition_list=$(aws ecs list-task-definitions --family-prefix "${task_definition_family}" --output text --sort DESC | grep 'TASKDEFINITIONARNS' | cut -f 2) - task_definition_list_formatted=$(echo "$task_definition_list" | tr '\n' '|') # Replace newline with '|' - echo "task_definition_list=$task_definition_list_formatted" >> $GITHUB_ENV + # Collect selected environments + if [ "${{ github.event.inputs.environment }}" == "staging" ]; then + envs+=("\"staging-eu\"") + fi + if [ "${{ github.event.inputs.environment }}" == "production-us" ]; then + envs+=("\"prod-us\"") + fi + if [ "${{ github.event.inputs.environment }}" == "production-eu" ]; then + envs+=("\"prod-eu\"") + fi + if [ "${{ github.event.inputs.environment }}" == "production-both" ]; then + envs+=("\"prod-us\"") + envs+=("\"prod-eu\"") + fi - if [ -n "$task_definition_list" ]; then - echo "Retrieving previous_task_definition_arn..." - index=$(echo "$task_definition_list" | grep -n "$current_task_definition_arn" | cut -d ':' -f 1) - if [ -n "$index" ]; then - if [ "$index" -ge 1 ]; then # Greater than or equal to 1 - previous_index=$((index + 1)) - previous_task_definition_arn=$(echo "$task_definition_list" | sed -n "${previous_index}p") - echo "previous_task_definition_arn=$previous_task_definition_arn" >> $GITHUB_ENV - else - echo "Invalid index value: $index" - fi - else - echo "Previous task definition not found. It seems to me someone deleted the current task from the list and that is why I can't find the previous task." - exit 1 - fi - else - echo "No task definitions found." - exit 1 + # Collect selected services + if [ "${{ github.event.inputs.rollback_api }}" == "true" ]; then + services+=("\"api\"") + fi + if [ "${{ github.event.inputs.rollback_worker }}" == "true" ]; then + services+=("\"worker\"") + fi + if [ "${{ github.event.inputs.rollback_ws }}" == "true" ]; then + services+=("\"ws\"") + fi + if [ "${{ github.event.inputs.rollback_webhook }}" == "true" ]; then + services+=("\"webhook\"") fi - - name: ECS get output by using commit hash - if: contains(fromJson('["Commit Hash"]'), github.event.inputs.mode) - id: ecs-output-commit-hash - env: - IMAGE_TAG: ${{ steps.commit.outputs.hash }} - run: | - task_definition_family=$(aws ecs describe-task-definition --task-definition ${{ env.task_name }} --query 'taskDefinition.family' --output text) - task_definition_arns=$(aws ecs list-task-definitions --family-prefix "${task_definition_family}" --query 'taskDefinitionArns' --output text --sort DESC) - found=false - for arn in $(echo "$task_definition_arns" | tr '\t' '\n' | head -n 20); do - task_definition=$(aws ecs describe-task-definition --task-definition $arn) - if echo "$task_definition" | grep -q "$IMAGE_TAG"; then - echo "Found task definition with image tag $IMAGE_TAG: $arn" - found=true - needed_arn=$arn - break + # Parse service secrets and generate rollback_matrix + for service in "${services[@]}"; do + if [ "$service" == "\"worker\"" ]; then + IFS=',' read -r -a worker_services <<< "$WORKER_SERVICE" + for worker_service in $(echo "$WORKER_SERVICE" | jq -c '.[]'); do + cluster_name=$(echo "$worker_service" | jq -r '.cluster_name') + container_name=$(echo "$worker_service" | jq -r '.container_name') + service_name=$(echo "$worker_service" | jq -r '.service') + task_name=$(echo "$worker_service" | jq -r '.task_name') + image=$(echo "$worker_service" | jq -r '.image') + rollback_matrix+=("{\"cluster_name\": \"$cluster_name\", \"container_name\": \"$container_name\", \"service_name\": \"$service_name\", \"task_name\": \"$task_name\", \"image\": \"$image\"}") + done + elif [ "$service" == "\"api\"" ]; then + cluster_name=api-cluster + container_name=api-container + service_name=api-service + task_name=api-task + image=api + rollback_matrix+=("{\"cluster_name\": \"$cluster_name\", \"container_name\": \"$container_name\", \"service_name\": \"$service_name\", \"task_name\": \"$task_name\", \"image\": \"$image\"}") + elif [ "$service" == "\"ws\"" ]; then + cluster_name=ws-cluster + container_name=ws-container + service_name=ws-service + task_name=ws-task + image=ws + rollback_matrix+=("{\"cluster_name\": \"$cluster_name\", \"container_name\": \"$container_name\", \"service_name\": \"$service_name\", \"task_name\": \"$task_name\", \"image\": \"$image\"}") + elif [ "$service" == "\"webhook\"" ]; then + cluster_name=webhook-cluster + container_name=webhook-container + service_name=webhook-service + task_name=webhook-task + image=webhook + rollback_matrix+=("{\"cluster_name\": \"$cluster_name\", \"container_name\": \"$container_name\", \"service_name\": \"$service_name\", \"task_name\": \"$task_name\", \"image\": \"$image\"}") fi done - if [ "$found" = false ]; then - echo "Error: Task definition with image tag $IMAGE_TAG not found within the last 20 tasks." - exit 1 - fi - current_task_definition_arn=$(aws ecs describe-services --cluster ${{ env.ecs_cluster }} --services ${{ env.ecs_service }} --query 'services[0].taskDefinition' --output text) - echo "current_task_definition_arn=$current_task_definition_arn" >> $GITHUB_ENV - echo "previous_task_definition_arn=$needed_arn" >> $GITHUB_ENV - echo "Your task definition ARN is $needed_arn" - - name: Rollback a service to the previous task definition - id: rollback - env: - PREVIOUS_TASK: ${{ env.previous_task_definition_arn }} - CURRENT_TASK: ${{ env.current_task_definition_arn }} - run: | - aws ecs update-service --cluster ${{ env.ecs_cluster }} --service ${{ env.ecs_service }} --task-definition ${{ env.PREVIOUS_TASK }} - aws ecs wait services-stable --cluster ${{ env.ecs_cluster }} --service ${{ env.ecs_service }} - echo "After Rollback:" - echo "The previous task definition: $(echo $CURRENT_TASK | awk -F'task-definition/' '{print $2}')" - echo "The current task definition: $(echo $PREVIOUS_TASK | awk -F'task-definition/' '{print $2}')" - - netlify: - if: contains(fromJson('["web", "widget"]'), github.event.inputs.service) + env_matrix="{\"environment\": [$( + IFS=','; echo "${envs[*]}" + )]}" + service_matrix="{\"service\": [$( + IFS=','; echo "${services[*]}" + )]}" + rollback_matrix="[$( + IFS=','; echo "${rollback_matrix[*]}" + )]" + echo "env_matrix=$env_matrix" >> $GITHUB_OUTPUT + echo "service_matrix=$service_matrix" >> $GITHUB_OUTPUT + echo "rollback_matrix=$rollback_matrix" >> $GITHUB_OUTPUT + + rollback: + needs: [prepare-matrix] runs-on: ubuntu-latest strategy: matrix: - region: ${{ fromJSON(github.event.inputs.region) }} - timeout-minutes: 60 - environment: ${{ github.event.inputs.environment }} - permissions: - contents: read - packages: write - deployments: write - env: - NETLIFY_ACCESS_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} - steps: - - run: echo "Rolling back ${{ github.event.inputs.service }} in ${{ github.event.inputs.environment }}" - - - id: commit-netlify - if: contains(fromJson('["Commit Hash"]'), github.event.inputs.mode) - uses: prompt/actions-commit-hash@v3 - - - name: Prepare variables - id: variables - run: | - if [[ "${{ github.event.inputs.service }}" == "widget" && "${{ github.event.inputs.environment }}" == "Development" && "${{ matrix.region }}" == "EU" ]]; then - echo "Using netlify_site_id: b9147448-b835-4eb1-a2f0-11102f611f5f" - echo "netlify_site_id=b9147448-b835-4eb1-a2f0-11102f611f5f" >> $GITHUB_ENV - elif [[ "${{ github.event.inputs.service }}" == "web" && "${{ github.event.inputs.environment }}" == "Development" && "${{ matrix.region }}" == "EU" ]]; then - echo "Using netlify_site_id: 45396446-dc86-4ad6-81e4-86d3eb78d06f" - echo "netlify_site_id=45396446-dc86-4ad6-81e4-86d3eb78d06f" >> $GITHUB_ENV - elif [[ "${{ github.event.inputs.environment }}" == "Development" && "${{ matrix.region }}" == "US" ]]; then - echo "Error: Development environment doesn't exist in the US region." >&2 - exit 1 - elif [[ "${{ github.event.inputs.service }}" == "web" && "${{ github.event.inputs.environment }}" == "Production" && "${{ matrix.region }}" == "EU" ]]; then - echo "Using netlify_site_id: d2e8b860-7016-4202-9256-ebca0f13259a" - echo "netlify_site_id=d2e8b860-7016-4202-9256-ebca0f13259a" >> $GITHUB_ENV - elif [[ "${{ github.event.inputs.service }}" == "web" && "${{ github.event.inputs.environment }}" == "Production" && "${{ matrix.region }}" == "US" ]]; then - echo "Using netlify_site_id: 8639d8b9-81f9-44c3-b885-585a7fd2b5ff" - echo "netlify_site_id=8639d8b9-81f9-44c3-b885-585a7fd2b5ff" >> $GITHUB_ENV - elif [[ "${{ github.event.inputs.service }}" == "widget" && "${{ github.event.inputs.environment }}" == "Production" && "${{ matrix.region }}" == "EU" ]]; then - echo "Using netlify_site_id: 20a64bdd-1934-4284-875f-862410c69a3b" - echo "netlify_site_id=20a64bdd-1934-4284-875f-862410c69a3b" >> $GITHUB_ENV - elif [[ "${{ github.event.inputs.service }}" == "widget" && "${{ github.event.inputs.environment }}" == "Production" && "${{ matrix.region }}" == "US" ]]; then - echo "Using netlify_site_id: 6f927fd4-dcb0-4cf3-8c0b-8c5539d0d034" - echo "netlify_site_id=6f927fd4-dcb0-4cf3-8c0b-8c5539d0d034" >> $GITHUB_ENV - fi - - - name: Get Current Deploy ID - if: contains(fromJson('["Previous Version"]'), github.event.inputs.mode) - id: get_current_deploy - env: - NETLIFY_SITE_ID: ${{ env.netlify_site_id }} - run: | - response=$(curl -s -H "Authorization: Bearer $NETLIFY_ACCESS_TOKEN" "https://api.netlify.com/api/v1/sites/${NETLIFY_SITE_ID}") - current_deploy_id=$(echo "$response" | jq -r '.published_deploy.id') - echo "current_deploy_id=$current_deploy_id" >> $GITHUB_ENV + env: ${{ fromJson(needs.prepare-matrix.outputs.env_matrix).environment }} + service: ${{ fromJson(needs.prepare-matrix.outputs.rollback_matrix) }} - - name: Find Previous Production Deployments and Determine Previous Deploy ID - if: contains(fromJson('["Previous Version"]'), github.event.inputs.mode) - id: previous_deploy_id - env: - NETLIFY_SITE_ID: ${{ env.netlify_site_id }} - run: | - response=$(curl -s -H "Authorization: Bearer $NETLIFY_ACCESS_TOKEN" "https://api.netlify.com/api/v1/sites/${NETLIFY_SITE_ID}/deploys?per_page=100") - deploy_ids=$(echo "$response" | jq -r '.[] | select(.context == "production" and .state == "ready" and .published_at != null) | .id' | sort) - current_index=$(echo "$deploy_ids" | grep -n "$current_deploy_id" | cut -d ":" -f 1) - previous_index=$((current_index - 1)) - previous_deploy_id=$(echo "$deploy_ids" | sed "${previous_index}q;d") - echo "previous_deploy_id=$previous_deploy_id" >> $GITHUB_ENV + environment: ${{ matrix.env }} - - name: Determine Previous Deploy ID - if: contains(fromJson('["Commit Hash"]'), github.event.inputs.mode) - env: - NETLIFY_SITE_ID: ${{ env.netlify_site_id }} - COMMIT_REF: ${{ steps.commit-netlify.outputs.hash }} - run: | - response=$(curl -s -H "Authorization: Bearer $NETLIFY_ACCESS_TOKEN" "https://api.netlify.com/api/v1/sites/$NETLIFY_SITE_ID/deploys") - deploy_id=$(echo "$response" | jq -r ".[] | select(.commit_ref == \"$COMMIT_REF\") | .id") - if [ -n "$deploy_id" ]; then - echo "Deploy ID for commit $COMMIT_REF: $deploy_id" - echo "previous_deploy_id=$deploy_id" >> $GITHUB_ENV - else - echo "Deploy not found for commit $COMMIT_REF" + steps: + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ vars.AWS_REGION }} + + - name: ECS get output + env: + TASK_NAME: ${{ vars.ECS_PREFIX }}-${{ matrix.service.task_name }} + CONTAINER_NAME: ${{ vars.ECS_PREFIX }}-${{ matrix.service.container_name }} + SERVICE_NAME: ${{ vars.ECS_PREFIX }}-${{ matrix.service.service_name }} + CLUSTER_NAME: ${{ vars.ECS_PREFIX }}-${{ matrix.service.cluster_name }} + id: ecs-output + run: | + echo "Retrieving current_task_definition_arn..." + current_task_definition_arn=$(aws ecs describe-services --cluster ${CLUSTER_NAME} --services ${SERVICE_NAME} --query 'services[0].taskDefinition' --output text) + echo "current_task_definition_arn=$current_task_definition_arn" >> $GITHUB_ENV + + echo "Retrieving task_definition_family..." + task_definition_family=$(aws ecs describe-task-definition --task-definition ${TASK_NAME} --query 'taskDefinition.family' --output text) + echo "task_definition_family=$task_definition_family" >> $GITHUB_ENV + + echo "Retrieving task_definition_list..." + task_definition_list=$(aws ecs list-task-definitions --family-prefix "${task_definition_family}" --output text --sort DESC | grep 'TASKDEFINITIONARNS' | cut -f 2) + task_definition_list_formatted=$(echo "$task_definition_list" | tr '\n' '|') # Replace newline with '|' + echo "task_definition_list=$task_definition_list_formatted" >> $GITHUB_ENV + + if [ -n "$task_definition_list" ]; then + echo "Retrieving previous_task_definition_arn..." + index=$(echo "$task_definition_list" | grep -n "$current_task_definition_arn" | cut -d ':' -f 1) + if [ -n "$index" ]; then + if [ "$index" -ge 1 ]; then # Greater than or equal to 1 + previous_index=$((index + 1)) + previous_task_definition_arn=$(echo "$task_definition_list" | sed -n "${previous_index}p") + echo "previous_task_definition_arn=$previous_task_definition_arn" >> $GITHUB_ENV + else + echo "Invalid index value: $index" + fi + else + echo "Previous task definition not found. It seems to me someone deleted the current task from the list and that is why I can't find the previous task." + exit 1 + fi + else + echo "No task definitions found." exit 1 - fi - - - name: Rollback to Previous Deploy - if: env.previous_deploy_id != null - env: - NETLIFY_SITE_ID: ${{ env.netlify_site_id }} - run: | - echo "Restoring previous deploy..." - curl -X POST -H "Authorization: Bearer $NETLIFY_ACCESS_TOKEN" "https://api.netlify.com/api/v1/sites/${{ env.netlify_site_id }}/deploys/${{ env.previous_deploy_id }}/restore" + fi + + - name: Rollback a service to the previous task definition + id: rollback-service + env: + PREVIOUS_TASK: ${{ env.previous_task_definition_arn }} + CURRENT_TASK: ${{ env.current_task_definition_arn }} + SERVICE_NAME: ${{ vars.ECS_PREFIX }}-${{ matrix.service.service_name }} + CLUSTER_NAME: ${{ vars.ECS_PREFIX }}-${{ matrix.service.cluster_name }} + run: | + aws ecs update-service --cluster ${CLUSTER_NAME} --service ${SERVICE_NAME} --task-definition ${{ env.PREVIOUS_TASK }} + aws ecs wait services-stable --cluster ${CLUSTER_NAME} --service ${SERVICE_NAME} + echo "After Rollback:" + echo "The previous task definition: $(echo $CURRENT_TASK | awk -F'task-definition/' '{print $2}')" + echo "The current task definition: $(echo $PREVIOUS_TASK | awk -F'task-definition/' '{print $2}')" + echo "Rollback completed successfully." + + \ No newline at end of file diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile index 88ccd7362f5..1b56f7f3c6b 100644 --- a/apps/api/Dockerfile +++ b/apps/api/Dockerfile @@ -1,17 +1,4 @@ -FROM node:20-alpine3.19 AS dev_base -RUN apk add g++ make py3-pip - -ENV NX_DAEMON=false - -RUN npm i pm2 -g -RUN npm --no-update-notifier --no-fund --global install pnpm@9.11.0 -RUN pnpm --version - -USER 1000 -WORKDIR /usr/src/app - -# ------- DEV BUILD ---------- -FROM dev_base AS dev +FROM ghcr.io/novuhq/novu/base:1.0.0 AS dev ARG PACKAGE_PATH COPY --chown=1000:1000 ./meta . @@ -48,7 +35,7 @@ WORKDIR /usr/src/app RUN rm -rf node_modules && pnpm recursive exec -- rm -rf ./src ./node_modules # ------- PRODUCTION BUILD ---------- -FROM dev_base AS prod +FROM ghcr.io/novuhq/novu/base:1.0.0 AS prod ARG PACKAGE_PATH diff --git a/apps/worker/Dockerfile b/apps/worker/Dockerfile index e7cf7ced4ca..57e1957ebdc 100644 --- a/apps/worker/Dockerfile +++ b/apps/worker/Dockerfile @@ -2,10 +2,12 @@ FROM node:20-alpine3.19 AS dev_base RUN apk --update --no-cache add curl g++ make py3-pip ENV NX_DAEMON=false + RUN npm i pm2 -g RUN npm --no-update-notifier --no-fund --global install pnpm@9.11.0 RUN pnpm --version + USER 1000 WORKDIR /usr/src/app diff --git a/apps/ws/Dockerfile b/apps/ws/Dockerfile index f16c1b0e57a..e5b93ea746e 100644 --- a/apps/ws/Dockerfile +++ b/apps/ws/Dockerfile @@ -1,13 +1,4 @@ -FROM node:20-alpine3.19 - -ENV NX_DAEMON=false - -RUN npm install -g pnpm@9.11.0 --loglevel notice -RUN npm i pm2 -g -RUN apk --no-cache add g++ make py3-pip - -USER 1000 -WORKDIR /usr/src/app +FROM ghcr.io/novuhq/novu/base:1.0.0 COPY --chown=1000:1000 .npmrc . COPY --chown=1000:1000 .npmrc-cloud . @@ -19,9 +10,8 @@ COPY --chown=1000:1000 libs/dal ./libs/dal COPY --chown=1000:1000 packages/shared ./packages/shared COPY --chown=1000:1000 libs/testing ./libs/testing COPY --chown=1000:1000 libs/application-generic ./libs/application-generic -COPY --chown=1000:1000 packages/client ./packages/client -COPY --chown=1000:1000 packages/stateless ./packages/stateless COPY --chown=1000:1000 packages/framework ./packages/framework +COPY --chown=1000:1000 packages/stateless ./packages/stateless COPY --chown=1000:1000 packages/providers ./packages/providers COPY --chown=1000:1000 ["tsconfig.json","nx.json","pnpm-workspace.yaml","pnpm-lock.yaml", ".npmrc", "./"]