-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Automate pypi deployment #919
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| #!/bin/bash | ||
|
|
||
| # The $1 argument is the version number passed from the workflow | ||
| VERSION=$1 | ||
|
|
||
| echo "version: $VERSION" | ||
|
|
||
| for i in {1..5}; do | ||
| if python3 -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple dspy-ai-test=="$VERSION"; then | ||
| break | ||
| else | ||
| echo "Attempt $i failed. Waiting before retrying..." | ||
| sleep 10 | ||
| fi | ||
| done |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| --- | ||
| name: Publish Python 🐍 distributions 📦 to PyPI | ||
| on: | ||
| push: | ||
| tags: | ||
| - "*" | ||
| jobs: | ||
|
|
||
| extract-tag: | ||
| runs-on: ubuntu-latest | ||
| outputs: | ||
| version: ${{ steps.extract_tag.outputs.tag }} | ||
| steps: | ||
| - uses: actions/checkout@v2 | ||
| - id: extract_tag | ||
| name: Extract tag name | ||
| run: echo "::set-output name=tag::$(echo $GITHUB_REF | cut -d / -f 3)" | ||
|
|
||
| build-and-publish-test-pypi: | ||
| needs: extract-tag | ||
| runs-on: ubuntu-latest | ||
| environment: | ||
| name: pypi | ||
| permissions: | ||
| id-token: write # IMPORTANT: mandatory for trusted publishing | ||
| steps: | ||
| - uses: actions/checkout@master | ||
| - name: Set up Python 3.9 | ||
| uses: actions/setup-python@v3 | ||
| with: | ||
| python-version: "3.9" | ||
| - name: Install dependencies | ||
| run: python3 -m pip install setuptools wheel twine semver packaging | ||
| - name: Get correct version for TestPyPI release | ||
| id: check_version | ||
| run: | | ||
| VERSION=${{ needs.extract-tag.outputs.version }} | ||
| PACKAGE_NAME="dspy-ai-test" | ||
| echo "Checking if $VERSION for $PACKAGE_NAME exists on TestPyPI" | ||
| NEW_VERSION=$(python3 build_utils/test_version.py $PACKAGE_NAME $VERSION) | ||
| echo "Version to be used for TestPyPI release: $NEW_VERSION" | ||
| echo "::set-output name=version::$NEW_VERSION" | ||
| - name: Update version in setup.py | ||
| run: sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.check_version.outputs.version }}/g" setup.py | ||
| - name: Update version in pyproject.toml | ||
| run: sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.check_version.outputs.version }}/g" pyproject.toml | ||
| - name: Update package name in setup.py | ||
| run: sed -i "s/{{PACKAGE_NAME_PLACEHOLDER}}/dspy-ai-test/g" setup.py | ||
| - name: Update package name in pyproject.toml | ||
| run: sed -i "s/{{PACKAGE_NAME_PLACEHOLDER}}/dspy-ai-test/g" pyproject.toml | ||
| - name: Build a binary wheel | ||
| run: python3 setup.py sdist bdist_wheel | ||
| - name: Publish distribution 📦 to test-PyPI | ||
| uses: pypa/gh-action-pypi-publish@release/v1 # This requires a trusted publisher to be setup in pypi/testpypi | ||
| with: | ||
| repository-url: https://test.pypi.org/legacy/ | ||
|
|
||
| test-intro-script: | ||
| needs: [extract-tag, build-and-publish-test-pypi] | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - name: Set up Python 3.9 | ||
| uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: "3.9" | ||
| cache: "pip" | ||
| - name: Install package from TestPyPI | ||
| run: | | ||
| .github/workflow_scripts/install_testpypi_pkg.sh ${{ needs.extract-tag.outputs.version }} | ||
| - name: Install other dependencies | ||
| run: | | ||
| python3 -m pip install Jinja2 | ||
| python3 -m pip install -r requirements.txt | ||
| python3 -m pip install -r requirements-dev.txt | ||
| python3 -m pip install openai==0.28.1 | ||
| - name: Set up cache directory | ||
| run: | | ||
| mkdir -p cache | ||
| echo "DSP_NOTEBOOK_CACHEDIR=$(pwd)/cache" >> $GITHUB_ENV | ||
| - name: Run Python script | ||
| run: | | ||
| pytest -c tests_integration/pytest.ini tests_integration/ | ||
|
|
||
| build-and-publish-pypi: | ||
| needs: [extract-tag, build-and-publish-test-pypi, test-intro-script] | ||
| runs-on: ubuntu-latest | ||
| environment: | ||
| name: pypi | ||
| permissions: | ||
| id-token: write # IMPORTANT: mandatory for trusted publishing | ||
| steps: | ||
| - uses: actions/checkout@master | ||
| - name: Set up Python 3.9 | ||
| uses: actions/setup-python@v3 | ||
| with: | ||
| python-version: "3.9" | ||
| - name: Install dependencies | ||
| run: python3 -m pip install setuptools wheel twine | ||
| - name: Update version in setup.py | ||
| run: sed -i "s/{{VERSION_PLACEHOLDER}}/${{ needs.extract-tag.outputs.version }}/g" setup.py | ||
| - name: Update version in pyproject.toml | ||
| run: sed -i "s/{{VERSION_PLACEHOLDER}}/${{ needs.extract-tag.outputs.version }}/g" pyproject.toml | ||
| - name: Update package name in setup.py | ||
| run: sed -i "s/{{PACKAGE_NAME_PLACEHOLDER}}/dspy-ai/g" setup.py | ||
| - name: Update package name in pyproject.toml | ||
| run: sed -i "s/{{PACKAGE_NAME_PLACEHOLDER}}/dspy-ai/g" pyproject.toml | ||
| - name: Build a binary wheel | ||
| run: python3 setup.py sdist bdist_wheel | ||
| - name: Publish distribution 📦 to PyPI | ||
| uses: pypa/gh-action-pypi-publish@release/v1 # This requires a trusted publisher to be setup in pypi/testpypi |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| import sys | ||
| from datetime import datetime | ||
|
|
||
| import requests | ||
| import semver | ||
| from packaging.version import Version as PyPIVersion | ||
|
|
||
|
|
||
| def get_latest_version(package_name, tag_version): | ||
| # Returns latest version, and T/F as to whether it needs to be incremented | ||
| response = requests.get(f"https://test.pypi.org/pypi/{package_name}/json") | ||
| if response.status_code == 200: | ||
| data = response.json() | ||
| # Flatten the list of files for all releases and get the latest upload | ||
| all_uploads = [ | ||
| (release['upload_time'], release['filename'], version) | ||
| for version, releases in data['releases'].items() | ||
| for release in releases | ||
| ] | ||
| # If a release with tag_version does not exist, that is the latest version | ||
| # Then increment is False, as no need to increment the version | ||
| tag_release_exists = any(upload for upload in all_uploads if upload[2] == tag_version) | ||
| if not(tag_release_exists): | ||
| return tag_version, False | ||
| # Else, get the latest release version, and set increment to True | ||
| else: | ||
| # Sort all uploads by upload time in descending order | ||
| latest_upload = max(all_uploads, key=lambda x: datetime.fromisoformat(x[0].rstrip('Z'))) | ||
| return latest_upload[2], True | ||
|
|
||
| elif response.status_code == 404: | ||
| # If no existing releases can get a 404 | ||
| return tag_version, False | ||
| return None, None | ||
|
|
||
| def increment_version(curr_version): | ||
| pypi_v = PyPIVersion(curr_version) | ||
| if pypi_v.pre: | ||
| pre = "".join([str(i) for i in pypi_v.pre]) | ||
| parsed_v = semver.Version(*pypi_v.release, pre) | ||
| else: | ||
| parsed_v = semver.Version(*pypi_v.release) | ||
| new_v = str(parsed_v.bump_prerelease()) | ||
| return new_v | ||
|
|
||
| if __name__ == "__main__": | ||
| if len(sys.argv) != 3: | ||
| raise ValueError("Usage: python get_latest_testpypi_version.py <package_name> <tag_version>") | ||
|
|
||
| package_name = sys.argv[1] | ||
| tag_v = sys.argv[2] | ||
|
|
||
| latest_version, increment = get_latest_version(package_name, tag_v) | ||
| if increment: | ||
| new_version = increment_version(latest_version) | ||
| else: | ||
| new_version = latest_version | ||
|
|
||
| # Output new version | ||
| print(new_version) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| # Build & Release Workflow Implementation | ||
|
|
||
| The [build_and_release](../../../.github/workflows/build_and_release.yml) workflow automates deployments of dspy-ai to pypi. For a guide to triggering a release using the workflow, refer to [release checklist](release-checklist.md). | ||
|
|
||
| ## Overview | ||
|
|
||
| At a high level, the workflow works as follows: | ||
|
|
||
| 1. Maintainer of the repo pushes a tag following [semver](https://semver.org/) versioning for the new release. | ||
| 2. This triggers the github action which extracts the tag (the version) | ||
| 3. Builds and publishes a release on [test-pypi](https://test.pypi.org/project/dspy-ai-test/) | ||
| 4. Uses the test-pypi release to run build_utils/tests/intro.py with the new release as an integration test. Note intro.py is a copy of the intro notebook. | ||
| 5. Assuming the test runs successfully, it pushes a release to [pypi](https://pypi.org/project/dspy-ai/). If not, the user can delete the tag, make the fixes and then push the tag again. Versioning for multiple releases to test-pypi with the same tag version is taken care of by the workflow by appending a pre-release identifier, so the user only needs to consider the version for pypi. | ||
| 6. (Currently manual) the user creates a release and includes release notes, as described in docs/docs/release-checklist.md | ||
|
|
||
| ## Implementation Details | ||
|
|
||
| The workflow executes a series of jobs in sequence: | ||
| - extract-tag | ||
| - build-and-publish-test-pypi | ||
| - test-intro-script | ||
| - build-and-publish-pypi | ||
|
|
||
| #### extract-tag | ||
| Extracts the tag pushed to the commit. This tag is expected to be the version of the new deployment. | ||
|
|
||
| #### build-and-publish-test-pypi | ||
| Builds and publishes the package to test-pypi. | ||
| 1. Determines the version that should be deployed to test-pypi. There may be an existing deployment with the version specified by the tag in the case that a deployment failed and the maintainer made some changes and pushed the same tag again (which is the intended usage). The following logic is implemented [test_version.py](../../../build_utils/test_version.py) | ||
| 1. Load the releases on test-pypi | ||
| 1. Check if there is a release matching our current tag | ||
| 1. If not, create a release with the current tag | ||
| 1. If it exists, oad the latest published version (this will either be the version with the tag itself, or the tag + a pre-release version). In either case, increment the pre-release version. | ||
| 1. Updates the version placeholder in [setup.py](../../../setup.py) to the version obtained in step 1. | ||
| 1. Updates the version placeholder in [pyproject.toml](../../../pyproject.toml) to the version obtained in step 1. | ||
| 1. Updates the package name placeholder in [setup.py](../../../setup.py) to `dspy-ai-test`* | ||
| 1. Updates the package name placeholder in [pyproject.toml](../../../pyproject.toml) to `dspy-ai-test`* | ||
| 1. Builds the binary wheel | ||
| 1. Publishes the package to test-pypi. | ||
|
|
||
|
|
||
| #### test-intro-script | ||
| Runs the pytest containing the intro script as an integration test using the package published to test-pypi. This is a validation step before publishing to pypi. | ||
| 1. Uses a loop to install the version just published to test-pypi as sometimes there is a race condition between the package becoming available for installation and this job executing. | ||
| 2. Runs the test to ensure the package is working as expected. | ||
| 3. If this fails, the workflow fails and the maintainer needs to make a fix and delete and then recreate the tag. | ||
|
|
||
| #### build-and-publish-pypi | ||
| Builds and publishes the package to pypi. | ||
|
|
||
| 1. Updates the version placeholder in [setup.py](../../../setup.py) to the version obtained in step 1. | ||
| 1. Updates the version placeholder in [pyproject.toml](../../../pyproject.toml) to the version obtained in step 1. | ||
| 1. Updates the package name placeholder in [setup.py](../../../setup.py) to `dspy-ai`* | ||
| 1. Updates the package name placeholder in [pyproject.toml](../../../pyproject.toml) to `dspy-ai`* | ||
| 1. Builds the binary wheel | ||
| 1. Publishes the package to pypi. | ||
|
|
||
|
|
||
| \* The package name is updated by the worfklow to allow the same files to be used to build both the pypi and test-pypi packages. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| # Release Checklist | ||
|
|
||
| * [ ] On `main` Create a git tag with pattern X.Y.Z where X, Y, and Z follow the [semver pattern](https://semver.org/). Then push the tag to the origin git repo (github). | ||
| * ```bash | ||
| git tag X.Y.Z | ||
| git push origin --tags | ||
| ``` | ||
| * This will trigger the github action to build and release the package. | ||
| * [ ] Confirm the tests pass and the package has been published to pypi. | ||
| * If the tests fail, you can remove the tag from your local and github repo using: | ||
| ```bash | ||
| git push origin --delete X.Y.Z # Delete on Github | ||
| git tag -d X.Y.Z # Delete locally | ||
| ``` | ||
| * Fix the errors and then repeat the steps above to recreate the tag locally and push to Github to restart the process. | ||
| * Note that the github action takes care of incrementing the release version on test-pypi automatically by adding a pre-release identifier in the scenario where the tests fail and you need to delete and push the same tag again. | ||
| * [ ] [Create a release](https://docs.github.com/en/repositories/releasing-projects-on-github/managing-releases-in-a-repository) | ||
| * [ ] Add release notes. You can make use of [automatically generated release notes](https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes) | ||
| * If creating a new release for major or minor version: | ||
| * [ ] Create a new release branch with the last commit and name it 'release/X.Y` | ||
| * [ ] [Update the default branch](https://docs.github.com/en/organizations/managing-organization-settings/managing-the-default-branch-name-for-repositories-in-your-organization) on the github rep to the new release branch. | ||
|
|
||
| ### Prerequisites | ||
|
|
||
| The automation requires a [trusted publisher](https://docs.pypi.org/trusted-publishers/) to be set up on both the pypi and test-pypi packages. If the package is migrated to a new project, please follow the [steps](https://docs.pypi.org/trusted-publishers/adding-a-publisher/) to create a trusted publisher. If you have no releases on the new project, you may have to create a [pending trusted publisher](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/) to allow the first automated deployment. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.