From bcb0bc2c8618343be5d256a0a06442af31025bc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Sp=C3=A1=C4=8Dil?= Date: Tue, 1 Sep 2026 23:43:20 +0200 Subject: [PATCH 1/3] =?UTF-8?q?chore:=20konfigurace=20lint=C5=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bez explicitní konfigurace se výsledek lintů mění sám od sebe: výchozí sada pravidel ruffu se liší verzi od verze a flake8 hlásí generovaný resources.py, který se ručně neformátuje. Konfigurace je rozdělená schválně. amcr_viewer/.flake8 leží vedle metadata.txt, protože scanner na plugins.qgis.org hledá config soubory jen v kořeni balíčku uvnitř ZIPu; stejná pravidla tak platí v CI, lokálně i při uploadu. Konfigurace ruffu je v kořenovém pyproject.toml, ruff se do balíčku pluginu nedistribuuje. Ignorovaná pravidla mají v pyproject.toml odůvodnění: UP009 (hlavička utf-8 je konvence šablony Plugin Builderu), BLE001 (except Exception je záměr, výjimka nesmí propadnout do QGISu), SIM103 a SIM105 (čitelnost). Dvě opravy, které z konfigurace plynou: - open(path, 'r', encoding=...) -> open(path, encoding=...) [UP015] - zbytečné else: po return v get_komponenty() [RET505] Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01WhQyc1uQFcpkezLwM8if6T --- amcr_viewer/.flake8 | 12 ++++++++++ amcr_viewer/amcr_codelists.py | 2 +- amcr_viewer/amcr_dialog.py | 3 +-- pyproject.toml | 44 +++++++++++++++++++++++++++++++++++ 4 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 amcr_viewer/.flake8 create mode 100644 pyproject.toml diff --git a/amcr_viewer/.flake8 b/amcr_viewer/.flake8 new file mode 100644 index 0000000..4ea6cc1 --- /dev/null +++ b/amcr_viewer/.flake8 @@ -0,0 +1,12 @@ +# Konfigurace flake8 pro plugin AMČR Viewer. +# +# Soubor leží vedle metadata.txt schválně: scanner na plugins.qgis.org +# hledá .flake8 pouze v kořeni balíčku uvnitř ZIPu, takže stejná pravidla +# platí v CI, lokálně i při uploadu. +# https://plugins.qgis.org/docs/security-scanning/config-files +[flake8] +# resources.py je vygenerovaný výstup pyrcc ("All changes made in this +# file will be lost"), není nikde importovaný a zdrojový .qrc v repu není. +# Ručně se neformátuje. +per-file-ignores = + *resources.py: E302,E305,E501 diff --git a/amcr_viewer/amcr_codelists.py b/amcr_viewer/amcr_codelists.py index c409359..8646b38 100644 --- a/amcr_viewer/amcr_codelists.py +++ b/amcr_viewer/amcr_codelists.py @@ -65,7 +65,7 @@ def parse_codelist_file(filename, target_dict=None): try: # Open the file using standard UTF-8 encoding - with open(path, 'r', encoding='utf-8') as f: + with open(path, encoding='utf-8') as f: reader = csv.reader(f, delimiter=';') # Skip the CSV header row diff --git a/amcr_viewer/amcr_dialog.py b/amcr_viewer/amcr_dialog.py index 3cd69b9..1fac5f7 100644 --- a/amcr_viewer/amcr_dialog.py +++ b/amcr_viewer/amcr_dialog.py @@ -659,8 +659,7 @@ class AmcrFilterDialog(QDialog): def get_komponenty(self): if self.typ_dat in ["akce", "lokalita"]: return "true" if self.chk_komponenty.isChecked() else "false" - else: - return "false" + return "false" def get_filters(self): """Compiles the user selections from the cache into diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d60d6e9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,44 @@ +# Konfigurace lintů pro tento repozitář. +# +# Plugin se nedistribuuje jako Python balíček (do QGISu jde ZIP složky +# amcr_viewer/), takže tenhle soubor nic nebalí ani neinstaluje – slouží +# jen k tomu, aby ruff choval stejně v CI, lokálně i za rok. Bez explicitní +# konfigurace se výchozí sada pravidel mezi verzemi ruffu mění. +# +# Konfigurace flake8 je záměrně jinde: v amcr_viewer/.flake8, protože ji +# musí najít i scanner na plugins.qgis.org. + +[tool.ruff] +line-length = 79 +# QGIS 3.44 běží na Pythonu 3.9 a novějším +target-version = "py39" +# Generovaný výstup pyrcc, "All changes made in this file will be lost" +extend-exclude = ["amcr_viewer/resources.py"] + +[tool.ruff.lint] +select = [ + "E", # pycodestyle – chyby + "W", # pycodestyle – varování + "F", # pyflakes + "I", # pořadí importů + "UP", # zastaralé konstrukce + "B", # bugbear + "C4", # comprehensions + "SIM", # zjednodušení + "RET", # návratové hodnoty + "BLE", # holé except +] +ignore = [ + # Hlavička "# -*- coding: utf-8 -*-" je konvence šablony Plugin + # Builderu a drží se v celém projektu jednotně. + "UP009", + # "except Exception" je v pluginu záměr: výjimka nesmí propadnout do + # QGISu, chyba se uživateli ukáže v liště zpráv. + "BLE001", + # Obě dotčená místa mají ke každé větvi vysvětlující komentář, + # sloučením do jednoho výrazu by se čitelnost zhoršila. + "SIM103", + # contextlib.suppress() by kvůli jednomu místu přidal import a odsunul + # komentář, který vysvětluje, proč tam ta výjimka je. + "SIM105", +] From f4b1c39e6ca864c83b5d7f4c281249f38c32f9ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Sp=C3=A1=C4=8Dil?= Date: Tue, 1 Sep 2026 23:43:33 +0200 Subject: [PATCH 2/3] =?UTF-8?q?test:=20kontrola=20zdroj=C3=A1k=C5=AF=20a?= =?UTF-8?q?=20smoke=20test=20v=20QGIS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tests/check_sources.py hlídá pravidla, která nepotřebují QGIS a jejichž porušení je tiché: - UTF-8 BOM na začátku .py souboru. Pythonu ani QGISu nevadí, ale oficiální pyqgis4-checker na něm spadne a soubor vůbec nezkontroluje, takže rozbitý soubor vypadá čistě. Stalo se tady už dvakrát. - přímý import z PyQt5/PyQt6 mimo shim qgis.PyQt - spustitelná práva, skryté soubory a podezřelé typy souborů, které hlásí analýza souborů na plugins.qgis.org tests/smoke_test.py načte plugin ve skutečném QGIS, přečte scoped enumy, vytvoří UpdateCodelistsTask a všechny tři filtrační dialogy a ověří doplňování hraničního data. Běží offline a bez X serveru (QT_QPA_PLATFORM=offscreen), takže nezávisí na dostupnosti API. Očekávaná hodnota filtru je v testu napsaná natvrdo, ne přes DATE_OPEN_TO. Porovnání proti konstantě z modulu dokazuje jen to, že se modul shodne sám se sebou, a prošlo i s rozbitým sentinelem. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01WhQyc1uQFcpkezLwM8if6T --- tests/check_sources.py | 85 +++++++++++++++++++++++++++ tests/smoke_test.py | 130 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 215 insertions(+) create mode 100644 tests/check_sources.py create mode 100644 tests/smoke_test.py diff --git a/tests/check_sources.py b/tests/check_sources.py new file mode 100644 index 0000000..fb960a2 --- /dev/null +++ b/tests/check_sources.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +""" +Repository hygiene rules that need no QGIS and therefore run first. + +Each rule guards a mistake that has already happened here at least once, +or one the plugins.qgis.org file analysis reports: + +* a UTF-8 BOM makes the official pyqgis4-checker skip the file entirely, + so a broken file looks clean – it is silent, which is what makes it bad +* a direct PyQt5/PyQt6 import breaks the other Qt version +* an executable or hidden file in the package is reported on upload + +Run it from the repository root: + + python3 tests/check_sources.py +""" + +import os +import re +import stat +import sys + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +BALICEK = os.path.join(ROOT, "amcr_viewer") + +# Files that belong in the plugin package even though the upload scanner +# would otherwise call them hidden +POVOLENE_SKRYTE = {".flake8", ".bandit", ".secrets.baseline"} + +# Extensions that have no business inside a plugin package +PODEZRELE = {".exe", ".dll", ".so", ".dylib", ".sh", ".bat", ".cmd", + ".pyc", ".pyd", ".jar", ".bin"} + +PRIMY_IMPORT = re.compile(r"^\s*(?:from|import)\s+PyQt[56]\b", re.MULTILINE) + +nalezy = [] + + +def zdrojaky(): + for adresar, _, soubory in os.walk(BALICEK): + for soubor in sorted(soubory): + if soubor.endswith(".py"): + yield os.path.join(adresar, soubor) + + +def vsechny_soubory(): + for adresar, _, soubory in os.walk(BALICEK): + for soubor in sorted(soubory): + yield os.path.join(adresar, soubor) + + +def zkratka(cesta): + return os.path.relpath(cesta, ROOT) + + +for cesta in zdrojaky(): + with open(cesta, "rb") as f: + zacatek = f.read(3) + if zacatek == b"\xef\xbb\xbf": + nalezy.append(f"{zkratka(cesta)}: UTF-8 BOM na začátku souboru") + + with open(cesta, encoding="utf-8-sig") as f: + text = f.read() + for shoda in PRIMY_IMPORT.finditer(text): + radek = text[:shoda.start()].count("\n") + 1 + nalezy.append(f"{zkratka(cesta)}:{radek}: přímý import z PyQt5/PyQt6, " + f"použij shim qgis.PyQt") + +for cesta in vsechny_soubory(): + jmeno = os.path.basename(cesta) + rezim = os.stat(cesta).st_mode + if rezim & (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH): + nalezy.append(f"{zkratka(cesta)}: spustitelná práva " + f"({stat.filemode(rezim)})") + if jmeno.startswith(".") and jmeno not in POVOLENE_SKRYTE: + nalezy.append(f"{zkratka(cesta)}: skrytý soubor v balíčku pluginu") + if os.path.splitext(jmeno)[1].lower() in PODEZRELE: + nalezy.append(f"{zkratka(cesta)}: podezřelý typ souboru") + +if nalezy: + print("Nálezy:") + for nalez in nalezy: + print(f" {nalez}") + sys.exit(1) +print("Kontrola zdrojáků: bez nálezů") diff --git a/tests/smoke_test.py b/tests/smoke_test.py new file mode 100644 index 0000000..8327eaf --- /dev/null +++ b/tests/smoke_test.py @@ -0,0 +1,130 @@ +# -*- coding: utf-8 -*- +""" +Smoke test: loads the plugin inside a real QGIS and exercises the parts +that differ between Qt5 and Qt6. + +It is deliberately offline – no request ever leaves the machine, so the +test says nothing about the AMCR API, only about the plugin loading and +its widgets being constructible. + +Run it from the repository root: + + python3 tests/smoke_test.py + +QGIS must be importable (inside the qgis/qgis Docker image it already is). +The exit code is 0 when everything passed, 1 otherwise. +""" + +import os +import sys +import traceback + +# Offscreen, otherwise the dialogs need an X server +os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, ROOT) + +selhani = [] + + +def zkouska(nazev, funkce): + """Runs one check and keeps going even when it raises.""" + try: + detail = funkce() + except Exception: + selhani.append(nazev) + print(f" FAIL {nazev}") + print(traceback.format_exc().rstrip()) + else: + print(f" OK {nazev}" + (f" – {detail}" if detail else "")) + + +from qgis.core import ( # noqa: E402 + Qgis, + QgsApplication, + QgsTask, + QgsWkbTypes, +) +from qgis.PyQt import QtCore # noqa: E402 +from qgis.PyQt.QtCore import QDate # noqa: E402 + +print(f"QGIS {Qgis.QGIS_VERSION.split('-')[0]} | Qt {QtCore.QT_VERSION_STR} " + f"| PyQt {QtCore.PYQT_VERSION_STR}") + +QgsApplication.setPrefixPath(os.environ.get("QGIS_PREFIX_PATH", "/usr"), True) +qgs = QgsApplication([], True) +qgs.initQgis() + +import amcr_viewer.amcr_codelists # noqa: E402,F401 +import amcr_viewer.amcr_dialog as dialog # noqa: E402 +import amcr_viewer.amcr_tools # noqa: E402,F401 +import amcr_viewer.amcr_viewer # noqa: E402,F401 + +print(" OK import všech modulů pluginu") + + +def enumy(): + """ + The scoped enum forms must exist. Unscoped aliases still resolve in + QGIS 4.2, so a plain import proves nothing – these are read explicitly. + """ + return (f"QgsTask.Flag.CanCancel={int(QgsTask.Flag.CanCancel)}, " + f"PointGeometry={int(QgsWkbTypes.GeometryType.PointGeometry)}, " + f"MessageLevel.Info={int(Qgis.MessageLevel.Info)}") + + +def uloha(): + ukol = dialog.UpdateCodelistsTask("smoke") + assert ukol.canCancel() is True + return "canCancel=True" + + +def dialogy(): + # A modal warning would block the offscreen run forever + dialog.QMessageBox.warning = staticmethod(lambda *a, **k: None) + popis = [] + for typ in ("akce", "lokalita", "samostatny_nalez"): + okno = dialog.AmcrFilterDialog(typ) + okno.show() + QgsApplication.processEvents() + popis.append(f"{typ}: {len(okno.date_ranges)} rozmezí") + okno.close() + return ", ".join(popis) + + +def filtr_datumu(): + """ + A half-filled range must be completed with the sentinel. The API + rejects a one-sided range, so this is the part worth guarding. + + The expected value is written out on purpose – comparing against + dialog.DATE_OPEN_TO would only prove the module agrees with itself. + """ + okno = dialog.AmcrFilterDialog("samostatny_nalez") + pole, _, od, _do = okno.date_ranges[0] + od.setDate(QDate(2016, 1, 1)) + hodnota = okno.get_filters()[pole] + assert hodnota == "2016-01-01,9999-12-31", hodnota + + # A range left completely empty must add no filter at all + prazdne = dialog.AmcrFilterDialog("samostatny_nalez") + pole_prazdne = prazdne.date_ranges[0][0] + assert pole_prazdne not in prazdne.get_filters() + prazdne.close() + + okno.close() + return hodnota + + +zkouska("scoped enumy", enumy) +zkouska("UpdateCodelistsTask", uloha) +zkouska("filtrační dialogy", dialogy) +zkouska("filtr podle data", filtr_datumu) + +qgs.exitQgis() + +if selhani: + print(f"\nNEPROŠLO: {', '.join(selhani)}") + sys.exit(1) +print("\nVše prošlo") From 3591ac4c53187863419c9937369b8bbd37f443b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Sp=C3=A1=C4=8Dil?= Date: Tue, 1 Sep 2026 23:43:53 +0200 Subject: [PATCH 3/3] =?UTF-8?q?ci:=20kontrola=20kvality=20k=C3=B3du=20p?= =?UTF-8?q?=C5=99i=20ka=C5=BEd=C3=A9m=20PR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Workflow code_quality.yml pouští při každém PR a při pushi do main kontroly, které se dosud dělaly ručně, ve čtyřech jobech: - Lint a bezpečnost: check_sources.py, bandit, detect-secrets, flake8, ruff. Bandit a detect-secrets jsou tytéž kontroly, které blokují schválení na plugins.qgis.org. - Kompatibilita s Qt6: pyqgis4-checker v dockeru. Skript končí kódem 0, i když něco najde, výsledek je jen v logu; job proto ověřuje, že log obsahuje pouze hlavičku. - Smoke test: smoke_test.py v qgis/qgis:ltr (3.44, Qt5) i qgis/qgis:stable (4.x, Qt6). Tagy se posouvají schválně, aby bylo vidět, že plugin drží krok s aktuálním QGISem. - Balíček pluginu: sestaví amcr_viewer.zip stejně jako release workflow, ověří, že v něm je metadata.txt, __init__.py i .flake8 a nejsou v něm git soubory, a přiloží ho jako artefakt běhu. Recenzent ho nainstaluje přes Install from ZIP bez ručního balení. detect-secrets se pouští s --all-files. Bez toho prohledá jen soubory sledované gitem a o nesledovaném souboru mlčí, což vypadá jako čistý výsledek. Verze nástrojů jsou napevno. Bez pinu by se výsledek měnil s každým vydáním ruffu, které rozšíří výchozí sadu pravidel. CodeQL a GitGuardian běží zvlášť, nastavené na úrovni organizace, a schválně se tu neduplikují. AGENTS.md popisuje, jak totéž pustit lokálně, a tři místa, kde kontroly tiše lžou (výstupní kód pyqgis4-checkeru, detect-secrets bez --all-files, umístění config souborů pro scanner). Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01WhQyc1uQFcpkezLwM8if6T --- .github/workflows/code_quality.yml | 171 +++++++++++++++++++++++++++++ AGENTS.md | 56 +++++++++- 2 files changed, 225 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/code_quality.yml diff --git a/.github/workflows/code_quality.yml b/.github/workflows/code_quality.yml new file mode 100644 index 0000000..dfd7e9a --- /dev/null +++ b/.github/workflows/code_quality.yml @@ -0,0 +1,171 @@ +name: Code Quality + +# Pouští při každém PR tytéž kontroly, které se dosud dělaly ručně: +# +# * co spouští plugins.qgis.org při uploadu (bandit, detect-secrets, +# flake8, analýza souborů) – https://plugins.qgis.org/docs/security-scanning +# * oficiální kontrolu kompatibility s Qt6 (pyqgis4-checker) +# * skutečné načtení pluginu v QGIS 3.44 (Qt5) i v QGIS 4 (Qt6) +# * sestavení ZIPu, který si recenzent stáhne a nainstaluje přímo z PR +# +# CodeQL a GitGuardian běží zvlášť, nastavené na úrovni organizace – +# tady se schválně neduplikují. + +on: + pull_request: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + +env: + # Verze se drží napevno, aby se výsledek nezměnil sám od sebe. Výchozí + # sada pravidel ruffu se mezi verzemi mění; povýšení je vědomý krok. + BANDIT: bandit==1.9.4 + DETECT_SECRETS: detect-secrets==1.5.0 + FLAKE8: flake8==7.3.0 + RUFF: ruff==0.16.5 + +jobs: + # -------------------------------------------------------------------- + # 1. Statické kontroly – běží první, protože trvají desítky sekund + # -------------------------------------------------------------------- + lint: + name: Lint a bezpečnost + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: '3.12' + + - name: Install tools + run: pip install "$BANDIT" "$DETECT_SECRETS" "$FLAKE8" "$RUFF" + + # Hygiena repozitáře: BOM, přímé importy z PyQt5/PyQt6, spustitelná + # práva a podezřelé typy souborů. Padá jako první, protože BOM + # zneviditelní soubor pro kontrolu níž. + - name: Source hygiene + run: python3 tests/check_sources.py + + # Blokující kontrola na plugins.qgis.org + - name: Bandit + run: bandit -r amcr_viewer/ + + # Blokující kontrola na plugins.qgis.org. + # --all-files je podstatné: bez něj detect-secrets prohledá jen + # soubory sledované gitem a nesledovaný soubor tiše přeskočí. + - name: detect-secrets + run: | + detect-secrets scan --all-files amcr_viewer/ > vysledek.json + python3 -c " + import json, sys + nalezy = json.load(open('vysledek.json'))['results'] + if nalezy: + print(json.dumps(nalezy, indent=2)) + sys.exit(1) + print('detect-secrets: bez nálezů') + " + + # Na plugins.qgis.org je informativní, tady blokuje – konfigurace + # v amcr_viewer/.flake8 je stejná pro obě místa. + - name: Flake8 + run: flake8 --config amcr_viewer/.flake8 amcr_viewer/ + + # Nad rámec plugins.qgis.org; konfigurace v pyproject.toml + - name: Ruff + run: ruff check . + + # -------------------------------------------------------------------- + # 2. Kompatibilita s Qt6 – oficiální skript z QGISu + # -------------------------------------------------------------------- + qt6: + name: Kompatibilita s Qt6 + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + # Pozor: skript končí kódem 0 i když něco najde, výsledek je jen + # v logu. Prázdný log (samotná hlavička) znamená čisto. + - name: pyqgis4-checker + run: | + docker run --rm --user "$(id -u):$(id -g)" \ + --workdir /workspace/ -v "$PWD:/workspace/" \ + ghcr.io/qgis/pyqgis4-checker:main-ubuntu \ + pyqt5_to_pyqt6.py --dry_run --logfile /workspace/pyqt6_checker.log . + echo "--- pyqt6_checker.log ---" + cat pyqt6_checker.log + nalezu=$(grep -v '=== dry_run mode | Start Logs ===' pyqt6_checker.log | wc -l) + if [ "$nalezu" -ne 0 ]; then + echo "::error::pyqgis4-checker nahlásil nálezy, viz log výše" + exit 1 + fi + + # -------------------------------------------------------------------- + # 3. Načtení pluginu ve skutečném QGIS, v obou podporovaných verzích + # -------------------------------------------------------------------- + qgis: + name: Smoke test (QGIS ${{ matrix.qgis }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + # ltr = 3.44 na Qt5, stable = 4.x na Qt6. Tagy se posouvají + # schválně: chceme vědět, že plugin drží krok s aktuálním QGISem. + qgis: [ltr, stable] + + steps: + - name: Checkout code + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - name: Smoke test + run: | + docker run --rm -v "$PWD:/work:ro" -w /work \ + --user "$(id -u):$(id -g)" -e HOME=/tmp \ + "qgis/qgis:${{ matrix.qgis }}" python3 tests/smoke_test.py + + # -------------------------------------------------------------------- + # 4. ZIP k instalaci – stejný postup jako v release_plugin.yml + # -------------------------------------------------------------------- + package: + name: Balíček pluginu + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - name: Zip plugin + run: zip -r amcr_viewer.zip amcr_viewer -x "*.git*" + + # Kontrola obsahu ZIPu. Config soubory pro scanner musí být uvnitř + # vedle metadata.txt, jinak je plugins.qgis.org nenajde – a některé + # nástroje skryté soubory tiše vynechávají. + - name: Verify archive contents + run: | + unzip -l amcr_viewer.zip + for soubor in amcr_viewer/metadata.txt amcr_viewer/__init__.py \ + amcr_viewer/.flake8; do + unzip -l amcr_viewer.zip | grep -qF " $soubor" \ + || { echo "::error::v ZIPu chybí $soubor"; exit 1; } + done + if unzip -l amcr_viewer.zip | grep -qE '\.git'; then + echo "::error::v ZIPu jsou git soubory" + exit 1 + fi + + - name: Upload artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: amcr_viewer-plugin + path: amcr_viewer.zip + if-no-files-found: error diff --git a/AGENTS.md b/AGENTS.md index b6107b8..fa12dcd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -176,5 +176,57 @@ flatpak run --command=sh org.qgis.qgis -c \ Plugin se testuje načtením do QGIS (Plugins → Manage and Install Plugins → Install from ZIP, nebo nasazením složky `amcr_viewer/` do adresáře pluginů -QGIS). Automatizované testy zatím repozitář neobsahuje – změny ověřuj ručně -v QGIS na podporované verzi. +QGIS). **Ruční test v QGIS nic nenahrazuje** – automatické kontroly ověřují, +že se plugin načte a že projde kontrolami kvality, ne že dělá správnou věc. + +### Automatické kontroly + +Workflow `.github/workflows/code_quality.yml` pouští při každém PR do `main` +tohle: + +| job | co dělá | +|---|---| +| **Lint a bezpečnost** | `check_sources.py`, bandit, detect-secrets, flake8, ruff | +| **Kompatibilita s Qt6** | `pyqgis4-checker` v dockeru | +| **Smoke test** | `smoke_test.py` v `qgis/qgis:ltr` i `qgis/qgis:stable` | +| **Balíček pluginu** | sestaví ZIP, ověří obsah, přiloží jako artefakt | + +Smoke test běží v obou podporovaných řadách: `ltr` je QGIS 3.44 na Qt5, +`stable` je QGIS 4.x na Qt6. + +Artefakt z posledního jobu se dá stáhnout ze stránky běhu a rovnou +nainstalovat přes *Install from ZIP* – recenzent nemusí nic balit ručně. + +Totéž lokálně: + +```sh +pip install bandit detect-secrets flake8 ruff + +python3 tests/check_sources.py +bandit -r amcr_viewer/ +detect-secrets scan --all-files amcr_viewer/ +flake8 --config amcr_viewer/.flake8 amcr_viewer/ +ruff check . + +# smoke test v obou verzích QGIS (docker, bez instalace čehokoli) +for tag in ltr stable; do + docker run --rm -v "$PWD:/work:ro" -w /work \ + --user "$(id -u):$(id -g)" -e HOME=/tmp \ + "qgis/qgis:$tag" python3 tests/smoke_test.py +done +``` + +Na co si dát pozor: + +- **`pyqgis4-checker` končí kódem 0, i když něco najde** – výsledek je jen + v logu. Workflow proto kontroluje, že log obsahuje jen hlavičku. +- **`detect-secrets` bez `--all-files` prohledá jen soubory sledované + gitem** a o nesledovaném souboru mlčí. Vypadá to jako čistý výsledek. +- **Konfigurace lintů je rozdělená schválně.** `amcr_viewer/.flake8` leží + vedle `metadata.txt`, protože scanner na plugins.qgis.org hledá config + soubory jen v kořeni balíčku uvnitř ZIPu; díky tomu platí stejná pravidla + v CI, lokálně i při uploadu. Konfigurace ruffu je naopak v kořenovém + `pyproject.toml` – ruff se do balíčku pluginu nedistribuuje. + Viz https://plugins.qgis.org/docs/security-scanning/config-files +- **Verze nástrojů jsou v workflow napevno.** Výchozí sada pravidel ruffu se + mezi verzemi mění, takže bez pinu by CI začalo padat samo od sebe.