feat: publish isolated site artifacts
Test / test (pull_request) Successful in 5s

This commit is contained in:
Evelyn Chen
2026-08-29 21:28:28 +00:00
parent f1f780f5a3
commit 7aca96e320
10 changed files with 1032 additions and 109 deletions
+77 -12
View File
@@ -1,9 +1,10 @@
# 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.
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:
@@ -24,6 +25,11 @@ 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):
@@ -55,6 +61,65 @@ type: static # static | hugo | mkdocs
# # (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
@@ -87,9 +152,9 @@ my-site.fritzlab.net 300 IN CNAME traefik.edge.svc.k8s.sjc001.fritzlab.net.
| 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 |
| `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 |
@@ -107,12 +172,12 @@ Org secrets in `websites`: `CI_BOT_TOKEN`, `GARAGE_S3_ACCESS_KEY`,
```
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
→ 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 Service →
garage.storage.svc, Traefik Ingress (canonical + aliases), cert-manager
Certificate (canonical + aliases as SANs), kustomization
→ 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
```