Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
341 changes: 341 additions & 0 deletions .github/workflows/update-python-docker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,341 @@
name: Update Python and Docker versions

on:
# Run every 2 months (first day at 3 AM UTC)
schedule:
- cron: '0 3 1 */2 *'
# Allow manual triggering for testing
workflow_dispatch: {}

Comment on lines +1 to +9
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The workflow doesn't define a concurrency group, which could allow multiple simultaneous runs if triggered both by schedule and manual dispatch. This could lead to duplicate PR creation attempts or wasted CI resources running the same tests multiple times. Consider adding a concurrency configuration at the job or workflow level to cancel in-progress runs when a new one starts, similar to how other workflows might handle this.

Copilot uses AI. Check for mistakes.
concurrency:
group: python-docker-update
cancel-in-progress: true

jobs:
update-python:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Get current Python version
id: current-version
run: |
# Extract current Python version from build-ci-image.yml
CURRENT_VERSION=$(grep -m1 "python-version:" .github/workflows/build-ci-image.yml | sed -E "s/.*python-version:[[:space:]]*['\"]?([0-9.]+)['\"]?.*/\1/")
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
echo "Using Python $CURRENT_VERSION for workflow execution"

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ steps.current-version.outputs.version }}

Comment on lines +33 to +37
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This workflow sets up Python using the current version (from build-ci-image.yml) and never re-runs actions/setup-python after updating files. As a result, the venv/lint/pytest steps run on the old Python version, not the newly detected version you’re trying to validate. Add a second actions/setup-python step gated on update_available == 'true' that uses ${{ steps.get-version.outputs.new_version }} before creating the venv and running tests.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 8b949d9. Added a new "Set up new Python version for testing" step that runs actions/setup-python@v5 with the new version after files are updated, ensuring all tests run with the correct Python version.

- name: Check for Python updates
id: check-update
run: |
# Run the update script; if it fails, this step (and workflow) will fail.
set -euo pipefail
python3 scripts/update_python_version.py

# Determine whether any tracked files were modified by the script.
# git diff exits with 1 if there are differences, so disable pipefail for this check
set +e
git diff --quiet
DIFF_EXIT=$?
set -e

if [ $DIFF_EXIT -eq 0 ]; then
echo "update_available=false" >> "$GITHUB_OUTPUT"
else
echo "update_available=true" >> "$GITHUB_OUTPUT"
fi

- name: Get updated Python version
id: get-version
if: steps.check-update.outputs.update_available == 'true'
run: |
# Extract the new Python version from build-ci-image.yml
NEW_VERSION=$(grep -m1 "python-version:" .github/workflows/build-ci-image.yml | sed -E "s/.*python-version:[[:space:]]*['\"]?([0-9.]+)['\"]?.*/\1/")
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
echo "Updated to Python $NEW_VERSION"

- name: Set up new Python version for testing
if: steps.check-update.outputs.update_available == 'true'
uses: actions/setup-python@v5
with:
python-version: ${{ steps.get-version.outputs.new_version }}

- name: Create venv and install deps
if: steps.check-update.outputs.update_available == 'true'
run: |
set -euo pipefail
python3 -m venv .venv
./.venv/bin/python -m pip install --upgrade pip
./.venv/bin/python -m pip install -r scripts/requirements.txt -r scripts/requirements-dev.txt

- name: Run linting tests
id: lint-test
if: steps.check-update.outputs.update_available == 'true'
continue-on-error: true
run: |
set -euo pipefail
echo "Running flake8..."
./.venv/bin/python -m flake8 -q
echo "lint_passed=true" >> $GITHUB_OUTPUT

- name: Run pytest tests
id: pytest-test
if: steps.check-update.outputs.update_available == 'true'
continue-on-error: true
run: |
set -euo pipefail
echo "Running pytest..."
./.venv/bin/python -m pytest -q
echo "tests_passed=true" >> $GITHUB_OUTPUT

- name: Set up Docker Buildx
if: steps.check-update.outputs.update_available == 'true'
uses: docker/setup-buildx-action@v3

- name: Test build Docker images
id: docker-test
if: steps.check-update.outputs.update_available == 'true'
continue-on-error: true
run: |
set -euo pipefail
echo "Testing QR Dockerfile build..."
docker build -f .github/ci/Dockerfile.qr -t test-qr:latest .
echo "Testing Infra Dockerfile build..."
docker build -f .github/ci/Dockerfile.infra -t test-infra:latest .
echo "docker_build_passed=true" >> $GITHUB_OUTPUT

- name: Run tests in Docker container
id: docker-pytest
if: steps.check-update.outputs.update_available == 'true'
continue-on-error: true
run: |
set -euo pipefail
echo "Running pytest in Docker container..."
docker run --rm --user 0 -v "$PWD:/workspace" -w /workspace test-infra:latest bash -c "python -m venv --system-site-packages .venv && ./.venv/bin/python -m pytest -q"
echo "docker_tests_passed=true" >> $GITHUB_OUTPUT
Comment on lines +81 to +125
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test steps (lint-test, pytest-test, docker-test, docker-pytest) unconditionally set their output variables to "true" even if they succeed. If any of these steps fail, the shell will exit due to "set -euo pipefail" before the output variable is set, leaving it undefined. The PR creation condition checks if these outputs equal 'true', which would be false if undefined. However, the "Handle test failures" step checks if outputs != 'true', which would be true for both failures and undefined values. This could cause the failure handler to run even when tests didn't execute at all. Consider using GitHub Actions' built-in conclusion/outcome checks or ensure outputs are always set.

Copilot uses AI. Check for mistakes.
Comment on lines +123 to +125
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test-infra:latest sets USER ci (see .github/ci/Dockerfile.infra), but this docker run mounts the repo and tries to create .venv inside /workspace. This likely fails due to filesystem permissions (the existing container-based CI workflows run with options: --user 0, e.g. .github/workflows/pytest.yml:19-21). Run the container as root (--user 0) or write the venv to a writable path (e.g. /tmp`) to match established CI behavior.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 8b949d9. Added --user 0 flag to the docker run command to match the pattern used in other workflows (e.g., pytest.yml).


- name: Check for existing update issue
id: check-issue
if: steps.check-update.outputs.update_available == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "Checking for existing Python update issues..."
EXISTING_ISSUES=$(gh issue list --state open --label "python-update" --json number,title --limit 1)
if [ "$EXISTING_ISSUES" != "[]" ]; then
ISSUE_NUMBER=$(echo "$EXISTING_ISSUES" | jq -r '.[0].number')
echo "Found existing open issue #$ISSUE_NUMBER"
echo "has_existing=true" >> $GITHUB_OUTPUT
echo "issue_number=$ISSUE_NUMBER" >> $GITHUB_OUTPUT
else
echo "No existing open Python update issue found"
echo "has_existing=false" >> $GITHUB_OUTPUT
fi

- name: Check for existing update PR
id: check-pr
if: steps.check-update.outputs.update_available == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "Checking for existing Python update PRs..."
EXISTING_PRS=$(gh pr list --state open --label "python-update" --json number,title --limit 1)
if [ "$EXISTING_PRS" != "[]" ]; then
PR_NUMBER=$(echo "$EXISTING_PRS" | jq -r '.[0].number')
echo "Found existing open PR #$PR_NUMBER"
echo "has_existing=true" >> $GITHUB_OUTPUT
echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT
else
echo "No existing open Python update PR found"
echo "has_existing=false" >> $GITHUB_OUTPUT
fi

- name: Create issue for update
id: create-issue
if: |
steps.check-update.outputs.update_available == 'true' &&
steps.lint-test.outcome == 'success' &&
steps.pytest-test.outcome == 'success' &&
steps.docker-test.outcome == 'success' &&
steps.docker-pytest.outcome == 'success' &&
steps.check-issue.outputs.has_existing != 'true' &&
steps.check-pr.outputs.has_existing != 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
NEW_VERSION="${{ steps.get-version.outputs.new_version }}"

# Ensure the 'python-update' label exists
if ! gh label view "python-update" >/dev/null 2>&1; then
echo "Creating missing label 'python-update'"
gh label create "python-update" --description "Automated Python version updates" --color 0366d6 || true
fi

# Create issue with heredoc
cat > /tmp/issue_body.txt << 'ISSUE_EOF'
## Python and Docker Version Update Available

A new stable Python version is available.

### Proposed Changes
- Update `.github/ci/Dockerfile.infra` to use new Python slim image
- Update `.github/ci/Dockerfile.qr` to use new Python slim image
- Update `.github/workflows/build-ci-image.yml` to use new Python version
- Update `.github/workflows/check-todo.yml` to use new Python version

### Automated Test Results
All automated tests have passed with the new version:
- ✅ Linting (flake8)
- ✅ Unit tests (pytest)
- ✅ Docker image builds
- ✅ Tests in Docker containers

### Next Steps
A pull request will be automatically created and linked to this issue with the necessary changes.
ISSUE_EOF

ISSUE_NUMBER=$(gh issue create \
--title "Update Python to version ${NEW_VERSION}" \
--body-file /tmp/issue_body.txt \
--label "python-update" \
--json number -q '.number')

echo "issue_number=$ISSUE_NUMBER" >> $GITHUB_OUTPUT
echo "Created issue #$ISSUE_NUMBER"

- name: Create Pull Request
if: |
steps.check-update.outputs.update_available == 'true' &&
steps.lint-test.outcome == 'success' &&
steps.pytest-test.outcome == 'success' &&
steps.docker-test.outcome == 'success' &&
steps.docker-pytest.outcome == 'success' &&
steps.check-issue.outputs.has_existing != 'true' &&
steps.check-pr.outputs.has_existing != 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Configure git
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

NEW_VERSION="${{ steps.get-version.outputs.new_version }}"
ISSUE_NUMBER="${{ steps.create-issue.outputs.issue_number }}"
BRANCH_NAME="automated/update-python-${NEW_VERSION}"

# Create and switch to new branch
git checkout -b "$BRANCH_NAME"

# Add changes
git add .github/ci/Dockerfile.infra
git add .github/ci/Dockerfile.qr
git add .github/workflows/build-ci-image.yml
git add .github/workflows/check-todo.yml

# Commit changes with proper formatting
git commit \
-m "chore: update Python to version ${NEW_VERSION}" \
-m "" \
-m "- Update Dockerfile.infra to use python:${NEW_VERSION}-slim" \
-m "- Update Dockerfile.qr to use python:${NEW_VERSION}-slim" \
-m "- Update build-ci-image.yml to use Python ${NEW_VERSION}" \
-m "- Update check-todo.yml to use Python ${NEW_VERSION}" \
-m "" \
-m "All tests passed:" \
-m "- Linting: ✓" \
-m "- Pytest: ✓" \
-m "- Docker builds: ✓" \
-m "- Docker tests: ✓" \
-m "" \
-m "Fixes #${ISSUE_NUMBER}"

# Push branch (use force-with-lease to allow updates if no remote changes)
if ! git push --force-with-lease origin "$BRANCH_NAME"; then
echo "⚠️ Force-with-lease failed - remote branch has been updated by another process"
echo "Attempting regular push (will fail if branch exists with different history)"
git push origin "$BRANCH_NAME"
fi

# Create PR body with heredoc
cat > /tmp/pr_body.txt << 'PR_EOF'
## Automated Python and Docker Version Update

This PR updates the Python version used across the repository.

### Changes Made
- Updated `.github/ci/Dockerfile.infra` to use new Python slim image
- Updated `.github/ci/Dockerfile.qr` to use new Python slim image
- Updated `.github/workflows/build-ci-image.yml` to use new Python version
- Updated `.github/workflows/check-todo.yml` to use new Python version

### Test Results
All automated tests have passed:
- ✅ Linting (flake8)
- ✅ Unit tests (pytest)
- ✅ Docker image builds
- ✅ Tests in Docker containers

### What to Review
- Verify that all CI workflows pass successfully
- Check that no new deprecation warnings are introduced
- Ensure Docker images build and push correctly

This PR was automatically created by the `update-python-docker` workflow.
PR_EOF

# Add issue reference to PR body
echo "" >> /tmp/pr_body.txt
echo "Closes #${ISSUE_NUMBER}" >> /tmp/pr_body.txt

# Create PR linked to the issue
gh pr create \
--title "chore: Update Python to version ${NEW_VERSION}" \
--body-file /tmp/pr_body.txt \
--label "python-update" \
--base main \
--head "$BRANCH_NAME"

- name: Handle test failures
if: |
!cancelled() &&
steps.check-update.outputs.update_available == 'true' &&
(steps.lint-test.outcome == 'failure' ||
steps.pytest-test.outcome == 'failure' ||
steps.docker-test.outcome == 'failure' ||
steps.docker-pytest.outcome == 'failure')
run: |
echo "❌ Tests failed with the new Python version"
echo "Lint outcome: ${{ steps.lint-test.outcome }}"
echo "Pytest outcome: ${{ steps.pytest-test.outcome }}"
echo "Docker build outcome: ${{ steps.docker-test.outcome }}"
echo "Docker tests outcome: ${{ steps.docker-pytest.outcome }}"
echo ""
echo "Manual intervention required to fix compatibility issues."
echo "The version update has been applied but not committed."
exit 1

- name: No update needed
if: steps.check-update.outputs.update_available != 'true'
run: |
echo "✅ Already using the latest Python version"

- name: Skip - existing issue or PR found
if: steps.check-update.outputs.update_available == 'true' && (steps.check-issue.outputs.has_existing == 'true' || steps.check-pr.outputs.has_existing == 'true')
run: |
echo "ℹ️ Skipping creation - existing Python update issue/PR found"
if [ "${{ steps.check-issue.outputs.has_existing }}" == "true" ]; then
echo "Existing issue: #${{ steps.check-issue.outputs.issue_number }}"
fi
if [ "${{ steps.check-pr.outputs.has_existing }}" == "true" ]; then
echo "Existing PR: #${{ steps.check-pr.outputs.pr_number }}"
fi
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,21 @@ This repository includes automated workflows:
- **Lint** — Runs flake8 on Python code
- **pytest** — Runs pytest test suite
- **Generate QR** — Automatically generates and commits QR codes
- **Update Python/Docker** — Periodically checks for Python updates and creates issues with linked PRs

**Python and Docker Version Updates**

- The repository includes a scheduled workflow (`.github/workflows/update-python-docker.yml`) that automatically keeps Python and Docker images up-to-date.
- High-level behavior:
- Runs every 2 months (first day at 3 AM UTC) and can be manually triggered via `workflow_dispatch`
- Detects the latest stable Python version from Docker Hub (supports Python 3.8+)
- Updates all Python version references in Dockerfiles and workflow files
- Runs comprehensive tests (linting, pytest, Docker builds) to verify compatibility
- Creates an issue documenting the available update
- Creates a linked PR with the updates if all tests pass
- Checks for existing update issues and PRs to avoid duplicates using the `python-update` label
- Files updated: `.github/ci/Dockerfile.infra`, `.github/ci/Dockerfile.qr`, `.github/workflows/build-ci-image.yml`, `.github/workflows/check-todo.yml`
- Why this helps: Keeps the repository secure and up-to-date with the latest Python releases without manual intervention, ensuring compatibility is tested before merging.

**Copilot Trigger & Automation Label**

Expand Down
Loading