Authored-By: OpenAI (GPT-5) <noreply@openai.com>
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
# 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 one or more **static-content** artifacts
|
||||
to one hostname. Each split surface owns its build input, Garage bucket,
|
||||
publication credential environment variables, cache rules, Service, Ingress,
|
||||
route prefix, and access middleware. The hostname shares one Certificate.
|
||||
`static`, `hugo`, and `mkdocs` builds are supported; a prebuilt Docusaurus
|
||||
output is a `static` artifact.
|
||||
|
||||
> **Containerized web apps (Dockerfile-based) are NOT handled here.** Use the
|
||||
> standard image-producer chain instead:
|
||||
@@ -16,9 +18,10 @@ Service with cert-manager TLS.
|
||||
> for the canonical example. site-publish errors out explicitly if
|
||||
> `site.yaml` has `type: docker`.
|
||||
|
||||
## Convention
|
||||
## Single-surface compatibility
|
||||
|
||||
Bucket name = repo name = canonical domain. Sibling hostnames (e.g. `www.`,
|
||||
Existing `site.yaml` files remain the `single-surface-v1` compatibility
|
||||
contract. 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;
|
||||
@@ -32,6 +35,113 @@ Scaffold a new site (handles repo creation + Garage bucket):
|
||||
./new-site.sh --name my-site.vino.network --domain my-site.vino.network --type static
|
||||
```
|
||||
|
||||
The compatibility path still writes `build/html`, uploads with
|
||||
`AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY`, and renders `service.yaml` plus
|
||||
`ingress.yaml`. Its only behavior change is the required root-cause repair:
|
||||
the website Service now targets the data-only `garage-s3` Service.
|
||||
|
||||
## Split-surface contract
|
||||
|
||||
Use `artifacts` and `routes` together. This example expresses an authenticated
|
||||
prebuilt portal at `/` and public bundles at `/dist`; it is illustrative and
|
||||
the schema has no Baseline-specific field.
|
||||
|
||||
```yaml
|
||||
domain: baseline.fritzlab.net
|
||||
artifacts:
|
||||
- name: distributions
|
||||
type: static
|
||||
content_dir: dist
|
||||
publish:
|
||||
bucket: baseline-dist
|
||||
credentials:
|
||||
access_key_env: DIST_S3_ACCESS_KEY
|
||||
secret_key_env: DIST_S3_SECRET_KEY
|
||||
cache:
|
||||
rules:
|
||||
- path: /
|
||||
cache_control: public, max-age=0, must-revalidate
|
||||
- path: releases
|
||||
cache_control: public, max-age=31536000, immutable
|
||||
- path: channels
|
||||
cache_control: public, max-age=0, must-revalidate
|
||||
- name: portal
|
||||
type: static
|
||||
content_dir: portal/build
|
||||
publish:
|
||||
bucket: baseline-portal
|
||||
credentials:
|
||||
access_key_env: PORTAL_S3_ACCESS_KEY
|
||||
secret_key_env: PORTAL_S3_SECRET_KEY
|
||||
cache:
|
||||
rules:
|
||||
- path: /
|
||||
cache_control: private, no-store
|
||||
routes:
|
||||
- name: distributions
|
||||
path: /dist
|
||||
artifact: distributions
|
||||
access:
|
||||
mode: public
|
||||
- name: portal
|
||||
path: /
|
||||
artifact: portal
|
||||
access:
|
||||
mode: protected
|
||||
middleware: authentik-forwardauth
|
||||
```
|
||||
|
||||
The caller supplies each declared credential name as an environment variable
|
||||
on the action step. Credential values are passed to `aws` only through its
|
||||
environment and never appear in a logged command or process argument.
|
||||
|
||||
```yaml
|
||||
- uses: https://code.fritzlab.net/action/site-publish@v1
|
||||
with:
|
||||
token: ${{ secrets.CI_BOT_TOKEN }}
|
||||
env:
|
||||
DIST_S3_ACCESS_KEY: ${{ secrets.DIST_S3_ACCESS_KEY }}
|
||||
DIST_S3_SECRET_KEY: ${{ secrets.DIST_S3_SECRET_KEY }}
|
||||
PORTAL_S3_ACCESS_KEY: ${{ secrets.PORTAL_S3_ACCESS_KEY }}
|
||||
PORTAL_S3_SECRET_KEY: ${{ secrets.PORTAL_S3_SECRET_KEY }}
|
||||
```
|
||||
|
||||
Routes are normalized and rendered longest-prefix first. Split mode requires
|
||||
one `/` catch-all so unmatched paths have an explicit access policy. If any
|
||||
route is protected, that catch-all must also be protected. Every artifact must
|
||||
belong to exactly one route and bucket; protected and public routes cannot
|
||||
reuse a bucket. A protected route requires an existing file-provider access
|
||||
middleware. Public routes cannot declare one.
|
||||
|
||||
Every cache policy requires a `/` default. More-specific cache paths override
|
||||
it, are reapplied in deterministic prefix order, and must exist in the built
|
||||
artifact. Contradictory directives (`public` plus `private`, `immutable` plus
|
||||
revalidation, or `no-store` plus a positive max-age) are rejected. Protected
|
||||
artifacts require `private` or `no-store` and cannot emit `public`.
|
||||
Metadata restamping transfers each artifact once even on a no-op publication;
|
||||
that is the cost of making policy changes effective on unchanged Garage objects.
|
||||
An immutable cache path is excluded from sync and deletion. Every object key in
|
||||
that path must contain exactly one full publication SHA-256, calculated over its
|
||||
cache policy, content type, and bytes. That content address makes concurrent
|
||||
writes identical even though Garage v2.2.0 has no conditional destination
|
||||
write. An identical retry converges; a changed object, missing digest metadata,
|
||||
wrong address, or nested policy under that immutable prefix fails publication.
|
||||
|
||||
Each split route gets a bucket-specific `<bucket>.web.sjc001.fritzlab.net`
|
||||
ExternalName Service annotated to disable pass-host-header and a separate Ingress. Route
|
||||
Ingresses share the hostname's certificate Secret. The access middleware and
|
||||
Garage bucket/key must already exist; the publisher doesn't create identity
|
||||
providers or credentials.
|
||||
|
||||
### Migrating a site
|
||||
|
||||
Leave an existing single-surface file unchanged until a real second surface
|
||||
exists. Then build every artifact before this action, move the old fields into
|
||||
an artifact, declare a route for every artifact, give each bucket a separately
|
||||
scoped key, and set the route access/cache contract. Run the repository tests
|
||||
and inspect generated Apps changes. Removing a route removes its generated
|
||||
Service and Ingress on the next render; bucket deletion remains manual.
|
||||
|
||||
Or do it manually. `site.yaml`:
|
||||
|
||||
```yaml
|
||||
@@ -75,11 +185,11 @@ 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:
|
||||
DNS: subdomains of `vino.network` are covered by the wildcard CNAME to the
|
||||
public gateway. For other zones, add an explicit CNAME:
|
||||
|
||||
```
|
||||
my-site.fritzlab.net 300 IN CNAME traefik.edge.svc.k8s.sjc001.fritzlab.net.
|
||||
my-site.fritzlab.net 300 IN CNAME gateway.sjc001.fritzlab.net.
|
||||
```
|
||||
|
||||
## Inputs
|
||||
@@ -87,10 +197,10 @@ 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 |
|
||||
| `garage-admin-token` | only if site has `aliases` | | Garage admin API token (`admin-token` from `garage-rpc-secret` in `storage` ns) |
|
||||
| `s3-access-key` | legacy only | | Garage access key id for single-surface sites |
|
||||
| `s3-secret-key` | legacy only | | Garage secret key for single-surface sites |
|
||||
| `s3-endpoint` | no | `http://garage-s3.storage.svc:3900` | Legacy Garage S3 endpoint |
|
||||
| `garage-admin-token` | legacy aliases only | | 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 +217,11 @@ 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
|
||||
→ 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
|
||||
→ reads and validates all of site.yaml before publication
|
||||
→ independently builds each static / Hugo / MkDocs artifact
|
||||
→ syncs each artifact to its route-owned Garage bucket and prefix
|
||||
→ reapplies the artifact's default and longest-prefix cache headers
|
||||
→ renders one Service + Ingress per route and one shared Certificate
|
||||
→ commits + pushes apps repo only if diff is non-empty
|
||||
→ ArgoCD syncs → site live with TLS
|
||||
```
|
||||
@@ -120,9 +229,10 @@ push to websites/<repo>
|
||||
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.
|
||||
No nginx pods, no per-site Docker images. Compatibility sites pass the public
|
||||
host to the shared data-only Garage website Service. Split routes disable host
|
||||
passing on their Service so Garage receives that artifact's bucket-specific
|
||||
website authority.
|
||||
|
||||
## History
|
||||
|
||||
|
||||
Reference in New Issue
Block a user