diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml new file mode 100644 index 0000000..34e4207 --- /dev/null +++ b/.github/workflows/ci-cd.yml @@ -0,0 +1,212 @@ +name: Node.js API CI/CD Pipeline + +on: + push: + branches: + - main + - develop + pull_request: + branches: + - main + - develop + +env: + AWS_REGION: us-east-1 + ECR_REPOSITORY: node-api + ECS_SERVICE: node-api-service + ECS_CLUSTER: production + +permissions: + contents: read + id-token: write + +jobs: + lint: + runs-on: ubuntu-22.04 + steps: + - name: Checkout code + uses: actions/checkout@9bb56186c3b09b1872191b74733305c135a6d0e9 + + - name: Setup Node.js + uses: actions/setup-node@60edb5dd545a775178fac7f3f667f4173c5c7605 + with: + node-version: '18.18.0' + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Run ESLint + run: npm run lint --if-present + + test: + runs-on: ubuntu-22.04 + needs: lint + steps: + - name: Checkout code + uses: actions/checkout@9bb56186c3b09b1872191b74733305c135a6d0e9 + + - name: Setup Node.js + uses: actions/setup-node@60edb5dd545a775178fac7f3f667f4173c5c7605 + with: + node-version: '18.18.0' + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm test --if-present + + - name: Generate coverage report + run: npm run test:coverage --if-present + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3.1.4 + with: + files: ./coverage/coverage-final.json + flags: unittests + fail_ci_if_error: false + + build: + runs-on: ubuntu-22.04 + needs: test + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') + outputs: + image_uri: ${{ steps.image.outputs.uri }} + steps: + - name: Checkout code + uses: actions/checkout@9bb56186c3b09b1872191b74733305c135a6d0e9 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4.0.1 + with: + role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} + aws-region: ${{ env.AWS_REGION }} + role-duration-seconds: 900 + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2.0.1 + + - name: Build Docker image + id: docker_build + uses: docker/build-push-action@v5.1.0 + with: + context: . + push: false + tags: | + ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ github.sha }} + ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:latest + cache-from: type=registry,ref=${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:buildcache + cache-to: type=registry,ref=${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:buildcache,mode=max + outputs: type=docker,dest=/tmp/image.tar + + - name: Push Docker image to ECR + run: | + docker load --input /tmp/image.tar + docker push ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ github.sha }} + docker push ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:latest + + - name: Output image URI + id: image + run: echo "uri=${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ github.sha }}" >> $GITHUB_OUTPUT + + validate-terraform: + runs-on: ubuntu-22.04 + needs: test + steps: + - name: Checkout code + uses: actions/checkout@9bb56186c3b09b1872191b74733305c135a6d0e9 + + - name: Setup Terraform + uses: hashicorp/setup-terraform@v2.1.0 + with: + terraform_version: 1.6.0 + + - name: Terraform Format Check + run: terraform fmt -check -recursive ./terraform + + - name: Terraform Init + run: cd terraform && terraform init -backend=false + + - name: Terraform Validate + run: cd terraform && terraform validate + + - name: TFLint + uses: terraform-linters/setup-tflint@v4.0.0 + with: + tflint_version: v0.48.0 + + - name: Run TFLint + run: cd terraform && tflint --init && tflint + + deploy-to-ecs: + runs-on: ubuntu-22.04 + needs: [build, validate-terraform] + if: github.ref == 'refs/heads/main' + environment: production + steps: + - name: Checkout code + uses: actions/checkout@9bb56186c3b09b1872191b74733305c135a6d0e9 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4.0.1 + with: + role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} + aws-region: ${{ env.AWS_REGION }} + role-duration-seconds: 1800 + + - name: Setup Terraform + uses: hashicorp/setup-terraform@v2.1.0 + with: + terraform_version: 1.6.0 + + - name: Terraform Init + run: cd terraform && terraform init + + - name: Terraform Plan + run: cd terraform && terraform plan -var="image_tag=${{ needs.build.outputs.image_uri }}" -out=tfplan + + - name: Terraform Apply + if: github.ref == 'refs/heads/main' + run: cd terraform && terraform apply -auto-approve tfplan + + - name: Update ECS service + run: | + aws ecs update-service \ + --cluster ${{ env.ECS_CLUSTER }} \ + --service ${{ env.ECS_SERVICE }} \ + --force-new-deployment \ + --region ${{ env.AWS_REGION }} + + - name: Wait for ECS service update + run: | + aws ecs wait services-stable \ + --cluster ${{ env.ECS_CLUSTER }} \ + --services ${{ env.ECS_SERVICE }} \ + --region ${{ env.AWS_REGION }} + + notify: + runs-on: ubuntu-22.04 + needs: [lint, test, build, validate-terraform, deploy-to-ecs] + if: always() + steps: + - name: Slack notification on failure + if: failure() + uses: slackapi/slack-github-action@v1.24.0 + with: + webhook-url: ${{ secrets.SLACK_WEBHOOK }} + payload: | + { + "text": "GitHub Action failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + } + + - name: Slack notification on success + if: success() + uses: slackapi/slack-github-action@v1.24.0 + with: + webhook-url: ${{ secrets.SLACK_WEBHOOK }} + payload: | + { + "text": "GitHub Action succeeded: ${{ github.server_url }}/${{ github.repository }}/ \ No newline at end of file