Skip to content

Commit d26efbd

Browse files
committed
ci: publicacao via Trusted Publishing (OIDC) + PUBLISHING.md
1 parent e23d3aa commit d26efbd

2 files changed

Lines changed: 286 additions & 18 deletions

File tree

Lines changed: 134 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,154 @@
11
name: Publicar pacote
22

3+
# Publicacao via Trusted Publishing (OIDC) - NAO usa token nem secret.
4+
# Disparada por tags git:
5+
# tag vX.Y.ZrcN (pre-release, ex: v1.5.4rc1) -> publica no TestPyPI
6+
# tag vX.Y.Z (versao final, ex: v1.5.4) -> publica no PyPI (producao)
7+
#
8+
# Trava de seguranca: a publicacao em producao so roda se ja existir uma
9+
# release candidate (vX.Y.ZrcN) da MESMA versao publicada no TestPyPI.
10+
#
11+
# Passo a passo, configuracao do trusted publisher no PyPI e como testar:
12+
# ver PUBLISHING.md na raiz do repositorio.
13+
314
on:
415
push:
5-
branches: [ main, develop ]
616
tags:
717
- "v*"
818
workflow_dispatch:
919

1020
jobs:
11-
deploy:
21+
build:
22+
name: Build e validacoes
1223
runs-on: ubuntu-latest
13-
24+
outputs:
25+
version: ${{ steps.meta.outputs.version }}
26+
package: ${{ steps.meta.outputs.package }}
27+
is_prerelease: ${{ steps.meta.outputs.is_prerelease }}
1428
steps:
15-
- uses: actions/checkout@v3
29+
- uses: actions/checkout@v4
1630

1731
- name: Setup Python
18-
uses: actions/setup-python@v4
32+
uses: actions/setup-python@v5
1933
with:
20-
python-version: "3.10"
34+
python-version: "3.12"
2135

22-
- name: Instalar dependências
36+
- name: Instalar ferramentas de build
37+
run: python -m pip install --upgrade pip build packaging twine
38+
39+
- name: Validar tag == versao do pyproject e detectar pre-release
40+
id: meta
2341
run: |
24-
python -m pip install --upgrade pip
25-
pip install build
26-
pip install twine
42+
python - <<'PY'
43+
import os, sys, tomllib
44+
from packaging.version import Version
45+
46+
ref = os.environ["GITHUB_REF_NAME"] # ex: v1.5.4rc1
47+
tag_version = ref[1:] if ref.startswith("v") else ref
48+
49+
with open("pyproject.toml", "rb") as f:
50+
proj = tomllib.load(f)["project"]
51+
pkg, pyproject_version = proj["name"], proj["version"]
52+
53+
if Version(tag_version) != Version(pyproject_version):
54+
print(f"::error::A tag ({tag_version}) difere da versao em "
55+
f"pyproject.toml ({pyproject_version}). Atualize a versao "
56+
f"no pyproject antes de criar a tag.")
57+
sys.exit(1)
58+
59+
is_pre = "true" if Version(tag_version).is_prerelease else "false"
60+
with open(os.environ["GITHUB_OUTPUT"], "a") as out:
61+
out.write(f"version={tag_version}\n")
62+
out.write(f"package={pkg}\n")
63+
out.write(f"is_prerelease={is_pre}\n")
64+
print(f"OK: {pkg} {tag_version} (pre-release={is_pre})")
65+
PY
2766
28-
- name: Build package
67+
- name: Build (sdist + wheel)
2968
run: python -m build
3069

31-
- name: Publish package on test pypi
32-
if: github.ref == 'refs/heads/develop'
33-
run: python -m twine upload -u __token__ -p ${{ secrets.TEST_PYPI_API_TOKEN }} --repository testpypi dist/*
70+
- name: twine check
71+
run: python -m twine check dist/*
72+
73+
- name: Subir artefato
74+
uses: actions/upload-artifact@v4
75+
with:
76+
name: dist
77+
path: dist/
78+
79+
testpypi:
80+
name: Publicar no TestPyPI (release candidate)
81+
needs: build
82+
if: needs.build.outputs.is_prerelease == 'true'
83+
runs-on: ubuntu-latest
84+
environment: testpypi
85+
permissions:
86+
id-token: write # obrigatorio para Trusted Publishing (OIDC)
87+
steps:
88+
- name: Baixar artefato
89+
uses: actions/download-artifact@v4
90+
with:
91+
name: dist
92+
path: dist/
93+
94+
- name: Publicar no TestPyPI
95+
uses: pypa/gh-action-pypi-publish@release/v1
96+
with:
97+
repository-url: https://test.pypi.org/legacy/
98+
99+
pypi:
100+
name: Publicar no PyPI (producao)
101+
needs: build
102+
if: needs.build.outputs.is_prerelease == 'false'
103+
runs-on: ubuntu-latest
104+
environment: pypi
105+
permissions:
106+
id-token: write # obrigatorio para Trusted Publishing (OIDC)
107+
steps:
108+
- name: Setup Python
109+
uses: actions/setup-python@v5
110+
with:
111+
python-version: "3.12"
112+
113+
- name: Gate - exige release candidate da mesma versao no TestPyPI
114+
env:
115+
PACKAGE: ${{ needs.build.outputs.package }}
116+
VERSION: ${{ needs.build.outputs.version }}
117+
run: |
118+
python -m pip install --upgrade packaging
119+
python - <<'PY'
120+
import json, os, sys, urllib.request, urllib.error
121+
from packaging.version import Version
122+
123+
pkg = os.environ["PACKAGE"]
124+
base = Version(os.environ["VERSION"]).base_version # ex: 1.5.4
125+
url = f"https://test.pypi.org/pypi/{pkg}/json"
126+
try:
127+
with urllib.request.urlopen(url, timeout=30) as r:
128+
data = json.load(r)
129+
except urllib.error.HTTPError as e:
130+
print(f"::error::Nao foi possivel consultar o TestPyPI ({e.code}). "
131+
f"Publique e teste a release candidate v{base}rc1 antes da "
132+
f"versao final v{base}.")
133+
sys.exit(1)
134+
135+
rcs = sorted(
136+
v for v in data.get("releases", {})
137+
if Version(v).base_version == base and Version(v).is_prerelease
138+
)
139+
if not rcs:
140+
print(f"::error::Gate bloqueou a publicacao em producao: nenhuma "
141+
f"release candidate de {base} encontrada no TestPyPI. Crie e "
142+
f"teste a tag v{base}rc1 antes de publicar a final v{base}.")
143+
sys.exit(1)
144+
print(f"Gate ok: release candidate(s) no TestPyPI para {base}: {rcs}")
145+
PY
146+
147+
- name: Baixar artefato
148+
uses: actions/download-artifact@v4
149+
with:
150+
name: dist
151+
path: dist/
34152

35-
- name: Publish package on pypi
36-
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
37-
run: python -m twine upload -u __token__ -p ${{ secrets.PYPI_API_TOKEN }} dist/*
38-
153+
- name: Publicar no PyPI (producao)
154+
uses: pypa/gh-action-pypi-publish@release/v1

PUBLISHING.md

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# Publicacao do pacote `msgram-parser`
2+
3+
Guia de como empacotar e publicar este repositorio no PyPI. Escrito para os
4+
proximos grupos: leia inteiro antes da primeira release.
5+
6+
Pacote no PyPI: **`msgram-parser`**.
7+
8+
---
9+
10+
## TL;DR
11+
12+
A publicacao e automatica via GitHub Actions e **Trusted Publishing (OIDC)**:
13+
nao existe token nem secret no repositorio. Tudo e disparado por **tag git**.
14+
15+
| Tag que voce cria | Onde publica | Quando usar |
16+
|--------------------------|--------------------|--------------------------------------|
17+
| `vX.Y.ZrcN` (`v1.2.2rc1`)| TestPyPI | Testar o pacote antes de producao |
18+
| `vX.Y.Z` (`v1.2.2`) | PyPI (producao) | Release final, depois de validar a rc|
19+
20+
**Trava de seguranca:** a tag final (`vX.Y.Z`) so publica em producao se ja
21+
existir uma release candidate (`vX.Y.ZrcN`) da mesma versao no TestPyPI. Sem rc,
22+
o job falha. E impossivel publicar em producao sem ter testado antes.
23+
24+
---
25+
26+
## Fluxo completo de uma release (passo a passo)
27+
28+
1. **Bump da versao para a rc.** Em `pyproject.toml`, ajuste:
29+
```toml
30+
version = "1.2.2rc1"
31+
```
32+
A versao do `pyproject.toml` tem que bater EXATAMENTE com a tag, senao o CI
33+
falha de proposito (step "Validar tag == versao do pyproject").
34+
2. **Commit + tag da rc:**
35+
```bash
36+
git commit -am "chore: bump 1.2.2rc1"
37+
git tag v1.2.2rc1
38+
git push origin develop --tags
39+
```
40+
O push da tag dispara o workflow, que publica no **TestPyPI**.
41+
3. **Teste a rc** instalando do TestPyPI (secao abaixo). Rode o que precisar.
42+
4. **Se estiver tudo certo, prepare a final.** No `pyproject.toml`:
43+
```toml
44+
version = "1.2.2"
45+
```
46+
5. **Commit + tag final:**
47+
```bash
48+
git commit -am "chore: release 1.2.2"
49+
git tag v1.2.2
50+
git push origin develop --tags
51+
```
52+
O workflow roda o **gate** (confere que `1.2.2rcN` existe no TestPyPI) e, se
53+
passar, publica em **producao**.
54+
55+
Achou um problema na rc? Corrija, suba a versao da rc (`1.2.2rc2`) e repita do
56+
passo 1. So promova para final quando a rc estiver boa.
57+
58+
---
59+
60+
## Pre-requisitos: configurar o Trusted Publisher (uma vez por projeto)
61+
62+
Quem tiver acesso de **owner** do projeto no PyPI precisa registrar o publisher
63+
confiavel nos DOIS indices (sao contas/sites separados):
64+
65+
- Producao: <https://pypi.org/manage/project/msgram-parser/settings/publishing/>
66+
- Teste: <https://test.pypi.org/manage/project/msgram-parser/settings/publishing/>
67+
68+
Em cada um, adicione um "GitHub" trusted publisher com:
69+
70+
| Campo | Valor |
71+
|---------------------|------------------------------------|
72+
| Owner | `fga-eps-mds` |
73+
| Repository name | `2026.1-MeasureSoftGram-Parser` |
74+
| Workflow filename | `python-publish.yml` |
75+
| Environment name | `pypi` no PyPI / `testpypi` no TestPyPI |
76+
77+
Depois, no GitHub do repo (Settings > Environments), crie os environments
78+
**`testpypi`** e **`pypi`**. Recomendado: no `pypi`, marque "Required reviewers"
79+
com alguem do time, assim toda release de producao passa por um OK humano alem
80+
do gate da rc.
81+
82+
> Nota: Trusted Publishing substitui os antigos secrets `PYPI_API_TOKEN` e
83+
> `TEST_PYPI_API_TOKEN`. Eles nao sao mais usados e podem ser removidos.
84+
85+
---
86+
87+
## Como testar a partir do TestPyPI
88+
89+
O TestPyPI nao tem todas as dependencias pesadas (requests, numpy, pandas). Por
90+
isso o `--extra-index-url` aponta para o PyPI de producao, de onde essas deps
91+
sao baixadas:
92+
93+
```bash
94+
uv venv --python 3.10 .venv
95+
uv pip install --python .venv/bin/python \
96+
--index-url https://test.pypi.org/simple/ \
97+
--extra-index-url https://pypi.org/simple/ \
98+
"msgram-parser==1.2.2rc1"
99+
100+
.venv/bin/python -c "import genericparser; print('import ok')"
101+
```
102+
103+
(Com `pip` puro: `pip install --index-url ... --extra-index-url ... msgram-parser==1.2.2rc1`.)
104+
105+
---
106+
107+
## Ordem de publicacao entre os pacotes do MeasureSoftGram
108+
109+
O ecossistema tem tres pacotes com dependencia entre eles:
110+
111+
```
112+
msgram (CLI) -> depende de -> msgram-core + msgram-parser
113+
```
114+
115+
`msgram-parser` (este repo) **nao depende** dos outros, entao faz parte da
116+
primeira onda. Ordem recomendada ao subir versoes novas em producao:
117+
118+
1. **msgram-core** e **msgram-parser** (este repo entra aqui)
119+
2. so depois, **msgram** (CLI)
120+
121+
Motivo: quando o CLI for publicado, o PyPI precisa ja ter as versoes novas de
122+
core e parser disponiveis para resolver as dependencias.
123+
124+
---
125+
126+
## Versionamento
127+
128+
- Segue [PEP 440](https://peps.python.org/pep-0440/). Release candidate e
129+
`X.Y.ZrcN` (ex: `1.2.2rc1`), final e `X.Y.Z`.
130+
- Cada versao so pode ser publicada **uma vez** em cada indice. Para republicar,
131+
suba o numero (nao da para sobrescrever no PyPI nem no TestPyPI).
132+
- A tag git sempre tem o prefixo `v` (`v1.2.2rc1`, `v1.2.2`).
133+
134+
---
135+
136+
## Troubleshooting
137+
138+
| Sintoma | Causa provavel / solucao |
139+
|---|---|
140+
| `403 ... isn't allowed to upload to project` | Trusted publisher nao configurado ou com campo divergente (owner/repo/workflow/environment). Confira a secao de pre-requisitos. |
141+
| `400 File already exists` | Essa versao ja foi publicada nesse indice. Suba o numero da versao. |
142+
| Job de producao falhou no "Gate" | Nao existe rc da mesma versao no TestPyPI. Publique e teste a `vX.Y.ZrcN` primeiro. |
143+
| `Tag ... difere da versao em pyproject.toml` | A tag e a `version` do `pyproject.toml` precisam ser iguais. Ajuste e re-tague. |
144+
| `pip` nao acha as deps ao instalar do TestPyPI | Faltou o `--extra-index-url https://pypi.org/simple/`. |
145+
146+
---
147+
148+
## Referencias
149+
150+
- Trusted Publishing (PyPI): <https://docs.pypi.org/trusted-publishers/>
151+
- Action oficial: <https://github.com/pypa/gh-action-pypi-publish>
152+
- Workflow deste repo: `.github/workflows/python-publish.yml`

0 commit comments

Comments
 (0)