Continuous Integration for Astro Knots Sites
The general pattern — the verify/deploy split,
--ignore-workspace, action currency, the hoisting trap — lives in the anchor monorepo atcontext-v/blueprints/Continuous-Integration-with-GitHub-Actions.md. Read that first. This document is only what differs for an Astro content site.
Why Care?
Every site here ships through Deploy splash to GitHub Pages or a near-identical sibling, and not one of them runs a check before shipping. For a content site that is more defensible than it sounds — but it stops being defensible in three specific situations, and we have already hit all three.
What astro build already proves
More than you would expect, which is why sites got away with no tests for so long. A successful build means:
every content file passed its collection schema
every
importresolvedevery component compiled
every page rendered to HTML without throwing
every
getStaticPathsproduced a route
That is a real integration test, and it runs on every push already.
What it does not prove is anything about the content of the HTML. A page that renders [[Some-Page]] as literal text builds perfectly. So does one whose internal links all 404, or whose markdown pipeline silently stopped running a plugin. The build checks that the machine ran; nothing checks what came out.
Four checks worth adding, cheapest first
1. astro check — the free one
- run: pnpm exec astro checkType errors in .astro frontmatter, unused props, bad component signatures. It usually already exists as a script. If a site adds exactly one check, this is the one.
2. Raw syntax leaking into rendered HTML
The failure mode LFM sites are most prone to, because it is invisible to the build and obvious to a reader. Grep the built output:
- name: No raw markdown syntax in output
run: |
if grep -rlE '\[\[|\[!' dist --include="*.html" | grep -v '/llms'; then
echo "::error::Unrendered wikilink or callout marker reached dist/"
exit 1
fiThis is not hypothetical. The lfm splash currently renders raw [[wikilinks]] on thirteen published pages, because its changelog and context-v collections go through Astro's own renderMarkdown rather than through LFM. A build-output grep would have caught it the day it started.
3. Internal links resolve
A link checker over dist/ catches the renames and moved files that a build cannot see. Worth it on sites with heavy cross-linking; skip on small ones.
4. Real tests, when there is real logic
Most sites are content plus components and do not need a suite. A site with a resolver, a loader, or a data transform does. Put it in pnpm test and add the job from the anchor blueprint.
The decision that creates most of the trouble: source or published package?
Astro Knots sites consume @lossless-group/lfm one of two ways, and the choice has CI consequences that are not obvious.
Pinned to JSR — "@lossless-group/lfm": "npm:@jsr/lossless-group__lfm@^0.5.1". What almost every site does, and what Workspace vs JSR for LFM Consumers says most sites should do. CI is simple: install, build, done.
Importing package source — import { parseMarkdown } from '../../../src/index.ts'. What the lfm splash does, deliberately: it makes the demo page a live integration test of the local package rather than a brochure for a published one.
The second buys real value and costs you this:
A nested site that compiles source from its parent inherits the parent's dependencies. Node resolves a bare import by walking up from the importing file, so an import inside
../../src/preset.tsresolves from the parent'snode_modulesand never from the site's — regardless of what the site'spackage.jsondeclares.
So the workflow needs two installs, parent first:
- name: Install package deps (site compiles package source)
working-directory: .
run: pnpm install --frozen-lockfile=false --ignore-workspace
- name: Install
run: pnpm install --frozen-lockfile=false --ignore-workspace # defaults to the site dirThis hides indefinitely on a developer machine, because you always have both installed and the site quietly borrows from one directory up. It surfaces the day a pnpm/action-setup bump installs a pnpm that hoists less — which is exactly how it surfaced on lfm, as two consecutive red deploys.
Reproduce it before pushing by hiding what CI does not have:
mv node_modules .node_modules_hidden
cd splash && pnpm build # fails the way the runner fails
cd .. && mv .node_modules_hidden node_modulesSplash deploys: what to watch
The Deploy splash to GitHub Pages workflow is replicated across roughly fourteen repos, so a fix to one is usually a fix to all of them. Two notes:
The Pages action trio moves together.
configure-pages,upload-pages-artifactanddeploy-pagesare a matched set acting on a live site. Bump them as their own change, not as a rider — unlikecheckout,setup-nodeandpnpm/action-setup, which are safe to bump freely and are currently several majors behind across the tree.--ignore-workspaceis already there in most of these, and it is load-bearing rather than incidental. Every site sits inside a pnpm workspace locally and does not on a runner.
Adding this to a site
Start with
astro checkin the existing deploy workflow, before the build step. One line, immediate value.Add the output grep if the site renders LFM content.
Only add a separate
test.ymlwhen there is logic worth asserting — follow the anchor blueprint's shape.If the site imports package source, add the parent install and verify with the hidden-
node_modulestrick above.Watch the first run and read the log, not just the checkmark.
See also
lossless-monorepo/context-v/blueprints/Continuous-Integration-with-GitHub-Actions.md— the general pattern this specializesWorkspace vs JSR for LFM Consumers — which consumption mode a given site should be in, and why only one site should be the sandbox
lfm/.github/workflows/pages.yml— a splash deploy that installs parent deps first, with the reasoning inlineMaintain a Github Splash Page for each Repo — the deploy workflow every site inherited