name: ChangeDetection.io App Test # Triggers the workflow on push or pull request events on: [push, pull_request] jobs: lint-code: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Lint with Ruff run: | pip install ruff # Check for syntax errors and undefined names, and gettext misuse ruff check . --select E9,F63,F7,F82,INT # Complete check with errors treated as warnings ruff check . --exit-zero - name: Validate OpenAPI spec run: | pip install openapi-spec-validator python3 -c "from openapi_spec_validator import validate_spec; import yaml; validate_spec(yaml.safe_load(open('docs/api-spec.yaml')))" lint-translations: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Check .po files with msgfmt run: | sudo apt-get install -y gettext find changedetectionio/translations -name "*.po" | while read f; do echo "Checking $f" msgfmt --check-format -o /dev/null "$f" done - name: Check translation catalog is up-to-date run: | pip install "$(grep -E '^babel==' requirements.txt)" python setup.py extract_messages python setup.py update_catalog python setup.py compile_catalog # Ignore POT-Creation-Date timestamp lines — they change on every extract_messages run if git diff changedetectionio/translations | grep -v 'POT-Creation-Date' | grep -qE '^[+-][^+-]'; then echo "ERROR: Translation catalog is out of sync. Run: python setup.py extract_messages && python setup.py update_catalog && python setup.py compile_catalog" git diff --stat changedetectionio/translations exit 1 fi lint-template-i18n: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Check for fragmented gettext calls in templates run: | python3 << 'PYEOF' import re, sys from pathlib import Path # Detects adjacent {{ _(...) }} calls on the same line separated only by HTML # tags, whitespace, or non-translating Jinja2 variables — the anti-pattern of # splitting a single sentence across multiple msgids. # See https://github.com/dgtlmoon/changedetection.io/issues/4074 for background. # # The correct fix is to consolidate fragments into one entire-sentence msgid, # injecting dynamic values via %(name)s kwargs — per the GNU gettext manual # sections "Entire sentences" and "No string concatenation". See PR #4076 for # worked examples of each consolidation pattern. # # BASELINE: this limit reflects pre-existing violations present when this check # was introduced. It must only ever go DOWN. Each time you fix a template, lower # the limit by the number of lines fixed so the improvement is locked in. # When the count reaches 0, replace the baseline check with a hard sys.exit(1). BASELINE_LIMIT = 44 FRAGMENT_RE = re.compile( r'\{\{[^{}]*\b_\s*\([^)]*\)[^{}]*\}\}' r'(?:\s*(?:<[^>]+>|\{\{(?![^}]*\b_\s*\()[^}]*\}\})\s*)+' r'\{\{[^{}]*\b_\s*\([^)]*\)[^{}]*\}\}' ) violations = [] for f in sorted(Path('changedetectionio').rglob('*.html')): for lineno, line in enumerate(f.read_text().splitlines(), 1): if FRAGMENT_RE.search(line): violations.append(f"{f}:{lineno}: {line.strip()[:120]}") count = len(violations) print(f"Fragmented i18n calls found: {count} (limit: {BASELINE_LIMIT})") for v in violations: print(v) if count > BASELINE_LIMIT: print(f"\nERROR: {count} fragmented gettext calls exceed the baseline of {BASELINE_LIMIT}.") print("Consolidate adjacent _() calls into a single entire-sentence msgid.") print("See https://github.com/dgtlmoon/changedetection.io/issues/4074 for patterns.") sys.exit(1) PYEOF test-application-3-10: # Only run on push to master (including PR merges) if: github.event_name == 'push' && github.ref == 'refs/heads/master' needs: [lint-code, lint-translations, lint-template-i18n] uses: ./.github/workflows/test-stack-reusable-workflow.yml with: python-version: '3.10' test-application-3-11: # Always run needs: [lint-code, lint-translations, lint-template-i18n] uses: ./.github/workflows/test-stack-reusable-workflow.yml with: python-version: '3.11' test-application-3-12: # Only run on push to master (including PR merges) if: github.event_name == 'push' && github.ref == 'refs/heads/master' needs: [lint-code, lint-translations, lint-template-i18n] uses: ./.github/workflows/test-stack-reusable-workflow.yml with: python-version: '3.12' skip-pypuppeteer: true test-application-3-13: # Only run on push to master (including PR merges) if: github.event_name == 'push' && github.ref == 'refs/heads/master' needs: [lint-code, lint-translations, lint-template-i18n] uses: ./.github/workflows/test-stack-reusable-workflow.yml with: python-version: '3.13' skip-pypuppeteer: true test-application-3-14: #if: github.event_name == 'push' && github.ref == 'refs/heads/master' needs: [lint-code, lint-translations, lint-template-i18n] uses: ./.github/workflows/test-stack-reusable-workflow.yml with: python-version: '3.14' skip-pypuppeteer: false