I’ve been missing for a while, busy with WiFiChallenge Academy and several exciting projects coming this year. I hope you find this article helpful.

api_template

GitHub Basic Workflows: Compile, Release, and Create Docker Images Link to heading

In software development, automating workflows is crucial for efficiency. This article guides you through:

  1. Using Git tags for automated versioning and releases.
  2. Building and pushing Docker images to GitHub Container Registry (GHCR) and Docker Hub.
  3. Creating multi-OS releases automatically upon tagging (Windows, Linux, macOS).

These workflows apply seamlessly to both your main and development branches.

All_workflows

Note: This method works well for me, but I’m always open to improvements. If you find a better way, feel free to reach out!


Fundamental Concepts Link to heading

GitHub Workflows and Actions Link to heading

GitHub Actions let you automate tasks such as building, testing and deploying your code directly in your repository. This automation consists of two main components.

  • A workflow is defined in YAML files located in the .github/workflows folder of your repository. It outlines the entire process by specifying when tasks should run, for example after a push event, on a pull request or at scheduled intervals, and it defines a series of jobs with multiple steps.

  • An action is an individual task or command that performs a specific function, for example checking out your code or running tests. Actions can be reused across various workflows to create a complete automation pipeline.

In short, workflows describe the overall process while actions serve as the building blocks that complete each step.

Git tags Link to heading

Git tags are essential markers in version control that help you identify significant points in your project’s history, such as stable releases (e.g., v1.0, v2.0). They serve as bookmarks that make it easier to locate or revert to important states of your repository. In this article, tags are used to trigger workflows on the main branch. You can create a tag and push it to your repository with the following commands:

# Create tag
git tag <tag-name>

# Push tags to GitHub
git push origin --tags

This approach ensures that every important release is clearly defined in your project’s timeline.

Docker Publishing Link to heading

We’ll automate building and pushing Docker images to:

  • GitHub Container Registry (ghcr.io)
  • Docker Hub

These images facilitate easy deployment in various environments.


Workflow 1: Build and Publish Docker Images (Main Branch – Tag Trigger) Link to heading

Creating Docker images automatically for both GitHub Packages and Docker Hub simplifies deployment and ensures consistency. I this case I will use a template repository in my GitHub:

Workflow Overview Link to heading

This GitHub Actions workflow includes several clearly defined steps:

  1. Trigger: (on: push → tags): The workflow is triggered whenever a tag that starts with v (like v1.0.0, v2.3, etc.) is pushed.
  2. Set up QEMU & Set up Docker Buildx
    • QEMU enables cross-compilation for multiple CPU architectures (e.g., amd64, arm64).
    • Buildx allows building Docker images for multiple platforms simultaneously.
  3. Checkout: This step pulls your repository’s code into the runner.
  4. Set lowercase repository name: Some container registries and Docker requirements prefer lowercase. This step transforms your repo’s name to lowercase and stores it as an output variable.
  5. Extract metadata (docker/metadata-action): This action automatically generates Docker tags and labels based on your repo’s metadata—very handy for consistent version tagging.
  6. Login to Docker Hub & GitHub Container Registry:
    • We use docker/login-action for Docker Hub, requiring DOCKERHUB_TOKEN as a secret in your GitHub repository settings.
    • For GitHub Container Registry, we pipe GITHUB_TOKEN to Docker login.
  7. Build and push
    This final step compiles the Docker image for multiple architectures and pushes it to both Docker Hub and GHCR, streamlining the deployment process.

Complete YAML Workflow Link to heading

Create a file at ./.github/workflows/docker-image.yml:

name: Docker Build and Publish project

on:
  push:
    tags:
      - 'v*'

jobs:
  docker:
    runs-on: ubuntu-latest
    steps:
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Checkout
        uses: actions/checkout@v3

      - name: Set lowercase repository name
        id: set_lowercase_repo
        run: |
          LOWERCASE_REPO=$(echo "${GITHUB_REPOSITORY}" | tr '[:upper:]' '[:lower:]')
          echo "::set-output name=lowercase_repo::${LOWERCASE_REPO}"
                    
      # docker
      - name: Extract metadata (tags, labels) for Docker
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ steps.set_lowercase_repo.outputs.lowercase_repo }}

      - name: Login to DockerHub
        uses: docker/login-action@v3
        with:
          username: ${{ github.actor }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Login to GitHub Container Registry
        run: echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin

      # Push
      - name: Build and push
        id: docker_build
        uses: docker/build-push-action@v5
        with:
          context: .
          file: ./Dockerfile
          platforms: linux/amd64,linux/arm64
          push: true
          tags: |
            ${{ steps.meta.outputs.tags }}
            ghcr.io/${{ steps.set_lowercase_repo.outputs.lowercase_repo }}:latest
            ghcr.io/${{ steps.meta.outputs.tags }}            
          labels: ${{ steps.meta.outputs.labels }}
  1. To upload to Docker Hub, add a DockerHub token to your repository’s Actions secrets. Go to Settings > Secrets and variables > Actions and add DOCKERHUB_TOKEN.

DOCKERHUB_TOKEN GitHub

  1. To enable pushing to GitHub’s container registry, ensure that write permissions are granted to the workflow.
    WorkFlows Permission GitHub

  2. The GITHUB_TOKEN is automatically provided by GitHub and used in the workflow for authenticating registry operations.

Example Result Link to heading

After the workflow completes successfully, you can view its status on your GitHub Actions page. The Docker images are published to both Docker Hub and GitHub Container Registry, as illustrated by these sample screenshots:

v0.1-latest-image-github

v0.1-latest-image-DockerHub


Workflow 2: Build and Publish Docker Images from the Dev Branch Link to heading

Workflow Overview Link to heading

All commits to your dev branch will trigger a Docker build. Most steps are identical, but note:

  • Trigger: Runs on every push to the dev branch.
  • Tags: We use :dev for both Docker Hub and GHCR images. This helps distinguish them from production/stable images.

Full YAML Link to heading

Create a file at ./.github/workflows/docker-image-dev.yml:

name: Docker Build and Publish project DEV

on:
  push:
    branches:
      - 'dev'

jobs:
  docker:
    runs-on: ubuntu-latest
    steps:
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3
        
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Checkout
        uses: actions/checkout@v3   

      - name: Set lowercase repository name
        id: set_lowercase_repo
        run: |
          LOWERCASE_REPO=$(echo "${GITHUB_REPOSITORY}" | tr '[:upper:]' '[:lower:]')
          echo "::set-output name=lowercase_repo::${LOWERCASE_REPO}"          

      # Docker
      - name: Extract metadata (tags, labels) for Docker
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ steps.set_lowercase_repo.outputs.lowercase_repo }}

      - name: Login to DockerHub
        uses: docker/login-action@v3
        with:
          username: ${{ github.actor }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Login to GitHub Container Registry
        run: echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin

      # Push
      - name: Build and push
        id: docker_build
        uses: docker/build-push-action@v5
        with:
          context: .
          file: ./Dockerfile
          platforms: linux/amd64,linux/arm64
          push: true
          tags: |
            ${{ steps.set_lowercase_repo.outputs.lowercase_repo }}:dev
            ghcr.io/${{ steps.set_lowercase_repo.outputs.lowercase_repo }}:dev            
          labels: ${{ steps.meta.outputs.labels }}

Example Result Link to heading

Once changes are pushed to the dev branch, GitHub Actions builds and publishes Docker images with the :dev tag. This continuous integration makes it clear when a non-production version is being updated, as shown in these screenshots:


Workflow 3: Building and Releasing for All Operating Systems (in Golang) Link to heading

Workflow Overview Link to heading

This workflow automates the process of compiling your project for multiple operating systems so that your software is easily accessible to a wide audience. Triggered by pushing a version tag (for example, v1.2.3), it follows these steps:

  1. Trigger: The workflow triggers on any pushed tag matching v* (e.g., v1.0.0, v2.3.1, etc.).
  2. Build Job (Matrix Build):
    • Check Out your code.
    • Set Up Go to a specific version.
    • Compile the application for each OS and architecture combination specified in the matrix (windows, linux, darwin for amd64 and arm64).
    • Upload each compiled binary as an artifact.
  3. Publish Release Job:
    • Download the artifacts from the Build job.
    • Create a new (draft) GitHub Release using your tag name.
    • Upload the downloaded binaries as Release assets, making them available for users to download.

YAML Workflow Link to heading

Create a file, for example, ./.github/workflows/release.yml:

name: Build and Release

on:
  push:
    tags:
      - 'v*'      # Trigger this workflow whenever a push event includes a tag like 'v1.0.0', 'v2.3.4', etc.

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    strategy:
      matrix:
        # Define the Go version(s) you want to use for the build
        go-version: [1.21.6]  # Make sure this version is valid
        # Define the set of operating systems (OS) to build for
        os: [windows, linux, darwin]
        # Define the set of architectures (ARCH) to build for
        arch: [amd64, arm64]  # darwin does not support 386 or arm

    steps:
      # Step 1: Check out the repository code so it can be built
      - name: Checkout code
        uses: actions/checkout@v3

      # Step 2: Set up the specified Go version
      - name: Set up Go
        uses: actions/setup-go@v3
        with:
          go-version: ${{ matrix.go-version }}

      # Step 3: Build the application for each OS and architecture in the matrix
      - name: Build binary
        run: |
          if [ "${{ matrix.os }}" = "windows" ]; then
            GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} go build -o "${{ github.event.repository.name }}-${{ matrix.os }}-${{ matrix.arch }}.exe"
          else
            GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} go build -o "${{ github.event.repository.name }}-${{ matrix.os }}-${{ matrix.arch }}"
          fi          

      # Step 4: Upload the built binaries as artifacts for use by the next job
      - name: Upload build artifact
        uses: actions/upload-artifact@v4
        with:
          # The artifact name includes OS and ARCH (and .exe for Windows)
          name: ${{ github.event.repository.name }}-${{ matrix.os }}-${{ matrix.arch }}${{ matrix.os == 'windows' && '.exe' || '' }}
          # Provide the path to the generated binary
          path: ${{ github.event.repository.name }}-${{ matrix.os }}-${{ matrix.arch }}${{ matrix.os == 'windows' && '.exe' || '' }}
          if-no-files-found: error

  publish_release:
    # This job depends on the build job completing successfully
    needs: [build]
    runs-on: ubuntu-latest
    permissions:
      contents: write   # Required to create and modify releases
      actions: read     # Access to actions

    steps:
      # Step 5: Download all the build artifacts produced by the previous job
      - name: Download build artifacts
        uses: actions/download-artifact@v4

      # Step 6: Create a new draft release using the tag's name
      - name: Create Release
        id: create_release
        uses: actions/create-release@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          tag_name: ${{ github.ref_name }}
          release_name: Release ${{ github.ref_name }}
          draft: true
          prerelease: false

      # Step 7: Upload the previously downloaded binaries to the new release as assets
      - name: Upload release assets
        uses: dwenegar/upload-release-assets@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          release_id: ${{ steps.create_release.outputs.id }}
          assets_path: .

Example Result Link to heading

After pushing a version tag (for example, v0.1), this workflow compiles the project for multiple OS/architecture combinations and publishes the resulting binaries in a draft GitHub Release. This clearly links the compiled assets to a specific version, as seen in the sample screenshot below:

Release v0.1

Changing the Configuration for Other Languages Link to heading

Building and Releasing C Binaries Link to heading

  1. Remove Go Steps:
    • Delete any references to actions/setup-go@v2 and the Go build commands in your workflow.
  2. Install C Compiler:
    • For Ubuntu runners, add a step to install build tools (e.g., sudo apt-get install build-essential).
  3. Compile C:
    • Replace the go build step with a GCC (or Clang) command, for example:
      gcc main.c -o myprogram-${{ matrix.os }}-${{ matrix.arch }}
      
  4. Upload Assets:
    • In the “Upload Release Assets” step, list your new C binary names (e.g., myprogram-windows-amd64.exe, myprogram-linux-amd64) instead of the Go binaries.

Building and Releasing Python Executables Link to heading

  1. Remove Go Steps:
    Similarly, remove the Go setup and build steps.
  2. Install Python & Dependencies:
    Use actions/setup-python@v2 to install Python and any necessary bundling tools such as pyinstaller (installed via pip).
  3. Create Python Executable:
    Replace the Go build command with a PyInstaller (or another bundler) command, for example:
    pyinstaller --onefile my_script.py
    mv dist/my_script my_script-${{ matrix.os }}
    
  4. Upload Assets:
    Update the asset paths in the “Upload Release Assets” step to reflect the Python executable names (for example, my_script-windows.exe, my_script-linux).

Resources Link to heading

Below are some official documentation links and resources you may find helpful: