Registering Packages from CI

The Registering Packages page covers registering a package from the JuliaHub web interface. JuliaHub also exposes the same functionality as a REST API, so you can register a package — or a new version of one — automatically from a CI pipeline, for example after pushing a tag to GitLab or GitHub.

This page describes that API and shows a complete, working pipeline example.

Note

The API performs exactly the same registration the web interface does, subject to the same permissions. Anything you cannot register from the web interface you also cannot register from CI, and the reverse.

Before you start

Registration from CI requires all of the following:

  • A registry configured on JuliaHub with Registrator enabled. See Registry Management for how to create a private registry and enable Registrator on it. Registering into the public General registry works the same way.
  • Permission to register into that registry. Registration is checked per user, per registry — see Permissions and access control below.
  • A JuliaHub authentication token for the account the pipeline will act as, available to the pipeline as a secret. See Authenticating.

API endpoints

All endpoints live under /registrator/v1 on your JuliaHub instance, and all of them accept a bearer token in the Authorization header.

MethodPathPurpose
POST/registrator/v1/registerSubmit one or more package registrations
GET/registrator/v1/status/{id}Poll the status of a submitted registration
GET/registrator/v1/configStatusList configured registries and your permissions on them
GET/registrator/v1/authorizationCheck whether your Git provider authorization is usable

Replace juliahub.example.com in the examples below with the hostname of your JuliaHub instance.

Authenticating

Every request must carry a JuliaHub authentication token:

Authorization: Bearer <token>

The most convenient way to obtain a token for a pipeline is the jh CLI, which stores a refresh token after an interactive login and can then mint a fresh ID token non-interactively:

jh auth env

This prints shell-style assignments, including JULIAHUB_ID_TOKEN, which is the value to use as the bearer token:

JULIAHUB_HOST=juliahub.example.com
JULIAHUB_PORT=443
JULIAHUB_ID_TOKEN=eyJhbGciOi...
JULIAHUB_ID_TOKEN_EXPIRES=1758210000

Alternatively, the id_token field of the auth.toml file downloaded from the account preferences page is the same kind of token.

Warning

These tokens are short-lived and act as the user who created them, with that user's full JuliaHub access — see Authentication tokens inside jobs for the trust model. Store the token in your CI system's secret storage (GitLab CI/CD variables, GitHub Actions secrets), never in the repository, and mark it masked/protected so it is not written to build logs.

Because tokens expire, a pipeline that runs infrequently should mint a token as a pipeline step rather than storing a long-lived one as a variable.

Checking which registries you can register into

Before wiring up a pipeline, confirm the registry is configured and that the account the pipeline will use is allowed to register into it:

curl -sS https://juliahub.example.com/registrator/v1/configStatus \
  -H "Authorization: Bearer $JULIAHUB_ID_TOKEN"

The response lists every registry configured on the instance:

{
  "registries": [
    {
      "ready": true,
      "registry_id": 2,
      "registry_name": "MyCompanyRegistry",
      "registry_uuid": "029dca29-6cbd-4fff-82a3-f7236bdedde2",
      "registry_url": "https://github.com/mycompany/MyCompanyRegistry",
      "git_server_type": "github",
      "can_register": true,
      "can_download": true,
      "owns": false,
      "message": ""
    }
  ]
}

The fields that matter for CI are:

  • can_register — whether the authenticated account may register packages into this registry. If this is false, POST /register will fail with a 401, and the fix is a permissions change rather than anything in the pipeline.
  • ready — whether the registry has finished its first synchronization. While this is false, message explains what is being waited on and registration will not succeed yet.
  • registry_name — the exact string to send as registry_name when registering. It must match, including case.

This endpoint is a good first call when debugging a pipeline, because it answers "is the registry there and am I allowed to use it?" without submitting anything.

Registering a package

POST /registrator/v1/register takes a JSON object with a requests array. Each entry describes one package to register:

curl -sS -X POST https://juliahub.example.com/registrator/v1/register \
  -H "Authorization: Bearer $JULIAHUB_ID_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
        "requests": [
          {
            "registry_name": "MyCompanyRegistry",
            "repo_url": "https://gitlab.example.com/mygroup/MyPackage.jl",
            "git_server_type": "gitlab",
            "branch": "v0.2.0",
            "subdir": "",
            "notes": "Registered automatically from CI."
          }
        ]
      }'

The fields of each request are:

FieldRequiredDescription
registry_nameyesName of the registry as configured on JuliaHub, exactly as configStatus reports it.
repo_urlyesURL of the package repository. Must use the https://, http:// or ssh:// scheme.
git_server_typeyesOne of github, gitlab or bitbucket. Any other value is treated as a plain Git server.
branchnoBranch or tag to register from. Defaults to the repository's default branch.
subdirnoPath within the repository if the package is not at the top level, e.g. packages/MyPackage.jl. Use / as the separator.
notesnoFree text added to the registration pull request description. TagBot picks this up as release notes.

A successful call returns immediately with a request id — registration itself happens asynchronously:

{
  "message": "Registration in progress. Please wait...",
  "id": "7cdd582f-b210-4373-8730-49ebdbc70a65"
}
Note

A 200 response means the request was accepted, not that the package was registered. A pipeline that should fail when registration fails must poll the status endpoint — see Polling for the result.

Some constraints apply to the requests array:

  • Every entry must target the same registry and the same Git server type. Register into two registries with two separate calls.
  • The number of entries is capped per registry. Exceeding it returns 429.
  • Two entries describing the same package are rejected as a likely mistake.

Polling for the result

GET /registrator/v1/status/{id} reports on a registration, using the id returned by /register:

curl -sS https://juliahub.example.com/registrator/v1/status/7cdd582f-b210-4373-8730-49ebdbc70a65 \
  -H "Authorization: Bearer $JULIAHUB_ID_TOKEN"
{
  "state": "success",
  "message": "https://github.com/mycompany/MyCompanyRegistry/pull/42"
}

state is one of:

StateMeaning
pendingThe registration is still being processed. Keep polling.
successThe registration succeeded. message is usually the URL of the registration pull request.
erroredThe registration failed. message explains why.
Warning

Registration statuses are retained for 24 hours and then deleted. Poll for the result within the same pipeline run rather than checking a stored id later; once the status has been cleaned up the endpoint can no longer tell you how the registration turned out.

Checking Git provider authorization

Registration into a registry backed by GitHub, GitLab or Bitbucket requires that the registering account has connected that Git provider to JuliaHub, so the platform can verify commit access to the package repository. GET /registrator/v1/authorization reports whether that connection is in place:

{
  "auth_set": true,
  "has_correct_scopes": true
}

If auth_set is false, or has_correct_scopes is false, the account must connect or reconnect the provider from the account preferences page before registration will work. This is an interactive, browser-based step — it cannot be performed from CI, so it has to be done once for the pipeline's account before the pipeline can register anything.

Permissions and access control

Two independent layers of access control apply to registration from CI. When a pipeline gets a 401, it is worth knowing which of the two rejected it.

Per-registry registration permission

Each registry on JuliaHub carries its own set of users and groups who may register into it. This is the check that produces:

Not authorized to register packages with registry MyCompanyRegistry

It corresponds directly to the can_register field from configStatus. Administrators manage it from Admin → Settings → Registries by editing the registry's permissions. Granting a pipeline access is a matter of granting it to the user account the pipeline authenticates as.

Platform API access control

Separately, administrators can restrict which groups or users may reach parts of the JuliaHub API at all. The registration endpoints belong to the registrator.registration API set, and configStatus belongs to registrator.userconfig.

If your platform has the customer admin role enabled, these can be adjusted from Admin → API Access Control — see API Access Control for the full description of how base and site rules combine. Note that changes there are cached and can take up to five minutes to take effect.

A useful way to tell the two layers apart: if configStatus returns registries but can_register is false, it is the per-registry permission. If the API call itself is rejected before returning data, it is more likely the API access control layer.

Which account should CI use?

Registration is always attributed to a real JuliaHub user — there is no separate machine identity for Registrator. Because the registering account must also have a connected Git provider authorization with commit access to the package repository, the practical options are:

  • A dedicated service account that is a member of the relevant registry and has its Git provider connected. This is the better option: the pipeline does not break when an individual leaves, and the audit trail clearly shows automated registrations.
  • An individual's account. Simplest to set up, but the pipeline stops working when that person's access changes.

Every registration is recorded as an audit event with the registering user, the registry and the repository URL, so either choice remains traceable. See Traceability (Audit Events) for how to review them.

Complete pipeline examples

Both examples below register a package when a version tag is pushed, then wait for the registration to finish and fail the job if it did not succeed.

They assume the JuliaHub token is available as a masked CI secret named JULIAHUB_ID_TOKEN, and that the registry name and JuliaHub host are set as variables.

GitLab CI

register:
  stage: deploy
  image: alpine:latest
  rules:
    - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
  variables:
    JULIAHUB_HOST: juliahub.example.com
    REGISTRY_NAME: MyCompanyRegistry
  before_script:
    - apk add --no-cache curl jq
  script:
    - |
      id=$(curl -sS --fail-with-body -X POST \
        "https://$JULIAHUB_HOST/registrator/v1/register" \
        -H "Authorization: Bearer $JULIAHUB_ID_TOKEN" \
        -H "Content-Type: application/json" \
        -d "$(jq -n \
              --arg registry "$REGISTRY_NAME" \
              --arg url "$CI_PROJECT_URL" \
              --arg branch "$CI_COMMIT_TAG" \
              '{requests: [{registry_name: $registry,
                            repo_url: $url,
                            git_server_type: "gitlab",
                            branch: $branch,
                            notes: "Registered automatically from CI."}]}')" \
        | jq -r .id)
      echo "Registration submitted: $id"

      for _ in $(seq 1 60); do
        sleep 10
        response=$(curl -sS \
          "https://$JULIAHUB_HOST/registrator/v1/status/$id" \
          -H "Authorization: Bearer $JULIAHUB_ID_TOKEN")
        state=$(echo "$response" | jq -r .state)
        message=$(echo "$response" | jq -r .message)
        echo "State: $state - $message"
        case "$state" in
          success) exit 0 ;;
          errored) echo "Registration failed: $message" >&2; exit 1 ;;
        esac
      done

      echo "Timed out waiting for registration to complete." >&2
      exit 1

GitHub Actions

name: Register package

on:
  push:
    tags: ['v*']

jobs:
  register:
    runs-on: ubuntu-latest
    env:
      JULIAHUB_HOST: juliahub.example.com
      REGISTRY_NAME: MyCompanyRegistry
      JULIAHUB_ID_TOKEN: ${{ secrets.JULIAHUB_ID_TOKEN }}
    steps:
      - name: Submit registration and wait for the result
        run: |
          id=$(curl -sS --fail-with-body -X POST \
            "https://$JULIAHUB_HOST/registrator/v1/register" \
            -H "Authorization: Bearer $JULIAHUB_ID_TOKEN" \
            -H "Content-Type: application/json" \
            -d "$(jq -n \
                  --arg registry "$REGISTRY_NAME" \
                  --arg url "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY" \
                  --arg branch "$GITHUB_REF_NAME" \
                  '{requests: [{registry_name: $registry,
                                repo_url: $url,
                                git_server_type: "github",
                                branch: $branch,
                                notes: "Registered automatically from CI."}]}')" \
            | jq -r .id)
          echo "Registration submitted: $id"

          for _ in $(seq 1 60); do
            sleep 10
            response=$(curl -sS \
              "https://$JULIAHUB_HOST/registrator/v1/status/$id" \
              -H "Authorization: Bearer $JULIAHUB_ID_TOKEN")
            state=$(echo "$response" | jq -r .state)
            message=$(echo "$response" | jq -r .message)
            echo "State: $state - $message"
            case "$state" in
              success) exit 0 ;;
              errored) echo "Registration failed: $message" >&2; exit 1 ;;
            esac
          done

          echo "Timed out waiting for registration to complete." >&2
          exit 1

Troubleshooting

401 Not authorized to register packages with registry ...

The account the pipeline authenticates as does not have registration permission on that registry. Call configStatus with the same token and check can_register for the registry. See Permissions and access control.

Unable to find registry configuration for ...

The registry_name does not match a registry that has Registrator enabled. Check the exact spelling against configStatus, which reports the authoritative name.

If the registry is enabled on the platform but still does not appear in configStatus, contact JuliaHub support rather than working around it — a registry that is configured but missing from this list indicates a problem on the platform side, not a mistake in the request.

Registry credentials in configuration is not allowed to access <url>

The credential configured on the registry is not permitted to reach the package repository. For a Personal Access Token, the token's URL Prefix must cover the package repository's URL. For a GitHub App, the app must be installed on the package repository as well as on the registry repository — see GitHub apps integration for the details.

Package URL cannot be registered since token is not configured for http(s) protocol

The registry has no credential configured, and the package is being registered over http(s). Add a credential to the registry under Admin → Settings → Registries.

Repo url is invalid

The repository URL failed validation. The common causes are a missing scheme (the URL must begin with https://, http:// or ssh://), credentials embedded in the URL (a user@ component is rejected), or a URL with no path component. Note that plain http:// is rejected for github.com, gitlab.com and bitbucket.org.

429 Number of concurrent package registration requests is limited to ...

Too many packages in a single requests array for that registry. Split the registration across several calls.

The registration succeeds but the version is not installable yet

Registration creates a pull request against the registry. The version becomes available once that pull request is merged and JuliaHub has synchronized the registry, which it does approximately every 30 minutes. See Availability of the package version on JuliaHub package server.

The wrong commit is registered

If branch is omitted, the registration uses the repository's default branch. When a package repository has been forked or renamed, its default branch may not be what you expect — a repository whose default branch is main while the pipeline assumes master (or the reverse) will register from the wrong place or fail to find the branch. Pass branch explicitly with the tag being released, as both pipeline examples above do.