Merge branch 'main' into feature/store-i18n

This commit is contained in:
kevinveenbirkenbach committed 2026-10-18 14:00:06 +02:00
commit 415542c754
11 files changed
+119 -7

No files matched your search

+46
View File
@@ -0,0 +1,46 @@
name: "🧬 Fuzz parsers"
run-name: "${{ github.event_name == 'schedule' && '⏰ ' || '' }}🧬 Fuzz parsers"
on:
schedule:
- cron: "0 4 * * 0"
workflow_dispatch:
concurrency:
group: fuzz-${{ github.repository }}
cancel-in-progress: true
permissions:
contents: read
jobs:
fuzz-yaml-bootstrap:
name: 🧬 Fuzz the bootstrap YAML reader
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: ⬇️ Checkout
uses: actions/checkout@v7.0.0
- name: 🔧 Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
cache: pip
cache-dependency-path: pyproject.toml
- name: 📦 Install project with the fuzz extra
env:
PYTHON: python3
run: bash scripts/install/python.sh fuzz
- name: 🧬 Compare load_block against yaml.safe_load
run: python3 tests/fuzz/_yaml_bootstrap.py -max_total_time=900
- name: 📤 Upload crash inputs
if: failure()
uses: actions/upload-artifact@v7
with:
name: fuzz-crashes
path: crash-*
retention-days: 30
+3 -2
View File
@@ -77,8 +77,9 @@ The two deploy-test workflows listed in the `Infrastructure tests` table of [wor
1. **Sentinel `__ALL__` in the `whitelist` input** (case-insensitive). The diff logic MUST be skipped and an empty whitelist MUST be emitted, which deploys everything in the workflow's scope. This is the explicit opt-out from diff narrowing for manual dispatch.
2. **Any other non-empty `whitelist`** (forwarded from `entry-manual-steer.yml` and similar). The explicit value MUST win over the diff and is passed through verbatim.
3. **Diff vs `origin/main`**, applied only when the `whitelist` input is empty:
- **No diff at all** OR **any changed path outside `roles/<role>/...`**: no whitelist is set, so the full deploy across the workflow's scope runs.
- **All changed paths under `roles/<role>/...`**: the changed roles become the seed set, and the whitelist is the transitive closure of those seeds expanded upwards over `run_after`, `dependencies`, and shared services. In other words, every role whose prerequisite set as defined by [combined resolver](../../../../cli/meta/roles/applications/resolution/combined/resolver.py) contains one of the seeds is included, together with the seeds themselves.
- **Skipped paths:** `.md` and `.rst` files and everything under `tests/` outside `roles/<role>/...` neither seed a role nor widen the deploy.
- **No diff at all**, OR **any other changed path outside `roles/<role>/...`**, OR **only skipped paths**: no whitelist is set, so the full deploy across the workflow's scope runs.
- **All remaining changed paths under `roles/<role>/...`**: the changed roles become the seed set, and the whitelist is the transitive closure of those seeds expanded upwards over `run_after`, `dependencies`, and shared services. In other words, every role whose prerequisite set as defined by [combined resolver](../../../../cli/meta/roles/applications/resolution/combined/resolver.py) contains one of the seeds is included, together with the seeds themselves.
Each role in that closure is then narrowed to the variants that actually reach a seed: variant `i` of a role is kept only when round `i`'s closure — walked on the same variant-merged services maps [the matrix planner](../../../../cli/administration/deploy/development/inventory/planner.py) builds its rounds from, and therefore variant-aware at every generation of the chain — contains one of the seeds. A role whose variants do not all qualify enters the whitelist as a `role#0,2` selection token, so an image bump in a provider no longer redeploys the variants that switch that provider off. This applies to every auto-derived whitelist — the weekly update PRs, Dependabot, and any other diff-driven run — because they all resolve through this one path.
@@ -43,6 +43,7 @@ Trigger column legend: **auto** = fires automatically (push, pull_request, sched
| Workflow | Description | Trigger | Inputs |
|---|---|---|---|
| [cron-security-codeql.yml](../../../../../.github/workflows/cron-security-codeql.yml): `🔐 Scan with CodeQL` | CodeQL static analysis. Invoked by [call-orchestrator.yml](../../../../../.github/workflows/call-orchestrator.yml) so every CI run produces a single scan; additionally runs on a weekly cron so coverage does not drop when no pushes land on `main`. See [pipeline.md](../../../artefact/git/pipeline.md) for the gating behaviour. | reusable, auto (`schedule`: weekly Mon 00:00 UTC) | none |
| [cron-security-fuzz.yml](../../../../../.github/workflows/cron-security-fuzz.yml): `🧬 Fuzz parsers` | Runs the [atheris](https://github.com/google/atheris) harness [_yaml_bootstrap.py](../../../../../tests/fuzz/_yaml_bootstrap.py) for 15 minutes and uploads every crashing input as the `fuzz-crashes` artifact. Reproduce one locally with `pip install -e ".[fuzz]"` and `python3 tests/fuzz/_yaml_bootstrap.py <crash-file>`. | auto (`schedule`: weekly Sun 04:00 UTC), manual | none |
| [call-lint.yml](../../../../../.github/workflows/call-lint.yml): `🧹 Lint` | Runs `make lint` (every lint check in parallel: action, ansible, javascript, makefile, markdown, mermaid, packages, playwright, python, shellcheck) plus a hadolint SARIF job on `Dockerfile`. | reusable, manual | none |
### CI images 🐳
+3
View File
@@ -46,6 +46,9 @@ dev = [
"sphinx",
"sphinxawesome-theme",
]
fuzz = [
"atheris==3.1.0",
]
[project.scripts]
infinito = "cli.__main__:main"
+6 -5
View File
@@ -8,11 +8,12 @@
# __ALL__ full deploy needed. Emitted when:
# * the diff is empty, OR
# * any changed path outside
# roles/<role>/ is not a Markdown
# or reStructuredText file (the
# `.md`/`.rst` skip-list mirrors
# roles/<role>/ is neither a
# Markdown or reStructuredText file
# (the `.md`/`.rst` skip-list mirrors
# scripts/meta/resolve/pr/scope.sh's
# `is_markdown_or_rst`), OR
# `is_markdown_or_rst`) nor under
# tests/, OR
# * the diff contains only such
# skipped paths and no role seeds
# could be derived, OR
@@ -75,7 +76,7 @@ for path in "${changed_paths[@]}"; do
[[ -z "${path}" ]] && continue
if [[ "${path}" != roles/*/* ]]; then
case "${path}" in
*.md | *.rst)
*.md | *.rst | tests/*)
continue
;;
esac
+1
View File
@@ -10,5 +10,6 @@ All automated tests for Infinito.Nexus live here.
| [`e2e/`](e2e/) | End-to-end Playwright tests | [playwright.md](../docs/contributing/actions/testing/playwright.md) |
| [`regression/`](regression/) | Regression guard rules | [regression.md](../docs/contributing/actions/testing/regression.md) |
| [`lint/`](lint/) | Structural lint checks | [lint.md](../docs/contributing/actions/testing/lint.md) |
| [`fuzz/`](fuzz/) | Coverage-guided atheris harnesses, run weekly in CI, excluded from `make test` | [workflows.md](../docs/contributing/tools/github/actions/workflows.md) |
Run each suite via the `make` targets documented in the linked guides.
View File
Whitespace-only changes.
+22
View File
@@ -0,0 +1,22 @@
import sys
import atheris
from utils.cache.yaml import load_yaml_str
with atheris.instrument_imports():
from utils.yaml_bootstrap import BootstrapYamlError, load_block
def fuzz_one_input(data: bytes) -> None:
text = data.decode("utf-8", errors="replace")
try:
parsed = load_block(text)
except BootstrapYamlError:
return
assert parsed == load_yaml_str(text), text
if __name__ == "__main__":
atheris.Setup(sys.argv, fuzz_one_input)
atheris.Fuzz()
@@ -139,6 +139,25 @@ class TestDiffAffectedRoles(unittest.TestCase):
)
self.assertEqual(self._run_script(repo, env), "web-app-foo")
def test_tests_outside_role_are_skipped_when_role_also_changed(self):
repo, env = self._setup_repo(
base_files={ROLE_TASKS_MAIN_REL: "- debug: msg=v1\n"},
feature_files={
ROLE_TASKS_MAIN_REL: "- debug: msg=v2\n",
"tests/unit/python/roles/web-app-foo/test_x.py": "x = 1\n",
"tests/integration/test_y.py": "y = 1\n",
},
fake_resolver_stdout="web-app-foo",
)
self.assertEqual(self._run_script(repo, env), "web-app-foo")
def test_only_tests_outside_role_falls_back_to_all(self):
repo, env = self._setup_repo(
base_files={"tests/unit/test_z.py": "z = 1\n"},
feature_files={"tests/unit/test_z.py": "z = 2\n"},
)
self.assertEqual(self._run_script(repo, env), "__ALL__")
def test_non_md_outside_role_still_triggers_all(self):
repo, env = self._setup_repo(
base_files={ROLE_TASKS_MAIN_REL: "- debug: msg=v1\n"},
@@ -115,6 +115,14 @@ STRUCTURES = (
"a: see this: here\n",
"a:b\n",
"a: # note\n",
"\x1e/: R\n",
"a: b\x0bc: d\n",
"# note\x01\na: b\n",
"# note\rc: d\n",
"# note\x85c: d\n",
"# note\N{LINE SEPARATOR}c: d\n",
"a: b\r\nc: d\r\n",
" ---\nC: L\n",
)
+10
View File
@@ -24,6 +24,9 @@ import re
_PLAIN = re.compile(r"^[A-Za-z_/][A-Za-z0-9_./+@:\- ]*$")
_RESERVED = re.compile(r"^(?:true|false|yes|no|on|off|y|n|null)$", re.IGNORECASE)
_NON_PRINTABLE = re.compile(
"[^\x09\x0a\x0d\x20-\x7e\x85\xa0-\U0000d7ff\U0000e000-\U0000fffd\U00010000-\U0010ffff]"
)
class BootstrapYamlError(ValueError):
@@ -64,6 +67,11 @@ def load_block(text: str) -> dict:
Raises:
BootstrapYamlError: for any construct outside the subset.
"""
non_printable = _NON_PRINTABLE.search(text)
if non_printable:
raise BootstrapYamlError(
f"non-printable character {non_printable.group()!r} at offset {non_printable.start()}"
)
root: dict = {}
stack: list[tuple[int, object]] = [(0, root)]
pending: tuple[dict, str, int] | None = None
@@ -76,6 +84,8 @@ def load_block(text: str) -> dict:
if body == "---":
if lineno != 1:
raise BootstrapYamlError(f"line {lineno}: second document")
if line.startswith(" "):
raise BootstrapYamlError(f"line {lineno}: indented document marker")
continue
indent = len(line) - len(line.lstrip(" "))
if pending is not None: