Authored-By: OpenAI (GPT-5) <noreply@openai.com>
5.5 KiB
action/site-publish
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 applications use action/image-build, action/image-push, and
action/image-deploy. type: docker fails validation.
Delivery contracts
There are two explicit internal modes:
single-surface-v1is selected only whenschema,artifacts, androutesare all absent. It preserves the original one-repository/one-bucket/one-/route behavior, existing action inputs, aliases, middleware, and revalidated cache.v2is selected byschema: v2. It requires named artifacts and routes and validates the entire access/storage graph before any upload or manifest write.
Never mix the modes. A partial v2 declaration fails closed.
Multi-surface schema
This example publishes immutable releases and revalidated channel pointers anonymously while protecting the portal catch-all with Authentik:
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]
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:
domain: my-site.vino.network
type: static
content_dir: html
# aliases: [www.my-site.vino.network]
# tidy: true
# enabled: true
# excludes: [welcome/welcome.pdf]
# middlewares: [authentik-forwardauth]
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.
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 }}
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
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 |
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.
Generated topology
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
Manual changes to generated Apps resources are overwritten. new-site.sh
continues to scaffold legacy one-surface sites.