diff --git a/CLI.md b/CLI.md index 264c1ce..826c279 100644 --- a/CLI.md +++ b/CLI.md @@ -366,3 +366,42 @@ clusterlog --root . --json verify commit HEAD ``` El agente debe detenerse ante cualquier código distinto de cero y presentar el error al usuario sin ocultarlo. + +## 16. Versión + +```bash +clusterlog version +``` + +No requiere `--root` ni identidad. Devuelve `{"name":"clusterlog","version":"..."}`. +El valor de `version` se fija en tiempo de compilación: + +```bash +go build -ldflags "-X example.org/bitacora-cluster/internal/clusterlog.Version=v0.2.0" ... +``` + +`make build` ya lo hace automáticamente a partir de `git describe --tags --always --dirty` (variable `CLUSTERLOG_VERSION` del `Makefile`); un build local sin tags produce algo como `abc1234-dirty`, no `dev` ni una cadena vacía. + +## 17. Instancias nuevas + +```bash +clusterlog bootstrap-instance \ + --output ./clusterlog-nuevo-deploy \ + --product-name "Bitácora de Ejemplo" \ + --base-url "https://clusterlog.ejemplo.org" +``` + +No requiere `--root`: `--output` es un directorio nuevo, normalmente fuera del repositorio actual. Tampoco usa `--commit` (no hay nada que firmar todavía). Genera: + +- `zola.toml` con `base_url`/`title`/`[extra] product_name` sustituidos y el resto de los valores genéricos del motor; +- `content//_index.md` para `documentacion`, `manuales`, `memorias`, `proyectos`, `tareas` y `revision`; +- `data/admins.json` y `config/allowed_signers` vacíos (mismo formato que produce `admin add`, sin ningún administrador todavía); +- `data/review-events/.gitkeep`, `data/task-events/.gitkeep`, `data/generated/`; +- `.clusterlog-version` con la versión del binario que generó el scaffold; +- `README.md` y `.gitignore` de arranque. + +Falla con `output_not_empty` (código `5`) si `--output` ya existe y no está vacío — nunca sobreescribe una instancia existente. Con `--dry-run` no crea el directorio; sólo muestra qué haría. + +Después de `bootstrap-instance`, el flujo es el mismo que "Registrar al primer administrador" en `README.md`, apuntando `--root` al directorio nuevo: `admin add` → `sync` → `validate --skip-zola`. `validate` antes de la primera `sync` falla con `generated_state_stale` a propósito: la proyección todavía no existe. + +**No incluido todavía** (fases posteriores, ver el ADR de motor/instancia cuando exista): plantillas Tera ni tema empaquetados para descarga, ni un `.gitea/workflows/ci.yml` de instancia — el motor aún no publica releases binarios. diff --git a/Makefile b/Makefile index 80f68c2..cb4cdc4 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,8 @@ SHELL := /usr/bin/env bash GO ?= go ZOLA ?= zola BINARY ?= bin/clusterlog +CLUSTERLOG_VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev) +LDFLAGS := -X example.org/bitacora-cluster/internal/clusterlog.Version=$(CLUSTERLOG_VERSION) .PHONY: all build test vet e2e theme sync sync-check validate validate-local site serve smoke ci clean runme-install runme-list runme-check runme-ci @@ -9,7 +11,7 @@ all: build build: mkdir -p $(dir $(BINARY)) - $(GO) build -trimpath -o $(BINARY) ./cmd/clusterlog + $(GO) build -trimpath -ldflags "$(LDFLAGS)" -o $(BINARY) ./cmd/clusterlog test: $(GO) test ./... diff --git a/README.md b/README.md index b7e51b4..aec44c0 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,16 @@ zola serve El sitio queda disponible en la dirección que muestre Zola. Antes de publicarlo, cambie `base_url` en `zola.toml` y manténgalo detrás de VPN, red privada o autenticación corporativa. +## Crear una instancia nueva + +Este repositorio hoy mezcla la herramienta con el contenido de un deployment. Para empezar un deployment **distinto** sin partir de este contenido, use: + +```bash +clusterlog bootstrap-instance --output ./mi-deploy --product-name "Mi Bitácora" +``` + +Genera `zola.toml`, las seis secciones de `content/` vacías, `data/admins.json`/`config/allowed_signers` vacíos y `.clusterlog-version`. Ver la sección 17 de [`CLI.md`](CLI.md) para el detalle completo; sigue siendo un primer paso — todavía no incluye plantillas/tema empaquetados ni CI de instancia (el motor no publica releases binarios todavía). + ## Registrar al primer administrador La llave pública identifica el registro del administrador. Para una auditoría fuerte, las operaciones posteriores deben usar `--commit` y la rama protegida debe rechazar commits sin firma válida. diff --git a/internal/clusterlog/commands.go b/internal/clusterlog/commands.go index e1aae74..5f3af99 100644 --- a/internal/clusterlog/commands.go +++ b/internal/clusterlog/commands.go @@ -24,6 +24,9 @@ func (a *App) Run(args []string) error { if command == "schema" { return a.runSchema(args[1:]) } + if command == "bootstrap-instance" { + return a.runBootstrapInstance(args[1:]) + } root, err := a.ResolveRoot() if err != nil { @@ -66,6 +69,8 @@ Opciones globales (antes del comando): --dry-run valida y muestra el resultado sin escribir Comandos: + version + bootstrap-instance --output DIR [--product-name NOMBRE] [--base-url URL] identity admin add | admin list memory create diff --git a/internal/clusterlog/commands_bootstrap.go b/internal/clusterlog/commands_bootstrap.go new file mode 100644 index 0000000..18dbe0d --- /dev/null +++ b/internal/clusterlog/commands_bootstrap.go @@ -0,0 +1,170 @@ +package clusterlog + +import ( + "embed" + "os" + "path/filepath" + "sort" + "strings" +) + +//go:embed all:scaffold +var scaffoldFS embed.FS + +const scaffoldRoot = "scaffold" + +var scaffoldSections = []string{"documentacion", "manuales", "memorias", "proyectos", "tareas", "revision"} + +// runBootstrapInstance genera el esqueleto de una instancia nueva de +// clusterlog (content/, data/, config/, zola.toml) en --output. No toca el +// proyecto actual: --output es un directorio nuevo, casi siempre fuera de +// este repositorio, así que este comando no pasa por ResolveRoot ni por el +// mecanismo de --commit. +func (a *App) runBootstrapInstance(args []string) error { + fs := newFlagSet("bootstrap-instance") + output := fs.String("output", "", "directorio donde crear la instancia nueva") + productName := fs.String("product-name", "Bitácora Operativa", "nombre del producto para zola.toml") + baseURL := fs.String("base-url", "https://clusterlog.example.org", "base_url de zola.toml; cámbielo antes de publicar") + if err := fs.Parse(args); err != nil { + return NewError(ExitUsage, "invalid_flags", err.Error(), nil) + } + if strings.TrimSpace(*output) == "" { + return NewError(ExitUsage, "missing_output", "indique --output con el directorio de la instancia nueva", nil) + } + root, err := filepath.Abs(*output) + if err != nil { + return NewError(ExitInternal, "internal", err.Error(), nil) + } + + if entries, statErr := os.ReadDir(root); statErr == nil { + if len(entries) > 0 { + return NewError(ExitConflict, "output_not_empty", "el directorio de salida ya existe y no está vacío", root) + } + } else if !os.IsNotExist(statErr) { + return NewError(ExitInternal, "internal", statErr.Error(), root) + } + + if a.Options.DryRun { + return a.printResult("bootstrap-instance", map[string]any{ + "output": root, + "product_name": *productName, + "base_url": *baseURL, + "dry_run": true, + }) + } + + created, err := writeScaffold(root, *productName, *baseURL) + if err != nil { + return err + } + + registry := AdminRegistry{Version: 1, ReviewPolicy: ReviewPolicy{Default: "all-active", IncludeAuthor: true}, Admins: []Admin{}} + if err := saveAdmins(root, registry); err != nil { + return NewError(ExitInternal, "internal", err.Error(), nil) + } + created = append(created, + filepath.Join(root, "data", "admins.json"), + filepath.Join(root, "config", "allowed_signers"), + ) + sort.Strings(created) + + return a.printResult("bootstrap-instance", map[string]any{ + "output": root, + "engine_version": Version, + "product_name": *productName, + "base_url": *baseURL, + "files_created": created, + "next_steps": []string{ + "clusterlog --root " + root + " --ssh-key admin add --id --name --email --key --roles owner,admin", + "clusterlog --root " + root + " sync", + "clusterlog --root " + root + " validate --skip-zola", + }, + }) +} + +func writeScaffold(root, productName, baseURL string) ([]string, error) { + created := []string{} + + for _, section := range scaffoldSections { + src := scaffoldRoot + "/content/" + section + "/_index.md" + data, err := scaffoldFS.ReadFile(src) + if err != nil { + return nil, NewError(ExitInternal, "internal", "plantilla de scaffold faltante: "+src, err.Error()) + } + dst := filepath.Join(root, "content", section, "_index.md") + if err := writeFileAtomic(dst, data, 0o644); err != nil { + return nil, NewError(ExitInternal, "internal", err.Error(), dst) + } + created = append(created, dst) + } + + zolaTmpl, err := scaffoldFS.ReadFile(scaffoldRoot + "/zola.toml.tmpl") + if err != nil { + return nil, NewError(ExitInternal, "internal", "plantilla zola.toml faltante", err.Error()) + } + title := "Bitácora Operativa" + if strings.TrimSpace(productName) != "" { + title = productName + } + zolaContent := renderScaffoldTemplate(string(zolaTmpl), map[string]string{ + "BASE_URL": baseURL, + "TITLE": title, + "PRODUCT_NAME": productName, + }) + zolaPath := filepath.Join(root, "zola.toml") + if err := writeFileAtomic(zolaPath, []byte(zolaContent), 0o644); err != nil { + return nil, NewError(ExitInternal, "internal", err.Error(), zolaPath) + } + created = append(created, zolaPath) + + gitignoreTmpl, err := scaffoldFS.ReadFile(scaffoldRoot + "/gitignore.tmpl") + if err != nil { + return nil, NewError(ExitInternal, "internal", "plantilla .gitignore faltante", err.Error()) + } + gitignorePath := filepath.Join(root, ".gitignore") + if err := writeFileAtomic(gitignorePath, gitignoreTmpl, 0o644); err != nil { + return nil, NewError(ExitInternal, "internal", err.Error(), gitignorePath) + } + created = append(created, gitignorePath) + + readmeTmpl, err := scaffoldFS.ReadFile(scaffoldRoot + "/README.md.tmpl") + if err != nil { + return nil, NewError(ExitInternal, "internal", "plantilla README faltante", err.Error()) + } + readmeContent := renderScaffoldTemplate(string(readmeTmpl), map[string]string{ + "TITLE": title, + "PRODUCT_NAME": productName, + }) + readmePath := filepath.Join(root, "README.md") + if err := writeFileAtomic(readmePath, []byte(readmeContent), 0o644); err != nil { + return nil, NewError(ExitInternal, "internal", err.Error(), readmePath) + } + created = append(created, readmePath) + + versionPath := filepath.Join(root, ".clusterlog-version") + if err := writeFileAtomic(versionPath, []byte(Version+"\n"), 0o644); err != nil { + return nil, NewError(ExitInternal, "internal", err.Error(), versionPath) + } + created = append(created, versionPath) + + for _, dir := range []string{filepath.Join(root, "data", "review-events"), filepath.Join(root, "data", "task-events")} { + keep := filepath.Join(dir, ".gitkeep") + if err := writeFileAtomic(keep, []byte{}, 0o644); err != nil { + return nil, NewError(ExitInternal, "internal", err.Error(), keep) + } + created = append(created, keep) + } + + if err := os.MkdirAll(filepath.Join(root, "data", "generated"), 0o755); err != nil { + return nil, NewError(ExitInternal, "internal", err.Error(), nil) + } + + return created, nil +} + +func renderScaffoldTemplate(text string, values map[string]string) string { + for key, value := range values { + text = strings.ReplaceAll(text, "{{"+key+"}}", value) + } + return text +} diff --git a/internal/clusterlog/commands_bootstrap_test.go b/internal/clusterlog/commands_bootstrap_test.go new file mode 100644 index 0000000..2b75110 --- /dev/null +++ b/internal/clusterlog/commands_bootstrap_test.go @@ -0,0 +1,149 @@ +package clusterlog + +import ( + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" +) + +func newBootstrapApp(t *testing.T, dryRun bool) *App { + t.Helper() + return New(Options{DryRun: dryRun, JSON: true, Stdout: os.Stdout, Stderr: os.Stderr, Stdin: os.Stdin, Executable: "clusterlog"}) +} + +func TestBootstrapInstanceCreatesExpectedFiles(t *testing.T) { + base := t.TempDir() + out := filepath.Join(base, "instancia-nueva") + app := newBootstrapApp(t, false) + + err := app.runBootstrapInstance([]string{"--output", out, "--product-name", "Bitácora de Prueba", "--base-url", "https://clusterlog.prueba.org"}) + if err != nil { + t.Fatalf("runBootstrapInstance falló: %v", err) + } + + for _, section := range scaffoldSections { + path := filepath.Join(out, "content", section, "_index.md") + if _, statErr := os.Stat(path); statErr != nil { + t.Fatalf("falta %s: %v", path, statErr) + } + } + + zolaData, err := os.ReadFile(filepath.Join(out, "zola.toml")) + if err != nil { + t.Fatal(err) + } + zola := string(zolaData) + if !strings.Contains(zola, `base_url = "https://clusterlog.prueba.org"`) { + t.Fatalf("zola.toml no tiene el base_url esperado: %s", zola) + } + if !strings.Contains(zola, `title = "Bitácora de Prueba"`) { + t.Fatalf("zola.toml no tiene el title esperado: %s", zola) + } + if !strings.Contains(zola, `product_name = "Bitácora de Prueba"`) { + t.Fatalf("zola.toml no tiene product_name esperado: %s", zola) + } + + var registry AdminRegistry + adminsData, err := os.ReadFile(filepath.Join(out, "data", "admins.json")) + if err != nil { + t.Fatal(err) + } + if err := json.Unmarshal(adminsData, ®istry); err != nil { + t.Fatal(err) + } + if registry.Version != 1 || len(registry.Admins) != 0 || registry.ReviewPolicy.Default != "all-active" { + t.Fatalf("registro de admins inesperado: %+v", registry) + } + + if _, err := os.Stat(filepath.Join(out, "config", "allowed_signers")); err != nil { + t.Fatalf("falta config/allowed_signers: %v", err) + } + + versionData, err := os.ReadFile(filepath.Join(out, ".clusterlog-version")) + if err != nil { + t.Fatal(err) + } + if trimmed := strings.TrimSpace(string(versionData)); trimmed != Version { + t.Fatalf(".clusterlog-version = %q, se esperaba %q", trimmed, Version) + } + + for _, dir := range []string{"review-events", "task-events"} { + if _, err := os.Stat(filepath.Join(out, "data", dir, ".gitkeep")); err != nil { + t.Fatalf("falta data/%s/.gitkeep: %v", dir, err) + } + } + + if _, err := os.Stat(filepath.Join(out, "README.md")); err != nil { + t.Fatalf("falta README.md: %v", err) + } + if _, err := os.Stat(filepath.Join(out, ".gitignore")); err != nil { + t.Fatalf("falta .gitignore: %v", err) + } +} + +func TestBootstrapInstanceScaffoldValidatesClean(t *testing.T) { + base := t.TempDir() + out := filepath.Join(base, "instancia-nueva") + app := newBootstrapApp(t, false) + if err := app.runBootstrapInstance([]string{"--output", out}); err != nil { + t.Fatalf("runBootstrapInstance falló: %v", err) + } + + report := ValidationReport{Checks: map[string]int{}, Errors: []ValidationIssue{}, Warnings: []ValidationIssue{}} + validateApp := &App{} + registry, err := loadAdmins(out) + if err != nil { + t.Fatal(err) + } + validateApp.validateAdmins(out, registry, &report) + pages := validateApp.validatePages(out, registry, &report) + validateApp.validateEvents(out, registry, pages, &report) + if len(report.Errors) != 0 { + t.Fatalf("el scaffold recién creado no debería tener errores de validate: %+v", report.Errors) + } +} + +func TestBootstrapInstanceRefusesNonEmptyOutput(t *testing.T) { + base := t.TempDir() + out := filepath.Join(base, "instancia-existente") + if err := os.MkdirAll(out, 0o755); err != nil { + t.Fatal(err) + } + mustWriteTestFile(t, filepath.Join(out, "algo.txt"), "ya había algo aquí") + + app := newBootstrapApp(t, false) + err := app.runBootstrapInstance([]string{"--output", out}) + if err == nil { + t.Fatal("se esperaba un error por directorio no vacío") + } + cliErr, ok := err.(*CLIError) + if !ok || cliErr.Kind != "output_not_empty" { + t.Fatalf("se esperaba output_not_empty, se obtuvo: %v", err) + } +} + +func TestBootstrapInstanceDryRunWritesNothing(t *testing.T) { + base := t.TempDir() + out := filepath.Join(base, "instancia-dry-run") + app := newBootstrapApp(t, true) + if err := app.runBootstrapInstance([]string{"--output", out}); err != nil { + t.Fatalf("runBootstrapInstance --dry-run falló: %v", err) + } + if _, err := os.Stat(out); err == nil { + t.Fatal("--dry-run no debería haber creado el directorio de salida") + } +} + +func TestBootstrapInstanceRequiresOutput(t *testing.T) { + app := newBootstrapApp(t, false) + err := app.runBootstrapInstance([]string{}) + if err == nil { + t.Fatal("se esperaba un error por --output faltante") + } + cliErr, ok := err.(*CLIError) + if !ok || cliErr.Kind != "missing_output" { + t.Fatalf("se esperaba missing_output, se obtuvo: %v", err) + } +} diff --git a/internal/clusterlog/scaffold/README.md.tmpl b/internal/clusterlog/scaffold/README.md.tmpl new file mode 100644 index 0000000..4f8c702 --- /dev/null +++ b/internal/clusterlog/scaffold/README.md.tmpl @@ -0,0 +1,51 @@ +# {{TITLE}} + +Este repositorio es una **instancia de clusterlog**: contiene el contenido, +los datos y la configuración de un deployment concreto, no el código del +motor. Se generó con: + +```bash +clusterlog bootstrap-instance --output . --product-name "{{PRODUCT_NAME}}" +``` + +## Qué hay aquí + +- `content/` — documentación, manuales, memorias, proyectos y tareas de esta + instancia. Empieza con las seis secciones vacías (`_index.md` únicamente). +- `data/admins.json`, `config/allowed_signers` — registro de administradores, + vacío hasta el primer `admin add`. +- `zola.toml` — configuración de este deployment (`base_url`, nombre del + producto). Cámbiela antes de publicar. +- `.clusterlog-version` — versión del motor `clusterlog` usada para generar + este esqueleto. Manténgala sincronizada con el binario que use su CI. + +## Primeros pasos + +1. Cambie `base_url` en `zola.toml` por el dominio real de este deployment. +2. Registre al primer administrador (recibe `owner` automáticamente): + + ```bash + clusterlog --root . --ssh-key ~/.ssh/id_ed25519.pub admin add \ + --id ana --name "Ana Torres" --email ana@example.org \ + --key ~/.ssh/id_ed25519.pub --roles owner,admin + ``` + +3. Inicialice Git y firme el primer commit (vea la sección "Registrar al + primer administrador" en la documentación del motor para el detalle + completo de `git config`/`commit -S`). +4. Genere la proyección y valide: + + ```bash + clusterlog --root . sync + clusterlog --root . validate --skip-zola + ``` + +## Pendiente (no incluido por este comando) + +- Plantillas Tera y el tema `zola.386`: todavía se obtienen desde un checkout + del repositorio del motor (`make theme` / `scripts/bootstrap-theme.sh`). + Empaquetarlos en un release descargable del motor es trabajo de una fase + posterior. +- Un workflow de CI para esta instancia (que descargue el binario del motor + por versión fijada) no se generó todavía: el motor aún no publica releases + binarios. Se agrega en cuanto exista ese release. diff --git a/internal/clusterlog/scaffold/content/documentacion/_index.md b/internal/clusterlog/scaffold/content/documentacion/_index.md new file mode 100644 index 0000000..a5fe80d --- /dev/null +++ b/internal/clusterlog/scaffold/content/documentacion/_index.md @@ -0,0 +1,9 @@ ++++ +title = "Documentación" +description = "Arquitectura, decisiones, inventarios, políticas y explicaciones técnicas duraderas." +sort_by = "weight" +template = "section.html" +page_template = "page.html" ++++ + +Aquí vive el conocimiento descriptivo del clúster: el **qué**, el **por qué** y las relaciones entre componentes. diff --git a/internal/clusterlog/scaffold/content/manuales/_index.md b/internal/clusterlog/scaffold/content/manuales/_index.md new file mode 100644 index 0000000..db8f5a7 --- /dev/null +++ b/internal/clusterlog/scaffold/content/manuales/_index.md @@ -0,0 +1,9 @@ ++++ +title = "Manuales" +description = "Runbooks y procedimientos operativos verificables, con prerrequisitos, pasos y reversión." +sort_by = "weight" +template = "section.html" +page_template = "page.html" ++++ + +Los manuales deben ser ejecutables por una persona distinta de quien los escribió y declarar claramente sus condiciones de seguridad. diff --git a/internal/clusterlog/scaffold/content/memorias/_index.md b/internal/clusterlog/scaffold/content/memorias/_index.md new file mode 100644 index 0000000..0a74653 --- /dev/null +++ b/internal/clusterlog/scaffold/content/memorias/_index.md @@ -0,0 +1,10 @@ ++++ +title = "Memorias" +description = "Historia cronológica y auditable de intervenciones, cambios, incidentes y correcciones." +sort_by = "date" +template = "memories.html" +page_template = "memory.html" +generate_feeds = true ++++ + +Cada entrada describe una sesión de trabajo o una unidad de cambio. Las entradas sujetas a revisión aparecen también en **Revisión semanal**. diff --git a/internal/clusterlog/scaffold/content/proyectos/_index.md b/internal/clusterlog/scaffold/content/proyectos/_index.md new file mode 100644 index 0000000..0fc4767 --- /dev/null +++ b/internal/clusterlog/scaffold/content/proyectos/_index.md @@ -0,0 +1,9 @@ ++++ +title = "Proyectos" +description = "Objetivos compartidos, alcance, responsables, hitos y criterios de éxito." +sort_by = "date" +template = "projects.html" +page_template = "project.html" ++++ + +Los proyectos dan contexto a las tareas y permiten explicar por qué existe cada línea de trabajo. diff --git a/internal/clusterlog/scaffold/content/revision/_index.md b/internal/clusterlog/scaffold/content/revision/_index.md new file mode 100644 index 0000000..188be64 --- /dev/null +++ b/internal/clusterlog/scaffold/content/revision/_index.md @@ -0,0 +1,8 @@ ++++ +title = "Revisión semanal" +description = "Memorias que aún requieren confirmación de uno o más administradores." +template = "review.html" +in_search_index = false ++++ + +Esta página es la agenda operativa de la reunión semanal. Una memoria desaparece cuando todos los revisores requeridos han registrado su revisión para el hash actual del contenido. diff --git a/internal/clusterlog/scaffold/content/tareas/_index.md b/internal/clusterlog/scaffold/content/tareas/_index.md new file mode 100644 index 0000000..7713a02 --- /dev/null +++ b/internal/clusterlog/scaffold/content/tareas/_index.md @@ -0,0 +1,9 @@ ++++ +title = "Tareas" +description = "Trabajo visible para el equipo, con prioridad, estimación, fecha objetivo y tiempo acumulado." +sort_by = "date" +template = "tasks.html" +page_template = "task.html" ++++ + +El estado actual y el tiempo real se calculan a partir de eventos anexables; la definición original de la tarea permanece legible en Markdown. diff --git a/internal/clusterlog/scaffold/gitignore.tmpl b/internal/clusterlog/scaffold/gitignore.tmpl new file mode 100644 index 0000000..34c6268 --- /dev/null +++ b/internal/clusterlog/scaffold/gitignore.tmpl @@ -0,0 +1,5 @@ +/public/ +/bin/ +/themes/zola.386/ +.DS_Store +*.tmp diff --git a/internal/clusterlog/scaffold/zola.toml.tmpl b/internal/clusterlog/scaffold/zola.toml.tmpl new file mode 100644 index 0000000..70ade15 --- /dev/null +++ b/internal/clusterlog/scaffold/zola.toml.tmpl @@ -0,0 +1,53 @@ +base_url = "{{BASE_URL}}" +title = "{{TITLE}}" +description = "Documentación, manuales, memorias de cambio, proyectos y tareas del equipo de soporte." +default_language = "es" +theme = "zola.386" +output_dir = "public" +compile_sass = true +minify_html = true +generate_feeds = true +feed_filenames = ["atom.xml", "rss.xml"] +build_search_index = true +generate_sitemap = true +generate_robots_txt = false + +taxonomies = [ + { name = "categories" }, + { name = "tags", feed = true }, + { name = "systems" }, +] + +[markdown] +external_links_target_blank = true +external_links_no_referrer = true +smart_punctuation = true +github_alerts = true +insert_anchor_links = "right" + +[markdown.highlighting] +error_on_missing_language = false +style = "inline" +theme = "vitesse-black" + +[search] +include_title = true +include_description = true +include_date = true +include_path = true +include_content = true +truncate_content_length = 12000 +index_format = "elasticlunr_json" + +[slugify] +paths = "on" +taxonomies = "on" +anchors = "on" +paths_keep_dates = false + +[extra] +product_name = "{{PRODUCT_NAME}}" +product_short_name = "clusterlog" +repository_label = "Repositorio interno" +review_meeting_label = "Revisión semanal" +footer_text = "Fuente auditable: Markdown + eventos JSON + Git" diff --git a/internal/clusterlog/types.go b/internal/clusterlog/types.go index 4610939..a95511d 100644 --- a/internal/clusterlog/types.go +++ b/internal/clusterlog/types.go @@ -10,7 +10,12 @@ import ( "time" ) -const Version = "0.1.0" +// Version se puede sobreescribir en tiempo de build con: +// +// -ldflags "-X example.org/bitacora-cluster/internal/clusterlog.Version=v0.2.0" +// +// Ver la variable CLUSTERLOG_VERSION en el Makefile. +var Version = "0.1.0" const ( ExitOK = 0