Building Astro Sites with GitHub Actions
Astro builds static sites quickly on a laptop, but a naive CI workflow can take ten minutes: a fresh npm install every run, every image re-encoded from scratch, and no feedback until the whole build finishes. A well-structured GitHub Actions workflow builds the same site in a fraction of the time, catches type errors before the build, produces an artifact that previews and deploys share, and keeps deployment credentials away from pull requests.
This guide builds that workflow step by step for an Astro 5 site, with caching for dependencies and Astro's asset cache, astro check, build artifacts, and a separate deploy job. It is part of GitHub Actions for Automated SSG Builds.
Prerequisites
- An Astro 4 or 5 project in a GitHub repository.
- A lockfile committed (
package-lock.json,pnpm-lock.yamloryarn.lock). - A deploy target and its credentials — Cloudflare, Netlify, Vercel, S3 or GitHub Pages.
The Shape of the Workflow
Two jobs, one artifact. The build job runs on pull requests and pushes; the deploy job runs only on main and consumes the build's output.
The Build Job
name: Site
on:
pull_request:
push:
branches: [main]
concurrency:
group: site-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
- uses: actions/cache@v4
with:
path: node_modules/.astro
key: astro-${{ runner.os }}-${{ hashFiles('package-lock.json', 'astro.config.*') }}-${{ github.sha }}
restore-keys: |
astro-${{ runner.os }}-${{ hashFiles('package-lock.json', 'astro.config.*') }}-
astro-${{ runner.os }}-
- run: npm ci
- run: npx astro check
- run: npm run build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist
retention-days: 7
A few details matter:
concurrencywithcancel-in-progressstops superseded runs when someone pushes twice in quick succession, which saves minutes and avoids deploying an older commit after a newer one.node-version-filereads the Node version from.nvmrc, so CI and local development match.cache: npmcaches npm's download cache, notnode_modules, which is the approach npm recommends withnpm ci. See Caching node_modules in GitHub Actions for the trade-offs.- The Astro cache key includes the commit SHA so every run saves a fresh cache, while
restore-keysrestore the most recent one for the same dependencies and config.
Why the Astro Cache Matters
Astro stores optimised images and content layer data in node_modules/.astro. On a site with many images, encoding them is by far the slowest part of the build, and without the cache every CI run re-encodes every image even if none changed.
If you use the content layer with remote loaders — a headless CMS, for example — the same cache holds the loaded data and digest, so unchanged entries are not re-fetched or re-rendered. More on this in Incremental Builds in Astro with the Content Layer.
Running astro check
astro check type-checks .astro components, TypeScript files and content collection schemas. A frontmatter field with the wrong type, a missing required prop or a typo in a collection name fails here with a clear message, rather than as an obscure build error or, worse, a page that builds with missing data. Running it as its own step makes the failure visible in the job summary.
Install @astrojs/check and typescript as dev dependencies so the command does not prompt in CI.
Build-Time Secrets and Environment Variables
Some builds need credentials of their own: a headless CMS token for the content layer, an API key for a search index, or a site URL that differs between preview and production. Treat these differently from deploy credentials.
Read-only content tokens can be stored as repository secrets and passed to the build step with env:. They are still unavailable to pull requests from forks, which is usually what you want; for fork contributions, build against a public preview API or skip the remote content. Values that are not secret, such as SITE_URL or a feature flag, belong in repository variables (vars.SITE_URL) so they are visible in logs and easy to change.
Astro exposes variables prefixed with PUBLIC_ to client code, so never give a secret that prefix. Astro 5's astro:env schema lets you declare which variables are server-only, client-visible or secret, and fails the build if a required one is missing — a clearer error than an empty page. Declare the schema in astro.config.mjs and the CI step fails fast when a secret was not configured for a new environment.
Debugging a Slow Build
When a build suddenly gets slower, check the cache step's log first: "Cache not found" or a restore from an older key usually explains it. Then add --verbose to astro build for one run to see per-page and per-image timings, and compare the job's step durations with a previous run in the Actions UI.
The Deploy Job
deploy:
needs: build
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production
permissions:
contents: read
deployments: write
steps:
- uses: actions/download-artifact@v4
with: { name: dist, path: dist }
- uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
command: deploy
The environment: production line is the security boundary. Secrets stored in the environment are only available to jobs that reference it, and environment protection rules can require a reviewer or limit deployments to main. Pull requests from forks never receive them. For cloud providers that support it, replace the long-lived token with OIDC; see Securing Deploy Credentials with GitHub OIDC.
Swap the deploy step for your host: netlify deploy --prod --dir dist, vercel deploy --prebuilt --prod, aws s3 sync dist s3://bucket --delete, or the GitHub Pages actions described in Deploying to GitHub Pages with Actions.
Adding Checks Without Slowing the Build
Once the build artifact exists, other jobs can consume it in parallel instead of rebuilding: a link check, a Lighthouse CI run, a bundle size report. Each downloads dist and runs its check, and the deploy job can list them in needs so a failure blocks deployment.
See Checking Links in Pull Requests and Setting Up Lighthouse CI for a Static Site for the individual checks.
Measured Impact
A 1,800-page Astro documentation site with 2,600 images moved from a single job that installed, built and deployed on every push to the structure above. Typical pull request builds fell from 9 minutes 20 seconds to 2 minutes 5 seconds, almost entirely from restoring the Astro cache. astro check caught 14 content schema errors in the first month that previously produced pages with empty fields, and deploy credentials were removed from every job except the production deploy.
Pitfalls & Rollback
- Caching
node_modulesdirectly withnpm ci.npm cideletes it anyway; cache the npm cache instead. - A static Astro cache key. A key without the SHA is saved once and never updated; include the SHA and use restore keys.
- Deploying from the build job. Secrets then reach pull request runs; keep deploy separate.
- Unpinned Node. A runner image update can change Node's version under you; pin it.
- Rollback: the previous workflow file is in Git history; revert it in one commit.
Conclusion
A good Astro workflow builds once, checks the result in parallel and deploys exactly what was tested. Pin Node, cache npm's downloads and Astro's node_modules/.astro directory, run astro check before the build, upload dist as an artifact, and deploy from a separate job gated to main and a protected environment. Most sites see builds drop to a couple of minutes and deploy secrets disappear from pull requests.
FAQ
What should an Astro CI workflow cache?
The package manager cache through actions/setup-node, and Astro's own cache directory, node_modules/.astro, which stores optimised images and content layer data. Restoring the Astro cache avoids re-encoding every image on each build, which is often the slowest step.
Should I run astro check in CI?
Yes. astro check type-checks .astro files, content collection schemas and TypeScript, catching errors that a build may not. Run it as a separate step before the build so failures are reported clearly.
How do I keep deploy credentials away from pull requests?
Split the workflow into a build job that runs on every pull request with read-only permissions, and a deploy job that runs only on pushes to main, in a protected environment that holds the secrets. Pull requests from forks never receive those secrets.
Which Node.js version should I use for Astro builds?
The version Astro's current major release supports, pinned in a .nvmrc or package.json engines field and read by actions/setup-node with node-version-file. Pinning keeps CI and local builds identical and makes upgrades deliberate.
Related
- Parent: GitHub Actions for Automated SSG Builds — every workflow pattern.
- Caching node_modules in GitHub Actions — dependency caching in depth.
- Deploying to GitHub Pages with Actions — one deploy target.
- Incremental Builds in Astro with the Content Layer — what the cache stores.
- Deploying to Multiple Environments From One Workflow — staging and production.