1.7.0-3 Bugfixes #153
Workflow file for this run
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: CI | |
| on: | |
| pull_request: | |
| push: | |
| branches: | |
| - main | |
| jobs: | |
| hassfest: | |
| name: Hassfest validation | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v4 | |
| with: | |
| repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }} | |
| ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref }} | |
| - name: Detect hassfest auto-fix check | |
| id: hassfest_autofix | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TARGET_SHA: ${{ github.event.pull_request.head.sha || github.sha }} | |
| run: | | |
| python - <<'PY' | |
| import json | |
| import os | |
| import re | |
| import sys | |
| import urllib.error | |
| import urllib.request | |
| repository = os.environ["GITHUB_REPOSITORY"] | |
| target_sha = os.environ["TARGET_SHA"] | |
| token = os.environ.get("GITHUB_TOKEN") | |
| request = urllib.request.Request( | |
| f"https://api.github.com/repos/{repository}/commits/{target_sha}/check-runs" | |
| ) | |
| request.add_header("Accept", "application/vnd.github+json") | |
| if token: | |
| request.add_header("Authorization", f"Bearer {token}") | |
| try: | |
| with urllib.request.urlopen(request, timeout=30) as response: | |
| payload = json.load(response) | |
| except urllib.error.HTTPError as exc: # pragma: no cover - network error surface | |
| print(f"Warning: unable to query check runs (status={exc.code}). Assuming none present.") | |
| payload = {"check_runs": []} | |
| except urllib.error.URLError as exc: # pragma: no cover - network error surface | |
| print(f"Warning: unable to reach GitHub API ({exc}). Assuming no matching checks.") | |
| payload = {"check_runs": []} | |
| pattern = re.compile(r"(?i)^hassfest( \(auto-fix\)( / hassfest.*)?)?$") | |
| has_match = any( | |
| pattern.search(run.get("name", "")) | |
| for run in payload.get("check_runs", []) | |
| ) | |
| output_path = os.environ["GITHUB_OUTPUT"] | |
| with open(output_path, "a", encoding="utf-8") as handle: | |
| handle.write(f"found={'true' if has_match else 'false'}\n") | |
| print( | |
| "Detected hassfest (auto-fix) check: {}".format( | |
| "yes" if has_match else "no" | |
| ) | |
| ) | |
| PY | |
| - name: Wait for hassfest auto-fix workflow to finish | |
| if: steps.hassfest_autofix.outputs.found == 'true' | |
| uses: lewagon/wait-on-check-action@v1.3.3 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha || github.sha }} | |
| check-regexp: '^[Hh]assfest( \(auto-fix\)( / hassfest.*)?)?$' | |
| allowed-conclusions: 'success,failure,neutral' | |
| repo-token: ${{ secrets.GITHUB_TOKEN }} | |
| wait-interval: 10 | |
| - name: Run hassfest | |
| id: hassfest | |
| uses: home-assistant/actions/hassfest@master | |
| - name: Write hassfest summary | |
| if: always() | |
| run: | | |
| { | |
| echo "## Hassfest Validation" | |
| echo "" | |
| if [ "${{ steps.hassfest.outcome }}" = "success" ]; then | |
| echo "✅ **Manifest validation passed**" | |
| echo "" | |
| echo "The \`manifest.json\` and integration structure are valid." | |
| else | |
| echo "❌ **Manifest validation failed**" | |
| echo "" | |
| echo "Check the logs above for details." | |
| fi | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| hacs: | |
| name: HACS validation | |
| runs-on: ubuntu-latest | |
| needs: hassfest | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v4 | |
| with: | |
| repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }} | |
| ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref }} | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.13" | |
| - name: Probe brands endpoint | |
| id: brands_probe | |
| run: | | |
| set -u | |
| if curl -sSfL https://brands.home-assistant.io/domains.json >/dev/null; then | |
| echo "available=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "available=false" >> "$GITHUB_OUTPUT" | |
| echo "::warning ::brands.home-assistant.io unavailable; skipping HACS validation to avoid false negatives." | |
| fi | |
| - name: Run HACS validation | |
| if: steps.brands_probe.outputs.available == 'true' | |
| id: hacs | |
| uses: hacs/action@main | |
| with: | |
| category: integration | |
| ignore: topics issues | |
| - name: Write HACS summary | |
| if: always() | |
| run: | | |
| { | |
| echo "## HACS Validation" | |
| echo "" | |
| if [ "${{ steps.brands_probe.outputs.available }}" != "true" ]; then | |
| echo "⚠️ **Skipped** - brands.home-assistant.io unavailable" | |
| elif [ "${{ steps.hacs.outcome }}" = "success" ]; then | |
| echo "✅ **HACS validation passed**" | |
| echo "" | |
| echo "The integration meets HACS requirements." | |
| else | |
| echo "❌ **HACS validation failed**" | |
| echo "" | |
| echo "Check the logs above for details." | |
| fi | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| lint: | |
| name: Lint (${{ matrix.check }}) | |
| runs-on: ubuntu-latest | |
| needs: hassfest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| check: [ruff, mypy, codespell, bandit] | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v4 | |
| with: | |
| repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }} | |
| ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref }} | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.13" | |
| - name: Install Poetry | |
| uses: snok/install-poetry@v1 | |
| with: | |
| version: latest | |
| virtualenvs-create: true | |
| virtualenvs-in-project: true | |
| - name: Cache Poetry dependencies | |
| uses: actions/cache@v4 | |
| with: | |
| path: .venv | |
| key: ${{ runner.os }}-poetry-${{ hashFiles('poetry.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-poetry- | |
| - name: Install dependencies | |
| run: poetry install --with dev,test | |
| - name: Run Ruff | |
| if: matrix.check == 'ruff' | |
| id: ruff | |
| run: | | |
| set +e | |
| poetry run ruff check . 2>&1 | tee ruff_output.txt | |
| EXIT_CODE=${PIPESTATUS[0]} | |
| echo "exit_code=$EXIT_CODE" >> "$GITHUB_OUTPUT" | |
| exit $EXIT_CODE | |
| - name: Run mypy (strict) | |
| if: matrix.check == 'mypy' | |
| id: mypy | |
| run: | | |
| set +e | |
| { | |
| poetry run mypy --install-types --non-interactive --strict -p custom_components.googlefindmy | |
| poetry run mypy --install-types --non-interactive --strict -p tests | |
| } 2>&1 | tee mypy_output.txt | |
| EXIT_CODE=${PIPESTATUS[0]} | |
| echo "exit_code=$EXIT_CODE" >> "$GITHUB_OUTPUT" | |
| exit $EXIT_CODE | |
| - name: Run codespell | |
| if: matrix.check == 'codespell' | |
| id: codespell | |
| continue-on-error: true # Non-blocking - informational only | |
| run: | | |
| set +e | |
| poetry run codespell 2>&1 | tee codespell_output.txt | |
| EXIT_CODE=${PIPESTATUS[0]} | |
| echo "exit_code=$EXIT_CODE" >> "$GITHUB_OUTPUT" | |
| exit $EXIT_CODE | |
| - name: Run bandit (security) | |
| if: matrix.check == 'bandit' | |
| id: bandit | |
| run: | | |
| set +e | |
| # Run bandit security scanner on custom_components and tests | |
| # Exclude known false positives and test files that intentionally test security | |
| poetry run bandit -r custom_components -c pyproject.toml -f txt 2>&1 | tee bandit_output.txt | |
| EXIT_CODE=${PIPESTATUS[0]} | |
| echo "exit_code=$EXIT_CODE" >> "$GITHUB_OUTPUT" | |
| exit $EXIT_CODE | |
| - name: Write lint summary | |
| if: always() | |
| run: | | |
| { | |
| echo "## ${{ matrix.check }} Results" | |
| echo "" | |
| if [ "${{ matrix.check }}" = "ruff" ]; then | |
| if [ -f ruff_output.txt ]; then | |
| ISSUES=$(grep -c "^" ruff_output.txt 2>/dev/null || echo "0") | |
| if [ "$ISSUES" = "0" ] || [ ! -s ruff_output.txt ]; then | |
| echo "✅ **No issues found**" | |
| else | |
| echo "❌ **Issues found:**" | |
| echo "" | |
| echo '```' | |
| head -100 ruff_output.txt | |
| echo '```' | |
| fi | |
| fi | |
| elif [ "${{ matrix.check }}" = "mypy" ]; then | |
| if [ -f mypy_output.txt ]; then | |
| ERRORS=$(grep -c "error:" mypy_output.txt 2>/dev/null || echo "0") | |
| if [ "$ERRORS" = "0" ]; then | |
| echo "✅ **No type errors found**" | |
| else | |
| echo "❌ **$ERRORS type error(s) found:**" | |
| echo "" | |
| echo '```' | |
| head -100 mypy_output.txt | |
| echo '```' | |
| fi | |
| fi | |
| elif [ "${{ matrix.check }}" = "codespell" ]; then | |
| if [ -f codespell_output.txt ]; then | |
| if [ ! -s codespell_output.txt ]; then | |
| echo "✅ **No spelling errors found**" | |
| else | |
| echo "ℹ️ **Spelling suggestions (non-blocking):**" | |
| echo "" | |
| echo "These are informational only. Add false positives to \`pyproject.toml\` ignore-words-list." | |
| echo "" | |
| echo '```' | |
| cat codespell_output.txt | |
| echo '```' | |
| fi | |
| fi | |
| elif [ "${{ matrix.check }}" = "bandit" ]; then | |
| if [ -f bandit_output.txt ]; then | |
| ISSUES=$(grep -c "Issue:" bandit_output.txt 2>/dev/null || echo "0") | |
| if [ "$ISSUES" = "0" ]; then | |
| echo "✅ **No security issues found**" | |
| else | |
| echo "⚠️ **$ISSUES potential security issue(s) found:**" | |
| echo "" | |
| echo '```' | |
| cat bandit_output.txt | |
| echo '```' | |
| fi | |
| fi | |
| fi | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| test: | |
| name: Test (Python ${{ matrix.python-version }}) | |
| runs-on: ubuntu-latest | |
| needs: | |
| - hassfest | |
| - hacs | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| python-version: ["3.13"] | |
| # Can add more Python versions when available: | |
| # python-version: ["3.13", "3.14"] | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v4 | |
| with: | |
| repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }} | |
| ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref }} | |
| - name: Set up Python ${{ matrix.python-version }} | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| - name: Install Poetry | |
| uses: snok/install-poetry@v1 | |
| with: | |
| version: latest | |
| virtualenvs-create: true | |
| virtualenvs-in-project: true | |
| - name: Cache Poetry dependencies | |
| uses: actions/cache@v4 | |
| with: | |
| path: .venv | |
| key: ${{ runner.os }}-py${{ matrix.python-version }}-poetry-${{ hashFiles('poetry.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-py${{ matrix.python-version }}-poetry- | |
| - name: Cache pre-commit environments | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.cache/pre-commit | |
| key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} | |
| restore-keys: | | |
| ${{ runner.os }}-pre-commit- | |
| - name: Install system dependencies | |
| run: sudo apt-get update && sudo apt-get install -y libc-ares-dev | |
| - name: Install dependencies | |
| run: poetry install --with dev,test | |
| - name: Run pytest with coverage | |
| env: | |
| PYTHONPATH: . | |
| run: | | |
| set -o pipefail | |
| poetry run pytest -q --cov --cov-report=xml --cov-report=term 2>&1 | tee pytest_output.log | |
| - name: Upload coverage to Codecov | |
| uses: codecov/codecov-action@v4 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| files: ./coverage.xml | |
| flags: unittests | |
| name: codecov-py${{ matrix.python-version }} | |
| fail_ci_if_error: false | |
| verbose: true | |
| - name: Write short summary to GitHub Step Summary | |
| if: always() | |
| run: | | |
| { | |
| echo "## Pytest summary (Python ${{ matrix.python-version }})" | |
| echo | |
| echo '```' | |
| tail -n 200 pytest_output.log | |
| echo '```' | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| # Summary job to require all tests pass | |
| ci-success: | |
| name: CI Success | |
| runs-on: ubuntu-latest | |
| needs: [hassfest, hacs, lint, test] | |
| if: always() | |
| steps: | |
| - name: Check all jobs passed | |
| id: check | |
| run: | | |
| if [[ "${{ needs.hassfest.result }}" != "success" ]] || \ | |
| [[ "${{ needs.hacs.result }}" != "success" ]] || \ | |
| [[ "${{ needs.lint.result }}" != "success" ]] || \ | |
| [[ "${{ needs.test.result }}" != "success" ]]; then | |
| echo "One or more jobs failed" | |
| echo "passed=false" >> "$GITHUB_OUTPUT" | |
| exit 1 | |
| fi | |
| echo "All CI jobs passed!" | |
| echo "passed=true" >> "$GITHUB_OUTPUT" | |
| - name: Write CI summary | |
| if: always() | |
| run: | | |
| { | |
| echo "# CI Summary" | |
| echo "" | |
| echo "| Job | Status |" | |
| echo "|-----|--------|" | |
| if [ "${{ needs.hassfest.result }}" = "success" ]; then | |
| echo "| Hassfest | ✅ Pass |" | |
| else | |
| echo "| Hassfest | ❌ Fail |" | |
| fi | |
| if [ "${{ needs.hacs.result }}" = "success" ]; then | |
| echo "| HACS | ✅ Pass |" | |
| else | |
| echo "| HACS | ❌ Fail |" | |
| fi | |
| if [ "${{ needs.lint.result }}" = "success" ]; then | |
| echo "| Lint (ruff, mypy, codespell, bandit) | ✅ Pass |" | |
| else | |
| echo "| Lint (ruff, mypy, codespell, bandit) | ❌ Fail |" | |
| fi | |
| if [ "${{ needs.test.result }}" = "success" ]; then | |
| echo "| Tests | ✅ Pass |" | |
| else | |
| echo "| Tests | ❌ Fail |" | |
| fi | |
| echo "" | |
| if [ "${{ steps.check.outputs.passed }}" = "true" ]; then | |
| echo "## ✅ All checks passed!" | |
| else | |
| echo "## ❌ Some checks failed" | |
| echo "" | |
| echo "Please review the failed jobs above." | |
| fi | |
| } >> "$GITHUB_STEP_SUMMARY" |