Add frient A/S Smart Plug Mini, Smart Cable, Smart DIN Relay (SPLZB-131, SPLZB-132, SPLZB-134 & SMRZB-143 & SMRZB-332) v3.12.16 #263
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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." |