-
Notifications
You must be signed in to change notification settings - Fork 3
342 lines (301 loc) · 14.1 KB
/
process-ota-submission.yml
File metadata and controls
342 lines (301 loc) · 14.1 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
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
name: Process OTA submission
on:
issues:
types: [labeled]
concurrency:
group: ota-submission-${{ github.event.issue.number }}
cancel-in-progress: false
env:
PYTHON_VERSION_DEFAULT: "3.14"
ISSUE_FILE: /tmp/issue.md
PR_BODY_FILE: /tmp/pr_body.md
COMMIT_MSG_FILE: /tmp/commit_msg.txt
CHANGES_PATCH_FILE: /tmp/images-changes.patch
ZIGPY_JSON_FILE: /tmp/zigpy_ota.json
ISSUE_NUMBER: ${{ github.event.issue.number }}
BRANCH_NAME: bot/ota-submission-issue/${{ github.event.issue.number }}
jobs:
# Remove ota-processed label and add resubmit warning if this is a resubmission
resubmit-pr-prep:
name: Resubmit PR preparation
permissions: {}
if: |
contains(github.event.issue.labels.*.name, 'ota-submit') &&
contains(github.event.issue.labels.*.name, 'ota-create') &&
github.event.action == 'labeled' &&
github.event.label.name == 'ota-resubmit'
runs-on: ubuntu-slim
timeout-minutes: 2
steps:
- name: Add resubmit warning to PR
env:
GH_TOKEN: ${{ secrets.BOT_ACCESS_TOKEN }}
GH_REPO: ${{ github.repository }}
run: |
set -euo pipefail
# Find PR for this issue branch
PR_NUMBER=$(gh pr list --head "$BRANCH_NAME" --state open --json number --jq '.[0].number' 2>/dev/null || echo "")
if [ -n "$PR_NUMBER" ]; then
echo "Adding resubmit warning to PR #$PR_NUMBER..."
# Get current PR body and save to temp file
gh pr view "$PR_NUMBER" --json body --jq '.body' > /tmp/current_pr_body.md
# Check if warning already present
if ! grep -q "This PR is Being Recreated" /tmp/current_pr_body.md; then
# Prepend warning to PR body
cat > /tmp/new_pr_body.md << 'EOF_HEADER'
# ⚠️ This PR is Being Recreated - Please Wait ⚠️
**This PR is being updated due to a resubmission. The content below may be outdated until the update completes.**
---
EOF_HEADER
cat /tmp/current_pr_body.md >> /tmp/new_pr_body.md
gh pr edit "$PR_NUMBER" --body-file /tmp/new_pr_body.md
fi
else
echo "No open PR found for branch $BRANCH_NAME"
fi
- name: Remove ota-processed label
env:
GH_TOKEN: ${{ secrets.BOT_ACCESS_TOKEN }}
GH_REPO: ${{ github.repository }}
run: gh issue edit "$ISSUE_NUMBER" --remove-label "ota-processed" || true
# Prepare submission by parsing OTA, but skip if resubmit preflight failed for some reason
prepare-changes:
name: Prepare PR changes
needs: [resubmit-pr-prep]
# Requires ota-create label (added manually by maintainers or auto-added for trusted submitters)
if: ${{ !cancelled() &&
(needs.resubmit-pr-prep.result == 'success' || needs.resubmit-pr-prep.result == 'skipped') &&
contains(github.event.issue.labels.*.name, 'ota-submit') &&
contains(github.event.issue.labels.*.name, 'ota-create') &&
github.event.action == 'labeled' &&
(github.event.label.name == 'ota-create' || github.event.label.name == 'ota-resubmit') }}
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
outputs:
has_changes: ${{ steps.prepare_pr.outputs.has_changes }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Python ${{ env.PYTHON_VERSION_DEFAULT }}
uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION_DEFAULT }}
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Install package
run: uv sync --group ci
- name: Extract issue body to file
env:
ISSUE_BODY: ${{ github.event.issue.body }}
run: |
printf '%s\n' "$ISSUE_BODY" > "$ISSUE_FILE"
echo "Issue content saved to $ISSUE_FILE"
- name: Prepare PR changes
id: prepare_pr
env:
# Optionally provide a PAT for downloading user attachments from issues
GITHUB_TOKEN: ${{ secrets.BOT_DOWNLOAD_TOKEN }}
run: |
uv run zigpy-ota prepare-pr "$ISSUE_FILE" \
--output-pr-markdown "$PR_BODY_FILE" \
--output-commit-message "$COMMIT_MSG_FILE"
# Check if there are changes in the images directory (both tracked and untracked)
if [[ -z "$(git status --porcelain images/)" ]]; then
echo "No changes detected in images directory"
echo "has_changes=false" >> "$GITHUB_OUTPUT"
else
echo "Changes detected in images directory"
echo "has_changes=true" >> "$GITHUB_OUTPUT"
fi
- name: Create images patch
if: steps.prepare_pr.outputs.has_changes == 'true'
run: |
git add -A images/
git diff --cached --binary > "$CHANGES_PATCH_FILE"
if [ ! -s "$CHANGES_PATCH_FILE" ]; then
echo "::error::Generated patch file is empty"
exit 1
fi
- name: Upload prepared changes artifact
if: steps.prepare_pr.outputs.has_changes == 'true'
uses: actions/upload-artifact@v7
with:
name: ota-prepare-output
if-no-files-found: error
retention-days: 1
path: |
${{ env.CHANGES_PATCH_FILE }}
${{ env.PR_BODY_FILE }}
${{ env.COMMIT_MSG_FILE }}
# Generate the full zigpy JSON index as a workflow artifact for reviewers
# to inspect or test. Not consumed by other jobs.
- name: Generate zigpy JSON metadata artifact
if: steps.prepare_pr.outputs.has_changes == 'true'
run: |
uv run zigpy-ota generate-index \
--format zigpy \
--channel dev \
--tag "$BRANCH_NAME" \
--output-file "$ZIGPY_JSON_FILE"
- name: Upload zigpy JSON artifact
if: steps.prepare_pr.outputs.has_changes == 'true'
uses: actions/upload-artifact@v7
with:
if-no-files-found: error
archive: false
path: ${{ env.ZIGPY_JSON_FILE }}
# Runs when prepare-changes ran (success or failure) to create the PR or post
# a failure comment. Also runs if resubmit-pr-prep failed (even when
# prepare-changes was skipped) so the issue gets a failure comment.
create-or-update-pr:
name: Create or update PR
permissions: {}
needs: [resubmit-pr-prep, prepare-changes]
if: ${{ !cancelled() && (needs.prepare-changes.result != 'skipped' || needs.resubmit-pr-prep.result == 'failure') }}
runs-on: ubuntu-slim
timeout-minutes: 5
env:
PREP_OK_WITH_CHANGES: ${{ needs.prepare-changes.result == 'success' && needs.prepare-changes.outputs.has_changes == 'true' }}
steps:
- name: Checkout repository
if: ${{ env.PREP_OK_WITH_CHANGES == 'true' }}
uses: actions/checkout@v6
with:
fetch-depth: 0
token: ${{ secrets.BOT_ACCESS_TOKEN }}
- name: Download prepared changes artifact
if: ${{ env.PREP_OK_WITH_CHANGES == 'true' }}
uses: actions/download-artifact@v8
with:
name: ota-prepare-output
path: /tmp/ota-prepare
- name: Apply prepared images patch
if: ${{ env.PREP_OK_WITH_CHANGES == 'true' }}
run: |
set -euo pipefail
PATCH_FILE=/tmp/ota-prepare/images-changes.patch
# Pre-check: reject any patch that targets files outside images/
PATCH_PATHS=$(git apply --numstat "$PATCH_FILE" | awk '{print $NF}')
if [ -n "$PATCH_PATHS" ] && grep -qv '^images/' <<<"$PATCH_PATHS"; then
echo "::error::Patch contains changes outside images/"
exit 1
fi
git apply --index "$PATCH_FILE"
- name: Configure git
if: ${{ env.PREP_OK_WITH_CHANGES == 'true' }}
run: |
git config --global user.name "zigpy-bot"
git config --global user.email "247691930+zigpy-bot@users.noreply.github.com"
# Make sure the script is executable:
# git update-index --chmod=+x .github/scripts/create-pr.sh
- name: Create Pull Request
id: create_pr
if: ${{ env.PREP_OK_WITH_CHANGES == 'true' }}
env:
GH_TOKEN: ${{ secrets.BOT_ACCESS_TOKEN }}
GH_REPO: ${{ github.repository }}
BRANCH_NAME: ${{ env.BRANCH_NAME }}
ISSUE_NUMBER: ${{ env.ISSUE_NUMBER }}
ISSUE_TITLE: ${{ github.event.issue.title }}
ISSUE_AUTHOR_LOGIN: ${{ github.event.issue.user.login }}
ISSUE_AUTHOR_ID: ${{ github.event.issue.user.id }}
PR_BODY_FILE: /tmp/ota-prepare/pr_body.md
COMMIT_MSG_FILE: /tmp/ota-prepare/commit_msg.txt
run: .github/scripts/create-pr.sh
- name: Remove ota-resubmit and ota-edited labels
if: always() && github.event.label.name == 'ota-resubmit'
env:
GH_TOKEN: ${{ secrets.BOT_ACCESS_TOKEN }}
GH_REPO: ${{ github.repository }}
run: |
gh issue edit "$ISSUE_NUMBER" --remove-label "ota-resubmit" || true
gh issue edit "$ISSUE_NUMBER" --remove-label "ota-edited" || true
- name: Comment on issue (success or closed PR)
if: ${{ env.PREP_OK_WITH_CHANGES == 'true' && steps.create_pr.outcome == 'success' }}
env:
GH_TOKEN: ${{ secrets.BOT_ACCESS_TOKEN }}
REPO_OWNER: ${{ github.repository_owner }}
REPO_NAME: ${{ github.event.repository.name }}
run: |
set -euo pipefail
# Check if PR is open before posting success message
PR_INFO=$(gh pr view "$BRANCH_NAME" --json state,url 2>/dev/null || echo "{}")
PR_STATE=$(echo "$PR_INFO" | jq -r '.state // "UNKNOWN"')
PR_URL=$(echo "$PR_INFO" | jq -r '.url // ""')
if [ "$PR_STATE" = "OPEN" ]; then
BOT_LOGIN="zigpy-bot"
# Fetch all non-minimized bot comments
BOT_COMMENTS=$(gh api graphql -f query='
query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
issue(number: $number) {
comments(first: 100) {
nodes {
id
body
author { login }
isMinimized
}
}
}
}
}
' -f owner="$REPO_OWNER" -f repo="$REPO_NAME" -F number="$ISSUE_NUMBER" \
--jq "[.data.repository.issue.comments.nodes[] | select(.author.login == \"$BOT_LOGIN\" and .isMinimized == false)]")
# Check if there's already a success comment for this PR
HAS_SUCCESS_COMMENT=$(echo "$BOT_COMMENTS" | jq -r --arg url "$PR_URL" \
'[.[] | select(.body | contains("Successfully processed your OTA submission") and contains($url))] | length > 0')
if [ "$HAS_SUCCESS_COMMENT" = "true" ]; then
# Update existing success comment with reprocessing timestamp
SUCCESS_COMMENT_ID=$(echo "$BOT_COMMENTS" | jq -r --arg url "$PR_URL" \
'[.[] | select(.body | contains("Successfully processed your OTA submission") and contains($url))] | last | .id')
SUCCESS_COMMENT_BODY=$(echo "$BOT_COMMENTS" | jq -r --arg url "$PR_URL" \
'[.[] | select(.body | contains("Successfully processed your OTA submission") and contains($url))] | last | .body')
TIMESTAMP=$(date -u '+%Y-%m-%d %H:%M UTC')
# Remove old timestamp line if present, strip trailing blank lines, append new timestamp
printf '%s\n' "$SUCCESS_COMMENT_BODY" > /tmp/success_comment.txt
sed -i '/^\*Last reprocessed:/d' /tmp/success_comment.txt
sed -i -e :a -e '/^\s*$/{$d;N;ba' -e '}' /tmp/success_comment.txt
printf '\n\n*Last reprocessed: %s*' "$TIMESTAMP" >> /tmp/success_comment.txt
gh api graphql -f query='
mutation($id: ID!, $body: String!) {
updateIssueComment(input: {id: $id, body: $body}) {
issueComment { body }
}
}
' -f id="$SUCCESS_COMMENT_ID" -f body="$(cat /tmp/success_comment.txt)" || true
echo "Updated existing success comment with reprocessing timestamp"
else
# Hide previous bot comments before posting new success message
COMMENT_IDS=$(echo "$BOT_COMMENTS" | jq -r '.[].id')
for COMMENT_ID in $COMMENT_IDS; do
gh api graphql -f query='
mutation($id: ID!) {
minimizeComment(input: {subjectId: $id, classifier: OUTDATED}) {
minimizedComment { isMinimized }
}
}
' -f id="$COMMENT_ID" || true
done
gh issue comment "$ISSUE_NUMBER" --body "✅ Successfully processed your OTA submission! A pull request has been created with your changes:
- $PR_URL"
fi
else
gh issue comment "$ISSUE_NUMBER" --body "❌ Failed to process your OTA submission. The PR was closed unexpectedly. Please check the workflow logs for more details."
fi
- name: Comment on issue (no changes)
if: needs.prepare-changes.result == 'success' && needs.prepare-changes.outputs.has_changes == 'false'
env:
GH_TOKEN: ${{ secrets.BOT_ACCESS_TOKEN }}
GH_REPO: ${{ github.repository }}
run: gh issue comment "$ISSUE_NUMBER" --body "⚠️ No changes were detected in the images directory. Please check your submission and ensure the OTA file URL is valid."
- name: Comment on issue (failure)
if: ${{ needs.prepare-changes.result != 'success' || (env.PREP_OK_WITH_CHANGES == 'true' && steps.create_pr.outcome == 'failure') }}
env:
GH_TOKEN: ${{ secrets.BOT_ACCESS_TOKEN }}
GH_REPO: ${{ github.repository }}
run: gh issue comment "$ISSUE_NUMBER" --body "❌ Failed to process your OTA submission. A maintainer will check on this shortly and let you know if any changes are needed. You can check the workflow logs for more details."