Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
4cb0f35
Publish to PyPI by trusted publishing instead of a token
zeevmoney Oct 1, 2026
b68b88b
Turn off the Trivy action's cache in the release scan
zeevmoney Oct 1, 2026
4db824c
Pin the PDP image of the required e2e jobs by version and digest
zeevmoney Oct 1, 2026
7fb2b77
Set timeouts on the pytest and compatibility jobs
zeevmoney Oct 1, 2026
dd82004
Fail a release whose wheel or sdist ships more than permit
zeevmoney Oct 1, 2026
a170bb9
Give each release job a timeout
zeevmoney Oct 1, 2026
16fe8a1
Run the e2e tests on the latest PDP image and on the cloud PDP
zeevmoney Oct 1, 2026
e8eb692
Merge the release publishing changes for PER-16676
zeevmoney Oct 1, 2026
6ff940e
Merge the CI PDP pin and PDP legs for PER-16676
zeevmoney Oct 1, 2026
23a20d4
Check the CI-built wheel and sdist for extra packages too
zeevmoney Oct 1, 2026
d1f8455
Say why PyPI accepts the release upload without a token
zeevmoney Oct 1, 2026
e77f4a4
Describe the release workflow's jobs and trusted publishing
zeevmoney Oct 1, 2026
0d48ed7
Describe the pinned, latest and cloud PDP e2e jobs
zeevmoney Oct 1, 2026
f3e7221
Name the CI job that runs the cloud PDP tests
zeevmoney Oct 1, 2026
3552bd7
Require the cloud PDP's 501 in the cloud PDP tests
zeevmoney Oct 1, 2026
3764512
Count the four e2e jobs in CONTRIBUTING.md
zeevmoney Oct 1, 2026
2f68a35
Say that the pypi environment limits uploads to release tags
zeevmoney Oct 1, 2026
fb8f928
Run the Test and Security workflows on every PR, whatever its base
zeevmoney Oct 1, 2026
f8192ab
Test offline that a PDP's 501 makes the SDK raise
zeevmoney Oct 1, 2026
22d3c8e
Test real decisions on the cloud PDP instead of a 501
zeevmoney Oct 1, 2026
c241a0a
Expect the cloud PDP's tenant-association role in user permissions
zeevmoney Oct 1, 2026
2595538
Invite with a role of the invited resource in the invites e2e test
zeevmoney Oct 1, 2026
8cbc891
Poll for the PDP's role assignment list in the RBAC e2e tests
zeevmoney Oct 1, 2026
ced83e0
Poll for the PDP's authorized users in the RBAC e2e test
zeevmoney Oct 2, 2026
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
80 changes: 56 additions & 24 deletions .github/workflows/python-sdk-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ jobs:
build:
name: Build distribution
runs-on: ubuntu-24.04
# Each job here usually takes under a minute. The timeouts end a hung
# step long before GitHub's default of six hours.
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand Down Expand Up @@ -77,7 +80,14 @@ jobs:
# published: the wheel, and the sdist, since a wheel built from the sdist
# (pip install --no-binary, a distribution's packager) holds only what
# the sdist does.
- name: Check the wheel and sdist ship their type information
#
# Nor may either one ship a package other than permit: permit 2.8.3's
# wheel installed a top-level `tests` package, which shadowed the
# consumer's own `tests` module. [tool.uv.build-backend] in
# pyproject.toml keeps it out now; this check fails the release if it
# comes back. test.yml's compatibility job runs the same check on pull
# requests to main; keep the two identical.
- name: Check the wheel and sdist contents
run: |
set -euo pipefail
uv run --no-project python - dist <<'PY'
Expand All @@ -86,22 +96,39 @@ jobs:
import zipfile
from pathlib import Path

REQUIRED = ["permit/py.typed", "permit/_sync_types.pyi"]


def check(artifact: Path, names: set[str], found: set[str], allowed: set[str]) -> None:
missing = [path for path in REQUIRED if path not in names]
if missing:
sys.exit(f"{artifact.name} is missing {missing}")
unexpected = sorted(found - allowed)
if unexpected:
sys.exit(f"{artifact.name} holds {unexpected}; only {sorted(allowed)} may ship")
print(f"{artifact.name} ships {' and '.join(REQUIRED)} and no package beside permit")


dist = Path(sys.argv[1])
wheels = sorted(dist.glob("*.whl"))
sdists = sorted(dist.glob("*.tar.gz"))
if len(wheels) != 1 or len(sdists) != 1:
found = [path.name for path in wheels + sdists]
sys.exit(f"expected one wheel and one sdist in {dist}, found {found}")
required = ["permit/py.typed", "permit/_sync_types.pyi"]
contents = {wheels[0]: set(zipfile.ZipFile(wheels[0]).namelist())}
# Every sdist path starts with its top-level permit-<version>/ directory.
with tarfile.open(sdists[0]) as sdist:
contents[sdists[0]] = {name.partition("/")[2] for name in sdist.getnames()}
for artifact, names in contents.items():
missing = [path for path in required if path not in names]
if missing:
sys.exit(f"{artifact.name} is missing {missing}")
print(f"{artifact.name} ships {' and '.join(required)}")
wheel, sdist = wheels[0], sdists[0]

# A wheel's top level is what lands in site-packages: permit/ and its
# permit-<version>.dist-info, named after permit-<version>-<tags>.whl.
with zipfile.ZipFile(wheel) as wheel_file:
names = set(wheel_file.namelist())
dist_info = "-".join(wheel.name.split("-")[:2]) + ".dist-info"
check(wheel, names, {name.split("/")[0] for name in names}, {"permit", dist_info})

# Every sdist path starts with its permit-<version>/ directory. Below
# it, the files are metadata and docs, and the only directory is permit/.
with tarfile.open(sdist) as sdist_file:
names = {name.partition("/")[2] for name in sdist_file.getnames()}
check(sdist, names, {name.split("/")[0] for name in names if "/" in name}, {"permit"})
PY

- name: Upload distribution
Expand All @@ -114,6 +141,7 @@ jobs:
scan:
name: Security Gate
runs-on: ubuntu-24.04
timeout-minutes: 15
needs: [build]
steps:
- name: Checkout code
Expand Down Expand Up @@ -150,6 +178,12 @@ jobs:
# See the same step in security.yml: this only installs Trivy, and
# hide-progress keeps its empty scan from logging a warning.
hide-progress: true
# The action's cache is on by default and restores the Trivy binary
# itself from the Actions cache, with no checksum check, so a cache
# entry would decide which scanner the release gate runs. Off: every
# release downloads the binary and checks it against the checksums
# of its Trivy release.
cache: false
# The migration skill's sample apps pin vulnerable versions on
# purpose and are never installed (skills/tests/README.md).
skip-dirs: skills/tests/fixtures
Expand Down Expand Up @@ -209,15 +243,21 @@ jobs:
publish:
name: Publish to PyPI
runs-on: ubuntu-24.04
timeout-minutes: 10
needs: [scan]
# PyPI trusted publishing: this job holds no PyPI token. PyPI accepts its
# upload because this repository, this workflow file and this environment
# are registered as a trusted publisher of the permit project on pypi.org.
# Renaming any of the three stops releases until that registration is
# changed to match. PyPI does not check which branch or tag the job ran
# from, so a branch that edits this file to run on push could upload too.
# What limits uploads to release tags is the pypi environment's deployment
# rules, a repository setting, not anything in this file.
environment:
name: pypi
url: https://pypi.org/p/permit
permissions:
# id-token is what lets gh-action-pypi-publish attach PEP 740 build
# attestations. contents/pull-requests write were previously granted and
# never used -- nothing in this workflow commits or opens a PR.
id-token: write
id-token: write # OIDC token PyPI exchanges for an upload token; also signs attestations
steps:
# NODE_OPTIONS: the unzip library download-artifact v8.0.1 bundles still
# calls the deprecated Buffer() constructor, so every download prints
Expand All @@ -231,14 +271,6 @@ jobs:
name: dist
path: dist/

# No password: with no token given, the action authenticates by OIDC.
- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
with:
# zizmor: ignore[use-trusted-publishing]
# TODO: migrate to PyPI Trusted Publishing (OIDC) and drop this
# secret. That cannot be done from this repo alone -- it requires
# registering permitio/permit-python + this workflow filename +
# the "pypi" environment as a trusted publisher on PyPI first.
# Flipping the workflow before that is configured would break the
# next release, so it is deliberately left as a follow-up.
password: ${{ secrets.PYPI_TOKEN }}
4 changes: 2 additions & 2 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ on:
# a required check that never runs as perpetually pending rather than
# passing, so a path filter here would block every PR that happens not to
# touch a dependency file. The audit takes under two minutes, which is
# cheaper than that failure mode.
# cheaper than that failure mode. Not base-filtered either: a stacked PR,
# whose base is another PR's branch, gets the same checks before it merges.
pull_request:
branches: [main, master]
# Run on every merge to main too, so a regression is surfaced immediately
# (failed run on main) rather than waiting for the next PR to trip over it.
# No PR comment is posted on push; the job summary carries the detail.
Expand Down
Loading