mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2026-10-14 07:50:48 +00:00
* ci: pin third party actions to commit SHAs A tag or branch ref is a movable pointer, whoever controls the action repository can repoint it and the next run executes their code with this repo's secrets, here the Docker Hub tokens and the container publish path. The March 2025 tj-actions/changed-files incident (CVE-2025-30066) worked exactly that way. Each pin keeps the resolved tag in a comment and dependabot's weekly github-actions config bumps SHA pins like tags, so they self-maintain. The pypi publish action moves from the floating release/v1 branch to its current release. * ci: declare least-privilege workflow permissions Without a permissions block every job token inherits the repository default scope. Each workflow now declares what it uses. The test and build workflows get read-only tokens, CodeQL keeps its code scanning upload, the container publish job keeps packages write for the ghcr push, and the pypi publish job already carried its own id-token block for trusted publishing, untouched. * ci: pass release metadata to inline scripts through env The two debug steps interpolated the release tag name and the workflow_run branch directly into bash. Those strings are event metadata that can carry shell syntax, and the workflow runs on release events with the publish credentials in reach. Routed through env they arrive as plain data, output unchanged. * ci: stop persisting the checkout token in .git/config actions/checkout writes the job token into .git/config so later steps can push. None of these 21 jobs push with git: the containers are published with registry credentials, PyPI uses trusted publishing, and the test and CodeQL jobs only read the tree. A token left in .git/config is one more credential for a compromised step or a packed artifact to pick up, so each checkout now sets persist-credentials: false.
161 lines
6.8 KiB
YAML
161 lines
6.8 KiB
YAML
name: ChangeDetection.io App Test
|
|
|
|
# Triggers the workflow on push or pull request events
|
|
on: [push, pull_request]
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
lint-code:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
persist-credentials: false
|
|
- 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@v7
|
|
with:
|
|
persist-credentials: false
|
|
- 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: Lint .pot template with dennis
|
|
run: |
|
|
pip install "$(grep -E '^dennis ?>=' requirements-dev.txt)"
|
|
dennis-cmd lint --strict changedetectionio/translations/messages.pot
|
|
- name: Lint .po files with dennis
|
|
run: |
|
|
dennis-cmd lint --strict --excluderules=W302 changedetectionio/translations/*/LC_MESSAGES/messages.po
|
|
# W302 (unchanged) is excluded due to high false-positive rate in this codebase:
|
|
# many msgstrs intentionally match msgid (units like "AI", "LLM", and proper nouns).
|
|
- 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
|
|
- name: Check translation overlay
|
|
# Deliberately after extract_messages above, so overrides are validated against a freshly
|
|
# extracted messages.pot. An overlay entry is keyed on the exact upstream msgid: when a
|
|
# string is reworded upstream the override stops matching and silently reverts to upstream
|
|
# wording. This is the only thing that makes that visible.
|
|
# See changedetectionio/translations_overlay/README.md
|
|
if: hashFiles('changedetectionio/translations_overlay/**/*.po') != ''
|
|
run: |
|
|
find changedetectionio/translations_overlay -name "*.po" | while read f; do
|
|
echo "Checking $f"
|
|
msgfmt --check-format -o /dev/null "$f"
|
|
done
|
|
python changedetectionio/translations_overlay/manage.py check
|
|
|
|
lint-template-i18n:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
persist-credentials: false
|
|
- 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-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
|