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.
Initial research revealed I had to look at CI/CD from two perspectives:
- setting up development environments, and
- setting up deployment flows.
Initial observations also lead me to conclude:
- I finally get a practical opportunity to learn Docker. 👏
- 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.
- Dev Play Flutter app: Open source =>
Deployment
[web] - KD-reCall Flutter apps: Private =>
Deployment
[android, ios] - 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](https://kdcinfo.com/blog/wp-content/uploads/2024/07/dev-play_workflow_tic-tac-tuple_cropped.png)
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
uses: peaceiris/actions-gh-pages@v4
- https://github.com/peaceiris/actions-gh-pages
- https://github.com/marketplace/actions/github-pages-action
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.
uses: JamesIves/github-pages-deploy-action@v4
- https://github.com/JamesIves/github-pages-deploy-action
- https://github.com/marketplace/actions/deploy-to-github-pages
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/
- https://docs.flutter.dev/ui/navigation/url-strategies#hosting-a-flutter-app-at-a-non-root-location
- https://stackoverflow.com/questions/64704308/flutter-web-base-href-subfolder
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!
- Play Tic Tac Tuple | https://kdcinfo.github.io/dev-play/tic-tac-tuple/
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