Authored-By: OpenAI (GPT-5) <noreply@openai.com>
This commit is contained in:
@@ -1,61 +1,117 @@
|
||||
# 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, Hugo, and MkDocs output to Garage
|
||||
and writes the matching Argo CD, Kubernetes, Traefik, and cert-manager resources
|
||||
to `fritzlab/apps`.
|
||||
|
||||
> **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`.
|
||||
Containerized applications use `action/image-build`, `action/image-push`, and
|
||||
`action/image-deploy`. `type: docker` fails validation.
|
||||
|
||||
## Convention
|
||||
## Delivery contracts
|
||||
|
||||
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.
|
||||
There are two explicit internal modes:
|
||||
|
||||
## Usage
|
||||
- `single-surface-v1` is selected only when `schema`, `artifacts`, and `routes` are all
|
||||
absent. It preserves the original one-repository/one-bucket/one-`/` route
|
||||
behavior, existing action inputs, aliases, middleware, and revalidated cache.
|
||||
- `v2` is selected by `schema: v2`. It requires named artifacts and routes and
|
||||
validates the entire access/storage graph before any upload or manifest write.
|
||||
|
||||
Scaffold a new site (handles repo creation + Garage bucket):
|
||||
Never mix the modes. A partial v2 declaration fails closed.
|
||||
|
||||
```sh
|
||||
./new-site.sh --name my-site.vino.network --domain my-site.vino.network --type static
|
||||
### Multi-surface schema
|
||||
|
||||
This example publishes immutable releases and revalidated channel pointers
|
||||
anonymously while protecting the portal catch-all with Authentik:
|
||||
|
||||
```yaml
|
||||
schema: v2
|
||||
domain: baseline.fritzlab.net
|
||||
type: static
|
||||
content_dir: output
|
||||
|
||||
artifacts:
|
||||
releases:
|
||||
source: dist/releases
|
||||
bucket: baseline-releases
|
||||
credentials:
|
||||
access_key_env: BASELINE_RELEASES_S3_ACCESS_KEY
|
||||
secret_key_env: BASELINE_RELEASES_S3_SECRET_KEY
|
||||
cache: immutable-release
|
||||
channels:
|
||||
source: dist/channels
|
||||
bucket: baseline-channels
|
||||
credentials:
|
||||
access_key_env: BASELINE_CHANNELS_S3_ACCESS_KEY
|
||||
secret_key_env: BASELINE_CHANNELS_S3_SECRET_KEY
|
||||
cache: revalidated-channel
|
||||
portal:
|
||||
source: portal
|
||||
bucket: baseline-portal
|
||||
credentials:
|
||||
access_key_env: BASELINE_PORTAL_S3_ACCESS_KEY
|
||||
secret_key_env: BASELINE_PORTAL_S3_SECRET_KEY
|
||||
cache: private
|
||||
|
||||
routes:
|
||||
- path: /dist/releases
|
||||
artifact: releases
|
||||
access: public
|
||||
- path: /dist/channels
|
||||
artifact: channels
|
||||
access: public
|
||||
- path: /
|
||||
artifact: portal
|
||||
access: authenticated
|
||||
middlewares: [authentik-forwardauth]
|
||||
```
|
||||
|
||||
Or do it manually. `site.yaml`:
|
||||
Artifact `source` is relative to `build/html` after the build phase. Credential
|
||||
fields name inherited environment variables; secret values never belong in
|
||||
`site.yaml`. Callers expose those variables to the composite action through the
|
||||
workflow `env` contract.
|
||||
|
||||
Cache policies are intentionally closed:
|
||||
|
||||
| Policy | Header | Publication behavior |
|
||||
|---|---|---|
|
||||
| `immutable-release` | `public, max-age=31536000, immutable` | write once; identical SHA-256 retry skips; changed key fails |
|
||||
| `revalidated-channel` | `public, max-age=0, must-revalidate` | replace and delete stale keys |
|
||||
| `private` | `private, no-store` | replace and delete stale keys |
|
||||
|
||||
Validation rejects unknown keys or references, unsafe paths, duplicate route
|
||||
prefixes, public `/`, authenticated routes without middleware, private/public
|
||||
cache mismatches, unrouted artifacts, an artifact routed twice, and bucket or
|
||||
publication credential reuse between artifacts. V2 also requires an explicit
|
||||
`/` access policy. Routes render deterministically by
|
||||
longest prefix.
|
||||
|
||||
Each route receives a distinct ExternalName Service and Ingress. The Service
|
||||
targets `<bucket>.web.sjc001.fritzlab.net` with Traefik host forwarding disabled,
|
||||
so Garage selects that route's bucket. Objects are stored under their route
|
||||
prefix, avoiding a path-rewrite middleware. Legacy Services directly target the
|
||||
live `garage-s3.storage.svc.k8s.sjc001.fritzlab.net:80` endpoint. All routes share
|
||||
one Certificate.
|
||||
|
||||
## Legacy usage
|
||||
|
||||
Existing callers remain valid:
|
||||
|
||||
```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).
|
||||
type: static
|
||||
content_dir: html
|
||||
# aliases: [www.my-site.vino.network]
|
||||
# tidy: true
|
||||
# enabled: true
|
||||
# excludes: [welcome/welcome.pdf]
|
||||
# middlewares: [authentik-forwardauth]
|
||||
```
|
||||
|
||||
`.gitea/workflows/publish.yaml`:
|
||||
Legacy bucket name is the repository name. Aliases are reconciled as Garage
|
||||
global aliases and require `garage-admin-token`; reconciliation errors stop the
|
||||
deploy. Disabling a site removes generated Apps manifests but never deletes its
|
||||
bucket or objects.
|
||||
|
||||
```yaml
|
||||
name: Publish
|
||||
@@ -75,58 +131,33 @@ jobs:
|
||||
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 |
|
||||
| `token` | yes | | Gitea token used for the Apps repository |
|
||||
| `s3-access-key` | single-surface-v1 | | Legacy Garage access key |
|
||||
| `s3-secret-key` | single-surface-v1 | | Legacy Garage secret key |
|
||||
| `s3-endpoint` | no | `http://garage-s3.storage.svc:3900` | Garage S3 API |
|
||||
| `garage-admin-token` | with legacy aliases | | Garage admin token |
|
||||
| `garage-admin-endpoint` | no | `http://garage.storage.svc:3903` | Garage admin API |
|
||||
| `username` | no | `ci-bot` | Gitea username |
|
||||
|
||||
Org secrets in `websites`: `CI_BOT_TOKEN`, `GARAGE_S3_ACCESS_KEY`,
|
||||
`GARAGE_S3_SECRET_KEY`, `GARAGE_ADMIN_TOKEN`.
|
||||
Git HTTPS authentication is supplied to Git through a short-lived inherited
|
||||
file descriptor. The token isn't placed in argv, command output, or a clone URL.
|
||||
|
||||
## Tools
|
||||
## Generated topology
|
||||
|
||||
- **`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
|
||||
```text
|
||||
push
|
||||
-> build/html
|
||||
-> validate complete site.yaml graph
|
||||
-> publish each artifact with its cache/write policy and credential pair
|
||||
-> one Certificate
|
||||
-> N bucket-specific Services + longest-prefix Ingresses
|
||||
-> commit changed manifests to fritzlab/apps
|
||||
-> Argo CD reconciliation
|
||||
```
|
||||
|
||||
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`.
|
||||
Manual changes to generated Apps resources are overwritten. `new-site.sh`
|
||||
continues to scaffold legacy one-surface sites.
|
||||
|
||||
Reference in New Issue
Block a user