Publish Python Packages with Twine Using JFrog CLI

Run twine commands to publish Python packages to Artifactory with optional build-info collection.

When to Use

Use jf twine to publish Python packages (wheels, sdists) to Artifactory. This is the upload counterpart to jf pip (which installs packages).

Typical Python publish workflow:

# 0. Install build tools (one-time)
pip3 install build twine

# 1. Build the package
python3 -m build

# 2. Upload to Artifactory with build-info
jf twine upload dist/* --build-name=my-python-lib --build-number=$BUILD_NUMBER

# 3. Publish build info
jf rt build-publish my-python-lib $BUILD_NUMBER

Prerequisites

  • Install twine separately:

    pip3 install twine

    macOS note: pip3 install places the twine binary in ~/Library/Python/X.Y/bin/, which is not in $PATH by default. Add it once with:

    export PATH="$PATH:$(python3 -m site --user-base)/bin"

    Add this line to your ~/.zshrc or ~/.bashrc to make it permanent.

  • Configure a JFrog server with jf config add or jf c add.

  • Configure the deployer (and optionally resolver) repository before running jf twine. Run jf pip-config with all required flags:

    jf pip-config \
      --server-id-resolve=<server-id> \
      --repo-resolve=<pypi-virtual-repo> \
      --server-id-deploy=<server-id> \
      --repo-deploy=<pypi-local-repo>

    This writes a .jfrog/projects/pip.yaml file that jf twine reads at runtime. You do not need a separate jf twine-config command.

  • Build your package before uploading:

    python3 -m build

Build: jf twine

Run twine commands to publish Python packages to Artifactory with optional build-info collection.

To publish Python packages with Twine:

  1. Complete Prerequisites (including jf pip-config and building the package with python3 -m build).
  2. Run jf twine with the Twine subcommand (for example, check or upload) and optional build-info flags (see Build Examples).

Synopsis

jf twine <twine-arguments> [command options]

Aliases: none

Arguments

ArgumentRequiredDescription
<twine-arguments>YesArguments and options for the twine command (for example, upload, check)

Build Options

FlagDefaultDescription
--build-nameBuild name for build-info. Requires --build-number.
--build-numberBuild number for build-info. Requires --build-name.
--moduleOptional module name for build-info. Requires --build-name and --build-number.
--projectJFrog Artifactory project key

Build Examples

Help

jf twine --help

Check Package

Run jf twine check before uploading to validate package metadata. This command runs entirely locally — no Artifactory authentication is required.

jf twine check dist/*

Expected output:

Checking dist/mypackage-1.0.0-py3-none-any.whl: PASSED
Checking dist/mypackage-1.0.0.tar.gz: PASSED

Upload with Build-Info

jf twine upload dist/* --build-name=<build-name> --build-number=<build-number>

Where:

  • <build-name>: A name for the build (for example, my-python-lib)
  • <build-number>: A number or identifier for the build run (for example, 1)

For example:

jf twine upload dist/* --build-name=my-python-lib --build-number=1

The glob dist/* expands to include both .whl and .tar.gz files automatically.

Expected output (twine verbose output followed by JFrog build-info collection):

Uploading distributions to https://<your-instance>.jfrog.io/artifactory/api/pypi/<repo>
Uploading mypackage-1.0.0-py3-none-any.whl
Uploading mypackage-1.0.0.tar.gz

Important Notes

  • pip-config or pipenv-config covers twine: The jf twine command reads its deployment repository from either a jf pip-config or jf pipenv-config configuration — whichever exists in your project. If both are present, the CLI checks for pip config first, then pipenv. You do not need a separate jf twine-config command. If neither config is found, you will see: no config file was found! Before running the 'jf twine' command on a project for the first time, the project should be configured with the 'jf pip-config OR pipenv-config' command.
  • Twine must be installed separately (pip3 install twine). See the macOS PATH note in Prerequisites.
  • The check subcommand validates package metadata locally before upload — no Artifactory authentication is required. Use it to catch packaging issues early.

CI/CD Example (GitHub Actions)

# .github/workflows/build.yml
steps:
  - uses: actions/checkout@v4
  - name: Setup JFrog CLI
    uses: jfrog/setup-jfrog-cli@v4
    env:
      JF_URL: ${{ vars.JF_URL }}
      JF_ACCESS_TOKEN: ${{ secrets.JF_ACCESS_TOKEN }}
  - name: Setup Python
    uses: actions/setup-python@v5
    with:
      python-version: '3.12'
  - name: Install build tools
    run: pip3 install build twine
  - name: Configure pip
    run: jf pip-config --server-id-resolve=setup-jfrog-cli-server --repo-resolve=pypi-virtual --server-id-deploy=setup-jfrog-cli-server --repo-deploy=pypi-local
  - name: Build package
    run: python3 -m build
  - name: Check package
    run: jf twine check dist/*
  - name: Upload to Artifactory
    run: jf twine upload dist/* --build-name=my-python-lib --build-number=${{ github.run_number }}
  - name: Publish build info
    run: jf rt build-publish my-python-lib ${{ github.run_number }}

Troubleshooting

SymptomCauseFix
no config file was foundjf pip-config was not run, or was run from a different directory than jf twineRun jf pip-config with --repo-deploy set from your project root; jf twine must be run from the same directory (see Prerequisites)
401 on upload — Token failed verification: parse, username shown as <empty>Server uses a reference (non-JWT) token and no username is set in the server configRe-run jf config add and enter a username — twine requires basic auth (username + token) and will fail silently if the username is empty
401 / 403 on upload — wrong credentials or missing permissionsInvalid credentials or user lacks deploy permissions on the target repoRe-run jf config add to update credentials; verify the user has deploy permissions on the PyPI local repo
[Warn] couldn't extract payload from Access TokenServer uses a reference (non-JWT) tokenExpected — safe to ignore for jf twine check (local only). For jf twine upload, ensure a username is configured via jf config add
twine not found / exec: "twine": executable file not found in $PATHTwine is not installed, or its binary is not in $PATHRun pip3 install twine. On macOS, also add export PATH="$PATH:$(python3 -m site --user-base)/bin" to your shell profile
No module named buildThe build package is not installedRun pip3 install build
NotOpenSSLWarning on every twine call (macOS)macOS ships with LibreSSL; urllib3 v2 requires OpenSSL 1.1.1+Safe to ignore; or install Python via Homebrew (brew install python) to use an OpenSSL-linked build
"file not found" on jf twine upload dist/*Package was not built firstRun python3 -m build before jf twine upload
Upload succeeds but package not visibleUploaded to wrong repositoryConfirm --repo-deploy in your pip/pipenv config points to a PyPI local repository

Enable debug logging: export JFROG_CLI_LOG_LEVEL=DEBUG


Related Topics