Ir al contenido

Windows

Windows es una plataforma publicada, no algo añadido después: cada cambio ejecuta la suite unitaria entera y la de extremo a extremo en un runner de Windows, y el código fuente lleva partes solo de Windows allí donde el sistema se comporta distinto. Lo que sigue está escrito para PowerShell, con la forma de cmd al lado siempre que las dos difieran.

Lee esto primero. Cuatro de las cinco líneas de abajo son la razón de que una orden copiada de una página de Linux no funcione.

  • El programa es ghchronicle.exe. Escribir ghchronicle lo encuentra, porque PATHEXT lista .EXE.
  • Dependencias en ejecución: ninguna. El binario se compila con CGO_ENABLED=0, así que no hay runtime de C que instalar.
  • Pararlo es Ctrl+C en su consola. No hay señal que enviarle, y taskkill /F lo termina sin cerrar sus destinos.
  • Rutas en la configuración: una barra invertida dentro de un escalar YAML entre comillas dobles es un escape. Usa barras normales.
  • Ejecución desatendida: una tarea programada. El binario no es un servicio del Administrador de control de servicios.

Los archivos de Windows son zip y no tar.gz, y se llaman ghchronicle_<versión>_windows_<arq>.zip, donde <arq> es amd64 o arm64.

$env:PROCESSOR_ARCHITECTUREEl archivo que toca
AMD64windows_amd64
ARM64windows_arm64

Esa variable describe el proceso, no la máquina. Un PowerShell de 32 bits en una máquina de 64 informa de x86 y deja la arquitectura real de la máquina en $env:PROCESSOR_ARCHITEW6432; un PowerShell x64 emulado en una máquina ARM64 informa de AMD64 y no pone nada más, que es el caso que te entrega sin ruido el archivo equivocado. Si alguno de los dos puedes ser tú, pregúntale a la máquina y no al intérprete:

Ventana de terminal
(Get-CimInstance Win32_Processor).Architecture # 9 es x64, 12 es ARM64
Ventana de terminal
$version = "1.0.0"
$arch = if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") { "arm64" } else { "amd64" }
$base = "https://github.com/jmrplens/ghchronicle/releases/download/v$version"
$zip = "ghchronicle_${version}_windows_${arch}.zip"
Invoke-WebRequest -UseBasicParsing -Uri "$base/$zip" -OutFile $zip

-UseBasicParsing no es adorno. En Windows PowerShell 5.1 Invoke-WebRequest arma su resultado a través del motor de Internet Explorer salvo que se le diga que no, y falla del todo en una máquina donde Internet Explorer se haya quitado o nunca haya pasado su configuración de primer arranque, que es el caso de Server Core y de casi cualquier imagen endurecida. En PowerShell 7 el modificador se acepta y no hace nada.

checksums.txt lleva el SHA-256 de cada archivo, y checksums.txt.sigstore.json es una firma sobre ese fichero.

  1. Coge el fichero de sumas y compara la única línea que es tuya.

    Ventana de terminal
    Invoke-WebRequest -UseBasicParsing -Uri "$base/checksums.txt" -OutFile checksums.txt
    $expected = (Select-String -Path checksums.txt -Pattern ([regex]::Escape($zip) + '$')).Line.Split(" ")[0]
    $actual = (Get-FileHash -Algorithm SHA256 -Path $zip).Hash
    if ($actual -eq $expected) { "OK" } else { "MISMATCH" }

    Get-FileHash devuelve el resumen en mayúsculas y checksums.txt lo guarda en minúsculas. Aun así comparan igual porque el -eq de PowerShell entre dos cadenas ignora las mayúsculas, que es el único sitio de esta página donde ese comportamiento por omisión resulta cómodo en vez de una trampa.

  2. Comprueba el propio fichero de sumas, si tienes cosign. La orden es la misma que imprimen las notas de la release, y la misma que ejecuta quien lea la página de Linux o la de macOS.

    Ventana de terminal
    cosign verify-blob `
    --certificate-identity-regexp 'https://github.com/jmrplens/ghchronicle/.github/workflows/release.yml@refs/tags/.*' `
    --certificate-oidc-issuer https://token.actions.githubusercontent.com `
    --bundle checksums.txt.sigstore.json `
    checksums.txt

    El acento grave es la continuación de línea de PowerShell, donde un guion de intérprete usa la barra invertida.

El archivo lleva tres ficheros y ningún directorio, así que descomprímelo en un directorio que hayas hecho.

  • Directorioghchronicle_1.0.0_windows_amd64.zip
    • ghchronicle.exe el binario
    • LICENSE
    • README.md

Un PowerShell elevado, porque tanto el directorio como el Path de máquina piden permisos de administrador.

Ventana de terminal
$dir = "C:\Program Files\ghchronicle"
Expand-Archive -Path $zip -DestinationPath $dir -Force
$machine = [Environment]::GetEnvironmentVariable("Path", "Machine")
[Environment]::SetEnvironmentVariable("Path", "$machine;$dir", "Machine")

Un Path nuevo solo llega a los procesos que arranquen después, así que abre una terminal nueva antes de la comprobación de abajo. La actual se queda con el entorno que le dieron.

Ventana de terminal
ghchronicle -version
ghchronicle 1.0.0 (commit 4e5dfc2, built 2026-09-14T23:04:02Z)

Cada ${VAR} del fichero de configuración se lee del entorno cuando arranca el proceso, así que el token nunca tiene que estar en el fichero.

Ventana de terminal
# Solo esta ventana
$env:GITHUB_TOKEN = "github_pat_..."
# Persistido para esta cuenta
[Environment]::SetEnvironmentVariable("GITHUB_TOKEN", "github_pat_...", "User")

El binario necesita un fichero de configuración, y el inicio rápido escribe uno en seis pasos. Guárdalo en UTF-8, y atención a los dos detalles de Windows de debajo.

Ventana de terminal
ghchronicle -config config.yaml -list # qué se recogería
ghchronicle -config config.yaml -once # una pasada y termina

Si el binario está en el directorio actual y no en el PATH, PowerShell necesita que se le nombre como ruta: .\ghchronicle.exe. Un nombre a secas es una orden, y el directorio actual no se busca para órdenes.

YAML trata la barra invertida como un escape dentro de un escalar entre comillas dobles y como un carácter corriente en cualquier otro sitio. Así que una ruta de Windows entre comillas dobles no es la ruta que escribiste, y normalmente tampoco es YAML válido:

state_file: "C:\ghchronicle\state.json" # ghchronicle: config.yaml: yaml: line N: found unknown escape character
state_file: C:\ghchronicle\state.json # correcto, escalar simple
state_file: 'C:\ghchronicle\state.json' # correcto, comillas simples
state_file: C:/ghchronicle/state.json # correcto, y el que conviene

Las barras normales son la respuesta más simple: Windows las acepta en una ruta, y sobreviven a entrecomillarlas de cualquiera de las maneras.

Windows PowerShell 5.1, el que viene de serie, no escribe ninguna de las dos cosas que quiere un analizador de YAML, y escribe una cosa equivocada distinta según cómo se lo pidas. > y Out-File producen UTF-16LE, que el analizador lee como binario. Set-Content produce la página de códigos activa del sistema, normalmente ANSI, que se analiza bien mientras el fichero sea ASCII puro y destroza el primer carácter acentuado que aparezca. PowerShell 7 usa UTF-8 sin marca de orden de bytes por omisión y no tiene ninguno de los dos problemas; en 5.1, sé explícito:

Ventana de terminal
Set-Content -Path config.yaml -Value $text -Encoding utf8

utf8 en 5.1 quiere decir UTF-8 con marca de orden de bytes, cosa que el valor no sabe expresar y de la que el cmdlet no avisa. El analizador del colector la salta, así que el fichero funciona; una herramienta que lea los primeros bytes por su cuenta puede que no.

No hay modo servicio. El colector es un programa de consola: no habla con el Administrador de control de servicios, así que registrarlo con sc.exe create produce un servicio que Windows arranca y luego abandona, informando de que «no respondió a la petición de inicio o control de manera oportuna». Existen envoltorios de servicio de terceros y este proyecto ni distribuye ni prueba ninguno.

La forma corriente de ejecutar algo desatendido en Windows es una tarea programada, y tiene dos formas.

El equivalente de cron en Windows, y el que conviene: no hay nada que parar, y una ejecución perdida cuesta una pasada.

Ventana de terminal
$action = New-ScheduledTaskAction `
-Execute "C:\Program Files\ghchronicle\ghchronicle.exe" `
-Argument '-config "C:\ProgramData\ghchronicle\config.yaml" -once'
$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) `
-RepetitionInterval (New-TimeSpan -Hours 1) `
-RepetitionDuration ([TimeSpan]::MaxValue)
Register-ScheduledTask -TaskName ghchronicle -Action $action -Trigger $trigger

-RepetitionDuration ([TimeSpan]::MaxValue) es lo que deja escrito «para siempre». La regla del propio Programador de tareas es que una repetición sin duración se repite indefinidamente, así que omitirlo no es un error, pero el cmdlet no tiene valor propio por omisión y la tarea queda registrada sin ninguna duración.

Esta forma trae dos condiciones, y ninguna es la de cron. Una tarea horaria da a todas las familias una cadencia horaria como mucho, así que el ritmo de quince minutos de actions se pierde, que es el mismo cambio que hace cron en Linux. Y aquí Register-ScheduledTask no nombra ni -User ni -Principal, así que la tarea queda registrada con la cuenta que la crea y con el tipo de inicio de sesión por omisión, y corre solo mientras esa cuenta tenga la sesión iniciada. Un trabajo de cron no se para al cerrar sesión. Para que esta se comporte igual, regístrala con una entidad de seguridad con «ejecutar tanto si el usuario inició sesión como si no» marcado, que es New-ScheduledTaskPrincipal.

Ventana de terminal
Start-ScheduledTask -TaskName ghchronicle
Get-ScheduledTaskInfo -TaskName ghchronicle # última ejecución, último resultado

Stop-ScheduledTask termina el proceso en vez de pedirle que acabe: es taskkill /F con otro nombre, y no cierra ningún destino al salir. En la forma con temporizador no hay nada que parar, que es buena parte de por qué es la mejor opción por omisión.

Go 1.27.1 o más nuevo, que es la versión que declara go.mod. CGO_ENABLED=0 quiere decir sin MSVC, sin MinGW y sin SDK de Windows.

Ventana de terminal
go install github.com/jmrplens/ghchronicle/cmd/ghchronicle@latest

Cae en $(go env GOPATH)\bin, que es %USERPROFILE%\go\bin salvo que lo hayas movido, y ese directorio tiene que estar en tu Path. Un binario hecho así informa de su versión pero no de su commit ni de su fecha de compilación, porque esas dos las sella la compilación de release y un módulo descargado por el proxy no lleva copia de trabajo de donde leerlas.

El colector no busca el fichero de configuración en ningún sitio concreto: -config vale por omisión config.yaml relativo al directorio de trabajo, y detrás no hay ninguna ruta de búsqueda. El directorio de trabajo de una tarea programada no es algo en lo que apoyarse, así que da todas las rutas del fichero en absoluto.

FicheroPara una tarea de máquinaPara una cuenta
ConfiguraciónC:/ProgramData/ghchronicle/${LOCALAPPDATA}/ghchronicle/
Estado y registroC:/ProgramData/ghchronicle/${LOCALAPPDATA}/ghchronicle/
LogC:/ProgramData/ghchronicle/${LOCALAPPDATA}/ghchronicle/

${LOCALAPPDATA} se escribe así porque ${VAR} es la única forma que el colector expande, desde el entorno, al arrancar el proceso. %VAR% es una notación del intérprete y para el fichero no significa nada: un %LOCALAPPDATA% copiado dentro del YAML te da un directorio llamado literalmente %LOCALAPPDATA%, al lado de donde la tarea estuviera trabajando. Son %LOCALAPPDATA% en cmd y $env:LOCALAPPDATA en PowerShell los que crean el directorio en primer lugar.

Un directorio bajo C:\ProgramData lo puede escribir quien lo creó y lo puede leer todo el mundo, así que créalo elevado y luego concede escritura a la cuenta con la que corre la tarea. El fichero de estado y su registro de escrituras son los dos que el colector reescribe en cada pasada; si uno de ellos está marcado como de solo lectura el colector quita ese atributo él mismo, porque NTFS se niega a reemplazar un fichero de solo lectura incluso cuando se le pide reemplazarlo, y una pasada no debería fallar por una propiedad de fichero.