Complete GitHub Actions CI/CD Tutorial for Astro: Deploy Fast Static Sites with Zero Downtime
Setting up a robust CI/CD pipeline for your Astro site shouldn’t be rocket science. Yet we see teams struggling with broken builds, slow deployments, and manual processes that waste hours every week. This GitHub Actions CI/CD tutorial for Astro will show you exactly how to build a production-ready pipeline that deploys your static site automatically, handles errors gracefully, and scales with your team.
We’ve implemented this exact workflow across multiple projects at Odea Works, including our own sites and client work. The pipeline we’re building handles everything from dependency caching to multi-environment deployments, taking your Astro site from git push to live in under 3 minutes.
Why Astro + GitHub Actions Makes Perfect Sense
Astro generates blazingly fast static sites, but you need a deployment pipeline that matches that speed. GitHub Actions provides free CI/CD minutes, integrates directly with your repository, and offers powerful caching mechanisms that can cut your build times by 60% or more.
Here’s what makes this combination particularly effective:
- Zero configuration overhead — Your CI/CD lives right in your repository
- Built-in secrets management — No external tools needed for API keys
- Parallel job execution — Run tests, builds, and deployments simultaneously
- Rich ecosystem — Thousands of pre-built actions for common tasks
- Free tier — 2,000 minutes per month for public repos, 500 for private
Setting Up Your Astro Project for CI/CD
Before diving into GitHub Actions, let’s ensure your Astro project follows CI/CD best practices. Here’s the project structure we recommend:
your-astro-site/
├── .github/
│ └── workflows/
│ └── deploy.yml
├── src/
├── public/
├── astro.config.mjs
├── package.json
├── package-lock.json
└── README.md
Your package.json should include these essential scripts:
{
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro build",
"preview": "astro preview",
"test": "vitest",
"test:ci": "vitest run",
"lint": "eslint . --ext .js,.ts,.astro",
"lint:fix": "eslint . --ext .js,.ts,.astro --fix"
}
}
The key here is the test:ci script — this runs your tests in CI mode without the interactive watcher, essential for automated environments.
Building Your First GitHub Actions Workflow
Create .github/workflows/deploy.yml in your repository. This workflow will trigger on pushes to your main branch and pull requests:
name: Deploy Astro Site
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Run tests
run: npm run test:ci
build:
runs-on: ubuntu-latest
needs: test
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build Astro site
run: npm run build
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: astro-build
path: dist/
This basic workflow separates testing and building into different jobs. The needs: test ensures builds only run after tests pass, saving compute resources and failing fast on test failures.
Advanced Caching Strategies
The real magic happens when you optimize caching. Here’s an enhanced version that caches both npm dependencies and Astro’s build cache:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: Cache Astro build
uses: actions/cache@v4
with:
path: |
.astro
node_modules/.astro
key: ${{ runner.os }}-astro-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('src/**/*.{astro,ts,js}') }}
restore-keys: |
${{ runner.os }}-astro-${{ hashFiles('**/package-lock.json') }}-
${{ runner.os }}-astro-
This caching setup can reduce build times from 2-3 minutes down to 30-45 seconds for incremental changes. The key includes both dependency and source file hashes, so the cache invalidates appropriately when either changes.
Deployment Strategies
Option 1: GitHub Pages Deployment
For simple static sites, GitHub Pages offers the easiest deployment path:
deploy:
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
permissions:
contents: read
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: astro-build
path: dist
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Upload to GitHub Pages
uses: actions/upload-pages-artifact@v3
with:
path: dist
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
Option 2: Netlify Deployment
For more advanced features like form handling and edge functions:
deploy:
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: astro-build
path: dist
- name: Deploy to Netlify
uses: nwtgck/actions-netlify@v3.0
with:
publish-dir: './dist'
production-branch: main
github-token: ${{ secrets.GITHUB_TOKEN }}
deploy-message: "Deploy from GitHub Actions"
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
You’ll need to add your Netlify tokens to GitHub Secrets under Settings → Secrets and variables → Actions.
Option 3: Custom Server Deployment
For deployment to your own servers, here’s a pattern we use for client projects:
deploy:
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: astro-build
path: dist
- name: Deploy to server via rsync
uses: burnett01/rsync-deployments@7.0.1
with:
switches: -avzr --delete
path: dist/
remote_path: /var/www/html/
remote_host: ${{ secrets.DEPLOY_HOST }}
remote_user: ${{ secrets.DEPLOY_USER }}
remote_key: ${{ secrets.DEPLOY_SSH_KEY }}
Multi-Environment Deployments
Real projects need staging and production environments. Here’s how we handle that:
name: Deploy Astro Site
on:
push:
branches: [ main, staging ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
# ... test steps same as before
build:
runs-on: ubuntu-latest
needs: test
steps:
# ... build steps same as before
deploy-staging:
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/staging' && github.event_name == 'push'
environment: staging
steps:
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: astro-build
path: dist
- name: Deploy to staging
run: |
echo "Deploying to staging environment"
# Your staging deployment commands here
deploy-production:
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
environment: production
steps:
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: astro-build
path: dist
- name: Deploy to production
run: |
echo "Deploying to production environment"
# Your production deployment commands here
The environment key enables GitHub’s environment protection rules, letting you require manual approvals for production deployments or restrict who can deploy.
Error Handling and Notifications
Production pipelines need robust error handling. Here’s how we add Slack notifications for deployment status:
notify:
runs-on: ubuntu-latest
needs: [test, build, deploy-production]
if: always()
steps:
- name: Notify deployment status
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
channel: '#deployments'
text: |
Astro site deployment ${{ job.status }}
Branch: ${{ github.ref_name }}
Commit: ${{ github.sha }}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
For email notifications, use GitHub’s built-in notification settings or add a custom step:
- name: Send email on failure
if: failure()
uses: dawidd6/action-send-mail@v3
with:
server_address: smtp.gmail.com
server_port: 587
username: ${{ secrets.EMAIL_USERNAME }}
password: ${{ secrets.EMAIL_PASSWORD }}
subject: 'Deployment Failed: ${{ github.repository }}'
to: team@yourcompany.com
from: github-actions@yourcompany.com
body: |
Deployment failed for ${{ github.repository }}
Branch: ${{ github.ref_name }}
Commit: ${{ github.sha }}
View logs: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
Performance Optimization
Parallel Job Execution
Run independent tasks simultaneously to cut total pipeline time:
jobs:
lint:
runs-on: ubuntu-latest
steps:
# ... linting steps
test:
runs-on: ubuntu-latest
steps:
# ... testing steps
build:
runs-on: ubuntu-latest
needs: [lint, test] # Wait for both to complete
steps:
# ... build steps
Matrix Builds
Test across multiple Node.js versions:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16, 18, 20]
steps:
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
# ... rest of test steps
Docker Layer Caching
For complex builds with Docker:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v5
with:
cache-from: type=gha
cache-to: type=gha,mode=max
Real-World Example: Our Production Workflow
Here’s the complete workflow we use at Odea Works for client Astro sites. This handles everything from code quality checks to multi-environment deployments:
name: Deploy Astro Site
on:
push:
branches: [ main, staging ]
pull_request:
branches: [ main ]
env:
NODE_VERSION: 18
jobs:
quality:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Run tests
run: npm run test:ci
- name: Check build
run: npm run build
security:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run security audit
run: npm audit --audit-level moderate
build:
runs-on: ubuntu-latest
needs: [quality, security]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Cache Astro build
uses: actions/cache@v4
with:
path: |
.astro
node_modules/.astro
key: ${{ runner.os }}-astro-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('src/**/*.{astro,ts,js}') }}
- name: Install dependencies
run: npm ci
- name: Build site
run: npm run build
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: astro-build-${{ github.sha }}
path: dist/
retention-days: 30
deploy-staging:
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/staging'
environment: staging
steps:
- name: Download artifacts
uses: actions/download-artifact@v4
with:
name: astro-build-${{ github.sha }}
path: dist
- name: Deploy to staging
uses: nwtgck/actions-netlify@v3.0
with:
publish-dir: './dist'
production-deploy: false
github-token: ${{ secrets.GITHUB_TOKEN }}
deploy-message: "Staging deploy from GitHub Actions"
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_STAGING_SITE_ID }}
deploy-production:
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main'
environment: production
steps:
- name: Download artifacts
uses: actions/download-artifact More from the blog
Need reliable infrastructure?
We build CI/CD pipelines, monitoring systems, and deployment infrastructure engineered for reliability.
Get our AI implementation playbook
A practical guide to evaluating, planning, and deploying AI in your business. Free, no spam.
Check your inbox.
Something went wrong. Please try again.