DEV Community

Cover image for Code Quality Checks and Deployment with GitHub Actions

Code Quality Checks and Deployment with GitHub Actions

“The XP philosophy is to start where you are now and move towards the ideal. From where you are now, could you improve a little bit?” Kent Beck

Table of Contents

  1. Introduction
  2. Overview: Two-Workflow Strategy (Quality Gate + Deploy)
  3. Workflow 1: Code Quality Checks (PR → stg)
  4. Workflow 2: Deploy (push → stg)
  5. Practical Setup (Step-by-step)
  6. FAQs
  7. Key Takeaways
  8. Conclusion

1. Introduction

This document explains a practical GitHub Actions setup that enforces Flutter code quality checks on every pull request to the stg branch and deploys the Flutter web build to AWS S3 whenever code is pushed to stg.

The approach uses two separate workflows:

  • Code Quality Checks: runs on pull_request events (stg)
  • Deploy (stg): runs on push events to stg

2. Overview: Two-Workflow Strategy (Quality Gate + Deploy)

  • Developers open a Pull Request targeting stg.
  • GitHub Actions runs Flutter clean → pub get → analyze → test.
  • If checks pass, the PR is safe to merge. If checks fail, a Rocket.Chat alert is sent with the build log.
  • After merge/push to stg, a separate Deploy workflow builds Flutter web and syncs build/web to an S3 bucket.
  • Deploy success/failure is reported to Rocket.Chat.

Why separate workflows?

  • PR checks run in the PR context and are ideal for gating merges.
  • Deploy runs only on trusted branch pushes, avoiding accidental deployments from feature branches.
  • Clearer troubleshooting: you can tell instantly whether a failure is quality-related or deployment-related.

3. Workflow 1: Code Quality Checks (PR → stg)

3.1 What it does

  • Triggers when a Pull Request targets branch stg.
  • Ensures only one run per PR branch using concurrency (cancels older in-progress runs when a new commit is pushed).
  • Runs Flutter quality steps and writes a readable log file (cq.log).
  • Uploads cq.log as an artifact (kept for 7 days).
  • Notifies Rocket.Chat on success or failure (failure attempts to upload cq.log to the room).

3.2 YAML: Code Quality Checks

name: Code Quality Checks

on:
  pull_request:
    branches: [stg]

concurrency:
  group: code-quality-${{ github.head_ref }}
  cancel-in-progress: true

env:
  SITE_URL:      ${{ secrets.SITE_URL }}
  ENV_NAME:      ${{ secrets.ENV_NAME }}
  RC_BASE_URL:   ${{ secrets.RC_BASE_URL }}
  RC_ROOM_ID:    ${{ secrets.RC_ROOM_ID }}
  RC_USER_ID:    ${{ secrets.RC_USER_ID }}
  RC_AUTH_TOKEN:  ${{ secrets.RC_AUTH_TOKEN }}

jobs:
  code-quality:
    name: Flutter Analyze & Test
    runs-on: ubuntu-latest
    timeout-minutes: 30

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Flutter
        uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.29.2'

      - name: Run code quality checks
        id: quality
        run: |
          set -e
          LOG="cq.log"

          echo "════════════════════════════════════════════════════" > "$LOG"
          echo " Code Quality Checks Started"                        >> "$LOG"
          echo "═══════════════════════════════════════════════════════" >> "$LOG"

          echo ""                                                      >> "$LOG"
          echo " STEP 1: flutter clean"                              >> "$LOG"
          echo "───────────────────────────────────────────────────────" >> "$LOG"
          flutter clean >> "$LOG" 2>&1

          echo ""                                                      >> "$LOG"
          echo " STEP 2: flutter pub get"                            >> "$LOG"
          echo "───────────────────────────────────────────────────────" >> "$LOG"
          flutter pub get >> "$LOG" 2>&1

          echo ""                                                      >> "$LOG"
          echo " STEP 3: flutter analyze"                            >> "$LOG"
          echo "───────────────────────────────────────────────────────" >> "$LOG"
          flutter analyze >> "$LOG" 2>&1

          echo ""                                                      >> "$LOG"
          echo " STEP 4: flutter test"                               >> "$LOG"
          echo "───────────────────────────────────────────────────────" >> "$LOG"
          flutter test >> "$LOG" 2>&1

          echo ""                                                      >> "$LOG"
          echo "═══════════════════════════════════════════════════════" >> "$LOG"
          echo "All checks passed!"                                  >> "$LOG"
          echo "═══════════════════════════════════════════════════════" >> "$LOG"

      - name: Upload build log
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: code-quality-log
          path: cq.log
          retention-days: 7

      - name: Notify Rocket.Chat (success)
        if: success()
        run: |
          REPO_URL="${{ github.event.repository.html_url }}"
          COMMIT_URL="${REPO_URL}/commit/${{ github.sha }}"
          AUTHOR="${{ github.event.pull_request.user.login }}"
          DURATION="${{ env.DURATION }}"

          MSG_TEXT="*Code Quality Checks Passed*

          *Site-URL:* ${SITE_URL}
          *PR Title:* ${{ github.event.pull_request.title }}
          *PR URL:* ${{ github.event.pull_request.html_url }}
          *Environment:* ${ENV_NAME}

          *on PR:* ${{ github.head_ref }} → ${{ github.base_ref }}
          *Git Commit:* ${COMMIT_URL}
          *Git Author:* ${AUTHOR}"
          jq -n \
            --arg roomId "${RC_ROOM_ID}" \
            --arg text "$MSG_TEXT" \
            '{roomId: $roomId, text: $text}' | \
          curl -sS -X POST "${RC_BASE_URL}/api/v1/chat.postMessage" \
            -H "X-User-Id: ${RC_USER_ID}" \
            -H "X-Auth-Token: ${RC_AUTH_TOKEN}" \
            -H "Content-Type: application/json" \
            -d @- || echo "Notification failed, continuing"

      - name: Notify Rocket.Chat (failure)
        if: failure()
        run: |
          REPO_URL="${{ github.event.repository.html_url }}"
          COMMIT_URL="${REPO_URL}/commit/${{ github.sha }}"
          AUTHOR="${{ github.event.pull_request.user.login }}"

          MSG_TEXT="*Code Quality Checks Failed*

          *Site-URL:* ${SITE_URL}
          *PR Title:* ${{ github.event.pull_request.title }}
          *PR URL:* ${{ github.event.pull_request.html_url }}
          *Environment:* ${ENV_NAME}

          *on PR:* ${{ github.head_ref }} → ${{ github.base_ref }}
          *Git Commit:* ${COMMIT_URL}
          *Git Author:* ${AUTHOR}

           *Full build log attached below:*"

          UPLOAD_RESP=$(curl -sS -X POST "${RC_BASE_URL}/api/v1/rooms.media/${RC_ROOM_ID}" \
            -H "X-User-Id: ${RC_USER_ID}" \
            -H "X-Auth-Token: ${RC_AUTH_TOKEN}" \
            -F "file=@cq.log") || true

          FILE_URL=$(echo "$UPLOAD_RESP" | jq -r '.file.url // empty')

          if [ -n "$FILE_URL" ]; then
            jq -n \
              --arg rid "${RC_ROOM_ID}" \
              --arg msg "$MSG_TEXT" \
              --arg title "📄 BUILD LOG (cq.log) - Click to download" \
              --arg link "$FILE_URL" \
              --arg desc "Contains detailed output of all build steps" \
              '{message: {rid: $rid, msg: $msg, attachments: [{title: $title, title_link: $link, text: $desc, collapsed: false}]}}' | \
            curl -sS -X POST "${RC_BASE_URL}/api/v1/chat.sendMessage" \
              -H "X-User-Id: ${RC_USER_ID}" \
              -H "X-Auth-Token: ${RC_AUTH_TOKEN}" \
              -H "Content-Type: application/json" \
              -d @-
          else
            jq -n \
              --arg roomId "${RC_ROOM_ID}" \
              --arg text "$MSG_TEXT" \
              '{roomId: $roomId, text: $text}' | \
            curl -sS -X POST "${RC_BASE_URL}/api/v1/chat.postMessage" \
              -H "X-User-Id: ${RC_USER_ID}" \
              -H "X-Auth-Token: ${RC_AUTH_TOKEN}" \
              -H "Content-Type: application/json" \
              -d @-
          fi || echo "Failure notification failed"
Enter fullscreen mode Exit fullscreen mode

3.3 How logs + artifacts work

  • The workflow writes all step output to cq.log so your team has one clean, shareable log file.
  • Upload build log runs with if: always(), so the artifact is saved even when analyze/test fails.
  • retention-days: 7 keeps the artifact for one week to reduce storage and keep logs relevant.

3.4 Rocket.Chat notifications (success/failure)
Two messages are sent:

  • Success: posts a summary (PR title, PR URL, environment, branch and commit details).
  • Failure: tries to upload cq.log to the room, then posts a message with a link to the uploaded file.

If uploading fails, it still posts the failure summary so you are notified.
Prerequisites on the runner:

  • jq must be available (it is available by default on ubuntu-latest runners).
  • Rocket.Chat API base URL, user id, auth token, and room id must be configured as secrets.

4. Workflow 2: Deploy (push → stg)

4.1 What it does

  • Triggers on push to stg (typically after PR merge).
  • Builds Flutter web (build/web output).
  • Configures AWS credentials on the runner.
  • Syncs build/web to S3 bucket using aws s3 sync --delete.
  • Notifies Rocket.Chat on success/failure withcommit and author details.

4.2 YAML: Deploy (stg)

name: Deploy (stg)

on:
  push:
    branches: [stg]

concurrency:
  group: deploy-stg
  cancel-in-progress: false

permissions:
  contents: read

env:
  SITE_URL:      ${{ secrets.SITE_URL }}
  ENV_NAME:      ${{ secrets.ENV_NAME }}
  RC_BASE_URL:   ${{ secrets.RC_BASE_URL }}
  RC_ROOM_ID:    ${{ secrets.RC_ROOM_ID }}
  RC_USER_ID:    ${{ secrets.RC_USER_ID }}
  RC_AUTH_TOKEN:  ${{ secrets.RC_AUTH_TOKEN }}

jobs:
  build-and-deploy:
    name: Build & Deploy to AWS S3
    runs-on: ubuntu-latest
    timeout-minutes: 30

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Flutter
        uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.29.2'

      - name: Flutter clean
        run: flutter clean

      - name: Flutter build web
        run: |
          flutter build web \
            --dart-define=FLUTTER_WEB_USE_SKIA=false \
            --pwa-strategy=none

      - 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: ${{ secrets.AWS_REGION }}

      - name: Deploy to S3
        run: |
          aws s3 sync build/web s3://${{ secrets.AWS_S3_BUCKET }} \
            --delete

      - name: Notify Rocket.Chat (success)
        if: success()
        run: |
          REPO_URL="${{ github.event.repository.html_url }}"
          COMMIT_URL="${REPO_URL}/commit/${{ github.sha }}"
          AUTHOR=$(git log -1 --pretty=%an)

          MSG_TEXT="*Deployment Successful*

          *Site-URL:* ${SITE_URL}
          *Environment:* ${ENV_NAME}

          *Branch:* ${{ github.ref_name }}
          *Git Commit:* ${COMMIT_URL}
          *Git Author:* ${AUTHOR}"

          jq -n \
            --arg roomId "${RC_ROOM_ID}" \
            --arg text "$MSG_TEXT" \
            '{roomId: $roomId, text: $text}' | \
          curl -sS -X POST "${RC_BASE_URL}/api/v1/chat.postMessage" \
            -H "X-User-Id: ${RC_USER_ID}" \
            -H "X-Auth-Token: ${RC_AUTH_TOKEN}" \
            -H "Content-Type: application/json" \
            -d @- || echo "Deployment notification failed"

      - name: Notify Rocket.Chat (failure)
        if: failure()
        run: |
          REPO_URL="${{ github.event.repository.html_url }}"
          COMMIT_URL="${REPO_URL}/commit/${{ github.sha }}"
          AUTHOR=$(git log -1 --pretty=%an)

          MSG_TEXT="*Deployment Failed*

          *Site-URL:* ${SITE_URL}
          *Environment:* ${ENV_NAME}

          *Branch:* ${{ github.ref_name }}
          *Git Commit:* ${COMMIT_URL}
          *Git Author:* ${AUTHOR}"

          jq -n \
            --arg roomId "${RC_ROOM_ID}" \
            --arg text "$MSG_TEXT" \
            '{roomId: $roomId, text: $text}' | \
          curl -sS -X POST "${RC_BASE_URL}/api/v1/chat.postMessage" \
            -H "X-User-Id: ${RC_USER_ID}" \
            -H "X-Auth-Token: ${RC_AUTH_TOKEN}" \
            -H "Content-Type: application/json" \
            -d @- || echo "Deployment failure notification failed"
Enter fullscreen mode Exit fullscreen mode

“Beware of bugs in the above code; I have only proved it correct, not tried it.” Donald Knuth

4.3 AWS S3 deployment notes

  • S3 bucket must exist and the AWS credentials must have permission to sync to it.
  • aws s3 sync --delete removes files in S3 that no longer exist in build/web (prevents stale assets).
  • If you use CloudFront, consider adding an invalidation step (optional) after sync.
  • If your site is a Single Page App, configure S3/CloudFront to route 404s to index.html.

5. Practical Setup (Step-by-step)

5.1 Create workflow files

  • In your repo, create folder: .github/workflows/
  • Add file: code-quality.yml (paste Workflow 1 YAML).
  • Add file: deploy.yml (paste Workflow 2 YAML).
  • Commit and push to GitHub.

5.2 Configure GitHub secrets
Go to: Repository → Settings → Secrets and variables → Actions → New repository secret

  • SITE_URL (example: https://stg.example.com)
  • ENV_NAME (example: stg)
  • RC_BASE_URL (example: https://chat.example.com)
  • RC_ROOM_ID (Rocket.Chat room id)
  • RC_USER_ID (Rocket.Chat API user id)
  • RC_AUTH_TOKEN (Rocket.Chat API token)
  • AWS_ACCESS_KEY_ID
  • AWS_SECRET_ACCESS_KEY
  • AWS_REGION (example: ap-south-1)
  • AWS_S3_BUCKET (bucket name only, without s3://)

5.3 Set up branch protection (recommended)

  • In GitHub: Settings → Branches → Add branch protection rule for stg.
  • Enable 'Require a pull request before merging'.
  • Enable 'Require status checks to pass before merging'.
  • Select the workflow check name that appears for the PR (Flutter Analyze & Test / Code Quality Checks).
    5.4 Verify end-to-end flow

  • Create a feature branch and open a PR to stg.

  • Confirm Code Quality Checks workflow starts automatically.

  • If it passes, merge the PR to stg.

  • Confirm Deploy (stg) runs on the push event and uploads new web build to S3.

  • Check Rocket.Chat channel for the success messages.

5.5 Common improvements (optional but practical)

  • Add caching (Flutter pub cache) to speed up builds.
  • Run flutter format --set-exit-if-changed and/or dart analyze for stricter linting.
  • Use AWS OIDC instead of long-lived AWS keys (more secure) if your org supports it.
  • Add environment protection for stg deployments (manual approval for deploy job).

Practical Demonstration (Images Explained)

Step 1: Modify the README file on the feature branch.
In this demonstration, the README.md file was updated with the text "Addwebsolution" and the changes were committed and pushed to the feature-dev branch.

Step 2: Create a Pull Request from feature-dev to the stg Branch

  • Once the Pull Request is created, the Code Quality Checks workflow is automatically triggered.

  • Upon successful completion of the GitHub Actions pipeline, a Rocket.Chat notification is dispatched to the configured channel.

The screenshot below shows the Pull Request in a mergeable state the Code Quality status check has passed (indicated by the green checkmark), and the "Merge pull request" button is now enabled.

Step 3: Click the "Confirm Merge" button to merge the Pull Request into stg. This push event automatically triggers the Deploy workflow.

  • The Deploy (stg) pipeline is automatically triggered and the deployment job begins execution

  • The deployment pipeline has completed successfully the Flutter web build artifacts have been synced to the configured AWS S3 bucket.

  • A Rocket.Chat notification confirms the successful deployment, including details such as site URL, environment name, branch, commit hash, and author.

Code Quality and Deployment Pipeline Failure Scenario

Step 4: Introduce a deliberate syntax error in main.dart (e.g., a missing closing parenthesis) and push the changes to the feature branch.

A new Pull Request is created targeting the stg branch, which automatically triggers the Code Quality Checks workflow.

Step 5: The Code Quality Checks workflow fails due to the syntax error for example, print("hello" is missing its closing parenthesis. The flutter analyze step detects the issue and the pipeline exits with a non-zero status code.

A failure notification is sent to Rocket.Chat with the build log attached. Since the Code Quality status check has failed, the Pull Request cannot be merged, and the Deploy workflow is never triggered effectively preventing broken code from reaching the stg environment.

  • This practical demonstration validates the end-to-end CI/CD pipeline behavior across both success and failure scenarios. When code quality checks pass, the Pull Request is merged into the stg branch and the Deploy workflow automatically builds and publishes the Flutter web application to AWS S3, with a success notification delivered to Rocket.Chat. Conversely, when a syntax error or lint violation is detected, the Code Quality Checks workflow fails, the Pull Request is blocked from merging, and the Deploy workflow is never invoked ensuring that only validated, production-ready code reaches the staging environment. This two-workflow strategy enforces a reliable quality gate at the PR level while keeping deployment isolated to trusted branch pushes.

“Continuous Integration is not a tool. It is a practice.” Common CI principle (team reminder)

8. FAQs

Q1. Why does Code Quality run on pull_request but Deploy runs on push?
A. pull_request is best for quality gates before merging, while push to stg is best for deployments because it limits deploys to the stg branch history (usually protected).

Q2. What does concurrency do in Code Quality Checks?
A. It groups runs by the PR branch name (github.head_ref). If a new commit is pushed to the same PR branch, the older run is canceled.

Q3. Why upload cq.log if GitHub Actions already has logs?
A. cq.log is a single consolidated file you can share, attach to chats, and keep as an artifact. It also makes Rocket.Chat upload easy.

Q4. What if Rocket.Chat notification fails?
A. The workflow prints a warning and continues. Your CI status still reflects pass/fail correctly.

Q5. What permissions are needed for S3 deploy?
A. The AWS identity used in GitHub Actions needs at minimum s3:ListBucket and s3:PutObject/DeleteObject permissions for the target bucket.

Q6. How do I confirm the build is deployed correctly?
A. Check the S3 bucket objects update time and open SITE_URL in browser. If CloudFront is used, confirm cache invalidation or TTL behavior.

9. Key Takeaways

  • PR quality checks prevent broken code from entering stg.
  • Concurrency prevents duplicate CI runs and saves time.
  • Artifacts (cq.log) make debugging faster and help the team collaborate.
  • Deploy is isolated to stg pushes, which is safer and easier to audit.
  • Rocket.Chat alerts keep the team informed without opening GitHub every time.

10. Conclusion

This setup provides a clean CI/CD workflow for a Flutter web project: enforce quality on pull requests and deploy only after stg updates. With proper secrets, branch protections, and clear notifications, your team gets fast feedback and reliable deployments.

About the Author:Rajan is a DevOps Engineer at AddWebSolution, specializing in automation infrastructure, Optimize the CI/CD Pipelines and ensuring seamless deployments.

Top comments (0)