From aae5ce89866398038ac9045db18fe6e57e1d653d Mon Sep 17 00:00:00 2001 From: Eugene Yurtsev Date: Mon, 17 Mar 2025 23:34:39 -0400 Subject: [PATCH] x --- .github/actions/uv_setup/action.yml | 21 ++++ .github/workflows/_lint.yml | 44 ++++++++ .github/workflows/_test.yml | 42 ++++++++ .github/workflows/ci.yml | 58 +++++++++++ .github/workflows/release.yml | 152 ++++++++++++++++++++++++++++ 5 files changed, 317 insertions(+) create mode 100644 .github/actions/uv_setup/action.yml create mode 100644 .github/workflows/_lint.yml create mode 100644 .github/workflows/_test.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml diff --git a/.github/actions/uv_setup/action.yml b/.github/actions/uv_setup/action.yml new file mode 100644 index 0000000..3b9db69 --- /dev/null +++ b/.github/actions/uv_setup/action.yml @@ -0,0 +1,21 @@ +# TODO: https://docs.astral.sh/uv/guides/integration/github/#caching + +name: uv-install +description: Set up Python and uv + +inputs: + python-version: + description: Python version, supporting MAJOR.MINOR only + required: true + +env: + UV_VERSION: "0.5.25" + +runs: + using: composite + steps: + - name: Install uv and set the python version + uses: astral-sh/setup-uv@v5 + with: + version: ${{ env.UV_VERSION }} + python-version: ${{ inputs.python-version }} diff --git a/.github/workflows/_lint.yml b/.github/workflows/_lint.yml new file mode 100644 index 0000000..3648eb6 --- /dev/null +++ b/.github/workflows/_lint.yml @@ -0,0 +1,44 @@ +name: lint + +on: + workflow_call: + inputs: + working-directory: + required: true + type: string + description: "From which folder this pipeline executes" + python-version: + required: true + type: string + description: "Python version to use" + +env: + WORKDIR: ${{ inputs.working-directory == '' && '.' || inputs.working-directory }} + + # This env var allows us to get inline annotations when ruff has complaints. + RUFF_OUTPUT_FORMAT: github + + UV_FROZEN: "true" + +jobs: + build: + name: "make lint #${{ inputs.python-version }}" + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ inputs.python-version }} + uv + uses: "./.github/actions/uv_setup" + with: + python-version: ${{ inputs.python-version }} + + - name: Install dependencies + working-directory: ${{ inputs.working-directory }} + run: | + uv sync --group test + + - name: Analysing the code with our lint + working-directory: ${{ inputs.working-directory }} + run: | + make lint diff --git a/.github/workflows/_test.yml b/.github/workflows/_test.yml new file mode 100644 index 0000000..12e4289 --- /dev/null +++ b/.github/workflows/_test.yml @@ -0,0 +1,42 @@ +name: test + +on: + workflow_call: + inputs: + working-directory: + required: true + type: string + description: "From which folder this pipeline executes" + python-version: + required: true + type: string + description: "Python version to use" + +env: + UV_FROZEN: "true" + UV_NO_SYNC: "true" + +jobs: + build: + defaults: + run: + working-directory: ${{ inputs.working-directory }} + runs-on: ubuntu-latest + timeout-minutes: 20 + name: "make test #${{ inputs.python-version }}" + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ inputs.python-version }} + uv + uses: "./.github/actions/uv_setup" + id: setup-python + with: + python-version: ${{ inputs.python-version }} + - name: Install dependencies + shell: bash + run: uv sync --group test + + - name: Run core tests + shell: bash + run: | + make test diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e100a95 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,58 @@ +--- +name: Run CI Tests + +on: + push: + branches: [ main ] + pull_request: + workflow_dispatch: # Allows to trigger the workflow manually in GitHub UI + +# If another push to the same PR or branch happens while this workflow is still running, +# cancel the earlier run in favor of the next run. +# +# There's no point in testing an outdated version of the code. GitHub only allows +# a limited number of job runners to be active at the same time, so it's better to cancel +# pointless jobs early so that more useful jobs can run sooner. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint: + strategy: + matrix: + # Only lint on the min and max supported Python versions. + # It's extremely unlikely that there's a lint issue on any version in between + # that doesn't show up on the min or max versions. + # + # GitHub rate-limits how many jobs can be running at any one time. + # Starting new jobs is also relatively slow, + # so linting on fewer versions makes CI faster. + python-version: + - "3.12" + uses: + ./.github/workflows/_lint.yml + with: + working-directory: . + python-version: ${{ matrix.python-version }} + secrets: inherit + test: + strategy: + matrix: + # Only lint on the min and max supported Python versions. + # It's extremely unlikely that there's a lint issue on any version in between + # that doesn't show up on the min or max versions. + # + # GitHub rate-limits how many jobs can be running at any one time. + # Starting new jobs is also relatively slow, + # so linting on fewer versions makes CI faster. + python-version: + - "3.9" + - "3.12" + uses: + ./.github/workflows/_test.yml + with: + working-directory: . + python-version: ${{ matrix.python-version }} + secrets: inherit + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..dc5a167 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,152 @@ +name: release +run-name: Release ${{ inputs.working-directory }} by @${{ github.actor }} +on: + workflow_call: + inputs: + working-directory: + required: true + type: string + description: "From which folder this pipeline executes" + workflow_dispatch: + inputs: + working-directory: + description: "From which folder this pipeline executes" + default: "." + dangerous-nonmain-release: + required: false + type: boolean + default: false + description: "Release from a non-main branch (danger!)" + +env: + PYTHON_VERSION: "3.11" + UV_FROZEN: "true" + UV_NO_SYNC: "true" + +jobs: + build: + if: github.ref == 'refs/heads/main' || inputs.dangerous-nonmain-release + environment: Scheduled testing + runs-on: ubuntu-latest + + outputs: + pkg-name: ${{ steps.check-version.outputs.pkg-name }} + version: ${{ steps.check-version.outputs.version }} + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uv + uses: "./.github/actions/uv_setup" + with: + python-version: ${{ env.PYTHON_VERSION }} + + # We want to keep this build stage *separate* from the release stage, + # so that there's no sharing of permissions between them. + # The release stage has trusted publishing and GitHub repo contents write access, + # and we want to keep the scope of that access limited just to the release job. + # Otherwise, a malicious `build` step (e.g. via a compromised dependency) + # could get access to our GitHub or PyPI credentials. + # + # Per the trusted publishing GitHub Action: + # > It is strongly advised to separate jobs for building [...] + # > from the publish job. + # https://github.com/pypa/gh-action-pypi-publish#non-goals + - name: Build project for distribution + run: uv build + - name: Upload build + uses: actions/upload-artifact@v4 + with: + name: dist + path: ${{ inputs.working-directory }}/dist/ + + - name: Check Version + id: check-version + shell: python + working-directory: ${{ inputs.working-directory }} + run: | + import os + import tomllib + with open("pyproject.toml", "rb") as f: + data = tomllib.load(f) + pkg_name = data["project"]["name"] + version = data["project"]["version"] + with open(os.environ["GITHUB_OUTPUT"], "a") as f: + f.write(f"pkg-name={pkg_name}\n") + f.write(f"version={version}\n") + + publish: + needs: + - build + runs-on: ubuntu-latest + permissions: + # This permission is used for trusted publishing: + # https://blog.pypi.org/posts/2023-04-20-introducing-trusted-publishers/ + # + # Trusted publishing has to also be configured on PyPI for each package: + # https://docs.pypi.org/trusted-publishers/adding-a-publisher/ + id-token: write + + defaults: + run: + working-directory: ${{ inputs.working-directory }} + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uv + uses: "./.github/actions/uv_setup" + with: + python-version: ${{ env.PYTHON_VERSION }} + + - uses: actions/download-artifact@v4 + with: + name: dist + path: ${{ inputs.working-directory }}/dist/ + + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: ${{ inputs.working-directory }}/dist/ + verbose: true + print-hash: true + # Temp workaround since attestations are on by default as of gh-action-pypi-publish v1.11.0 + attestations: false + + mark-release: + needs: + - build + - publish + runs-on: ubuntu-latest + permissions: + # This permission is needed by `ncipollo/release-action` to + # create the GitHub release. + contents: write + + defaults: + run: + working-directory: ${{ inputs.working-directory }} + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uv + uses: "./.github/actions/uv_setup" + with: + python-version: ${{ env.PYTHON_VERSION }} + + - uses: actions/download-artifact@v4 + with: + name: dist + path: ${{ inputs.working-directory }}/dist/ + + - name: Create Tag + uses: ncipollo/release-action@v1 + with: + artifacts: "dist/*" + token: ${{ secrets.GITHUB_TOKEN }} + generateReleaseNotes: true + tag: ${{needs.build.outputs.pkg-name}}==${{ needs.build.outputs.version }} + body: ${{ needs.release-notes.outputs.release-body }} + commit: main + makeLatest: true \ No newline at end of file