
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.
Step-by-Step Guide
Your First Workflow - Hello World
# .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.
Multi-Platform Matrix Strategy
# .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.
Docker Integration Workflow
# .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.
Windows-Specific Workflow
# .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.
macOS-Specific Workflow
# .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.
Environment Variables and Secrets
# .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.
Conditional Jobs and Dependencies
# .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.
Caching for Faster Builds
# .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.
Advanced Workflow with All Platforms
# .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