Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
fb5b531
Bench: Add Cython (pure and typed) and numba columns to the TypedPyth…
b-re-w Oct 3, 2026
b159f27
TypedPython: decode cl.exe output in the console code page
b-re-w Oct 29, 2026
460a267
Merge pull request #351 from thisisthepy/feat/typedpython-msvc-output…
b-re-w Oct 29, 2026
2a75e57
TypedPython: publish the package to PyPI by trusted publishing
b-re-w Oct 29, 2026
6eb83b1
Merge pull request #355 from thisisthepy/feat/typedpython-pypi
b-re-w Oct 29, 2026
a59096f
TypedPython: @compiled is the only mark; drop the '# typedpython: com…
b-re-w Oct 29, 2026
f702f48
TypedPython: no PyPI publishing; the wheel ships with the GitHub Rele…
b-re-w Oct 29, 2026
66a1410
Merge pull request #356 from thisisthepy/feat/typedpython-no-pypi
b-re-w Oct 29, 2026
cca2ef5
TypedPython tests: child processes install the compiled builtin thems…
b-re-w Oct 29, 2026
cc14724
Merge pull request #357 from thisisthepy/feat/typedpython-drop-module…
b-re-w Oct 29, 2026
0e54f8e
Feat: replace # typedpython: parallel comment with @compiled(parallel…
b-re-w Oct 31, 2026
cf96f26
Merge pull request #359 from thisisthepy/feat/typedpython-compiled-pa…
b-re-w Oct 31, 2026
8824385
Feat: add per-function @compiled(fastmath=True) opt-in (#332)
b-re-w Oct 31, 2026
1463c75
Merge pull request #360 from thisisthepy/feat/typedpython-fastmath
b-re-w Oct 31, 2026
2b1bac3
TypedPython: TSan test ignores races against new_threadstate reusing …
b-re-w Oct 31, 2026
5d81106
Merge pull request #361 from thisisthepy/feat/tsan-new-threadstate
b-re-w Oct 31, 2026
371e967
Test: Make mid() loop heavier in test_eval_breaker to avoid CI timing…
b-re-w Nov 1, 2026
185965c
Merge pull request #362 from thisisthepy/feat/tp-eval-breaker-flake
b-re-w Nov 1, 2026
c14d264
Feat: Switch notebook-e2e native-image CI to Maven Central and stage …
b-re-w Nov 1, 2026
c92c58b
Merge pull request #363 from thisisthepy/feat/phase-a-native-image-ci
b-re-w Nov 1, 2026
afec35c
Fix: Point Gradle toolchain resolution at GRAALVM_HOME in native-imag…
b-re-w Nov 4, 2026
83a7d0b
Merge pull request #364 from thisisthepy/feat/fix-native-image-ci-too…
b-re-w Nov 4, 2026
5b7ac9d
TypedPython: cache generated C for the pypackpack compile slot (#113)
b-re-w Nov 4, 2026
c50b53d
Merge pull request #365 from thisisthepy/feat/tp-c-cache
b-re-w Nov 4, 2026
831174c
Fix: Disable Gradle toolchain auto-detect in native-image CI so Liber…
b-re-w Nov 4, 2026
5464619
Merge pull request #366 from thisisthepy/feat/disable-toolchain-autod…
b-re-w Nov 4, 2026
693252c
Fix: Require BellSoft vendor for JDK 25 native-image toolchain (#251)
b-re-w Nov 4, 2026
7add3ea
Merge pull request #367 from thisisthepy/feat/bellsoft-vendor-native-…
b-re-w Nov 4, 2026
986c736
TypedPython: narrow GCC fastmath to reassociation and FMA contract (#…
b-re-w Nov 5, 2026
decc90e
Feat: add macosArm64 and linuxX64 Kotlin/Native desktop targets (#191)
b-re-w Nov 5, 2026
9394936
Merge pull request #370 from thisisthepy/feat/kn-desktop-targets
b-re-w Nov 5, 2026
5d93fe9
TypedPython test: ignore a probe that succeeds only after the call re…
b-re-w Nov 5, 2026
8ccfda4
TypedPython: fill a fresh New's slots by direct release stores (#125)
b-re-w Nov 5, 2026
6f7b669
TypedPython: MSVC fastmath enables FMA contraction only (#368)
b-re-w Nov 5, 2026
eb17d99
TypedPython: emit-c CLI and manifest for pypackpack's compile slot (#…
b-re-w Nov 5, 2026
c44a7d9
Merge pull request #371 from thisisthepy/feat/tp-new-fixed-direct
b-re-w Nov 5, 2026
23262de
Merge pull request #369 from thisisthepy/feat/tp-gcc-fastmath
b-re-w Nov 5, 2026
9975a41
Merge pull request #372 from thisisthepy/feat/tp-emit-c
b-re-w Nov 5, 2026
8f27c26
Feat: add TypedPythonEmitCTask and manifest v1 mapper in Gradle plugi…
b-re-w Nov 7, 2026
c8ad08c
Merge pull request #373 from thisisthepy/feat/tp-emit-c-gradle
b-re-w Nov 7, 2026
3c2ed3d
Feat(gradle-plugin): register emitTypedPythonC from pythonBindings DS…
b-re-w Nov 7, 2026
a148c07
Merge pull request #374 from thisisthepy/feat/tp-emit-c-plugin-wiring
b-re-w Nov 7, 2026
e01d422
Publish the library, KSP processor and Gradle plugin to Maven Central…
b-re-w Nov 7, 2026
8fa5a33
maven-central: install uv for the typedpython-gate bundle (#375)
b-re-w Nov 7, 2026
71e4598
Merge pull request #376 from thisisthepy/feat/maven-central-publish
b-re-w Nov 7, 2026
cf90930
Release: bump to 3.15.0-alpha03, the first Maven Central release (#377)
b-re-w Nov 7, 2026
88bf6a1
Merge pull request #378 from thisisthepy/feat/release-alpha03
b-re-w Nov 7, 2026
ac5e0dd
Publish python-multiplatform-compose to Maven Central; drop stale And…
b-re-w Nov 7, 2026
684d3f1
Merge pull request #380 from thisisthepy/feat/publish-compose-central
b-re-w Nov 7, 2026
aade0f8
AGENTS 7: what counts as the owner's decision (shared rule)
b-re-w Nov 7, 2026
6cf78d2
Merge pull request #381 from thisisthepy/feat/owner-decision-rule
b-re-w Nov 7, 2026
9e0718b
AGENTS 3, 10: clean up on merge; agents only check compilation
b-re-w Nov 7, 2026
b48ac32
Merge pull request #382 from thisisthepy/feat/agent-build-rule
b-re-w Nov 7, 2026
7638d75
Merge remote-tracking branch 'origin/develop' into feat/bench-cython-…
b-re-w Nov 7, 2026
9507e6d
Merge pull request #384 from thisisthepy/feat/bench-cython-numba
b-re-w Nov 7, 2026
ece2da1
Release: sync from 9507e6dd53b96228da86373b267b1405484d572f 9507e6d
github-actions[bot] Nov 7, 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
83 changes: 83 additions & 0 deletions .github/scripts/release/central-bundle.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#!/usr/bin/env bash
# Builds the Maven Central deployment of python-multiplatform: every component, signed, in one zip.
# .github/scripts/release/central-bundle.sh <out.zip> [--throwaway-key]
# Run from the repository root. Reads MAVEN_GPG_PRIVATE_KEY and MAVEN_GPG_PASSPHRASE (Gradle signs
# in memory, gradle/central-publishing.gradle.kts). --throwaway-key makes a key for this run only,
# for a dry run that must prove signing works without the release key. Without either, the bundle is
# unsigned and the script says so; Central would refuse it.
# Writes build/central-staging (a Maven layout) and <out.zip>; uploads nothing, see central-upload.sh.
set -euo pipefail
out="${1:?usage: central-bundle.sh <out.zip> [--throwaway-key]}"
mode="${2:-}"
root="$(pwd)"
staging="$root/build/central-staging"
version="$(sed -nE 's/^pythonVersion=(.*)$/\1/p' gradle.properties)-$(sed -nE 's/^libraryVersionSuffix=(.*)$/\1/p' gradle.properties)"

# Not under the repository: gpg-agent's socket path must stay under about 100 characters.
verify_home="$(mktemp -d)"
chmod 700 "$verify_home"
trap 'rm -rf "$verify_home"' EXIT

if [[ "$mode" == "--throwaway-key" ]]; then
GNUPGHOME="$verify_home" gpg --batch --quiet --passphrase '' --quick-generate-key \
"python-multiplatform dry run <dry-run@invalid>" rsa3072 sign 1d
MAVEN_GPG_PRIVATE_KEY="$(GNUPGHOME="$verify_home" gpg --batch --pinentry-mode loopback --passphrase '' --armor --export-secret-keys)"
export MAVEN_GPG_PRIVATE_KEY MAVEN_GPG_PASSPHRASE=""
elif [[ -n "${MAVEN_GPG_PRIVATE_KEY:-}" ]]; then
GNUPGHOME="$verify_home" gpg --batch --quiet --import <<< "$MAVEN_GPG_PRIVATE_KEY"
fi
signed=false
[[ -n "${MAVEN_GPG_PRIVATE_KEY:-}" ]] && signed=true

mkdir -p "$root/.cache"
rm -rf "$staging"
# Gradle reads the key from the environment; the plugin build is a separate build but inherits it.
./gradlew --console=plain publishAllToCentralStaging 2>&1 | tee "$root/.cache/central-gradle.log" || { echo "error: gradle failed (pipe status ${PIPESTATUS[0]})" >&2; exit 1; }

# What Central checks first: every module has pom + jar/klib/zip, sources and javadoc where it is a
# jar publication, and (signed) a good .asc beside every file.
fail=0
while IFS= read -r pom; do
dir="$(dirname "$pom")"; base="$(basename "$pom" .pom)"
ver="$(basename "$dir")"; art="$(basename "$(dirname "$dir")")"
[[ "$ver" == "$version" ]] || { echo "error: $pom is not at $version" >&2; fail=1; }
grep -q '<url>https://github.com/thisisthepy/python-multiplatform</url>' "$pom" || { echo "error: $pom has no project url" >&2; fail=1; }
grep -q '<licenses>' "$pom" && grep -q '<developers>' "$pom" && grep -q '<scm>' "$pom" || { echo "error: $pom lacks licenses, developers or scm" >&2; fail=1; }
if ! grep -q '<packaging>pom</packaging>' "$pom"; then
[[ -e "$dir/$base-javadoc.jar" ]] || { echo "error: $art has no javadoc jar" >&2; fail=1; }
[[ -e "$dir/$base-sources.jar" ]] || { echo "error: $art has no sources jar" >&2; fail=1; }
fi
done < <(find "$staging" -name '*.pom')
# An AAR must carry the native libraries of this pythonVersion only. A Copy-staged jniLibs tree once
# kept libpython3.14.so beside libpython3.15.so after a version bump. libpython3.so (the stable-ABI
# stub) carries no minor version and is allowed.
py_mm="$(sed -nE 's/^pythonVersion=([0-9]+\.[0-9]+).*$/\1/p' gradle.properties)"
while IFS= read -r aar; do
stale="$(unzip -Z1 "$aar" | grep -E '(^|/)lib(multiplatform_)?python[0-9]+\.[0-9]+[^/]*\.so$' | grep -vE "(^|/)lib(multiplatform_)?python${py_mm//./\\.}(t)?\.so$" || true)"
[[ -z "$stale" ]] || { echo "error: $aar carries native libraries for another python than $py_mm:" >&2; echo "$stale" >&2; fail=1; }
done < <(find "$staging" -name '*.aar')
if $signed; then
while IFS= read -r f; do
case "$f" in *.asc|*.md5|*.sha1|*.sha256|*.sha512) continue ;; esac
[[ -e "$f.asc" ]] || { echo "error: $f is not signed" >&2; fail=1; continue; }
GNUPGHOME="$verify_home" gpg --batch --quiet --verify "$f.asc" "$f" 2>/dev/null || { echo "error: bad signature on $f" >&2; fail=1; }
done < <(find "$staging" -type f -not -name 'maven-metadata*')
else
echo "warning: no signing key, the bundle is unsigned and Central will refuse it" >&2
fi
(( fail == 0 )) || exit 1

echo "== left out of the bundle (not built on this machine)"
grep -h "CENTRAL-SKIPPED" "$root/.cache/central-gradle.log" || echo "(none)"
echo "== coordinates in the bundle"
find "$staging" -name '*.pom' | while IFS= read -r pom; do
rel="${pom#"$staging"/}"; ver="$(basename "$(dirname "$rel")")"; art="$(basename "$(dirname "$(dirname "$rel")")")"
grp="$(dirname "$(dirname "$(dirname "$rel")")" | tr / .)"
echo "$grp:$art:$ver"
done | sort

mkdir -p "$(dirname "$out")"
out="$(cd "$(dirname "$out")" && pwd)/$(basename "$out")"
rm -f "$out"
(cd "$staging" && zip -qr "$out" . -x 'maven-metadata*' '*/maven-metadata*')
echo "== bundle $out ($(find "$staging" -type f | wc -l | tr -d ' ') files)"
68 changes: 68 additions & 0 deletions .github/scripts/release/central-upload.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
# Uploads a bundle made by central_bundle.py to the Sonatype Central Portal and waits for its
# validation.
#
# Usage: central-upload.sh <bundle.zip> <deployment name> [USER_MANAGED|AUTOMATIC]
#
# Reads CENTRAL_TOKEN_USERNAME and CENTRAL_TOKEN_PASSWORD, the two halves of a Central Portal
# user token (central.sonatype.com, View Account, Generate User Token). The Portal API takes
# them as a bearer token of base64("<username>:<password>").
#
# USER_MANAGED (the default) stops after validation: the deployment waits in the Portal under
# Deployments until someone presses Publish or Drop, so a person looks at it before it becomes
# permanent. AUTOMATIC publishes as soon as validation passes. Either way nothing published to
# Central can be changed or removed afterwards.
#
# Exits 0 when the deployment is VALIDATED (USER_MANAGED) or PUBLISHING/PUBLISHED (AUTOMATIC),
# 1 when it FAILED, printing the Portal's errors, and 2 on a usage or network error.
set -euo pipefail

API="https://central.sonatype.com/api/v1/publisher"

bundle="${1:?usage: central-upload.sh <bundle.zip> <deployment name> [USER_MANAGED|AUTOMATIC]}"
name="${2:?usage: central-upload.sh <bundle.zip> <deployment name> [USER_MANAGED|AUTOMATIC]}"
type="${3:-USER_MANAGED}"
case "$type" in USER_MANAGED|AUTOMATIC) ;; *) echo "error: publishing type '$type'" >&2; exit 2 ;; esac
[[ -s "$bundle" ]] || { echo "error: no bundle at $bundle" >&2; exit 2; }
[[ -n "${CENTRAL_TOKEN_USERNAME:-}" && -n "${CENTRAL_TOKEN_PASSWORD:-}" ]] || {
echo "error: CENTRAL_TOKEN_USERNAME and CENTRAL_TOKEN_PASSWORD must hold a Central Portal user token" >&2
exit 2
}

token="$(printf '%s:%s' "$CENTRAL_TOKEN_USERNAME" "$CENTRAL_TOKEN_PASSWORD" | base64 | tr -d '\n')"
auth="Authorization: Bearer $token"

encoded_name="$(python3 -I -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1]))' "$name")"
echo "==> uploading $bundle as '$name' ($type)"
id="$(curl --fail-with-body --silent --show-error --retry 3 \
--header "$auth" \
--form "bundle=@$bundle" \
"$API/upload?publishingType=$type&name=$encoded_name")"
echo "deployment $id"

# Validation of a large bundle takes minutes; give it an hour.
for _ in $(seq 1 120); do
status="$(curl --fail-with-body --silent --show-error --retry 3 --request POST \
--header "$auth" "$API/status?id=$id")"
state="$(python3 -I -c 'import json, sys; print(json.load(sys.stdin)["deploymentState"])' <<< "$status")"
echo "state $state"
case "$state" in
VALIDATED)
if [[ "$type" == USER_MANAGED ]]; then
echo "validated; publish or drop it at https://central.sonatype.com/publishing/deployments"
exit 0
fi
;;
PUBLISHING|PUBLISHED)
echo "publishing; it appears on https://repo1.maven.org/maven2/org/thisisthepy/ within about 30 minutes"
exit 0
;;
FAILED)
python3 -I -c 'import json, sys; print(json.dumps(json.load(sys.stdin).get("errors"), indent=2))' <<< "$status" >&2
exit 1
;;
esac
sleep 30
done
echo "error: deployment $id is still $state after an hour; see https://central.sonatype.com/publishing/deployments" >&2
exit 2
65 changes: 33 additions & 32 deletions .github/workflows/desktop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,31 @@ jobs:
# Using --console=plain so logs are readable in GitHub Actions
run: ./gradlew :python-multiplatform:desktopTest --console=plain

desktop-native-linux:
name: Desktop Kotlin/Native Tests (Linux x86_64)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Java
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'zulu'
cache: 'gradle'

- name: Cache Konan
uses: actions/cache@v4
with:
path: ~/.konan
key: ${{ runner.os }}-konan-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-konan-

- name: Run Linux x86_64 Kotlin/Native Tests
run: ./gradlew :python-multiplatform:linuxX64Test --rerun --console=plain

# The dev agent's protocol on the other desktop OSes (#238). Its socket options differ per platform
# (SO_EXCLUSIVEADDRUSE on Windows, SO_REUSEADDR on POSIX), and the port-takeover and restart tests are
# what check them. Stdlib only: no JDK, no Gradle.
Expand Down Expand Up @@ -151,26 +176,15 @@ jobs:
python-multiplatform-compose/fixtures/notebook-e2e/build/reports/tests/desktopTest
python-multiplatform-compose/fixtures/notebook-e2e/build/test-results/desktopTest

# The notebook E2E in a GraalVM native image on macOS (#249, #251). It takes `ui`, `ui-text` and
# `foundation` from compose-multiplatform-core-extended, which a runner reads from the fork's GitHub
# Packages registry; until the repository variable COMPOSE_EXTENDED_VERSION names a published
# development version (`1.11.1-ext.N-dev.<sha>`) the job is skipped.
# The notebook E2E in a GraalVM native image on macOS (#249, #251). It takes the fork's Compose
# (`org.thisisthepy.compose.*:1.11.1-ext.2`, `composeExtended.*` in `gradle.properties`) from
# Maven Central, builds the 20-scenario executable with Liberica NIK 25, and runs it with
# JAVA_HOME and PYTHONHOME removed.
notebook-e2e-native-image:
name: Notebook E2E in a native image (macOS)
if: ${{ vars.COMPOSE_EXTENDED_VERSION != '' }}
runs-on: macos-latest
permissions:
contents: read
packages: read
env:
WHEEL_DIR: ${{ github.workspace }}/.tmp/pythonx-compose-dist
COMPOSE_EXTENDED_VERSION: ${{ vars.COMPOSE_EXTENDED_VERSION }}
COMPOSE_EXTENDED_GROUP_PREFIX: ${{ vars.COMPOSE_EXTENDED_GROUP_PREFIX || 'org.thisisthepy.compose' }}
COMPOSE_EXTENDED_REPOSITORY: ${{ vars.COMPOSE_EXTENDED_REPOSITORY || 'https://maven.pkg.github.com/thisisthepy/compose-multiplatform-core-extended' }}
# GitHub Packages asks for a token even for a public package. A read:packages PAT in the secret
# GH_PACKAGES_TOKEN is used when it exists; otherwise this repository's own token.
GITHUB_TOKEN: ${{ secrets.GH_PACKAGES_TOKEN || secrets.GITHUB_TOKEN }}
TOKEN_SOURCE: ${{ secrets.GH_PACKAGES_TOKEN != '' && 'GH_PACKAGES_TOKEN' || 'GITHUB_TOKEN' }}
steps:
- name: Checkout
uses: actions/checkout@v4
Expand All @@ -183,19 +197,6 @@ jobs:
path: .tmp/pythonx-compose
fetch-depth: 1

# Before any build: can this token read the fork's packages? One known POM, so a refusal is
# reported as what it is rather than as a dependency resolution error minutes later.
- name: Check access to the fork's packages
run: |
group_path="${COMPOSE_EXTENDED_GROUP_PREFIX//.//}/ui"
pom="$COMPOSE_EXTENDED_REPOSITORY/$group_path/ui-desktop/$COMPOSE_EXTENDED_VERSION/ui-desktop-$COMPOSE_EXTENDED_VERSION.pom"
status=$(curl -s -o /dev/null -w '%{http_code}' -u "${GITHUB_ACTOR}:${GITHUB_TOKEN}" "$pom")
echo "reading $pom with $TOKEN_SOURCE: HTTP $status"
if [ "$status" != "200" ]; then
echo "::error::The fork's packages could not be read with $TOKEN_SOURCE (HTTP $status at $pom). Add a personal access token (classic) with read:packages as the repository secret GH_PACKAGES_TOKEN, or check COMPOSE_EXTENDED_VERSION / COMPOSE_EXTENDED_REPOSITORY."
exit 1
fi

- name: Setup uv
uses: astral-sh/setup-uv@v6

Expand All @@ -220,13 +221,13 @@ jobs:

# Records Compose and skiko's configuration against the fork version this run uses, then builds
# the image and runs it with JAVA_HOME and PYTHONHOME removed: 20 of 20 and no libjvm.
# `setup-graalvm` installs Liberica NIK under `GRAALVM_HOME` in the runner toolcache outside
# `/Users/runner/hostedtoolcache/Java_*`, so pass `org.gradle.java.installations.paths`
# explicitly so Gradle's BellSoft JDK 25 toolchain resolution finds it.
- name: Build and run the native image
run: >
./gradlew :python-multiplatform-compose:fixtures:notebook-e2e:notebookE2eNativeAgent
:python-multiplatform-compose:fixtures:notebook-e2e:notebookE2eNativeRun
-PpythonxComposeWheel="$WHEEL_DIR"
-PcomposeExtended.version="$COMPOSE_EXTENDED_VERSION"
-PcomposeExtended.groupPrefix="$COMPOSE_EXTENDED_GROUP_PREFIX"
-PcomposeExtended.mavenLocal=false
-PcomposeExtended.repository="$COMPOSE_EXTENDED_REPOSITORY"
-Porg.gradle.java.installations.paths="$GRAALVM_HOME"
--console=plain
144 changes: 144 additions & 0 deletions .github/workflows/maven-central.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
name: Maven Central

# Publishes python-multiplatform (library for every target, KSP processor, Gradle plugin and its
# plugin marker) to Maven Central through the Sonatype Central Portal, as ONE deployment.
#
# - A version tag (v<pythonVersion>-<libraryVersionSuffix>, the tag desktop-release.yml also reads)
# builds, signs with the release key and uploads.
# - A manual run is a dry run unless "dry_run" is turned off: it builds and signs with a key made in
# the job and thrown away, checks the bundle and keeps it as a workflow artifact. Nothing is
# uploaded. With dry_run off, pick the tag to publish in "tag".
#
# macOS is required: the Apple targets (iosArm64, iosSimulatorArm64, iosX64, macosArm64) only build
# there. Secrets read by a real upload, nothing else reads them:
# MAVEN_GPG_PRIVATE_KEY signing key, ASCII armoured (gpg --armor --export-secret-keys)
# MAVEN_GPG_PASSPHRASE its passphrase
# CENTRAL_TOKEN_USERNAME Central Portal user token, username half
# CENTRAL_TOKEN_PASSWORD Central Portal user token, password half
# The upload runs in the environment "maven-central", where required reviewers can be set.

on:
push:
tags:
- 'v*'
# A pull request that touches the publishing setup runs the dry run, so the bundle is checked
# before the first tag depends on it.
pull_request:
paths:
- '.github/workflows/maven-central.yml'
- '.github/scripts/release/central-*.sh'
- 'gradle/central-publishing.gradle.kts'
workflow_dispatch:
inputs:
dry_run:
description: 'Build and sign with a throwaway key, upload nothing'
type: boolean
default: true
tag:
description: 'Tag to publish when dry_run is off (e.g. v3.15.0-alpha02); ignored for a dry run'
required: false
default: ''
publishing_type:
description: 'USER_MANAGED waits in the Central Portal for someone to press Publish; AUTOMATIC publishes once validated'
type: choice
options: [USER_MANAGED, AUTOMATIC]
default: USER_MANAGED

permissions:
contents: read

concurrency:
group: maven-central-${{ github.event.inputs.tag || github.ref }}
cancel-in-progress: false

jobs:
bundle:
name: Build and sign the bundle (macOS)
runs-on: macos-latest
timeout-minutes: 180
env:
DRY_RUN: ${{ github.event_name == 'pull_request' || (github.event_name == 'workflow_dispatch' && inputs.dry_run) }}
TAG: ${{ github.event.inputs.tag || github.ref_name }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ (github.event_name == 'push' || inputs.dry_run == false) && (github.event.inputs.tag || github.ref) || github.ref }}
# The tag's own gradle.properties decides the version, not the branch it was cut from.

- name: Setup Java
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'zulu'

- name: Setup uv
# The KSP processor's typedpython-gate bundle is built with uv build.
uses: astral-sh/setup-uv@v6

- name: The tag names this build's version
if: env.DRY_RUN != 'true'
run: |
set -euo pipefail
version="$(sed -nE 's/^pythonVersion=(.*)$/\1/p' gradle.properties)-$(sed -nE 's/^libraryVersionSuffix=(.*)$/\1/p' gradle.properties)"
test "$TAG" = "v$version" || { echo "error: tag $TAG does not match version $version" >&2; exit 1; }
echo "VERSION=$version" >> "$GITHUB_ENV"

- name: Build and sign the bundle (dry run, throwaway key)
if: env.DRY_RUN == 'true'
run: .github/scripts/release/central-bundle.sh "$RUNNER_TEMP/central-bundle.zip" --throwaway-key

- name: Build and sign the bundle (release key)
if: env.DRY_RUN != 'true'
env:
MAVEN_GPG_PRIVATE_KEY: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }}
MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }}
run: |
set -euo pipefail
[[ -n "$MAVEN_GPG_PRIVATE_KEY" ]] || { echo "::error::the MAVEN_GPG_PRIVATE_KEY secret is not set"; exit 1; }
.github/scripts/release/central-bundle.sh "$RUNNER_TEMP/central-bundle.zip"

- name: Keep the bundle
uses: actions/upload-artifact@v4
with:
name: central-bundle
path: ${{ runner.temp }}/central-bundle.zip
retention-days: 14


upload:
name: Upload to the Central Portal
needs: bundle
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.dry_run == false)
runs-on: ubuntu-24.04
timeout-minutes: 90
# Required reviewers can be set on this environment, so even a pushed tag waits for a person
# before anything reaches Central. A dry run never gets here.
environment: maven-central
env:
TAG: ${{ github.event.inputs.tag || github.ref_name }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.tag || github.ref }}
sparse-checkout: |
.github/scripts/release
gradle.properties
sparse-checkout-cone-mode: false

- uses: actions/download-artifact@v4
with:
name: central-bundle
path: ${{ runner.temp }}

- name: Upload
env:
CENTRAL_TOKEN_USERNAME: ${{ secrets.CENTRAL_TOKEN_USERNAME }}
CENTRAL_TOKEN_PASSWORD: ${{ secrets.CENTRAL_TOKEN_PASSWORD }}
TYPE: ${{ inputs.publishing_type || 'USER_MANAGED' }}
run: |
set -euo pipefail
version="$(sed -nE 's/^pythonVersion=(.*)$/\1/p' gradle.properties)-$(sed -nE 's/^libraryVersionSuffix=(.*)$/\1/p' gradle.properties)"
.github/scripts/release/central-upload.sh "$RUNNER_TEMP/central-bundle.zip" \
"python-multiplatform $version" "$TYPE"
Loading