Compare commits

...

15 Commits

Author SHA1 Message Date
Evan Scamehorn
231d346695 add summary to transit sim
All checks were successful
Deploy website to VPS / build-and-deploy (push) Successful in 47s
2026-01-08 20:19:32 -06:00
Evan Scamehorn
8d1d939d94 transit sim project
All checks were successful
Deploy website to VPS / build-and-deploy (push) Successful in 49s
2026-01-08 20:17:34 -06:00
Evan Scamehorn
efa32c13b5 add transit sim project
All checks were successful
Deploy website to VPS / build-and-deploy (push) Successful in 41s
2026-01-08 20:04:03 -06:00
Evan Scamehorn
b094595b94 add: self hosted pipeline blog entry
All checks were successful
Deploy website to VPS / build-and-deploy (push) Successful in 41s
2026-01-08 19:52:58 -06:00
Evan Scamehorn
1c180baf89 fix ssl error
All checks were successful
Deploy website to VPS / build-and-deploy (push) Successful in 40s
2026-01-08 19:28:09 -06:00
Evan Scamehorn
8c95abdc50 remove pnpm verson lock during ci build
Some checks failed
Deploy website to VPS / build-and-deploy (push) Failing after 25s
2026-01-08 19:24:21 -06:00
Evan Scamehorn
f3ff668cb9 fix pnpm bug
Some checks failed
Deploy website to VPS / build-and-deploy (push) Failing after 23s
2026-01-08 19:20:45 -06:00
Evan Scamehorn
79e4593db3 use custom site-builder oci image
Some checks failed
Deploy website to VPS / build-and-deploy (push) Failing after 19s
2026-01-08 19:10:52 -06:00
Evan Scamehorn
4cdafa8d2b dont delete cv when rsyncing
All checks were successful
Deploy website to VPS / build-and-deploy (push) Successful in 1m15s
2026-01-08 15:48:13 -06:00
Evan Scamehorn
f5fd6fb36d cv upload
All checks were successful
Deploy website to VPS / build-and-deploy (push) Successful in 1m13s
2026-01-08 15:44:04 -06:00
Evan Scamehorn
4746ff594f fix perms on rsync
All checks were successful
Deploy website to VPS / build-and-deploy (push) Successful in 1m15s
2026-01-07 17:22:30 -06:00
Evan Scamehorn
39273e757c setup go
Some checks failed
Deploy website to VPS / build-and-deploy (push) Failing after 2m4s
2026-01-07 17:16:54 -06:00
Evan Scamehorn
a71126ea29 debian packages
Some checks failed
Deploy website to VPS / build-and-deploy (push) Failing after 1m0s
2026-01-07 17:13:35 -06:00
Evan Scamehorn
3f205c2a94 use debian
Some checks failed
Deploy website to VPS / build-and-deploy (push) Failing after 29s
2026-01-07 17:10:56 -06:00
Evan Scamehorn
ca42519222 gitea action to deploy with rsync w ssh key
Some checks failed
Deploy website to VPS / build-and-deploy (push) Failing after 2s
2026-01-07 16:59:40 -06:00
10 changed files with 209 additions and 276 deletions

View File

@@ -0,0 +1,71 @@
name: Deploy website to VPS
on:
push:
branches: ['main']
workflow_dispatch:
jobs:
build-and-deploy:
runs-on: site-builder
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get Hugo Version
run: |
if [ -f "hugoblox.yaml" ]; then
VERSION=$(grep "hugo_version" hugoblox.yaml | awk '{print $2}' | tr -d "'\"")
echo "HUGO_VERSION=$VERSION" >> $GITHUB_ENV
else
echo "HUGO_VERSION=0.125.0" >> $GITHUB_ENV
fi
- name: Install Node Dependencies
run: |
if [ -f "package.json" ]; then
# Force pnpm to use the version baked into our Nix image
sed -i '/"packageManager":/d' package.json
pnpm config set manage-package-manager false
pnpm install --no-frozen-lockfile
fi
- name: Build with Hugo
env:
HUGO_ENVIRONMENT: production
run: |
git config --global http.sslCAInfo /etc/ssl/certs/ca-certificates.crt
hugo --minify --baseURL "https://ejs.cam/"
- name: Generate Pagefind search index
run: |
if [ -f "package.json" ] && grep -q "pagefind" package.json; then
pnpm dlx pagefind --source "public" || npx pagefind --source "public"
fi
- name: Deploy via Rsync
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
REMOTE_HOST: ${{ secrets.REMOTE_HOST }}
REMOTE_USER: ${{ secrets.REMOTE_USER }}
REMOTE_PORT: ${{ secrets.REMOTE_PORT }}
run: |
mkdir -p ~/.ssh
chmod 700 ~/.ssh
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -p "$REMOTE_PORT" "$REMOTE_HOST" >> ~/.ssh/known_hosts
rsync -avz --delete \
--exclude 'cv.pdf' \
--omit-dir-times \
--no-perms \
--no-owner \
--no-group \
-e "ssh -p $REMOTE_PORT" \
./public/ \
$REMOTE_USER@$REMOTE_HOST:/var/www/

2
.github/FUNDING.yml vendored
View File

@@ -1,2 +0,0 @@
github: gcushen
custom: https://hugoblox.com/sponsor/

BIN
.github/preview.webp vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -1,124 +0,0 @@
name: Deploy website to GitHub Pages
env:
NODE_VERSION: '20'
on:
# Trigger the workflow every time you push to the `main` branch
push:
branches: ['main']
# Allows you to run this workflow manually from the Actions tab on GitHub
workflow_dispatch:
# Provide permission to clone the repo and deploy it to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: 'pages'
cancel-in-progress: false
jobs:
# Build website
build:
if: github.repository_owner != 'HugoBlox'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
# Fetch history for Hugo's .GitInfo and .Lastmod
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Setup pnpm
if: hashFiles('package.json') != ''
uses: pnpm/action-setup@v4
- name: Get Hugo Version
id: hugo-version
run: |
VERSION=$(grep "hugo_version" hugoblox.yaml | awk '{print $2}' | tr -d "'\"")
echo "HUGO_VERSION=$VERSION" >> $GITHUB_ENV
- name: Install dependencies
run: |
# Install Tailwind CLI if package.json exists
if [ -f "package.json" ]; then
echo "Installing Tailwind dependencies..."
pnpm install --no-frozen-lockfile || npm install
fi
- name: Setup Hugo
uses: peaceiris/actions-hugo@v3
with:
hugo-version: ${{ env.HUGO_VERSION }}
extended: true
# Cache dependencies (Go modules, node_modules) - stable, rarely changes
- uses: actions/cache@v4
with:
path: |
/tmp/hugo_cache_runner/
node_modules/
modules/*/node_modules/
key: ${{ runner.os }}-hugo-deps-${{ hashFiles('**/go.mod', '**/package-lock.json',
'**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-hugo-deps-
# Cache Hugo resources (processed images, CSS) - invalidates only when assets/config change
- uses: actions/cache@v4
with:
path: resources/
key: ${{ runner.os }}-hugo-resources-${{ hashFiles('assets/**/*', 'config/**/*',
'hugo.yaml', 'package.json') }}
restore-keys: |
${{ runner.os }}-hugo-resources-
- name: Setup Pages
id: pages
uses: actions/configure-pages@v5
- name: Build with Hugo
env:
HUGO_ENVIRONMENT: production
run: |
echo "Hugo Cache Dir: $(hugo config | grep cachedir)"
hugo --minify --baseURL "${{ steps.pages.outputs.base_url }}/"
- name: Generate Pagefind search index (if applicable)
run: |
# Check if site uses Pagefind search
if [ -f "package.json" ] && grep -q "pagefind" package.json; then
pnpm dlx pagefind --source "public" || npx pagefind --source "public"
fi
- name: Upload artifact
uses: actions/upload-pages-artifact@v4
with:
path: ./public
# Deploy website to GitHub Pages hosting
deploy:
if: github.repository_owner != 'HugoBlox'
needs: build
# Grant GITHUB_TOKEN the permissions required to make a Pages deployment
permissions:
pages: write # to deploy to Pages
id-token: write # to verify the deployment originates from an appropriate source
# Deploy to the github-pages environment
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

View File

@@ -1,122 +0,0 @@
# Hugo Blox GitHub Action to convert Bibtex publications to Markdown-based webpages
name: Import Publications From Bibtex
# Require permission to create a PR (least privilege principle)
permissions:
contents: write
pull-requests: write
# Run workflow when a `.bib` file is added or updated in the `data/` folder
on:
push:
branches: ['main']
paths: ['publications.bib']
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Prevent concurrent runs of this workflow
concurrency:
group: import-publications-${{ github.ref }}
cancel-in-progress: true
jobs:
hugoblox:
if: github.repository_owner != 'HugoBlox'
runs-on: ubuntu-latest
timeout-minutes: 10
env:
ACADEMIC_VERSION: '>=0.10.0'
PYTHON_VERSION: '3.12'
steps:
- name: Checkout the repo
uses: actions/checkout@v4
with:
# Only need recent history for publication import
fetch-depth: 1
- name: Set up Python 3.13
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Setup pip cache
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-academic-${{ env.ACADEMIC_VERSION }}
restore-keys: |
${{ runner.os }}-pip-academic-
${{ runner.os }}-pip-
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install "academic${{ env.ACADEMIC_VERSION }}"
- name: Validate publications.bib file
if: ${{ hashFiles('publications.bib') != '' }}
run: |
if [ ! -f "publications.bib" ]; then
echo "❌ publications.bib file not found"
exit 1
fi
echo "✅ publications.bib file found"
- name: Run Academic (Bibtex To Markdown Converter)
# Check `.bib` file exists for case when action runs on `.bib` deletion
# Note GH only provides hashFiles func in `steps.if` context, not `jobs.if` context
if: ${{ hashFiles('publications.bib') != '' }}
run: |
echo "🚀 Starting publication import..."
academic import publications.bib content/publication/ --compact --verbose
echo "✅ Publication import completed successfully"
# Verify that files were created
if [ -d "content/publication" ] && [ "$(ls -A content/publication/)" ]; then
echo "📚 Publications imported: $(ls content/publication/ | wc -l) items"
else
echo "⚠️ No publications were imported"
fi
continue-on-error: false
- name: Create Pull Request
# Set ID for `Check outputs` stage
id: cpr
uses: peter-evans/create-pull-request@v6
with:
commit-message: 'feat(publications): import latest publications from bibtex'
title: 'Hugo Blox Builder - Import latest publications from Bibtex'
body: |
🔄 **Automated Publication Import**
This PR automatically imports the latest publications from `publications.bib` to `content/publication/`.
**Changes:**
- 📚 Updated publication entries
- 🏷️ Processed bibliographic data
---
将最新的出版物从`publications.bib`导入到`content/publication/`。
📖 [View Documentation](https://github.com/GetRD/academic-file-converter)
base: main
labels: automated-pr, content, publications
branch: hugoblox-import-publications
delete-branch: true
draft: false
- name: Check outputs
if: ${{ steps.cpr.outputs.pull-request-number }}
run: |
echo "✅ Successfully created Pull Request!"
echo "📝 Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}"
echo "🔗 Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}"
echo "🎯 Operation completed at $(date)"
- name: Report workflow status
if: always()
run: |
if [ "${{ job.status }}" == "success" ]; then
echo "🎉 Workflow completed successfully"
else
echo "❌ Workflow failed - check logs for details"
exit 1
fi

View File

@@ -1,24 +0,0 @@
# This workflow is only for the Hugo Blox repository. It is safe to delete from your site.
name: Updater (WIP)
on:
schedule:
- cron: 0 0 * * 0
# Allows you to run this workflow manually from the Actions tab on GitHub.
workflow_dispatch:
# Provide permission to clone the repo and deploy it to GitHub Pages
permissions:
contents: write
jobs:
update:
if: github.repository_owner == 'HugoBlox'
runs-on: ubuntu-latest
steps:
- uses: HugoBlox/gh-action-updater@v2
with:
feed-url: https://hugoblox.com/rss.xml
readme-section: news
branch: main
github_token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,7 +1,7 @@
--- ---
# Leave the homepage title empty to use the site title # Leave the homepage title empty to use the site title
title: '' title: ''
date: 2022-10-24 date: 2026-01-08
type: landing type: landing
design: design:
@@ -17,7 +17,7 @@ sections:
# Show a call-to-action button under your biography? (optional) # Show a call-to-action button under your biography? (optional)
button: button:
text: Download CV text: Download CV
url: https://cv.ejs.cam url: https://ejs.cam/cv.pdf
headings: headings:
about: '' about: ''
education: '' education: ''
@@ -48,8 +48,9 @@ sections:
- projects - projects
# featured_only: true # featured_only: true
design: design:
view: article-grid view: card
columns: 3 # view: article-grid
# columns: 3
- block: collection - block: collection
id: blogs id: blogs
content: content:

View File

@@ -0,0 +1,74 @@
---
title: Building a Deterministic, Self-Hosted Build Pipeline on NixOS
summary: Taking control of reliability and privacy by implementing a private, fully controlled CI/CD pipeline that eliminates external dependencies.
date: 2026-01-08
authors:
- me
tags:
- CI/CD
- OCI
- Infrastructure as code
- Reproducability
- Envrionment Parity
- NixOS
content_meta:
trending: true
---
## **Building a Deterministic, Self-Hosted Build Pipeline on NixOS**
In modern DevOps, the "path of least resistance" usually involves a mix of GitHub Actions, Docker Hub, and managed hosting. While efficient, this approach often treats the build environment as a "black box," introducing external dependencies and privacy trade-offs. To deepen my understanding of infrastructure and gain total control over my deployment lifecycle, I migrated my infrastructure to a fully self-hosted, declarative stack on NixOS.
The journey began with a migration from a standard Debian VPS on Digital Ocean. Using the `nixos-consume` script, I bootstrapped NixOS directly onto the live instance, replacing a mutable, manually-managed OS with a deterministic one. My entire server is now defined via Nix Flakes, allowing me to manage Nginx, Gitea, and my CI/CD runners from a single, version-controlled source of truth.
##### **The Architecture: Gitea, Podman, and Nginx**
At the center of this ecosystem is Gitea, which handles both source control and CI/CD via Gitea Actions. For the container runtime, I opted for Podman, integrated directly into my NixOS configuration. Nginx acts as the frontend, utilizing the ACME module for automated SSL certificate renewals.
One of the most significant technical challenges was the creation of a custom OCI image for my runners. Rather than pulling generic images from the internet, I used Nixs `dockerTools.buildLayeredImage` to "bake" a specialized environment containing only the tools I need: Hugo, Go, Node.js, and Pythons `uv`.
```nix
# Snippet from runner-image.nix
runnerImage = pkgs.dockerTools.buildLayeredImage {
name = "localhost/site-builder";
tag = "latest";
contents = with pkgs; [ bashInteractive nodejs_20 hugo go uv rsync openssh cacert ];
fakeRootCommands = ''
mkdir -p etc/ssl/certs
ln -s ${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt etc/ssl/certs/ca-certificates.crt
echo "root:x:0:0:root:/root:/bin/bash" > etc/passwd
'';
};
```
##### **Solving the "Minimal Image" Hurdles**
Building a container image from scratch in Nix results in an extremely minimal environment. This led to several interesting DevOps puzzles. For instance, `rsync` and `ssh` initially failed because the naked image lacked a `/etc/passwd` file, leaving the system unable to identify the current user. Similarly, the Go compiler and Git struggled with SSL verification because they expected certificates at hardcoded Linux paths.
I solved these by using `fakeRootCommands` to inject a standard user structure and symlink the Nix-provided certificates into `/etc/ssl/certs`. This ensured that the polyglot toolchain—Node.js for search indexing, Go for Hugo modules, and Python for resume rendering—all shared a consistent, secure environment.
##### **The Deployment Pipeline**
With the custom runner image loaded into Podman, the deployment pipeline is remarkably lean. Because the image is pre-baked with all dependencies, the Gitea Action doesn't spend time downloading packages or setting up environments. It simply clones the repo, builds the site, and uses `rsync` to move the static files to `/var/www`.
```yaml
# deploy.yaml snippet
- name: Build with Hugo
run: hugo --minify --baseURL "https://ejs.cam/"
- name: Deploy via Rsync
run: |
ssh-keyscan -p "$REMOTE_PORT" "$REMOTE_HOST" >> ~/.ssh/known_hosts
rsync -avz --delete --exclude 'cv.pdf' ./public/ user@host:/var/www/
```
This setup protects specific files, like my CV (generated by a separate `rendercv` pipeline), while ensuring the rest of the site is updated atomically.
##### **Conclusion and Future Outlook**
By self-hosting this entire stack, Ive achieved a build-and-deploy cycle that is both private and incredibly fast, with zero reliance on external service uptimes. The next step in this project is migrating my plaintext secrets into an encrypted management system like `sops-nix`. This will allow me to publish my entire NixOS configuration publicly, serving as a fully transparent and reproducible blueprint for declarative DevOps.
This project was more than just a hosting exercise; it was a deep dive into the "plumbing" of the modern web, proving that with the right tools, its possible to build professional-grade infrastructure that is both autonomous and easy to maintain.

Binary file not shown.

After

Width:  |  Height:  |  Size: 722 KiB

View File

@@ -0,0 +1,59 @@
---
title: 3D City Transit Simulator
summary: Interactively plan, construct, manage, and view simulated ridership of your own transit network for cities with my transit simulator!
date: 2025-12-15
links:
- type: code
url: https://git.ejs.cam/evan/transit-sim
- type: site
url: https://evan203.github.io/city-sim
tags:
- THREE.js
- Python
---
Interactively plan, construct, manage, and view simulated ridership of your own
transit network for cities with my transit simulator!
### 1. Route Planning & Construction
* **Drafting Mode:** The game separates "Planning" from "Building." You must click "Create New Route" to enter a drafting mode where you can experiment without spending money.
* **Point-and-Click Plotting:** You build routes by clicking on the ground. The system automatically finds the shortest path along the road network between the points you select.
* **Smart Snapping (Ghost Marker):** When hovering the mouse over the map in drafting mode, a transparent "ghost" sphere appears, indicating exactly which road intersection or node the route will snap to if you click.
* **Draft Interaction:**
* **Add Point:** Left-click to extend the route.
* **Move Point:** Click and drag existing yellow markers to adjust the path dynamically.
* **Draft Statistics:** While planning, you see real-time estimates for:
* **Length:** Total distance of the route.
* **Cost:** Construction cost (track/road upgrades) + Fleet cost (buses required).
* **Est. Riders:** Projected daily passengers based on the population density near your stops.
* **Commit or Discard:** You can finalize the route (spending the money) or discard the draft to cancel.
### 2. Route Management
* **Active Route List:** A sidebar lists all currently operating transit lines.
* **Color Customization:** You can click the colored box next to any route number to pick a custom color for that line and its vehicles.
* **Editing:** You can click the "Pencil" icon to edit a route. *Note: This deletes the existing route and puts its nodes back into "Draft Mode" for you to redraw.*
* **Deleting:** You can permanently remove a route to clear clutter (though construction costs are sunk).
### 3. Economy & Simulation
* **Budget System:** You start with a fixed amount of capital ($1,000,000). You must manage construction costs against your remaining funds.
* **Daily Income:** Every in-game "Day," you earn cash based on the total ridership across all your lines (Ticket Sales).
* **Floating Feedback:** When a day passes, floating green text appears over the UI showing exactly how much cash you just earned.
* **Ridership Logic:** Ridership is calculated based on "Synergy"—connecting Residential areas (Population) to Commercial/Industrial areas (Jobs).
* **Public Approval:** A percentage score (0-100%) that tracks how happy the city is. This is calculated based on how many buildings are within walking distance (approx. 600m) of your transit stops.
### 4. Visuals & Map Modes
* **3D City Rendering:** The map features extruded 3D buildings, water bodies, parks, and a road network.
* **Vehicle Simulation:** Small blocky buses travel along your constructed routes in real-time.
* **Data Views:** You can toggle the map visualization to help plan better routes:
* **Standard:** Default visual look.
* **Zoning Density:** Colors buildings by type (Purple for Residential, Blue for Commercial) and intensity (darker colors = higher density/more potential riders).
* **Transit Coverage (Approval):** A heat map showing service coverage. Buildings turn Green if they are close to a stop, Yellow if they are borderline, and Red if they have no transit access.
### 5. System Features
* **Save/Load System:** You can save your current city state (routes, budget, day, approval) to a local JSON file and load it back later to continue playing.
* **UI Toggling:** The entire interface can be hidden/shown via a hamburger menu button for cinematic screenshots.