/
/
/
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