Lock Files Renewal Action (Pip) #445
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| --- | |
| # This GitHub action is meant to update the lock files (pylock.toml) | |
| name: Lock Files Renewal Action | |
| permissions: {} # least-privilege: grant per-job below | |
| on: # yamllint disable-line rule:truthy | |
| # Triggers the workflow every Wednesday at 1am UTC | |
| schedule: | |
| - cron: "0 1 * * 3" # Weekly lockfile update | |
| - cron: "0 9,15 * * 1-5" # Auto-merge check at 9am and 3pm UTC on weekdays | |
| workflow_dispatch: # for manual trigger workflow from GH Web UI | |
| inputs: | |
| operation: | |
| description: 'Which operation to run' | |
| required: true | |
| default: 'update-lockfiles' | |
| type: choice | |
| options: | |
| - 'update-lockfiles' | |
| - 'auto-merge' | |
| branch: | |
| description: 'Specify branch (for update-lockfiles)' | |
| required: false | |
| default: 'main' | |
| index_mode: | |
| description: 'Index mode for lock file generation (for update-lockfiles)' | |
| required: false | |
| default: 'auto' | |
| type: choice | |
| options: | |
| - 'auto' | |
| - 'public-index' | |
| - 'rh-index' | |
| force_upgrade: | |
| description: 'Force upgrade all packages to latest versions (for update-lockfiles)' | |
| required: false | |
| default: 'false' | |
| type: choice | |
| options: | |
| - 'false' | |
| - 'true' | |
| jobs: | |
| refresh-lock-files: | |
| # Only run on Wednesday schedule or manual dispatch with 'update-lockfiles' operation | |
| if: (github.event_name == 'workflow_dispatch' && github.event.inputs.operation == 'update-lockfiles') || (github.event_name == 'schedule' && github.event.schedule == '0 1 * * 3') | |
| runs-on: ubuntu-26.04 | |
| concurrency: | |
| group: refresh-lock-files-${{ github.ref }} | |
| cancel-in-progress: false | |
| permissions: | |
| contents: read # checkout only; push and PR creation use the PAT | |
| packages: read | |
| env: | |
| BRANCH: ${{ github.event.inputs.branch || 'main' }} | |
| INDEX_MODE: ${{ github.event.inputs.index_mode || 'auto' }} | |
| # Force upgrade on scheduled runs, or when explicitly requested | |
| FORCE_LOCKFILES_UPGRADE: ${{ github.event_name == 'schedule' && '1' || (github.event.inputs.force_upgrade == 'true' && '1' || '0') }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| ref: ${{ env.BRANCH }} | |
| persist-credentials: false | |
| fetch-depth: 0 # update_imagestream_annotations_from_pylock.py uses git show for tag SHAs | |
| - name: Configure Git | |
| run: | | |
| git config --global user.email "github-actions[bot]@users.noreply.github.com" | |
| git config --global user.name "GitHub Actions" | |
| - name: Setup uv and Python | |
| uses: ./.github/actions/setup-uv | |
| - name: Run make refresh-lock-files | |
| run: | | |
| make refresh-lock-files INDEX_MODE="$INDEX_MODE" | |
| env: | |
| FORCE_LOCKFILES_UPGRADE: ${{ env.FORCE_LOCKFILES_UPGRADE }} | |
| # Regenerate Dockerfiles after lockfile changes. | |
| # Lockfiles may bump package versions (e.g. uv) that dockerfile_fragments.py | |
| # derives and embeds in Dockerfiles. Without this step, Dockerfiles drift | |
| # out of sync and check-generated-code fails. | |
| # See: commit 41aed2702 (lockfile-derived versions), PR #3239 (broke main). | |
| - name: Regenerate Dockerfiles from updated lockfiles | |
| run: | | |
| uv run scripts/dockerfile_fragments.py | |
| uv run manifests/tools/generate_kustomization.py | |
| - name: Refresh ImageStream notebook dependency annotations from pylocks | |
| run: | | |
| uv run python manifests/tools/update_imagestream_annotations_from_pylock.py --variant odh | |
| uv run python manifests/tools/update_imagestream_annotations_from_pylock.py --variant rhoai | |
| - name: Login to GitHub Container Registry | |
| if: ${{ github.repository != 'opendatahub-io/notebooks' }} | |
| uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Validate manifests (make test) | |
| run: make test | |
| - name: Create Pull Request | |
| env: | |
| GH_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }} | |
| run: | | |
| git add . | |
| if git diff --cached --quiet; then | |
| echo "No changes to commit." | |
| exit 0 | |
| fi | |
| # Skip PR creation if only timestamp changes | |
| if git diff --cached -I '^#.*--exclude-newer' --quiet; then | |
| echo "Only timestamp changes. Skipping PR creation." | |
| exit 0 | |
| fi | |
| BRANCH_NAME="lockfile-update-$(date +%Y%m%d-%H%M)" | |
| git checkout -b "$BRANCH_NAME" | |
| git commit -m "Update lock files and ImageStream dependency annotations" | |
| # https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation#using-an-installation-access-token-to-authenticate-as-an-app-installation | |
| git push -u "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" "$BRANCH_NAME" | |
| gh pr create \ | |
| --title "Update lock files and ImageStream dependency annotations" \ | |
| --head "$BRANCH_NAME" \ | |
| --body "$(cat <<'EOF' | |
| Automated lock file update, Dockerfile/kustomization regeneration, and ImageStream `notebook-python-dependencies` / `notebook-software` annotations from pylocks. | |
| **Auto-merge policy:** This PR will be automatically merged after 1 working day unless: | |
| - Moved to draft status | |
| - Labeled with `do-not-merge/*` | |
| - Manually merged or closed | |
| EOF | |
| )" \ | |
| --label "automated-lockfile-update" \ | |
| --base "$BRANCH" | |
| auto-merge-lockfile-prs: | |
| # Run on auto-merge schedule or manual dispatch with 'auto-merge' operation | |
| if: (github.event_name == 'workflow_dispatch' && github.event.inputs.operation == 'auto-merge') || (github.event_name == 'schedule' && github.event.schedule == '0 9,15 * * 1-5') | |
| runs-on: ubuntu-26.04 | |
| permissions: | |
| pull-requests: write # github.token is only used for PR approval (line 225) | |
| steps: | |
| - name: Auto-merge eligible lockfile PRs | |
| env: | |
| GH_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }} | |
| GITHUB_TOKEN_FOR_APPROVAL: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| REPO="${{ github.repository }}" | |
| echo "Searching for PRs with label 'automated-lockfile-update'..." | |
| # Get all open PRs with the automated-lockfile-update label | |
| PRS=$(gh pr list --repo "$REPO" --label "automated-lockfile-update" --state open --json number,title,createdAt,isDraft,labels --limit 50) | |
| if [ "$PRS" = "[]" ] || [ -z "$PRS" ]; then | |
| echo "No open PRs found with label 'automated-lockfile-update'" | |
| exit 0 | |
| fi | |
| echo "Found PRs: $PRS" | |
| # Process each PR | |
| echo "$PRS" | jq -c '.[]' | while read -r pr; do | |
| PR_NUM=$(echo "$pr" | jq -r '.number') | |
| PR_TITLE=$(echo "$pr" | jq -r '.title') | |
| CREATED_AT=$(echo "$pr" | jq -r '.createdAt') | |
| IS_DRAFT=$(echo "$pr" | jq -r '.isDraft') | |
| LABELS=$(echo "$pr" | jq -r '.labels[].name' 2>/dev/null || echo "") | |
| echo "" | |
| echo "=== Processing PR #$PR_NUM: $PR_TITLE ===" | |
| echo "Created at: $CREATED_AT" | |
| echo "Is draft: $IS_DRAFT" | |
| echo "Labels: $LABELS" | |
| # Skip drafts | |
| if [ "$IS_DRAFT" = "true" ]; then | |
| echo "SKIP: PR #$PR_NUM is a draft" | |
| continue | |
| fi | |
| # Skip if has do-not-merge/* label | |
| if echo "$LABELS" | grep -q "^do-not-merge/"; then | |
| echo "SKIP: PR #$PR_NUM has a do-not-merge/* label" | |
| continue | |
| fi | |
| # Check if PR is at least 1 working day old | |
| # Working day = Monday-Friday, so: | |
| # - If created Mon-Thu, eligible next day | |
| # - If created Fri, eligible Mon | |
| # - If created Sat, eligible Mon | |
| # - If created Sun, eligible Tue | |
| CREATED_TS=$(date -d "$CREATED_AT" +%s 2>/dev/null || date -j -f "%Y-%m-%dT%H:%M:%SZ" "$CREATED_AT" +%s 2>/dev/null || date -j -f "%Y-%m-%dT%H:%M:%S" "${CREATED_AT%Z}" +%s) | |
| NOW_TS=$(date +%s) | |
| CREATED_DOW=$(date -d "$CREATED_AT" +%u 2>/dev/null || date -j -f "%Y-%m-%dT%H:%M:%SZ" "$CREATED_AT" +%u 2>/dev/null || date -j -f "%Y-%m-%dT%H:%M:%S" "${CREATED_AT%Z}" +%u) | |
| if [ -z "$CREATED_TS" ] || [ -z "$CREATED_DOW" ]; then | |
| echo "WARNING: Failed to parse date '$CREATED_AT' for PR #$PR_NUM. Skipping." | |
| continue | |
| fi | |
| # Calculate minimum age in seconds for 1 working day | |
| # Base: 24 hours = 86400 seconds | |
| # If created on Friday (5), add weekend: 72 hours = 259200 seconds | |
| # If created on Saturday (6), need to wait till Monday + 1 day: 48 + 24 = 72 hours | |
| # If created on Sunday (7), need to wait till Tuesday: 24 + 24 = 48 hours... actually Mon+1day = Tue | |
| case "$CREATED_DOW" in | |
| 5) MIN_AGE_SECONDS=$((72 * 3600)) ;; # Friday -> Monday (3 days) | |
| 6) MIN_AGE_SECONDS=$((48 * 3600)) ;; # Saturday -> Monday (2 days) | |
| 7) MIN_AGE_SECONDS=$((48 * 3600)) ;; # Sunday -> Tuesday (2 days, Mon is working day 0) | |
| *) MIN_AGE_SECONDS=$((24 * 3600)) ;; # Mon-Thu -> next day (1 day) | |
| esac | |
| AGE_SECONDS=$((NOW_TS - CREATED_TS)) | |
| AGE_HOURS=$((AGE_SECONDS / 3600)) | |
| echo "PR age: ${AGE_HOURS} hours (minimum required: $((MIN_AGE_SECONDS / 3600)) hours)" | |
| if [ "$AGE_SECONDS" -lt "$MIN_AGE_SECONDS" ]; then | |
| echo "SKIP: PR #$PR_NUM is not old enough (created $AGE_HOURS hours ago, need $((MIN_AGE_SECONDS / 3600)) hours)" | |
| continue | |
| fi | |
| echo "Checking review status for PR #$PR_NUM..." | |
| # Check if PR has an approving review | |
| REVIEWS=$(gh pr view "$PR_NUM" --repo "$REPO" --json reviews --jq '.reviews[] | select(.state == "APPROVED")' 2>/dev/null || echo "") | |
| if [ -z "$REVIEWS" ]; then | |
| echo "No approving review found. Adding approval using github-actions bot..." | |
| # Get PR author to ensure we don't approve our own PR | |
| PR_AUTHOR=$(gh pr view "$PR_NUM" --repo "$REPO" --json author --jq '.author.login') | |
| echo "PR author: $PR_AUTHOR" | |
| # Add approving review using GITHUB_TOKEN (github-actions bot) | |
| # This is different from GH_ACCESS_TOKEN which created the PR | |
| GH_TOKEN="$GITHUB_TOKEN_FOR_APPROVAL" gh pr review "$PR_NUM" --repo "$REPO" --approve --body "Auto-approved by lockfile renewal workflow after 1 working day waiting period." || { | |
| echo "WARNING: Failed to add approval for PR #$PR_NUM. May need manual approval." | |
| continue | |
| } | |
| echo "Approval added successfully." | |
| else | |
| echo "PR #$PR_NUM already has an approving review." | |
| fi | |
| echo "MERGING: PR #$PR_NUM meets all criteria" | |
| gh pr merge "$PR_NUM" --repo "$REPO" --merge --admin || echo "WARNING: Failed to merge PR #$PR_NUM" | |
| done | |
| echo "" | |
| echo "Auto-merge check complete." |