Skip to content

Publishing Packages to BatleHub

This guide walks through publishing packages to a BatleHub private registry for each supported registry type. Publishing requires the registry to be running in local or hybrid mode and a token with sufficient permissions.

1. Prerequisites

Publishing is only available when the registry is configured with mode = "local" or mode = "hybrid". In proxy mode (the default), all write requests are rejected.

ModeBehaviour
localBatleHub is the only source. No upstream needed.
hybridLocal packages take priority; unknown packages fall back to upstream.

See docs/guide/configuration.md § Registry modes for the full configuration reference.


2. Getting an API token

All publish requests require a Bearer token in the Authorization header.

Static tokens (config.toml)

The simplest option for CI pipelines or single-user setups:

toml
[[auth]]
type = "token"

[[auth.tokens]]
value   = "my-publish-token"
role    = "admin"
user_id = "ci"

User-generated API tokens (OIDC sessions)

If you use OIDC login, you can generate short-lived tokens from the Web UI (Settings → Tokens) or via the API:

sh
curl -s -X POST \
  -H "Authorization: Bearer <oidc-session-token>" \
  -H "Content-Type: application/json" \
  -d '{"name": "ci-publish", "expires_in_days": 30, "role": "user"}' \
  https://batlehub.example.com/api/v1/auth/tokens

The response contains the raw token value — save it, it is shown only once.

json
{
  "id": "...",
  "name": "ci-publish",
  "token": "bh_xxxxxxxxxxxxxxxxxxxx",
  "expires_at": "2026-06-21T00:00:00Z"
}

3. Your ecosystem's instructions

Publishing is per-ecosystem, and each ecosystem has one page. That page is the home for its server configuration, its client setup, the publish command, how to verify it worked, and its endpoint reference — this page is only the part that is the same whatever you are publishing.

CategoryRegistryPublishing instructions
Languagenpm/registries/npm
LanguageCargo/registries/cargo
LanguageGo Modules/registries/goproxy
LanguageMaven/registries/maven
LanguagePyPI/registries/pypi
LanguageConda/registries/conda
LanguageComposer/registries/composer
LanguageRubyGems/registries/rubygems
LanguageNuGet/registries/nuget
LanguageTerraform/registries/terraform
Editor extensionsOpenVSX/registries/openvsx
Editor extensionsVS Code Marketplace/registries/vscode-marketplace
Editor extensionsJetBrains Marketplace/registries/jetbrains-marketplace
OS packagesDebian / APT/registries/deb
OS packagesRPM / YUM / DNF/registries/rpm
OS packagesPacman / Arch/registries/pacman

Source forges (GitHub, GitLab, Forgejo), the JetBrains IDE mirror and the Generic mirror are proxy-only — there is nothing to publish to them.


4. Troubleshooting

403 Forbidden on publish

  • The token is missing, expired, or does not have the required role. Publish is restricted to admin role by default. Check the [registries.rbac] block — the role that should publish needs "*" (or at minimum write access).
  • Pass the token explicitly: -H "Authorization: Bearer <token>".

403 Forbidden — "registry is not in local or hybrid mode"

The registry mode is set to proxy (the default). Change it to "local" or "hybrid" in config.toml and restart the server.

409 Conflict

The version already exists in the registry. Bump the version in your package manifest and republish.

400 Bad Request (Go)

The module zip structure is invalid. Every entry inside the zip must be prefixed with {module}@{version}/. Rebuild with go mod zip to get the correct layout.

400 Bad Request (Cargo)

Cargo uses a binary wire format (length-prefixed metadata JSON followed by the .crate bytes). Only cargo publish produces this format — do not attempt to hand-craft the request.

Token accepted but cargo publish fails with "invalid token"

Cargo expects the sparse index config.json to match the token endpoint. Verify the index URL in .cargo/config.toml ends with /registry/:

sparse+https://batlehub.example.com/proxy/internal/registry/

400 Bad Request (Maven) — "POM missing groupId"

The uploaded .pom file is missing <groupId> or <artifactId>. These are required fields. Check that your pom.xml or Gradle build.gradle.kts sets group and archivesName/rootProject.name before publishing.

mvn deploy succeeds but maven-metadata.xml is not updated

BatleHub generates maven-metadata.xml dynamically from the database. A successful .pom upload (HTTP 201) means the version was recorded. If GET returns 404, the .pom upload may have failed — check the response status for each uploaded file in verbose output (mvn deploy -X).

Terraform terraform init fails — "registry does not have a provider"

Verify the source address in terraform required_providers matches the registry hostname and path exactly:

batlehub.example.com/proxy/{registry}/namespace/type

Ensure credentials for batlehub.example.com are set in ~/.terraformrc.

Terraform provider download fails — "no matching binary"

The provider manifest was uploaded without a binary for the requested platform. Upload the binary via:

PUT /proxy/{registry}/v1/providers/{ns}/{type}/{version}/artifact/{os}/{arch}

Released under the Apache 2.0 License. Made with ❤️ and too much ☕.