Nobody likes repetitive tasks. So why should you run all your tests manually with every new code or—even worse—check that the new code didn’t break your build process? Yeah, I know you want to spend your precious time on shiny features without having to waste hours tweaking your build pipeline. Or maybe you have excuses like “I would have to pay for the automation” or “It’s time-consuming to prepare pipelines, and I don’t have time for it.” Let me show you it can be done for free and very (and I mean very, VERY) fast.
At first sight, it might seem the automation of the building process is important only for big enterprise codebases. I reckon nothing could be further from the truth. I believe automated tasks might be beneficial also for small open-source or even personal side projects. But first things first ...
TL;DR If you are familiar with GitHub Actions, you can skip the theory and jump to the specific stack in the Examples section below.
Let’s start with the basics. The terms might sound very technical at first. However, the basic concept is quite simple. Continuous integration is the automation of building and testing. This means that typically, with some code changes, the machine checks whether it’s possible to build the project and whether your tests are passing. Continuous delivery is a little bit broader. To sum it up, this automation process will deploy or publish your code to various environments. This might be a little bit abstract and connected with the nature of your project, but you can visualize it as publishing to package registries like NPM or Nuget—or deploying your site to a staging or production environment.
GitHub Actions are the feature of GitHub that consists of the API and an environment for running your tasks. You just need to create a YAML file at a specific location in the repository. The configuration .yml file contains all the specific information about the environment, such as trigger events, jobs, or strategies. Additionally, you can choose the environment where you want to run your tasks—it might be Linux (Ubuntu), Windows (Windows Server), or even macOS (Big Sur or Catalina).
Each action is represented by a .yml
file located directly in the repository at the .github/workflows
location. To be able to run it, you need to allow the Actions feature in the repository’s Settings.
Note: If you are not familiar with the YAML format, for the purposes of the GitHub Actions, you just need to know that key-value pairs are represented by key: value
. The quotes for string values are optional. Nesting of objects is done by indentation (typically two spaces), and items of the array are denoted by a dash.
key: value
nested_property: Nested value
my_array:
- First Item
- Second Item
Let’s write the action that runs the Python script every day. This action will run on Ubuntu, using code from our repository, and will run our main.py script on the environment with Python 3.8. The explanation of each line is described in the respective comment directly in the code.
main.py
in the root of your repository with content print("Hello, World!")
..github/workflows
create the minimal-action.yml
file with the following content.name: Hello World From Python # Represents the name of the whole action.
on: # Specifies the section where we describe our build triggers.
schedule: # Specifies the section where we describe the schedule of running the action.
- cron: '* 1 * * *' # The CRON expression describing when to run the action.
workflow_dispatch: # Adds the ability to run the action manually by button on the Actions tab.
jobs: # Specifies the section where we describe our jobs.
hello_world_job: # Specific job section.
name: A greeting job # The name of the job.
runs-on: ubuntu-latest # Describes the environment.
steps: # Specifies the section where we describe the job's steps.
- name: Checkout # The name of the step.
uses: actions/checkout@v2 # Using already existing action actions/checkout@v2. This action provides us with access to the code of the repository.
- name: Set up Python 3.8 # The name of the step.
uses: actions/setup-python@v1 # Using already existing action actions/setup-python@v1. This action sets up a Python environment for us.
with: # Configuration of the python action.
python-version: 3.8 # Specific python version.
- name: Run the script # The name of the step.
run: python main.py # Command for running our main.py.
It’s not possible to cover all capabilities and combinations here. Nevertheless, in this section, you can find some real-world examples—you can choose them for scaffolding your Action quickly for your specific stack and use case. They showcase configuration for various languages, platforms, and publishing to external package repositories. However, before we start, we’ll need some more commands for these real-world examples.
The Action that builds the Node.js project written in TypeScript and publishes the package to the NPM registry when the release is created. The publishing of the NPM package is performed by the npm publish
command.
on:
release:
types: [published]
name: publish-to-npm
jobs:
publish:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
registry-url: 'https://registry.npmjs.org'
- run: npm install
- run: npm run build
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_API_KEY }}
The action that runs with push or pulls request to master. The action builds the Jekyll project and publishes it to a specific folder. After running this action, the site is accessible on GitHub Pages.
name: Ruby
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
ruby-version: ["2.6"]
env:
PROJECT_ID: ${{ secrets.PROJECT_ID }}
steps:
- uses: actions/checkout@v2
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby-version }}
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
- name: Update gems
run: gem update --system
- name: Install required packages
run: gem install jekyll bundler kontent-jekyll
- name: Build the output
run: bundle exec jekyll build
env:
JEKYLL_ENV: production
- name: Deploy 🚀
if: github.ref == 'refs/heads/master' # Checks if the current branch is master.
uses: JamesIves/[email protected]
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRANCH: gh-pages
FOLDER: _site
CLEAN: true # Automatically remove deleted files from the deploy branch
The Ruby project is built and published to RubyGems. This example uses dawidd6/action-publish-gem action for publishing.
name: publish-gem
on:
release:
types: [published]
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 2.6
- name: Install dependencies
run: bundle install
- name: Run tests
run: bundle exec rake
- name: Publish gem
uses: dawidd6/action-publish-gem@v1
with:
api_key: ${{secrets.RUBYGEMS_API_KEY}}
The tests are performed by executing the npm run test:all
command.
name: Test
on: [pull_request]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm i
- run: npm run test:all
Sometimes the test environment requires a browser, and default virtual environments come with a lot of preinstalled browsers. This action runs tests directly in the browser using the npm run test:all
command.
name: Test
on: [pull_request]
jobs:
build:
runs-on: windows-latest
strategy:
matrix:
node-version: [14.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm i
- run: npm run test:all
This action sets up the PHP environment on the Ubuntu machine using shivammathur/setup-php@v2. Moreover, it validates and caches Composer packages. In the end, it runs a code coverage tool by Codecov.
name: Build & Test & Report
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
php-versions: ['7.0', '7.1', '7.2', '7.3']
phpunit-versions: ['latest']
steps:
- uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
extensions: mbstring, intl
ini-values: post_max_size=256M, max_execution_time=180
coverage: xdebug
tools: php-cs-fixer, phpunit:${{ matrix.phpunit-versions }}
- name: Validate composer.json and composer.lock
run: composer validate --strict
- name: Cache Composer packages
id: composer-cache
uses: actions/cache@v2
with:
path: vendor
key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-php-
- name: Install dependencies
run: composer install --prefer-dist --no-progress --no-suggest
- name: Coverage
run: vendor/bin/phpunit --coverage-clover coverage.xml
- name: Codecov
uses: codecov/codecov-action@v1
This example covers the release process of the .NET Core SDK package. The action restores packages, builds the solution, runs tests, and publishes artifacts to Nuget.
name: Publish to NuGet
on:
release:
types: [published]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: 5.0.x
- name: Extract version from tag
id: get_version
uses: battila7/get-version-action@v2
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore --configuration Release /p:ContinuousIntegrationBuild=true /p:Version="${{ steps.get_version.outputs.version-without-v }}"
- name: Test
run: dotnet test --no-build --verbosity normal --configuration Release /p:CollectCoverage=true /p:CoverletOutputFormat=opencover
- name: Codecov
uses: codecov/codecov-action@v1
- name: Pack
run: dotnet pack --no-build --include-symbols --verbosity normal --configuration Release --output ./artifacts /p:PackageVersion="${{ steps.get_version.outputs.version-without-v }}"
- name: Upload artifacts
uses: actions/upload-artifact@v2
with:
name: "NuGet packages"
path: ./artifacts
- name: Publish artifacts to NuGet.org
run: dotnet nuget push './artifacts/*.nupkg' -s https://api.nuget.org/v3/index.json -k ${NUGET_API_KEY} --skip-duplicate
env:
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
- name: Upload artifacts to the GitHub release
uses: Roang-zero1/[email protected]
with:
args: ./artifacts
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
In this Action, the package is built and published using Gradle. The package is deployed to the Nexus repository.
name: Publish package to the Maven Central Repository
on:
release:
types: [created]
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Java
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Version release
run: echo Releasing verion ${{ github.event.release.tag_name }}
- name: Publish package
run: ./gradlew publish
env:
RELEASE_TAG: ${{ github.event.release.tag_name }}
NEXUS_USERNAME: ${{ secrets.NEXUS_USERNAME }}
NEXUS_PASSWORD: ${{ secrets.NEXUS_PASSWORD }}
SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }}
SIGNING_KEY: ${{ secrets.SIGNING_KEY }}
SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
At first, the Cocoapods dependencies are restored. Afterward, the project is built, and the Action runs tests. In the end, the package is pushed to the Cocoapods repository using the pod trunk push
command.
on:
release:
types: [published]
name: publish-to-cocoapods
jobs:
publish-to-cocoapods:
name: publish-to-cocoapods
runs-on: macOS-latest
strategy:
matrix:
destination: ['platform=iOS Simulator,OS=12.2,name=iPhone 11']
steps:
- name: Checkout
uses: actions/checkout@master
- name: Build and test
run: |
gem install cocoapods
pod repo update
pod install --project-directory=Example
set -o pipefail && xcodebuild test -enableCodeCoverage YES -workspace Example/KenticoKontentDelivery.xcworkspace -scheme KenticoKontentDelivery-Example -sdk iphonesimulator -destination 'name=iPhone 11' ONLY_ACTIVE_ARCH=NO | xcpretty
- name: Publish Cocoapod
run: pod trunk push
env:
COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }}
Not only can GitHub Actions be used for automating your build, running tests, and release pipelines but you can also leverage them for scheduling automatic tasks, such as web scraping or automatic update routines. This last bonus action runs the Python script at a specific time. The Python script does some automation—in this case, it scrapes the HTML page and makes data readable for machine processing. The output of the script is then committed and pushed to the specified GIT repository.
name: Python application
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
schedule:
- cron: '0 0 * * *'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.8
uses: actions/setup-python@v1
with:
python-version: 3.8
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Lint with flake8
run: |
pip install flake8
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Run script
run: |
python stocks.py
- name: Deploy page
run: |
git config --local user.email "[email protected]"
git config --local user.name "GitHub Action"
git add docs/index.html -f
git commit -m "Update docs/index.html" -a
git push "https://makma:${{ secrets.GITHUB_PERSONAL_TOKEN }}@github.com/makma/stocks-movement.git" HEAD:master --follow-tags
I believe GitHub Actions can be utilized for many different scenarios. You can find the majority of the example actions above in real-world use—they were based on and taken over from open-source Kentico Kontent’s GitHub. Feel free to take a look, get inspired, or maybe improve them ;). Didn’t you find your use case in the examples above, or did I not include your favorite GitHub Actions feature? Let me know on Twitter or Discord. I’ll be happy to discuss your use case and update the article.
Originally published at hackernoon.