#3.446 — GitLab ci\cd for Flutter

Igor Kravchenko
It_challenge
Published in
5 min readApr 29, 2020

I finished my GitLab ci\cd for Flutter project. I deploy the ios app to TestFlight, android to Google Play(beta) and web with Docker and kubernetes. Run unit tests and flutter driver tests with ios simulators.

.gitlab-ci.yml

# https://medium.com/it-challenge/92-gitlab-kubernetes-rancher-96d24af7b14c# $CI_BUILD_REF_SLUG =  branch($CI_BUILD_REF_NAME) without /image: docker:latest
variables:
appName: flutter-web-meows
appNamespace: meows-space
# only for kubernetes
# DOCKER_HOST: tcp://localhost:2375
services:
- docker:dind
stages:
- test
- build
- test_deployment
- review
- deploy
echo:
stage: test
script:
- echo $CI_COMMIT_REF_NAME
- echo $CI_BUILD_REF_SLUG
- echo $CI_PIPELINE_IID
tags:
- flutter
flutter_test:
stage: test
script:
- flutter test --coverage
- lcov --list coverage/lcov.info
- genhtml coverage/lcov.info --output=coverage
tags:
- flutter
artifacts:
paths:
- coverage
expire_in: 4 week

flutter_test_driver:
stage: test
script:
- sh scripts/test_driver_ios.sh "iPhone 8 Plus"
# - sh scripts/test_driver_ios.sh "iPhone 11 Pro Max"
# - sh scripts/test_driver_ios.sh "iPad Pro (12.9-inch) (3rd generation)"
# - sh scripts/test_driver_ios.sh "iPad Pro (12.9-inch) (2nd generation)"
tags:
- flutter
.flutter_build:
stage: build
before_script:
- sh _bump.sh $CI_COMMIT_REF_NAME $CI_PIPELINE_IID
- mkdir release
- flutter packages get
- flutter clean
artifacts:
paths:
- release
expire_in: 1 week
tags:
- flutter
flutter_build_android:
extends: .flutter_build
script:
- flutter build appbundle --release
- cp -Rf build/app/outputs/bundle/release release/android/
flutter_build_ios:
extends: .flutter_build
script:
- sh scripts/build_ios.sh
ios_testflight_beta_deployment:
stage: test_deployment
dependencies:
- flutter_build_ios
script:
- sh scripts/testflight.sh
tags:
- flutter
google_play_beta_deployment:
stage: test_deployment
dependencies:
- flutter_build_android
script:
- sh scripts/google_play.sh
tags:
- flutter
when: manual
flutter_build_web:
extends: .flutter_build
script:
- flutter build web --release
- cp -R ./build/web ./release
- ran=$RANDOM
- echo $ran
- mv -i release/web/main.dart.js release/web/main.$ran.dart.js
# - mv -i release/web/main.dart.js.deps release/web/main.$ran.dart.js.deps
- mv -i release/web/main.dart.js.map release/web/main.$ran.dart.js.map
- sed -i '' 's/main\.dart/main\.'$ran'\.dart/g' ./release/web/index.html
- sed -i '' 's/main\.dart/main\.'$ran'\.dart/g' ./release/web/flutter_service_worker.js
image_build:
stage: test_deployment
dependencies:
- flutter_build_web
tags:
- docker
image: docker:latest
services:
- docker:dind
variables:
IMAGE_TAG: '/fl:$CI_BUILD_REF_SLUG-$CI_PIPELINE_ID'
IMAGE_TAG_LATEST: '/fl:$CI_BUILD_REF_SLUG-latest'
script:
- docker build --pull -f Dockerfile.local -t "$CI_REGISTRY_IMAGE$IMAGE_TAG" .
- docker login -u "gitlab-ci-token" -p "$CI_JOB_TOKEN" $CI_REGISTRY
- docker push "$CI_REGISTRY_IMAGE$IMAGE_TAG"
- docker tag $CI_REGISTRY_IMAGE$IMAGE_TAG $CI_REGISTRY_IMAGE$IMAGE_TAG_LATEST
- docker push $CI_REGISTRY_IMAGE$IMAGE_TAG_LATEST
.kubic_job: &kubic_job
image:
name: lachlanevenson/k8s-kubectl:latest
entrypoint: ["/bin/sh", "-c"]
deploy_review:
<<: *kubic_job
stage: review
only:
- branches
except:
- master
- tags
when: manual
environment:
name: review/$CI_BUILD_REF_NAME
url: https://$CI_BUILD_REF_SLUG.meows.app
on_stop: stop_review
variables:
hostUrl: $CI_BUILD_REF_SLUG.meows.app
refApp: ${CI_BUILD_REF_SLUG}
script:
- kubectl version
- cd manifests/
- sed -i "s/__CI_BUILD_REF_SLUG__/${refApp}/" daemonset.yaml ingress.yaml service.yaml
- sed -i "s/__BRANCH__/${refApp}/" daemonset.yaml ingress.yaml service.yaml
- sed -i "s/__VERSION__/${CI_PIPELINE_ID}/" daemonset.yaml ingress.yaml service.yaml
- sed -i "s/__HOST__/${hostUrl}/" daemonset.yaml ingress.yaml service.yaml
- sed -i "s/__NAMEAPP__/${appName}/" daemonset.yaml ingress.yaml service.yaml
- sed -i "s/__NAMESPACE__/${appNamespace}/" daemonset.yaml ingress.yaml service.yaml
- |
if kubectl apply -f daemonset.yaml | grep -q unchanged; then
echo "=> Patching daemonset to force image update."
kubectl patch -f daemonset.yaml -p "{\"spec\":{\"template\":{\"metadata\":{\"annotations\":{\"ci-last-updated\":\"$(date +'%s')\"}}}}}"
else
echo "=> DaemonSet apply has changed the object, no need to force image update."
fi
- kubectl apply -f service.yaml || true
- kubectl apply -f ingress.yaml
- kubectl rollout status -f daemonset.yaml
- kubectl get all,ing --namespace=${appNamespace} -l app=${appName},ref=${refApp}
stop_review:
stage: review
tags:
- docker
image:
name: lachlanevenson/k8s-kubectl:latest
entrypoint: ["/bin/sh", "-c"]
variables:
GIT_STRATEGY: none
refApp: ${CI_BUILD_REF_SLUG}
when: manual
only:
- branches
except:
- master
- tags
environment:
name: review/$CI_BUILD_REF_NAME
action: stop
script:
- kubectl version
- kubectl delete ing --namespace=${appNamespace} -l app=${appName},ref=${refApp}
- kubectl delete all --namespace=${appNamespace} -l app=${appName},ref=${refApp}
deploy_prod:
<<: *kubic_job
stage: deploy
environment:
name: prod
url: https://beta.meows.app
only:
- master
# when: manual
except:
- tags
variables:
hostUrl: beta.meows.app
refApp: prod
script:
- kubectl version
- cd manifests/
- sed -i "s/__CI_BUILD_REF_SLUG__/${refApp}/" daemonset.yaml ingress.yaml service.yaml
- sed -i "s/__BRANCH__/master/" daemonset.yaml ingress.yaml service.yaml
- sed -i "s/__VERSION__/${CI_PIPELINE_ID}/" daemonset.yaml ingress.yaml service.yaml
- sed -i "s/__HOST__/${hostUrl}/" daemonset.yaml ingress.yaml service.yaml
- sed -i "s/__NAMEAPP__/${appName}/" daemonset.yaml ingress.yaml service.yaml
- sed -i "s/__NAMESPACE__/${appNamespace}/" daemonset.yaml ingress.yaml service.yaml
- kubectl apply -f daemonset.yaml
- kubectl apply -f service.yaml
- kubectl apply -f ingress.yaml
- kubectl rollout status -f daemonset.yaml
- kubectl get all,ing --namespace=${appNamespace} -l app=${appName},ref=${refApp}

test_driver_ios.sh

echo 'erase' "$1"
xcrun simctl erase "$1"
echo 'boot' "$1"
xcrun simctl boot "$1"
echo 'start ci test'
flutter drive --target=test_driver/ci.dart
echo 'shutdown' "$1"
xcrun simctl shutdown "$1"

Change folder doesn’t work in ci for me. I run shell script =)

build_ios.sh

echo 'Start'
echo 'Clean'
rm ios/Podfile
flutter clean
flutter pub get
echo 'export env'
export SWIFT_VERSION=4.0
echo 'Start build'
cd ios
pod install
fastlane build
echo 'Awesome!'

ios/fastlane

default_platform(:ios)platform :ios do
desc "a new build ios"
lane :build do
build_app(
workspace: "Runner.xcworkspace",
scheme: "Runner",
include_bitcode: false,
output_directory: "../release/ios", # Destination directory. Defaults to current directory.
output_name: "meows.ipa",
)
end
lane :beta do
get_certificates # invokes cert
get_provisioning_profile # invokes sigh (check profiles in XCode withpout auotosignin)
# build_app(workspace: "Runner.xcworkspace", scheme: "Runner", include_bitcode: false)
upload_to_testflight(skip_waiting_for_build_processing: true,ipa: "../release/ios/meows.ipa" )
end
end

testflight.sh

echo 'Start'
echo 'export env'
export FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD=password
echo 'Start build'
cd ios
fastlane beta
echo 'Awesome!'

google_play.sh

echo 'Start'
echo 'Start build'
cd android
fastlane beta_google
echo 'Awesome!'

android/fastalne

default_platform(:android)platform :android dodesc "Deploy a new version to the Google Play(beta)"
lane :beta_google do
upload_to_play_store(track: 'beta', aab: '../release/android/app-release.aab', skip_upload_metadata: true, skip_upload_images:true, skip_upload_screenshots: true)
end
end

Dockerfile.local

FROM nginx:alpine
COPY ./release/web /app
COPY ./web/nginx.conf /etc/nginx/nginx.conf
COPY ./web/mime.types /etc/nginx/mime.types
COPY ./web/gzip.conf /etc/nginx/gzip.conf
EXPOSE 9000/tcp

nginx.conf

user  nginx;
worker_processes 4;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
log_format gzip '[$time_local] ' '"$request" $status $bytes_sent';
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log /dev/stdout;
charset utf-8;
sendfile on;
keepalive_timeout 65;
#include /etc/nginx/conf.d/*.conf;server {
listen 9000;
server_name localhost;
access_log /dev/stdout;root /app;location / {
try_files $uri$args $uri$args/ /index.html;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
# Compression
include /etc/nginx/gzip.conf;
}

gzip.conf

# Enable Gzip compressed.
gzip on;
# Enable compression both for HTTP/1.0 and HTTP/1.1 (required for CloudFront).
gzip_http_version 1.0;
# Compression level (1-9).
# 5 is a perfect compromise between size and cpu usage, offering about
# 75% reduction for most ascii files (almost identical to level 9).
gzip_comp_level 5;
# Don't compress anything that's already small and unlikely to shrink much
# if at all (the default is 20 bytes, which is bad as that usually leads to
# larger files after gzipping).
gzip_min_length 256;
# Compress data even for clients that are connecting to us via proxies,
# identified by the "Via" header (required for CloudFront).
gzip_proxied any;
# Tell proxies to cache both the gzipped and regular version of a resource
# whenever the client's Accept-Encoding capabilities header varies;
# Avoids the issue where a non-gzip capable client (which is extremely rare
# today) would display gibberish if their proxy gave them the gzipped version.
gzip_vary on;
# Compress all output labeled with one of the following MIME-types.
gzip_types
application/atom+xml
application/javascript
application/json
application/rss+xml
application/vnd.ms-fontobject
application/x-font-ttf
application/x-web-app-manifest+json
application/xhtml+xml
application/xml
font/opentype
image/svg+xml
image/x-icon
text/css
text/plain
text/x-component;
# text/html is always compressed by HttpGzipModule

_bump.sh

echo '$1 = ' $1
echo '$2 = ' $2
newNumber=$(($2 + 100)) #need more number
echo $newNumber
if [[ $1 == release/* ]];
then
echo 'release'
newversion=$(echo $1 | sed "s|release\/\([0-9]*\)\.\([0-9]*\)\.\([0-9]*\)|\1\.\2\.\3+$newNumber|")
else
echo 'else branch'
version=$(sed -n '10p' pubspec.yaml)
newversion=$(echo $version | sed "s|version: \([0-9]*\)\.\([0-9]*\)\.\([0-9]*\)\+\([0-9]*\)|\1\.\2\.\3+$newNumber|g")
fi
echo 'version: '$newversion
sed -i '' 's|version: .*|version: '$newversion'|' pubspec.yaml
echo 'bumped version'

Подписка на телеграмм: https://t.me/It_challenge

29.04.2020

--

--