diff --git a/.github/issue-labeler.yml b/.github/issue-labeler.yml new file mode 100644 index 000000000..5a685d6d4 --- /dev/null +++ b/.github/issue-labeler.yml @@ -0,0 +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 +--- +bug: + - '\[[xX]\]\s*[Bb]ugfix' + +enhancement: + - '\[[xX]\]\s*[Nn]ew [Ff]eature' + +breaking-change: + - '\[[xX]\]\s*[Bb]reaking [Cc]hange' + +code-quality: + - '\[[xX]\]\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/frontend.yaml b/.github/workflows/frontend-checks.yml similarity index 76% rename from .github/workflows/frontend.yaml rename to .github/workflows/frontend-checks.yml index 7541dd485..ff20c38e3 100644 --- a/.github/workflows/frontend.yaml +++ b/.github/workflows/frontend-checks.yml @@ -1,32 +1,8 @@ --- -name: Frontend +name: Frontend Checks -# yamllint disable-line rule:truthy 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" + workflow_call: jobs: lint-and-build: @@ -66,7 +42,7 @@ jobs: script: | core.setFailed('Repo is dirty after build! Run yarn build locally before pushing changes.') - test: + vitest: name: Vitest runs-on: ubuntu-latest steps: diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml index c0ac0e23c..37703b99f 100644 --- a/.github/workflows/integration.yaml +++ b/.github/workflows/integration.yaml @@ -5,48 +5,46 @@ 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: - test: - name: Pytest + check-changes: + name: Check Changes runs-on: ubuntu-latest + outputs: + python: ${{ steps.filter.outputs.python }} + frontend: ${{ steps.filter.outputs.frontend }} steps: - uses: actions/checkout@v6 - - name: Set up Python - uses: actions/setup-python@v6 + - uses: dorny/paths-filter@v3 + id: filter 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 + filters: | + python: + - '**.py' + - 'requirements*.txt' + - 'pyproject.toml' + - '.github/workflows/integration.yaml' + - '.github/workflows/python-checks.yml' + frontend: + - '**.js' + - '**.ts' + - 'ts/**' + - '.eslintrc.cjs' + - 'package.json' + - 'rollup.config.js' + - 'tsconfig.json' + - 'vitest.config.ts' + - '.github/workflows/integration.yaml' + - '.github/workflows/frontend-checks.yml' + + python: + name: Python + needs: check-changes + if: needs.check-changes.outputs.python == 'true' + uses: ./.github/workflows/python-checks.yml + secrets: inherit hacs: name: HACS @@ -64,3 +62,10 @@ jobs: steps: - uses: actions/checkout@v6 - uses: home-assistant/actions/hassfest@master + + frontend: + name: Frontend + needs: check-changes + if: needs.check-changes.outputs.frontend == 'true' + uses: ./.github/workflows/frontend-checks.yml + secrets: inherit 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/.github/workflows/python-checks.yml b/.github/workflows/python-checks.yml new file mode 100644 index 000000000..f5c61c26d --- /dev/null +++ b/.github/workflows/python-checks.yml @@ -0,0 +1,85 @@ +--- +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: ${{ needs.setup.outputs.target-python }} + - 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 . + + pytest: + name: Pytest (${{ matrix.python-version }}) + needs: setup + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ${{ fromJSON(needs.setup.outputs.python-versions) }} + 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 == needs.setup.outputs.target-python + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + flags: python + files: coverage.xml diff --git a/.github/workflows/repository.yaml b/.github/workflows/repository.yaml index b07a2d0f2..aa8a85034 100644 --- a/.github/workflows/repository.yaml +++ b/.github/workflows/repository.yaml @@ -99,14 +99,105 @@ 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,HACS,Hassfest + # Checks that must pass IF they run (may be skipped by path filter) + # Use * suffix for pattern matching (e.g., "Python / Pytest *" matches any version) + # yamllint disable-line rule:line-length + 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" + ALL_CHECKS=("${MUST[@]}" "${OPTIONAL[@]}") + 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 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 "$MATCHES" | 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 + 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 "$NAME: $STATUS" + echo "PENDING" >> /tmp/check_status + elif [ "$CONCLUSION" != "success" ] && [ "$CONCLUSION" != "skipped" ]; then + echo "::error::$NAME failed ($CONCLUSION)" + echo "FAILED" >> /tmp/check_status + else + echo "$NAME: $CONCLUSION" + fi + done + done + + # 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 + 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 diff --git a/TODO.md b/TODO.md index 1ec473664..fd6db2606 100644 --- a/TODO.md +++ b/TODO.md @@ -3,9 +3,12 @@ ## 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. -- Explore alternative autolabeler workflows that can remove labels when patterns no longer - match (release-drafter only adds labels, never removes them). ## Testing