Publicando versões de distribuição de pacotes usando fluxos de trabalho de CI/CD do GitHub Actions#

O CI/CD do GitHub Actions permite que você execute uma série de comandos sempre que ocorrer um evento na plataforma GitHub. Uma escolha popular é ter um fluxo de trabalho que é disparado por um evento push. Este guia mostra como publicar uma distribuição Python sempre que um commit marcado é enviado. Ele usará o GitHub Action pypa/gh-action-pypi-publish para publicação. Ele também usa as actions upload-artifact e download-artifact do GitHub para temporariamente armazenar e baixar os pacotes fontes.

Atenção

Este guia presume que você já tem um projeto para o qual sabe como construir distribuições e ele reside no GitHub. Este guia também evita detalhes da construção de projetos específicos da plataforma. Se você tiver componentes binários, confira os exemplos de GitHub Action do cibuildwheel.

Configurando publicação confiável#

Este guia se baseia na implementação de publicação confiável do PyPI para se conectar ao CI/CD do GitHub Actions. Isso é recomendado por motivos de segurança, pois os tokens gerados são criados para cada um dos seus projetos individualmente e expiram automaticamente. Caso contrário, você precisará gerar um token de API para PyPI e TestPyPI. No caso de publicação em índices de terceiros como devpi, pode ser necessário fornecer uma combinação de nome de usuário/senha.

Como este guia demonstrará o envio para PyPI e TestPyPI, precisaremos de dois publicadores confiáveis configurados. As etapas a seguir guiarão você na criação dos publicadores “pendentes” para seu novo Projeto PyPI. No entanto, também é possível adicionar publicação confiável a qualquer projeto preexistente, se você for o proprietário.

Atenção

Se você seguiu versões anteriores deste guia, você criou os segredos PYPI_API_TOKEN e TEST_PYPI_API_TOKEN para acesso direto ao PyPI e TestPyPI. Eles estão descontinuados agora e você deve removê-los de seu repositório GitHub e revogá-los nas configurações de sua conta PyPI e TestPyPI, caso esteja substituindo sua configuração antiga pela nova.

Vamos começar! 🚀

  1. Acesse https://pypi.org/manage/account/publishing/.

  2. Preencha o nome com o qual deseja publicar seu novo projeto PyPI em (o valor name em seu setup.cfg ou pyproject.toml), o nome do proprietário do repositório no GitHub (organização ou usuário), e nome do repositório, e o nome do arquivo de fluxo de trabalho de lançamento na pasta .github/, consulte Criando uma definição de fluxo de trabalho. Por fim, adicione o nome do ambiente do GitHub (pypi) que iremos configurar em seu repositório. Registre o publicador confiável.

  3. Agora, acesse https://test.pypi.org/manage/account/publishing/ e repita a segunda etapa, mas desta vez, insira testpypi como o nome do ambiente do GitHub.

  4. Seus publicadores “pendentes” agora estão prontos para o primeiro uso e criarão seus projetos automaticamente assim que você usá-los pela primeira vez.

    Nota

    Se você não tiver uma conta TestPyPI, precisará criá-la. Não é o mesmo que uma conta do PyPI comum.

    Atenção

    Por motivos de segurança, você deve exigir a aprovação manual em cada execução para o ambiente pypi.

Criando uma definição de fluxo de trabalho#

Os fluxos de trabalho de CI/CD do GitHub são declarados em arquivos YAML armazenados no diretório .github/workflows/ do seu repositório.

Vamos criar um arquivo chamado .github/workflows/publish-to-test-pypi.yml.

Comece com um nome significativo e defina o evento que deve fazer o GitHub executar este fluxo de trabalho:

name: Publish Python 🐍 distribution 📦 to PyPI and TestPyPI

on: push

Fazendo checkout do projeto e construindo as distribuições#

Teremos que definir dois trabalhos (“jobs”) para publicar em PyPI e TestPyPI respectivamente, e um trabalho adicional para construir os pacotes de distribuição.

Primeiro, definiremos o trabalho para construir os pacotes dist do seu projeto e armazená-los para uso posterior:

jobs:
  build:
    name: Build distribution 📦
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v4
    - name: Set up Python
      uses: actions/setup-python@v5
      with:
        python-version: "3.x"

Isso fará o download do seu repositório no executor de CI e, em seguida, instalará e ativará o lançamento mais recente disponível do Python 3.

E agora podemos construir os dists a partir da fonte e armazená-los. Neste exemplo, usaremos o pacote build. Então, adicione isso à lista de etapas:

    - name: Install pypa/build
      run: >-
        python3 -m
        pip install
        build
        --user
    - name: Build a binary wheel and a source tarball
      run: python3 -m build
    - name: Store the distribution packages
      uses: actions/upload-artifact@v3
      with:
        name: python-package-distributions
        path: dist/

Definindo um ambiente de tarefa do fluxo de trabalho#

Agora, vamos adicionar a configuração inicial para nosso trabalho que será publicado no PyPI. É um processo que executará comandos que definiremos mais tarde. Neste guia, usaremos a versão estável mais recente do Ubuntu LTS fornecida pelo GitHub Actions. Isso também define um ambiente GitHub para o trabalho ser executado em seu contexto e uma URL a ser exibida na interface do GitHub. Além disso, permite adquirir um token OpenID Connect que as ações pypi-publish precisam para implementar publicação confiável sem segredo para PyPI.

  publish-to-pypi:
    name: >-
      Publish Python 🐍 distribution 📦 to PyPI
    if: startsWith(github.ref, 'refs/tags/')  # only publish to PyPI on tag pushes
    needs:
    - build
    runs-on: ubuntu-latest
    environment:
      name: pypi
      url: https://pypi.org/p/<package-name>  # Replace <package-name> with your PyPI project name
    permissions:
      id-token: write  # IMPORTANT: mandatory for trusted publishing

Isso também garantirá que o fluxo de trabalho de publicação do PyPI só seja acionado se o commit atual estiver marcado.

Publicando a distribuição para PyPI#

Finalmente, adicione as seguintes etapas ao final:

    steps:
    - name: Download all the dists
      uses: actions/download-artifact@v3
      with:
        name: python-package-distributions
        path: dist/
    - name: Publish distribution 📦 to PyPI
      uses: pypa/gh-action-pypi-publish@release/v1

Essa etapa usa a GitHub Action pypa/gh-action-pypi-publish: após o primeiro pacote de distribuição armazenado ter sido baixado pela ação download-artifact, ele envia o conteúdo da pasta dist / para PyPI incondicionalmente.

Assinando os pacotes de distribuição#

O trabalho a seguir assina os pacotes de distribuição com Sigstore, o mesmo sistema de assinatura de artefato usado para assinar o CPython.

Em primeiro lugar, ele usa sigstore/gh-action-sigstore-python do GitHub Action para assinar os pacotes de distribuição. Na próxima etapa, uma versão vazia do GitHub da tag atual é criada usando a CLI gh. Observe que esta etapa pode ser ainda mais personalizada. Consulte a documentação de lançamento do gh como referência.

Dica

Pode ser necessário gerenciar suas permissões do GITHUB_TOKEN para permitir a criação da versão do GitHub. Consulte a documentação do GitHub para obter instruções. Especificamente, o token precisa da permissão contents: write.

Finalmente, as distribuições assinadas são enviadas para o GitHub Release.

  github-release:
    name: >-
      Sign the Python 🐍 distribution 📦 with Sigstore
      and upload them to GitHub Release
    needs:
    - publish-to-pypi
    runs-on: ubuntu-latest

    permissions:
      contents: write  # IMPORTANT: mandatory for making GitHub Releases
      id-token: write  # IMPORTANT: mandatory for sigstore

    steps:
    - name: Download all the dists
      uses: actions/download-artifact@v3
      with:
        name: python-package-distributions
        path: dist/
    - name: Sign the dists with Sigstore
      uses: sigstore/gh-action-sigstore-python@v2.1.1
      with:
        inputs: >-
          ./dist/*.tar.gz
          ./dist/*.whl
    - name: Create GitHub Release
      env:
        GITHUB_TOKEN: ${{ github.token }}
      run: >-
        gh release create
        '${{ github.ref_name }}'
        --repo '${{ github.repository }}'
        --notes ""
    - name: Upload artifact signatures to GitHub Release
      env:
        GITHUB_TOKEN: ${{ github.token }}
      # Upload to GitHub Release using the `gh` CLI.
      # `dist/` contains the built packages, and the
      # sigstore-produced signatures and certificates.
      run: >-
        gh release upload
        '${{ github.ref_name }}' dist/**
        --repo '${{ github.repository }}'

Nota

Este é um substituto para assinaturas GPG, para as quais o suporte foi removido do PyPI. No entanto, este trabalho não é obrigatório para o envio para PyPI e pode ser omitido.

Fluxo de trabalho separado para publicação no TestPyPI#

Agora, repita essas etapas e crie outro trabalho para publicação no índice do pacote TestPyPI na seção jobs:

  publish-to-testpypi:
    name: Publish Python 🐍 distribution 📦 to TestPyPI
    needs:
    - build
    runs-on: ubuntu-latest

    environment:
      name: testpypi
      url: https://test.pypi.org/p/<package-name>

    permissions:
      id-token: write  # IMPORTANT: mandatory for trusted publishing

    steps:
    - name: Download all the dists
      uses: actions/download-artifact@v3
      with:
        name: python-package-distributions
        path: dist/
    - name: Publish distribution 📦 to TestPyPI
      uses: pypa/gh-action-pypi-publish@release/v1
      with:
        repository-url: https://test.pypi.org/legacy/

Dica

Exigir aprovações manuais no ambiente GitHub testpypi normalmente é desnecessário, pois ele é projetado para ser executado em cada commit no branch principal e é frequentemente usado para indicar um pipeline de publicação de lançamento íntegro.

O fluxo de trabalho de CI/CD completo#

Este parágrafo mostra todo o fluxo de trabalho após seguir o guia acima.

Click here to display the entire GitHub Actions CI/CD workflow definition
name: Publish Python 🐍 distribution 📦 to PyPI and TestPyPI

on: push

jobs:
  build:
    name: Build distribution 📦
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v4
    - name: Set up Python
      uses: actions/setup-python@v5
      with:
        python-version: "3.x"
    - name: Install pypa/build
      run: >-
        python3 -m
        pip install
        build
        --user
    - name: Build a binary wheel and a source tarball
      run: python3 -m build
    - name: Store the distribution packages
      uses: actions/upload-artifact@v3
      with:
        name: python-package-distributions
        path: dist/

  publish-to-pypi:
    name: >-
      Publish Python 🐍 distribution 📦 to PyPI
    if: startsWith(github.ref, 'refs/tags/')  # only publish to PyPI on tag pushes
    needs:
    - build
    runs-on: ubuntu-latest
    environment:
      name: pypi
      url: https://pypi.org/p/<package-name>  # Replace <package-name> with your PyPI project name
    permissions:
      id-token: write  # IMPORTANT: mandatory for trusted publishing

    steps:
    - name: Download all the dists
      uses: actions/download-artifact@v3
      with:
        name: python-package-distributions
        path: dist/
    - name: Publish distribution 📦 to PyPI
      uses: pypa/gh-action-pypi-publish@release/v1

  github-release:
    name: >-
      Sign the Python 🐍 distribution 📦 with Sigstore
      and upload them to GitHub Release
    needs:
    - publish-to-pypi
    runs-on: ubuntu-latest

    permissions:
      contents: write  # IMPORTANT: mandatory for making GitHub Releases
      id-token: write  # IMPORTANT: mandatory for sigstore

    steps:
    - name: Download all the dists
      uses: actions/download-artifact@v3
      with:
        name: python-package-distributions
        path: dist/
    - name: Sign the dists with Sigstore
      uses: sigstore/gh-action-sigstore-python@v2.1.1
      with:
        inputs: >-
          ./dist/*.tar.gz
          ./dist/*.whl
    - name: Create GitHub Release
      env:
        GITHUB_TOKEN: ${{ github.token }}
      run: >-
        gh release create
        '${{ github.ref_name }}'
        --repo '${{ github.repository }}'
        --notes ""
    - name: Upload artifact signatures to GitHub Release
      env:
        GITHUB_TOKEN: ${{ github.token }}
      # Upload to GitHub Release using the `gh` CLI.
      # `dist/` contains the built packages, and the
      # sigstore-produced signatures and certificates.
      run: >-
        gh release upload
        '${{ github.ref_name }}' dist/**
        --repo '${{ github.repository }}'

  publish-to-testpypi:
    name: Publish Python 🐍 distribution 📦 to TestPyPI
    needs:
    - build
    runs-on: ubuntu-latest

    environment:
      name: testpypi
      url: https://test.pypi.org/p/<package-name>

    permissions:
      id-token: write  # IMPORTANT: mandatory for trusted publishing

    steps:
    - name: Download all the dists
      uses: actions/download-artifact@v3
      with:
        name: python-package-distributions
        path: dist/
    - name: Publish distribution 📦 to TestPyPI
      uses: pypa/gh-action-pypi-publish@release/v1
      with:
        repository-url: https://test.pypi.org/legacy/

Isso é tudo, pessoal!#

Agora, sempre que você fizer um push de um commit com tag para seu repositório Git remoto no GitHub, este fluxo de trabalho irá publicá-lo no PyPI. E publicará qualquer push para TestPyPI que seja útil para fornecer construções de teste para seus usuários alfa, bem como garantir que sua versão pipeline permaneça saudável!

Atenção

Se o seu repositório tiver atividade de commit frequente e cada push for carregado no TestPyPI conforme descrito, o projeto poderá exceder o limite de tamanho do projeto PyPI. O limite poderia ser aumentado, mas uma solução melhor pode consistir em usar um servidor compatível com PyPI como pypiserver no CI para fins de teste.

Nota

Recomenda-se manter o GitHub Actions integrado em suas versões mais recentes, atualizando-as frequentemente.