Automating Software Releases via GitHub Actions

Md. Fuad Hasan
4 min readFeb 11, 2025

--

Introduction

Automating software releases streamlines the development workflow, ensuring consistency and reliability. GitHub Actions provides a powerful mechanism to automate testing, building, and releasing software. This article explores a CI/CD pipeline using two GitHub Actions workflows: one for continuous integration (CI) and another for releasing software.

Continuous Integration (CI) Workflow

The CI workflow ensures that every code push and pull request meets quality standards before merging. The workflow file (.github/workflows/ci.yml) automates tasks such as dependency installation, linting, formatting checks, vetting, building, and testing.

CI Workflow Breakdown

name: CI
on:
push:
branches: [ "**" ]
pull_request:
branches: [ "**" ]

This triggers the workflow on every push and pull request to any branch.

jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4

The job runs on Ubuntu and starts by checking out the repository.

- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.23'

This sets up the Go environment with version 1.23.

- name: Cache Dependencies
uses: actions/cache@v3
id: gomod-cache
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('go.mod') }}
restore-keys: |
${{ runner.os }}-go-

Caches Go modules to speed up dependency installation in future runs.

- name: Install Dependencies
run: go mod tidy

Ensures the module dependencies are correct.

- name: Lint
run: |
go install golang.org/x/lint/golint@latest
golint ./...

Runs golint to check for style violations.

- name: Format Check
run: |
unformatted=$(go fmt ./...)
if [ -n "$unformatted" ]; then
echo "The following files are not formatted:"
echo "$unformatted"
exit 1
fi

Ensures the code is properly formatted; fails if any files are not.

- name: Vet
run: go vet ./...

Runs go vet to catch possible issues.

- name: Build
run: go build -v ./...

Builds the project.

- name: Test
run: go test -v ./...

Runs unit tests.

Problems and Solutions

Slow CI due to dependency installation

  • Solution: Use caching to speed up builds.

Linting and formatting failures

  • Solution: Developers should run golint and go fmt locally before pushing code.

Release Workflow

The Release workflow automates software versioning and packaging when a CI workflow completes successfully on the stable branch.

Release Workflow Breakdown

name: Release
on:
workflow_run:
workflows: ["CI"]
types:
- completed
branches:
- stable

This workflow runs after the CI workflow finishes successfully on the stable branch.

jobs:
release:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
permissions:
contents: write
issues: write
pull-requests: write

Ensures that the workflow only runs if the CI pipeline passes.

steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
fetch-depth: 0

Fetches the full repository history, which is needed for proper versioning.

- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.23'

Sets up Go.

- name: Build for Multiple Platforms
run: |
mkdir -p releases
PLATFORMS=("windows/amd64" "linux/amd64" "linux/arm64" "darwin/amd64" "darwin/arm64")
for platform in "${PLATFORMS[@]}"; do
OS=${platform%/*}
ARCH=${platform#*/}
output_name="releases/tinyass-${OS}-${ARCH}"
if [ $OS = "windows" ]; then
output_name="$output_name.exe"
fi
GOOS=$OS GOARCH=$ARCH go build -o "$output_name" .
done

Compiles the project for multiple platforms, including Windows, Linux, and macOS. You can change the value of PLATFORM for specific target OS.

- name: Generate Next Version
id: semver
uses: mathieudutour/github-tag-action@v6.1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
dry_run: true

Determines the next semantic version without actually tagging the repository.

- name: Create Release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ steps.semver.outputs.new_tag }}
name: Release ${{ steps.semver.outputs.new_tag }}
body: ${{ steps.semver.outputs.changelog }}
files: |
releases/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Publishes a new release with the generated version and includes the compiled binaries.

Problems and Solutions

Incorrect versioning

  • Solution: Ensure that mathieudutour/github-tag-action correctly increments versions based on commit history.

Build failures for different OS/architectures

  • Solution: Verify platform compatibility and cross-compilation settings in Go.

Permissions issues

  • Solution: Ensure GITHUB_TOKEN has the necessary permissions in GitHub repository settings.

Here is the full code at once

CI.yml

name: CI

on:
push:
branches: [ "**" ]
pull_request:
branches: [ "**" ]

jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.23'

- name: Cache Dependencies
uses: actions/cache@v3
id: gomod-cache
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('go.mod') }}
restore-keys: |
${{ runner.os }}-go-

- name: Install Dependencies
run: go mod tidy

- name: Lint
run: |
go install golang.org/x/lint/golint@latest
golint ./...

- name: Format Check
run: |
unformatted=$(go fmt ./...)
if [ -n "$unformatted" ]; then
echo "The following files are not formatted:"
echo "$unformatted"
exit 1
else
echo "Code is formatted."
fi

- name: Vet
run: go vet ./...

- name: Build
run: go build -v ./...

- name: Test
run: go test -v ./...

Release.yml

name: Release

on:
workflow_run:
workflows: ["CI"]
types:
- completed
branches:
- stable

jobs:
release:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
permissions:
contents: write
issues: write
pull-requests: write
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.23'

- name: Build for Multiple Platforms
run: |
mkdir -p releases
PLATFORMS=("windows/amd64" "linux/amd64" "linux/arm64" "darwin/amd64" "darwin/arm64")
for platform in "${PLATFORMS[@]}"; do
OS=${platform%/*}
ARCH=${platform#*/}
output_name="releases/tinyass-${OS}-${ARCH}"
if [ $OS = "windows" ]; then
output_name="$output_name.exe"
fi
GOOS=$OS GOARCH=$ARCH go build -o "$output_name" .
done

- name: Generate Next Version
id: semver
uses: mathieudutour/github-tag-action@v6.1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
dry_run: true

- name: Create Release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ steps.semver.outputs.new_tag }}
name: Release ${{ steps.semver.outputs.new_tag }}
body: ${{ steps.semver.outputs.changelog }}
files: |
releases/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Conclusion

Using GitHub Actions for CI/CD ensures a seamless workflow for building, testing, and releasing software. The CI workflow validates code quality, while the release workflow automates software packaging and deployment. By following these structured steps, teams can streamline their development process and maintain high-quality releases efficiently.

--

--

No responses yet