diff --git a/.VERSION b/.VERSION new file mode 100644 index 00000000..9e11b32f --- /dev/null +++ b/.VERSION @@ -0,0 +1 @@ +0.3.1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..b5863fd3 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,172 @@ +name: Release Python SDKs + +on: + workflow_dispatch: + inputs: + version: + description: "New version for the SDKs in the format of M.m.p" + required: true + type: string + build_number: + description: "New build number for the SDKs in the format of Mmmppbb " + required: true + type: string + +jobs: + prepare-release: + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/heads/sdk-core/') # Only run on branches that start with sdk-core/ + env: + SDK_VERSION: ${{ github.event.inputs.version }} + SDK_BUILD_NUMBER: ${{ github.event.inputs.build_number }} + steps: + - name: Checkout the code + uses: actions/checkout@v4 + + - name: Import GPG key + uses: crazy-max/ghaction-import-gpg@v6 + with: + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + passphrase: ${{ secrets.PASSPHRASE }} + git_user_signingkey: true + git_commit_gpgsign: true + + - name: Run the Prep Release Script + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SDK_CI: 1 + run: | + make prep-release VERSION=${{ env.SDK_VERSION }} BUILD_NUMBER=${{ env.SDK_BUILD_NUMBER }} + shell: bash + + build-wheels: + name: Build wheels for Python SDK on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + if: startsWith(github.ref, 'refs/heads/sdk-core/') + needs: [prepare-release] + strategy: + # we don't want all of them failing if one fails + fail-fast: false + matrix: + # macOS 13 is an Intel runner and macOS 14 is an Apple Silicon runner + os: [ubuntu-22.04, ubuntu-22.04-arm, windows-latest, macos-13, macos-14] + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.ref }} + + - name: Upgrade build dependencies + run: python -m pip install --upgrade pip setuptools wheel + + # Need to grab the SDK version for the wheel name + - name: Extract SDK Version + run: echo "SDK_VERSION=$(cat .VERSION)" >> "$GITHUB_ENV" + shell: bash + + - name: Install cibuildwheel + run: | + python -m pip install cibuildwheel + + - name: Build wheels + env: + # Skip all the unneeded images like PyPy and musllinux images + CIBW_SKIP: pp* *-musllinux_* + # Set the manylinux X86_64/aarch64 image to this specific 2.34 image + CIBW_MANYLINUX_X86_64_IMAGE: "quay.io/pypa/manylinux_2_34_x86_64" + CIBW_MANYLINUX_AARCH64_IMAGE: "quay.io/pypa/manylinux_2_34_aarch64" + # Set the wheel to the native archtiecture (output of platform.machine() which we use in the setup.py script) + CIBW_ARCHS: "native" + # Windows reparing of wheels are not supported so manually install it. + CIBW_BEFORE_BUILD_WINDOWS: "pip install delvewheel" + CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: "delvewheel repair -w {dest_dir} {wheel}" + # Dependencies required for testing each wheel + CIBW_TEST_REQUIRES: "pydantic pytest pytest-asyncio" + # The minimum MacOS version for darwin wheels (matches OPH) + MACOSX_DEPLOYMENT_TARGET: "12.0" + # The command to test every wheel + CIBW_TEST_COMMAND: "python -m pytest {project}/src/onepassword/test_client.py" + # Pass the service account token in all wheel buildings for testing of the wheels. + CIBW_ENVIRONMENT: OP_SERVICE_ACCOUNT_TOKEN=${{ secrets.TEST_SERVICE_ACCOUNT_TOKEN }} + run: | + python -m cibuildwheel --output-dir dist + + - uses: actions/upload-artifact@v4 + with: + name: onepassword-sdk-${{ env.SDK_VERSION }}-${{ matrix.os }} + path: ./dist/*.whl + + build-sdist: + name: Build source distribution for Python SDK + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/heads/sdk-core/') + needs: [prepare-release] + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.ref }} + # Need to grab the SDK version for the wheel name + - name: Extract SDK Version + run: echo "SDK_VERSION=$(cat .VERSION)" >> "$GITHUB_ENV" + shell: bash + + - name: Install dependencies + run: pip3 install build pydantic pytest pytest-asyncio + + - name: Build source distribution + run: python3 -m build --sdist + + - name: Test Source Distribution + env: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.TEST_SERVICE_ACCOUNT_TOKEN }} + run: | + python3 -m pip install dist/*.tar.gz + python3 -m pytest src/onepassword/test_client.py + + - uses: actions/upload-artifact@v4 + with: + name: onepassword-sdk-${{ env.SDK_VERSION }} + path: ./dist/*.tar.gz + + Release-SDK: + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/heads/sdk-core/') # Only run on branches that start with sdk-core/ + needs: [build-wheels, build-sdist] + steps: + - name: Checkout the code + uses: actions/checkout@v4 + with: + ref: ${{ github.ref }} + + - name: Import GPG key + uses: crazy-max/ghaction-import-gpg@v6 + with: + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + passphrase: ${{ secrets.PASSPHRASE }} + git_user_signingkey: true + git_commit_gpgsign: true + git_tag_gpgsign: true + + - name: Run the Release Script + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: make release + shell: bash + + publish-to-pypi: + name: Publish to PyPI + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/heads/sdk-core/') + environment: + name: pypi + url: https://pypi.org/project/onepassword-sdk/ + permissions: + id-token: write # IMPORTANT: this permission is mandatory for trusted publishing + needs: [Release-SDK] + steps: + - uses: actions/download-artifact@v4 + with: + pattern: onepassword-sdk-* + path: ./dist + merge-multiple: true + - name: Publish package distributions to PyPi + uses: pypa/gh-action-pypi-publish@release/v1.12 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..43d85305 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,33 @@ +[build-system] +requires = ["setuptools>=66", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "onepassword-sdk" +dynamic = ["version"] +description = "The 1Password Python SDK offers programmatic read access to your secrets in 1Password in an interface native to Python." +authors = [{ name = "1Password" }] +license = { file = "LICENSE" } +readme = "README.md" +requires-python = ">=3.9" +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Operating System :: MacOS", + "Operating System :: POSIX :: Linux", + "Operating System :: Microsoft :: Windows", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "License :: OSI Approved :: MIT License", +] +dependencies = [ + "pydantic>=2.5", +] + +[project.urls] +Homepage = "https://github.com/1Password/onepassword-sdk-python" + +[tool.setuptools.dynamic] +version = {file = "./.VERSION"} diff --git a/setup.py b/setup.py index 9e4a7b7a..da45642b 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,5 @@ -from pathlib import Path -from setuptools import setup, find_packages +from setuptools import setup, find_packages, Distribution from sysconfig import get_platform -from version import SDK_VERSION import platform import os @@ -19,6 +17,9 @@ def finalize_options(self): except ImportError: bdist_wheel = None +class BinaryDistribution(Distribution): + def has_ext_modules(self): + return True def get_shared_library_data_to_include(): # Return the correct uniffi C shared library extension for the given platform @@ -46,35 +47,11 @@ def get_shared_library_data_to_include(): setup( - name="onepassword-sdk", - version=SDK_VERSION, - author="1Password", - long_description=(Path(__file__).parent / "README.md").read_text(), - long_description_content_type="text/markdown", - description="The 1Password Python SDK offers programmatic read access to your secrets in 1Password in an interface native to Python.", - url="https://github.com/1Password/onepassword-sdk-python", packages=find_packages( where="src", ), - license="MIT", - license_files="LICENSE", + distclass=BinaryDistribution, package_dir={"": "src"}, - python_requires=">=3.9", - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Operating System :: MacOS", - "Operating System :: POSIX :: Linux", - "Operating System :: Microsoft :: Windows", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "License :: OSI Approved :: MIT License", - ], cmdclass={"bdist_wheel": bdist_wheel}, package_data={"": get_shared_library_data_to_include()}, - install_requires=[ - "pydantic>=2.5", # Minimum Pydantic version to run the Python SDK - ], ) diff --git a/src/release/README.md b/src/release/README.md index 8c6d4683..b7e07c7a 100644 --- a/src/release/README.md +++ b/src/release/README.md @@ -1,5 +1,15 @@ -## How to Prepare a Release for the Python SDK +# How to Release the Python SDK +## Release off an Release Candidate Branch in Github Actions +To release the Python SDK via Github Action, you must do the following: +1. SDK core opens a new PR with the latest generated code and latest core. This branch should start off with `sdk-core/...` +2. Add the release notes for the RC as well as update the examples if needed. +3. Run the `Release Python SDKs` action and input the correct build and version number while referencing the RC branch. +4. After the action is completed, the Python SDK is released on Github and PyPi, you can merge the PR branch. + +If the Github Action isn't working, you can follow the manual steps below to release the Python SDK. + +## Manual Steps to release a Python SDK Before running this script, the user must make sure that they have the write permissions to the Python SDK repository. Run this make command to install all dependencies required for the Python SDK release process. @@ -10,7 +20,7 @@ release/install-dependencies Step 1. Make any changes to the SDK as required on a feature branch or main branch. NOTE: If ran on a main branch, a release branch will be created. -Step 2. Go to the root of the repo and run +Step 2. Go to the root of the repo and run ``` make prep-release ``` diff --git a/src/release/scripts/build-wheels.sh b/src/release/scripts/build-wheels.sh index 484875d3..4d1e5a7b 100755 --- a/src/release/scripts/build-wheels.sh +++ b/src/release/scripts/build-wheels.sh @@ -2,27 +2,21 @@ # Helper script to build the required wheels for the Python SDK -output_version_file="version.py" - # The list of python verisons the SDKs release for python_versions=("$@") # Minimum glibc version we support -glibc_version=2-32 - -# These versions are being supported due to the SDKs supporting Python 3.9+ -macOS_version_x86_64=10.9 -macOS_version_arm64=11.0 +glibc_version=2-34 # Extracts the current verison number for cleanup function -current_version=$(awk -F "['\"]" '/SDK_VERSION =/{print $2}' "$output_version_file") +current_version=$(cat .VERSION) # Function to execute upon exit cleanup() { echo "Performing cleanup tasks..." # Remove dist and egg-info and the potential release candidate if created rm -r dist src/*.egg-info/ onepassword_sdk-"${current_version}" - exit 1 + exit 1 } # Set the trap to call the cleanup function on exit @@ -44,23 +38,9 @@ build_wheels() { export PYTHON_OS_PLATFORM=$os_platform export PYTHON_MACHINE_PLATFORM=$machine_platform - case "$os_platform" in + case "$os_platform" in Darwin) - macos_version= - # Min MacOS version for Python 3.13+ is 10.13 - python_version=$(pyenv exec python3 --version 2>&1) - - if [[ "$machine_platform" == "x86_64" ]]; then - if [[ "$python_version" == "Python 3.13"* ]]; then - macos_version="10.13" - else - macos_version=$macOS_version_x86_64 - fi - else - macos_version=$macOS_version_arm64 - fi - - export _PYTHON_HOST_PLATFORM="macosx-${macos_version}-${PYTHON_MACHINE_PLATFORM}" + export _PYTHON_HOST_PLATFORM="macosx-12.0-${PYTHON_MACHINE_PLATFORM}" ;; Linux) export _PYTHON_HOST_PLATFORM="manylinux-${glibc_version}-${PYTHON_MACHINE_PLATFORM}" diff --git a/src/release/scripts/prep-release.sh b/src/release/scripts/prep-release.sh index 88e709d2..f85f81f0 100755 --- a/src/release/scripts/prep-release.sh +++ b/src/release/scripts/prep-release.sh @@ -2,23 +2,22 @@ # Helper script to prepare a release for the Python SDK. -output_version_file="version.py" +output_version_file=".VERSION" output_build_file="src/onepassword/build_number.py" -version_template_file="src/release/templates/version.tpl.py" build_number_template_file="src/release/templates/build_number.tpl.py" -# Extracts the current build/version number for comparison and backup -current_version=$(awk -F "['\"]" '/SDK_VERSION =/{print $2}' "$output_version_file") -current_build=$(awk -F "['\"]" '/SDK_BUILD_NUMBER =/{print $2}' "$output_build_file") +# Extracts the current build/version number for comparison and backup +current_version=$(cat "$output_version_file" | tr -d '[:space:]') +current_build=$(awk -F "['\"]" '/SDK_BUILD_NUMBER =/{print $2}' "$output_build_file" | tr -d '[:space:]') # Function to execute upon exit cleanup() { echo "Performing cleanup tasks..." # Revert changes to file if any - sed -e "s/{{ version }}/$current_version/" "$version_template_file" > "$output_version_file" + echo -n "$current_version" > "$output_version_file" sed -e "s/{{ build }}/$current_build/" "$build_number_template_file" > "$output_build_file" - exit 1 + exit 1 } # Set the trap to call the cleanup function on exit @@ -39,23 +38,27 @@ update_and_validate_version() { read -p "Enter the version number (format: x.y.z(-beta.w)): " version # Validate the version number format - if [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-beta\.[0-9]+)?$ ]]; then + if [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-beta\.[0-9]+)?$ ]]; then if [[ "${current_version}" != "${version}" ]]; then # TODO: Check the less than case as well. echo "New version number is: ${version}" return 0 else echo "Version hasn't changed." - fi + fi else echo "Invalid version number format: ${version}" echo "Please enter a version number in the 'x.y.z(-beta.w)' format." fi + # If running in CI, exit immediately + if [[ -n "${SDK_CI}" ]]; then + exit 1 + fi done } # Function to validate the build number format. -# SEMVER Format: Mmmppbb - 7 Digits +# SEMVER Format: Mmmppbb - 7 Digits update_and_validate_build() { while true; do # Prompt the user to input the build number @@ -74,6 +77,10 @@ update_and_validate_build() { echo "Invalid build number format: ${build}" echo "Please enter a build number in the 'Mmmppbb' format." fi + # If running in CI, exit immediately + if [[ -n "${SDK_CI}" ]]; then + exit 1 + fi done } @@ -84,10 +91,10 @@ enforce_latest_code update_and_validate_version # Update and validate the build number -update_and_validate_build +update_and_validate_build -# Update version & build number in version.py and build_number.py respectively -sed -e "s/{{ version }}/$version/" "$version_template_file" > "$output_version_file" +# Update version & build number in .VERSION and build_number.py respectively +echo -n "$version" > "$output_version_file" sed -e "s/{{ build }}/$build/" "$build_number_template_file" > "$output_build_file" diff --git a/src/release/scripts/release.sh b/src/release/scripts/release.sh index 050c1489..e0952a7a 100755 --- a/src/release/scripts/release.sh +++ b/src/release/scripts/release.sh @@ -5,7 +5,7 @@ set -e # Read the contents of the files into variables -version=$(awk -F "['\"]" '/SDK_VERSION =/{print $2}' "version.py") +version=$(cat ".VERSION") build=$(awk -F "['\"]" '/SDK_BUILD_NUMBER =/{print $2}' "src/onepassword/build_number.py") release_notes=$(< src/release/RELEASE-NOTES) @@ -27,10 +27,3 @@ git tag -a -s "v${version}" -m "${version}" git push origin tag "v${version}" gh release create "v${version}" --title "Release ${version}" --notes "${release_notes}" --repo github.com/1Password/onepassword-sdk-python - -# Release on PyPi -python3 -m twine upload dist/* - -# Delete the dist folder after published -rm -r dist src/*.egg-info - diff --git a/src/release/templates/version.tpl.py b/src/release/templates/version.tpl.py deleted file mode 100644 index 9c787be2..00000000 --- a/src/release/templates/version.tpl.py +++ /dev/null @@ -1 +0,0 @@ -SDK_VERSION = "{{ version }}" diff --git a/version.py b/version.py deleted file mode 100644 index b2637133..00000000 --- a/version.py +++ /dev/null @@ -1 +0,0 @@ -SDK_VERSION = "0.3.0"