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-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.

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.

S
Description
fritzlab composite action: site-publish
Readme
755 KiB
Languages
Python 93.8%
Shell 4%
Jinja 2.2%