music-assistant-server

8.5 KBYML
auto-merge-dependency-updates.yml
8.5 KB220 lines • yaml
1# Auto approve and merge dependency update PRs
2# for the frontend and models packages.
3
4name: Auto-merge dependency updates
5
6on:
7  pull_request_target:
8    types: [opened, synchronize, reopened]
9    branches:
10      - dev
11
12# CRITICAL SECURITY: This workflow uses pull_request_target which runs in the context
13# of the base repository and has access to secrets. Multiple security checks ensure
14# only trusted automation PRs are auto-merged.
15
16jobs:
17  auto-merge:
18    name: Auto-approve and merge
19    runs-on: ubuntu-latest
20    # Only run if branch name matches the expected pattern
21    if: |
22      startsWith(github.event.pull_request.head.ref, 'auto-update-frontend-') ||
23      startsWith(github.event.pull_request.head.ref, 'auto-update-models-')
24
25    permissions:
26      contents: write
27      pull-requests: write
28
29    steps:
30      # Security check 1: Verify PR is from user with write access
31      - name: Verify PR is from trusted source
32        id: verify_pr_author
33        run: |
34          PR_AUTHOR="${{ github.event.pull_request.user.login }}"
35
36          # Check if PR author has write access to the repository (includes org members and bots)
37          if gh api "/repos/${{ github.repository }}/collaborators/$PR_AUTHOR/permission" --jq '.permission' 2>/dev/null | grep -qE "^(admin|write|maintain)$"; then
38            echo "✅ PR is from user with write access: $PR_AUTHOR"
39          else
40            echo "❌ PR author does not have write access: $PR_AUTHOR"
41            exit 1
42          fi
43        env:
44          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
45
46      # Security check 2: Verify PR labels and source branch
47      - name: Verify PR labels and source
48        run: |
49          LABELS="${{ join(github.event.pull_request.labels.*.name, ',') }}"
50          BRANCH="$BRANCH"
51
52          if [[ "$LABELS" != *"dependencies"* ]]; then
53            echo "❌ PR does not have 'dependencies' label"
54            exit 1
55          fi
56
57          if [[ "$BRANCH" != auto-update-frontend-* && "$BRANCH" != auto-update-models-* ]]; then
58            echo "❌ Branch name does not match expected pattern: $BRANCH"
59            exit 1
60          fi
61
62          echo "✅ PR has 'dependencies' label and valid branch name"
63
64        env:
65          BRANCH: ${{ github.event.pull_request.head.ref }}
66      # IMPORTANT: Checkout the PR's head to validate file changes
67      # This is required for the git commands in security check 5
68      - name: Checkout PR branch
69        uses: actions/checkout@v6
70        with:
71          ref: ${{ github.event.pull_request.head.sha }}
72          fetch-depth: 2
73
74      # Security check 3: Get PR details for validation
75      - name: Get PR details
76        id: pr
77        run: |
78          PR_NUMBER="${{ github.event.pull_request.number }}"
79          echo "number=$PR_NUMBER" >> $GITHUB_OUTPUT
80
81          # Get commit author
82          COMMIT_AUTHOR=$(gh pr view "$PR_NUMBER" --json commits --jq '.commits[0].authors[0].login')
83          echo "commit_author=$COMMIT_AUTHOR" >> $GITHUB_OUTPUT
84
85          echo "PR #$PR_NUMBER with commits from $COMMIT_AUTHOR"
86        env:
87          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
88
89      # Security check 4: Verify commit author has write access
90      - name: Verify commit author
91        run: |
92          COMMIT_AUTHOR="${{ steps.pr.outputs.commit_author }}"
93
94          # Check if commit author has write access to the repository
95          if gh api "/repos/${{ github.repository }}/collaborators/$COMMIT_AUTHOR/permission" --jq '.permission' 2>/dev/null | grep -qE "^(admin|write|maintain)$"; then
96            echo "✅ Commit author has write access: $COMMIT_AUTHOR"
97          else
98            echo "❌ Commit author does not have write access: $COMMIT_AUTHOR"
99            exit 1
100          fi
101        env:
102          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
103
104      # Security check 5: Verify only dependency files were changed
105      - name: Verify only dependency files were changed
106        run: |
107          # Only pyproject.toml and requirements_all.txt should be modified
108          CHANGED_FILES=$(git diff --name-only HEAD~1 HEAD)
109
110          echo "Changed files:"
111          echo "$CHANGED_FILES"
112
113          for file in $CHANGED_FILES; do
114            if [[ "$file" != "pyproject.toml" ]] && [[ "$file" != "requirements_all.txt" ]]; then
115              echo "❌ Unexpected file changed: $file"
116              echo "Only pyproject.toml and requirements_all.txt should be modified"
117              exit 1
118            fi
119          done
120
121          echo "✅ Only expected dependency files were changed"
122
123      # Security check 6: Verify changes are only version bumps
124      - name: Verify changes are version bumps
125        run: |
126          # Check that only music-assistant-frontend or music-assistant-models version changed
127          DIFF=$(git diff HEAD~1 HEAD pyproject.toml requirements_all.txt)
128
129          if ! echo "$DIFF" | grep -qE "music-assistant-(frontend|models)=="; then
130            echo "❌ Changes do not appear to be version bumps"
131            exit 1
132          fi
133
134          echo "✅ Changes are version bumps"
135
136      # Security check 7: Wait for package to be available on PyPI
137      - name: Wait for package availability on PyPI
138        run: |
139          # Extract the package name and version from the changes
140          DIFF=$(git diff HEAD~1 HEAD pyproject.toml)
141
142          if echo "$DIFF" | grep -q "music-assistant-frontend=="; then
143            PACKAGE="music-assistant-frontend"
144            VERSION=$(echo "$DIFF" | grep -oP 'music-assistant-frontend==\K[0-9.]+' | head -1)
145          elif echo "$DIFF" | grep -q "music-assistant-models=="; then
146            PACKAGE="music-assistant-models"
147            VERSION=$(echo "$DIFF" | grep -oP 'music-assistant-models==\K[0-9.]+' | head -1)
148          else
149            echo "❌ Could not determine package name and version"
150            exit 1
151          fi
152
153          echo "Waiting for $PACKAGE version $VERSION to be available on PyPI..."
154
155          # Retry for up to 20 minutes (20 attempts with 60 second intervals)
156          MAX_ATTEMPTS=20
157          SLEEP_DURATION=60
158          ATTEMPT=1
159
160          while [ $ATTEMPT -le $MAX_ATTEMPTS ]; do
161            echo "Attempt $ATTEMPT/$MAX_ATTEMPTS: Checking if $PACKAGE==$VERSION is available..."
162
163            # Try to get package info from PyPI JSON API
164            HTTP_CODE=$(curl -s -o /tmp/pypi_response.json -w "%{http_code}" "https://pypi.org/pypi/$PACKAGE/json")
165
166            if [ "$HTTP_CODE" -eq 200 ]; then
167              # Check if the specific version exists
168              if grep -q "\"$VERSION\"" /tmp/pypi_response.json; then
169                echo "✅ Package $PACKAGE version $VERSION is available on PyPI"
170
171                # Additional verification: try to download the package
172                if python3 -m pip download --no-deps "$PACKAGE==$VERSION" > /dev/null 2>&1; then
173                  echo "✅ Package $PACKAGE==$VERSION can be installed"
174                  exit 0
175                else
176                  echo "⚠️  Package found in PyPI API but pip download failed, retrying..."
177                fi
178              else
179                echo "ℹ️  Package $PACKAGE exists but version $VERSION not yet available"
180              fi
181            else
182              echo "ℹ️  HTTP $HTTP_CODE when accessing PyPI API"
183            fi
184
185            if [ $ATTEMPT -lt $MAX_ATTEMPTS ]; then
186              echo "Waiting ${SLEEP_DURATION}s before retry..."
187              sleep $SLEEP_DURATION
188            fi
189
190            ATTEMPT=$((ATTEMPT + 1))
191          done
192
193          echo "❌ Package $PACKAGE version $VERSION did not become available within the timeout period"
194          echo "This might indicate:"
195          echo "  - The package was not published to PyPI"
196          echo "  - PyPI is experiencing delays"
197          echo "  - The version number in the PR is incorrect"
198          exit 1
199
200      # All security checks passed - approve the PR
201      - name: Auto-approve PR
202        run: |
203          gh pr review "${{ steps.pr.outputs.number }}" --approve --body "✅ Automated dependency update - all security checks passed"
204        env:
205          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
206
207      # Enable auto-merge with squash
208      - name: Enable auto-merge
209        run: |
210          gh pr merge "${{ steps.pr.outputs.number }}" --auto --squash
211        env:
212          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
213
214      - name: Comment on success
215        if: success()
216        run: |
217          gh pr comment "${{ steps.pr.outputs.number }}" --body "🤖 This PR has been automatically approved and will be merged once all checks pass."
218        env:
219          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
220