A Deploy Pipeline That Catches Problems Before Production
Most deploy pipelines do one thing: build the site and upload it. That is automation, not verification. If the build succeeds and the site is wrong, the pipeline has accomplished nothing except making the mistake faster.
A pipeline that catches problems is a sequence of gates, where each one can stop the deploy. The mechanics are well documented. The failure modes of those mechanics are less well known.
The check that most static sites are missing
Start with the single highest-value gate, because it is one line and it is off by default on a lot of projects.
astro build does not type-check your project. The documentation is explicit about the mechanism: the build transpiles, and the dev server “won’t perform any type checking.” Types that do not hold will happily produce a build.
The documented remedy is a separate command, and its own documentation states its purpose in CI terms — astro check “runs diagnostics (such as type-checking within .astro files)… If any errors are found the process will exit with a code of 1. This command is intended to be used in CI workflows.”
So the recommended build script is:
{
"scripts": {
"build": "astro check && astro build"
}
}
Two practical notes. astro check defaults to failing only on errors, not warnings, and that threshold is configurable. And because it fails by exit code, it works as a gate with no extra plumbing — the build fails, the deploy never runs.
If you take one thing from this article, take this: a static site that builds is not a static site that compiles.
Gating: needs, and the thing that makes it useless
In GitHub Actions, job dependencies are set with needs, which accepts a single job name or a list. The documented behaviour has a sting in it:
“If a job fails or is skipped, all jobs that need it are skipped unless the jobs use a conditional expression.”
That is what you want for a deploy job that needs a test job. But it also means a skipped dependency skips the deploy — not just a failed one. If you conditionally skip your checks, you conditionally skip the thing that depends on them, and the site stops deploying. The documented escape is always(), which overrides the skip.
A minimal shape:
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npx astro check
deploy:
needs: check
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- run: echo "deploy here"
Required checks, and the two configuration mistakes
Making a check required is a repository setting, not a workflow setting, and two documented quirks make it less reliable than it looks.
Duplicate job names across workflows break it. The documentation warns that identical job names “can cause ambiguous status check results and block pull requests.” If you have build in two workflow files, your required check may never report a clean result — and the symptom is a pull request that cannot be merged, which reads as a permissions problem rather than a naming one.
Rulesets aggregate restrictively. Multiple rulesets can target the same branch, and the documented behaviour is that aggregated rules take “the most restrictive version” of conflicting requirements. So adding a rule does not replace an old one — it can tighten it in a way you did not intend. Combine, don’t stack.
Also note the documented rule for what a required check must report: successful, skipped, or neutral. A check that reports anything else blocks the merge.
Caching, done correctly
Dependency installation is the slowest part of most pipelines and the easiest to cache. The mechanism is actions/cache, and the documented key format is worth copying rather than improvising:
${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
Two documented constraints to respect: the key is required and capped at 512 characters, and restore-keys are searched sequentially — so ordering them from most specific to least specific is what makes partial restores work.
For Node specifically, actions/setup-node with cache: npm creates the cache for you with minimal configuration, which is usually the right amount of effort.
One caveat on cost that applies to private repositories: cache storage is metered, and Actions minutes are billed for private repos after a monthly allowance that varies by plan. Public repositories are free on standard runners. If your repository is private and you are caching aggressively, check the storage line.
The checks that are worth adding after astro check
Four candidates, in order of how often they catch a real mistake:
A link check. Internal broken links are the most common defect on a content site and the least visible. This belongs in CI, but external link checks produce noise — see the link-checking article for what to exclude and why a scheduled run is the right shape for external URLs, not a per-commit gate.
A build-time content validation. Do your articles have required frontmatter? A schema in the content collection already enforces this, and it fails the build. If you are not using a schema, this is the highest-leverage thing to add after astro check.
A size budget. If your bundle grows past a threshold, fail. This catches dependency additions that nobody reviewed, and it is far cheaper than discovering the problem in a Core Web Vitals report three months later.
A redirect check. If you maintain a redirect file, verify that the destinations actually exist in the build output. A redirect to a 404 is worse than no redirect, because it looks deliberate.
Note what is not on that list: there is no documented recommendation from any platform that static sites run an HTML validator in CI. It is a reasonable thing to do and it is not a standard practice — treat it as your decision.
The pipeline shape I would use
| Stage | Gate | Runs |
|---|---|---|
| Build | astro check + astro build | Every push |
| Content | Frontmatter schema validation | Every push |
| Links (internal) | Link check, internal URLs only | Every push |
| Deploy | Needs all of the above | Main branch only |
| Links (external) | Full link check | Scheduled, weekly |
The split between internal and external link checking is deliberate and it is the difference between a check people trust and a check people ignore. External URLs fail for reasons that have nothing to do with you — rate limits, bot protection, transient outages — and a gate that fails for those reasons gets disabled within a month.
When not to build this
If you are the only committer, deploying directly from your laptop, a CI pipeline is overhead you will resent. The one piece worth keeping regardless is astro check in the build script — it costs nothing to run locally and it catches the class of error that a successful build hides.
The rest of it earns its place when there is a second person, or when a broken deploy costs more than a few minutes of your own time.
Written by TestedHost. Every recommendation on this site comes from running the setup described, on a live deployment — not from a vendor spec sheet. Spotted something out of date? Tell us.