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 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.

.github/workflows/main.yml
name: github-actions-example
on: [push]
jobs:
  check-node-version:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: 16
      - 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 repository
  • on: - The event trigger to run the workflow. Here, the workflow will run on a push 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 (20.04 as of now).
  • steps: - Set of steps to be taken for the job check-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@v3
  • - uses: actions/checkout@v3 - 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 workflow to access the repository code.
    - uses: actions/setup-node@v3 - This action sets up NodeJS 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 an artifact so that we can download and share it 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 add the following content to your .github/workflows/main.yml file.

.github/workflows/main.yml
name: CI
 
on:
  push:
    branches:
      - main
 
jobs:
  build:
    name: Build APK
    runs-on: ubuntu-20.04
    steps:
      - name: Checkout
        uses: actions/checkout@v3
 
      - name: Setup Java
        uses: actions/setup-java@v3
        with:
          distribution: "zulu"
          java-version: "17.x"
 
      - name: Setup Flutter
        uses: subosito/flutter-action@v2
        with:
          flutter-version: "3.0.5"
 
      - 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@v3
        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-20.04 runner with steps,

Pretty straightforward right? You can refer to the linked documentation of the actions we are using for this workflow to see different attributes and their usage.

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 new changes.

  • Event to trigger on every push to tags that matches the given 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*
  • 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 output string extracted from the GITHUB_REF (pushed tag name).
    • Action actions/download-artifact@v3 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.
.github/workflows/main.yml
jobs:
  build: ....
 
  release:
    name: Release APK
    runs-on: ubuntu-20.04
    needs: build
    if: success() && startsWith(github.ref, 'refs/tags/v')
    steps:
      - name: Generate version string
        id: version-string
        run: echo "::set-output name=VERSION_STR::${GITHUB_REF##*/}"
 
      - name: Download all artifacts
        uses: actions/download-artifact@v3
 
      - name: Create a Release APK
        uses: ncipollo/release-action@v1
        with:
          allowUpdates: true
          artifacts: "android_release/*.apk"
          name: Release ${{ steps.version-string.outputs.VERSION_STR }}
          prerelease: ${{ contains(github.ref, '-alpha') || contains(github.ref, '-beta') }}
          tag: ${{ steps.version-string.outputs.VERSION_STR }}
          token: ${{ secrets.GITHUB_TOKEN }}

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.

.github/workflows/main.yml
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-20.04
    steps:
      - name: Checkout
        uses: actions/checkout@v3
 
      - name: Setup Java
        uses: actions/setup-java@v3
        with:
          distribution: "zulu"
          java-version: "17.x"
 
      - name: Setup Flutter
        uses: subosito/flutter-action@v2
        with:
          flutter-version: "3.0.5"
 
      - 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@v3
        with:
          name: android_release
          path: "build/app/outputs/apk/release"
 
  release:
    name: Release APK
    runs-on: ubuntu-20.04
    needs: build
    if: success() && startsWith(github.ref, 'refs/tags/v')
    steps:
      - name: Generate version string
        id: version-string
        run: echo "::set-output name=VERSION_STR::${GITHUB_REF##*/}"
 
      - name: Download all artifacts
        uses: actions/download-artifact@v3
 
      - name: Create a Release APK
        uses: ncipollo/release-action@v1
        with:
          allowUpdates: true
          artifacts: "android_release/*.apk"
          name: Release ${{ steps.version-string.outputs.VERSION_STR }}
          prerelease: ${{ contains(github.ref, '-alpha') || contains(github.ref, '-beta') }}
          tag: ${{ steps.version-string.outputs.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