feat(motor): agregar version fijable en build y clusterlog bootstrap-instance
Primer paso hacia separar el motor (herramienta genérica) de cada instancia (contenido de un deployment): Version deja de ser const para poder inyectarse con -ldflags en `make build` (git describe, sin tags aún), y el comando nuevo bootstrap-instance genera el esqueleto mínimo de una instancia (zola.toml, content/<sección>/_index.md, admins.json vacío, .clusterlog-version) reusando saveAdmins() en vez de duplicar su formato. Documentado en CLI.md; el empaquetado de templates/tema y el CI de instancia quedan para cuando el motor publique releases. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 <su-llave.pub> admin add --id <id> --name <nombre> --email <correo> --key <su-llave.pub> --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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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**.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
/public/
|
||||
/bin/
|
||||
/themes/zola.386/
|
||||
.DS_Store
|
||||
*.tmp
|
||||
@@ -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"
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user