Ir al contenido

Publicación de versiones

Una versión se publica al subir una etiqueta v* sobre un commit de main; nada más publica. Hasta entonces el mecanismo de publicación está inactivo: se puede ensayar en GitHub sin publicar nada, y todo salvo la firma y las atestaciones se puede comprobar en local sin credenciales.

Ventana de terminal
make release-check # goreleaser check, actionlint, comprobación de fijaciones y de avisos
make release-snapshot RELEASE_TAG=v0.1.0 # todos los artefactos en dist/, nada firmado ni publicado
make release-preflight # lo que decide preflight para la versión de VERSION (REHEARSAL=true: como en un ensayo)
make image-smoke # la imagen de contenedor para linux/amd64, pasada por la prueba de humo
make check-server-json # server.json frente al esquema y las reglas del registro (sin red)
make check-mcpb # los manifiestos de la extensión frente al esquema MCPB, y los dos scripts
make mcpb # los paquetes .mcpb en dist/assets, tras make release-snapshot
make check-npm # el lanzador npm, las listas de plataformas y los dos scripts npm (sin red)
make npm # los paquetes npm en dist/npm, validados y ejecutados desde sus tarballs
make check-pypi # la línea del README de PyPI, los dos scripts de wheels y el flujo (sin red)
make pypi # las wheels de PyPI en dist/pypi, validadas, instaladas en un venv y ejecutadas
make check-nuget # la línea del README de NuGet, los dos scripts de paquetes y el flujo (sin red)
make nuget # los paquetes de NuGet en dist/nuget, validados, instalados con dotnet tool y dnx y ejecutados
make check-homebrew # el generador de la fórmula, el ensayo del tap y el flujo (sin red)
make homebrew # la fórmula en dist/homebrew; con Docker, brew style, audit, install y test
make check-winget # los manifiestos de winget contra sus esquemas, y el flujo (sin red)
make winget # los manifiestos de winget de la instantánea en dist/winget, contra los esquemas

release-check y release-snapshot necesitan GoReleaser y syft en el PATH, en las versiones que fija .github/workflows/release.yml (mk/release.mk nombra las mismas, y un test comprueba que ambas listas coinciden); actionlint es opcional.

.github/workflows/release.yml se ejecuta al subir una etiqueta v* o al lanzarlo a mano:

TrabajoQué hace
preflightDetermina la versión: la etiqueta subida o, al lanzarlo a mano, el fichero VERSION (la entrada opcional tag debe coincidir con él). scripts/release-order.sh rechaza todo lo que no sea un vMAYOR.MENOR.PARCHE simple, un VERSION que no coincida y una etiqueta cuya versión no tenga sección en la página del registro de cambios en inglés o en español (un encabezado ## vX.Y.Z, seguido opcionalmente de la fecha: ## v0.1.0 (2026-11-20); en un ensayo, la falta de sección es un aviso y la ejecución sigue), y decide si la etiqueta es la versión estable más reciente (una inferior es una corrección de una rama anterior). Una etiqueta subida debe apuntar a un commit de main.
gateLlama a ci.yml sobre el commit etiquetado: tests y cobertura, e2e de transporte, código generado y paridad, compilación cruzada, lint y govulncheck.
buildGoReleaser (.goreleaser.yml) construye todos los artefactos en una versión en borrador y firma checksums.txt; después el trabajo verifica esa firma, comprueba que el binario informa de la versión publicada, atestigua la procedencia de cada artefacto y los guarda como el artefacto de flujo release-dist. También empaqueta las extensiones de Claude Desktop a partir de los binarios sueltos, las adjunta al borrador y atestigua su procedencia; después escribe las dos atestaciones en arcane-mcp.intoto.jsonl, comprueba que nombra exactamente los ficheros que publica la versión, verifica cada fichero contra él y lo adjunta al borrador (ver Verificar una versión).
reproduceRecompila cada binario desde una descarga limpia del commit que compiló build (historial completo, la cadena de herramientas de go.mod, sin caché de Actions, los artefactos descargados fuera de la copia) con la receta que declara .goreleaser.yml, monta el binario universal de macOS con las dos recompilaciones de darwin en el orden de las porciones publicado, y compara los siete byte a byte con checksums.txt (cmd/reproduce_release). Se ejecuta también en un ensayo; publish lo espera.
publishEl último paso: convierte el borrador en la versión publicada, marcada como Latest solo cuando preflight dice que es la estable más reciente.
publish-imageDespués de publish: publish-image.yml construye, prueba, sube, firma y atestigua la imagen de contenedor (ver Imágenes de contenedor).
publish-npmDespués de publish: publish-npm.yml construye, valida y publica los paquetes npm (ver npm).
publish-pypi, upload-pypiDespués de publish: publish-pypi.yml construye y valida las wheels de PyPI, y upload-pypi las sube (ver PyPI).
publish-nuget, upload-nugetDespués de publish: publish-nuget.yml construye y valida los paquetes de NuGet, y upload-nuget los atestigua y los sube (ver NuGet).
verify-publishedDespués de todos los que publican: vuelve a leer cada canal y lo contrasta con el checksums.txt firmado, sin permisos de escritura (ver Verificar lo que se publicó).
publish-homebrewDespués de verify-published, solo la versión más reciente: publish-homebrew.yml confirma la fórmula en el tap jmrplens/homebrew-tap (ver Homebrew).
publish-wingetDespués de verify-published: publish-winget.yml valida los manifiestos de winget y abre la pull request de la versión en microsoft/winget-pkgs (ver winget).
publish-registryDespués de verify-published: publish-registry.yml sella server.json y lo publica en el MCP Registry (ver MCP Registry).

El borrador oculta las versiones a medias: ni instaladores ni gestores de paquetes ven una versión cuyos artefactos aún se están subiendo y, con las versiones inmutables, una versión se publica una sola vez y completa. Por eso cualquier trabajo que añada un artefacto a la versión (los paquetes .mcpb, por ejemplo) debe terminar antes de publish.

Lanza el flujo a mano (Actions → Release → Run workflow, o gh workflow run release.yml --ref <rama>). Un lanzamiento manual es siempre un ensayo: ejecuta preflight, gate y build sobre la referencia elegida, con la versión que indica VERSION, en el entorno release-rehearsal. Una página del registro de cambios sin la sección de la versión solo produce un aviso aquí, para poder ensayar main antes de elegir la versión, pero la etiqueta se rechazaría. GoReleaser se ejecuta como instantánea con --skip=publish,sign, así que no hay versión en GitHub, ni etiqueta, ni firma o atestación en un registro de transparencia público, ni subida de imagen; publish se omite, publish-image construye y prueba la imagen sin iniciar sesión en ningún registro, publish-npm construye, valida y empaqueta los paquetes npm, publish-pypi y publish-nuget construyen y validan las wheels y los paquetes de NuGet (publish-pypi ejecuta además el script de subida a PyPI con --dry-run; upload-pypi y upload-nuget se omiten), verify-published vuelve a leer los paquetes de la propia ejecución desde sustitutos locales de los registros, publish-homebrew confirma la fórmula en un clon local del tap y la muestra, publish-winget valida los manifiestos y ejecuta komac en seco, y publish-registry sella server.json con los paquetes del ensayo y pide al registro que lo valide (solo lectura). Los artefactos quedan siete días en el artefacto release-dist de la ejecución, y el resumen del trabajo los lista con su tamaño.

Cada canal de distribución (imágenes de contenedor, MCP Registry y .mcpb, npm, PyPI, NuGet, Homebrew, winget) es un flujo reutilizable propio, .github/workflows/publish-<canal>.yml con on: workflow_call, llamado desde release.yml con las entradas release_tag, version, newest, rehearsal y artifact. Todos existen ya; PyPI y NuGet suben desde un trabajo del propio release.yml, porque su publicación de confianza rechaza el token de un flujo reutilizable. Homebrew y winget anuncian una versión en repositorios ajenos a este; cuando exista una verificación posterior a la publicación de todos los canales (verify-published), ambos esperarán a ella. Los artefactos de la propia versión (los paquetes .mcpb) los adjunta build, antes de publish, porque una versión inmutable no admite ninguno después; los canales van después de publish y, en un ensayo, toman su entrada del artefacto release-dist y no publican nada.

ArtefactoQué es
arcane-mcp-<so>-<arquitectura> (.exe en Windows)Binarios sueltos para linux, darwin y windows en amd64 y arm64, los ficheros que consumen los gestores de paquetes
arcane-mcp-darwin-universalUn único binario de macOS para ambas arquitecturas
arcane-mcp_<versión>_<so>_<arquitectura>.tar.gz (.zip en Windows)Archivos con el binario, LICENSE, README.md y THIRD_PARTY_NOTICES
<binario>.sbom.jsonSBOM SPDX de cada binario suelto (syft, con las licencias leídas de la caché de módulos)
THIRD_PARTY_NOTICESTextos de licencia, aviso y patentes de la biblioteca estándar de Go y de cada módulo que enlazan los binarios
checksums.txtSHA-256 de todos los artefactos anteriores
checksums.txt.sigstore.jsonFirma cosign sin claves de checksums.txt (paquete Sigstore: firma y certificado)
arcane-mcp-{darwin,windows,linux}.mcpb, arcane-mcp.mcpbExtensiones de Claude Desktop, una por sistema y una para los tres (ver Extensión de Claude Desktop); no figuran en checksums.txt (se construyen después de que GoReleaser lo firme), las cubre su propia atestación de procedencia y, a las tres por sistema, el SHA-256 de server.json
arcane-mcp.intoto.jsonlLa procedencia de compilación SLSA de todos los ficheros anteriores como artefacto de la versión: una línea por atestación (los ficheros de checksums.txt y después las extensiones), cada una el paquete Sigstore que escribió el paso de atestación, en la forma que lee gh attestation verify --bundle

Los binarios son estáticos (CGO_ENABLED=0), se compilan con -trimpath y llevan la versión, el commit abreviado y la fecha del commit (arcane-mcp --version), con las fechas de fichero fijadas a la del commit. Dos compilaciones del mismo commit dan binarios, archivos y avisos idénticos byte a byte. Cada ejecución de publicación lo demuestra para los binarios: el trabajo reproduce los recompila desde una descarga limpia y compara cada uno con checksums.txt (go run ./cmd/reproduce_release -published <directorio con checksums.txt y el binario universal> -tag vX.Y.Z hace lo mismo desde cualquier copia limpia del commit, con la cadena de herramientas que selecciona go.mod; un fichero sin seguimiento en la copia hace que Go marque vcs.modified=true y que todos los binarios difieran. El commit abreviado que llevan los binarios es el %h de git, cuya longitud crece con el número de objetos del repositorio, así que una copia posterior puede abreviar el mismo commit de otra forma: el comando encuentra la longitud de la versión recompilando el primer binario con el commit abreviado de 7 a 16 caracteres hasta que uno coincide, y solo puede coincidir un prefijo del commit de la copia. En una etiqueta, el trabajo verifica antes checksums.txt contra su firma cosign). El binario universal se monta como lo dispone GoReleaser (una cabecera fat big-endian y cada porción en el siguiente múltiplo de 2^14), en el orden de porciones que lleva el publicado, así que solo tienen que coincidir las dos recompilaciones de darwin. GoReleaser compila una plataforma cada vez (--parallelism=1) porque el binario universal apila las dos compilaciones de darwin en el orden en que terminaron. Los documentos SBOM difieren en su fecha de creación y su espacio de nombres, no en su contenido.

THIRD_PARTY_NOTICES está en el repositorio y lo genera cmd/gen_third_party_notices a partir de lo que enlaza ./cmd/arcane-mcp en cada plataforma de publicación (go list -deps), no de go.mod, que también lista módulos que solo usan los tests y las herramientas. make check falla si está desactualizado; tras cambiar una dependencia, ejecuta make gen-third-party-notices.

Ventana de terminal
cosign verify-blob \
--bundle checksums.txt.sigstore.json \
--certificate-identity "https://github.com/jmrplens/arcane-mcp/.github/workflows/release.yml@refs/tags/vX.Y.Z" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
checksums.txt
sha256sum --check --ignore-missing checksums.txt
gh attestation verify arcane-mcp-linux-amd64 --repo jmrplens/arcane-mcp \
--signer-workflow jmrplens/arcane-mcp/.github/workflows/release.yml

La procedencia es además un artefacto de la versión, arcane-mcp.intoto.jsonl, así que una descarga se puede verificar contra el propio fichero de la versión en lugar del almacén de atestaciones de GitHub (sin gh auth login; la raíz de confianza pública de Sigstore se sigue descargando):

Ventana de terminal
gh attestation verify arcane-mcp-linux-amd64 --bundle arcane-mcp.intoto.jsonl --repo jmrplens/arcane-mcp \
--cert-identity "https://github.com/jmrplens/arcane-mcp/.github/workflows/release.yml@refs/tags/vX.Y.Z"

Antes de adjuntarlo, el trabajo de construcción lo contrasta con la versión (scripts/release_provenance.py): sus declaraciones deben nombrar exactamente las líneas de checksums.txt y el SHA-256 de cada .mcpb, sin que falte ni sobre ninguno, cada uno de esos ficheros debe tener ese resumen, y después cada fichero se verifica contra el paquete con la identidad del flujo de publicación en la etiqueta. Un ensayo escribe los mismos sujetos como declaraciones sin firmar y ejecuta las mismas comprobaciones de sujetos y resúmenes, así que un checksums.txt con una línea añadida o un fichero que difiere de él también falla ahí; solo las firmas esperan a una etiqueta.

Las comprobaciones anteriores prueban lo que hizo la construcción. Tras una versión real, el trabajo verify-published vuelve a leer cada canal desde donde lo obtienen los usuarios y lo contrasta con el checksums.txt firmado de la versión, en un trabajo sin permisos de escritura, sin secretos y sin identidad OIDC (solo contents: read):

  1. el checksums.txt que sirve la versión, contrastado con su firma cosign (identidad release.yml@refs/tags/vX.Y.Z): la referencia de todo lo demás;
  2. la imagen: el digest de ghcr.io/jmrplens/arcane-mcp:X.Y.Z debe llevar la firma de publish-image.yml e imprimir esta versión, y Docker Hub, cuando tenga la etiqueta, debe servir el mismo índice (el binario de la imagen se construye aparte de los binarios de la versión, así que checksums.txt no puede nombrarlo);
  3. scripts/verify_published.py, solo biblioteca estándar y anónimo:
    • versión de GitHub: cada artefacto que nombra checksums.txt, descargado y con su hash, cada paquete .mcpb comparado con la copia de la construcción, y arcane-mcp.intoto.jsonl, cuyas declaraciones deben nombrar exactamente esos ficheros con esos resúmenes (antes del script, el trabajo descarga además cada artefacto y lo verifica contra ese fichero con gh attestation verify --bundle);
    • npm: el lanzador en esta versión fija a ella los seis paquetes de plataforma, el binario y THIRD_PARTY_NOTICES de cada paquete de plataforma son los firmados, y los siete tarballs son exactamente los ficheros que validó publish-npm (su salida npm_sha256; publish-npm.sh vuelve a empaquetar para publicar, y los paquetes son reproducibles byte a byte), así que un script de instalación añadido o un cli.js cambiado fallan, con el dist.integrity (sha512) de npm contrastado con cada descarga;
    • PyPI: cada una de las seis wheels lleva el binario y los avisos firmados, y es exactamente el fichero que validó publish-pypi (su salida wheels_sha256);
    • NuGet: el puntero aparece en esta versión, cada paquete de runtime lleva el binario firmado, y los siete paquetes, quitada la firma de repositorio de nuget.org (.signature.p7s) como NuGet define un paquete sin firmar, son los ficheros que validó publish-nuget y atestiguó upload-nuget (su salida packages_sha256).

El MCP Registry, el tap de Homebrew y winget esperan a este trabajo, así que una caída de cualquier registro los retrasa a los tres hasta que se vuelva a ejecutar (a propósito: anuncian la versión solo cuando todos los canales la sirven), porque cada uno anuncia la versión a personas y servicios ajenos a este repositorio: ninguno debe nombrar paquetes que no se hayan vuelto a leer y resultado ser los bytes firmados. Eso le cuesta a la publicación en el registro el tiempo de la verificación (minutos; la validación de NuGet es lo largo), que un listado puede permitirse.

Un registro que aún no ha terminado de publicar responde 404 (nuget.org solo lista una versión tras validar su subida), así que las descargas se reintentan, con 30 segundos entre intentos, contra plazos de reloj contados desde el inicio del paso: GitHub, npm y PyPI hasta los 15 minutos, NuGet hasta los 40, y ninguna petición después de los 45, cada una con un tiempo máximo de 60 segundos, de modo que incluso un servidor que no responde acaba en la lista de problemas dentro de los 50 minutos que puede durar el trabajo. Un digest que no coincide nunca se reintenta: el trabajo falla en el acto y nombra el canal, el fichero y los dos digests.

Un fallo significa que un canal sirve algo distinto de lo que firmó la versión, o que nunca lo sirvió. Nada anuncia la versión (el registro, el tap y winget se omiten), pero lo ya publicado se queda: una versión equivocada de npm, PyPI o NuGet no se puede sustituir, solo marcar como obsoleta, retirar u ocultar, y el arreglo es una versión nueva. Un fallo que solo dice que una descarga nunca volvió (la espera agotada) es retraso o una caída: vuelve a ejecutar los trabajos fallidos desde la página de la ejecución, lo que repite verify-published y después los trabajos que esperan a él. El script también se ejecuta a mano cuando se quiera, contra cualquier versión publicada: python3 scripts/verify_published.py X.Y.Z checksums.txt --retry-budget 0 (el paso de la imagen es del flujo).

En un ensayo no se publica nada, así que el trabajo sirve los paquetes de la propia ejecución (los artefactos release-dist, npm-packages, pypi-wheels y nuget-packages) desde sustitutos locales de los registros (scripts/fake_registries.py, que añade una firma de repositorio a cada paquete de NuGet como hace nuget.org) y ejecuta la misma comparación contra el checksums.txt de la instantánea; la imagen y la firma de las sumas no se comprueban, y lo dice. En local, make verify-published hace lo mismo tras make release-snapshot, make npm, make pypi, make nuget y make mcpb; make check-verify-published ejecuta el script contra paquetes sustitutos construidos con los scripts de construcción reales (parte de go test ./...).

publish-image.yml publica ghcr.io/jmrplens/arcane-mcp y, cuando existen los secretos DOCKERHUB_USERNAME y DOCKERHUB_TOKEN, docker.io/jmrplens/arcane-mcp (sin ellos Docker Hub se omite con un aviso). La imagen es el binario estático sobre gcr.io/distroless/static-debian12:nonroot, construido desde el Dockerfile para linux/amd64 y linux/arm64, con la licencia y THIRD_PARTY_NOTICES en /usr/share/licenses/arcane-mcp/, las etiquetas OCI e io.modelcontextprotocol.server.name=io.github.jmrplens/arcane-mcp. Su binario lleva los mismos datos que los binarios de la versión (arcane-mcp --version da la versión sin la v, el commit abreviado y la fecha del commit).

EtiquetaCuándo
X.Y.ZEn cada versión
X.Y, latestSolo en la versión estable más reciente; una corrección de una rama anterior no mueve ninguna

Dos trabajos, para que el que ejecuta las herramientas de construcción nunca tenga la identidad de firma y una firma fallida se reintente sobre el digest ya subido sin reconstruir:

  • build: cada plataforma se construye, se carga y se pasa por scripts/smoke-test-image.sh (arm64 con QEMU); después una única construcción multiarquitectura, con el SBOM y la procedencia de BuildKit dentro del índice, se sube a ambos registros a la vez, de modo que el digest es el mismo en los dos. syft escribe un SBOM SPDX por plataforma, y el trabajo falla si no nombra el servidor como v<versión>. En la versión más reciente, la descripción de Docker Hub se sustituye por docker/README.md.
  • sign-attest: firma sin claves con cosign el índice y sus manifiestos de plataforma en ambos registros, y atestigua la procedencia SLSA y los SBOM del índice y de ambas plataformas, adjuntos en ghcr.io y guardados en el almacén de atestaciones de GitHub.

La construcción de la imagen conserva -ldflags en la información de compilación del binario (sin -trimpath), porque ahí leen los analizadores de SBOM la versión del servidor: el contexto de construcción no incluye .git y, con -trimpath, syft da el módulo como UNKNOWN.

La prueba de humo no necesita Arcane ni credenciales. Para cada imagen comprueba --version, las etiquetas y el usuario sin privilegios, la versión en la información de compilación, los ficheros de licencia, un initialize y un tools/list de MCP por stdio, y /healthz más un initialize por Streamable HTTP. docker-ci.yml ejecuta hadolint y la prueba de humo para ambas plataformas en cada pull request que toca la imagen; en local, make image-smoke (añade IMAGE_PLATFORM=linux/arm64 donde QEMU esté registrado).

Ventana de terminal
cosign verify ghcr.io/jmrplens/arcane-mcp:X.Y.Z \
--certificate-identity "https://github.com/jmrplens/arcane-mcp/.github/workflows/publish-image.yml@refs/tags/vX.Y.Z" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"
gh attestation verify oci://ghcr.io/jmrplens/arcane-mcp:X.Y.Z --repo jmrplens/arcane-mcp

El certificado nombra publish-image.yml, no release.yml: un flujo reutilizable firma con su propia identidad.

Distroless no tiene shell ni cliente HTTP, así que el HEALTHCHECK de la imagen ejecuta el propio binario: arcane-mcp --probe pide /healthz al servidor que describe el entorno del contenedor, y un contenedor stdio se da por sano (consulta Escucha, sondas y parada).

scripts/build-mcpb.sh empaqueta cuatro paquetes .mcpb a partir de los binarios sueltos de la versión, mcpb/manifest.json, mcpb/platform.jq, mcpb/linux/launch.sh, el icono de 512×512 (site/public/logo-512.png), LICENSE y THIRD_PARTY_NOTICES:

PaqueteContiene
arcane-mcp-darwin.mcpbel binario universal de macOS
arcane-mcp-windows.mcpbel ejecutable de Windows amd64 (Windows sobre Arm lo ejecuta emulado)
arcane-mcp-linux.mcpblaunch.sh y los dos binarios de Linux; el lanzador elige uno según uname -m
arcane-mcp.mcpblos tres, una sola descarga para cualquier sistema

Un manifiesto elige su orden solo por sistema operativo, así que el manifiesto de cada paquete por sistema (derivado por mcpb/platform.jq) lista solo su plataforma, y Claude Desktop lo rechaza en otro sistema con un mensaje claro. Los ajustes de la extensión son la URL y la clave de API de Arcane (obligatorias; la clave es un campo sensible), el entorno por defecto, la superficie de herramientas, los modos de solo lectura y seguro, omitir la verificación TLS, el nivel de registro y los iconos; cada uno llega al servidor como su variable de entorno y cada uno tiene un valor por defecto, porque el servidor toma una superficie o un entorno vacíos como un error. Las entradas se escriben en un orden, con unos permisos y unas fechas fijos, de modo que los mismos binarios dan paquetes idénticos byte a byte; después el script relee cada archivo y comprueba sus entradas, los bits de ejecución y cada ruta que nombra su manifiesto. make check-mcpb valida el manifiesto universal y los tres derivados frente al esquema de manifiesto MCPB (v0.3, incluido en test/release/testdata), comprueba cada ajuste frente a server.json y ejecuta ambos scripts con binarios de prueba.

npx -y @jmrp.io/arcane-mcp ejecuta el servidor sin instalar nada más. El lanzador @jmrp.io/arcane-mcp (npm/arcane-mcp: package.json, cli.js, README.md) depende de seis paquetes por plataforma como dependencias opcionales, y npm instala solo el que coincide en os y cpu:

PaqueteBinario
@jmrp.io/arcane-mcp-linux-x64, -linux-arm64arcane-mcp-linux-amd64, -arm64
@jmrp.io/arcane-mcp-darwin-x64, -darwin-arm64arcane-mcp-darwin-amd64, -arm64
@jmrp.io/arcane-mcp-win32-x64, -win32-arm64arcane-mcp-windows-amd64.exe, -arm64.exe

No se ejecuta nada al instalar y no se descarga nada más que los paquetes. cli.js localiza el paquete de la plataforma, ejecuta su binario heredando la entrada y la salida estándar, le reenvía SIGTERM, SIGINT y SIGHUP (y lo detiene si muere el shell de npx) y termina con su código de salida o su señal; en una plataforma sin paquete indica cuáles existen. Los binarios son estáticos, así que los paquetes de Linux funcionan también con musl (Alpine). El lanzador lleva mcpName: io.github.jmrplens/arcane-mcp, que el MCP Registry comprueba antes de aceptar la entrada npm de server.json.

publish-npm.yml:

  1. toma los binarios sueltos del artefacto release-dist y, fuera de un ensayo, verifica la firma cosign de checksums.txt;
  2. construye los siete paquetes con scripts/build-npm.mjs, que contrasta cada binario y THIRD_PARTY_NOTICES con checksums.txt y estampa la versión en una copia del lanzador del repositorio;
  3. los valida con scripts/validate-npm.mjs: nombres, una sola versión, MIT, la URL del repositorio que exige la publicación de confianza, os/cpu, los binarios y su bit de ejecución, el bin, el mcpName y las seis versiones fijadas del lanzador, la lista de ficheros de cada tarball, tarballs idénticos byte a byte en dos empaquetados y, desde una instalación sin red de los siete tarballs, npx arcane-mcp --version y un initialize y un tools/list de MCP a través del lanzador;
  4. los publica con scripts/publish-npm.sh: primero los seis paquetes por plataforma y al final el lanzador, con --provenance, bajo la etiqueta latest en la versión más reciente y release-X.Y en una corrección de una rama anterior; una versión que ya está en npm se omite, así que repetir la ejecución completa una publicación a medias. Un ensayo solo empaqueta.

La publicación usa la publicación de confianza de npm: no existe ningún token de npm. npm contrasta el token OIDC del trabajo con el publicador de confianza de cada paquete, que nombra el flujo que llama (release.yml, no publish-npm.yml) y el entorno release. Necesita npm 11.5.1 o posterior (el flujo instala una 12.x fijada, comprobada contra el SHA-512 que publica el registro) y un repositorio público para la procedencia.

Solo se puede configurar un publicador de confianza en un paquete que ya existe, así que los siete nombres se crean una vez con paquetes provisionales (0.0.0-bootstrap.0: un README y la licencia, sin binario), y la primera versión real, con procedencia, la publica el flujo. Desde una copia de main, con sesión iniciada en npm como dueño del ámbito @jmrp.io:

Ventana de terminal
npm login
node scripts/build-npm.mjs --bootstrap --out /tmp/arcane-mcp-npm-bootstrap
node scripts/validate-npm.mjs /tmp/arcane-mcp-npm-bootstrap
bash scripts/publish-npm.sh /tmp/arcane-mcp-npm-bootstrap 0.0.0-bootstrap.0 --tag bootstrap

Después, para cada uno de los siete paquetes (@jmrp.io/arcane-mcp y @jmrp.io/arcane-mcp-{linux-x64,linux-arm64,darwin-x64,darwin-arm64,win32-x64,win32-arm64}), en npmjs.com → el paquete → Settings:

  1. Trusted publisher → GitHub Actions: organización o usuario jmrplens, repositorio arcane-mcp, nombre del flujo release.yml, entorno release, acción permitida npm publish.
  2. Publishing access: exigir autenticación en dos pasos y no admitir tokens, de modo que el flujo sea la única forma de publicar.

npm puede apuntar latest al paquete provisional hasta que la primera versión lo mueva. Tras esa versión, retira los provisionales:

Ventana de terminal
for p in arcane-mcp arcane-mcp-linux-x64 arcane-mcp-linux-arm64 arcane-mcp-darwin-x64 arcane-mcp-darwin-arm64 arcane-mcp-win32-x64 arcane-mcp-win32-arm64; do
npm deprecate "@jmrp.io/$p@0.0.0-bootstrap.0" "placeholder; install a released version"
npm dist-tag rm "@jmrp.io/$p" bootstrap
done

uvx arcane-mcp ejecuta el servidor sin instalar nada más (igual que pipx run arcane-mcp, uv tool install arcane-mcp y pip install arcane-mcp). El proyecto de PyPI es arcane-mcp; el nombre estaba libre cuando se escribió esto, pero un publicador de confianza pendiente no lo reserva. Cada una de las seis wheels lleva un binario de la versión en su directorio .data/scripts, como distribuyen los suyos ruff, uv y ziglang: el instalador lo coloca en la ruta de scripts del entorno con el bit de ejecución, así que la orden arcane-mcp es el propio binario de la versión, byte a byte, sin ningún proceso de Python en medio (su stdio, sus señales y su código de salida son los suyos). No hay punto de entrada de consola (chocaría con el binario, que ya tiene el nombre de la distribución, el que busca uvx arcane-mcp). Un pequeño módulo arcane_mcp añade python -m arcane_mcp (que ejecuta el binario en lugar del proceso o, en Windows, lo lanza y devuelve su código de salida) y arcane_mcp.find_binary().

Etiqueta de plataforma de la wheelBinario
manylinux_2_17_x86_64.manylinux2014_x86_64.musllinux_1_1_x86_64 (y su gemela aarch64)arcane-mcp-linux-amd64 (-arm64)
macosx_13_0_x86_64, macosx_13_0_arm64arcane-mcp-darwin-amd64, -arm64
win_amd64, win_arm64arcane-mcp-windows-amd64.exe, -arm64.exe

Los binarios de Linux son estáticos (sin intérprete de programa ni sección dinámica), así que una sola wheel con un conjunto de etiquetas comprimido sirve a sistemas con glibc y con musl (Alpine), como hace ziglang; manylinux_2_17 (manylinux2014) es la política manylinux más antigua que sigue mantenida; el binario estático no necesita nada de glibc. La etiqueta de macOS es el sistema mínimo que Go escribe en los binarios (macOS 13), que la validación vuelve a leer de cada binario, de modo que una actualización de Go que lo suba hace fallar la validación en lugar de publicar una wheel que promete demasiado. No hay distribución de código fuente: construirla supondría compilar el binario de Go, cosa que un instalador de Python no puede hacer, y ni PyPI ni el MCP Registry la necesitan (el registro lee la línea mcp-name: io.github.jmrplens/arcane-mcp de la descripción de la versión, que las wheels llevan desde pypi/README.md).

publish-pypi.yml:

  1. toma los binarios sueltos del artefacto release-dist y, fuera de un ensayo, verifica la firma cosign de checksums.txt;
  2. construye las seis wheels con scripts/build_pypi.py (solo biblioteca estándar), que contrasta cada binario y THIRD_PARTY_NOTICES con checksums.txt y escribe las entradas en un orden fijo, con fechas y permisos fijos, de modo que los mismos binarios dan wheels idénticas byte a byte;
  3. las valida con scripts/validate_pypi.py: los seis nombres de fichero y sus etiquetas, las entradas, sus permisos y el RECORD, los metadatos (2.4, License-Expression: MIT, LICENSE y THIRD_PARTY_NOTICES como License-File, las URL de incidencias y de seguridad, pypi/README.md como descripción), cada binario frente a checksums.txt y frente a su etiqueta (formato, CPU, enlazado estático en Linux, macOS mínimo), una segunda construcción idéntica byte a byte, twine check --strict si twine está instalado y, por último, la wheel del sistema instalada en un entorno virtual nuevo con pip --no-index: la orden instalada debe ser el binario de la versión, y arcane-mcp --version, python -m arcane_mcp --version y un initialize y un tools/list de MCP deben responder (además de uvx --offline --from <wheel> arcane-mcp --version si uv está en el PATH);
  4. guarda sus SHA-256 como la salida wheels_sha256 del flujo;
  5. ejecuta la primera mitad del script de subida, scripts/upload-pypi.sh dist/pypi/wheels <versión> --dry-run, en todas las ejecuciones, ensayos incluidos: instala twine y pypi-attestations desde el fichero bloqueado con hashes .github/requirements/pypi-upload.txt y ejecuta twine check --strict sobre las seis wheels, de modo que el bloqueo y la comprobación se ponen a prueba antes de que una etiqueta los necesite;
  6. entrega las wheels como el artefacto pypi-wheels. No las sube.

La publicación de confianza de PyPI no acepta un token emitido dentro de un flujo reutilizable (pypa/gh-action-pypi-publish#166; pypa aconseja dejar el trabajo de publicación en el flujo de nivel superior), así que release.yml sube las wheels en su propio trabajo upload-pypi, que solo tiene id-token: write (y contents: read, para el script y su fichero bloqueado), solo se ejecuta al subir una etiqueta, en el entorno release, descarga la etiqueta sin credenciales, contrasta el artefacto descargado con los resúmenes de wheels_sha256 mediante sha256sum -c (cualquier trabajo de la ejecución que tenga el token de runtime podría sustituir un artefacto, pero no la salida de otro trabajo, así que lo que se sube es lo que se validó) y ejecuta scripts/upload-pypi.sh. El script exige que el directorio contenga exactamente las seis wheels de la versión, instala twine y pypi-attestations en un entorno virtual desechable del python3 del runner desde .github/requirements/pypi-upload.txt (--require-hashes --only-binary :all:: nada se resuelve ni se compila al ejecutar), firma una atestación de publicación PEP 740 por wheel con la identidad del flujo, cambia después el token OIDC del trabajo por un token de subida de corta duración como documenta PyPI la publicación de confianza a mano (https://pypi.org/_/oidc/audience y luego https://pypi.org/_/oidc/mint-token) y sube wheels y atestaciones con twine upload --skip-existing --attestations (repetir la ejecución completa una subida a medias). Sustituyó a pypa/gh-action-pypi-publish, cuyo paso compuesto ejecuta una imagen de contenedor nombrada por una etiqueta de registro (docker://ghcr.io/pypa/gh-action-pypi-publish:<ref>), que el SHA fijado de la acción no fija. No existe ningún token de PyPI. El fichero bloqueado se compila con pip-compile --generate-hashes (su cabecera nombra la orden), así que la entrada pip de Dependabot para /.github/requirements lo vuelve a compilar tras su periodo de espera de 7 días, con las dependencias transitivas y sus hashes. Para mover una versión a mano, edita .github/requirements/pypi-upload.in y vuelve a compilarlo con Python 3.12 y pip 26 o posterior, con PIP_UPLOADED_PRIOR_TO en siete días antes del día en que lo ejecutes (el periodo de espera de las dependencias), como muestra el fichero .in. Los artefactos de la ejecución duran siete días: repetir upload-pypi después exige repetir también publish-pypi, como ocurre con release-dist en todos los canales. PyPI no tiene etiquetas de distribución: pip instala la versión más alta, así que una corrección de una rama anterior nunca desplaza a la versión más reciente.

make pypi, después de make release-snapshot RELEASE_TAG=v$(cat VERSION), construye y valida las wheels a partir de la instantánea igual que el flujo (necesita python3; la prueba de humo ejecuta la wheel de la máquina en la que corre).

El proyecto aún no existe, así que su publicador de confianza se crea como publicador pendiente, que crea el proyecto en la primera subida: sin token y sin primera subida a mano. En pypi.org, con la sesión del propietario (PyPI exige autenticación en dos pasos): Account → Publishing → Add a new pending publisher → GitHub, con nombre del proyecto en PyPI arcane-mcp, propietario jmrplens, repositorio arcane-mcp, nombre del flujo release.yml y entorno release. Un publicador pendiente no reserva el nombre: si alguien registra arcane-mcp antes, el publicador pendiente deja de valer y el canal necesitará otro nombre. Tras la primera versión, la página Publishing del proyecto muestra el publicador como activo; no crees tokens de API para el proyecto.

dnx arcane-mcp ejecuta el servidor con el SDK de .NET 10 y nada más (igual que dotnet tool install -g arcane-mcp, que deja la orden en el PATH). Los paquetes forman una herramienta de .NET cuya orden es el binario de la versión, con la estructura que usa el SDK de .NET 10 para herramientas específicas de plataforma: el paquete puntero arcane-mcp, cuyo tools/net10.0/any/DotnetToolSettings.xml (versión 2) nombra un paquete por identificador de runtime, y seis paquetes de runtime arcane-mcp.<rid>, cada uno con un binario de la versión en tools/any/<rid>/ y un fichero de configuración que lo ejecuta directamente (Runner="executable"). El SDK elige el paquete del sistema, así que la orden es el binario de la versión, byte a byte, sin ningún runtime de .NET en medio. El puntero lleva además .mcp/server.json (el tipo de paquete McpServer, con el que nuget.org muestra una configuración MCP) y nuget/README.md, con la línea mcp-name: io.github.jmrplens/arcane-mcp que el MCP Registry lee del README de la versión.

PaqueteBinario
arcane-mcp.linux-x64, arcane-mcp.linux-arm64arcane-mcp-linux-amd64, -arm64
arcane-mcp.osx-x64, arcane-mcp.osx-arm64arcane-mcp-darwin-amd64, -arm64
arcane-mcp.win-x64, arcane-mcp.win-arm64arcane-mcp-windows-amd64.exe, -arm64.exe

El SDK elige mediante el grafo de identificadores de runtime, en el que linux-musl-x64 recurre a linux-x64; los binarios de Linux son estáticos, así que Alpine recibe el paquete linux-x64 y funciona (comprobado en la imagen mcr.microsoft.com/dotnet/sdk:10.0-alpine). Cuando su entrada estándar no es un terminal, como cuando lo arranca un cliente MCP, dnx instala la herramienta sin preguntar; los argumentos para el servidor van después de -- (dnx arcane-mcp -- --version), porque dnx lee sus propias opciones en cualquier punto de la línea.

publish-nuget.yml:

  1. toma los binarios sueltos del artefacto release-dist y, fuera de un ensayo, verifica la firma cosign de checksums.txt;
  2. instala el SDK de .NET en la versión exacta que fijan los flujos, contrastado con el SHA-512 que publica Microsoft;
  3. construye los siete paquetes con scripts/build_nuget.py (solo biblioteca estándar), que contrasta cada binario y THIRD_PARTY_NOTICES con checksums.txt y escribe las entradas en un orden fijo, con fechas y permisos fijos, de modo que los mismos binarios dan paquetes idénticos byte a byte;
  4. los valida con scripts/validate_nuget.py: los siete nombres de fichero y el orden de subida, las entradas y sus permisos, las partes OPC, una estructura a la que la firma de repositorio de nuget.org se puede añadir y quitar sin tocar nada más (sin descriptores de datos, zip64 ni comentario), cada nuspec (id, versión, tipos de paquete, la expresión MIT con el licenseUrl que nuget.org exige a su lado, readme, icono, repositorio), la configuración de herramienta del puntero y de cada paquete de runtime, .mcp/server.json frente a server.json, cada binario frente a checksums.txt y frente a su identificador de runtime (formato, CPU, enlazado estático en Linux, macOS mínimo), una segunda construcción idéntica byte a byte y, por último, para la plataforma del ejecutor, dotnet tool install solo desde los paquetes (la orden instalada debe ser el binario de la versión) y dotnet dnx, que deben responder a --version y a un initialize y un tools/list de MCP;
  5. entrega los paquetes como el artefacto nuget-packages, y sus SHA-256 como la salida packages_sha256 del flujo. No los sube; un ensayo lista las subidas que haría.

La publicación de confianza de nuget.org no funciona desde un flujo reutilizable (NuGet/login#6 y, para un flujo reutilizable del mismo repositorio, NuGet/login#9: nuget.org compara la política con el job_workflow_ref del token, que nombra el flujo llamado y no release.yml; NuGetGallery#11000), así que release.yml sube los paquetes en su propio trabajo upload-nuget, que solo se ejecuta al subir una etiqueta, en el entorno release, solo tiene id-token: write y attestations: write, no descarga el repositorio y solo ejecuta acciones fijadas y el SDK fijado: contrasta el artefacto descargado con los resúmenes de packages_sha256 mediante sha256sum -c (como hace upload-pypi, así que lo que se sube es lo que se validó), atestigua los siete paquetes tal como se empaquetaron (procedencia SLSA; nuget.org añade encima una firma de repositorio, que quien verifica quita primero), cambia el token OIDC del trabajo por una clave de API de una hora con NuGet/login, sube los seis paquetes de runtime antes que el puntero que los nombra, con --skip-duplicate y la clave leída de NUGET_API_KEY en lugar de pasarla en la línea de órdenes, (repetir la ejecución completa una subida a medias), y ahí termina; después verify-published, sin permisos de escritura, espera hasta 40 minutos a que nuget.org liste la versión (valida cada subida antes, lo que tarda minutos) y comprueba lo que sirve, y el MCP Registry, que descarga el README, espera a ese trabajo. No se guarda ninguna clave de API de NuGet. Como en PyPI, repetir upload-nuget después de los siete días que duran los artefactos exige repetir también publish-nuget. NuGet no tiene etiquetas de distribución: la versión más alta es la predeterminada, así que una corrección de una rama anterior nunca desplaza a la versión más reciente.

make nuget, después de make release-snapshot RELEASE_TAG=v$(cat VERSION), construye y valida los paquetes a partir de la instantánea igual que el flujo (necesita python3 y el SDK de .NET que fijan los flujos; DOTNET=<ruta> indica uno que no esté en el PATH).

Política de publicación de confianza (una vez, a mano)

Sección titulada «Política de publicación de confianza (una vez, a mano)»

Las políticas de nuget.org pertenecen a una cuenta, no a un paquete, así que no hace falta que exista nada antes ni hay primera subida a mano. En nuget.org, con la sesión de jmrplens: tu nombre de usuario → Trusted Publishing → Create, con propietario del paquete jmrplens, propietario del repositorio jmrplens, repositorio arcane-mcp, fichero del flujo release.yml (solo el nombre) y entorno release. Una política creada mientras el repositorio es privado empieza activa solo siete días y pasa a ser permanente tras su primer inicio de sesión correcto; si pasan esos días, reactívala desde la misma página antes de etiquetar. La política permite a ese flujo subir cualquier paquete de la cuenta, y por eso nombra el entorno. No crees claves de API para estos paquetes; los identificadores arcane-mcp y arcane-mcp.<rid> estaban libres cuando se escribió esto, y nada los reserva hasta la primera versión.

brew install jmrplens/tap/arcane-mcp instala el servidor en macOS y Linux. La fórmula Formula/arcane-mcp.rb vive en el tap existente jmrplens/homebrew-tap y la escribe scripts/update-homebrew-tap.sh: para cada sistema (on_macos, on_linux) y CPU (on_arm, on_intel) descarga el binario suelto de la versión y lo fija a su SHA-256 de checksums.txt, lo instala como arcane-mcp, deja en el keg LICENSE (del árbol de la etiqueta, fijado al fichero de este repositorio) y THIRD_PARTY_NOTICES (un artefacto de la versión, fijado a su suma), y su bloque test ejecuta arcane-mcp --version. Requiere macOS 13 (Ventura) o posterior (depends_on macos: :ventura), el mínimo que Go escribe en los binarios de darwin; en Linux no hay tal requisito. Windows no tiene Homebrew; tiene winget.

publish-homebrew.yml se ejecuta solo para la versión más reciente (el tap sirve una única versión, y una corrección de una rama anterior bajaría de versión a todo brew upgrade; el script además se niega a escribir una versión anterior sobre otra más nueva). Verifica la firma cosign de checksums.txt y que es exactamente el fichero que sirve la versión, después genera la fórmula, la comprueba con ruby -c, la confirma en un clon del tap y la sube con una clave de despliegue SSH (TAP_DEPLOY_KEY_B64, en el entorno release; la clave de host Ed25519 de GitHub está fijada en el flujo, no se escanea). Sin el secreto, el trabajo avisa y no sube nada; el fichero de la clave se borra al terminar el paso, sea cual sea el resultado. El tap se comparte con las versiones de otros proyectos, así que una subida rechazada porque el tap ha avanzado se rebasa una vez sobre su main y se repite; volver a ejecutar una versión antigua después de publicada otra más nueva deja la fórmula como está y termina con un aviso. Un ensayo clona el tap en solo lectura por HTTPS, confirma en local y muestra el commit; la clave nunca le llega.

make homebrew, después de make release-snapshot RELEASE_TAG=v$(cat VERSION), genera la fórmula de la instantánea en dist/homebrew y, si hay Docker, ejecuta scripts/check-homebrew-formula.sh en la imagen homebrew/brew fijada por digest: brew style y brew audit --strict sobre la fórmula tal como la tendrá el tap, y después brew install y brew test de la misma fórmula con URL file:// a los artefactos de la instantánea, y una comprobación de que la orden instalada es el binario de la versión byte a byte (HOMEBREW_DOCKER_FLAGS pasa opciones extra a docker run, como el certificado de la CA de un proxy).

La clave de despliegue del tap (una vez, a mano)

Sección titulada «La clave de despliegue del tap (una vez, a mano)»
  1. Crea un par de claves solo para este repositorio: ssh-keygen -t ed25519 -N "" -C "arcane-mcp release" -f tap_deploy_key.
  2. En jmrplens/homebrew-tap → Settings → Deploy keys → Add deploy key: el contenido de tap_deploy_key.pub, título arcane-mcp release, con Allow write access marcado.
  3. En este repositorio → Settings → Environments → release → Add secret: TAP_DEPLOY_KEY_B64, la salida de base64 -w0 tap_deploy_key (base64 -i tap_deploy_key en macOS). Después borra los dos ficheros locales.
  4. Añade arcane-mcp a la lista de fórmulas del README.md del tap (el flujo solo escribe Formula/arcane-mcp.rb). La fórmula se sube a la rama main del tap.

winget install jmrplens.arcane-mcp instala el servidor en Windows (x64 y arm64) desde microsoft/winget-pkgs, como paquete portable: el binario suelto de la versión arcane-mcp-windows-<arch>.exe, que se ejecuta como arcane-mcp. Se envían todas las versiones estables, incluidas las correcciones de ramas anteriores: winget-pkgs conserva todas las versiones e instala la más alta.

publish-winget.yml tiene dos trabajos:

  1. manifests verifica la firma cosign de checksums.txt, genera los tres manifiestos de la versión (esquema 1.12.0, el que escribe komac) con scripts/build_winget.py, los comprueba contra los esquemas de winget incluidos en test/release/testdata/winget y los guarda como el artefacto winget-manifests;
  2. submit instala komac en una versión fijada, contrastada con su SHA-256 (komac no publica firma), sincroniza la bifurcación jmrplens/winget-pkgs con el original (komac crea su rama en ella, y una bifurcación desfasada falla) y ejecuta komac update jmrplens.arcane-mcp --version X.Y.Z --urls <las dos URL de la versión> --submit, que abre la pull request de la versión desde la bifurcación; después borra las ramas ya fusionadas de la bifurcación. Toda escritura usa WINGET_TOKEN (en el entorno release); sin él, el trabajo avisa y no envía nada.

komac update añade una versión a un paquete que winget-pkgs ya tiene, así que hasta que se fusione la primera versión el trabajo submit avisa de que falta el paquete y no envía nada; la primera versión se envía a mano (más abajo). Un ensayo genera y valida los manifiestos y, cuando el paquete ya existe, ejecuta komac update --dry-run con la última versión publicada (la ensayada no está publicada), con el token propio del trabajo. make winget genera los manifiestos de la instantánea en dist/winget y los comprueba contra los esquemas.

La primera versión de winget y el token (una vez, a mano)

Sección titulada «La primera versión de winget y el token (una vez, a mano)»
  1. Token: un token de acceso personal clásico de jmrplens con el permiso public_repo (komac crea la rama en la bifurcación y la pull request en microsoft/winget-pkgs, algo que un token de grano fino no puede hacer en un repositorio que no es suyo), guardado como WINGET_TOKEN en el entorno release. Un token así puede escribir en todos los repositorios públicos de jmrplens, y el flujo se lo entrega a komac; una cuenta de máquina dueña de la bifurcación de winget-pkgs, con su propio token, lo limitaría a la bifurcación. La bifurcación jmrplens/winget-pkgs debe existir (komac también la crea la primera vez).

  2. Primera versión, una vez publicada la primera versión en GitHub, desde cualquier máquina con komac 2.17.1 y ese token en GITHUB_TOKEN (komac pregunta lo que no puede detectar; responde que el instalador es portable, con la orden arcane-mcp):

    Ventana de terminal
    V=0.1.0 # la primera versión
    BASE=https://github.com/jmrplens/arcane-mcp/releases/download/v$V
    komac new jmrplens.arcane-mcp --version "$V" \
    --urls "$BASE/arcane-mcp-windows-amd64.exe" "$BASE/arcane-mcp-windows-arm64.exe" \
    --package-locale en-US --publisher "José M. Requena Plens" --publisher-url https://github.com/jmrplens \
    --publisher-support-url https://github.com/jmrplens/arcane-mcp/issues --author "José M. Requena Plens" \
    --package-name arcane-mcp --package-url https://docs.jmrp.io/arcane-mcp/ --moniker arcane-mcp \
    --license MIT --license-url "https://github.com/jmrplens/arcane-mcp/blob/v$V/LICENSE" \
    --copyright "Copyright (c) 2026 José M. Requena Plens" \
    --short-description "MCP server exposing every Arcane (Docker management) capability as tools" \
    --description "A Model Context Protocol server for Arcane, the Docker management UI: containers, images, Compose projects, volumes, networks, Swarm, environments, registries and templates as tools for MCP clients, over stdio or Streamable HTTP. A single static binary; configure it with ARCANE_URL and ARCANE_API_KEY." \
    --release-notes-url "https://github.com/jmrplens/arcane-mcp/releases/tag/v$V" \
    --submit

    Compara lo que muestra komac con el artefacto winget-manifests de la ejecución de la versión, que contiene los mismos campos ya comprobados contra los esquemas; o envía ese artefacto tal cual: komac submit manifests/j/jmrplens/arcane-mcp/$V desde dentro de él (antes con --dry-run). Cuando Microsoft fusione la pull request, el flujo enviará todas las versiones siguientes.

server.json es la entrada del registro io.github.jmrplens/arcane-mcp: los tres paquetes por sistema (URL del artefacto de la versión y SHA-256) la imagen en ghcr.io y docker.io, fijada por digest, el lanzador npm @jmrp.io/arcane-mcp (que se ejecuta con npx), el paquete de PyPI arcane-mcp (que se ejecuta con uvx) y la herramienta de NuGet arcane-mcp (que se ejecuta con dnx, con registryBaseUrl https://api.nuget.org/v3/index.json), todos con las mismas variables de entorno. No hay entrada remotes: las instancias de Arcane viven en redes privadas, así que no hay ningún servidor alojado al que apuntar. El fichero del repositorio es una plantilla con marcadores a cero; publish-registry.yml, solo en la versión estable más reciente:

  1. toma los paquetes del artefacto release-dist y comprueba que son los bytes publicados en la versión;
  2. obtiene el digest de ghcr.io/jmrplens/arcane-mcp:<versión>, comprueba que lleva la firma de publish-image.yml y declara Docker Hub solo si sirve el mismo índice;
  3. sella una copia con scripts/update-server-json-sha.sh, que rechaza un conjunto que no tenga exactamente un paquete por sistema o en el que quede algún marcador;
  4. comprueba el fichero sellado frente al esquema del registro (2025-12-11, incluido en el repositorio) y sus reglas (make check-server-json con SERVER_JSON);
  5. lo publica con mcp-publisher (fijado, con SHA-256 y firma verificados), iniciando sesión con el token OIDC de GitHub del flujo, que demuestra el espacio de nombres io.github.jmrplens; no interviene ningún secreto.

El registro descarga cada paquete que lista cuando acepta una versión, así que la entrada solo se puede publicar cuando el repositorio y el paquete de ghcr.io sean públicos. En un ensayo el digest es de prueba y mcp-publisher validate sustituye a la publicación. El fichero sellado se guarda como el artefacto de flujo server-json; la plantilla del repositorio no se reescribe.

Los registros y directorios (Glama, por ejemplo, que construye el Dockerfile) arrancan el servidor para listar sus herramientas sin acceso a ningún Arcane. El catálogo es estático, así que initialize, tools/list, prompts/list y resources/list responden con cualquier URL, alcanzable o no; por stdio hace falta una clave de API, pero no se comprueba hasta que se ejecuta una herramienta:

Ventana de terminal
export ARCANE_URL=http://arcane.invalid:3552 ARCANE_API_KEY=introspection
arcane-mcp # stdio
docker run -i --rm -e ARCANE_URL -e ARCANE_API_KEY ghcr.io/jmrplens/arcane-mcp:latest
ARCANE_MCP_HTTP_ADDR=:8080 arcane-mcp # Streamable HTTP en /mcp; /healthz responde ok

Una llamada a una herramienta falla entonces con un error que dice que no se puede contactar con Arcane, que es la respuesta esperada.

Acciones fijadas y el control de la cadena de suministro

Sección titulada «Acciones fijadas y el control de la cadena de suministro»

Cada uses: de .github/workflows/ indica un SHA de commit completo con la versión en un comentario (actions/checkout@<sha> # v7), de modo que mover una etiqueta no puede cambiar lo que se ejecuta en un trabajo que tiene un token de escritura o la identidad de firma; Dependabot actualiza a la vez el SHA y el comentario. cmd/audit_workflow_pins (make check-workflow-pins, parte de make check y de la CI) falla ante cualquier otra referencia, y vigila siete reglas más. Lee cada flujo con sus alias de YAML resueltos, así que un bloque permissions o env con alias cuenta como escrito.

  • Ninguna credencial olvidada. Cada actions/checkout fija persist-credentials: false. Un trabajo que de verdad publica con el token del checkout se declara en cmd/audit_workflow_pins/policy.go con su motivo; hoy no hay ninguno.
  • Periodo de espera. Cada entrada de .github/dependabot.yml espera al menos 7 días tras una versión antes de proponerla.
  • Nada de código resuelto al ejecutar en un trabajo con credenciales. Un trabajo tiene credenciales cuando su token tiene cualquier permiso en write (o write-all, o ningún bloque permissions), o cuando el trabajo o el env del flujo leen un secreto que policy.go no declara de solo lectura. Un trabajo así no ejecuta, ni en sus bloques run: ni en los scripts/*.sh que estos nombran (ni en los que nombran esos), npx, npm exec, pnpm dlx, yarn dlx ni bunx sin una versión exacta, ni @latest, ni una descarga pasada a un shell o un intérprete (por nombre o por ruta, tras sudo o env, o mediante $(…) o <(…)), ni go install o go run con algo que no sea una versión exacta, ni pip install sin --require-hashes, ni uvx o uv tool run sin una versión exacta, ni docker run, docker pull o docker create de una imagen escrita sin resumen (una imagen guardada en una $variable no se juzga), ni make, directamente o tras sh -c, bash -c, xargs o timeout (sus recetas no se siguen, así que no se pueden juzgar: un trabajo así escribe las órdenes, como hace sonar.yml con make cover, y una prueba mantiene esa copia igual a la receta del Makefile). De un paso goreleaser/goreleaser-action se lee su configuración (--config/-f en args, o los nombres de fichero propios de GoReleaser en workdir): sus ganchos y la orden de cada firma, SBOM y publicador se juzgan igual, y una configuración que no se puede leer es un hallazgo. No se leen ahí: los ganchos de universal_binaries y archives, dockers/kos (sus imágenes base) ni includes; .goreleaser.yml no usa ninguno.
  • Ninguna imagen por etiqueta en un trabajo con credenciales. Sus imágenes de container: y services: indican un resumen (nombre:etiqueta@sha256:…). docker/setup-qemu-action recibe image: y docker/setup-buildx-action un image= en driver-opts (salvo que su controlador no arranque un constructor) fijados por resumen, porque sus valores por defecto son etiquetas (tonistiigi/binfmt:latest, que se arranca con --privileged, y moby/buildkit:buildx-stable-1). Las acciones que ejecutan una imagen que su SHA fijado no fija se rechazan ahí (pypa/gh-action-pypi-publish). Dependabot no lee estas entradas: publish-image.yml y docker-ci.yml anotan la etiqueta junto al resumen, para actualizarlo a mano con docker buildx imagetools inspect <imagen>:<etiqueta>.
  • Ninguna caché de Actions en un trabajo con credenciales, hasta donde el control conoce las acciones. Cualquier ejecución del repositorio puede escribir en la caché de Actions. En un trabajo así se rechaza actions/cache (también restore y save); setup-go fija cache: false, setup-node package-manager-cache: false y ningún cache:, setup-python ningún cache:, setup-qemu-action cache-image: false y setup-buildx-action cache-binary: false; y ninguna entrada de un paso nombra una caché de BuildKit type=gha (cache-from y cache-to de build-push-action). Una caché que otra acción abra por su cuenta no se ve.
  • Formas que las demás reglas leerían mal. Un bloque permissions que no es ni un mapa ni un escalar (que se considera capaz de escribir), una clave de fusión de YAML (<<) y un alias anidado en sí mismo son hallazgos.
  • Las herramientas descargadas se fijan y se comprueban, hasta donde el control sabe leer. En todos los trabajos:
    • Un fichero que un bloque run: guarda con curl o wget debe aparecer, en el mismo paso, en una línea sha256sum/sha512sum -c o en cosign verify-blob (o gh attestation verify, minisign -V, gpg --verify).
    • Una descarga pasada por tubería a algo que no sea un lector de texto (tar, tee, un intérprete) se rechaza, porque nada puede comprobarla antes.
    • Una instalación global de npm/pnpm debe ser de un fichero local comprobado.
    • pip necesita --require-hashes. Las instalaciones con gem, cargo, pipx y uv tool se rechazan.
    • go install indica una versión exacta, que fija la base de datos de sumas de verificación de Go.
    • Los valores de env cuyo nombre acaba en _VERSION son versiones exactas.
    • Las acciones que instalan herramientas reciben una versión exacta: golangci-lint se instala con go install. setup-go y setup-node la toman de go.mod y site/.node-version, pero ninguna comprueba un hash de la herramienta que descarga.

Lo que el control no lee:

  • los objetivos de make fuera de los trabajos con credenciales (dentro, make se rechaza, salvo que se llegue a él de un modo que el control no lee, como una variable que guarda su nombre), los scripts de Node y Python de scripts/ y los scripts alcanzados mediante $(dirname "$0");
  • lo que importan los paquetes de un go run o go build (solo se juzga la versión);
  • una orden guardada en una variable o una función y ejecutada después;
  • el código de las acciones, incluidas las acciones compuestas locales (uses: ./…);
  • versiones fijadas en nombres de env que no acaban en _VERSION.

Funciona sin red. Una comprobación cuenta cuando nombra el fichero. Si la suma de verificación es la correcta, o si viene del mismo servidor que el fichero, queda para la revisión.

Las herramientas que descargan los trabajos de publicación se fijan así: GoReleaser y cosign mediante sus acciones (que verifican la descarga), syft contra su SHA-256 y sus sumas de verificación firmadas, mcp-publisher contra su SHA-256 y su firma, komac contra su SHA-256, npm contra el SHA-512 que publica el registro, y el SDK de .NET contra el SHA-512 que publica Microsoft. Dependabot no gestiona estas versiones; se actualizan a mano, con la nueva suma de verificación tomada de las que publica el propio proyecto.

La versión (VERSION contiene la candidata, 0.1.0) y si el repositorio pasa a ser público son decisiones del propietario. En GitHub:

  1. Entornos. release, con una regla de despliegue que solo admita etiquetas v*; release-rehearsal, sin secretos. Los canales posteriores añaden sus secretos y publicadores de confianza solo a release.
  2. Settings → General → Releases: activar las versiones inmutables.
  3. Rulesets: proteger las etiquetas v* contra borrado, actualización y forzado, y restringir quién puede crearlas.
  4. Actions → General: los permisos de los flujos siguen siendo de solo lectura por defecto (cada trabajo pide lo que necesita).
  5. Docker Hub (opcional): crea el repositorio jmrplens/arcane-mcp y un token de acceso con permisos de lectura, escritura y borrado, y guárdalos como DOCKERHUB_USERNAME y DOCKERHUB_TOKEN en el entorno release. Tras la primera subida, haz público el paquete arcane-mcp de ghcr.io y vincúlalo al repositorio.
  6. MCP Registry y la extensión de Claude Desktop: no hay que crear nada (el espacio de nombres lo demuestra el token OIDC del flujo), pero ambos necesitan que el repositorio y el paquete de ghcr.io sean públicos. Que la extensión aparezca en el directorio de Claude Desktop es un formulario aparte (pide una política de privacidad, que es Privacidad, y credenciales de prueba para un Arcane accesible).
  7. npm: la primera publicación, una sola vez, y el publicador de confianza de cada paquete (ver Primera publicación).
  8. PyPI: el publicador de confianza pendiente de arcane-mcp (ver Publicador de confianza). En la primera versión, la API JSON de PyPI aún puede responder 404 para el proyecto recién creado cuando se ejecuta publish-registry, que entonces falla sin reintentar; basta con volver a ejecutar publish-registry a mano una vez.
  9. NuGet: la política de publicación de confianza de la cuenta jmrplens (ver Política de publicación de confianza).
  10. Homebrew: la clave de despliegue del tap y la entrada del README (ver La clave de despliegue del tap).
  11. winget: WINGET_TOKEN y la primera versión a mano tras la primera versión publicada (ver La primera versión de winget y el token).
  12. Directorios: tras la primera versión, los listados que se envían a mano (ver Directorios y botones de instalación).
  13. Ensaya una vez desde main y revisa el artefacto; después sube VERSION en un commit de main (con los ficheros que llevan la versión: server.json, mcpb/manifest.json, npm/arcane-mcp/package.json, plugin.json, .plugin/plugin.json y lhm.plugin.json; make check nombra cualquiera que se quede atrás) que además renombre Sin publicar en la página del registro de cambios, en inglés y en español, a ## vX.Y.Z (AAAA-MM-DD) bajo una nueva sección Sin publicar vacía (preflight rechaza la etiqueta si no; make release-preflight lo muestra antes de subirla), etiqueta ese commit como vX.Y.Z y sube la etiqueta.

La firma (Sigstore) y las atestaciones de procedencia no necesitan secretos: usan la identidad OIDC del flujo (id-token: write). Ambas están pensadas para un repositorio público: las atestaciones en un repositorio privado requieren GitHub Enterprise Cloud, y una firma sin claves registra los nombres del repositorio y del flujo en el registro de transparencia público de Sigstore.