Rustelo/templates/website-leptos/provisioning/content.nu

144 lines
5.8 KiB
Text
Raw Normal View History

2026-07-18 19:57:03 +01:00
#!/usr/bin/env nu
# Hot-deploy content to running pod without restart (within hot-reload caveats below).
#
# Hot-reload caveats:
# Markdown / images / CSS / static: hot-reload works (filesystem-served per request)
# FTL translation changes (any): requires restart — registry loaded at startup
# New routes or new pages: requires full rebuild (build.nu) + deploy.nu update
#
# Usage:
# nu provisioning/content.nu pack [--since <git-ref>] [--all]
# nu provisioning/content.nu deploy [--pod <name>] [--namespace <ns>]
# nu provisioning/content.nu sync [--since <git-ref>] [--all] [--pod <name>] [--namespace <ns>]
# Hot-updatable paths come from the single distro manifest (provisioning/distro.ncl),
# the same source the Dockerfiles and `just distro` read — no divergent lists.
def content-dirs [project_root: string]: nothing -> list<string> {
^nickel export $"($project_root)/provisioning/distro.ncl" --field content_hot --format json
| from json
}
const CONTENT_EXCLUDES = [
".draft"
".DS_Store"
"/_archive/"
]
const PVC_MOUNT = "/var/www"
const NAMESPACE = "example-pro"
const APP_LABEL = "app.kubernetes.io/name=website"
def find-pod [ns: string]: nothing -> string {
let r = (do { ^kubectl get pods -n $ns -l $APP_LABEL --no-headers -o jsonpath="{.items[0].metadata.name}" } | complete)
if $r.exit_code != 0 or ($r.stdout | str trim | is-empty) {
error make { msg: $"no running pod found for ($APP_LABEL) in ($ns)" }
}
$r.stdout | str trim
}
def changed-files [since: string]: nothing -> list<string> {
let r = (do { ^git diff --name-only $since HEAD } | complete)
if $r.exit_code != 0 { error make { msg: "git diff failed" } }
$r.stdout | lines | where { |f| $f | is-not-empty }
}
def excluded? [f: string]: nothing -> bool {
CONTENT_EXCLUDES | any { |pat| $f | str contains $pat }
}
def content-files [project_root: string, since: string, all: bool]: nothing -> list<string> {
let content_dirs = (content-dirs $project_root)
let candidates = if $all {
$content_dirs | each { |d|
do { ^find $"($project_root)/($d)" -type f } | complete
| if $in.exit_code == 0 { $in.stdout | lines | where { |l| $l | is-not-empty } } else { [] }
} | flatten
} else {
let changed = (changed-files $since)
$changed
| where { |f| $content_dirs | any { |d| $f | str starts-with $d } }
| each { |f| $"($project_root)/($f)" }
}
$candidates
| where { |f| $f | path exists }
| where { |f| not (excluded? $f) }
}
def pick-tar []: nothing -> string {
let gtar = (do { ^which gtar } | complete)
if $gtar.exit_code == 0 { ($gtar.stdout | str trim) } else { "tar" }
}
def do-pack [project_root: string, since: string, all: bool]: nothing -> string {
let timestamp = (date now | format date "%Y%m%dT%H%M%S")
let git_sha = (do { ^git rev-parse --short HEAD } | complete | if $in.exit_code == 0 { $in.stdout | str trim } else { "unknown" })
let pack_dir = $"($project_root)/provisioning/.content-packs"
let outfile = $"($pack_dir)/($timestamp)-content.tgz"
mkdir $pack_dir
let files = (content-files $project_root $since $all)
if ($files | is-empty) { print "no content files to pack"; return "" }
let manifest_path = $"($pack_dir)/($timestamp)-manifest.json"
let file_list_path = $"($pack_dir)/($timestamp)-files.txt"
{ version: $timestamp, git_sha: $git_sha, file_count: ($files | length), files: $files }
| to json --raw
| save -f $manifest_path
let rel_files = ($files | each { |f| $f | str replace $"($project_root)/" "" })
$rel_files | str join "\n" | save -f $file_list_path
let tar_bin = (pick-tar)
do { cd $project_root; ^$tar_bin -czf $outfile -T $file_list_path } | complete
| if $in.exit_code != 0 { error make { msg: "tar failed" } }
print $"packed ($files | length) files → ($outfile)"
$outfile
}
def do-deploy [pack_path: string, pod: string, ns: string]: nothing -> nothing {
let remote_tmp = $"/tmp/($pack_path | path basename)"
print $"[deploy] ($pack_path) → ($pod):($remote_tmp)"
^kubectl cp $pack_path $"($ns)/($pod):($remote_tmp)"
^kubectl exec -n $ns $pod -- tar -xzf $remote_tmp -C $PVC_MOUNT
^kubectl exec -n $ns $pod -- rm -f $remote_tmp
print "[deploy] done — no restart required (markdown/CSS/static)"
print "[deploy] note: FTL changes require: nu provisioning/deploy.nu restart"
}
def main [
subcmd: string = "sync"
--since: string = "HEAD~1"
--all
--pod: string = ""
--namespace: string = $NAMESPACE
]: nothing -> nothing {
let project_root = ($env.FILE_PWD | path dirname)
match $subcmd {
"pack" => { do-pack $project_root $since $all | ignore }
"deploy" => {
let pack_dir = $"($project_root)/provisioning/.content-packs"
let tgz_files = (glob $"($pack_dir)/*.tgz")
if ($tgz_files | is-empty) {
error make { msg: "no packs found — run pack first" }
}
let latest = ($tgz_files | each { |p| { name: $p, mtime: (ls $p | first | get modified) } } | sort-by mtime | last | get name)
let resolved_pod = if ($pod | is-not-empty) { $pod } else { find-pod $namespace }
do-deploy $latest $resolved_pod $namespace
}
"sync" => {
let pack_path = (do-pack $project_root $since $all)
if ($pack_path | is-not-empty) {
let resolved_pod = if ($pod | is-not-empty) { $pod } else { find-pod $namespace }
do-deploy $pack_path $resolved_pod $namespace
}
}
_ => {
print "Usage: content.nu <pack|deploy|sync> [--since <ref>] [--all] [--pod <name>] [--namespace <ns>]"
}
}
}