strip docker type — site-publish is static-content only

Removes type: docker handling from action.yaml, scripts (build/deploy/utils/setup),
and templates (deployment.yaml.j2, service-docker.yaml.j2). Renamed
service-static.yaml.j2 -> service.yaml.j2.

If site.yaml has type: docker, parse_site_yaml() now dies with a clear message
pointing to action/image-build + action/image-push + action/image-deploy with
hand-authored apps-repo manifests. rainsounds.vino.network was the only docker
consumer and has already migrated.

Drops registry-password input from action.yaml (no longer needed).
This commit is contained in:
Donavan Fritz
2026-05-06 10:01:09 -05:00
parent e53776af5e
commit 8cc34552c6
13 changed files with 69 additions and 250 deletions
+25 -11
View File
@@ -1,12 +1,20 @@
# action/site-publish # action/site-publish
Composite Gitea Action that publishes a website to the fritzlab k8s cluster. Composite Gitea Action that publishes a **static-content** website to the
Supports `static`, `hugo`, `mkdocs` (content → Garage S3 + ExternalName Service) fritzlab k8s cluster. Supports `static`, `hugo`, and `mkdocs`. Content goes
and `docker` (Dockerfile → Deployment + headless Service). Handles manifest to a Garage S3 bucket; Traefik fronts the bucket via an `ExternalName`
rendering, TLS via cert-manager, and Garage bucket aliases. Service with cert-manager TLS.
Renamed from `fritzlab/publish-site``action/site-publish` as part of the > **Containerized web apps (Dockerfile-based) are NOT handled here.** Use the
2026 action-org consolidation. > 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`.
## Convention ## Convention
@@ -28,7 +36,7 @@ Or do it manually. `site.yaml`:
```yaml ```yaml
domain: my-site.vino.network domain: my-site.vino.network
type: static # static | hugo | mkdocs | docker type: static # static | hugo | mkdocs
# content_dir: html # subdirectory containing content (default: repo root) # content_dir: html # subdirectory containing content (default: repo root)
# aliases: # additional hostnames (each gets a globalAlias on the bucket) # aliases: # additional hostnames (each gets a globalAlias on the bucket)
# - www.my-site.vino.network # - www.my-site.vino.network
@@ -68,12 +76,11 @@ my-site.fritzlab.net 300 IN CNAME traefik.edge.svc.k8s.sjc001.fritzlab.net.
| Input | Required | Default | Description | | Input | Required | Default | Description |
|---|---|---|---| |---|---|---|---|
| `token` | yes | | Gitea token for apps repo push | | `token` | yes | | Gitea token for apps repo push |
| `s3-access-key` | static/hugo/mkdocs | | Garage `ci-deploy-key` access key id | | `s3-access-key` | yes | | Garage `ci-deploy-key` access key id |
| `s3-secret-key` | static/hugo/mkdocs | | Garage `ci-deploy-key` secret key | | `s3-secret-key` | yes | | Garage `ci-deploy-key` secret key |
| `s3-endpoint` | no | `http://garage.storage.svc:3900` | Garage S3 endpoint | | `s3-endpoint` | no | `http://garage.storage.svc:3900` | Garage S3 endpoint |
| `garage-admin-token` | static/hugo/mkdocs (only if site has `aliases`) | | Garage admin API token (`admin-token` from `garage-rpc-secret` in `storage` ns) | | `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 | | `garage-admin-endpoint` | no | `http://garage.storage.svc:3903` | Garage admin API endpoint |
| `registry-password` | docker | inputs.token | Container registry password |
| `username` | no | `ci-bot` | Gitea username | | `username` | no | `ci-bot` | Gitea username |
Org secrets in `websites`: `CI_BOT_TOKEN`, `GARAGE_S3_ACCESS_KEY`, Org secrets in `websites`: `CI_BOT_TOKEN`, `GARAGE_S3_ACCESS_KEY`,
@@ -105,3 +112,10 @@ 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 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 bucket name (or any of its globalAliases), so every site shares a single
ExternalName target. 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`.
+6 -10
View File
@@ -1,24 +1,21 @@
name: Publish Site name: Publish Site
description: Build and deploy a site (static, hugo, mkdocs, or docker) to fritzlab k8s. description: Build and deploy a static-content site (static, hugo, mkdocs) to Garage S3 with Traefik + cert-manager. Containerized apps should use action/image-build + action/image-push + action/image-deploy.
inputs: inputs:
token: token:
description: Gitea token (ci-bot) for apps repo push and API operations description: Gitea token (ci-bot) for apps repo push and API operations
required: true required: true
s3-access-key: s3-access-key:
description: Garage ci-deploy-key access key id (required for static/hugo/mkdocs) description: Garage ci-deploy-key access key id
required: false required: true
s3-secret-key: s3-secret-key:
description: Garage ci-deploy-key secret access key (required for static/hugo/mkdocs) description: Garage ci-deploy-key secret access key
required: false required: true
registry-password:
description: Container registry password (required for docker type; defaults to token)
required: false
s3-endpoint: s3-endpoint:
description: Garage S3 endpoint URL description: Garage S3 endpoint URL
required: false required: false
default: http://garage.storage.svc:3900 default: http://garage.storage.svc:3900
garage-admin-token: garage-admin-token:
description: Garage admin API token (required for static/hugo/mkdocs to reconcile bucket aliases) description: Garage admin API token (required only when site.yaml has aliases — used to reconcile bucket globalAliases)
required: false required: false
garage-admin-endpoint: garage-admin-endpoint:
description: Garage admin API endpoint URL description: Garage admin API endpoint URL
@@ -43,7 +40,6 @@ runs:
SITE_DIR: ${{ github.workspace }} SITE_DIR: ${{ github.workspace }}
ACTION_DIR: ${{ github.action_path }} ACTION_DIR: ${{ github.action_path }}
GITHUB_RUN_NUMBER: ${{ github.run_number }} GITHUB_RUN_NUMBER: ${{ github.run_number }}
REGISTRY_PASSWORD: ${{ inputs.registry-password || inputs.token }}
CI_BOT_USER: ${{ inputs.username }} CI_BOT_USER: ${{ inputs.username }}
- name: Deploy - name: Deploy
Binary file not shown.
Binary file not shown.
Binary file not shown.
+2 -35
View File
@@ -1,12 +1,11 @@
"""Build phase — content prep (static) or docker image build.""" """Build phase — content prep for static-content sites."""
import os
import shutil import shutil
import subprocess import subprocess
import tempfile import tempfile
from pathlib import Path from pathlib import Path
from utils import EXCLUDE_FILES, die, env, parse_site_yaml, run from utils import EXCLUDE_FILES, env, parse_site_yaml, run
def build_static(site_dir, cfg): def build_static(site_dir, cfg):
@@ -55,35 +54,6 @@ def build_static(site_dir, cfg):
print(f"Build complete — content at {html_dir}") print(f"Build complete — content at {html_dir}")
def docker_login():
password = os.environ.get("REGISTRY_PASSWORD", "")
user = env("CI_BOT_USER", "ci-bot")
if not password:
die("REGISTRY_PASSWORD is required for docker builds")
subprocess.run(
f"echo '{password}' | docker login code.fritzlab.net -u {user} --password-stdin",
shell=True, check=True,
)
def build_docker(site_dir, cfg):
docker_login()
image = cfg["image"]
run_number = env("GITHUB_RUN_NUMBER", "0")
tags = [f"{image}:latest", f"{image}:{run_number}"]
cmd_parts = ["docker", "build"]
for tag in tags:
cmd_parts += ["-t", tag]
cmd_parts += ["--network", "host", "--provenance=false"]
for key, val in cfg.get("build_args", {}).items():
cmd_parts += ["--build-arg", f"{key}={val}"]
cmd_parts.append(str(site_dir))
run(" ".join(cmd_parts))
print(f"Docker build complete — tags: {', '.join(tags)}")
def cmd_build(): def cmd_build():
site_dir = Path(env("SITE_DIR")) site_dir = Path(env("SITE_DIR"))
cfg = parse_site_yaml(site_dir) cfg = parse_site_yaml(site_dir)
@@ -92,7 +62,4 @@ def cmd_build():
print("Site disabled — skipping build") print("Site disabled — skipping build")
return return
if cfg["type"] == "docker":
build_docker(site_dir, cfg)
else:
build_static(site_dir, cfg) build_static(site_dir, cfg)
+3 -89
View File
@@ -1,9 +1,8 @@
"""Deploy phase — S3 sync or docker push, manifest rendering, alias reconcile.""" """Deploy phase — S3 sync, manifest rendering, alias reconcile."""
import json import json
import os import os
import shutil import shutil
import subprocess
import tempfile import tempfile
from pathlib import Path from pathlib import Path
from urllib.error import HTTPError, URLError from urllib.error import HTTPError, URLError
@@ -108,62 +107,7 @@ def ensure_bucket_aliases(site_name, aliases, admin_token):
raise raise
def docker_push(cfg): def render_site_manifests(site_name, action_dir, app_dir, manifests_dir, cfg):
image = cfg["image"]
run_number = env("GITHUB_RUN_NUMBER", "0")
for tag in [f"{image}:latest", f"{image}:{run_number}"]:
run(f"docker push {tag}")
def docker_tag_cleanup(cfg, token):
"""Keep the 3 newest numeric tags, delete the rest via Gitea API."""
image = cfg["image"]
parts = image.split("/")
if len(parts) < 3:
print(f"Cannot parse image for tag cleanup: {image}")
return
org = parts[1]
pkg_name = parts[2]
url = f"https://{GITEA_HOST}/api/v1/packages/{org}?type=container"
req = Request(url, headers={"Authorization": f"token {token}"})
try:
with urlopen(req) as resp:
packages = json.loads(resp.read())
except Exception as e:
print(f"Warning: failed to list packages for cleanup: {e}")
return
numeric_versions = []
for pkg in packages:
if pkg.get("name") == pkg_name:
ver = pkg.get("version", "")
if ver.isdigit():
numeric_versions.append(int(ver))
numeric_versions.sort()
to_delete = numeric_versions[:-3] if len(numeric_versions) > 3 else []
for ver in to_delete:
del_url = f"https://{GITEA_HOST}/api/v1/packages/{org}/container/{pkg_name}/{ver}"
print(f" Deleting {pkg_name}:{ver}")
req = Request(del_url, method="DELETE", headers={"Authorization": f"token {token}"})
try:
urlopen(req)
except Exception as e:
print(f" Warning: failed to delete {pkg_name}:{ver}: {e}")
if to_delete:
print(f" Cleaned up {len(to_delete)} old tags")
else:
print(" No old tags to clean up")
def docker_cleanup():
run("docker system prune --all --force")
def render_site_manifests(site_name, action_dir, app_dir, manifests_dir, cfg, extra=None):
"""Always re-render manifests from current site.yaml. Templates own """Always re-render manifests from current site.yaml. Templates own
domain + aliases, so changes propagate without manual edits.""" domain + aliases, so changes propagate without manual edits."""
manifests_dir.mkdir(parents=True, exist_ok=True) manifests_dir.mkdir(parents=True, exist_ok=True)
@@ -173,11 +117,8 @@ def render_site_manifests(site_name, action_dir, app_dir, manifests_dir, cfg, ex
"domain": cfg["domain"], "domain": cfg["domain"],
"aliases": cfg["aliases"], "aliases": cfg["aliases"],
"namespace": NAMESPACE, "namespace": NAMESPACE,
"site_type": cfg["type"],
} }
if extra: render_templates(action_dir, template_vars, app_dir, manifests_dir)
template_vars.update(extra)
render_templates(action_dir, template_vars, app_dir, manifests_dir, cfg["type"])
def deploy_static(site_name, site_dir, action_dir, token, cfg): def deploy_static(site_name, site_dir, action_dir, token, cfg):
@@ -193,30 +134,6 @@ def deploy_static(site_name, site_dir, action_dir, token, cfg):
commit_and_push(apps_dir, f"Deploy {site_name}") commit_and_push(apps_dir, f"Deploy {site_name}")
def deploy_docker(site_name, site_dir, action_dir, token, cfg):
docker_push(cfg)
run_number = env("GITHUB_RUN_NUMBER", "0")
image = cfg["image"]
apps_dir = clone_apps(token)
app_dir = apps_dir / "sjc001" / "websites" / site_name
manifests_dir = app_dir / "manifests"
render_site_manifests(site_name, action_dir, app_dir, manifests_dir, cfg, extra={
"image": image,
"port": cfg["port"],
"health_path": cfg["health_path"],
"replicas": cfg["replicas"],
})
# Pin to this build's tag (kustomize edit appends an `images:` override).
run(f"cd {manifests_dir} && kustomize edit set image {image}={image}:{run_number}")
commit_and_push(apps_dir, f"Deploy {site_name} #{run_number}")
docker_tag_cleanup(cfg, token)
docker_cleanup()
def decommission(site_name, token): def decommission(site_name, token):
"""Remove manifests from apps repo.""" """Remove manifests from apps repo."""
user = env("CI_BOT_USER", "ci-bot") user = env("CI_BOT_USER", "ci-bot")
@@ -249,7 +166,4 @@ def cmd_deploy():
decommission(site_name, token) decommission(site_name, token)
return return
if cfg["type"] == "docker":
deploy_docker(site_name, site_dir, action_dir, token, cfg)
else:
deploy_static(site_name, site_dir, action_dir, token, cfg) deploy_static(site_name, site_dir, action_dir, token, cfg)
-8
View File
@@ -24,13 +24,6 @@ def ensure_aws():
subprocess.run(["aws", "--version"], check=True) subprocess.run(["aws", "--version"], check=True)
def ensure_docker():
if shutil.which("docker"):
subprocess.run(["docker", "version", "--format", "{{.Client.Version}}"], check=True)
return
print("WARNING: docker CLI not found — docker builds will fail", file=sys.stderr)
def ensure_jinja2(): def ensure_jinja2():
try: try:
import jinja2 import jinja2
@@ -45,5 +38,4 @@ def ensure_jinja2():
if __name__ == "__main__": if __name__ == "__main__":
ensure_jinja2() ensure_jinja2()
ensure_aws() ensure_aws()
ensure_docker()
print("Setup complete") print("Setup complete")
+30 -26
View File
@@ -1,4 +1,4 @@
"""Shared utilities for the publish-site action.""" """Shared utilities for the site-publish action."""
import os import os
import shutil import shutil
@@ -20,7 +20,26 @@ EXCLUDE_FILES = {
"Dockerfile", ".dockerignore", "go.mod", "go.sum", "Dockerfile", ".dockerignore", "go.mod", "go.sum",
} }
VALID_TYPES = {"static", "hugo", "mkdocs", "docker"} VALID_TYPES = {"static", "hugo", "mkdocs"}
DOCKER_DEPRECATION_MSG = """\
type: docker is no longer supported by action/site-publish.
site-publish handles only static-content sites (static, hugo, mkdocs)
that ship to Garage S3. For containerized web apps, use the standard
image-producer chain:
- uses: action/image-build@v1 # build + smoke-test
- uses: action/image-push@v1 # push + prune
- uses: action/image-deploy@v1 # apps repo image-pin
Hand-author your apps-repo manifests once (Deployment, Service, Ingress,
Certificate, kustomization with images: block) under
sjc001/websites/<repo>/manifests/. image-deploy will pin the tag on
every CI run. See action/image-deploy README and
sjc001/websites/rainsounds.vino.network/manifests/ for the canonical
example.\
"""
def k8s_name(name): def k8s_name(name):
@@ -57,6 +76,10 @@ def parse_site_yaml(site_dir):
die("domain is required in site.yaml") die("domain is required in site.yaml")
site_type = cfg.get("type", "static") site_type = cfg.get("type", "static")
if site_type == "docker":
die(DOCKER_DEPRECATION_MSG)
if site_type not in VALID_TYPES: if site_type not in VALID_TYPES:
die(f"Unknown site type: {site_type} (valid: {', '.join(sorted(VALID_TYPES))})") die(f"Unknown site type: {site_type} (valid: {', '.join(sorted(VALID_TYPES))})")
@@ -65,20 +88,10 @@ def parse_site_yaml(site_dir):
"type": site_type, "type": site_type,
"enabled": cfg.get("enabled", True), "enabled": cfg.get("enabled", True),
"aliases": cfg.get("aliases") or [], "aliases": cfg.get("aliases") or [],
"content_dir": cfg.get("content_dir", ""),
"tidy": cfg.get("tidy", True),
} }
if site_type == "docker":
if not cfg.get("image"):
die("image is required in site.yaml for type: docker")
site["image"] = cfg["image"]
site["port"] = cfg.get("port", 8080)
site["build_args"] = cfg.get("build_args") or {}
site["health_path"] = cfg.get("health_path", "/healthz")
site["replicas"] = cfg.get("replicas", 1)
else:
site["content_dir"] = cfg.get("content_dir", "")
site["tidy"] = cfg.get("tidy", True)
print("Site config:") print("Site config:")
for k, v in site.items(): for k, v in site.items():
print(f" {k}: {v}") print(f" {k}: {v}")
@@ -96,30 +109,21 @@ def clone_apps(token):
return apps_dir return apps_dir
def render_templates(action_dir, template_vars, app_dir, manifests_dir, site_type): def render_templates(action_dir, template_vars, app_dir, manifests_dir):
"""Render Jinja2 templates, selecting the right set for the site type.""" """Render Jinja2 templates for a static-content site."""
templates_dir = Path(action_dir) / "templates" templates_dir = Path(action_dir) / "templates"
jinja_env = Environment( jinja_env = Environment(
loader=FileSystemLoader(str(templates_dir)), loader=FileSystemLoader(str(templates_dir)),
keep_trailing_newline=True, keep_trailing_newline=True,
) )
if site_type == "docker":
service_tmpl = "service-docker.yaml.j2"
tmpl_names = ["app.yaml.j2", "certificate.yaml.j2", "ingress.yaml.j2", tmpl_names = ["app.yaml.j2", "certificate.yaml.j2", "ingress.yaml.j2",
"kustomization.yaml.j2", service_tmpl, "deployment.yaml.j2"] "kustomization.yaml.j2", "service.yaml.j2"]
else:
service_tmpl = "service-static.yaml.j2"
tmpl_names = ["app.yaml.j2", "certificate.yaml.j2", "ingress.yaml.j2",
"kustomization.yaml.j2", service_tmpl]
for tmpl_name in tmpl_names: for tmpl_name in tmpl_names:
tmpl = jinja_env.get_template(tmpl_name) tmpl = jinja_env.get_template(tmpl_name)
rendered = tmpl.render(**template_vars) rendered = tmpl.render(**template_vars)
out_name = tmpl_name.replace(".j2", "") out_name = tmpl_name.replace(".j2", "")
# service-docker.yaml / service-static.yaml → service.yaml
if out_name.startswith("service-"):
out_name = "service.yaml"
dest = app_dir / out_name if tmpl_name == "app.yaml.j2" else manifests_dir / out_name dest = app_dir / out_name if tmpl_name == "app.yaml.j2" else manifests_dir / out_name
dest.write_text(rendered) dest.write_text(rendered)
print(f" Rendered {tmpl_name} -> {dest}") print(f" Rendered {tmpl_name} -> {dest}")
-52
View File
@@ -1,52 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ site_k8s }}
namespace: {{ namespace }}
labels:
app: {{ site_k8s }}
spec:
replicas: {{ replicas }}
strategy:
type: Recreate
selector:
matchLabels:
app: {{ site_k8s }}
template:
metadata:
labels:
app: {{ site_k8s }}
spec:
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
fsGroup: 65532
seccompProfile:
type: RuntimeDefault
containers:
- name: {{ site_k8s }}
image: {{ image }}:latest
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: {{ port }}
resources:
requests:
memory: 64Mi
cpu: 50m
limits:
memory: 256Mi
livenessProbe:
httpGet: {path: {{ health_path }}, port: http}
periodSeconds: 30
timeoutSeconds: 3
readinessProbe:
httpGet: {path: {{ health_path }}, port: http}
periodSeconds: 10
timeoutSeconds: 3
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
-3
View File
@@ -1,9 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1 apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization kind: Kustomization
resources: resources:
{% if site_type == "docker" %}
- deployment.yaml
{% endif %}
- service.yaml - service.yaml
- ingress.yaml - ingress.yaml
- certificate.yaml - certificate.yaml
-13
View File
@@ -1,13 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: {{ site_k8s }}
namespace: {{ namespace }}
spec:
clusterIP: None
selector:
app: {{ site_k8s }}
ports:
- name: http
port: 80
targetPort: http