Files
site-publish/README.md
T
Donavan Fritz d431fbddb4 site-publish: honor site.yaml excludes during S3 sync
site.yaml can now declare excludes: [paths/patterns] that are passed to
`aws s3 sync` and `aws s3 cp` as --exclude flags, so the listed objects
are neither uploaded from the build dir nor deleted from the bucket.
Escape hatch for assets managed out-of-band (e.g. large PDFs uploaded
via aws-cli) that would otherwise be wiped by --delete.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 10:12:10 -05:00

5.1 KiB

action/site-publish

Composite Gitea Action that publishes a static-content website to the fritzlab k8s cluster. Supports static, hugo, and mkdocs. Content goes to a Garage S3 bucket; Traefik fronts the bucket via an ExternalName Service with cert-manager TLS.

Containerized web apps (Dockerfile-based) are NOT handled here. Use the standard image-producer chain instead: action/image-build + action/image-push + action/image-deploy. Hand-author the apps-repo manifests once (Deployment, Service, Ingress, Certificate, kustomization with images: block) and let image-deploy pin the tag on every push. See sjc001/websites/rainsounds.vino.network/ for the canonical example. site-publish errors out explicitly if site.yaml has type: docker.

Convention

Bucket name = repo name = canonical domain. Sibling hostnames (e.g. www., ipv6.) are declared as aliases: in site.yaml — the action registers each as a Garage globalAlias on the bucket and adds it to the Ingress + Certificate on every deploy. Manual edits to manifests in the apps repo are clobbered; edit site.yaml instead.

Usage

Scaffold a new site (handles repo creation + Garage bucket):

./new-site.sh --name my-site.vino.network --domain my-site.vino.network --type static

Or do it manually. site.yaml:

domain: my-site.vino.network
type: static          # static | hugo | mkdocs
# content_dir: html   # subdirectory containing content (default: repo root)
# aliases:            # additional hostnames (each gets a globalAlias on the bucket)
#   - www.my-site.vino.network
# tidy: true          # set false to skip HTML tidy
# enabled: true       # set false to decommission
# excludes:           # paths/patterns to skip during sync (relative to bucket root).
#   - welcome/welcome.pdf
#                     # These are passed verbatim to `aws s3 sync --exclude`,
#                     # so they're both un-uploaded AND un-deleted. Use this
#                     # for large assets managed out-of-band via aws-cli
#                     # (e.g. media files updated more often than the site code).

.gitea/workflows/publish.yaml:

name: Publish
on:
  push:
    branches: [main]
jobs:
  publish:
    runs-on: fritzlab
    steps:
      - uses: actions/checkout@v4
      - uses: https://code.fritzlab.net/action/site-publish@v1
        with:
          token: ${{ secrets.CI_BOT_TOKEN }}
          s3-access-key: ${{ secrets.GARAGE_S3_ACCESS_KEY }}
          s3-secret-key: ${{ secrets.GARAGE_S3_SECRET_KEY }}
          garage-admin-token: ${{ secrets.GARAGE_ADMIN_TOKEN }}

DNS: subdomains of vino.network are covered by the wildcard CNAME to traefik.edge.svc…. For other zones, add an explicit CNAME:

my-site.fritzlab.net  300  IN  CNAME  traefik.edge.svc.k8s.sjc001.fritzlab.net.

Inputs

Input Required Default Description
token yes Gitea token for apps repo push
s3-access-key yes Garage ci-deploy-key access key id
s3-secret-key yes Garage ci-deploy-key secret key
s3-endpoint no http://garage.storage.svc:3900 Garage S3 endpoint
garage-admin-token only if site has aliases Garage admin API token (admin-token from garage-rpc-secret in storage ns)
garage-admin-endpoint no http://garage.storage.svc:3903 Garage admin API endpoint
username no ci-bot Gitea username

Org secrets in websites: CI_BOT_TOKEN, GARAGE_S3_ACCESS_KEY, GARAGE_S3_SECRET_KEY, GARAGE_ADMIN_TOKEN.

Tools

  • new-site.sh — create a new site: Gitea repo, Garage bucket, web hosting enabled.
  • scripts/publish.py decommission <site> — remove a site's manifests from apps repo. Bucket purge is manual.

Architecture

push to websites/<repo>
  → CI runs site-publish action
  → reads site.yaml, builds content (static copy / hugo / mkdocs), runs tidy
  → aws s3 sync → Garage bucket named after the repo
  → admin API: ensures every alias from site.yaml is a globalAlias on the bucket
  → renders manifests in fritzlab/apps from templates: ExternalName Service →
    garage.storage.svc, Traefik Ingress (canonical + aliases), cert-manager
    Certificate (canonical + aliases as SANs), kustomization
  → commits + pushes apps repo only if diff is non-empty
  → ArgoCD syncs → site live with TLS

The Ingress + Certificate are re-rendered on every deploy from site.yaml. There is no "first-deploy vs. update" branching — every deploy is idempotent.

No nginx pods, no per-site Docker images. Garage matches Host: header to bucket name (or any of its globalAliases), so every site shares a single ExternalName target.

History

  • 2026-05-06: removed type: docker support. The single docker site (rainsounds.vino.network) migrated to the image-* chain. site-publish is now scoped strictly to static-content sites.
  • 2026-05-06: renamed from fritzlab/publish-siteaction/site-publish.