mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 19:56:53 +00:00
Merge branch 'main' into feature/store-i18n
This commit is contained in:
11 files changed
+119
-7
No files matched your search
@@ -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
|
||||
@@ -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 🐳
|
||||
|
||||
@@ -46,6 +46,9 @@ dev = [
|
||||
"sphinx",
|
||||
"sphinxawesome-theme",
|
||||
]
|
||||
fuzz = [
|
||||
"atheris==3.1.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
infinito = "cli.__main__:main"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
Whitespace-only changes.
@@ -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",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user