Skip to content

Commit 6d2de92

Browse files
committed
Improve deps and test multiple Python versions (#95)
1 parent 9289833 commit 6d2de92

File tree

19 files changed

+1062
-601
lines changed

19 files changed

+1062
-601
lines changed

.github/actions/prepare/action.yml

Lines changed: 70 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
name: "Prepare the Python environment"
44
description: "Prepares the Python environment leveraging cache where possible"
55

6+
inputs:
7+
skip_cache_restore:
8+
description: "Skip restoring the uv cache even if it exists"
9+
required: false
10+
default: "false"
11+
612
runs:
713
using: "composite"
814
steps:
@@ -19,7 +25,10 @@ runs:
1925
echo "PIPX_HOME=${HOME}/.local/share/pipx" >> $GITHUB_ENV
2026
echo "PIPX_BIN_DIR=${HOME}/.local/share/pipx/bin" >> $GITHUB_ENV
2127
22-
# Create report directores
28+
# Configure uv environment
29+
echo "UV_LINK_MODE=symlink" >> $GITHUB_ENV
30+
31+
# Create report directories
2332
set -x
2433
mkdir -p ./reports/tests
2534
mkdir -p ./reports/security
@@ -32,37 +41,90 @@ runs:
3241
UV_VERSION=$(python3 -c "import sys, json, packaging.specifiers, packaging.version; spec = packaging.specifiers.SpecifierSet('${SPEC}'); data = json.load(sys.stdin); print(max((v for v in data['releases'].keys() if v in spec), key=packaging.version.Version, default=''))" < <(curl -s "https://pypi.org/pypi/${PACKAGE}/json"))
3342
echo "uv_version=${UV_VERSION}" >> $GITHUB_OUTPUT
3443
35-
- name: Configure pipx cache
44+
# Cache Lookups
45+
- name: Lookup pipx cache
3646
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 #v5.0.3
37-
id: pipx
47+
id: pipx_lookup
3848
with:
49+
lookup-only: true
3950
path: |
4051
/home/runner/.local/share/pipx/
4152
key: pipx-uv-${{ steps.config.outputs.uv_version }}
4253

43-
- name: Configure uv cache
54+
- name: Lookup uv cache
4455
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 #v5.0.3
45-
id: uv
56+
id: uv_lookup
57+
with:
58+
lookup-only: true
59+
path: |
60+
/home/runner/.cache/uv/
61+
/home/runner/.local/share/uv/
62+
.venv
63+
key: uv-${{ hashFiles('uv.lock') }}
64+
65+
# Pipx Cache Control
66+
- name: Restore pipx cache
67+
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 #v5.0.3
68+
if: ${{ steps.pipx_lookup.outputs.cache-hit == 'true' && (inputs.skip_cache_restore != 'true' || steps.uv_lookup.outputs.cache-hit != 'true') }}
69+
with:
70+
path: |
71+
/home/runner/.local/share/pipx/
72+
key: pipx-uv-${{ steps.config.outputs.uv_version }}
73+
74+
- name: Report pipx cache restore skipped
75+
if: ${{ steps.pipx_lookup.outputs.cache-hit == 'true' && inputs.skip_cache_restore == 'true' && steps.uv_lookup.outputs.cache-hit == 'true' }}
76+
shell: bash
77+
run: |
78+
echo "::notice::Skipped pipx cache restoration due to skip_cache_restore==true"
79+
80+
# UV Cache Control
81+
- name: Restore uv cache
82+
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 #v5.0.3
83+
if: ${{ steps.uv_lookup.outputs.cache-hit == 'true' && inputs.skip_cache_restore != 'true' }}
4684
with:
4785
path: |
4886
/home/runner/.cache/uv/
87+
/home/runner/.local/share/uv/
88+
.venv
4989
key: uv-${{ hashFiles('uv.lock') }}
5090

91+
- name: Report uv cache restore skipped
92+
if: ${{ steps.uv_lookup.outputs.cache-hit == 'true' && inputs.skip_cache_restore == 'true' }}
93+
shell: bash
94+
run: |
95+
echo "::notice::Skipped uv cache restoration due to skip_cache_restore==true"
96+
97+
# Install tools and dependencies
5198
- name: Install pipx tools
52-
if: ${{ steps.pipx.outputs.cache-hit != 'true' }}
99+
if: ${{ steps.pipx_lookup.outputs.cache-hit != 'true' }}
53100
shell: bash
54101
run: |
55102
set -x
56103
pipx environment
57104
pipx install -f "uv==${{ steps.config.outputs.uv_version }}"
58105
106+
- name: Warm UV cache
107+
if: ${{ steps.uv_lookup.outputs.cache-hit != 'true' }}
108+
shell: bash
109+
run: |
110+
set -x
111+
uv run just warm_uv_cache
112+
59113
- name: Install package dependencies
114+
if: ${{ steps.uv_lookup.outputs.cache-hit != 'true' }}
115+
shell: bash
116+
run: |
117+
set -x
118+
uv sync --all-extras
119+
120+
- name: Report on environment
121+
if: ${{ steps.uv_lookup.outputs.cache-hit != 'true' || inputs.skip_cache_restore != 'true' }}
60122
shell: bash
61123
run: |
62124
set -x
63125
which uv
64126
readlink -f $(which uv)
65127
uv --version
66128
uv cache dir
67-
uv python ls
68-
uv sync --link-mode symlink --all-extras
129+
which python
130+
python --version

.github/actions/tests/action.yml

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# Copyright 2026 Latchfield Technologies http://latchfield.com
3+
name: "Run functional tests"
4+
description: "Prepares the environment and runs tests for a given Python version"
5+
6+
inputs:
7+
python_version:
8+
description: "Python version to test against (may end with '-lowest_deps' for lowest dependency resolution)"
9+
required: true
10+
full:
11+
description: "Run full test suite with coverage and integration tests"
12+
required: false
13+
default: "false"
14+
15+
runs:
16+
using: "composite"
17+
steps:
18+
- name: Prepare the Python environment
19+
uses: ./.github/actions/prepare
20+
21+
- name: Run tests
22+
shell: bash
23+
run: |
24+
py_ver="${{ inputs.python_version }}"
25+
resolution=()
26+
if [[ "${{ inputs.python_version }}" == *"-lowest_deps" ]]; then
27+
py_ver="${py_ver%-lowest_deps}"
28+
resolution=(--resolution lowest-direct)
29+
fi
30+
31+
pytest_args=(-q --no-cov -o addopts=)
32+
if [[ "${{ inputs.full }}" == "true" ]]; then
33+
mkdir -p ./reports/tests
34+
pytest_args=(--cov-report lcov:./reports/tests/coverage.lcov)
35+
36+
if [[ "${{ github.event_name }}" != "pull_request" ]]; then
37+
pytest_args+=("--plus_integration")
38+
fi
39+
fi
40+
41+
uv run --isolated --python "$py_ver" "${resolution[@]}" --all-extras pytest "${pytest_args[@]}"
42+
43+
- name: Record compatibility result
44+
shell: bash
45+
run: |
46+
mkdir -p ./reports/compatibility/${{ inputs.python_version }}
47+
echo "pass" > ./reports/compatibility/${{ inputs.python_version }}/result.txt

.github/dependabot.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ updates:
4646
applies-to: security-updates
4747

4848
# Python
49-
- package-ecosystem: pip
49+
- package-ecosystem: uv
5050
directory: "/"
5151
schedule:
5252
interval: weekly

.github/workflows/ci-build.yml

Lines changed: 67 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ jobs:
3535
security-cache-hit: ${{ steps.security.outputs.cache-hit && !inputs.skip_cache }}
3636
analysis-cache-hit: ${{ steps.analysis.outputs.cache-hit && !inputs.skip_cache }}
3737
build-cache-hit: ${{ steps.build.outputs.cache-hit && !inputs.skip_cache }}
38+
py_vers_current: ${{ steps.config.outputs.py_vers_current }}
39+
py_vers_lesser: ${{ steps.config.outputs.py_vers_lesser }}
40+
py_vers_min: ${{ steps.config.outputs.py_vers_min }}
41+
py_vers_max: ${{ steps.config.outputs.py_vers_max }}
3842
steps:
3943
- name: Lookup test result cache
4044
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 #v5.0.3
@@ -91,16 +95,71 @@ jobs:
9195
echo "fully_cached=${{ steps.tests.outputs.cache-hit && steps.security.outputs.cache-hit && steps.analysis.outputs.cache-hit }}" >> $GITHUB_OUTPUT
9296
9397
- name: Checkout codebase
94-
if: ${{ !steps.cache.outputs.fully_cached || inputs.skip_cache }}
9598
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
9699

97100
# Cache warmup for parallel dependent jobs
98101
- name: Prepare the Python environment
99102
if: ${{ !steps.cache.outputs.fully_cached || inputs.skip_cache }}
100103
uses: ./.github/actions/prepare
104+
with:
105+
skip_cache_restore: true
106+
107+
# Get configuration info, this step must not depend on anything from the prepare action
108+
- name: Get configuration info
109+
id: config
110+
shell: bash
111+
run: |
112+
# Resolve the Python versions supported by the project:
113+
python3 utils/pyvers.py >> $GITHUB_OUTPUT
114+
115+
compatibility:
116+
name: Compatibility Tests on Python ${{ matrix.python_version }}
117+
runs-on: ubuntu-24.04
118+
needs: prebuild
119+
concurrency:
120+
group: compatibility-${{ matrix.python_version }}-${{ github.sha }}
121+
env:
122+
CI: true
123+
strategy:
124+
# max-parallel: 1
125+
matrix:
126+
python_version: ${{ fromJSON(needs.prebuild.outputs.py_vers_lesser) }}
127+
include:
128+
- python_version: ${{ needs.prebuild.outputs.py_vers_min }}-lowest_deps
129+
steps:
130+
- name: Fetch compatibility result cache
131+
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 #v5.0.3
132+
id: compat
133+
with:
134+
lookup-only: true
135+
path: ./reports/compatibility/${{ matrix.python_version }}
136+
key: compatibility-${{ matrix.python_version }}-${{ github.sha }}
137+
138+
- name: Report on cache
139+
if: ${{ steps.compat.outputs.cache-hit == 'true' && !inputs.skip_cache }}
140+
run: |
141+
echo "::notice::Skipping compatibility test for Python ${{ matrix.python_version }} due to cached result for this commit."
142+
143+
- name: Checkout codebase
144+
if: ${{ steps.compat.outputs.cache-hit != 'true' || inputs.skip_cache }}
145+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
146+
147+
- name: Run compatibility test
148+
if: ${{ steps.compat.outputs.cache-hit != 'true' || inputs.skip_cache }}
149+
uses: ./.github/actions/tests
150+
with:
151+
python_version: ${{ matrix.python_version }}
152+
full: false
101153

154+
- name: Save compatibility result cache
155+
if: ${{ steps.compat.outputs.cache-hit != 'true' || inputs.skip_cache }}
156+
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 #v5.0.3
157+
with:
158+
path: ./reports/compatibility/${{ matrix.python_version }}
159+
key: compatibility-${{ matrix.python_version }}-${{ github.sha }}
160+
102161
tests:
103-
name: Functional Tests
162+
name: Functional Tests on Python ${{ needs.prebuild.outputs.py_vers_max }}
104163
runs-on: ubuntu-24.04
105164
needs: prebuild
106165
if: ${{ needs.prebuild.outputs.tests-cache-hit != 'true' }}
@@ -125,22 +184,14 @@ jobs:
125184
if: ${{ !steps.tests.outputs.cache-hit }}
126185
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
127186

128-
- name: Prepare the Python environment
187+
- name: Run the tests
129188
if: ${{ !steps.tests.outputs.cache-hit }}
130-
uses: ./.github/actions/prepare
131-
132-
- name: Run unit tests
133-
if: ${{ !steps.tests.outputs.cache-hit && github.event_name == 'pull_request'}}
134-
shell: bash
135-
run: pytest --cov-report lcov:./reports/tests/coverage.lcov
136-
137-
# For security and cost reasons, don't run integration tests on PRs
138-
- name: Run unit and integration tests
139-
if: ${{ !steps.tests.outputs.cache-hit && github.event_name != 'pull_request' }}
140-
shell: bash
189+
uses: ./.github/actions/tests
141190
env:
142191
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
143-
run: pytest --cov-report lcov:./reports/tests/coverage.lcov --plus_integration
192+
with:
193+
python_version: ${{ needs.prebuild.outputs.py_vers_max }}
194+
full: true
144195

145196
security:
146197
name: Security Checks
@@ -233,7 +284,7 @@ jobs:
233284
build:
234285
name: Build Package
235286
runs-on: ubuntu-24.04
236-
needs: [prebuild, tests, security, analysis]
287+
needs: [prebuild, tests, compatibility, security, analysis]
237288
if: ${{ !failure() && !cancelled() && needs.prebuild.outputs.build-cache-hit != 'true' && needs.prebuild.result != 'skipped' }}
238289
concurrency:
239290
group: build-${{ github.sha }}

.github/workflows/ci-publish.yml

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ jobs:
3232
build-cache-hit: ${{ steps.build.outputs.cache-hit && !inputs.skip_cache }}
3333
repo_name: ${{ steps.config.outputs.repo_name }}
3434
project_name: ${{ steps.config.outputs.project_name }}
35+
repository_url: ${{ steps.config.outputs.repository_url }}
3536
steps:
3637
- name: Sparse checkout codebase
3738
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
@@ -50,15 +51,7 @@ jobs:
5051
5152
# Ensure we are operating on a version tag
5253
if [[ "${GITHUB_REF}" != refs/tags/v* ]]; then
53-
echo "::error::Publishing must be done from a version tag."
54-
exit 1
55-
fi
56-
57-
# Ensure the version in pyproject.toml matches the git tag
58-
GIT_TAG_VERSION=${GITHUB_REF#refs/tags/v}
59-
PYPROJECT_VERSION=$(grep -Eo '^version\s*=\s*"[0-9.]*' pyproject.toml | cut -d'"' -f2)
60-
if [[ "${GITHUB_REF#refs/tags/v}" != "$PYPROJECT_VERSION" ]]; then
61-
echo "::error::Version mismatch! Git tag ($GIT_TAG_VERSION) doesn't match pyproject.toml version ($PYPROJECT_VERSION)"
54+
echo "::error::Publishing must be from a version-tagged commit."
6255
exit 1
6356
fi
6457

.gitignore

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
!.vscode/**
1111
!src/
1212
!src/**
13+
!utils/
14+
!utils/**
1315
!docs/
1416
!docs/**
1517
!tests/
@@ -33,6 +35,9 @@
3335
!pyproject.toml
3436
!uv.lock
3537
!mkdocs.yml
38+
!justfile
3639

3740
# Ignore these after the recusive include
38-
__pycache__
41+
__pycache__
42+
# This file is generated by hatch-vcs:
43+
src/vulcan_core/__about__.py

.vscode/extensions.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
{
22
"recommendations": [
3-
"github.copilot",
43
"github.copilot-chat"
54
]
65
}

dockerfiles/actimage.dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ FROM ubuntu:24.04
55
ARG DEBIAN_FRONTEND=noninteractive
66
ENV TZ=Etc/UTC
77

8-
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl sudo gnupg pipx jq build-essential
8+
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl sudo zstd gnupg pipx jq build-essential
99

1010
RUN useradd -m -d /home/runner -s /bin/bash runner &&\
1111
echo "runner ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/runner

0 commit comments

Comments
 (0)