198 lines
8.1 KiB
Markdown
198 lines
8.1 KiB
Markdown
# action/site-publish
|
|
|
|
Composite Gitea Action that publishes static content to the fritzlab cluster.
|
|
The legacy form supports one `static`, `hugo`, or `mkdocs` output. The
|
|
multi-artifact form publishes caller-built outputs to separate Garage buckets
|
|
and gives each URL path its own cache, CORS, credential, and middleware
|
|
boundary. Traefik fronts the buckets and cert-manager owns TLS.
|
|
|
|
> **Containerized web apps (Dockerfile-based) are NOT handled here.** Use the
|
|
> standard image-producer chain instead:
|
|
> [`action/image-build`](https://code.fritzlab.net/action/image-build) +
|
|
> [`action/image-push`](https://code.fritzlab.net/action/image-push) +
|
|
> [`action/image-deploy`](https://code.fritzlab.net/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.
|
|
|
|
New sites with more than one security or caching boundary use the
|
|
multi-artifact form below. An artifact is storage and release metadata. A
|
|
route is edge behavior. Keeping them separate prevents a public path from
|
|
inheriting the authenticated catalogue's cache or credentials.
|
|
|
|
## Usage
|
|
|
|
Scaffold a new site (handles repo creation + Garage bucket):
|
|
|
|
```sh
|
|
./new-site.sh --name my-site.vino.network --domain my-site.vino.network --type static
|
|
```
|
|
|
|
Or do it manually. `site.yaml`:
|
|
|
|
```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).
|
|
# middlewares: # extra Traefik FILE-PROVIDER middleware names appended to the
|
|
# - authentik-forwardauth # Ingress annotation (after https-redirect,retry-upstream).
|
|
# # The middleware must already exist in the traefik-dynamic
|
|
# # ConfigMap. Use authentik-forwardauth to auth-gate a site
|
|
# # (also requires an Authentik proxy provider + app for the host).
|
|
```
|
|
|
|
### Multiple artifacts on one host
|
|
|
|
Build each output before invoking the action, then declare the materialized
|
|
directories and their routes:
|
|
|
|
```yaml
|
|
domain: baseline.fritzlab.net
|
|
artifacts:
|
|
catalogue:
|
|
source: apps/catalogue/build
|
|
bucket: baseline-catalogue
|
|
credential: catalogue
|
|
cache:
|
|
default: private, no-store
|
|
dist:
|
|
source: dist
|
|
bucket: baseline-dist
|
|
credential: dist
|
|
cache:
|
|
default: public, max-age=0, must-revalidate, no-transform
|
|
rules:
|
|
- match: releases/*
|
|
value: public, max-age=31536000, immutable, no-transform
|
|
cors_origins: ["*"]
|
|
routes:
|
|
- name: catalogue
|
|
path: /
|
|
artifact: catalogue
|
|
access: protected
|
|
- name: dist
|
|
path: /dist
|
|
artifact: dist
|
|
access: public
|
|
```
|
|
|
|
`source` is a repository-relative directory and cannot be the repository
|
|
root. Every artifact needs an explicit, unique bucket and at least one route.
|
|
Routes use Kubernetes `Prefix` matching. The route path becomes part of the
|
|
published object key: `dist/baseline.css` is served at `/dist/baseline.css`.
|
|
|
|
`access` is required. Protected routes receive the organization Authentik
|
|
middleware automatically and their artifacts must use `private` or `no-store`
|
|
cache metadata. Public and protected routes cannot share an artifact. Cache
|
|
rules use aws-cli include patterns in order after the default metadata pass;
|
|
use immutable caching only for content-addressed or version-pinned paths.
|
|
|
|
The `default` credential profile uses the existing `AWS_ACCESS_KEY_ID` and
|
|
`AWS_SECRET_ACCESS_KEY` inputs. A named profile such as `catalogue` reads
|
|
`SITE_PUBLISH_CATALOGUE_S3_ACCESS_KEY_ID` and
|
|
`SITE_PUBLISH_CATALOGUE_S3_SECRET_ACCESS_KEY` from the caller environment.
|
|
Use a different Garage key for every security boundary. The action never logs
|
|
credential values and clones the Apps repository without credentials in the
|
|
remote URL.
|
|
|
|
Each artifact Service resolves through Garage's bucket virtual host and sets
|
|
Traefik `passHostHeader` to false. The upstream therefore receives the bucket
|
|
host while the browser retains the public host. This lets one DNS host address
|
|
isolated Garage buckets without shared proxy configuration or CRDs.
|
|
|
|
`.gitea/workflows/publish.yaml`:
|
|
|
|
```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` | only for the `default` profile | | Garage access key id |
|
|
| `s3-secret-key` | only for the `default` profile | | Garage secret key |
|
|
| `s3-endpoint` | no | `http://garage-s3.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 and either builds legacy content or snapshots caller-built artifacts
|
|
→ aws s3 sync → one Garage bucket per artifact, with isolated credentials and metadata
|
|
→ admin API: ensures every alias from site.yaml is a globalAlias on the bucket
|
|
→ renders manifests in fritzlab/apps from templates: ExternalName Services →
|
|
Garage bucket virtual hosts, path-scoped Traefik Ingresses,
|
|
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-site` → `action/site-publish`.
|