skip to content

cms pipeline

git-based content management with github actions
published:
0 views

what is this?

content lives in a private repo (lib) for local editing with obsidian/neovim. on push, a github action syncs it to the public site repo and triggers a deploy to cloudflare pages.

no database. no cms dashboard. just markdown and git.

the flow

local edit -> push to lib -> webhook -> sooriya action -> build -> cloudflare pages
| | |
neovim/obsidian repository_dispatch rsync content

content sync action

the action lives in .github/actions/content-sync/action.yml. it’s a composite action that:

  1. clones private content repo
  2. rsyncs to src/content/
  3. validates structure

clone step

- name: clone private content repository
shell: bash
env:
GITHUB_TOKEN: ${{ inputs.token }}
REPO_URL: ${{ inputs.repo-url }}
run: |
git config --global url."https://${GITHUB_TOKEN}@github.com/".insteadOf "https://github.com/"
git clone --depth 1 "${REPO_URL}" /tmp/content-repo

uses a fine-grained pat with read access to the private repo. stored as CONTENT_REPO_TOKEN secret.

sync step

- name: sync content to destination
shell: bash
run: |
rsync -av --delete \
--exclude='.git' \
--exclude='.github' \
--exclude='README.md' \
--exclude='tmp/' \
--exclude='lists/' \
--exclude='.DS_Store' \
/tmp/content-repo/ "${CONTENT_DIR}/"

--delete ensures removed files get deleted on the site too. excludes filter out non-content stuff.

validation

counts files per collection and warns if something looks wrong:

for dir in posts tweets pages proj; do
if [ ! -d "${CONTENT_DIR}/${dir}" ]; then
echo "warning: ${dir} directory not found"
fi
done

deploy workflow

the main workflow in .github/workflows/deploy.yml:

on:
push:
branches: [main]
repository_dispatch:
types: [content-updated]
jobs:
build:
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/content-sync
with:
token: ${{ secrets.CONTENT_REPO_TOKEN }}
repo-url: ${{ secrets.CONTENT_REPO_URL }}
content-dir: src/content
- uses: withastro/action@v2
with:
package-manager: bun

triggers on:

  • push to main (site code changes)
  • repository_dispatch (content changes via webhook)

triggering from content repo

to trigger a deploy when content changes, you need a webhook from the private repo.

option 1: github action in the content repo:

# in lib/.github/workflows/trigger-deploy.yml
on:
push:
paths:
- "log/**"
- "wiki/**"
jobs:
trigger:
runs-on: ubuntu-latest
steps:
- run: |
curl -X POST \
-H "Authorization: token ${{ secrets.SITE_REPO_TOKEN }}" \
-H "Accept: application/vnd.github.v3+json" \
https://api.github.com/repos/thuvasooriya/sooriya/dispatches \
-d '{"event_type":"content-updated"}'

option 2: repository webhook (simpler, but less control).

secrets needed

secretwherepurpose
CONTENT_REPO_TOKENsooriya repopat to clone private content
CONTENT_REPO_URLsooriya repofull url of content repo
SITE_REPO_TOKENlib repopat to trigger site deploy

local development

for local dev, content is symlinked or copied manually. the action only runs in ci.

you can test the sync locally:

Terminal window
rsync -av --delete \
--exclude='.git' \
--exclude='tmp/' \
~/arc/lib/log/ ~/arc/dev/sooriya/src/content/

gotchas

  1. pat expiration: fine-grained pats expire. set a reminder to rotate them.

  2. sync is destructive: --delete flag means anything not in source gets removed. make sure excludes are right.

  3. build caching: astro caches content in .astro/. if things look stale, try bun run clean.

  4. wiki content: wiki lives in lib/wiki/ not lib/log/wiki/. the rsync paths handle this separately in the actual workflow.