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