GitHub Actions workflow screenshot - cropped less

Preliminary Adventures in CI/CD with Flutter

In an effort to become a more well-rounded and full(er) stack developer, the time has come for me to embark on a path to understanding the world of CI/CD within the realm of Flutter development.

I had, admittedly, put off learning CI/CD because, while I looked forward to automating the development and deployment processes, immersing myself into the depths of such tangled tech was a tad intimidating for a greenish mobile dev. So I opted to procrastinate by spending a year refactoring two of my apps from Provider to Bloc, then creating a fully functional practical prototype.

Procrastination complete . . .

The time has come to learn CI/CD.

Learning Am I - Yoda

Initial research revealed I had to look at CI/CD from two perspectives:

  1. setting up development environments, and
  2. setting up deployment flows.

Initial observations also lead me to conclude:

  1. I finally get a practical opportunity to learn Docker. 👏
  2. As imagined, there is no shortage of deployment approaches and tools to choose from.

I next realized I had to determine which of my four Flutter apps would fall into which CI/CD tool or tech approach. There is one open source app in the ‘Dev Play’ repository, and there are three private apps related to KD-reCall, which is a small suite of simple reminder apps available in both app stores. Here’s the breakdown I ended up with.

  1. Dev Play Flutter app: Open source => Deployment [web]
  2. KD-reCall Flutter apps: Private => Deployment [android, ios]
  3. KD-reCall APIs: Private => Development via Docker

Development with Docker

I spent some time setting up a development image for API development, but have yet to try it out as I got busy setting up my first deployment flow. I trust I should have no issues with the development environment image as it really is just a modularized Virtual Machine (like VirtualBox), and I’m already familiar with shelling into servers thanks to web hosting.

Although I know it’s been around forever, it was still great to see so many Docker images supporting so many environments and tools. For instance, having a MariaDB image in lieu of MySQL.

Draft Docker Image: Custom LAMP Development (untested)
# Mac: docker-compose

## > $ cd ~/Development/dev-play/tic-tac-tuple/

## > vi docker-compose.yml

```yaml
version: '3.1'
services:
    web:
        image: nginx:latest
        ports:
            - "8080:80"
            - "443:443"
        environment:
            - NGINX_HOST=kdcinfo.com
            - NGINX_PORT=80
        volumes:
            - ./nginx.conf:/etc/nginx/conf.d/nginx.conf # default.conf
            - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
            - .:/var/www/html # Web: kdcinfo
    php:
        # image: php:fpm
        build:
            context: .
            dockerfile: PHP.Dockerfile
        volumes:
            - .:/var/www/html
    database:
        # image: mariadb:latest
        build:
            dockerfile: docker/database/Dockerfile
        ports:
            - "3306:3306" # 3306:3306
        environment:
            #
            # MYSQL_ROOT_PASSWORD: 'deepsecret'
            # MYSQL_DATABASE: 'tutorialdb'
            # MYSQL_USER: 'tutorial'
            # MYSQL_PASSWORD: 'goodsecret'
            MARIADB_ROOT_PASSWORD: 'deepsecret'
            MARIADB_DATABASE: 'tutorialdb'
            MARIADB_USER: 'tutorial'
            MARIADB_PASSWORD: 'goodsecret'
        volumes:
            - database_data:/var/lib/mysql
volumes:
    # mysqldata: {}
    database_data:
        driver: local
```

- The `nginx.conf` file from the host is placed at `/etc/nginx/conf.d/nginx.conf` inside the container.
- The `app` folder is created at the root of the container in `/app`; place PHP scripts, images and JavaScript files.

> vi nginx.conf

```yaml
server {
    listen 80 default_server;
    # root /app/public;
    root /app/web;

    index index.php index.html index.htm;

    location ~ \.php$ {
        fastcgi_pass php:9000;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }
}
```

> vi PHP.Dockerfile

```yaml
FROM php:fpm # php:8.2-fpm

RUN docker-php-ext-install pdo pdo_mysql

RUN pecl install xdebug && docker-php-ext-enable xdebug
```

- `xdebug` is installed through `pecl`, which is provided as part of the official PHP image.
- Rebuild the image with `docker-compose build`,
-   then restart the server with `docker-compose up`. The output of `phpinfo()` should show that both `pdo_mysql` and `xdebug` are installed.

> vi index.php => <?php \r phpinfo();

> docker-compose build # Rebuild the image

> docker-compose up
> ctrl-c

> vi sqltest.php

```php
<?php
$pdo = new PDO('mysql:dbname=tutorialdb;host=mysql', 'tutorial', 'goodsecret', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);

$query = $pdo->query('SHOW VARIABLES like "version"');

$row = $query->fetch();

echo 'MySQL version:' . $row['Value'];
```

> $ docker compose up -d

Deployment Flows

On the deployment side, I found I had some decisions to make.

After perusing a dozen or so CI/CD deployment approaches, and knowing I can’t learn everything at once, I trimmed the tech list I was looking to learn down to:

The open source app appeared the easiest deployment to tackle first, as it doesn’t involve publishing to the app stores. And if it were deployed to the web using GitHub Pages, my initial understanding was the entire flow could be handled by GitHub Actions.

And while I aim to go through at least 3 or 4 other deployment approaches in the coming weeks to months, the following pertains to the deployment flow for the open source Flutter project (dev-play) using GitHub Actions and GitHub Pages.

GitHub Actions => GH Pages => Deployment: [web]

It all began with an article from LogRocket: Flutter CI/CD using GitHub Actions

Before starting, note that maintaining your workflows and viewing the run history can be done either on GitHub directly, or in your IDE with the help of the GitHub Actions VS Code extension.

GitHub Actions workflow screenshot - cropped more

Setting up GitHub Actions to deploy to GitHub Pages for the open source Flutter App — even with a step by step walkthrough — became a tad challenging, and in the end could not be fully automated. The one failing automation step can still be performed manually, and is likely to do with the action’s deployment target being a subfolder, and not the root directory (the reason for the subfolder is the open source dev-play repository may end up with more than one app, and so it was set up to target specific release branches to trigger individual deployment workflows, and also share the gh-pages website structure by each app having its own subfolder).

The workflow I ended up with, pasted below, has a build and a deploy section. If analyzed for a bit, these action yaml images (script files) are fairly self-explanatory — that is, once they’ve been constructed. The details of setting them up is where things can take a bit of time, especially in learning the pros and cons of the various preset actions, depending on your developmental environment needs.

Deployment Automation Workflow: GitHub Actions => GitHub Pages
name: build_release_tictactuple

on:
  push:
    branches: [ "build_release_tictactuple" ]
  pull_request:
    branches: [ "build_release_tictactuple" ]

  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest

    defaults:
      run:
        working-directory: ./tic-tac-tuple

    steps:
      # https://github.com/marketplace/actions/setup-java-jdk#basic-configuration
      # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
      - uses: actions/checkout@v4
      # https://github.com/Homebrew/brew/actions/runs/9770285377/workflow
      # https://8thlight.com/insights/how-to-set-up-github-actions
      - name: Set up Homebrew
        id: set-up-homebrew
        uses: Homebrew/actions/setup-homebrew@master
      - name: Install YQ and output version
        id: get_flutter_version
        # https://github.com/subosito/flutter-action/issues/282
        # https://github.com/subosito/flutter-action/discussions/293
        # https://github.com/subosito/flutter-action/blob/b6150a9d644588978ac48783d01e7dbde7031cfa/README.md
        run: |
          brew install yq
          echo "result=$(yq '.environment.flutter' pubspec.yaml)" >> $GITHUB_OUTPUT
      - name: Set up Java
        uses: actions/setup-java@v4
        with:
          distribution: 'zulu'
          java-version: '21'
          # cache: 'gradle'
      - name: Set up Flutter
        uses: subosito/flutter-action@v2
        with:
          channel: beta
          # flutter-version: 3.22.0
          flutter-version: ${{ steps.get_flutter_version.outputs.result }}
          # flutter-version-file: pubspec.yaml
          # flutter-version-file: pubspec.yaml | environment: flutter: 3.22.0
          # flutter-version-file: ${{ steps.get_flutter_version.outputs.result }}
          cache: true

      # Note: This workflow uses the latest stable version of the Dart SDK.
      # You can specify other versions if desired, see documentation here:
      # https://github.com/dart-lang/setup-dart/blob/main/README.md
      # - uses: dart-lang/setup-dart@v1
      # - uses: dart-lang/setup-dart@9a04e6d73cca37bd455e0608d7e5092f881fd603

      - name: Install dependencies
        run: flutter pub get
      - name: Verify formatting
        run: dart format --output=none --line-length 100 --set-exit-if-changed .

      # Consider passing '--fatal-infos' for slightly stricter analysis.
      - name: Analyze project source
        run: dart analyze
      - name: Run tests
        run: flutter test
      - name: Start Web Release Build
        # run: flutter build web --release
        run: flutter build web --release --base-href /dev-play/tic-tac-tuple/
      - name: Upload Web Build Files
        uses: actions/upload-artifact@v4
        with:
          name: web-release
          path: ./tic-tac-tuple/build/web
      - name: List Files
        run: ls -R

  deploy:
    name: Deploy Web Build
    needs: build
    runs-on: ubuntu-latest

    steps:
    - name: Download Web Release
      uses: actions/download-artifact@v4
      with:
        name: web-release
        path: ./tic-tac-tuple/build/web

    # https://github.com/peaceiris/actions-gh-pages
    - name: Deploy to gh-pages
      uses: peaceiris/actions-gh-pages@v4
      with:
        github_token: ${{ secrets.GITHUB_TOKEN }}
        # publish_branch: your-branch  # default: gh-pages
        destination_dir: tic-tac-tuple
        publish_dir: ./tic-tac-tuple/build/web
        # enable_jekyll: true
        # jekyll_build_options: --trace
        # keep_files: true

Readme.md in GH-Pages Branch

You won’t need this Readme.md if your workflow deployment target is the root directory. But because the dev-play project deploys apps to subfolders, and the action being used (peaceiris) doesn’t auto-trigger the publishing to GitHub Pages, the Readme pasted below helps to work as a manual trigger. It should reside in the root directory of the gh-pages branch (a branch that is created automatically on the first workflow run), and so long as specific app deployments are made into subfolders, the Readme file should remain intact.

If a deployment were, at some point, to be set up to deploy to the root, then the keep_files option could be set to true for that workflow, or this Readme could somehow be included in the deployment.

As noted in the Readme contents below, although the workflow completes successfully, to complete the actual push to GH Pages, the number (e.g. 001) at the bottom of the Readme should be increased to allow the branch source change to trigger a push to the GitHub Pages website (after the trigger source is temporarily changed to branch).

Readme.md contents in root of `gh-pages` branch
For some reason the deployment workflow doesn't trigger GH Pages to publish any updates to the app's website when the `gh-pages` branch is updated via the workflow.

To get it to publish, when an update is made to the `main` branch, switch to the `build_release_tictactuple` branch, and select the 'update from main' option in GitHub Desktop. Once pushed, it will trigger the 'build and deploy' workflow.

- [View Workflow Runs](https://github.com/KDCinfo/dev-play/actions/workflows/tuple-web-release.yml)

When the workflow has completed:

1. Change the [Build and Deployment "source"](https://github.com/KDCinfo/dev-play/settings/pages) from `GitHub Actions` to `Deploy from a branch`.
2. Edit and commit this readme to the `gh-pages` branch --- this will trigger the publishing of the site.
3. Once published, set the trigger source back to `GitHub Actions`.

Timestamp: 2024-07-12 | 001

Deploy Actions

A commonly used deployment action is peaceiris/actions-gh-pages. This action works well, but as mentioned just above, had one issue with auto-publishing to GitHub Pages, which is likely to do with the action’s deployment target being a subfolder, and not the root directory.

‘Keep Files’ Option: When you use the peaceiris/actions-gh-pages action, there is a keep_files option.

By default, existing files in the publish branch (or only in destination_dir if given) will be removed.

Because this workflow uses the destination_dir option, files in the project’s root folder, like the readme, are not removed.

destination_dir: tic-tac-tuple
# ^--> https://kdcinfo.github.io/dev-play/tic-tac-tuple/
# keep_files: true # Not needed; subfolder will be cleaned, but not the root.

Due to the failure with the automated publishing using the peaceiris action, the JamesIves action could be tried as an alternative (see @TODO: below). It also has options to target a specific folder and perform cleans.

Web | base-href

As seen in the workflow, because the app is being deployed into a subfolder, for sites that aren’t hosted on the web’s root, you can use the --base-href Flutter build option to point to the app’s proper subfolder.

run: flutter build web --release --base-href /dev-play/tic-tac-tuple/

Adding Badges

Be sure to add a badge to your Readme.md when you’ve got your workflow running. In addition to being a tad reassuring, it can help visitors to alert you if something out of the ordinary were to happen to your build.

Resources

This project’s workflow file (pasted in the expandable section above)
https://github.com/KDCinfo/dev-play/actions/workflows/tuple-web-release.yml

Docker Crash Course for Absolute Beginners | DevOps Tools | TechWorld with Nana
https://www.youtube.com/watch?v=pg19Z8LL06w&ab_channel=TechWorldwithNana

How to Get Started with Docker | DockerCon 2020: Best Practices & How To | Docker
https://www.youtube.com/watch?v=iqqDU2crIEQ&t=1029s&ab_channel=Docker

@TODO:

  • Change favicon.
  • Add badges (per above). Done
  • Try alternate deployment action (JamesIves/github-pages-deploy-action).

End Result — Play the Game!

Tic Tac Tuple - Web Play

Still being new to CI/CD, does this line up with others’ uses of CI/CD? I’m always open to input.

Next, I’ll be moving on to learning fastlane with Firebase, thanks to yet another fabulous walk-through article by LogRocket: Using fastlane for Flutter: A complete guide.

Cheers!
– Keith | https://keithdc.com

Leave a Reply