Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions .github/workflows/python-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ jobs:
version: ${{ steps.dist.outputs.version }}
steps:
- name: Require release tag vX.Y.Z
if: ${{ !contains(inputs.repository-url, 'test.pypi.org') }}
env:
REF_TYPE: ${{ github.ref_type }}
TAG: ${{ github.ref_name }}
Expand All @@ -61,6 +62,7 @@ jobs:
fetch-depth: 0
persist-credentials: false
- name: Require tagged commit on main branch
if: ${{ !contains(inputs.repository-url, 'test.pypi.org') }}
env:
MAIN_BRANCH: ${{ inputs.main-branch }}
run: |
Expand All @@ -82,6 +84,8 @@ jobs:
env:
PACKAGE_NAME: ${{ inputs.package-name }}
TAG: ${{ github.ref_name }}
# TestPyPI: žádný tag, ale verze MUSÍ být dev release (X.Y.Z.devN), aby nešlo vydat produkční verzi
TEST_MODE: ${{ contains(inputs.repository-url, 'test.pypi.org') }}
run: |
python - <<'PY'
import glob, os, sys, tarfile, zipfile
Expand All @@ -90,7 +94,9 @@ jobs:
from packaging.version import Version

expected_name = canonicalize_name(os.environ["PACKAGE_NAME"])
expected_version = Version(os.environ["TAG"].removeprefix("v"))
test_mode = os.environ.get("TEST_MODE") == "true"
expected_version = None if test_mode else Version(os.environ["TAG"].removeprefix("v"))
seen_versions = set()
errors = []
wheels = glob.glob("dist/*.whl")
sdists = glob.glob("dist/*.tar.gz")
Expand All @@ -107,8 +113,17 @@ jobs:
meta = Parser().parsestr(t.extractfile(member).read().decode())
if canonicalize_name(meta["Name"]) != expected_name:
errors.append(f"{path}: název {meta['Name']} != {expected_name}")
if Version(meta["Version"]) != expected_version:
version = Version(meta["Version"])
seen_versions.add(version)
if test_mode:
if not version.is_devrelease:
errors.append(f"{path}: TestPyPI přijímá jen dev verze (X.Y.Z.devN), nalezeno {version}")
elif version != expected_version:
errors.append(f"{path}: verze {meta['Version']} != tag {os.environ['TAG']}")
if test_mode and len(seen_versions) == 1:
expected_version = next(iter(seen_versions))
elif test_mode:
errors.append(f"nesourodé verze v archivech: {sorted(map(str, seen_versions))}")
if errors:
for e in errors:
print(f"::error::{e}")
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@ Všechny šablony jsou přednastaveny na **self-hosted runner** (`hermes-vm`).
| `python-publish.yml` | ověření tagu/verze, publikace na PyPI přes OIDC (Trusted Publishing) |
| `python-release.yml` | GitHub Release z `CHANGELOG.md` |

Volat přes `@v1`. Dokumentace: [`docs/python-packaging.md`](docs/python-packaging.md),
Volat přes přesný tag (`@v1.1.0`). Dokumentace: [`docs/python-packaging.md`](docs/python-packaging.md),
[`docs/python-release-process.md`](docs/python-release-process.md),
[`docs/pypi-trusted-publishing.md`](docs/pypi-trusted-publishing.md).
10 changes: 10 additions & 0 deletions docs/pypi-trusted-publishing.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,13 @@ při publikaci. Pro privátní repozitáře se attestations nevytvářejí (`att
- PyPI: role *Owner* jen pro nezbytné osoby, ostatní *Maintainer*; vyžadujte 2FA.
- Po zprovoznění Trusted Publishingu smazat secret `PYPI_API_TOKEN` (repo i org) a token na PyPI zneplatnit.
- Odchod člena týmu: odebrat z PyPI projektů a z GitHub týmů s právem push na tagy.

## TestPyPI (od `v1.1.0`)

`python-publish.yml` s `repository-url: https://test.pypi.org/legacy/` běží v testovacím režimu: nevyžaduje tag ani
větev `main`, ale **přijme jen dev verze** (`X.Y.Z.devN`), takže produkční číslo verze nelze touto cestou spálit.
Doporučené spouštění: `workflow_dispatch` ve vlastním `publish-test.yml` s environmentem `testpypi`.

## Verze reusable workflows

Odkazujte přesným tagem (`@v1.1.0`); existující tagy se nepřepisují, ani pohyblivý `v1` (zůstává na `v1.0.0`).
8 changes: 4 additions & 4 deletions docs/python-packaging.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ jobs:
ci:
permissions:
contents: read
uses: SYSNET-CZ/.github/.github/workflows/python-ci.yml@v1
uses: SYSNET-CZ/.github/.github/workflows/python-ci.yml@v1.1.0
with:
python-versions: '["3.11", "3.12", "3.13"]'
```
Expand All @@ -77,19 +77,19 @@ jobs:
permissions:
contents: read
id-token: write
uses: SYSNET-CZ/.github/.github/workflows/python-publish.yml@v1
uses: SYSNET-CZ/.github/.github/workflows/python-publish.yml@v1.1.0
with:
package-name: sysnet-priklad
release:
needs: publish
permissions:
contents: write
uses: SYSNET-CZ/.github/.github/workflows/python-release.yml@v1
uses: SYSNET-CZ/.github/.github/workflows/python-release.yml@v1.1.0
with:
package-name: sysnet-priklad
```

Volejte vždy přes stabilní tag `@v1`, nikdy `@main`. Reusable workflow se vydává tagem `v1.x.y` a pohyblivým tagem `v1`.
Volejte vždy přes přesný verzovaný tag (`@v1.1.0`), nikdy `@main`. Tagy se nepřepisují, každá změna reusable workflow je nový tag `v1.x.y`.

## 4. Runnery

Expand Down
35 changes: 30 additions & 5 deletions tests/test_publish_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,32 @@ def _step_script() -> str:
return textwrap.dedent(run[start:run.rindex("PY")])


@pytest.fixture(scope="module")
def dist_dir(tmp_path_factory):
def _make_dist(tmp_path_factory, version):
root = tmp_path_factory.mktemp("pkg")
(root / "fixture_pkg").mkdir()
(root / "fixture_pkg/__init__.py").write_text("")
(root / "pyproject.toml").write_text(
'[build-system]\nrequires=["setuptools>=68"]\nbuild-backend="setuptools.build_meta"\n'
'[project]\nname="Fixture_Pkg"\nversion="1.2.3"\n'
'[project]\nname="Fixture_Pkg"\nversion="%s"\n' % version
)
subprocess.run([sys.executable, "-m", "build", "--outdir", "dist"], cwd=root, check=True, capture_output=True)
return root


def _run(root: Path, tag: str, name: str):
@pytest.fixture(scope="module")
def dist_dir(tmp_path_factory):
return _make_dist(tmp_path_factory, "1.2.3")


@pytest.fixture(scope="module")
def dev_dist_dir(tmp_path_factory):
return _make_dist(tmp_path_factory, "1.2.4.dev1")


def _run(root: Path, tag: str, name: str, test_mode: bool = False):
out = root / "gh_output"
out.write_text("")
env = {**os.environ, "TAG": tag, "PACKAGE_NAME": name, "GITHUB_OUTPUT": str(out)}
env = {**os.environ, "TAG": tag, "PACKAGE_NAME": name, "TEST_MODE": "true" if test_mode else "false", "GITHUB_OUTPUT": str(out)}
r = subprocess.run([sys.executable, "-c", _step_script()], cwd=root, env=env, capture_output=True, text=True)
return r, out.read_text()

Expand All @@ -61,6 +70,22 @@ def test_name_mismatch_fails(dist_dir):
assert r.returncode == 1 and "název" in r.stdout


def test_test_mode_accepts_dev_version_without_tag(dev_dist_dir):
r, out = _run(dev_dist_dir, "feature-branch", "fixture-pkg", test_mode=True)
assert r.returncode == 0, r.stdout + r.stderr
assert "version=1.2.4.dev1" in out


def test_test_mode_rejects_release_version(dist_dir):
r, _ = _run(dist_dir, "feature-branch", "fixture-pkg", test_mode=True)
assert r.returncode == 1 and "dev verze" in r.stdout


def test_dev_version_rejected_in_production_mode(dev_dist_dir):
r, _ = _run(dev_dist_dir, "v1.2.4", "fixture-pkg")
assert r.returncode == 1


def test_tag_regex_in_workflow():
wf = yaml.safe_load(WORKFLOW.read_text())
script = wf["jobs"]["verify"]["steps"][0]["run"]
Expand Down
Loading