What is GitHub Actions?
GitHub Actions is a continuous integration and continuous delivery (CI/CD) platform to create automated workflows that can build, test or deploy your code from GitHub. The workflow can be configured to be triggered by an event in the GitHub repository. For example, on push
, pull_request
, fork
, etc. GitHub provides Linux, Windows, and macOS virtual machines to run your workflows, or you can host your own self-hosted runners in your own data centre or cloud infrastructure.
To get started, a workflow file must be defined in the root of your repository in the .github/workflows/
directory. The configuration file uses YAML syntax. So the file must be in *.yml
format.
.github/workflows/main.yml
A workflow is a configurable and automated process that will run one or more jobs in sequential or parallel order. It can be triggered by a repository event, manually or at a defined schedule. GitHub Actions will automatically run the workflow based on its configuration. You can define multiple workflow files inside the .github/workflows/
directory which will run in parallel order.
Understanding the Workflow file
The workflow file has several components that define a run or an action. Consider the below example workflow.
name: github-actions-example
on: [push]
jobs:
check-node-version:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: node -v
Let's break down the example workflow.
name:
- The name of the workflow. This will be shown in the Actions tab in the repositoryon:
- The event trigger to run the workflow. Here, the workflow will run on apush
of the last commit. This can also be a list of events. For example,
on: [push, pull_request]
jobs:
- A set of steps that execute on the same runner. Each step is either a shell script or an action which is executed in order. Steps can also share data since they are executed on the same runner. A workflow can have multiple jobs and they can take a dependency on another job and will wait for the dependent job to complete before it can run.check-node-version:
- Defines the job name.runs-on:
The virtual machine that the workflow is run when triggered. GitHub provides Ubuntu, Windows and macOS runners and each workflow run executes in a newly-provisioned virtual machine. The above example is configured to run on the latest version of Ubuntu.steps:
- Set of steps to be taken for the jobcheck-node-version
Each item is a separate shell script or an action. You can organise steps by adding a name to each step.
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: actions/checkout@v4
-uses
keyword specifies that this step will run the<action-name>@<version>
(action/checkout
) action checks out (clone) the repository into the runner. This action must be used for the runner to access the repository code.
- uses: actions/setup-node@v4
- This action sets up Node.js in the runner.
with:
specifies the action attributes as per its documentation.- run: node -v
-run
keyword executes a shell command in the runner.
Build APK from Workflow
Let's create a workflow to build an APK using a Linux runner. Here we will build a release APK and upload the APK as an artifact so that we can download it and share with anyone.
First, create a new Flutter project and add a new workflow file main.yml
in the directory .github/workflows/
in your flutter project repository.
Then we add the following content in the .github/workflows/main.yml
file.
name: CI
on:
push:
branches:
- main
jobs:
build:
name: Build APK
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: "21"
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: "3.24.4"
- name: Get dependencies
run: flutter pub get
- name: Build Android APK
run: flutter build apk --release
- name: Upload Android release artifacts
uses: actions/upload-artifact@v4
with:
name: android_release
path: "build/app/outputs/apk/release"
The above workflow is defined to trigger on every push
to the main
branch.
It has only one job build
the gets executed in the ubuntu-latest
runner with the below steps:
- Checkout (actions/checkout)
- Set up Java (actions/setup-java)
- Set up Flutter (subosito/flutter-action)
- Get Flutter dependencies
- Build APK file
- Upload Artifact (actions/upload-artifact)
Pretty straightforward right? You can refer to the linked documentation of the actions to configure them.
Once you commit and push the changes to the main
branch, GitHub Actions will execute the workflow as shown below.
Workflow action artifact | Workflow action artifact download |
Release APK from Workflow
Now that we have a workflow to build an APK and upload the artifacts, let's update the configuration to create a tagged release in the repository. To achieve this, we need to add a new event to trigger on tags
, add a new dependent job release
, add two new actions to Download Artifact (actions/download-artifact) and Release Action (ncipollo/release-action).
Let's break down the steps
Trigger on Tags
Event to trigger on every push to tags
that matches the format (Semantic Versioning format). For example, v0.1.0
, v1.0.0
, v1.0.0-alpha
on:
push:
branches:
- main
tags:
- v[0-9]+.[0-9]+.[0-9]+
- v[0-9]+.[0-9]+.[0-9]+-alpha*
- v[0-9]+.[0-9]+.[0-9]+-beta*
Execute the Release Job
Dependent job release
gets executed after the job build
completes and with the condition having a pushed commit to a tag
starting with v*
- The step
version-string
sets an environment variable extracted from theGITHUB_REF
(pushed tag name). - Action
actions/download-artifact@v4
downloads all the artifacts to the runner's file system from the current action. - Action
ncipollo/release-action@v1
creates the release from the downloaded artifacts.
jobs:
build: ....
release:
name: Release APK
runs-on: ubuntu-latest
needs: build
if: success() && startsWith(github.ref, 'refs/tags/v')
steps:
- name: Generate version string
id: version-string
run: echo "VERSION_STR=${GITHUB_REF##*/}" >> $GITHUB_ENV
- name: Download all artifacts
uses: actions/download-artifact@v4
- name: Create a Release APK
uses: ncipollo/release-action@v1
with:
allowUpdates: true
artifacts: "android_release/*.apk"
name: Release ${{ env.VERSION_STR }}
prerelease: ${{ contains(github.ref, '-alpha') || contains(github.ref, '-beta') }}
tag: ${{ env.VERSION_STR }}
token: ${{ secrets.GITHUB_TOKEN }}
Using Environment Variables
You can use environment variables in your workflow file to store information. Here I'm using GitHub's default environment file to store variables that I'll be using with workflow commands.
Here's how we can use it in our workflow:
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Set build mode
id: build-mode
run: |
BUILD_MODE='release'
if [[ "${GITHUB_REF}" =~ ^refs\/tags\/v* ]]; then
BUILD_MODE='release'
elif [[ "${GITHUB_REF}" =~ ^refs\/heads\/main ]]; then
BUILD_MODE='release'
fi
echo "Build mode: ${BUILD_MODE}"
echo "BUILD_MODE=${BUILD_MODE}" >> $GITHUB_ENV
- name: Generate version string
if: startsWith(github.ref, 'refs/tags/v')
id: version-string
run: echo "VERSION_STR=${GITHUB_REF##*/}" >> $GITHUB_ENV
...
Here, we add the BUILD_MODE
and VERSION_STR
as variables to the environment file.
Note: To avoid issues, it's good practice to treat environment variables as case-sensitive, irrespective of the behavior of the operating system and shell you are using.
Complete Workflow - Build and Release APK
The final configuration of the workflow file looks like this. It will be triggered by the last pushed commit on the main
branch or any matching tags
of the repository. Check it out here.
name: CI
on:
push:
branches:
- main
tags:
- v[0-9]+.[0-9]+.[0-9]+
- v[0-9]+.[0-9]+.[0-9]+-alpha*
- v[0-9]+.[0-9]+.[0-9]+-beta*
jobs:
build:
name: Build APK
runs-on: ubuntu-latest
steps:
- name: Set build mode
id: build-mode
run: |
BUILD_MODE='release'
if [[ "${GITHUB_REF}" =~ ^refs\/tags\/v* ]]; then
BUILD_MODE='release'
elif [[ "${GITHUB_REF}" =~ ^refs\/heads\/main ]]; then
BUILD_MODE='release'
fi
echo "Build mode: ${BUILD_MODE}"
echo "BUILD_MODE=${BUILD_MODE}" >> $GITHUB_ENV
- name: Generate version string
if: startsWith(github.ref, 'refs/tags/v')
id: version-string
run: echo "VERSION_STR=${GITHUB_REF##*/}" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@v4
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: "21"
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
flutter-version: "3.24.4"
- name: Get dependencies
run: flutter pub get
- name: Build Android APK
run: flutter build apk --$BUILD_MODE
- name: Build Android App Bundle
if: startsWith(github.ref, 'refs/tags/v')
run: flutter build appbundle --$BUILD_MODE
- name: Rename APK with version string
if: startsWith(github.ref, 'refs/tags/v')
run: mv build/app/outputs/apk/$BUILD_MODE/app-$BUILD_MODE.apk build/app/outputs/apk/$BUILD_MODE/app-$BUILD_MODE-$VERSION_STR.apk
- name: Rename App Bundle with version string
if: startsWith(github.ref, 'refs/tags/v')
run: mv build/app/outputs/bundle/$BUILD_MODE/app-$BUILD_MODE.aab build/app/outputs/bundle/$BUILD_MODE/app-$BUILD_MODE-$VERSION_STR.aab
- name: Upload Android release artifacts
if: ${{ env.BUILD_MODE == 'release' }}
uses: actions/upload-artifact@v4
with:
name: android_release
path: "build/app/outputs/apk/release"
- name: Upload Android App Bundle artifacts
if: startsWith(github.ref, 'refs/tags/v')
uses: actions/upload-artifact@v4
with:
name: android_appbundle
path: "build/app/outputs/bundle/release"
- name: Upload Android debug artifacts
if: ${{ env.BUILD_MODE == 'debug' }}
uses: actions/upload-artifact@v4
with:
name: android_debug
path: "build/app/outputs/apk/debug"
release:
name: Release APK
runs-on: ubuntu-latest
needs: build
if: success() && startsWith(github.ref, 'refs/tags/v')
steps:
- name: Generate version string
id: version-string
run: echo "VERSION_STR=${GITHUB_REF##*/}" >> $GITHUB_ENV
- name: Download all artifacts
uses: actions/download-artifact@v4
- name: Create a Release APK and AAB
uses: ncipollo/release-action@v1
with:
allowUpdates: true
artifacts: "android_release/*.apk,android_appbundle/*.aab"
name: Release ${{ env.VERSION_STR }}
prerelease: ${{ contains(github.ref, '-alpha') || contains(github.ref, '-beta') }}
tag: ${{ env.VERSION_STR }}
token: ${{ secrets.GITHUB_TOKEN }}
Artifacts expire in 90 days and consume Storage for Actions and Packages, so you can tag and release your code following the semantic versioning scheme.
That's it! You can build and release your APK right from GitHub easily. GitHub Actions provide plenty more features for CI/CD automation. Check out their official documentation for more tutorials and guides.
GitHub repo: https://github.com/pasanjg/flutter-gh-actions