somleng/somleng

View on GitHub
.github/workflows/build.yml

Summary

Maintainability
Test Coverage
on: push
name: Build Somleng

env:
  ECR_REGISTRY: 324279636507.dkr.ecr.ap-southeast-1.amazonaws.com
  GHCR_REGISTRY: ghcr.io/somleng

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    outputs:
      matrix: ${{ steps.set-deployment-matrix.outputs.matrix }}
      matrixLength: ${{ steps.set-deployment-matrix.outputs.matrixLength }}
      packageMatrix: ${{ steps.set-deployment-matrix.outputs.packageMatrix }}

    env:
      PGHOST: localhost
      PGUSER: postgres
      RAILS_ENV: test
      CI: true

    services:
      postgres:
        image: postgres
        env:
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: postgres
          POSTGRES_HOST_AUTH_METHOD: trust
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - name: Set Deployment Matrix
        id: set-deployment-matrix
        run: |
          branchName=$(echo '${{ github.ref }}' | sed 's,refs/heads/,,g')
          matrixSource=$(cat << EOF
          [
            {
              "identifier": "somleng-staging",
              "branch": "develop",
              "environment": "staging",
              "friendly_image_tag": "beta",
              "image_tag": "stag-${{ github.sha }}",
              "ecs_cluster": "somleng-staging",
              "docs_path": "staging/docs"
            },
            {
              "identifier": "somleng",
              "branch": "master",
              "environment": "production",
              "friendly_image_tag": "latest",
              "image_tag": "prod-${{ github.sha }}",
              "ecs_cluster": "somleng",
              "docs_path":  "docs"
            }
          ]
          EOF
          )
          matrix=$(echo $matrixSource | jq --arg branchName "$branchName" 'map(. | select((.branch==$branchName)) )')
          echo "matrix={\"include\":$(echo $matrix)}" >> $GITHUB_OUTPUT
          echo "matrixLength=$(echo $matrix | jq length)" >> $GITHUB_OUTPUT
          echo "packageMatrix={\"platform\":[\"amd64\",\"arm64\"],\"include\":$(echo $matrix)}" >> $GITHUB_OUTPUT

      - name: Checkout
        uses: actions/checkout@v4

      - name: Install native dependencies
        run: |
          sudo apt-get update
          sudo apt-get install libvips
          sudo apt-get install ffmpeg

      - name: Setup Ruby
        uses: ruby/setup-ruby@v1
        with:
          bundler-cache: true

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version-file: ".tool-versions"
          cache: 'yarn'

      - name: Setup DB
        run: bundle exec rails db:create db:schema:load

      - name: Run Specs
        run: |
          bundle exec rails spec:prepare
          bundle exec rspec --format RspecApiDocumentation::ApiFormatter

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

      - name: Prepare Documentation Source
        if: steps.set-deployment-matrix.outputs.matrixLength > 0
        run: |
          cp app/assets/images/logo_documentation.png doc/logo.png

      - name: Upload Documentation Source
        if: steps.set-deployment-matrix.outputs.matrixLength > 0
        uses: actions/upload-artifact@v4
        with:
          name: documentation_source
          path: doc/
          retention-days: 1

  build_documentation:
    name: Build Documentation
    runs-on: ubuntu-latest
    needs: build
    if: needs.build.outputs.matrixLength > 0

    strategy:
      matrix: ${{fromJSON(needs.build.outputs.matrix)}}

    steps:
      - name: Checkout Slate
        uses: actions/checkout@v4
        with:
          ref: main
          repository: slatedocs/slate

      - name: Setup Ruby
        uses: ruby/setup-ruby@v1
        with:
          bundler-cache: true
          ruby-version: '3'

      - name: Download Documentation Source
        uses: actions/download-artifact@v4
        with:
          name: documentation_source
          path: api_docs

      - name: Prepare Slate
        run: |
          cp -R api_docs/slate/source/stylesheets/* source/stylesheets
          cp api_docs/logo.png source/logo.png
          echo "@import 'overrides';" >> source/stylesheets/_variables.scss

      - name: Build API Documentation
        run: |
          cp -R api_docs/carrier_api/* source
          bundle exec middleman build --build-dir=build/carrier_api

          cp -R api_docs/twilio_api/* source
          bundle exec middleman build --build-dir=build/twilio_api

      - 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 }}
          role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
          role-skip-session-tagging: true
          role-duration-seconds: 3600
          aws-region: ap-southeast-1

      - name: Deploy API Documentation
        run: |
          aws s3 sync --delete --acl public-read build/twilio_api s3://www.somleng.org/${{ matrix.docs_path }}/twilio_api
          aws s3 sync --delete --acl public-read build/carrier_api s3://www.somleng.org/${{ matrix.docs_path }}/carrier_api
          aws cloudfront create-invalidation --distribution-id E3962XCJFZ0KB1 --paths /${{ matrix.docs_path }}/\*

  build-packages:
    name: Build Packages
    runs-on: ubuntu-latest
    if: needs.build.outputs.matrixLength > 0

    strategy:
      matrix: ${{fromJSON(needs.build.outputs.packageMatrix)}}
      fail-fast: false

    needs:
      - build
      - build_documentation

    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 }}
          role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
          role-skip-session-tagging: true
          role-duration-seconds: 3600
          aws-region: ap-southeast-1

      - name: Build image
        uses: aws-actions/aws-codebuild-run-build@v1
        with:
          project-name: somleng-${{ matrix.platform }}
          buildspec-override:   |
            version: 0.2
            phases:
              install:
                commands:
                  # Temp fix: Remove this install phase. See: https://github.com/aws/aws-codebuild-docker-images/pull/642
                  - export BUILDX_VERSION=$(curl --silent "https://api.github.com/repos/docker/buildx/releases/latest" |jq -r .tag_name)
                  - curl -JLO "https://github.com/docker/buildx/releases/download/$BUILDX_VERSION/buildx-$BUILDX_VERSION.linux-${{ matrix.platform }}"
                  - mkdir -p ~/.docker/cli-plugins
                  - mv "buildx-$BUILDX_VERSION.linux-${{ matrix.platform }}" ~/.docker/cli-plugins/docker-buildx
                  - chmod +x ~/.docker/cli-plugins/docker-buildx

              build:
                steps:
                  - name: Download Documentation
                    run: |
                      aws s3 sync --acl public-read s3://www.somleng.org/${{ matrix.docs_path }}/twilio_api public/docs
                      mv public/docs/index.html public/docs/twilio_api/

                  - name: Build
                    run: |
                      aws ecr get-login-password --region ap-southeast-1 | docker login --username AWS --password-stdin ${{ env.ECR_REGISTRY }}
                      export DOCKER_BUILDKIT=1
                      docker build --cache-from ${{ env.ECR_REGISTRY }}/somleng:${{ matrix.friendly_image_tag }}-${{ matrix.platform }} --tag ${{ env.ECR_REGISTRY }}/somleng:${{ matrix.friendly_image_tag }}-${{ matrix.platform }} --push .
                      docker build --cache-from ${{ env.ECR_REGISTRY }}/somleng-nginx:${{ matrix.friendly_image_tag }}-${{ matrix.platform }} --tag ${{ env.ECR_REGISTRY }}/somleng-nginx:${{ matrix.friendly_image_tag }}-${{ matrix.platform }} --push docker/nginx

  build-manifest:
    name: Build Manifest
    runs-on: ubuntu-latest

    needs:
      - build
      - build-packages

    strategy:
      matrix: ${{fromJSON(needs.build.outputs.matrix)}}

    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 }}
            role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
            role-skip-session-tagging: true
            role-duration-seconds: 3600
            aws-region: ap-southeast-1

        - name: Set up Docker Buildx
          uses: docker/setup-buildx-action@v3

        - name: Build Manifest
          run: |
            aws ecr get-login-password --region ap-southeast-1 | docker login --username AWS --password-stdin ${{ env.ECR_REGISTRY }}
            declare -a platforms=("amd64" "arm64")
            declare -a components=("somleng" "somleng-nginx")
            for component in "${components[@]}"
            do
              source_images=$(printf "${{ env.ECR_REGISTRY }}/$component:${{ matrix.friendly_image_tag }}-%s " "${platforms[@]}")
              docker buildx imagetools create -t ${{ env.ECR_REGISTRY }}/$component:${{ matrix.friendly_image_tag }} -t ${{ env.ECR_REGISTRY }}/$component:${{ matrix.image_tag }} $source_images
            done

  # Do this step in Github Actions because pushing to Github from AWS CodeBuild is slow
  publish_images:
    name: Publish Images
    runs-on: ubuntu-latest

    needs:
      - build
      - build-packages

    strategy:
      matrix: ${{fromJSON(needs.build.outputs.matrix)}}

    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 }}
          role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
          role-skip-session-tagging: true
          role-duration-seconds: 3600
          aws-region: ap-southeast-1

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Login to ECR
        uses: docker/login-action@v3
        with:
          registry: ${{ env.ECR_REGISTRY }}

      - name: Publish Images
        run: |
          declare -a platforms=("amd64" "arm64")
          declare -a components=("somleng" "somleng-nginx")

          for platform in "${platforms[@]}"
          do
            for component in "${components[@]}"
            do
              docker image pull ${{ env.ECR_REGISTRY }}/$component:${{ matrix.friendly_image_tag }}-$platform
              docker tag ${{ env.ECR_REGISTRY }}/$component:${{ matrix.friendly_image_tag }}-$platform ${{ env.GHCR_REGISTRY }}/$component:${{ matrix.friendly_image_tag }}-$platform
              docker push ${{ env.GHCR_REGISTRY }}/$component:${{ matrix.friendly_image_tag }}-$platform
            done
          done

          for component in "${components[@]}"
          do
            source_images=$(printf "${{ env.GHCR_REGISTRY }}/$component:${{ matrix.friendly_image_tag }}-%s " "${platforms[@]}")
            docker buildx imagetools create -t ${{ env.GHCR_REGISTRY }}/$component:${{ matrix.friendly_image_tag }} $source_images
          done

  deploy:
    name: Deploy
    runs-on: ubuntu-latest

    needs:
      - build
      - build-manifest

    strategy:
      matrix: ${{fromJSON(needs.build.outputs.matrix)}}

    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 }}
          role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
          role-skip-session-tagging: true
          role-duration-seconds: 3600
          aws-region: ap-southeast-1

      - name: Get current webserver task definition
        run: |
          aws ecs describe-task-definition --task-definition "${{ matrix.identifier }}-appserver" --query 'taskDefinition' > task-definition.json

      - name: Inject new NGINX image into webserver task definition
        id: render-nginx-task-def
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        with:
          task-definition: task-definition.json
          container-name: nginx
          image: ${{ env.ECR_REGISTRY }}/somleng-nginx:${{ matrix.image_tag }}

      - name: Inject new app image into webserver task definition
        id: render-appserver-task-def
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        with:
          task-definition: ${{ steps.render-nginx-task-def.outputs.task-definition }}
          container-name: app
          image: ${{ env.ECR_REGISTRY }}/somleng:${{ matrix.image_tag }}

      - name: Get current worker task definition
        run: |
          aws ecs describe-task-definition --task-definition "${{ matrix.identifier }}-worker" --query 'taskDefinition' > task-definition.json

      - name: Inject new app image into worker task definition
        id: render-worker-task-def
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        with:
          task-definition: task-definition.json
          container-name: worker
          image: ${{ env.ECR_REGISTRY }}/somleng:${{ matrix.image_tag }}

      - name: Get current anycable task definition
        run: |
          aws ecs describe-task-definition --task-definition "${{ matrix.identifier }}-anycable" --query 'taskDefinition' > task-definition.json

      - name: Inject new app image into anycable task definition
        id: render-anycable-task-def
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        with:
          task-definition: task-definition.json
          container-name: anycable
          image: ${{ env.ECR_REGISTRY }}/somleng:${{ matrix.image_tag }}

      - name: Get current websockets task definition
        id: get-ws-task-def
        run: |
          aws ecs describe-task-definition --task-definition "${{ matrix.identifier }}-ws" --query 'taskDefinition' > task-definition.json
          image=$(cat task-definition.json | jq -r '.containerDefinitions[] | select(.name == "ws") .image')
          echo "image=$(cat task-definition.json | jq -r '.containerDefinitions[] | select(.name == "ws") .image')" >> $GITHUB_OUTPUT

      - name: Inject new app image into websockets task definition
        id: render-ws-task-def
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        with:
          task-definition: task-definition.json
          container-name: ws
          image: ${{ steps.get-ws-task-def.outputs.image }}

      - name: Get current Fargate worker task definition
        run: |
          aws ecs describe-task-definition --task-definition "${{ matrix.identifier }}-worker-fargate" --query 'taskDefinition' > task-definition.json

      - name: Inject new app image into Fargate worker task definition
        id: render-fargate-worker-task-def
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        with:
          task-definition: task-definition.json
          container-name: worker
          image: ${{ env.ECR_REGISTRY }}/somleng:${{ matrix.image_tag }}

      - name: Register Fargate task definition
        uses: aws-actions/amazon-ecs-deploy-task-definition@v1
        with:
          task-definition: ${{ steps.render-fargate-worker-task-def.outputs.task-definition }}

      - name: Run DB Migrate using Fargate
        run: |
          network_configuration=$(aws ecs describe-services --cluster ${{ matrix.ecs_cluster }} --service ${{ matrix.identifier }}-worker --query 'services[0]' | jq 'with_entries(select([.key] | inside(["networkConfiguration"])))')
          run_task_params=$(echo $network_configuration | jq '.startedBy = "db_migrate_ci" | .cluster = "${{ matrix.ecs_cluster }}" | .launchType = "FARGATE" | .taskDefinition = "${{ matrix.identifier }}-worker-fargate" | .overrides.containerOverrides[0].name = "worker" | .overrides.containerOverrides[0].command = ["bundle", "exec", "rails", "db:migrate"]' | jq -r tostring)
          aws ecs run-task --cli-input-json $run_task_params

      - name: Deploy App Server
        uses: aws-actions/amazon-ecs-deploy-task-definition@v1
        with:
          task-definition: ${{ steps.render-appserver-task-def.outputs.task-definition }}
          service: ${{ matrix.identifier}}-appserver
          cluster: ${{ matrix.ecs_cluster}}
          wait-for-service-stability: true

      - name: Deploy Worker
        uses: aws-actions/amazon-ecs-deploy-task-definition@v1
        with:
          task-definition: ${{ steps.render-worker-task-def.outputs.task-definition }}
          service: ${{ matrix.identifier}}-worker
          cluster: ${{ matrix.ecs_cluster}}
          wait-for-service-stability: true

      - name: Deploy Anycable
        uses: aws-actions/amazon-ecs-deploy-task-definition@v1
        with:
          task-definition: ${{ steps.render-anycable-task-def.outputs.task-definition }}
          service: ${{ matrix.identifier}}-anycable
          cluster: ${{ matrix.ecs_cluster}}
          wait-for-service-stability: true

      - name: Deploy Websockets Server
        uses: aws-actions/amazon-ecs-deploy-task-definition@v1
        with:
          task-definition: ${{ steps.render-ws-task-def.outputs.task-definition }}
          service: ${{ matrix.identifier}}-ws
          cluster: ${{ matrix.ecs_cluster}}
          wait-for-service-stability: true