# Publish from CircleCI

> Publish Maven, Gradle, npm, Python and Docker artifacts to CloudRepo from a CircleCI workflow, with a repository token held in a CircleCI context.

A CircleCI job publishes to CloudRepo the way your laptop does: the build tool sends a repository token with the upload. What changes is where the token lives. Keep it in a CircleCI context, which gives every job that names the context an environment variable, and let a committed config file name that variable instead of holding the token.

## Before you start

- A repository, and a [repository token](/docs/authenticate/repository-tokens.html) that reaches it. A token that publishes is **Read + write**; a job that only pulls needs **Read only**.
- A token you create under **Repository Tokens** belongs to your account. Deleting a user from your organization deletes that user’s tokens, and any job using one stops authenticating. So create the token as an account that outlives any one person: a user you make for builds (see [Create Your First User](/docs/get-started/add-users.html)), signed in to the admin portal when it mints the token. The owner can instead create a [service key](/docs/authenticate/repository-tokens.html#service-keys-for-your-pipelines), which belongs to your organization rather than to a user.
- Your repository URL, `https://your-org.mycloudrepo.io/repositories/your-repo`, and the username that Maven, Gradle, Python and Docker clients send: the email address of the account that created the token. npm sends the token alone.

## Store the token in a context

1. In CircleCI, open **Org**, then **Contexts**, and click **Create Context**. Name it `cloudrepo`.
2. Open the context, click **Add Environment Variable**, name it `CLOUDREPO_TOKEN`, and paste the token as the value.

A job that lists `context: cloudrepo` in the workflow receives `CLOUDREPO_TOKEN` as an environment variable, which is how the files below read it. CircleCI can restrict which members may use a context. See [Contexts](https://circleci.com/docs/guides/security/contexts/) in CircleCI’s documentation.

To keep the token with one project instead, open **Project Settings**, then **Environment Variables**, and add `CLOUDREPO_TOKEN` there. CircleCI hides the value once it is saved, and the job then needs no `context:` line. See [Set an environment variable](https://circleci.com/docs/guides/security/set-environment-variable/).

Keep the token off the command line, which logs and process listings can show. The blocks on this page pass it through the environment or a file the tool reads.

## Publish from a workflow

Pick your client. The choice is remembered on every page of these docs. Each tab has the same parts: a file that names the token’s variable, the repository settings of your build, and the `.circleci/config.yml`. Commit them; none holds the token. Each workflow runs when you push a tag that starts with `release-`.

**Maven**

Maven reads the credential from a settings file. Commit this one as `.mvn/settings.xml`; `${env.CLOUDREPO_TOKEN}` is replaced by the environment variable when Maven runs. Replace `you@example.com` with the email address of the account that created the token.

`.mvn/settings.xml`

```xml
<settings>
  <servers>
    <server>
      <id>cloudrepo</id>
      <username>you@example.com</username>
      <password>${env.CLOUDREPO_TOKEN}</password>
    </server>
  </servers>
</settings>
```

The `<id>` must match the repository’s `<id>` in your `pom.xml`. `<distributionManagement>` is where `mvn deploy` publishes:

`pom.xml`

```xml
<distributionManagement>
  <repository>
    <id>cloudrepo</id>
    <url>https://your-org.mycloudrepo.io/repositories/your-repo</url>
  </repository>
</distributionManagement>
```

The job runs in CircleCI’s OpenJDK image, which carries Maven, and passes the settings file with `--settings`. The workflow gives the job the context and a tag filter:

`.circleci/config.yml`

```yaml
version: 2.1
jobs:
  publish:
    docker:
      - image: cimg/openjdk:17.0
    steps:
      - checkout
      - run: mvn --batch-mode --settings .mvn/settings.xml deploy
workflows:
  publish:
    jobs:
      - publish:
          context:
            - cloudrepo
          filters:
            tags:
              only: /^release-.*/
            branches:
              ignore: /.*/
```

Expected: the job ends with `BUILD SUCCESS`, and the artifact is listed in the repository in the [admin portal](https://admin.cloudrepo.io). To resolve dependencies from the same repository in a build, add a `<repositories>` block with the same URL; see [Maven Repositories](/docs/formats/maven.html).

**Gradle**

Gradle reads the token from the environment in the build script. Replace `you@example.com` with the email address of the account that created the token.

`build.gradle.kts`

```kotlin
publishing {
    publications {
        create<MavenPublication>("library") {
            from(components["java"])
        }
    }
    repositories {
        maven {
            url = uri("https://your-org.mycloudrepo.io/repositories/your-repo")
            credentials {
                username = "you@example.com"
                password = providers.environmentVariable("CLOUDREPO_TOKEN").orNull
            }
        }
    }
}
```

`.orNull` keeps every other Gradle command working on a machine where the variable is not set; only `publish` needs it. Add the `java-library` and `maven-publish` plugins to the `plugins` block if your build does not have them.

`.circleci/config.yml`

```yaml
version: 2.1
jobs:
  publish:
    docker:
      - image: cimg/openjdk:17.0
    steps:
      - checkout
      - run: gradle publish
workflows:
  publish:
    jobs:
      - publish:
          context:
            - cloudrepo
          filters:
            tags:
              only: /^release-.*/
            branches:
              ignore: /.*/
```

If your project has the Gradle wrapper, run `./gradlew publish` instead. A Groovy build script takes the same `maven { ... }` block under `publishing { repositories { ... } }`, with the credential read from `System.getenv("CLOUDREPO_TOKEN")`.

**npm**

npm replaces `${CLOUDREPO_TOKEN}` in `.npmrc` with the environment variable of that name, so this file can be committed. Keep the trailing `/` on both lines, and keep the second line identical to the registry URL without `https:`.

`.npmrc`

```ini
registry=https://your-org.mycloudrepo.io/repositories/your-repo/
//your-org.mycloudrepo.io/repositories/your-repo/:_authToken=${CLOUDREPO_TOKEN}
```

npm sends the token on its own, so there is no username to set.

`.circleci/config.yml`

```yaml
version: 2.1
jobs:
  publish:
    docker:
      - image: cimg/node:lts
    steps:
      - checkout
      - run: npm publish
workflows:
  publish:
    jobs:
      - publish:
          context:
            - cloudrepo
          filters:
            tags:
              only: /^release-.*/
            branches:
              ignore: /.*/
```

The same `.npmrc` and context serve `npm ci` and `npm install` in a job that only pulls. For a scoped package, or a repository that sits beside the public registry, see [npm Repositories](/docs/formats/npm.html).

**pip**

twine uploads a built package. It reads its username and password from `TWINE_USERNAME` and `TWINE_PASSWORD`, which the step sets from the context’s variable, and takes the repository URL without `/simple/`. Replace `you@example.com` with the email address of the account that created the token.

`.circleci/config.yml`

```yaml
version: 2.1
jobs:
  publish:
    docker:
      - image: cimg/python:3.13
    steps:
      - checkout
      - run: |
          pip wheel . --no-deps -w dist
          pip install twine
          export TWINE_USERNAME=you@example.com
          export TWINE_PASSWORD="$CLOUDREPO_TOKEN"
          twine upload --repository-url https://your-org.mycloudrepo.io/repositories/your-repo dist/*
workflows:
  publish:
    jobs:
      - publish:
          context:
            - cloudrepo
          filters:
            tags:
              only: /^release-.*/
            branches:
              ignore: /.*/
```

To install from the repository in a job, set `PIP_INDEX_URL` from the variable rather than passing `--index-url`, which would put the token on the command line. See [Repository Tokens](/docs/authenticate/repository-tokens.html) for the URL, and write the `@` in your email address as `%40`.

**Docker**

Log in to the host alone, with your email address and the token on standard input, then build and push. The image reference carries the repository after the host. CircleCI’s `setup_remote_docker` step is what allows Docker commands to run in the job; it works with the `docker` executor used below and not with the `machine` executor.

`.circleci/config.yml`

```yaml
version: 2.1
jobs:
  publish-image:
    docker:
      - image: cimg/base:stable
    steps:
      - checkout
      - setup_remote_docker
      - run: |
          echo "$CLOUDREPO_TOKEN" | docker login your-org.mycloudrepo.io --username you@example.com --password-stdin
          docker build -t your-org.mycloudrepo.io/repositories/your-repo/my-app:1.0.0 .
          docker push your-org.mycloudrepo.io/repositories/your-repo/my-app:1.0.0
workflows:
  publish:
    jobs:
      - publish-image:
          context:
            - cloudrepo
          filters:
            tags:
              only: /^release-.*/
            branches:
              ignore: /.*/
```

Use your own image name and tag. The password is the repository token, not a portal password. More: [Docker Repositories](/docs/formats/docker.html).

## When publishing fails

- **`401 Unauthorized`.** The credential was refused. The username must be the email address of the account that created the token, and the token must not be expired or revoked. Check that the variable is named `CLOUDREPO_TOKEN`, that the job lists the context that holds it, and that its value is the whole token. The full list is on [Repository Tokens](/docs/authenticate/repository-tokens.html#when-a-client-answers-401-403-or-404).
- **`403 Forbidden`.** The token does not reach this repository, or it is **Read only** and the job publishes. Create a **Read + write** token that includes the repository.
- **`409 Conflict` from `mvn deploy`.** The release version you are publishing already exists, and Overwrite Protection refuses to replace it. Publish a new version, or use a `-SNAPSHOT` version while you iterate. See [Maven Overwrite Protection](/docs/formats/maven.html#maven-overwrite-protection). Python repositories have the same setting: [Python Repositories](/docs/formats/python.html#overwrite-protection).

---

The page: https://www.cloudrepo.io/docs/ci/circleci.html
