From fdf3e2bd16cc5a3e83456e38b1b5bcca5ab852cd Mon Sep 17 00:00:00 2001
From: JSONbored <49853598+JSONbored@users.noreply.github.com>
Date: Fri, 24 Apr 2026 14:05:34 -0600
Subject: [PATCH 1/4] ci(build): consolidate pytest workflow steps
---
.github/actions/run-pytest/action.yml | 51 +++++++++++++
.github/workflows/build.yml | 90 +++++++----------------
tests/unit/test_workflow_ci_efficiency.py | 40 ++++++++++
3 files changed, 117 insertions(+), 64 deletions(-)
create mode 100644 .github/actions/run-pytest/action.yml
create mode 100644 tests/unit/test_workflow_ci_efficiency.py
diff --git a/.github/actions/run-pytest/action.yml b/.github/actions/run-pytest/action.yml
new file mode 100644
index 0000000..02d7f47
--- /dev/null
+++ b/.github/actions/run-pytest/action.yml
@@ -0,0 +1,51 @@
+name: Run pytest with Trunk upload
+description: Run a pytest target with JUnit output and optional Trunk Flaky Tests upload.
+
+inputs:
+ junit-path:
+ description: Path to write the JUnit XML report.
+ required: true
+ pytest-args:
+ description: Arguments passed to pytest.
+ required: true
+ trunk-api-token:
+ description: Trunk API token. Leave empty to skip upload.
+ required: false
+ default: ""
+ trunk-org-slug:
+ description: Trunk organization URL slug.
+ required: true
+
+runs:
+ using: composite
+ steps:
+ - name: Run pytest
+ id: pytest
+ shell: bash
+ run: |
+ mkdir -p "$(dirname "${{ inputs.junit-path }}")"
+ rm -f "${{ inputs.junit-path }}"
+ set +e
+ python -m pytest \
+ ${{ inputs.pytest-args }} \
+ --junit-xml="${{ inputs.junit-path }}" \
+ -o junit_family=xunit1
+ status=$?
+ echo "exit_code=${status}" >> "${GITHUB_OUTPUT}"
+ exit 0
+
+ - name: Upload test results to Trunk
+ if: ${{ always() && inputs.trunk-api-token != '' }}
+ continue-on-error: true
+ uses: trunk-io/analytics-uploader@95a0fb8b29e45b6068304261fb518644b426a803 # v2.0.8
+ env:
+ TRUNK_API_TOKEN: ${{ inputs.trunk-api-token }}
+ with:
+ junit-paths: ${{ inputs.junit-path }}
+ org-slug: ${{ inputs.trunk-org-slug }}
+ token: ${{ inputs.trunk-api-token }}
+
+ - name: Fail if pytest failed
+ if: ${{ steps.pytest.outputs.exit_code != '0' }}
+ shell: bash
+ run: exit 1
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 3340c5b..8eab48e 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -13,6 +13,7 @@ on:
- scripts/**
- tests/**
- .trunk/**
+ - .github/actions/**
- upstream.toml
- sure-aio.xml
- renovate.json
@@ -29,6 +30,7 @@ on:
- scripts/**
- tests/**
- .trunk/**
+ - .github/actions/**
- upstream.toml
- sure-aio.xml
- renovate.json
@@ -41,6 +43,7 @@ env:
IMAGE_NAME: jsonbored/sure-aio
PYTHON_VERSION: "3.13"
TRUNK_ORG_URL_SLUG: aethereal
+ DOCKER_CACHE_SCOPE: sure-aio-image
permissions:
contents: read
@@ -151,7 +154,7 @@ jobs:
CHANGELOG.md|cliff.toml|scripts/*|.trunk/*)
tooling_related=true
;;
- .github/workflows/*)
+ .github/actions/*|.github/workflows/*)
tooling_related=true
workflow_related=true
;;
@@ -216,12 +219,15 @@ jobs:
import re
import sys
- workflow_dir = pathlib.Path(".github/workflows")
+ workflow_paths = [
+ *pathlib.Path(".github/workflows").glob("*.yml"),
+ *pathlib.Path(".github/actions").glob("*/action.yml"),
+ ]
pattern = re.compile(r"^\s*uses:\s*([^@\s]+)@([^\s#]+)")
sha_pattern = re.compile(r"^[0-9a-f]{40}$")
failures = []
- for path in sorted(workflow_dir.glob("*.yml")):
+ for path in sorted(workflow_paths):
for lineno, line in enumerate(path.read_text().splitlines(), start=1):
match = pattern.match(line)
if not match:
@@ -271,35 +277,13 @@ jobs:
- name: Install pytest
run: python -m pip install -r requirements-dev.txt
- - name: Run pytest unit tests
- id: unit_pytest
- run: |
- mkdir -p reports
- rm -f reports/pytest-unit.xml
- set +e
- python -m pytest \
- tests/unit \
- tests/template \
- --junit-xml=reports/pytest-unit.xml \
- -o junit_family=xunit1
- status=$?
- echo "exit_code=${status}" >> "${GITHUB_OUTPUT}"
- exit 0
-
- - name: Upload unit test results to Trunk
- if: ${{ always() && env.TRUNK_API_TOKEN != '' }}
- continue-on-error: true
- uses: trunk-io/analytics-uploader@95a0fb8b29e45b6068304261fb518644b426a803 # v2.0.8
- env:
- TRUNK_API_TOKEN: ${{ secrets.TRUNK_API_TOKEN }}
+ - name: Run unit and template tests
+ uses: ./.github/actions/run-pytest
with:
- junit-paths: reports/pytest-unit.xml
- org-slug: ${{ env.TRUNK_ORG_URL_SLUG }}
- token: ${{ env.TRUNK_API_TOKEN }}
-
- - name: Fail if unit tests failed
- if: ${{ steps.unit_pytest.outputs.exit_code != '0' }}
- run: exit 1
+ junit-path: reports/pytest-unit.xml
+ pytest-args: tests/unit tests/template
+ trunk-api-token: ${{ env.TRUNK_API_TOKEN }}
+ trunk-org-slug: ${{ env.TRUNK_ORG_URL_SLUG }}
integration-tests:
if: ${{ needs.detect-changes.outputs.run_tests_requested == 'true' && (needs.detect-changes.outputs.build_related == 'true' || (github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.detect-changes.outputs.publish_requested == 'true')) }}
@@ -330,51 +314,29 @@ jobs:
platforms: linux/amd64
load: true
tags: sure-aio:pytest
- cache-from: type=gha,scope=sure-aio-pytest
- cache-to: type=gha,mode=max,scope=sure-aio-pytest
+ cache-from: type=gha,scope=${{ env.DOCKER_CACHE_SCOPE }}
+ cache-to: type=gha,mode=max,scope=${{ env.DOCKER_CACHE_SCOPE }}
- name: Install pytest
run: python -m pip install -r requirements-dev.txt
- - name: Run pytest integration tests
- id: integration_pytest
+ - name: Run integration tests
env:
AIO_PYTEST_USE_PREBUILT_IMAGE: "true"
- run: |
- mkdir -p reports
- rm -f reports/pytest-integration.xml
- set +e
- python -m pytest \
- tests/integration \
- -m integration \
- --junit-xml=reports/pytest-integration.xml \
- -o junit_family=xunit1
- status=$?
- echo "exit_code=${status}" >> "${GITHUB_OUTPUT}"
- exit 0
-
- - name: Upload integration test results to Trunk
- if: ${{ always() && env.TRUNK_API_TOKEN != '' }}
- continue-on-error: true
- uses: trunk-io/analytics-uploader@95a0fb8b29e45b6068304261fb518644b426a803 # v2.0.8
- env:
- TRUNK_API_TOKEN: ${{ secrets.TRUNK_API_TOKEN }}
+ uses: ./.github/actions/run-pytest
with:
- junit-paths: reports/pytest-integration.xml
- org-slug: ${{ env.TRUNK_ORG_URL_SLUG }}
- token: ${{ env.TRUNK_API_TOKEN }}
+ junit-path: reports/pytest-integration.xml
+ pytest-args: tests/integration -m integration
+ trunk-api-token: ${{ env.TRUNK_API_TOKEN }}
+ trunk-org-slug: ${{ env.TRUNK_ORG_URL_SLUG }}
- name: Dump Docker diagnostics
- if: ${{ failure() || (steps.integration_pytest.conclusion == 'success' && steps.integration_pytest.outputs.exit_code != '0') }}
+ if: ${{ failure() }}
run: |
docker ps -a
echo
docker images
- - name: Fail if integration tests failed
- if: ${{ steps.integration_pytest.outputs.exit_code != '0' }}
- run: exit 1
-
publish:
if: ${{ (needs.detect-changes.outputs.build_related == 'true' || needs.detect-changes.outputs.xml_related == 'true') && github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.detect-changes.outputs.publish_requested == 'true' && needs.integration-tests.result == 'success' }}
needs:
@@ -506,8 +468,8 @@ jobs:
context: .
platforms: linux/amd64,linux/arm64
push: true
- cache-from: type=gha,scope=sure-aio-publish
- cache-to: type=gha,mode=max,scope=sure-aio-publish
+ cache-from: type=gha,scope=${{ env.DOCKER_CACHE_SCOPE }}
+ cache-to: type=gha,mode=max,scope=${{ env.DOCKER_CACHE_SCOPE }}
tags: ${{ steps.prep.outputs.tags }}
labels: |
org.opencontainers.image.source=https://github.com/JSONbored/sure-aio
diff --git a/tests/unit/test_workflow_ci_efficiency.py b/tests/unit/test_workflow_ci_efficiency.py
new file mode 100644
index 0000000..d6e08ba
--- /dev/null
+++ b/tests/unit/test_workflow_ci_efficiency.py
@@ -0,0 +1,40 @@
+from __future__ import annotations
+
+from pathlib import Path
+
+BUILD_WORKFLOW = Path(".github/workflows/build.yml")
+PYTEST_ACTION = Path(".github/actions/run-pytest/action.yml")
+
+
+def test_pytest_jobs_use_shared_local_action() -> None:
+ workflow = BUILD_WORKFLOW.read_text()
+
+ assert workflow.count("uses: ./.github/actions/run-pytest") == 2 # nosec B101
+ assert "Upload unit test results to Trunk" not in workflow # nosec B101
+ assert "Upload integration test results to Trunk" not in workflow # nosec B101
+ assert "trunk-io/analytics-uploader@" in PYTEST_ACTION.read_text() # nosec B101
+
+
+def test_integration_and_publish_share_docker_cache_scope() -> None:
+ workflow = BUILD_WORKFLOW.read_text()
+
+ assert "DOCKER_CACHE_SCOPE: sure-aio-image" in workflow # nosec B101
+ assert ( # nosec B101
+ workflow.count("cache-from: type=gha,scope=${{ env.DOCKER_CACHE_SCOPE }}") == 2
+ )
+ assert ( # nosec B101
+ workflow.count(
+ "cache-to: type=gha,mode=max,scope=${{ env.DOCKER_CACHE_SCOPE }}"
+ )
+ == 2
+ )
+
+
+def test_local_actions_participate_in_ci_change_detection_and_pin_checks() -> None:
+ workflow = BUILD_WORKFLOW.read_text()
+
+ assert "- .github/actions/**" in workflow # nosec B101
+ assert ".github/actions/*|.github/workflows/*)" in workflow # nosec B101
+ assert (
+ 'pathlib.Path(".github/actions").glob("*/action.yml")' in workflow
+ ) # nosec B101
From aaa773ba7a4228ea972fe01106c67682b548b83d Mon Sep 17 00:00:00 2001
From: JSONbored <49853598+JSONbored@users.noreply.github.com>
Date: Fri, 24 Apr 2026 14:50:36 -0600
Subject: [PATCH 2/4] test(ci): cover action and container contracts
---
.github/workflows/build.yml | 2 +-
Dockerfile | 4 +-
tests/template/test_container_contract.py | 165 ++++++++++++++++++++++
tests/unit/test_workflow_ci_efficiency.py | 2 +-
4 files changed, 170 insertions(+), 3 deletions(-)
create mode 100644 tests/template/test_container_contract.py
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 8eab48e..4981e2b 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -154,7 +154,7 @@ jobs:
CHANGELOG.md|cliff.toml|scripts/*|.trunk/*)
tooling_related=true
;;
- .github/actions/*|.github/workflows/*)
+ .github/actions/*/action.yml|.github/workflows/*)
tooling_related=true
workflow_related=true
;;
diff --git a/Dockerfile b/Dockerfile
index 3e164a8..5e1eb40 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -81,10 +81,12 @@ RUN find /etc/s6-overlay/s6-rc.d -type f \( -name "run" -o -name "up" \) -exec c
# 4. Expose the App Storage
VOLUME ["/rails/storage", "/var/lib/postgresql/data", "/var/lib/redis"]
+EXPOSE 3000
+
ENV SKYLIGHT_ENABLED=false
ENV S6_CMD_WAIT_FOR_SERVICES_MAXTIME=300000
HEALTHCHECK --interval=30s --timeout=10s --start-period=180s --retries=3 \
- CMD curl -f http://localhost:3000/up || exit 1
+ CMD curl -fsS http://localhost:3000/up >/dev/null || exit 1
ENTRYPOINT ["/init"]
diff --git a/tests/template/test_container_contract.py b/tests/template/test_container_contract.py
new file mode 100644
index 0000000..b8a3ee9
--- /dev/null
+++ b/tests/template/test_container_contract.py
@@ -0,0 +1,165 @@
+from __future__ import annotations
+
+import json
+import re
+from pathlib import Path
+
+from defusedxml import ElementTree as ET
+
+ROOT = Path(__file__).resolve().parents[2]
+DOCKERFILE = ROOT / "Dockerfile"
+
+SECRET_KEYWORDS = (
+ "ACCESS_KEY",
+ "API_KEY",
+ "AUTH_TOKEN",
+ "CLIENT_SECRET",
+ "PASSWORD",
+ "PRIVATE_KEY",
+ "SECRET",
+ "TOKEN",
+)
+
+
+def _template_path() -> Path:
+ candidates = sorted(ROOT.glob("*.xml"))
+ assert candidates, "repository must include an Unraid XML template" # nosec B101
+ return candidates[0]
+
+
+def _template_root() -> ET.Element:
+ return ET.parse(_template_path()).getroot()
+
+
+def _dockerfile_text() -> str:
+ return DOCKERFILE.read_text()
+
+
+def _dockerfile_volumes() -> set[str]:
+ volumes: set[str] = set()
+ for match in re.finditer(r"(?m)^VOLUME\s+(\[[^\]]+\])", _dockerfile_text()):
+ volumes.update(json.loads(match.group(1)))
+ return volumes
+
+
+def _exposed_ports() -> set[str]:
+ ports: set[str] = set()
+ for line in _dockerfile_text().splitlines():
+ if not line.startswith("EXPOSE "):
+ continue
+ for token in line.split()[1:]:
+ ports.add(token.split("/", 1)[0])
+ return ports
+
+
+def _arg_defaults() -> dict[str, str]:
+ defaults: dict[str, str] = {}
+ for line in _dockerfile_text().splitlines():
+ if not line.startswith("ARG ") or "=" not in line:
+ continue
+ name, value = line.removeprefix("ARG ").split("=", 1)
+ defaults[name] = value
+ return defaults
+
+
+def _config_elements() -> list[ET.Element]:
+ return list(_template_root().findall("Config"))
+
+
+def test_unraid_metadata_contract_is_complete_and_unprivileged() -> None:
+ root = _template_root()
+
+ assert root.findtext("Privileged") == "false" # nosec B101
+ for tag in (
+ "Name",
+ "Repository",
+ "Support",
+ "Project",
+ "TemplateURL",
+ "Icon",
+ "WebUI",
+ ):
+ value = root.findtext(tag)
+ assert value and value.strip(), f"{tag} must be populated" # nosec B101
+ assert (
+ _config_elements()
+ ), "template must expose configurable settings" # nosec B101
+
+
+def test_secret_like_template_variables_are_masked() -> None:
+ for config in _config_elements():
+ name = config.get("Name") or ""
+ target = config.get("Target") or ""
+ default = config.get("Default") or ""
+ if (
+ target.endswith("_PATH")
+ or target.endswith("_ENABLED")
+ or target.startswith(("MAX_", "MIN_"))
+ or name.upper().endswith(" PATH")
+ or set(default.split("|")) == {"false", "true"}
+ ):
+ continue
+ haystack = " ".join(filter(None, (name, target))).upper()
+ if any(keyword in haystack for keyword in SECRET_KEYWORDS):
+ assert (
+ config.get("Mask") == "true"
+ ), ( # nosec B101
+ f"{config.get('Name') or config.get('Target')} should be masked"
+ )
+
+
+def test_required_appdata_paths_are_declared_as_container_volumes() -> None:
+ volumes = _dockerfile_volumes()
+ assert volumes, "Dockerfile must declare persistent volumes" # nosec B101
+
+ for config in _config_elements():
+ if config.get("Type") != "Path" or config.get("Required") != "true":
+ continue
+ default = config.get("Default") or config.text or ""
+ target = config.get("Target") or ""
+ if not default.startswith("/mnt/user/appdata"):
+ continue
+ assert any(
+ target == volume or target.startswith(f"{volume.rstrip('/')}/")
+ for volume in volumes
+ ), f"{target} must be covered by a Dockerfile VOLUME" # nosec B101
+
+
+def test_template_ports_are_exposed_by_image() -> None:
+ exposed_ports = _exposed_ports()
+ assert exposed_ports, "Dockerfile must expose template ports" # nosec B101
+
+ for config in _config_elements():
+ if config.get("Type") == "Port":
+ assert config.get("Target") in exposed_ports # nosec B101
+
+
+def test_dockerfile_has_runtime_safety_contract() -> None:
+ dockerfile = _dockerfile_text()
+ arg_defaults = _arg_defaults()
+ from_lines = [
+ line.split()[1] for line in dockerfile.splitlines() if line.startswith("FROM ")
+ ]
+
+ assert from_lines, "Dockerfile must declare at least one base image" # nosec B101
+ for image in from_lines:
+ digest_arg = re.search(r"@\$\{([^}]+)\}", image)
+ assert "@sha256:" in image or ( # nosec B101
+ digest_arg
+ and arg_defaults.get(digest_arg.group(1), "").startswith("sha256:")
+ ), f"{image} must be digest-pinned"
+
+ assert "HEALTHCHECK" in dockerfile # nosec B101
+ assert "curl -fsS" in dockerfile # nosec B101
+ assert 'ENTRYPOINT ["/init"]' in dockerfile # nosec B101
+ assert "S6_CMD_WAIT_FOR_SERVICES_MAXTIME" in dockerfile # nosec B101
+
+
+def test_docker_socket_mount_is_advanced_and_documented_when_present() -> None:
+ for config in _config_elements():
+ if config.get("Target") != "/var/run/docker.sock":
+ continue
+ description = (config.get("Description") or "").lower()
+ assert config.get("Display") == "advanced" # nosec B101
+ assert config.get("Required") == "false" # nosec B101
+ assert "socket" in description and "security" in description # nosec B101
diff --git a/tests/unit/test_workflow_ci_efficiency.py b/tests/unit/test_workflow_ci_efficiency.py
index d6e08ba..958f109 100644
--- a/tests/unit/test_workflow_ci_efficiency.py
+++ b/tests/unit/test_workflow_ci_efficiency.py
@@ -34,7 +34,7 @@ def test_local_actions_participate_in_ci_change_detection_and_pin_checks() -> No
workflow = BUILD_WORKFLOW.read_text()
assert "- .github/actions/**" in workflow # nosec B101
- assert ".github/actions/*|.github/workflows/*)" in workflow # nosec B101
+ assert ".github/actions/*/action.yml|.github/workflows/*)" in workflow # nosec B101
assert (
'pathlib.Path(".github/actions").glob("*/action.yml")' in workflow
) # nosec B101
From 253a344a68d8bee6799b8c2007a9108d73f72ee6 Mon Sep 17 00:00:00 2001
From: JSONbored <49853598+JSONbored@users.noreply.github.com>
Date: Fri, 24 Apr 2026 15:02:19 -0600
Subject: [PATCH 3/4] fix(ci): classify local action changes
---
.github/workflows/build.yml | 2 +-
tests/unit/test_workflow_ci_efficiency.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 4981e2b..7109a9a 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -154,7 +154,7 @@ jobs:
CHANGELOG.md|cliff.toml|scripts/*|.trunk/*)
tooling_related=true
;;
- .github/actions/*/action.yml|.github/workflows/*)
+ .github/actions/**|.github/workflows/*)
tooling_related=true
workflow_related=true
;;
diff --git a/tests/unit/test_workflow_ci_efficiency.py b/tests/unit/test_workflow_ci_efficiency.py
index 958f109..af62667 100644
--- a/tests/unit/test_workflow_ci_efficiency.py
+++ b/tests/unit/test_workflow_ci_efficiency.py
@@ -34,7 +34,7 @@ def test_local_actions_participate_in_ci_change_detection_and_pin_checks() -> No
workflow = BUILD_WORKFLOW.read_text()
assert "- .github/actions/**" in workflow # nosec B101
- assert ".github/actions/*/action.yml|.github/workflows/*)" in workflow # nosec B101
+ assert ".github/actions/**|.github/workflows/*)" in workflow # nosec B101
assert (
'pathlib.Path(".github/actions").glob("*/action.yml")' in workflow
) # nosec B101
From 7576a0288eddf58dc8d637325a011408a3e1c620 Mon Sep 17 00:00:00 2001
From: JSONbored <49853598+JSONbored@users.noreply.github.com>
Date: Fri, 24 Apr 2026 18:28:49 -0600
Subject: [PATCH 4/4] fix(template): enforce init fail-fast and category
metadata
---
Dockerfile | 1 +
scripts/validate-template.py | 4 ++--
sure-aio.xml | 2 +-
tests/template/test_container_contract.py | 2 ++
4 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/Dockerfile b/Dockerfile
index 5e1eb40..b3d015e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -85,6 +85,7 @@ EXPOSE 3000
ENV SKYLIGHT_ENABLED=false
ENV S6_CMD_WAIT_FOR_SERVICES_MAXTIME=300000
+ENV S6_BEHAVIOUR_IF_STAGE2_FAILS=2
HEALTHCHECK --interval=30s --timeout=10s --start-period=180s --retries=3 \
CMD curl -fsS http://localhost:3000/up >/dev/null || exit 1
diff --git a/scripts/validate-template.py b/scripts/validate-template.py
index e66db55..6ac557d 100644
--- a/scripts/validate-template.py
+++ b/scripts/validate-template.py
@@ -176,8 +176,8 @@
"Full changelog and release notes:",
)
ALLOWED_CATEGORY_TOKENS = {
- "Productivity",
- "Tools-Utilities",
+ "Productivity:",
+ "Tools:Utilities",
}
diff --git a/sure-aio.xml b/sure-aio.xml
index 81b730e..fd46477 100644
--- a/sure-aio.xml
+++ b/sure-aio.xml
@@ -45,7 +45,7 @@
- Always publish on manual main workflow dispatch
- Let manual publish proceed when smoke test is skipped
- Trigger package publish for aio tag alignment
- Productivity Tools-Utilities
+ Productivity: Tools:Utilities
http://[IP]:[PORT:3000]
https://raw.githubusercontent.com/JSONbored/awesome-unraid/main/sure-aio.xml
https://raw.githubusercontent.com/JSONbored/awesome-unraid/main/icons/sure.png
diff --git a/tests/template/test_container_contract.py b/tests/template/test_container_contract.py
index b8a3ee9..5111762 100644
--- a/tests/template/test_container_contract.py
+++ b/tests/template/test_container_contract.py
@@ -77,6 +77,7 @@ def test_unraid_metadata_contract_is_complete_and_unprivileged() -> None:
"Project",
"TemplateURL",
"Icon",
+ "Category",
"WebUI",
):
value = root.findtext(tag)
@@ -153,6 +154,7 @@ def test_dockerfile_has_runtime_safety_contract() -> None:
assert "curl -fsS" in dockerfile # nosec B101
assert 'ENTRYPOINT ["/init"]' in dockerfile # nosec B101
assert "S6_CMD_WAIT_FOR_SERVICES_MAXTIME" in dockerfile # nosec B101
+ assert "S6_BEHAVIOUR_IF_STAGE2_FAILS=2" in dockerfile # nosec B101
def test_docker_socket_mount_is_advanced_and_documented_when_present() -> None: