Skip to content

Publish Python Package to PyPI #2

Publish Python Package to PyPI

Publish Python Package to PyPI #2

name: Publish Python Package to PyPI
on:
# Manual trigger only - no automatic publishing
workflow_dispatch:
inputs:
package:
description: 'Package to publish'
required: true
type: choice
options:
- sdks/python
- integrations/adk-middleware/python
- integrations/agent-spec/python
- integrations/aws-strands/python
- integrations/crew-ai/python
- integrations/langgraph/python
dry_run:
description: 'Run in dry-run mode (build without uploading)'
required: false
type: boolean
default: false
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # Required for PyPI trusted publishing
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Authorize actor against CODEOWNERS
env:
ACTOR: ${{ github.actor }}
PACKAGE: ${{ inputs.package }}
run: |
node << 'SCRIPT'
const fs = require('fs');
const actor = process.env.ACTOR;
const pkg = process.env.PACKAGE;
// Parse CODEOWNERS
const lines = fs.readFileSync('.github/CODEOWNERS', 'utf-8').split('\n');
const rules = [];
let rootOwners = [];
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const parts = trimmed.split(/\s+/);
const pattern = parts[0];
const owners = parts.slice(1).map(o => o.replace('@', ''));
if (pattern === '*') {
rootOwners = owners;
} else {
rules.push({ pattern, owners });
}
}
// Find the most specific matching rule for this package path
// Strip trailing /python to match CODEOWNERS entries like "integrations/adk-middleware"
const pathsToCheck = [pkg];
const pythonSuffix = '/python';
if (pkg.endsWith(pythonSuffix)) {
pathsToCheck.push(pkg.slice(0, -pythonSuffix.length));
}
let authorized = false;
let matchedRule = null;
for (const checkPath of pathsToCheck) {
for (const rule of rules) {
const pattern = rule.pattern.replace(/\/$/, '');
if (checkPath === pattern || checkPath.startsWith(pattern + '/')) {
matchedRule = rule;
authorized = rule.owners.includes(actor);
break;
}
}
if (matchedRule) break;
}
// Fall back to root owners if no specific rule matched
if (!matchedRule) {
matchedRule = { pattern: '*', owners: rootOwners };
authorized = rootOwners.includes(actor);
}
console.log(`Actor: ${actor}`);
console.log(`Package: ${pkg}`);
console.log(`Matched rule: ${matchedRule.pattern} -> ${matchedRule.owners.join(', ')}`);
console.log(`Authorized: ${authorized}`);
if (!authorized) {
console.error(`\nERROR: ${actor} is not a CODEOWNERS owner for ${pkg}`);
console.error(`Allowed users: ${matchedRule.owners.join(', ')}`);
process.exit(1);
}
SCRIPT
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
version: ">=0.8.0"
- name: Set up Python
run: uv python install 3.12
- name: Install dependencies
working-directory: ${{ inputs.package }}
run: uv sync
- name: Run tests
working-directory: ${{ inputs.package }}
run: uv run python -m pytest
- name: Build package
working-directory: ${{ inputs.package }}
run: uv build
- name: Verify wheel permissions
working-directory: ${{ inputs.package }}
run: |
echo "## Wheel file permissions check"
uv run python -c "
import zipfile, glob, sys
whl = glob.glob('dist/*.whl')[0]
print(f'Checking {whl}')
bad = []
for info in zipfile.ZipFile(whl).infolist():
perms = (info.external_attr >> 16) & 0o777
readable = perms & 0o444
print(f' {oct(perms):>8s} {info.filename}')
if not readable:
bad.append(info.filename)
if bad:
print(f'ERROR: {len(bad)} file(s) missing read permissions:')
for f in bad:
print(f' - {f}')
sys.exit(1)
print('All files have correct permissions.')
"
- name: Publish to PyPI
if: inputs.dry_run == false
working-directory: ${{ inputs.package }}
run: uv publish
env:
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
- name: Summary
if: success()
working-directory: ${{ inputs.package }}
run: |
NAME=$(uv run python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['name'])")
VERSION=$(uv run python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
if [ "${{ inputs.dry_run }}" = "true" ]; then
echo "## ✅ Dry-run Complete!" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Built **${NAME} ${VERSION}** successfully. No artifacts were uploaded." >> $GITHUB_STEP_SUMMARY
else
echo "## ✅ Published ${NAME} ${VERSION} to PyPI" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Install with: \`pip install ${NAME}==${VERSION}\`" >> $GITHUB_STEP_SUMMARY
fi