Local release workflow with npm version hooks
I don’t have a CI/CD pipeline for this site. It’s a static Astro site deployed through Laravel Forge. Forge pulls from
git and serves the dist/ folder directly. No build step on the server.
That means dist/ needs to be in git. But I don’t want it showing up in every commit cluttering the history. So I
gitignore it and only include it in release commits.
The setup
Three scripts in package.json do the work:
{
"scripts": {
"test:routes": "vitest run tests/routes.test.js",
"preversion": "npm run build && npm run test:routes",
"version": "git add -f dist",
"release:major": "npm version major",
"release:minor": "npm version minor",
"release:patch": "npm version patch"
}
}
npm version is a built-in npm command that bumps the version in package.json, creates a commit and tags it. The
hooks run automatically:
- preversion — build and run tests. If anything fails the release stops. No need to clean first since Astro writes
the entire
dist/from scratch on every build. - version — force-add
dist/to the commit. The-fflag overrides.gitignore.
How I release
npm run release:patch # 3.0.1 → 3.0.2
git push origin main --tags
That’s it. Forge picks up the push and the site is live. The git history stays clean — dist/ only appears in tagged
release commits, not in every little change so you can commit and push without worrying about it’s going to production
before I really want it to. No need to have a develop branch which can feel a bit overkill for a simple static site as
this.
The gitignore trick
.gitignore has dist/ listed. This means:
- Day to day work:
git statusdoesn’t show dist changes - Release time:
git add -f distforces it in for that one commit - After release: dist is tracked in that commit but gitignore still hides future changes
You get the best of both worlds. Clean working directory during development, built files included when you actually release.
Why not build on the server?
I could add npm install && npm run build as a deploy script in Forge. But that means Node.js on the server,
dependencies to install, builds that can fail in production or get messed up. For a static site that feels like
unnecessary complexity.
Building locally means I see exactly what goes out. If the build breaks or a test fails I catch it before pushing. The server just serves files. Simple.