diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml
index 759cc1e..26bb3e0 100644
--- a/.github/workflows/copilot-setup-steps.yml
+++ b/.github/workflows/copilot-setup-steps.yml
@@ -8,26 +8,25 @@ on:
paths:
- .github/workflows/copilot-setup-steps.yml
+permissions: {}
+
jobs:
copilot-setup-steps:
runs-on: ubuntu-latest
-
permissions:
contents: read
steps:
- - uses: actions/checkout@v4
- with:
- fetch-depth: 0
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Set up Python
- uses: actions/setup-python@v5
+ uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
id: setup-python
with:
python-version-file: "pyproject.toml"
- name: Install uv
- uses: astral-sh/setup-uv@v5
+ uses: astral-sh/setup-uv@e58605a9b6da7c637471fab8847a5e5a6b8df081 # v5
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index e474109..32073d0 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -6,6 +6,12 @@ on:
branches:
- main
+permissions: {}
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: false
+
jobs:
release:
if: |
@@ -14,23 +20,26 @@ jobs:
contains(github.event.pull_request.labels.*.name, 'bump:minor') ||
contains(github.event.pull_request.labels.*.name, 'bump:patch'))
runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ id-token: write # required for OIDC token request
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- name: Install uv
- uses: astral-sh/setup-uv@v5
+ uses: astral-sh/setup-uv@e58605a9b6da7c637471fab8847a5e5a6b8df081 # v5
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
-
+
- name: Set up Python
- uses: actions/setup-python@v5
+ uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
id: setup-python
with:
python-version-file: "pyproject.toml"
-
+
- name: Set up venv
run: |
uv venv --python ${{ steps.setup-python.outputs.python-path }}
@@ -44,28 +53,27 @@ jobs:
run: |
echo "VERSION=$(cat VERSION)" >> $GITHUB_OUTPUT
- - name: Create and push tag
- run: |
- git tag "v${{ steps.current_version.outputs.version }}"
- git push origin "v${{ steps.current_version.outputs.version }}"
- # Wait for tag to be available
- sleep 2
-
- name: Build package
run: |
uv build
+ - name: Validate package
+ run: |
+ uv run twine check dist/*
+
+ - name: Create and push tag
+ run: |
+ git tag "v${{ steps.current_version.outputs.VERSION }}"
+ git push origin "v${{ steps.current_version.outputs.VERSION }}"
+
- name: Publish to PyPI
- env:
- TWINE_USERNAME: __token__
- TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
run: |
- uv run twine upload dist/*
+ uv publish
- name: Create GitHub Release
- uses: softprops/action-gh-release@v1
+ uses: softprops/action-gh-release@26994186c0ac3ef5cae75ac16aa32e8153525f77 # v1
with:
- tag_name: "v${{ steps.current_version.outputs.version }}"
+ tag_name: "v${{ steps.current_version.outputs.VERSION }}"
files: |
dist/*.whl
dist/*.tar.gz
diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml
new file mode 100644
index 0000000..5279925
--- /dev/null
+++ b/.github/workflows/smoke.yml
@@ -0,0 +1,44 @@
+name: Smoke Test
+
+on:
+ push:
+ branches:
+ - main
+
+permissions: {}
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ smoke:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ steps:
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
+
+ - name: Set up Python
+ uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
+ id: setup-python
+ with:
+ python-version-file: "pyproject.toml"
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@e58605a9b6da7c637471fab8847a5e5a6b8df081 # v5
+ with:
+ enable-cache: true
+ cache-dependency-glob: "uv.lock"
+
+ - name: Set up venv
+ run: |
+ uv venv --python ${{ steps.setup-python.outputs.python-path }}
+
+ - name: Install dependencies
+ run: |
+ uv pip install -e .[test]
+
+ - name: Run tests
+ run: |
+ uv run pytest
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 638356b..90c7755 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -5,25 +5,31 @@ on:
branches:
- main
+permissions: {}
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
lint:
runs-on: ubuntu-latest
+ permissions:
+ contents: read
steps:
- - uses: actions/checkout@v4
- with:
- fetch-depth: 0
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Set up Python
- uses: actions/setup-python@v5
+ uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
id: setup-python
with:
python-version-file: "pyproject.toml"
-
+
- name: Install uv
- uses: astral-sh/setup-uv@v5
+ uses: astral-sh/setup-uv@e58605a9b6da7c637471fab8847a5e5a6b8df081 # v5
with:
- enable-cache: true
- cache-dependency-glob: "uv.lock"
+ enable-cache: true
+ cache-dependency-glob: "uv.lock"
- name: Set up venv
run: |
@@ -45,22 +51,20 @@ jobs:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
runs-on: ubuntu-latest
permissions:
- pull-requests: write
- checks: write
- contents: write
+ contents: read
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v5
+ uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
id: setup-python
with:
python-version: ${{ matrix.python-version }}
- name: Install uv
- uses: astral-sh/setup-uv@v5
+ uses: astral-sh/setup-uv@e58605a9b6da7c637471fab8847a5e5a6b8df081 # v5
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
@@ -75,17 +79,39 @@ jobs:
- name: Run tests with coverage
run: |
- uv run pytest --cov=src --cov-report=term-missing
+ if [[ "${{ matrix.python-version }}" == "3.13" ]]; then
+ uv run pytest --cov=src --cov-report=term-missing --cov-report=xml:coverage.xml
+ else
+ uv run pytest --cov=src --cov-report=term-missing
+ fi
- - name: Generate coverage report
+ - name: Upload coverage report
if: matrix.python-version == '3.13'
- run: |
- uv run pytest --cov=src --cov-report=xml:coverage.xml
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
+ with:
+ name: coverage-report
+ path: coverage.xml
+ retention-days: 1
+
+ badge:
+ needs: test
+ runs-on: ubuntu-latest
+ permissions:
+ pull-requests: write
+ contents: write
+ steps:
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
+ with:
+ ref: ${{ github.head_ref }}
+
+ - name: Download coverage report
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
+ with:
+ name: coverage-report
- name: Pytest coverage comment
- if: matrix.python-version == '3.13'
id: coverageComment
- uses: MishaKav/pytest-coverage-comment@main
+ uses: MishaKav/pytest-coverage-comment@45d7be6c0a6b6d3c3a5fc40345e8fb4a1c4aa800 # main
with:
pytest-xml-coverage-path: ./coverage.xml
title: "Test Coverage Report"
@@ -99,30 +125,21 @@ jobs:
unique-id-for-comment: "python-coverage"
- name: Update README with coverage badge
- if: matrix.python-version == '3.13'
+ env:
+ COVERAGE: ${{ steps.coverageComment.outputs.coverage }}
+ COLOR: ${{ steps.coverageComment.outputs.color }}
run: |
- # Extract coverage percentage and color from the coverageComment step
- COVERAGE_PERCENTAGE=$(echo "${{ steps.coverageComment.outputs.coverage }}" | grep -o '[0-9]*%' | tr -d '%')
- BADGE_COLOR=$(echo "${{ steps.coverageComment.outputs.color }}" | tr -d '#')
-
- # Create the badge URL
+ COVERAGE_PERCENTAGE=$(echo "$COVERAGE" | grep -o '[0-9]*%' | tr -d '%')
+ BADGE_COLOR=$(echo "$COLOR" | tr -d '#')
BADGE_URL="https://img.shields.io/badge/coverage-${COVERAGE_PERCENTAGE}%25-${BADGE_COLOR}"
-
- # Update README with the badge
sed -i "//,//c\\
\\
- \\
+
\\
" README.md
- - name: Clean up coverage file
- if: matrix.python-version == '3.13'
- run: |
- rm -f coverage.xml
-
- name: Commit and push README changes
- if: matrix.python-version == '3.13'
- uses: actions-js/push@master
+ uses: actions-js/push@5a7cbd780d82c0c937b5977586e641b2fd94acc5 # v1.5
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: ${{ github.head_ref }}
- message: "docs: update coverage badge"
\ No newline at end of file
+ message: "docs: update coverage badge"
diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml
index c18f4a8..3513bfe 100644
--- a/.github/workflows/version-bump.yml
+++ b/.github/workflows/version-bump.yml
@@ -6,6 +6,12 @@ on:
branches:
- main
+permissions: {}
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
determine-bump:
runs-on: ubuntu-latest
@@ -18,45 +24,44 @@ jobs:
steps:
- name: Determine highest bump type
id: bump_type
+ env:
+ LABELS: ${{ toJSON(github.event.pull_request.labels.*.name) }}
run: |
- LABEL="${{ github.event.label.name }}"
- BUMP_TYPE="patch" # Default to lowest impact
-
- if [[ "$LABEL" == "bump:major" ]]; then
+ # Evaluate all current PR labels and pick the highest impact
+ BUMP_TYPE="patch"
+
+ if echo "$LABELS" | grep -q '"bump:major"'; then
BUMP_TYPE="major"
- elif [[ "$LABEL" == "bump:minor" ]]; then
+ elif echo "$LABELS" | grep -q '"bump:minor"'; then
BUMP_TYPE="minor"
fi
-
+
echo "bump_type=$BUMP_TYPE" >> $GITHUB_OUTPUT
echo "Using $BUMP_TYPE version bump (highest impact label found)"
bump-version:
needs: determine-bump
runs-on: ubuntu-latest
- if: |
- contains(github.event.pull_request.labels.*.name, 'bump:major') ||
- contains(github.event.pull_request.labels.*.name, 'bump:minor') ||
- contains(github.event.pull_request.labels.*.name, 'bump:patch')
-
+ permissions:
+ contents: write
steps:
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
-
+
- name: Set up Python
- uses: actions/setup-python@v5
+ uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
id: setup-python
with:
python-version-file: "pyproject.toml"
-
+
- name: Install uv
- uses: astral-sh/setup-uv@v5
+ uses: astral-sh/setup-uv@e58605a9b6da7c637471fab8847a5e5a6b8df081 # v5
with:
- enable-cache: true
- cache-dependency-glob: "uv.lock"
-
+ enable-cache: true
+ cache-dependency-glob: "uv.lock"
+
- name: Set up venv
run: |
uv venv --python ${{ steps.setup-python.outputs.python-path }}
@@ -68,18 +73,26 @@ jobs:
- name: Get current version
id: current_version
run: |
- CURRENT_VERSION=$(cat VERSION)
+ # Always read from main to avoid double-bumping when labels change mid-PR
+ CURRENT_VERSION=$(git show origin/main:VERSION)
echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
- name: Calculate new version
id: new_version
+ env:
+ CURRENT: ${{ steps.current_version.outputs.current_version }}
+ BUMP_TYPE: ${{ needs.determine-bump.outputs.bump_type }}
run: |
- CURRENT=${{ steps.current_version.outputs.current_version }}
- BUMP_TYPE=${{ needs.determine-bump.outputs.bump_type }}
-
+ # Validate VERSION format (must be MAJOR.MINOR.PATCH with numeric parts only)
+ if ! echo "$CURRENT" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
+ echo "ERROR: VERSION file contains invalid format: '$CURRENT'"
+ echo "Expected format: MAJOR.MINOR.PATCH (e.g. 1.2.3)"
+ exit 1
+ fi
+
# Split version into parts
IFS='.' read -r major minor patch <<< "$CURRENT"
-
+
# Bump version based on type
case "$BUMP_TYPE" in
"major")
@@ -95,17 +108,19 @@ jobs:
patch=$((patch + 1))
;;
esac
-
+
NEW_VERSION="${major}.${minor}.${patch}"
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
- name: Update VERSION file
+ env:
+ NEW_VERSION: ${{ steps.new_version.outputs.new_version }}
run: |
- echo "${{ steps.new_version.outputs.new_version }}" > VERSION
+ echo "$NEW_VERSION" > VERSION
- name: Commit and push version changes
- uses: actions-js/push@master
+ uses: actions-js/push@5a7cbd780d82c0c937b5977586e641b2fd94acc5 # v1.5
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: ${{ github.head_ref }}
- message: "chore: bump version to ${{ steps.new_version.outputs.new_version }}"
\ No newline at end of file
+ message: "chore: bump version to ${{ steps.new_version.outputs.new_version }}"
diff --git a/README.md b/README.md
index a4a5790..e363731 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,7 @@
-
+