From 78e54e9fd90dddf67ad3173f73bb269a9f5ded5d Mon Sep 17 00:00:00 2001 From: Pablo Pardo Garcia Date: Thu, 26 Mar 2026 12:28:07 +0100 Subject: [PATCH 1/2] ci: harden and improve GitHub Actions workflows Security: - Pin all third-party actions to immutable commit SHAs - Add permissions: {} at workflow level with explicit per-job grants - Migrate PyPI publishing to OIDC trusted publishing (remove PYPI_API_TOKEN) Correctness: - Fix version-bump to read base version from main, preventing double-bumps when labels change mid-PR - Fix label evaluation to check all PR labels (not just triggering event) and pick highest impact (major > minor > patch) - Reverse tag/publish order in release: tag first, then publish to PyPI - Add VERSION format validation before arithmetic in version-bump - Add twine check before PyPI upload Reliability: - Add concurrency groups to all workflows (cancel-in-progress where safe) - Move coverage badge commit to dedicated post-matrix job to eliminate race condition with parallel matrix runners - Eliminate duplicate pytest run on Python 3.13 (single pass with all reports) - Fix shell injection vectors: pass GitHub context via env: not inline New: - Add smoke.yml: fast test run on every push to main to catch merge-induced regressions before the next release Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/copilot-setup-steps.yml | 11 ++- .github/workflows/release.yml | 44 ++++++----- .github/workflows/smoke.yml | 44 +++++++++++ .github/workflows/test.yml | 91 ++++++++++++++--------- .github/workflows/version-bump.yml | 71 +++++++++++------- 5 files changed, 172 insertions(+), 89 deletions(-) create mode 100644 .github/workflows/smoke.yml 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 }}" From 39162d1ccd3f74a0e29f147fb7fa7cd214603cbf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 26 Mar 2026 11:51:55 +0000 Subject: [PATCH 2/2] docs: update coverage badge --- README.md | 2 +- coverage.xml | 1183 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1184 insertions(+), 1 deletion(-) create mode 100644 coverage.xml diff --git a/README.md b/README.md index a4a5790..e363731 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ - +

diff --git a/coverage.xml b/coverage.xml new file mode 100644 index 0000000..da4e3ae --- /dev/null +++ b/coverage.xml @@ -0,0 +1,1183 @@ + + + + + + /home/runner/work/glassflow-python-sdk/glassflow-python-sdk/src + /home/runner/work/glassflow-python-sdk/glassflow-python-sdk/src/glassflow + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +