My new Python application files-to-claude-xml is now on PyPI, which means they are packaged and pip installable. My preferred way of running files-to-claude-xml is via UV’s tool run, which will install it if it still needs to be installed and then execute it.

$ uv tool run files-to-claude-xml --version

Publishing on PyPi with UV

UV has both build and publish commands, so I took them for a spin today.

uv build just worked, and a Python package was built.

When I tried uv publish, it prompted me for some auth settings for which I had to log in to PyPI to create a token.

I added those to my local ENV variables I manage with direnv.

export UV_PUBLISH_PASSWORD=<your-PyPI-token-here>
export UV_PUBLISH_USERNAME=__token__

Once both were set and registered, uv publish published my files on PyPI.

GitHub Action

To make files-to-claude-xml easier to run on GitHub, I created a custom action to build a _claude.xml from the GitHub repository.

To use this action, I wrote this example workflow, which runs from files-to-claude-xml-example

name: Convert Files to Claude XML


on:
  push


jobs:
  convert-to-xml:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - name: Convert files to Claude XML
      uses: jefftriplett/files-to-claude-xml-action@main
      with:
        files: |
          README.md
          main.py          
        output: '_claude.xml'
        verbose: 'true'
    - name: Upload XML artifact
      uses: actions/upload-artifact@v4
      with:
        name: claude-xml
        path: _claude.xml

My GitHub action is built with a Dockerfile, which installs files-to-claude-xml.

# Dockerfile
FROM ghcr.io/astral-sh/uv:bookworm-slim


ENV UV_LINK_MODE=copy


RUN --mount=type=cache,target=/root/.cache/uv \
    --mount=type=bind,source=uv.lock,target=uv.lock \
    --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
    uv sync --frozen --no-install-project


WORKDIR /app


ENTRYPOINT ["uvx", "files-to-claude-xml"]

To turn a GitHub repository into a runnable GitHub Action, an action.yml file needs to exist in the repository. This file describes the input arguments and which Dockerfile or command to run.

# action.yml
name: 'Files to Claude XML'
description: 'Convert files to XML format for Claude'
inputs:
  files:
    description: 'Input files to process'
    required: true
    type: list
  output:
    description: 'Output XML file path'
    required: false
    default: '_claude.xml'
  verbose:
    description: 'Enable verbose output'
    required: false
    default: 'false'
  version:
    description: 'Display the version number'
    required: false
    default: 'false'
runs:
  using: 'docker'
  image: 'Dockerfile'
  args:
    - ${{ join(inputs.files, ' ') }}
    - --output
    - ${{ inputs.output }}
    - ${{ inputs.verbose == 'true' && '--verbose' || '' }}
    - ${{ inputs.version == 'true' && '--version' || '' }}

Overall, this works. Claude’s prompting helped me figure it out, which felt fairly satisfying given the goal of files-to-claude-xml.