diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4a59df7..8db61f7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,8 +69,10 @@ jobs: with: persist-credentials: false - - name: Install uv - uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + # The tests below need uv, which the python/setup action installs. Using it + # here also exercises the action itself on every run. + - name: Install Python tooling + uses: $/python/setup - name: Run uv lock update tests run: | diff --git a/README.md b/README.md index 5c7dbb3..d51913f 100644 --- a/README.md +++ b/README.md @@ -422,218 +422,11 @@ jobs: token: ${{ github.token }} ``` -## Python Helper Scripts +## Python Actions -These scripts are opinionated helper scripts for Python releases. +Python helper actions have their own READMEs: -### Pre-Publish - -Bump the version and create a new tag. Verify the tag. -Push the commit and tag to the source branch unless `dry_run` is set. - -```yaml -- name: Setup - uses: mongodb-labs/drivers-github-tools/setup@v3 - with: - ... - -- uses: mongodb-labs/drivers-github-tools/python/pre-publishv2 - with: - version: ${{ inputs.version }} - version_bump_script: ./.github/scripts/bump-version.sh - dry_run: ${{ inputs.dry_run }} -``` - -### Post-publish - -To be run after separately publishing the [Python package](https://github.com/pypa/gh-action-pypi-publish#trusted-publishing). -Handles follow-up tasks related to publishing Python packages, including -signing `dist` files and uploading report assets to S3. -It will also push the following (dev) version to the source branch. -It will create a draft GitHub release and attach the signature files. -If `dry_run` is set, nothing will be pushed. - -The jobs should look something like: - -```yaml -publish: - name: Upload release to PyPI - runs-on: ubuntu-latest - environment: release - permissions: - id-token: write - steps: - - name: Download all the dists - uses: actions/download-artifact@v4 - with: - name: all-dist-${{ github.run_id }} - path: dist/ - - name: Publish package distributions to PyPI - if: inputs.dry_run == 'false' - uses: pypa/gh-action-pypi-publish@release/v1 - -post-publish: - needs: [publish] - name: Handle post-publish actions - runs-on: ubuntu-latest - environment: release - permissions: - id-token: write - contents: write - attestations: write - security-events: write - steps: - - name: Setup - uses: mongodb-labs/drivers-github-tools/setup@v3 - with: - ... - - - uses: mongodb-labs/drivers-github-tools/python/post-publish@v3 - with: - version: ${{ inputs.version }} - following_version: ${{ inputs.following_version }} - version_bump_script: ./.github/scripts/bump-version.sh - product_name: winkerberos - token: ${{ github.token }} - dry_run: ${{ inputs.dry_run }} -``` - -### uv Lock Update - -This action runs `uv lock --upgrade` and opens a pull request with the resulting -lock file changes. It maintains a single open pull request: a subsequent run -updates the existing one rather than opening a second. - -The caller checks out the repository and puts `uv` on `PATH`. - -```yaml -name: Update uv.lock - -on: - schedule: - - cron: "0 7 * * 1" - workflow_dispatch: - -# Runs must serialize: two at once would force push the same branch and race on -# the pull request. Keep the group static rather than keying it on the ref. -concurrency: - group: uv-lock-update - cancel-in-progress: false - -jobs: - update-lock: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - with: - persist-credentials: false - - uses: astral-sh/setup-uv@v8 - - uses: mongodb-labs/drivers-github-tools/python/uv-lock-update@v3 - with: - app_id: ${{ vars.APP_ID }} - private_key: ${{ secrets.APP_PRIVATE_KEY }} -``` - -`app_id` and `private_key` are required unless `dry_run` is true. - -`base` defaults to the ref the workflow ran on, which is what a checkout with no -`ref` takes. If you check out a different ref, set `base` to match it, or the -pull request will contain every unrelated commit between the two branches. - -Every label named in `labels` must already exist in the repository, because -GitHub rejects a pull request that asks for an unknown one. - -Set `dry_run: true` to log the branch and pull request the action would have -created, without pushing or opening anything. - -The upgrade skips releases published within the last 7 days, so a broken or -compromised release has time to be yanked before it can land in the lock file. -Change the cutoff with `exclude_newer`, which takes anything uv's -`--exclude-newer` accepts: a date, an RFC 3339 timestamp, a duration such as -`30 days`, or `false` to upgrade to the newest releases with no cutoff at all. -It reaches uv as `UV_EXCLUDE_NEWER`, so it overrides an `exclude-newer` the -repository sets in `pyproject.toml` or `uv.toml`. Pass `exclude_newer: ""` to -leave that setting in charge instead. - -```yaml - - uses: mongodb-labs/drivers-github-tools/python/uv-lock-update@v3 - with: - app_id: ${{ vars.APP_ID }} - private_key: ${{ secrets.APP_PRIVATE_KEY }} - exclude_newer: 14 days -``` - -## Python Labs Helper Scripts - -These scripts are opinionated helper scripts for Python releases in MongoDB Labs. -In contrast to the regulare Python scripts, it does not generate the -SSDLC compliance assets or upload anything to S3. - -### Pre-Publish - -Create a new tag. Verify the tag. -Push the commit and tag to the source branch unless `dry_run` is set. - -```yaml -- name: Setup - uses: mongodb-labs/drivers-github-tools/setup@v3 - with: - ... - -- uses: mongodb-labs/drivers-github-tools/python-labs/pre-publishv2 - with: - version_bump_script: ./.github/scripts/bump-version.sh - dry_run: ${{ inputs.dry_run }} -``` - -### Post-publish - -To be run after separately publishing the [Python package](https://github.com/pypa/gh-action-pypi-publish#trusted-publishing). -Handles follow-up tasks related to publishing Python packages. -It will push the following (dev) version to the source branch. -It will create a draft GitHub release with generated release notes. -If `dry_run` is set, nothing will be pushed. - -The jobs should look something like: - -```yaml -publish: - name: Upload release to PyPI - runs-on: ubuntu-latest - environment: release - permissions: - id-token: write - steps: - - name: Download all the dists - uses: actions/download-artifact@v4 - with: - name: all-dist-${{ github.run_id }} - path: dist/ - - name: Publish package distributions to PyPI - if: inputs.dry_run == 'false' - uses: pypa/gh-action-pypi-publish@release/v1 - -post-publish: - needs: [publish] - name: Handle post-publish actions - runs-on: ubuntu-latest - environment: release - permissions: - id-token: write - contents: write - attestations: write - security-events: write - steps: - - name: Setup - uses: mongodb-labs/drivers-github-tools/setup@v3 - with: - ... - - - uses: mongodb-labs/drivers-github-tools/python-labs/post-publish@v3 - with: - following_version: ${{ inputs.following_version }} - version_bump_script: ./.github/scripts/bump-version.sh - product_name: python-bsonjs - token: ${{ github.token }} - dry_run: ${{ inputs.dry_run }} -``` \ No newline at end of file +- [`python/`](python/README.md) covers setup, pre-publish, post-publish, and uv + lock updates for the Python drivers. +- [`python-labs/`](python-labs/README.md) covers the same ground for MongoDB Labs + projects, without the SSDLC assets and S3 upload. diff --git a/python-labs/README.md b/python-labs/README.md new file mode 100644 index 0000000..299e079 --- /dev/null +++ b/python-labs/README.md @@ -0,0 +1,74 @@ +# Python Labs Actions + +Opinionated helper actions for Python releases in MongoDB Labs. Unlike the +[regular Python actions](../python/README.md), these do not generate the SSDLC +compliance assets or upload anything to S3. + +## Pre-Publish + +Create a new tag. Verify the tag. +Push the commit and tag to the source branch unless `dry_run` is set. + +```yaml +- name: Setup + uses: mongodb-labs/drivers-github-tools/setup@v3 + with: + ... + +- uses: mongodb-labs/drivers-github-tools/python-labs/pre-publishv2 + with: + version_bump_script: ./.github/scripts/bump-version.sh + dry_run: ${{ inputs.dry_run }} +``` + +## Post-publish + +To be run after separately publishing the [Python package](https://github.com/pypa/gh-action-pypi-publish#trusted-publishing). +Handles follow-up tasks related to publishing Python packages. +It will push the following (dev) version to the source branch. +It will create a draft GitHub release with generated release notes. +If `dry_run` is set, nothing will be pushed. + +The jobs should look something like: + +```yaml +publish: + name: Upload release to PyPI + runs-on: ubuntu-latest + environment: release + permissions: + id-token: write + steps: + - name: Download all the dists + uses: actions/download-artifact@v4 + with: + name: all-dist-${{ github.run_id }} + path: dist/ + - name: Publish package distributions to PyPI + if: inputs.dry_run == 'false' + uses: pypa/gh-action-pypi-publish@release/v1 + +post-publish: + needs: [publish] + name: Handle post-publish actions + runs-on: ubuntu-latest + environment: release + permissions: + id-token: write + contents: write + attestations: write + security-events: write + steps: + - name: Setup + uses: mongodb-labs/drivers-github-tools/setup@v3 + with: + ... + + - uses: mongodb-labs/drivers-github-tools/python-labs/post-publish@v3 + with: + following_version: ${{ inputs.following_version }} + version_bump_script: ./.github/scripts/bump-version.sh + product_name: python-bsonjs + token: ${{ github.token }} + dry_run: ${{ inputs.dry_run }} +``` diff --git a/python/README.md b/python/README.md new file mode 100644 index 0000000..043d6ae --- /dev/null +++ b/python/README.md @@ -0,0 +1,183 @@ +# Python Actions + +Opinionated helper actions for Python CI and releases. See the +[top-level README](../README.md) for actions shared across all drivers. + +## Setup + +This action puts Python, uv, and just on `PATH`. It replaces the block of setup +steps that Python driver repos otherwise repeat in every CI job. + +```yaml +- uses: mongodb-labs/drivers-github-tools/python/setup@v3 + with: + python-version: "3.10" +``` + +Python comes from `actions/setup-python`, because the runner images already ship +several versions and installing from them is faster than having uv download a +managed interpreter. uv is then pointed at that exact interpreter through +`UV_PYTHON`, so it does not pick a different one based on the project's +`pyproject.toml` or `.python-version`. + +Add `run-install: "true"` for a job that also needs the project's dependencies: + +```yaml +- uses: mongodb-labs/drivers-github-tools/python/setup@v3 + with: + python-version: "3.10" + run-install: "true" +``` + +That runs the project's `just install` recipe, and requires one to exist. It is +off by default because installing dependencies changes the job's environment, and +plenty of jobs only need the tools. + +The action sets no resolution policy of its own. A repo that wants to hold back +newly published packages configures `exclude-newer` in its own `pyproject.toml` or +`uv.toml`, or commits a `uv.lock`. + +`enable-cache` defaults to `true` and is passed through to `astral-sh/setup-uv`. + +`allow-prereleases` defaults to `true`, so asking for a Python version with no +stable release yet gets the prerelease instead of failing. uv resolves such a +version on its own, so leaving this on keeps that behaviour for a repo moving to +this action. Set it to `"false"` for a job that should fail rather than quietly +test against a beta. + +## Pre-Publish + +Bump the version and create a new tag. Verify the tag. +Push the commit and tag to the source branch unless `dry_run` is set. + +```yaml +- name: Setup + uses: mongodb-labs/drivers-github-tools/setup@v3 + with: + ... + +- uses: mongodb-labs/drivers-github-tools/python/pre-publishv2 + with: + version: ${{ inputs.version }} + version_bump_script: ./.github/scripts/bump-version.sh + dry_run: ${{ inputs.dry_run }} +``` + +## Post-publish + +To be run after separately publishing the [Python package](https://github.com/pypa/gh-action-pypi-publish#trusted-publishing). +Handles follow-up tasks related to publishing Python packages, including +signing `dist` files and uploading report assets to S3. +It will also push the following (dev) version to the source branch. +It will create a draft GitHub release and attach the signature files. +If `dry_run` is set, nothing will be pushed. + +The jobs should look something like: + +```yaml +publish: + name: Upload release to PyPI + runs-on: ubuntu-latest + environment: release + permissions: + id-token: write + steps: + - name: Download all the dists + uses: actions/download-artifact@v4 + with: + name: all-dist-${{ github.run_id }} + path: dist/ + - name: Publish package distributions to PyPI + if: inputs.dry_run == 'false' + uses: pypa/gh-action-pypi-publish@release/v1 + +post-publish: + needs: [publish] + name: Handle post-publish actions + runs-on: ubuntu-latest + environment: release + permissions: + id-token: write + contents: write + attestations: write + security-events: write + steps: + - name: Setup + uses: mongodb-labs/drivers-github-tools/setup@v3 + with: + ... + + - uses: mongodb-labs/drivers-github-tools/python/post-publish@v3 + with: + version: ${{ inputs.version }} + following_version: ${{ inputs.following_version }} + version_bump_script: ./.github/scripts/bump-version.sh + product_name: winkerberos + token: ${{ github.token }} + dry_run: ${{ inputs.dry_run }} +``` + +## uv Lock Update + +This action runs `uv lock --upgrade` and opens a pull request with the resulting +lock file changes. It maintains a single open pull request: a subsequent run +updates the existing one rather than opening a second. + +The caller checks out the repository and puts `uv` on `PATH`. + +```yaml +name: Update uv.lock + +on: + schedule: + - cron: "0 7 * * 1" + workflow_dispatch: + +# Runs must serialize: two at once would force push the same branch and race on +# the pull request. Keep the group static rather than keying it on the ref. +concurrency: + group: uv-lock-update + cancel-in-progress: false + +jobs: + update-lock: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - uses: astral-sh/setup-uv@v8 + - uses: mongodb-labs/drivers-github-tools/python/uv-lock-update@v3 + with: + app_id: ${{ vars.APP_ID }} + private_key: ${{ secrets.APP_PRIVATE_KEY }} +``` + +`app_id` and `private_key` are required unless `dry_run` is true. + +`base` defaults to the ref the workflow ran on, which is what a checkout with no +`ref` takes. If you check out a different ref, set `base` to match it, or the +pull request will contain every unrelated commit between the two branches. + +Every label named in `labels` must already exist in the repository, because +GitHub rejects a pull request that asks for an unknown one. + +Set `dry_run: true` to log the branch and pull request the action would have +created, without pushing or opening anything. + +The upgrade skips releases published within the last 7 days, so a broken or +compromised release has time to be yanked before it can land in the lock file. +Change the cutoff with `exclude_newer`, which takes anything uv's +`--exclude-newer` accepts: a date, an RFC 3339 timestamp, a duration such as +`30 days`, or `false` to upgrade to the newest releases with no cutoff at all. +It reaches uv as `UV_EXCLUDE_NEWER`, so it overrides an `exclude-newer` the +repository sets in `pyproject.toml` or `uv.toml`. Pass `exclude_newer: ""` to +leave that setting in charge instead. + +```yaml + - uses: mongodb-labs/drivers-github-tools/python/uv-lock-update@v3 + with: + app_id: ${{ vars.APP_ID }} + private_key: ${{ secrets.APP_PRIVATE_KEY }} + exclude_newer: 14 days +``` diff --git a/python/setup/action.yml b/python/setup/action.yml new file mode 100644 index 0000000..a7b724b --- /dev/null +++ b/python/setup/action.yml @@ -0,0 +1,66 @@ +name: Python Setup +description: Installs Python, uv, and just, and optionally runs the project's `just install` recipe +inputs: + python-version: + description: Python version for setup-python to install, and the interpreter uv will use + default: "3.10" + allow-prereleases: + description: >- + Let setup-python fall back to a prerelease when python-version has no + stable release yet, so asking for a Python still in beta gets that beta + instead of failing. On by default, because uv resolves such a version on + its own and a consumer moving to this action should not lose that. + default: "true" + enable-cache: + description: Cache the uv download cache between runs + default: "true" + run-install: + description: >- + Run the project's `just install` recipe. Off by default, because putting + the tools on PATH is what this action is for and installing dependencies + changes the job's environment. A job that needs them asks for them. The + project must define an `install` recipe, or the step fails. + default: "false" + +runs: + using: composite + steps: + # The runner images already ship several Python versions, so installing from + # them is faster than having uv download a managed interpreter. uv is pointed + # at this one below rather than choosing its own. + - name: Install Python + id: setup-python + uses: actions/setup-python@v7 + with: + python-version: ${{ inputs.python-version }} + allow-prereleases: ${{ inputs.allow-prereleases }} + + - name: Point uv at that interpreter + shell: bash + env: + # The path goes through env rather than into the script body, so a value + # containing shell metacharacters cannot alter the command below. + PYTHON_PATH: ${{ steps.setup-python.outputs.python-path }} + # Exporting to the caller's later steps is the point of this step, and + # GITHUB_ENV is the only way a composite action can do it, so the + # github-env audit has nothing actionable to say here. The value comes from + # setup-python, not from a caller's untrusted content. + run: | # zizmor: ignore[github-env] + # An absolute interpreter path makes the choice exact: uv uses this + # interpreter and never falls back to downloading one, whatever the + # consuming project's pyproject.toml or .python-version asks for. + echo "UV_PYTHON=$PYTHON_PATH" >> "$GITHUB_ENV" + + - name: Install uv + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + with: + enable-cache: ${{ inputs.enable-cache }} + + - name: Install just + shell: bash + run: uv tool install rust-just + + - name: Run just install + if: inputs.run-install == 'true' + shell: bash + run: just install