From 4e9aa0b2e3384326033aff5a27e469ea4bac0a39 Mon Sep 17 00:00:00 2001 From: raman325 <7243222+raman325@users.noreply.github.com> Date: Thu, 15 Jan 2026 01:12:10 -0500 Subject: [PATCH 01/17] Replace release-drafter autolabeler with dedicated labeling actions Use actions/labeler for file-based labels and github/issue-labeler for body-based labels. Both support sync-labels to add AND remove labels when patterns match or no longer match. - Added .github/labeler.yml for file-based label patterns - Added .github/issue-labeler.yml for body-based (checkbox) patterns - Added .github/workflows/labeler.yaml to run both labelers - Removed autolabeler section from release-drafter.yml - Removed related TODO item Co-Authored-By: Claude Opus 4.5 --- .github/issue-labeler.yml | 18 ++++++++++ .github/labeler.yml | 54 ++++++++++++++++++++++++++++++ .github/release-drafter.yml | 61 +++------------------------------- .github/workflows/labeler.yaml | 34 +++++++++++++++++++ TODO.md | 2 -- 5 files changed, 110 insertions(+), 59 deletions(-) create mode 100644 .github/issue-labeler.yml create mode 100644 .github/labeler.yml create mode 100644 .github/workflows/labeler.yaml diff --git a/.github/issue-labeler.yml b/.github/issue-labeler.yml new file mode 100644 index 000000000..64b011e6d --- /dev/null +++ b/.github/issue-labeler.yml @@ -0,0 +1,18 @@ +# Configuration for github/issue-labeler - body-based labels +# See: https://github.com/github/issue-labeler +# Uses regex patterns to match PR/issue body content +--- +dependencies: + - '\[x\]\s*[Dd]ependency' + +bug: + - '\[x\]\s*[Bb]ugfix' + +enhancement: + - '\[x\]\s*[Nn]ew [Ff]eature' + +breaking-change: + - '\[x\]\s*[Bb]reaking [Cc]hange' + +code-quality: + - '\[x\]\s*[Cc]ode [Qq]uality' diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 000000000..31fb0d4e6 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,54 @@ +# Configuration for actions/labeler - file-based labels +# See: https://github.com/actions/labeler +--- +pre-commit: + - changed-files: + - any-glob-to-any-file: '.pre-commit-config.yaml' + +javascript: + - changed-files: + - any-glob-to-any-file: + - '*.ts' + - '*.js' + - 'ts/**/*' + - '.eslintrc.cjs' + - 'tsconfig.json' + - 'rollup.config.js' + - 'package.json' + - 'package-lock.json' + - 'yarn.lock' + +python: + - changed-files: + - any-glob-to-any-file: + - '*.py' + - 'custom_components/**/*.py' + - 'tests/**/*.py' + - 'tox.ini' + - 'requirements*.txt' + - 'pyproject.toml' + - 'Pipfile' + - 'Pipfile.lock' + +github_actions: + - changed-files: + - any-glob-to-any-file: '.github/workflows/**/*' + +dependencies: + - changed-files: + - any-glob-to-any-file: + - 'requirements*.txt' + - 'pyproject.toml' + - 'Pipfile' + - 'Pipfile.lock' + - 'package.json' + - 'package-lock.json' + - 'yarn.lock' + - '.pre-commit-config.yaml' + +documentation: + - changed-files: + - any-glob-to-any-file: + - '*.md' + - '**/*.md' + - 'docs/**/*' diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index f7bf2747e..188771368 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -3,63 +3,10 @@ name-template: "$RESOLVED_VERSION" tag-template: "$RESOLVED_VERSION" change-template: "- $TITLE @$AUTHOR (#$NUMBER)" sort-direction: ascending -autolabeler: - - label: 'pre-commit' - files: - - '.pre-commit-config.yaml' - - label: 'javascript' - files: - - '*.ts' - - '*.js' - - 'ts/**/*' - - '.eslintrc.cjs' - - 'tsconfig.json' - - 'rollup.config.js' - - 'package.json' - - 'package-lock.json' - - 'yarn.lock' - - label: 'python' - files: - - '*.py' - - 'custom_components/**/*.py' - - 'tests/**/*.py' - - 'tox.ini' - - 'requirements*.txt' - - 'pyproject.toml' - - 'Pipfile' - - 'Pipfile.lock' - - label: 'github_actions' - files: - - '.github/workflows/**/*' - - label: 'dependencies' - files: - - 'requirements*.txt' - - 'pyproject.toml' - - 'Pipfile' - - 'Pipfile.lock' - - 'package.json' - - 'package-lock.json' - - 'yarn.lock' - - '.pre-commit-config.yaml' - body: - - '/\[x\] dependency/i' - - label: 'documentation' - files: - - '*.md' - - '**/*.md' - - 'docs/**/*' - - label: 'bug' - body: - - '/\[x\] bugfix/i' - - label: 'enhancement' - body: - - '/\[x\] new feature/i' - - label: 'breaking-change' - body: - - '/\[x\] breaking change/i' - - label: 'code-quality' - body: - - '/\[x\] code quality/i' + +# Note: Autolabeling is handled by .github/workflows/labeler.yaml +# using actions/labeler (file-based) and github/issue-labeler (body-based) +# These support sync-labels to remove labels when patterns no longer match. version-resolver: major: diff --git a/.github/workflows/labeler.yaml b/.github/workflows/labeler.yaml new file mode 100644 index 000000000..8ff9d2d5e --- /dev/null +++ b/.github/workflows/labeler.yaml @@ -0,0 +1,34 @@ +--- +name: Labeler + +# yamllint disable-line rule:truthy +on: + pull_request_target: + types: + - edited + - opened + - reopened + - synchronize + +permissions: + contents: read + pull-requests: write + +jobs: + label-files: + name: Label by Files + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@v5 + with: + sync-labels: true + + label-body: + name: Label by Body + runs-on: ubuntu-latest + steps: + - uses: github/issue-labeler@v3 + with: + configuration-path: .github/issue-labeler.yml + enable-versioned-regex: 0 + sync-labels: 1 diff --git a/TODO.md b/TODO.md index 1ec473664..64fb88fbe 100644 --- a/TODO.md +++ b/TODO.md @@ -4,8 +4,6 @@ - Unify design across slot and lock data cards, with a preference towards the slot card design. - Test visual editor for both cards. -- Explore alternative autolabeler workflows that can remove labels when patterns no longer - match (release-drafter only adds labels, never removes them). ## Testing From 51096111d25d40cf6484d64440295f0ab0fa75d8 Mon Sep 17 00:00:00 2001 From: raman325 <7243222+raman325@users.noreply.github.com> Date: Thu, 15 Jan 2026 01:21:46 -0500 Subject: [PATCH 02/17] Add CI gate workflow to wait for all checks Self-contained workflow that polls for all other checks to complete. Use "All Checks Pass" as the single required status check in branch protection - solves the issue where path-filtered workflows don't always run. No external action dependencies - uses inline gh API calls. Co-Authored-By: Claude Opus 4.5 --- .github/workflows/ci-gate.yaml | 74 ++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 .github/workflows/ci-gate.yaml diff --git a/.github/workflows/ci-gate.yaml b/.github/workflows/ci-gate.yaml new file mode 100644 index 000000000..6e2ae7a4c --- /dev/null +++ b/.github/workflows/ci-gate.yaml @@ -0,0 +1,74 @@ +--- +name: CI Gate + +# This workflow waits for all other checks to complete. +# Use "All Checks Pass" as the single required status check in branch protection. +# This solves the issue where workflows with path filters don't always run. + +# yamllint disable-line rule:truthy +on: + pull_request: + push: + branches: [main] + +permissions: + checks: read + pull-requests: read + +jobs: + gate: + name: All Checks Pass + runs-on: ubuntu-latest + steps: + - name: Wait for all checks to complete + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + SHA: ${{ github.event.pull_request.head.sha || github.sha }} + CURRENT_JOB: All Checks Pass + run: | + echo "Waiting for all checks on $SHA to complete..." + + while true; do + # Get all check runs for this commit + CHECKS=$(gh api "repos/$REPO/commits/$SHA/check-runs" \ + --jq '.check_runs | map({name: .name, status: .status, conclusion: .conclusion})') + + # Count checks still in progress (excluding this job) + IN_PROGRESS=$(echo "$CHECKS" | jq --arg current "$CURRENT_JOB" \ + '[.[] | select(.name != $current and .status != "completed")] | length') + + # Check for any failures (excluding this job) + # Conclusion must be success, skipped, or neutral to pass + FAILED=$(echo "$CHECKS" | jq --arg current "$CURRENT_JOB" ' + [.[] | select( + .name != $current and + .status == "completed" and + .conclusion != "success" and + .conclusion != "skipped" and + .conclusion != "neutral" + )] | length') + + echo "In progress: $IN_PROGRESS, Failed: $FAILED" + + if [ "$FAILED" -gt 0 ]; then + echo "::error::One or more checks failed" + echo "$CHECKS" | jq --arg current "$CURRENT_JOB" ' + .[] | select( + .name != $current and + .status == "completed" and + .conclusion != "success" and + .conclusion != "skipped" and + .conclusion != "neutral" + )' + exit 1 + fi + + if [ "$IN_PROGRESS" -eq 0 ]; then + echo "All checks completed successfully!" + break + fi + + echo "Waiting 30 seconds..." + sleep 30 + done From c097ba8789013ef6d09e854cb805b27bb49d661d Mon Sep 17 00:00:00 2001 From: raman325 <7243222+raman325@users.noreply.github.com> Date: Thu, 15 Jan 2026 01:33:34 -0500 Subject: [PATCH 03/17] Add initial delay to CI gate to let other workflows start Fixes race condition where CI gate could complete before slower workflows even start. Co-Authored-By: Claude Opus 4.5 --- .github/workflows/ci-gate.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci-gate.yaml b/.github/workflows/ci-gate.yaml index 6e2ae7a4c..ec7cdd6d4 100644 --- a/.github/workflows/ci-gate.yaml +++ b/.github/workflows/ci-gate.yaml @@ -29,6 +29,10 @@ jobs: run: | echo "Waiting for all checks on $SHA to complete..." + # Initial delay to let other workflows start + echo "Waiting 60 seconds for other workflows to start..." + sleep 60 + while true; do # Get all check runs for this commit CHECKS=$(gh api "repos/$REPO/commits/$SHA/check-runs" \ From 0533002fe02d4f5093090683d57334cd494bd598 Mon Sep 17 00:00:00 2001 From: raman325 <7243222+raman325@users.noreply.github.com> Date: Thu, 15 Jan 2026 01:37:12 -0500 Subject: [PATCH 04/17] Move path filtering from trigger to job-level conditions Workflows now always trigger but skip jobs internally when paths don't match. This ensures a consistent number of check runs regardless of which files changed. Uses dorny/paths-filter to check changed files, then jobs use if: conditions to skip when no relevant changes. Also removed initial delay from CI gate since workflows now start immediately. Co-Authored-By: Claude Opus 4.5 --- .github/workflows/ci-gate.yaml | 4 --- .github/workflows/frontend.yaml | 46 +++++++++++++++++------------- .github/workflows/integration.yaml | 31 ++++++++++++++------ 3 files changed, 49 insertions(+), 32 deletions(-) diff --git a/.github/workflows/ci-gate.yaml b/.github/workflows/ci-gate.yaml index ec7cdd6d4..6e2ae7a4c 100644 --- a/.github/workflows/ci-gate.yaml +++ b/.github/workflows/ci-gate.yaml @@ -29,10 +29,6 @@ jobs: run: | echo "Waiting for all checks on $SHA to complete..." - # Initial delay to let other workflows start - echo "Waiting 60 seconds for other workflows to start..." - sleep 60 - while true; do # Get all check runs for this commit CHECKS=$(gh api "repos/$REPO/commits/$SHA/check-runs" \ diff --git a/.github/workflows/frontend.yaml b/.github/workflows/frontend.yaml index 7541dd485..0a0cc738e 100644 --- a/.github/workflows/frontend.yaml +++ b/.github/workflows/frontend.yaml @@ -5,33 +5,37 @@ name: Frontend on: push: branches: [main] - paths: - - "**.js" - - "**.ts" - - "ts/**" - - ".eslintrc.cjs" - - "package.json" - - "rollup.config.js" - - "tsconfig.json" - - "vitest.config.ts" - - ".github/workflows/frontend.yaml" pull_request: branches: [main] - paths: - - "**.js" - - "**.ts" - - "ts/**" - - ".eslintrc.cjs" - - "package.json" - - "rollup.config.js" - - "tsconfig.json" - - "vitest.config.ts" - - ".github/workflows/frontend.yaml" jobs: + check-changes: + name: Check Changes + runs-on: ubuntu-latest + outputs: + frontend: ${{ steps.filter.outputs.frontend }} + steps: + - uses: actions/checkout@v6 + - uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + frontend: + - '**.js' + - '**.ts' + - 'ts/**' + - '.eslintrc.cjs' + - 'package.json' + - 'rollup.config.js' + - 'tsconfig.json' + - 'vitest.config.ts' + - '.github/workflows/frontend.yaml' + lint-and-build: # pre-commit.ci skips yarn-lint and yarn-build (Node.js not available), so CI needs this job name: Yarn Lint and Build + needs: check-changes + if: needs.check-changes.outputs.frontend == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 @@ -68,6 +72,8 @@ jobs: test: name: Vitest + needs: check-changes + if: needs.check-changes.outputs.frontend == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml index c0ac0e23c..7348e72ee 100644 --- a/.github/workflows/integration.yaml +++ b/.github/workflows/integration.yaml @@ -5,20 +5,31 @@ name: Integration on: push: branches: [main] - paths: - - "**.py" - - ".github/workflows/integration.yaml" pull_request: branches: [main] - paths: - - "**.py" - - "requirements*.txt" - - "pyproject.toml" - - ".github/workflows/integration.yaml" jobs: + check-changes: + name: Check Changes + runs-on: ubuntu-latest + outputs: + python: ${{ steps.filter.outputs.python }} + steps: + - uses: actions/checkout@v6 + - uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + python: + - '**.py' + - 'requirements*.txt' + - 'pyproject.toml' + - '.github/workflows/integration.yaml' + test: name: Pytest + needs: check-changes + if: needs.check-changes.outputs.python == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 @@ -50,6 +61,8 @@ jobs: hacs: name: HACS + needs: check-changes + if: needs.check-changes.outputs.python == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 @@ -60,6 +73,8 @@ jobs: hassfest: name: Hassfest + needs: check-changes + if: needs.check-changes.outputs.python == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 From 95c29dd3847aff8b477c44fd4dec42e9e4d20120 Mon Sep 17 00:00:00 2001 From: raman325 <7243222+raman325@users.noreply.github.com> Date: Thu, 15 Jan 2026 01:39:48 -0500 Subject: [PATCH 05/17] Address Copilot review comments on labeler config - Remove 'dependencies' from body-based labeler to avoid conflict with file-based labeler (both had sync-labels which could fight) - Match both [x] and [X] in checkbox patterns for manual edits Co-Authored-By: Claude Opus 4.5 --- .github/issue-labeler.yml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/issue-labeler.yml b/.github/issue-labeler.yml index 64b011e6d..5a685d6d4 100644 --- a/.github/issue-labeler.yml +++ b/.github/issue-labeler.yml @@ -1,18 +1,16 @@ # Configuration for github/issue-labeler - body-based labels # See: https://github.com/github/issue-labeler # Uses regex patterns to match PR/issue body content +# Note: [xX] matches both lowercase and uppercase X in checkboxes --- -dependencies: - - '\[x\]\s*[Dd]ependency' - bug: - - '\[x\]\s*[Bb]ugfix' + - '\[[xX]\]\s*[Bb]ugfix' enhancement: - - '\[x\]\s*[Nn]ew [Ff]eature' + - '\[[xX]\]\s*[Nn]ew [Ff]eature' breaking-change: - - '\[x\]\s*[Bb]reaking [Cc]hange' + - '\[[xX]\]\s*[Bb]reaking [Cc]hange' code-quality: - - '\[x\]\s*[Cc]ode [Qq]uality' + - '\[[xX]\]\s*[Cc]ode [Qq]uality' From 71d73832a1a344f63ea6a92cc3f846938fa44d67 Mon Sep 17 00:00:00 2001 From: raman325 <7243222+raman325@users.noreply.github.com> Date: Thu, 15 Jan 2026 01:42:35 -0500 Subject: [PATCH 06/17] Add settle period to CI gate for late-arriving checks After all checks appear complete, wait 60 seconds and re-verify to catch late-arriving checks like Codecov that report asynchronously. Co-Authored-By: Claude Opus 4.5 --- .github/workflows/ci-gate.yaml | 40 ++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-gate.yaml b/.github/workflows/ci-gate.yaml index 6e2ae7a4c..58644325d 100644 --- a/.github/workflows/ci-gate.yaml +++ b/.github/workflows/ci-gate.yaml @@ -65,8 +65,44 @@ jobs: fi if [ "$IN_PROGRESS" -eq 0 ]; then - echo "All checks completed successfully!" - break + echo "All checks appear complete. Waiting 60s for late arrivals (e.g., Codecov)..." + sleep 60 + + # Re-check after settle period + CHECKS=$(gh api "repos/$REPO/commits/$SHA/check-runs" \ + --jq '.check_runs | map({name: .name, status: .status, conclusion: .conclusion})') + + IN_PROGRESS=$(echo "$CHECKS" | jq --arg current "$CURRENT_JOB" \ + '[.[] | select(.name != $current and .status != "completed")] | length') + + FAILED=$(echo "$CHECKS" | jq --arg current "$CURRENT_JOB" ' + [.[] | select( + .name != $current and + .status == "completed" and + .conclusion != "success" and + .conclusion != "skipped" and + .conclusion != "neutral" + )] | length') + + if [ "$FAILED" -gt 0 ]; then + echo "::error::One or more checks failed after settle period" + echo "$CHECKS" | jq --arg current "$CURRENT_JOB" ' + .[] | select( + .name != $current and + .status == "completed" and + .conclusion != "success" and + .conclusion != "skipped" and + .conclusion != "neutral" + )' + exit 1 + fi + + if [ "$IN_PROGRESS" -eq 0 ]; then + echo "All checks completed successfully!" + break + fi + + echo "New checks appeared, continuing to wait..." fi echo "Waiting 30 seconds..." From c23d5c2413de579ae031e25b679780b7ae6935bd Mon Sep 17 00:00:00 2001 From: raman325 <7243222+raman325@users.noreply.github.com> Date: Thu, 15 Jan 2026 01:46:02 -0500 Subject: [PATCH 07/17] Replace CI gate with explicit check polling in auto-merge Instead of using branch protection required checks + CI gate workflow: - Auto-merge now polls for specific checks directly - "Check Changes" must exist and pass (always runs) - Other checks (Pytest, HACS, etc.) must pass IF they run - Deleted CI gate workflow and branch protection ruleset This simplifies the CI setup and avoids race conditions with late-arriving checks like Codecov. Co-Authored-By: Claude Opus 4.5 --- .github/workflows/ci-gate.yaml | 110 ------------------------------ .github/workflows/repository.yaml | 75 +++++++++++++++++++- 2 files changed, 72 insertions(+), 113 deletions(-) delete mode 100644 .github/workflows/ci-gate.yaml diff --git a/.github/workflows/ci-gate.yaml b/.github/workflows/ci-gate.yaml deleted file mode 100644 index 58644325d..000000000 --- a/.github/workflows/ci-gate.yaml +++ /dev/null @@ -1,110 +0,0 @@ ---- -name: CI Gate - -# This workflow waits for all other checks to complete. -# Use "All Checks Pass" as the single required status check in branch protection. -# This solves the issue where workflows with path filters don't always run. - -# yamllint disable-line rule:truthy -on: - pull_request: - push: - branches: [main] - -permissions: - checks: read - pull-requests: read - -jobs: - gate: - name: All Checks Pass - runs-on: ubuntu-latest - steps: - - name: Wait for all checks to complete - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - REPO: ${{ github.repository }} - SHA: ${{ github.event.pull_request.head.sha || github.sha }} - CURRENT_JOB: All Checks Pass - run: | - echo "Waiting for all checks on $SHA to complete..." - - while true; do - # Get all check runs for this commit - CHECKS=$(gh api "repos/$REPO/commits/$SHA/check-runs" \ - --jq '.check_runs | map({name: .name, status: .status, conclusion: .conclusion})') - - # Count checks still in progress (excluding this job) - IN_PROGRESS=$(echo "$CHECKS" | jq --arg current "$CURRENT_JOB" \ - '[.[] | select(.name != $current and .status != "completed")] | length') - - # Check for any failures (excluding this job) - # Conclusion must be success, skipped, or neutral to pass - FAILED=$(echo "$CHECKS" | jq --arg current "$CURRENT_JOB" ' - [.[] | select( - .name != $current and - .status == "completed" and - .conclusion != "success" and - .conclusion != "skipped" and - .conclusion != "neutral" - )] | length') - - echo "In progress: $IN_PROGRESS, Failed: $FAILED" - - if [ "$FAILED" -gt 0 ]; then - echo "::error::One or more checks failed" - echo "$CHECKS" | jq --arg current "$CURRENT_JOB" ' - .[] | select( - .name != $current and - .status == "completed" and - .conclusion != "success" and - .conclusion != "skipped" and - .conclusion != "neutral" - )' - exit 1 - fi - - if [ "$IN_PROGRESS" -eq 0 ]; then - echo "All checks appear complete. Waiting 60s for late arrivals (e.g., Codecov)..." - sleep 60 - - # Re-check after settle period - CHECKS=$(gh api "repos/$REPO/commits/$SHA/check-runs" \ - --jq '.check_runs | map({name: .name, status: .status, conclusion: .conclusion})') - - IN_PROGRESS=$(echo "$CHECKS" | jq --arg current "$CURRENT_JOB" \ - '[.[] | select(.name != $current and .status != "completed")] | length') - - FAILED=$(echo "$CHECKS" | jq --arg current "$CURRENT_JOB" ' - [.[] | select( - .name != $current and - .status == "completed" and - .conclusion != "success" and - .conclusion != "skipped" and - .conclusion != "neutral" - )] | length') - - if [ "$FAILED" -gt 0 ]; then - echo "::error::One or more checks failed after settle period" - echo "$CHECKS" | jq --arg current "$CURRENT_JOB" ' - .[] | select( - .name != $current and - .status == "completed" and - .conclusion != "success" and - .conclusion != "skipped" and - .conclusion != "neutral" - )' - exit 1 - fi - - if [ "$IN_PROGRESS" -eq 0 ]; then - echo "All checks completed successfully!" - break - fi - - echo "New checks appeared, continuing to wait..." - fi - - echo "Waiting 30 seconds..." - sleep 30 - done diff --git a/.github/workflows/repository.yaml b/.github/workflows/repository.yaml index b07a2d0f2..411f40f55 100644 --- a/.github/workflows/repository.yaml +++ b/.github/workflows/repository.yaml @@ -99,14 +99,83 @@ jobs: ACTOR: ${{ github.event.pull_request.user.login }} UPDATE_TYPE: ${{ steps.metadata.outputs.update-type }} - # Wait for required CI checks (--required avoids deadlock with this workflow) + # Wait for specific CI checks (explicit list, not branch protection) - name: Wait for CI checks if: steps.eligible.outputs.result == 'true' timeout-minutes: 30 - run: gh pr checks "$PR_URL" --watch --fail-fast --required env: - PR_URL: ${{ github.event.pull_request.html_url }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + SHA: ${{ github.event.pull_request.head.sha }} + # Checks that must exist and pass (always run) + MUST_PASS: Check Changes + # Checks that must pass IF they run (may be skipped by path filter) + IF_RUN_PASS: Pytest,HACS,Hassfest,Vitest,Yarn Lint and Build + run: | + IFS=',' read -ra MUST <<< "$MUST_PASS" + IFS=',' read -ra OPTIONAL <<< "$IF_RUN_PASS" + ALL_CHECKS=("${MUST[@]}" "${OPTIONAL[@]}") + echo "Must pass: ${MUST[*]}" + echo "If run, must pass: ${OPTIONAL[*]}" + + while true; do + ALL_DONE=true + ANY_FAILED=false + MUST_FOUND=0 + + for CHECK in "${ALL_CHECKS[@]}"; do + # Check may appear multiple times (e.g., "Check Changes" in multiple workflows) + STATUSES=$(gh api "repos/$REPO/commits/$SHA/check-runs" \ + --jq "[.check_runs[] | select(.name == \"$CHECK\")] | map({s: .status, c: .conclusion})" \ + 2>/dev/null) + + COUNT=$(echo "$STATUSES" | jq 'length') + + if [ "$COUNT" -eq 0 ]; then + echo "$CHECK: not found" + continue + fi + + # Track if this is a must-pass check + for M in "${MUST[@]}"; do + if [ "$CHECK" = "$M" ]; then + MUST_FOUND=$((MUST_FOUND + COUNT)) + break + fi + done + + # Check each instance + for i in $(seq 0 $((COUNT - 1))); do + STATUS=$(echo "$STATUSES" | jq -r ".[$i].s") + CONCLUSION=$(echo "$STATUSES" | jq -r ".[$i].c") + + if [ "$STATUS" != "completed" ]; then + echo "$CHECK [$i]: $STATUS" + ALL_DONE=false + elif [ "$CONCLUSION" != "success" ] && [ "$CONCLUSION" != "skipped" ]; then + echo "::error::$CHECK [$i] failed ($CONCLUSION)" + ANY_FAILED=true + else + echo "$CHECK [$i]: $CONCLUSION" + fi + done + done + + if [ "$ANY_FAILED" = true ]; then + exit 1 + fi + + if [ "$ALL_DONE" = true ]; then + if [ "$MUST_FOUND" -eq 0 ]; then + echo "::error::No required checks found - something is wrong" + exit 1 + fi + echo "All checks passed! (found $MUST_FOUND required check instances)" + break + fi + + sleep 30 + done # Merge after CI passes - name: Merge PR From 52c6d19d4a6524e3ce6d63a87a94a0425aaf567b Mon Sep 17 00:00:00 2001 From: raman325 <7243222+raman325@users.noreply.github.com> Date: Thu, 15 Jan 2026 01:55:05 -0500 Subject: [PATCH 08/17] Add initial delay to auto-merge check polling Give workflows 15 seconds to register their check runs before polling starts. This prevents the edge case where auto-merge starts polling before other workflows have registered, which would cause a "No required checks found" error. Co-Authored-By: Claude Opus 4.5 --- .github/workflows/repository.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/repository.yaml b/.github/workflows/repository.yaml index 411f40f55..57ef8ce36 100644 --- a/.github/workflows/repository.yaml +++ b/.github/workflows/repository.yaml @@ -112,6 +112,11 @@ jobs: # Checks that must pass IF they run (may be skipped by path filter) IF_RUN_PASS: Pytest,HACS,Hassfest,Vitest,Yarn Lint and Build run: | + # Give workflows time to register their check runs + # Longer delay is fine since auto-merge only runs for automated/low-risk PRs + echo "Waiting for workflows to register..." + sleep 60 + IFS=',' read -ra MUST <<< "$MUST_PASS" IFS=',' read -ra OPTIONAL <<< "$IF_RUN_PASS" ALL_CHECKS=("${MUST[@]}" "${OPTIONAL[@]}") From 95c567c195ebb07ffc54e6ae957fc6d3b0010e66 Mon Sep 17 00:00:00 2001 From: raman325 <7243222+raman325@users.noreply.github.com> Date: Thu, 15 Jan 2026 01:59:31 -0500 Subject: [PATCH 09/17] Combine frontend and integration workflows Merge frontend.yaml into integration.yaml with a single check-changes job that outputs both python and frontend filters. Jobs conditionally run based on their respective filter outputs. Benefits: - Single paths-filter execution instead of two - Unified workflow view in GitHub Actions - Simpler auto-merge check list (one Check Changes job) Co-Authored-By: Claude Opus 4.5 --- .github/workflows/frontend.yaml | 93 ------------------------------ .github/workflows/integration.yaml | 74 ++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 93 deletions(-) delete mode 100644 .github/workflows/frontend.yaml diff --git a/.github/workflows/frontend.yaml b/.github/workflows/frontend.yaml deleted file mode 100644 index 0a0cc738e..000000000 --- a/.github/workflows/frontend.yaml +++ /dev/null @@ -1,93 +0,0 @@ ---- -name: Frontend - -# yamllint disable-line rule:truthy -on: - push: - branches: [main] - pull_request: - branches: [main] - -jobs: - check-changes: - name: Check Changes - runs-on: ubuntu-latest - outputs: - frontend: ${{ steps.filter.outputs.frontend }} - steps: - - uses: actions/checkout@v6 - - uses: dorny/paths-filter@v3 - id: filter - with: - filters: | - frontend: - - '**.js' - - '**.ts' - - 'ts/**' - - '.eslintrc.cjs' - - 'package.json' - - 'rollup.config.js' - - 'tsconfig.json' - - 'vitest.config.ts' - - '.github/workflows/frontend.yaml' - - lint-and-build: - # pre-commit.ci skips yarn-lint and yarn-build (Node.js not available), so CI needs this job - name: Yarn Lint and Build - needs: check-changes - if: needs.check-changes.outputs.frontend == 'true' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 2 - - name: Set up Node - uses: actions/setup-node@v6 - with: - node-version: lts/iron - - name: Install dependencies - run: yarn install - - name: Lint - run: yarn lint:fix - - name: Check for changes from lint - id: change_lint - run: echo "changed=$(git status --porcelain | wc -l)" >> $GITHUB_OUTPUT - - name: Check if clean from lint - if: steps.change_lint.outputs.changed != 0 - uses: actions/github-script@v8 - with: - script: | - core.setFailed('Repo is dirty after lint! Run yarn lint:fix locally before pushing changes.') - - name: Build - run: yarn build - - name: Check for changes from build - id: change_build - run: echo "changed=$(git status --porcelain | wc -l)" >> $GITHUB_OUTPUT - - name: Check if clean from build - if: steps.change_build.outputs.changed != 0 - uses: actions/github-script@v8 - with: - script: | - core.setFailed('Repo is dirty after build! Run yarn build locally before pushing changes.') - - test: - name: Vitest - needs: check-changes - if: needs.check-changes.outputs.frontend == 'true' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - name: Set up Node - uses: actions/setup-node@v6 - with: - node-version: lts/iron - - name: Install dependencies - run: yarn install - - name: Run tests - run: yarn test --coverage --coverage.reporter=json - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v5 - with: - token: ${{ secrets.CODECOV_TOKEN }} - flags: typescript - files: ./coverage/coverage-final.json diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml index 7348e72ee..c4a8d17d9 100644 --- a/.github/workflows/integration.yaml +++ b/.github/workflows/integration.yaml @@ -14,6 +14,7 @@ jobs: runs-on: ubuntu-latest outputs: python: ${{ steps.filter.outputs.python }} + frontend: ${{ steps.filter.outputs.frontend }} steps: - uses: actions/checkout@v6 - uses: dorny/paths-filter@v3 @@ -25,7 +26,18 @@ jobs: - 'requirements*.txt' - 'pyproject.toml' - '.github/workflows/integration.yaml' + frontend: + - '**.js' + - '**.ts' + - 'ts/**' + - '.eslintrc.cjs' + - 'package.json' + - 'rollup.config.js' + - 'tsconfig.json' + - 'vitest.config.ts' + - '.github/workflows/integration.yaml' + # Python jobs test: name: Pytest needs: check-changes @@ -79,3 +91,65 @@ jobs: steps: - uses: actions/checkout@v6 - uses: home-assistant/actions/hassfest@master + + # Frontend jobs + lint-and-build: + # pre-commit.ci skips yarn-lint and yarn-build (Node.js not available), so CI needs this job + name: Yarn Lint and Build + needs: check-changes + if: needs.check-changes.outputs.frontend == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 2 + - name: Set up Node + uses: actions/setup-node@v6 + with: + node-version: lts/iron + - name: Install dependencies + run: yarn install + - name: Lint + run: yarn lint:fix + - name: Check for changes from lint + id: change_lint + run: echo "changed=$(git status --porcelain | wc -l)" >> $GITHUB_OUTPUT + - name: Check if clean from lint + if: steps.change_lint.outputs.changed != 0 + uses: actions/github-script@v8 + with: + script: | + core.setFailed('Repo is dirty after lint! Run yarn lint:fix locally before pushing changes.') + - name: Build + run: yarn build + - name: Check for changes from build + id: change_build + run: echo "changed=$(git status --porcelain | wc -l)" >> $GITHUB_OUTPUT + - name: Check if clean from build + if: steps.change_build.outputs.changed != 0 + uses: actions/github-script@v8 + with: + script: | + core.setFailed('Repo is dirty after build! Run yarn build locally before pushing changes.') + + vitest: + name: Vitest + needs: check-changes + if: needs.check-changes.outputs.frontend == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Set up Node + uses: actions/setup-node@v6 + with: + node-version: lts/iron + - name: Install dependencies + run: yarn install + - name: Run tests + run: yarn test --coverage --coverage.reporter=json + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + flags: typescript + files: ./coverage/coverage-final.json From 470487d3cabf3ad09c326ea00968f8c418a1f127 Mon Sep 17 00:00:00 2001 From: raman325 <7243222+raman325@users.noreply.github.com> Date: Thu, 15 Jan 2026 02:03:11 -0500 Subject: [PATCH 10/17] Refactor to reusable workflows for better UI hierarchy Split integration.yaml into reusable workflows: - python-checks.yml: Pytest, HACS, Hassfest jobs - frontend-checks.yml: Yarn Lint and Build, Vitest jobs - integration.yaml: Orchestrates with check-changes and calls reusable workflows Benefits: - GitHub UI shows collapsible "Python" and "Frontend" groups - Clear visual hierarchy in the Actions tab - Maintains skip behavior via caller job conditions Update auto-merge to use prefixed check names (e.g., "Python / Pytest") since reusable workflow jobs get prefixed with caller job name. Co-Authored-By: Claude Opus 4.5 --- .github/workflows/frontend-checks.yml | 63 ++++++++++++++ .github/workflows/integration.yaml | 121 +++----------------------- .github/workflows/python-checks.yml | 54 ++++++++++++ .github/workflows/repository.yaml | 3 +- 4 files changed, 129 insertions(+), 112 deletions(-) create mode 100644 .github/workflows/frontend-checks.yml create mode 100644 .github/workflows/python-checks.yml diff --git a/.github/workflows/frontend-checks.yml b/.github/workflows/frontend-checks.yml new file mode 100644 index 000000000..ff20c38e3 --- /dev/null +++ b/.github/workflows/frontend-checks.yml @@ -0,0 +1,63 @@ +--- +name: Frontend Checks + +on: + workflow_call: + +jobs: + lint-and-build: + # pre-commit.ci skips yarn-lint and yarn-build (Node.js not available), so CI needs this job + name: Yarn Lint and Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 2 + - name: Set up Node + uses: actions/setup-node@v6 + with: + node-version: lts/iron + - name: Install dependencies + run: yarn install + - name: Lint + run: yarn lint:fix + - name: Check for changes from lint + id: change_lint + run: echo "changed=$(git status --porcelain | wc -l)" >> $GITHUB_OUTPUT + - name: Check if clean from lint + if: steps.change_lint.outputs.changed != 0 + uses: actions/github-script@v8 + with: + script: | + core.setFailed('Repo is dirty after lint! Run yarn lint:fix locally before pushing changes.') + - name: Build + run: yarn build + - name: Check for changes from build + id: change_build + run: echo "changed=$(git status --porcelain | wc -l)" >> $GITHUB_OUTPUT + - name: Check if clean from build + if: steps.change_build.outputs.changed != 0 + uses: actions/github-script@v8 + with: + script: | + core.setFailed('Repo is dirty after build! Run yarn build locally before pushing changes.') + + vitest: + name: Vitest + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Set up Node + uses: actions/setup-node@v6 + with: + node-version: lts/iron + - name: Install dependencies + run: yarn install + - name: Run tests + run: yarn test --coverage --coverage.reporter=json + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + flags: typescript + files: ./coverage/coverage-final.json diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml index c4a8d17d9..cb4ed7f28 100644 --- a/.github/workflows/integration.yaml +++ b/.github/workflows/integration.yaml @@ -26,6 +26,7 @@ jobs: - 'requirements*.txt' - 'pyproject.toml' - '.github/workflows/integration.yaml' + - '.github/workflows/python-checks.yml' frontend: - '**.js' - '**.ts' @@ -36,120 +37,18 @@ jobs: - 'tsconfig.json' - 'vitest.config.ts' - '.github/workflows/integration.yaml' + - '.github/workflows/frontend-checks.yml' - # Python jobs - test: - name: Pytest + python: + name: Python needs: check-changes if: needs.check-changes.outputs.python == 'true' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: "3.13" - - name: Install uv - uses: astral-sh/setup-uv@v5 - with: - enable-cache: true - - name: Install system dependencies - run: | - sudo apt-get update - sudo apt-get install libudev-dev - - name: Install dependencies - run: uv pip install --system -r requirements_dev.txt - - name: Run tests and generate coverage report - # CI-only deps installed separately to keep requirements_dev.txt lightweight - run: | - uv pip install --system pytest-cov pytest-github-actions-annotate-failures - pytest ./tests/ --cov=custom_components/lock_code_manager/ --cov-report=xml - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v5 - with: - token: ${{ secrets.CODECOV_TOKEN }} - flags: python - files: coverage.xml - - hacs: - name: HACS - needs: check-changes - if: needs.check-changes.outputs.python == 'true' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - name: HACS Action - uses: hacs/action@main - with: - category: integration - - hassfest: - name: Hassfest - needs: check-changes - if: needs.check-changes.outputs.python == 'true' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - uses: home-assistant/actions/hassfest@master + uses: ./.github/workflows/python-checks.yml + secrets: inherit - # Frontend jobs - lint-and-build: - # pre-commit.ci skips yarn-lint and yarn-build (Node.js not available), so CI needs this job - name: Yarn Lint and Build + frontend: + name: Frontend needs: check-changes if: needs.check-changes.outputs.frontend == 'true' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 2 - - name: Set up Node - uses: actions/setup-node@v6 - with: - node-version: lts/iron - - name: Install dependencies - run: yarn install - - name: Lint - run: yarn lint:fix - - name: Check for changes from lint - id: change_lint - run: echo "changed=$(git status --porcelain | wc -l)" >> $GITHUB_OUTPUT - - name: Check if clean from lint - if: steps.change_lint.outputs.changed != 0 - uses: actions/github-script@v8 - with: - script: | - core.setFailed('Repo is dirty after lint! Run yarn lint:fix locally before pushing changes.') - - name: Build - run: yarn build - - name: Check for changes from build - id: change_build - run: echo "changed=$(git status --porcelain | wc -l)" >> $GITHUB_OUTPUT - - name: Check if clean from build - if: steps.change_build.outputs.changed != 0 - uses: actions/github-script@v8 - with: - script: | - core.setFailed('Repo is dirty after build! Run yarn build locally before pushing changes.') - - vitest: - name: Vitest - needs: check-changes - if: needs.check-changes.outputs.frontend == 'true' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - name: Set up Node - uses: actions/setup-node@v6 - with: - node-version: lts/iron - - name: Install dependencies - run: yarn install - - name: Run tests - run: yarn test --coverage --coverage.reporter=json - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v5 - with: - token: ${{ secrets.CODECOV_TOKEN }} - flags: typescript - files: ./coverage/coverage-final.json + uses: ./.github/workflows/frontend-checks.yml + secrets: inherit diff --git a/.github/workflows/python-checks.yml b/.github/workflows/python-checks.yml new file mode 100644 index 000000000..8a1cc64c3 --- /dev/null +++ b/.github/workflows/python-checks.yml @@ -0,0 +1,54 @@ +--- +name: Python Checks + +on: + workflow_call: + +jobs: + test: + name: Pytest + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.13" + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install libudev-dev + - name: Install dependencies + run: uv pip install --system -r requirements_dev.txt + - name: Run tests and generate coverage report + # CI-only deps installed separately to keep requirements_dev.txt lightweight + run: | + uv pip install --system pytest-cov pytest-github-actions-annotate-failures + pytest ./tests/ --cov=custom_components/lock_code_manager/ --cov-report=xml + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + flags: python + files: coverage.xml + + hacs: + name: HACS + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: HACS Action + uses: hacs/action@main + with: + category: integration + + hassfest: + name: Hassfest + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: home-assistant/actions/hassfest@master diff --git a/.github/workflows/repository.yaml b/.github/workflows/repository.yaml index 57ef8ce36..e6c789156 100644 --- a/.github/workflows/repository.yaml +++ b/.github/workflows/repository.yaml @@ -110,7 +110,8 @@ jobs: # Checks that must exist and pass (always run) MUST_PASS: Check Changes # Checks that must pass IF they run (may be skipped by path filter) - IF_RUN_PASS: Pytest,HACS,Hassfest,Vitest,Yarn Lint and Build + # Reusable workflow jobs are prefixed with caller job name + IF_RUN_PASS: Python / Pytest,Python / HACS,Python / Hassfest,Frontend / Vitest,Frontend / Yarn Lint and Build run: | # Give workflows time to register their check runs # Longer delay is fine since auto-merge only runs for automated/low-risk PRs From c9633a3cc951b155863e383ccdf5ec2e72a4104e Mon Sep 17 00:00:00 2001 From: raman325 <7243222+raman325@users.noreply.github.com> Date: Thu, 15 Jan 2026 02:05:33 -0500 Subject: [PATCH 11/17] Rename Python caller job to Integration HACS and Hassfest validate the Home Assistant integration as a whole, not just Python code. "Integration" better reflects the scope of these checks. Co-Authored-By: Claude Opus 4.5 --- .github/workflows/integration.yaml | 4 ++-- .github/workflows/repository.yaml | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml index cb4ed7f28..2c9b506b9 100644 --- a/.github/workflows/integration.yaml +++ b/.github/workflows/integration.yaml @@ -39,8 +39,8 @@ jobs: - '.github/workflows/integration.yaml' - '.github/workflows/frontend-checks.yml' - python: - name: Python + integration: + name: Integration needs: check-changes if: needs.check-changes.outputs.python == 'true' uses: ./.github/workflows/python-checks.yml diff --git a/.github/workflows/repository.yaml b/.github/workflows/repository.yaml index e6c789156..8b31bdd09 100644 --- a/.github/workflows/repository.yaml +++ b/.github/workflows/repository.yaml @@ -111,7 +111,8 @@ jobs: MUST_PASS: Check Changes # Checks that must pass IF they run (may be skipped by path filter) # Reusable workflow jobs are prefixed with caller job name - IF_RUN_PASS: Python / Pytest,Python / HACS,Python / Hassfest,Frontend / Vitest,Frontend / Yarn Lint and Build + # yamllint disable-line rule:line-length + IF_RUN_PASS: Integration / Pytest,Integration / HACS,Integration / Hassfest,Frontend / Vitest,Frontend / Yarn Lint and Build run: | # Give workflows time to register their check runs # Longer delay is fine since auto-merge only runs for automated/low-risk PRs From eb634030019126e13c33427fd5a2c70c04f4684a Mon Sep 17 00:00:00 2001 From: raman325 <7243222+raman325@users.noreply.github.com> Date: Thu, 15 Jan 2026 02:12:49 -0500 Subject: [PATCH 12/17] Separate HACS and Hassfest with proper path filters HACS and Hassfest validate different aspects of the integration: - HACS: hacs.json, README.md, custom_components/ structure - Hassfest: custom_components/ manifest, translations, services Each now has its own path filter and runs as a direct job rather than nested under Python. This means they only run when relevant files change. Structure: - Check Changes: Outputs python, hacs, hassfest, frontend filters - Python (reusable): Pytest only - HACS (direct): Runs when hacs.json, README.md, or custom_components changes - Hassfest (direct): Runs when custom_components changes - Frontend (reusable): Vitest, Yarn Lint and Build Co-Authored-By: Claude Opus 4.5 --- .github/workflows/integration.yaml | 35 +++++++++++++++++++++++++++-- .github/workflows/python-checks.yml | 19 +--------------- .github/workflows/repository.yaml | 2 +- 3 files changed, 35 insertions(+), 21 deletions(-) diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml index 2c9b506b9..a28c0dee0 100644 --- a/.github/workflows/integration.yaml +++ b/.github/workflows/integration.yaml @@ -14,6 +14,8 @@ jobs: runs-on: ubuntu-latest outputs: python: ${{ steps.filter.outputs.python }} + hacs: ${{ steps.filter.outputs.hacs }} + hassfest: ${{ steps.filter.outputs.hassfest }} frontend: ${{ steps.filter.outputs.frontend }} steps: - uses: actions/checkout@v6 @@ -27,6 +29,14 @@ jobs: - 'pyproject.toml' - '.github/workflows/integration.yaml' - '.github/workflows/python-checks.yml' + hacs: + - 'hacs.json' + - 'README.md' + - 'custom_components/**' + - '.github/workflows/integration.yaml' + hassfest: + - 'custom_components/**' + - '.github/workflows/integration.yaml' frontend: - '**.js' - '**.ts' @@ -39,13 +49,34 @@ jobs: - '.github/workflows/integration.yaml' - '.github/workflows/frontend-checks.yml' - integration: - name: Integration + python: + name: Python needs: check-changes if: needs.check-changes.outputs.python == 'true' uses: ./.github/workflows/python-checks.yml secrets: inherit + hacs: + name: HACS + needs: check-changes + if: needs.check-changes.outputs.hacs == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: HACS Action + uses: hacs/action@main + with: + category: integration + + hassfest: + name: Hassfest + needs: check-changes + if: needs.check-changes.outputs.hassfest == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: home-assistant/actions/hassfest@master + frontend: name: Frontend needs: check-changes diff --git a/.github/workflows/python-checks.yml b/.github/workflows/python-checks.yml index 8a1cc64c3..98b8855b6 100644 --- a/.github/workflows/python-checks.yml +++ b/.github/workflows/python-checks.yml @@ -5,7 +5,7 @@ on: workflow_call: jobs: - test: + pytest: name: Pytest runs-on: ubuntu-latest steps: @@ -35,20 +35,3 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} flags: python files: coverage.xml - - hacs: - name: HACS - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - name: HACS Action - uses: hacs/action@main - with: - category: integration - - hassfest: - name: Hassfest - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - uses: home-assistant/actions/hassfest@master diff --git a/.github/workflows/repository.yaml b/.github/workflows/repository.yaml index 8b31bdd09..536f7c850 100644 --- a/.github/workflows/repository.yaml +++ b/.github/workflows/repository.yaml @@ -112,7 +112,7 @@ jobs: # Checks that must pass IF they run (may be skipped by path filter) # Reusable workflow jobs are prefixed with caller job name # yamllint disable-line rule:line-length - IF_RUN_PASS: Integration / Pytest,Integration / HACS,Integration / Hassfest,Frontend / Vitest,Frontend / Yarn Lint and Build + IF_RUN_PASS: Python / Pytest,HACS,Hassfest,Frontend / Vitest,Frontend / Yarn Lint and Build run: | # Give workflows time to register their check runs # Longer delay is fine since auto-merge only runs for automated/low-risk PRs From df45044b21376eba125976204bc41df343658cc8 Mon Sep 17 00:00:00 2001 From: raman325 <7243222+raman325@users.noreply.github.com> Date: Thu, 15 Jan 2026 02:16:55 -0500 Subject: [PATCH 13/17] Simplify HACS and Hassfest to always run Remove path filtering for HACS and Hassfest - they're quick validation checks (~30 seconds) that can run on every PR. Making them required for auto-merge provides a natural delay, eliminating the need for an artificial sleep. Changes: - HACS and Hassfest run unconditionally (no needs/if) - Moved to MUST_PASS in auto-merge (always exist and must pass) - Removed 60-second sleep delay (HACS/Hassfest provide natural delay) Co-Authored-By: Claude Opus 4.5 --- .github/workflows/integration.yaml | 14 -------------- .github/workflows/repository.yaml | 10 ++-------- 2 files changed, 2 insertions(+), 22 deletions(-) diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml index a28c0dee0..37703b99f 100644 --- a/.github/workflows/integration.yaml +++ b/.github/workflows/integration.yaml @@ -14,8 +14,6 @@ jobs: runs-on: ubuntu-latest outputs: python: ${{ steps.filter.outputs.python }} - hacs: ${{ steps.filter.outputs.hacs }} - hassfest: ${{ steps.filter.outputs.hassfest }} frontend: ${{ steps.filter.outputs.frontend }} steps: - uses: actions/checkout@v6 @@ -29,14 +27,6 @@ jobs: - 'pyproject.toml' - '.github/workflows/integration.yaml' - '.github/workflows/python-checks.yml' - hacs: - - 'hacs.json' - - 'README.md' - - 'custom_components/**' - - '.github/workflows/integration.yaml' - hassfest: - - 'custom_components/**' - - '.github/workflows/integration.yaml' frontend: - '**.js' - '**.ts' @@ -58,8 +48,6 @@ jobs: hacs: name: HACS - needs: check-changes - if: needs.check-changes.outputs.hacs == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 @@ -70,8 +58,6 @@ jobs: hassfest: name: Hassfest - needs: check-changes - if: needs.check-changes.outputs.hassfest == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 diff --git a/.github/workflows/repository.yaml b/.github/workflows/repository.yaml index 536f7c850..a9ce80d50 100644 --- a/.github/workflows/repository.yaml +++ b/.github/workflows/repository.yaml @@ -108,17 +108,11 @@ jobs: REPO: ${{ github.repository }} SHA: ${{ github.event.pull_request.head.sha }} # Checks that must exist and pass (always run) - MUST_PASS: Check Changes + MUST_PASS: Check Changes,HACS,Hassfest # Checks that must pass IF they run (may be skipped by path filter) # Reusable workflow jobs are prefixed with caller job name - # yamllint disable-line rule:line-length - IF_RUN_PASS: Python / Pytest,HACS,Hassfest,Frontend / Vitest,Frontend / Yarn Lint and Build + IF_RUN_PASS: Python / Pytest,Frontend / Vitest,Frontend / Yarn Lint and Build run: | - # Give workflows time to register their check runs - # Longer delay is fine since auto-merge only runs for automated/low-risk PRs - echo "Waiting for workflows to register..." - sleep 60 - IFS=',' read -ra MUST <<< "$MUST_PASS" IFS=',' read -ra OPTIONAL <<< "$IF_RUN_PASS" ALL_CHECKS=("${MUST[@]}" "${OPTIONAL[@]}") From 7a9fc742bb061610f6cd4da22f5aebd13d2043bd Mon Sep 17 00:00:00 2001 From: raman325 <7243222+raman325@users.noreply.github.com> Date: Thu, 15 Jan 2026 02:25:47 -0500 Subject: [PATCH 14/17] Add Ruff and Mypy checks, run Pytest on Python 3.13 and 3.14 Python checks now include: - Ruff: Format check and linting (runs on 3.13) - Mypy: Type checking (runs on 3.13) - Pytest: Tests with coverage (matrix: 3.13, 3.14) Notes: - Static analysis (Ruff, Mypy) runs on single version - Pytest uses matrix with fail-fast: false for independent results - Coverage only uploaded for 3.13 to avoid duplicates - Python 3.14 uses allow-prereleases: true Co-Authored-By: Claude Opus 4.5 --- .github/workflows/python-checks.yml | 52 +++++++++++++++++++++++++++-- .github/workflows/repository.yaml | 3 +- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/.github/workflows/python-checks.yml b/.github/workflows/python-checks.yml index 98b8855b6..d00aee207 100644 --- a/.github/workflows/python-checks.yml +++ b/.github/workflows/python-checks.yml @@ -5,8 +5,28 @@ on: workflow_call: jobs: - pytest: - name: Pytest + ruff: + name: Ruff + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.13" + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + - name: Install dependencies + run: uv pip install --system ruff + - name: Check formatting + run: ruff format --check . + - name: Check linting + run: ruff check . + + mypy: + name: Mypy runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 @@ -24,12 +44,40 @@ jobs: sudo apt-get install libudev-dev - name: Install dependencies run: uv pip install --system -r requirements_dev.txt + - name: Run mypy + run: mypy custom_components/lock_code_manager/ + + pytest: + name: Pytest (${{ matrix.python-version }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.13", "3.14"] + steps: + - uses: actions/checkout@v6 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + allow-prereleases: true + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install libudev-dev + - name: Install dependencies + run: uv pip install --system -r requirements_dev.txt - name: Run tests and generate coverage report # CI-only deps installed separately to keep requirements_dev.txt lightweight run: | uv pip install --system pytest-cov pytest-github-actions-annotate-failures pytest ./tests/ --cov=custom_components/lock_code_manager/ --cov-report=xml - name: Upload coverage to Codecov + if: matrix.python-version == '3.13' uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/repository.yaml b/.github/workflows/repository.yaml index a9ce80d50..96740a83c 100644 --- a/.github/workflows/repository.yaml +++ b/.github/workflows/repository.yaml @@ -111,7 +111,8 @@ jobs: MUST_PASS: Check Changes,HACS,Hassfest # Checks that must pass IF they run (may be skipped by path filter) # Reusable workflow jobs are prefixed with caller job name - IF_RUN_PASS: Python / Pytest,Frontend / Vitest,Frontend / Yarn Lint and Build + # yamllint disable-line rule:line-length + IF_RUN_PASS: Python / Ruff,Python / Mypy,Python / Pytest (3.13),Python / Pytest (3.14),Frontend / Vitest,Frontend / Yarn Lint and Build run: | IFS=',' read -ra MUST <<< "$MUST_PASS" IFS=',' read -ra OPTIONAL <<< "$IF_RUN_PASS" From 142e5ba53c0115812b7ee04cb0f009bd479d751d Mon Sep 17 00:00:00 2001 From: raman325 <7243222+raman325@users.noreply.github.com> Date: Thu, 15 Jan 2026 02:30:58 -0500 Subject: [PATCH 15/17] Remove mypy from CI, add TODO for type checking Mypy has ~30 errors in the codebase. Removing from this PR to keep scope focused on CI improvements. Added TODO to: - Add mypy (or alternative) to pre-commit hooks - Add type checking CI job - Explore alternatives (Astral may have a replacement) - Fix existing type errors Co-Authored-By: Claude Opus 4.5 --- .github/workflows/python-checks.yml | 22 ---------------------- .github/workflows/repository.yaml | 2 +- TODO.md | 5 +++++ 3 files changed, 6 insertions(+), 23 deletions(-) diff --git a/.github/workflows/python-checks.yml b/.github/workflows/python-checks.yml index d00aee207..868e335e9 100644 --- a/.github/workflows/python-checks.yml +++ b/.github/workflows/python-checks.yml @@ -25,28 +25,6 @@ jobs: - name: Check linting run: ruff check . - mypy: - name: Mypy - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: "3.13" - - name: Install uv - uses: astral-sh/setup-uv@v5 - with: - enable-cache: true - - name: Install system dependencies - run: | - sudo apt-get update - sudo apt-get install libudev-dev - - name: Install dependencies - run: uv pip install --system -r requirements_dev.txt - - name: Run mypy - run: mypy custom_components/lock_code_manager/ - pytest: name: Pytest (${{ matrix.python-version }}) runs-on: ubuntu-latest diff --git a/.github/workflows/repository.yaml b/.github/workflows/repository.yaml index 96740a83c..90f3939c5 100644 --- a/.github/workflows/repository.yaml +++ b/.github/workflows/repository.yaml @@ -112,7 +112,7 @@ jobs: # Checks that must pass IF they run (may be skipped by path filter) # Reusable workflow jobs are prefixed with caller job name # yamllint disable-line rule:line-length - IF_RUN_PASS: Python / Ruff,Python / Mypy,Python / Pytest (3.13),Python / Pytest (3.14),Frontend / Vitest,Frontend / Yarn Lint and Build + IF_RUN_PASS: Python / Ruff,Python / Pytest (3.13),Python / Pytest (3.14),Frontend / Vitest,Frontend / Yarn Lint and Build run: | IFS=',' read -ra MUST <<< "$MUST_PASS" IFS=',' read -ra OPTIONAL <<< "$IF_RUN_PASS" diff --git a/TODO.md b/TODO.md index 64fb88fbe..fd6db2606 100644 --- a/TODO.md +++ b/TODO.md @@ -3,6 +3,11 @@ ## New Items - Unify design across slot and lock data cards, with a preference towards the slot card design. +- Add type checking to CI and pre-commit: + - Add mypy (or alternative) to pre-commit hooks + - Add type checking CI job to python-checks.yml + - Explore alternatives to mypy (Astral may have a replacement - check for "ty" or similar) + - Fix existing type errors (~30 errors as of Jan 2026) - Test visual editor for both cards. ## Testing From 9254ca9d3ef613aa317aed8ba0089b8455a2db24 Mon Sep 17 00:00:00 2001 From: raman325 <7243222+raman325@users.noreply.github.com> Date: Thu, 15 Jan 2026 02:34:02 -0500 Subject: [PATCH 16/17] Use variables for Python versions in CI matrix Define Python versions as env vars at workflow level: - TARGET_PYTHON: Primary version for coverage and static analysis - OTHER_PYTHON_VERSIONS: JSON array of additional test versions A setup job computes the combined matrix and passes it to downstream jobs. This makes version management centralized - just update the env vars to test different Python versions. Co-Authored-By: Claude Opus 4.5 --- .github/workflows/python-checks.yml | 28 +++++++++++++++++++++++++--- .github/workflows/repository.yaml | 3 ++- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/.github/workflows/python-checks.yml b/.github/workflows/python-checks.yml index 868e335e9..f5c61c26d 100644 --- a/.github/workflows/python-checks.yml +++ b/.github/workflows/python-checks.yml @@ -4,16 +4,37 @@ name: Python Checks on: workflow_call: +env: + # Target Python version for coverage and static analysis + TARGET_PYTHON: "3.13" + # Additional Python versions to test (JSON array, can be empty: '[]') + OTHER_PYTHON_VERSIONS: '["3.14"]' + jobs: + setup: + name: Setup + runs-on: ubuntu-latest + outputs: + target-python: ${{ env.TARGET_PYTHON }} + python-versions: ${{ steps.versions.outputs.matrix }} + steps: + - name: Build version matrix + id: versions + run: | + # Combine target with other versions into a JSON array + MATRIX=$(echo '${{ env.OTHER_PYTHON_VERSIONS }}' | jq -c '. + ["${{ env.TARGET_PYTHON }}"] | unique | sort') + echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT" + ruff: name: Ruff + needs: setup runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: - python-version: "3.13" + python-version: ${{ needs.setup.outputs.target-python }} - name: Install uv uses: astral-sh/setup-uv@v5 with: @@ -27,11 +48,12 @@ jobs: pytest: name: Pytest (${{ matrix.python-version }}) + needs: setup runs-on: ubuntu-latest strategy: fail-fast: false matrix: - python-version: ["3.13", "3.14"] + python-version: ${{ fromJSON(needs.setup.outputs.python-versions) }} steps: - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} @@ -55,7 +77,7 @@ jobs: uv pip install --system pytest-cov pytest-github-actions-annotate-failures pytest ./tests/ --cov=custom_components/lock_code_manager/ --cov-report=xml - name: Upload coverage to Codecov - if: matrix.python-version == '3.13' + if: matrix.python-version == needs.setup.outputs.target-python uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/repository.yaml b/.github/workflows/repository.yaml index 90f3939c5..08738dcf1 100644 --- a/.github/workflows/repository.yaml +++ b/.github/workflows/repository.yaml @@ -112,7 +112,8 @@ jobs: # Checks that must pass IF they run (may be skipped by path filter) # Reusable workflow jobs are prefixed with caller job name # yamllint disable-line rule:line-length - IF_RUN_PASS: Python / Ruff,Python / Pytest (3.13),Python / Pytest (3.14),Frontend / Vitest,Frontend / Yarn Lint and Build + # yamllint disable-line rule:line-length + IF_RUN_PASS: Python / Setup,Python / Ruff,Python / Pytest (3.13),Python / Pytest (3.14),Frontend / Vitest,Frontend / Yarn Lint and Build run: | IFS=',' read -ra MUST <<< "$MUST_PASS" IFS=',' read -ra OPTIONAL <<< "$IF_RUN_PASS" From db187bdc1935b966cbb17511cec49620328e37d0 Mon Sep 17 00:00:00 2001 From: raman325 <7243222+raman325@users.noreply.github.com> Date: Thu, 15 Jan 2026 02:35:35 -0500 Subject: [PATCH 17/17] Support pattern matching for auto-merge check names Use * suffix for pattern matching in IF_RUN_PASS checks: - "Python / Pytest *" matches "Python / Pytest (3.13)", "Python / Pytest (3.14)", etc. This decouples repository.yaml from specific Python versions defined in python-checks.yml. When Python versions change, only python-checks.yml needs updating. Also optimized to fetch all check runs once per iteration instead of per-check queries. Co-Authored-By: Claude Opus 4.5 --- .github/workflows/repository.yaml | 53 +++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/.github/workflows/repository.yaml b/.github/workflows/repository.yaml index 08738dcf1..aa8a85034 100644 --- a/.github/workflows/repository.yaml +++ b/.github/workflows/repository.yaml @@ -110,10 +110,9 @@ jobs: # Checks that must exist and pass (always run) MUST_PASS: Check Changes,HACS,Hassfest # Checks that must pass IF they run (may be skipped by path filter) - # Reusable workflow jobs are prefixed with caller job name + # Use * suffix for pattern matching (e.g., "Python / Pytest *" matches any version) # yamllint disable-line rule:line-length - # yamllint disable-line rule:line-length - IF_RUN_PASS: Python / Setup,Python / Ruff,Python / Pytest (3.13),Python / Pytest (3.14),Frontend / Vitest,Frontend / Yarn Lint and Build + IF_RUN_PASS: Python / Setup,Python / Ruff,Python / Pytest *,Frontend / Vitest,Frontend / Yarn Lint and Build run: | IFS=',' read -ra MUST <<< "$MUST_PASS" IFS=',' read -ra OPTIONAL <<< "$IF_RUN_PASS" @@ -121,18 +120,31 @@ jobs: echo "Must pass: ${MUST[*]}" echo "If run, must pass: ${OPTIONAL[*]}" + # Fetch all check runs once per iteration + fetch_checks() { + gh api "repos/$REPO/commits/$SHA/check-runs" --paginate \ + --jq '.check_runs[] | {name: .name, status: .status, conclusion: .conclusion}' \ + 2>/dev/null + } + while true; do ALL_DONE=true ANY_FAILED=false MUST_FOUND=0 + CHECKS_JSON=$(fetch_checks) for CHECK in "${ALL_CHECKS[@]}"; do - # Check may appear multiple times (e.g., "Check Changes" in multiple workflows) - STATUSES=$(gh api "repos/$REPO/commits/$SHA/check-runs" \ - --jq "[.check_runs[] | select(.name == \"$CHECK\")] | map({s: .status, c: .conclusion})" \ - 2>/dev/null) + # Check if this is a pattern (ends with *) + if [[ "$CHECK" == *'*' ]]; then + PREFIX="${CHECK%\*}" + MATCHES=$(echo "$CHECKS_JSON" | jq -s --arg p "$PREFIX" \ + '[.[] | select(.name | startswith($p))]') + else + MATCHES=$(echo "$CHECKS_JSON" | jq -s --arg n "$CHECK" \ + '[.[] | select(.name == $n)]') + fi - COUNT=$(echo "$STATUSES" | jq 'length') + COUNT=$(echo "$MATCHES" | jq 'length') if [ "$COUNT" -eq 0 ]; then echo "$CHECK: not found" @@ -148,25 +160,32 @@ jobs: done # Check each instance - for i in $(seq 0 $((COUNT - 1))); do - STATUS=$(echo "$STATUSES" | jq -r ".[$i].s") - CONCLUSION=$(echo "$STATUSES" | jq -r ".[$i].c") + echo "$MATCHES" | jq -c '.[]' | while read -r item; do + NAME=$(echo "$item" | jq -r '.name') + STATUS=$(echo "$item" | jq -r '.status') + CONCLUSION=$(echo "$item" | jq -r '.conclusion') if [ "$STATUS" != "completed" ]; then - echo "$CHECK [$i]: $STATUS" - ALL_DONE=false + echo "$NAME: $STATUS" + echo "PENDING" >> /tmp/check_status elif [ "$CONCLUSION" != "success" ] && [ "$CONCLUSION" != "skipped" ]; then - echo "::error::$CHECK [$i] failed ($CONCLUSION)" - ANY_FAILED=true + echo "::error::$NAME failed ($CONCLUSION)" + echo "FAILED" >> /tmp/check_status else - echo "$CHECK [$i]: $CONCLUSION" + echo "$NAME: $CONCLUSION" fi done done - if [ "$ANY_FAILED" = true ]; then + # Check for failures or pending (subshell writes to temp file) + if grep -q "FAILED" /tmp/check_status 2>/dev/null; then + rm -f /tmp/check_status exit 1 fi + if grep -q "PENDING" /tmp/check_status 2>/dev/null; then + ALL_DONE=false + fi + rm -f /tmp/check_status if [ "$ALL_DONE" = true ]; then if [ "$MUST_FOUND" -eq 0 ]; then