Re-running the same commit collided with the prior run's deploy/<sha> branch (non-fast-forward reject). The branch is a disposable deploy artifact; main is the protected human-merge gate, so force-push is safe and makes re-runs work.
bztmon-site
The source for www.bztmon.com — Jonathon Wright's portfolio / résumé site. A fast, animated, security-hardened static site for a platform / infrastructure engineer.
This repo is public. It lives on a self-hosted public Gitea (
git.bztmon.com), isolated from the private homelab GitOps. Never commit secrets — the static site needs none.
Stack
- Astro (static output) + TypeScript + Tailwind v4
- Zero JS by default; tiny islands for the theme toggle + scroll reveals
- Content & config are data-driven (
src/data/) — adding a project never touches a component
Develop
npm install
npm run dev # http://localhost:4321
npm run check # astro check (types + diagnostics)
npm run build # static build → dist/
npm run preview # serve the build locally
npm run gen:og # regenerate the social-preview image (public/og.png)
Project layout
src/
data/ site.ts, socials.ts, skills.ts, projects.ts, experience.ts
components/ Hero, Nav, ThemeToggle, ProjectCard, SkillGroup, ...
layouts/ Layout.astro (SEO/OG, theme bootstrap)
pages/ index.astro, projects/, 404.astro
styles/ tokens.css (theme), global.css
lib/ build-time helpers (cv detection)
scripts/ gen-og.mjs, build-image.sh
nginx/ default.conf (security headers, caching) baked into the image
Dockerfile Debian build stage → nginx-unprivileged runtime
Content TODOs (Jonathon)
- Drop a real CV at
public/cv.pdf— the Download CV button appears automatically. - Fill the
TODO(Jonathon)markers insrc/data/experience.ts,projects.ts,socials.ts(employer names, dates, GitHub/LinkedIn handles).
Publishing a post
A post is just a Markdown file in src/content/blog/. Write one by hand, or generate
a schema-valid one with the publish helper (this is the seam an IaC/CI step calls):
# from flags
node scripts/new-post.mjs --title "My post" --summary "One line" \
--tags "kubernetes,gpu" [--draft] [--bodyFile notes.md]
# from a JSON event (e.g. an Ansible/AWX deploy summary)
echo '{"title":"...","summary":"...","tags":["x"],"body":"## Hi\n..."}' \
| node scripts/new-post.mjs --stdin
Commit the file to main → CI rebuilds and ships. A malformed post fails the build
(frontmatter is zod-validated), so a bad pipeline event never reaches production.
CI/CD
.gitea/workflows/deploy.yml runs on a self-hosted runner (a dedicated unprivileged
user on the bastion):
npm ci → astro check → audit-ci (high/critical gate) → build → scan dist →
SBOM (CycloneDX) → buildah build+push → open a digest-bump PR to home-ops
The PR is never auto-merged — home-ops main is branch-protected; merging it is
what triggers the ArgoCD rollout. The runner holds only least-privilege creds (a
home-ops-scoped deploy key + a PR token + a registry push token).
npm run scan— build-time gate: no secrets, no inline scripts, no third-party origins..audit-ci.json— fails on high/critical advisories. One allowlisted:GHSA-gv7w-rqvm-qjhr(esbuild install-integrity; build-time only, mitigated by the committed lockfile + trusted registry).renovate.json— keeps npm deps and the digest-pinned base images current.
Deploy
Built into a container image, served by nginx-unprivileged on a homelab Kubernetes
cluster, exposed via Cloudflare Tunnel. The image is pinned by digest in the private
home-ops repo and rolled out by ArgoCD. Manual/bootstrap build: scripts/build-image.sh push.
See SECURITY.md for the full security posture.