Skip to content

Add frient A/S Smart Button (SBTZB-110) v2.0.4 #249

Add frient A/S Smart Button (SBTZB-110) v2.0.4

Add frient A/S Smart Button (SBTZB-110) v2.0.4 #249

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."