Back to Blogs
Master GitHub Actions for Mac and Windows
DevOps
June 2025
12 min read
DevNest Team

Master GitHub Actions for Mac and Windows

Learn to automate your development workflow with GitHub Actions across Mac and Windows platforms. Perfect follow-up to our Docker tutorial!

Why GitHub Actions After Docker?

If you've read our Docker tutorial, you already understand containerization and how it creates consistent environments. GitHub Actions takes this further by automating your entire development workflow - building, testing, and deploying your containerized applications automatically. Think of GitHub Actions as the orchestrator that manages your Docker containers in the cloud.

What is GitHub Actions?

GitHub Actions is a CI/CD platform that automates your software development workflows. It runs your code in virtual machines (including the Docker containers you learned about) whenever specific events occur in your repository. Whether you're on Mac or Windows, Actions provides consistent cloud-based automation.

Cross-Platform Development Made Easy

One of the biggest advantages of GitHub Actions is running the same workflows across different operating systems. You can test your application on Ubuntu, Windows, and macOS simultaneously - just like how Docker containers ensure consistency across different environments.

💻 HANDS-ON COMMANDS

Step-by-Step Guide

1

Your First Workflow - Hello World

YMLyaml
# .github/workflows/hello-world.yml
name: Hello World Workflow

# Trigger this workflow on push to main branch
on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  greet:
    runs-on: ubuntu-latest
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v4
      
    - name: Say hello
      run: |
        echo "Hello from GitHub Actions!"
        echo "Current OS: ${{ runner.os }}"
                echo "Workflow triggered by: ${{ github.event_name }}"
        
💡

This basic workflow runs on every push or pull request to the main branch. Just like your first Docker container, this introduces core concepts: triggers (on), jobs, and steps. The workflow runs on Ubuntu, but we'll explore Mac and Windows runners next.

2

Multi-Platform Matrix Strategy

YMLyaml
# .github/workflows/cross-platform.yml
name: Cross-Platform Testing

on:
  push:
    branches: [ main, develop ]

jobs:
  test:
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        node-version: [16, 18, 20]
        
    runs-on: ${{ matrix.os }}
    
    steps:
    - uses: actions/checkout@v4
    
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: ${{ matrix.node-version }}
        cache: 'npm'
    
    - name: Install dependencies
      run: npm ci
    
    - name: Run tests
      run: npm test
      
    - name: Display OS info
      run: |
        echo "Running on: ${{ runner.os }}"
        echo "Node version: ${{ matrix.node-version }}"
💡

Matrix strategies run your job across multiple configurations simultaneously. This tests your app on 3 operating systems × 3 Node versions = 9 different environments! It's like running multiple Docker containers with different base images, but managed automatically by GitHub.

3

Docker Integration Workflow

YMLyaml
# .github/workflows/docker-build.yml
name: Docker Build and Push

on:
  push:
    branches: [ main ]
    tags: [ 'v*' ]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
      
    steps:
    - name: Checkout
      uses: actions/checkout@v4
      
    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v3
      
    - name: Login to Container Registry
      uses: docker/login-action@v3
      with:
        registry: ${{ env.REGISTRY }}
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}
        
    - name: Extract metadata
      id: meta
      uses: docker/metadata-action@v5
      with:
        images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
        
    - name: Build and push Docker image
      uses: docker/build-push-action@v5
      with:
        context: .
        push: true
        tags: ${{ steps.meta.outputs.tags }}
        labels: ${{ steps.meta.outputs.labels }}
💡

This workflow builds and pushes Docker images automatically - perfect if you followed our Docker tutorial! It uses the same Docker commands you learned, but automated in the cloud. The image gets pushed to GitHub Container Registry whenever you push code or create a release tag.

4

Windows-Specific Workflow

YMLyaml
# .github/workflows/windows-build.yml
name: Windows Build and Test

on:
  push:
    branches: [ main ]

jobs:
  windows-build:
    runs-on: windows-latest
    
    steps:
    - uses: actions/checkout@v4
    
    - name: Setup .NET
      uses: actions/setup-dotnet@v4
      with:
        dotnet-version: '8.0.x'
        
    - name: Restore dependencies
      run: dotnet restore
      
    - name: Build application
      run: dotnet build --no-restore --configuration Release
      
    - name: Run tests
      run: dotnet test --no-build --configuration Release --verbosity normal
      
    - name: Publish application
      run: dotnet publish --no-build --configuration Release --output ./publish
      
    - name: Upload build artifacts
      uses: actions/upload-artifact@v4
      with:
        name: windows-build
        path: ./publish/
        
    - name: Windows-specific commands
      run: |
        echo "Windows version info:"
        systeminfo | findstr /C:"OS Name" /C:"OS Version"
        echo "Available PowerShell version:"
        $PSVersionTable.PSVersion
💡

Windows runners support both PowerShell and Command Prompt. This example shows a .NET application build, but you can adapt it for any Windows technology stack. Notice how we upload build artifacts - these can be downloaded or used by other jobs.

5

macOS-Specific Workflow

YMLyaml
# .github/workflows/macos-build.yml
name: macOS Build and Test

on:
  push:
    branches: [ main ]

jobs:
  macos-build:
    runs-on: macos-latest
    
    steps:
    - uses: actions/checkout@v4
    
    - name: Setup Xcode
      uses: maxim-lobanov/setup-xcode@v1
      with:
        xcode-version: latest-stable
        
    - name: Install CocoaPods
      run: |
        sudo gem install cocoapods
        pod --version
        
    - name: Setup Node.js (for React Native)
      uses: actions/setup-node@v4
      with:
        node-version: '18'
        cache: 'npm'
        
    - name: Install dependencies
      run: |
        npm ci
        cd ios && pod install
        
    - name: Build iOS app
      run: |
        cd ios
        xcodebuild -workspace YourApp.xcworkspace           -scheme YourApp           -configuration Release           -destination 'generic/platform=iOS'           clean build
          
    - name: macOS system info
      run: |
        echo "macOS version:"
        sw_vers
        echo "Xcode version:"
        xcodebuild -version
        echo "Available simulators:"
        xcrun simctl list devices
💡

macOS runners are perfect for iOS and macOS app development. This example shows a React Native iOS build, but works for native Swift/Objective-C projects too. macOS runners include Xcode, simulators, and common development tools pre-installed.

6

Environment Variables and Secrets

YMLyaml
# .github/workflows/env-secrets.yml
name: Environment and Secrets Demo

on:
  workflow_dispatch:  # Manual trigger
    inputs:
      environment:
        description: 'Deployment environment'
        required: true
        default: 'staging'
        type: choice
        options:
        - staging
        - production

env:
  # Global environment variables
  NODE_ENV: production
  APP_NAME: my-awesome-app

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: ${{ github.event.inputs.environment }}
    
    steps:
    - uses: actions/checkout@v4
    
    - name: Display environment info
      run: |
        echo "Deploying to: ${{ github.event.inputs.environment }}"
        echo "App name: ${{ env.APP_NAME }}"
        echo "Node environment: ${{ env.NODE_ENV }}"
        
    - name: Use secrets safely
      env:
        API_KEY: ${{ secrets.API_KEY }}
        DATABASE_URL: ${{ secrets.DATABASE_URL }}
      run: |
        # Never echo secrets directly!
        echo "API key is set: $([[ -n "$API_KEY" ]] && echo "✓" || echo "✗")"
        echo "Database URL is set: $([[ -n "$DATABASE_URL" ]] && echo "✓" || echo "✗")"
        
    - name: Deploy with Docker (referencing our Docker tutorial)
      env:
        DOCKER_REGISTRY: ${{ secrets.DOCKER_REGISTRY }}
        DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
        DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
      run: |
        # Login to registry (like we learned in Docker tutorial)
        echo "$DOCKER_PASSWORD" | docker login $DOCKER_REGISTRY -u $DOCKER_USERNAME --password-stdin
        
        # Deploy using docker-compose (from Docker tutorial)
        docker-compose -f docker-compose.prod.yml up -d
💡

Secrets keep sensitive data safe - never hardcode passwords or API keys! This workflow shows manual triggers, environment-specific deployments, and secure secret usage. Notice how we integrate Docker commands from our previous tutorial for deployment.

7

Conditional Jobs and Dependencies

YMLyaml
# .github/workflows/conditional-jobs.yml
name: Conditional Workflows

on:
  push:
    branches: [ main, develop ]

jobs:
  changes:
    runs-on: ubuntu-latest
    outputs:
      backend: ${{ steps.changes.outputs.backend }}
      frontend: ${{ steps.changes.outputs.frontend }}
      docker: ${{ steps.changes.outputs.docker }}
    steps:
    - uses: actions/checkout@v4
    - uses: dorny/paths-filter@v2
      id: changes
      with:
        filters: |
          backend:
            - 'api/**'
            - 'server/**'
          frontend:
            - 'src/**'
            - 'public/**'
          docker:
            - 'Dockerfile'
            - 'docker-compose.yml'

  test-backend:
    needs: changes
    if: needs.changes.outputs.backend == 'true'
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - name: Test backend
      run: |
        echo "Running backend tests..."
        # Your backend tests here

  test-frontend:
    needs: changes
    if: needs.changes.outputs.frontend == 'true'
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - name: Test frontend
      run: |
        echo "Running frontend tests..."
        # Your frontend tests here

  rebuild-docker:
    needs: changes
    if: needs.changes.outputs.docker == 'true'
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - name: Rebuild Docker images
      run: |
        echo "Docker files changed, rebuilding images..."
        # Use Docker commands from our tutorial
        docker build -t myapp:latest .

  deploy:
    needs: [test-backend, test-frontend, rebuild-docker]
    if: always() && (needs.test-backend.result == 'success' || needs.test-backend.result == 'skipped') && (needs.test-frontend.result == 'success' || needs.test-frontend.result == 'skipped')
    runs-on: ubuntu-latest
    steps:
    - name: Deploy application
      run: |
        echo "All tests passed, deploying..."
💡

This advanced workflow only runs jobs when relevant files change, saving time and resources. Jobs can depend on each other using 'needs', and conditional logic determines what runs. Perfect for monorepos or when you want to optimize CI/CD performance.

8

Caching for Faster Builds

YMLyaml
# .github/workflows/caching-demo.yml
name: Caching Strategy

on:
  push:
    branches: [ main ]

jobs:
  build-with-cache:
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
    
    runs-on: ${{ matrix.os }}
    
    steps:
    - uses: actions/checkout@v4
    
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '18'
        cache: 'npm'  # Automatic npm cache
        
    - name: Cache Docker layers
      if: matrix.os == 'ubuntu-latest'
      uses: actions/cache@v4
      with:
        path: /tmp/.buildx-cache
        key: ${{ runner.os }}-buildx-${{ github.sha }}
        restore-keys: |
          ${{ runner.os }}-buildx-
          
    - name: Cache Maven dependencies
      if: matrix.os != 'windows-latest'
      uses: actions/cache@v4
      with:
        path: ~/.m2
        key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
        restore-keys: |
          ${{ runner.os }}-maven-
          
    - name: Cache Gradle dependencies
      uses: actions/cache@v4
      with:
        path: |
          ~/.gradle/caches
          ~/.gradle/wrapper
        key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
        restore-keys: |
          ${{ runner.os }}-gradle-
          
    - name: Install dependencies
      run: npm ci
      
    - name: Build with cached dependencies
      run: |
        echo "Building with cached dependencies..."
        npm run build
        
    - name: Docker build with cache (Ubuntu only)
      if: matrix.os == 'ubuntu-latest'
      run: |
        # Use Docker buildx for better caching (from our Docker tutorial)
        docker buildx create --use
        docker buildx build           --cache-from type=local,src=/tmp/.buildx-cache           --cache-to type=local,dest=/tmp/.buildx-cache-new,mode=max           --tag myapp:latest           --load .
💡

Caching dramatically speeds up your workflows by reusing downloaded dependencies and build artifacts. Different tools have different caching strategies - npm has built-in support, while Docker and Maven need explicit cache configuration. Notice OS-specific caching for different platforms.

9

Advanced Workflow with All Platforms

YMLyaml
# .github/workflows/complete-pipeline.yml
name: Complete CI/CD Pipeline

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]
  release:
    types: [published]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  test:
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        node-version: [16, 18, 20]
        include:
          - os: ubuntu-latest
            docker: true
          - os: windows-latest
            msbuild: true
          - os: macos-latest
            xcode: true
            
    runs-on: ${{ matrix.os }}
    
    steps:
    - uses: actions/checkout@v4
    
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: ${{ matrix.node-version }}
        cache: 'npm'
        
    - name: Setup Docker (Linux only)
      if: matrix.docker
      uses: docker/setup-buildx-action@v3
      
    - name: Setup MSBuild (Windows only)
      if: matrix.msbuild
      uses: microsoft/setup-msbuild@v1
      
    - name: Setup Xcode (macOS only)
      if: matrix.xcode
      uses: maxim-lobanov/setup-xcode@v1
      with:
        xcode-version: latest-stable
        
    - name: Install dependencies
      run: npm ci
      
    - name: Run tests
      run: npm test
      
    - name: Build application
      run: npm run build
      
    - name: Docker tests (Linux only)
      if: matrix.docker && matrix.node-version == '18'
      run: |
        # Use skills from Docker tutorial
        docker build -t test-app .
        docker run --rm test-app npm test

  security-scan:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - name: Run security audit
      run: npm audit --audit-level high
      
    - name: Scan Docker image for vulnerabilities
      run: |
        docker build -t security-scan .
        # Example with trivy scanner
        docker run --rm -v /var/run/docker.sock:/var/run/docker.sock           aquasec/trivy:latest image security-scan

  deploy:
    needs: [test, security-scan]
    if: github.event_name == 'release'
    runs-on: ubuntu-latest
    environment: production
    
    steps:
    - uses: actions/checkout@v4
    
    - name: Deploy with Docker Compose
      env:
        DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
        DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
      run: |
        # Deploy using Docker concepts from our tutorial
        echo "Deploying to production..."
        # SSH and deploy with docker-compose
        # docker-compose -f docker-compose.prod.yml up -d
💡

This comprehensive pipeline combines everything: multi-platform testing, security scanning, and deployment. It uses matrix strategies for thorough testing, includes platform-specific tools, and integrates Docker workflows from our tutorial. The deployment only runs on releases and requires manual approval via environment protection rules.

🚀

Pro Tips & Best Practices

✅

Use docker system prune regularly to clean up unused resources

✅

Always use specific image tags instead of latest in production

✅

Use .dockerignore files to exclude unnecessary files

✅

Multi-stage builds can significantly reduce image sizes

STAY UPDATED

Never Miss an Update

Get the latest insights, tutorials, and exclusive content delivered straight to your inbox. Join thousands of developers already subscribed.

No spam, unsubscribe at any time. We respect your privacy.

Loading subscriber count...