From 1d6bcdc9071e8542d47e8d14f239a69a8d676494 Mon Sep 17 00:00:00 2001 From: "infinito-nexus-updater[bot]" <286144613+infinito-nexus-updater[bot]@users.noreply.github.com> Date: Mon, 19 Oct 2026 10:26:53 +0000 Subject: [PATCH 01/28] update(versions): update pins with a declared source (web-app-xwiki) --- roles/web-app-xwiki/meta/addons/ai-llm.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/web-app-xwiki/meta/addons/ai-llm.yml b/roles/web-app-xwiki/meta/addons/ai-llm.yml index 5fa3d267ec..990505f251 100644 --- a/roles/web-app-xwiki/meta/addons/ai-llm.yml +++ b/roles/web-app-xwiki/meta/addons/ai-llm.yml @@ -5,7 +5,7 @@ mechanism: extension source: upstream bridges: - litellm -version: "0.10" +version: "0.11" update: type: http_regex url: https://maven.xwiki.org/releases/org/xwiki/contrib/llm/application-ai-llm-models-ui/maven-metadata.xml From faa89679443b562f48b8a5b95a5e3b3f8dd2cc8a Mon Sep 17 00:00:00 2001 From: "infinito-nexus-updater[bot]" <286144613+infinito-nexus-updater[bot]@users.noreply.github.com> Date: Mon, 19 Oct 2026 13:17:58 +0000 Subject: [PATCH 02/28] update(images): update Docker image versions (web-app-wazuh) --- roles/web-app-wazuh/meta/services.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/roles/web-app-wazuh/meta/services.yml b/roles/web-app-wazuh/meta/services.yml index 9b521fd994..d38e9054c9 100644 --- a/roles/web-app-wazuh/meta/services.yml +++ b/roles/web-app-wazuh/meta/services.yml @@ -68,7 +68,7 @@ certs_generator: pids_limit: 128 manager: image: wazuh/wazuh-manager - version: "4.14.6" + version: "4.14.9" name: wazuh.manager # nocheck: min-storage-headroom Reason: budgets room for wazuh-manager's own operational data (var-ossec logs, alerts, agent queue/state) to grow well beyond the base image over the service's lifetime, not just the image itself. min_storage: 10GB @@ -98,7 +98,7 @@ indexer: # nocheck: healthcheck-timing-override covers the indexer's real boot without waiting out the curl flavor's 10m default, which would otherwise stall manager/dashboard's startup order far longer than the indexer actually needs. start_period: 90s image: wazuh/wazuh-indexer - version: "4.14.6" + version: "4.14.9" name: wazuh.indexer # nocheck: min-storage-headroom Reason: budgets room for the OpenSearch index data (security events/alerts Wazuh continuously ingests) to grow well beyond the base image over the service's lifetime, not just the image itself. min_storage: 20GB @@ -116,7 +116,7 @@ indexer: http: 8097 console: image: wazuh/wazuh-dashboard - version: "4.14.6" + version: "4.14.9" name: wazuh.dashboard # nocheck: min-storage-headroom Reason: budgets room for the dashboard's own runtime state (OpenSearch Dashboards bundle/build cache rebuilt on first boot, plus exported reports and saved-object data) to grow beyond the base image, not just the image itself. min_storage: 2GB From 155ca9a8a92d333c073e20469c89e06bf9746c8e Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Mon, 19 Oct 2026 15:54:49 +0200 Subject: [PATCH 03/28] fix(web-app-wazuh): declare the empty mesh membership #414 merged web-app-wazuh with a meta/networks.yml that carries only `local`, and test_mesh_subnet_pool.py requires every stack role to ship `meshes: []` so a compose host can join meshes through it. main's test-lint has been red since d0698daa5e: AssertionError: 'meshes' not found in {'local': {'subnet': '192.168.81.0/24'}} Co-Authored-By: Claude Opus 5.5 (1M context) --- roles/web-app-wazuh/meta/networks.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/roles/web-app-wazuh/meta/networks.yml b/roles/web-app-wazuh/meta/networks.yml index 7404ee091c..7200f150c2 100644 --- a/roles/web-app-wazuh/meta/networks.yml +++ b/roles/web-app-wazuh/meta/networks.yml @@ -1,3 +1,4 @@ --- +meshes: [] local: subnet: 192.168.81.0/24 From 52ad8549e0eb656f4eb87cffc1f32da9d0342490 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Mon, 19 Oct 2026 15:54:56 +0200 Subject: [PATCH 04/28] fix(ci): pin GitHub-hosted runners to ubuntu-24.04 `ubuntu-latest` moved from ubuntu-24.04 to ubuntu-26.04 on its own schedule, in the middle of run 39166850273, and every job that landed on the new image met a different OS: sudo-rs instead of sudo, no ip_tables loaded, and now the NFS probe-write of svc-storage-nfs-client failing on 26.04 swarm nodes. The same branch went red without a code change. Every `runs-on` now names `ubuntu-24.04`. GitHub still rebuilds that image weekly, so patches and tool updates keep arriving; only the release jump becomes a deliberate change of the label. The CodeQL job drops its dead swift/macos-latest branch, since no swift language is in its matrix. act already maps ubuntu-24.04 to the local platform image. A lint rejects any job whose `runs-on` contains a `*-latest` label. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/call-images-build-ci.yml | 2 +- .../workflows/call-images-mirror-missing.yml | 2 +- .github/workflows/call-lint.yml | 2 +- .github/workflows/call-orchestrator.yml | 4 +-- .github/workflows/call-release-version.yml | 4 +-- .github/workflows/call-test-deploy.yml | 4 +-- .github/workflows/call-test-dns.yml | 2 +- .github/workflows/call-test-install-make.yml | 2 +- .../workflows/call-test-install-pkgmgr.yml | 2 +- .github/workflows/call-test-workspace.yml | 4 +-- .github/workflows/call-test.yml | 2 +- .github/workflows/call-validation.yml | 6 ++-- .github/workflows/cron-cleanup-stale.yml | 6 ++-- .github/workflows/cron-collection-source.yml | 2 +- .github/workflows/cron-images-cleanup-ci.yml | 2 +- .github/workflows/cron-images-mirror-all.yml | 2 +- .github/workflows/cron-release-highest.yml | 2 +- .github/workflows/cron-security-codeql.yml | 2 +- .github/workflows/cron-security-fuzz.yml | 2 +- .github/workflows/cron-security-scorecard.yml | 2 +- .github/workflows/cron-update.yml | 10 +++--- .github/workflows/entry-cancel-superseded.yml | 4 +-- .github/workflows/entry-delete-branch.yml | 2 +- .../workflows/entry-manual-mirror-cleanup.yml | 2 +- .../workflows/entry-pr-change-orchestrate.yml | 8 ++--- .../entry-pr-closed-cancel-workflows.yml | 2 +- .../entry-pr-open-dependabot-close.yml | 2 +- .github/workflows/entry-push-latest.yml | 10 +++--- docs/contributing/actions/debugging/ci.md | 6 ++-- .../repository/test_workflow_runner_pinned.py | 34 +++++++++++++++++++ 30 files changed, 85 insertions(+), 51 deletions(-) create mode 100644 tests/lint/repository/test_workflow_runner_pinned.py diff --git a/.github/workflows/call-images-build-ci.yml b/.github/workflows/call-images-build-ci.yml index 6167740b27..e88665756a 100644 --- a/.github/workflows/call-images-build-ci.yml +++ b/.github/workflows/call-images-build-ci.yml @@ -43,7 +43,7 @@ concurrency: jobs: build: name: πŸ’Ώ Image - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: contents: read packages: write diff --git a/.github/workflows/call-images-mirror-missing.yml b/.github/workflows/call-images-mirror-missing.yml index 3127bc68ef..1fe1e5e252 100644 --- a/.github/workflows/call-images-mirror-missing.yml +++ b/.github/workflows/call-images-mirror-missing.yml @@ -78,7 +78,7 @@ concurrency: jobs: sync: name: πŸͺž Mirror - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: contents: read packages: write diff --git a/.github/workflows/call-lint.yml b/.github/workflows/call-lint.yml index d0d54bbff1..fce0906703 100644 --- a/.github/workflows/call-lint.yml +++ b/.github/workflows/call-lint.yml @@ -10,7 +10,7 @@ permissions: jobs: lint: name: 🧹 Lint - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 45 env: INFINITO_LINT_RUNNER: host diff --git a/.github/workflows/call-orchestrator.yml b/.github/workflows/call-orchestrator.yml index 7bf5546484..48453f6ab3 100644 --- a/.github/workflows/call-orchestrator.yml +++ b/.github/workflows/call-orchestrator.yml @@ -343,7 +343,7 @@ jobs: - test-deploy-chunk-2 - test-deploy-chunk-3 if: ${{ always() && github.ref == 'refs/heads/main' && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) }} - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: issues: write actions: read @@ -374,7 +374,7 @@ jobs: - lint - security-codeql if: ${{ always() }} - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: πŸ›‘ Fail if any upstream job failed or was cancelled if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }} diff --git a/.github/workflows/call-release-version.yml b/.github/workflows/call-release-version.yml index c6cc449a20..e16cfea5ca 100644 --- a/.github/workflows/call-release-version.yml +++ b/.github/workflows/call-release-version.yml @@ -30,7 +30,7 @@ env: jobs: check-ci-images: name: πŸ”Ž Check CI images - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 outputs: missing: ${{ steps.check.outputs.missing }} ci_tag: ${{ steps.citag.outputs.ci_tag }} @@ -123,7 +123,7 @@ jobs: needs.build-ci-images-if-missing.result == 'success' || needs.build-ci-images-if-missing.result == 'skipped' ) - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: contents: read packages: write diff --git a/.github/workflows/call-test-deploy.yml b/.github/workflows/call-test-deploy.yml index b1da0d71dd..9fb7d25eff 100644 --- a/.github/workflows/call-test-deploy.yml +++ b/.github/workflows/call-test-deploy.yml @@ -163,7 +163,7 @@ env: jobs: discover: name: πŸ” Chunk ${{ inputs.index }}${{ inputs.marker }} - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 60 outputs: apps: ${{ steps.discover.outputs.apps }} @@ -251,7 +251,7 @@ jobs: deploy: name: "${{ matrix.label }}" if: needs.discover.outputs.apps != '' && needs.discover.outputs.apps != '[]' - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 355 needs: discover diff --git a/.github/workflows/call-test-dns.yml b/.github/workflows/call-test-dns.yml index 133b4194bf..b45c0aacca 100644 --- a/.github/workflows/call-test-dns.yml +++ b/.github/workflows/call-test-dns.yml @@ -23,7 +23,7 @@ permissions: jobs: dns: name: 🌐 DNS - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 30 steps: - name: 🧹 Free disk space diff --git a/.github/workflows/call-test-install-make.yml b/.github/workflows/call-test-install-make.yml index 06acb0304f..36dcf5444d 100644 --- a/.github/workflows/call-test-install-make.yml +++ b/.github/workflows/call-test-install-make.yml @@ -10,7 +10,7 @@ permissions: jobs: test-install-make: name: πŸ› οΈ Makefile - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 30 env: NIX_CONFIG: | diff --git a/.github/workflows/call-test-install-pkgmgr.yml b/.github/workflows/call-test-install-pkgmgr.yml index 836eb87a31..e37e2281ea 100644 --- a/.github/workflows/call-test-install-pkgmgr.yml +++ b/.github/workflows/call-test-install-pkgmgr.yml @@ -10,7 +10,7 @@ permissions: jobs: test-install-pkgmgr: name: πŸ“¦ PKGMGR - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 30 env: NIX_CONFIG: | diff --git a/.github/workflows/call-test-workspace.yml b/.github/workflows/call-test-workspace.yml index 102f25ae7d..f1021a8366 100644 --- a/.github/workflows/call-test-workspace.yml +++ b/.github/workflows/call-test-workspace.yml @@ -21,7 +21,7 @@ permissions: jobs: prepare: name: πŸ§‘β€πŸ’» Workspace Matrix - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 5 outputs: matrix: ${{ steps.matrix.outputs.matrix }} @@ -44,7 +44,7 @@ jobs: test-workspace: name: ${{ matrix.icon }} Workspace (${{ matrix.label }}) - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 345 needs: prepare diff --git a/.github/workflows/call-test.yml b/.github/workflows/call-test.yml index ac26225993..47ed98878e 100644 --- a/.github/workflows/call-test.yml +++ b/.github/workflows/call-test.yml @@ -10,7 +10,7 @@ permissions: jobs: test: name: πŸ§ͺ Test - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 45 env: INFINITO_TEST_RUNNER: host diff --git a/.github/workflows/call-validation.yml b/.github/workflows/call-validation.yml index 5a1c32c004..6ff6dc2715 100644 --- a/.github/workflows/call-validation.yml +++ b/.github/workflows/call-validation.yml @@ -84,7 +84,7 @@ permissions: jobs: validation: name: βœ… Validation - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: ⬇️ Checkout the calling ref for its tooling uses: actions/checkout@v7.0.0 @@ -148,7 +148,7 @@ jobs: wait-fork-prereq-run: name: ⏳ Wait for fork run if: ${{ inputs.fork_prereqs }} - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 env: WAIT_REQUIRED: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository && !inputs.trusted_pr }} steps: @@ -194,7 +194,7 @@ jobs: name: 🚦 Gate Dependencies needs: wait-fork-prereq-run if: ${{ !cancelled() && inputs.fork_prereqs }} - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v7.0.0 - name: βœ… Assert privileged prerequisites are ready diff --git a/.github/workflows/cron-cleanup-stale.yml b/.github/workflows/cron-cleanup-stale.yml index 35a5d0168e..d340ec5255 100644 --- a/.github/workflows/cron-cleanup-stale.yml +++ b/.github/workflows/cron-cleanup-stale.yml @@ -12,7 +12,7 @@ permissions: jobs: stale: name: 🏷️ Close inactive issues and pull requests - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: issues: write pull-requests: write @@ -45,7 +45,7 @@ jobs: delete-stale-branches: name: 🌿 Delete inactive branches - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: contents: write issues: write @@ -65,7 +65,7 @@ jobs: cleanup-ghcr-ci-images: name: πŸ“¦ Delete old GHCR CI images - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: contents: read packages: write diff --git a/.github/workflows/cron-collection-source.yml b/.github/workflows/cron-collection-source.yml index 50dc753b10..be31adc04b 100644 --- a/.github/workflows/cron-collection-source.yml +++ b/.github/workflows/cron-collection-source.yml @@ -18,7 +18,7 @@ jobs: benchmark-collection-sources: name: βš–οΈ Time Galaxy against the git fallback if: vars.CI_ENABLE_AUTO_UPDATES == 'true' - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 env: HISTORY_PATH: build/collection-source-history.json steps: diff --git a/.github/workflows/cron-images-cleanup-ci.yml b/.github/workflows/cron-images-cleanup-ci.yml index 6f54549d13..57f076f6f4 100644 --- a/.github/workflows/cron-images-cleanup-ci.yml +++ b/.github/workflows/cron-images-cleanup-ci.yml @@ -22,7 +22,7 @@ concurrency: jobs: cleanup: name: 🧹 Delete old ci-* image versions - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: actions: read contents: read diff --git a/.github/workflows/cron-images-mirror-all.yml b/.github/workflows/cron-images-mirror-all.yml index 6b84158dfc..16427fd220 100644 --- a/.github/workflows/cron-images-mirror-all.yml +++ b/.github/workflows/cron-images-mirror-all.yml @@ -61,7 +61,7 @@ concurrency: jobs: sync: name: πŸͺž Mirror - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: contents: read packages: write diff --git a/.github/workflows/cron-release-highest.yml b/.github/workflows/cron-release-highest.yml index 8e17fadbae..571e35e6cd 100644 --- a/.github/workflows/cron-release-highest.yml +++ b/.github/workflows/cron-release-highest.yml @@ -19,7 +19,7 @@ env: jobs: check-and-trigger: name: πŸ”Ž Backfill highest missing release tag - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: contents: read packages: read diff --git a/.github/workflows/cron-security-codeql.yml b/.github/workflows/cron-security-codeql.yml index 35f47e7b7f..bc8c1cee3e 100644 --- a/.github/workflows/cron-security-codeql.yml +++ b/.github/workflows/cron-security-codeql.yml @@ -34,7 +34,7 @@ jobs: # - https://gh.io/supported-runners-and-hardware-resources # - https://gh.io/using-larger-runners (GitHub.com only) # Consider using larger runners or machines with greater resources for possible analysis time improvements. - runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + runs-on: ubuntu-24.04 permissions: security-events: write packages: read diff --git a/.github/workflows/cron-security-fuzz.yml b/.github/workflows/cron-security-fuzz.yml index 56e4a1e9f1..fb3a90cb17 100644 --- a/.github/workflows/cron-security-fuzz.yml +++ b/.github/workflows/cron-security-fuzz.yml @@ -16,7 +16,7 @@ permissions: jobs: fuzz-yaml-bootstrap: name: 🧬 Fuzz the bootstrap YAML reader - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 30 steps: - name: ⬇️ Checkout diff --git a/.github/workflows/cron-security-scorecard.yml b/.github/workflows/cron-security-scorecard.yml index 69eb85746f..33eb5cebfc 100644 --- a/.github/workflows/cron-security-scorecard.yml +++ b/.github/workflows/cron-security-scorecard.yml @@ -26,7 +26,7 @@ permissions: read-all jobs: analysis: name: πŸ” Scorecard analysis - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 # `publish_results: true` only works when run from the default branch. conditional can be removed if disabled. if: github.event.repository.default_branch == github.ref_name || github.event_name == 'pull_request' permissions: diff --git a/.github/workflows/cron-update.yml b/.github/workflows/cron-update.yml index 57f610fc05..f946f46cac 100644 --- a/.github/workflows/cron-update.yml +++ b/.github/workflows/cron-update.yml @@ -21,7 +21,7 @@ jobs: update-docker-image-versions: name: 🐳 Update Docker image versions if: vars.CI_ENABLE_AUTO_UPDATES == 'true' - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: ⬇️ Checkout uses: actions/checkout@v7.0.0 @@ -68,7 +68,7 @@ jobs: update-version-sources: name: πŸ”– Update declared version sources if: vars.CI_ENABLE_AUTO_UPDATES == 'true' - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: ⬇️ Checkout uses: actions/checkout@v7.0.0 @@ -115,7 +115,7 @@ jobs: update-pip-pins: name: 🐍 Update pinned pip requirements if: vars.CI_ENABLE_AUTO_UPDATES == 'true' - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: ⬇️ Checkout uses: actions/checkout@v7.0.0 @@ -162,7 +162,7 @@ jobs: update-galaxy-pins: name: 🌌 Update pinned Ansible collections if: vars.CI_ENABLE_AUTO_UPDATES == 'true' - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: ⬇️ Checkout uses: actions/checkout@v7.0.0 @@ -209,7 +209,7 @@ jobs: update-repository-refs: name: 🌿 Update repository refs if: vars.CI_ENABLE_AUTO_UPDATES == 'true' - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: ⬇️ Checkout uses: actions/checkout@v7.0.0 diff --git a/.github/workflows/entry-cancel-superseded.yml b/.github/workflows/entry-cancel-superseded.yml index dfd1df7907..defc9a7e5d 100644 --- a/.github/workflows/entry-cancel-superseded.yml +++ b/.github/workflows/entry-cancel-superseded.yml @@ -27,7 +27,7 @@ jobs: cancel-branch-runs: name: 🚫 Cancel superseded branch runs if: ${{ github.event_name == 'push' && (github.ref_name != 'main' || (github.ref_name == 'main' && startsWith(github.event.head_commit.message, 'Release version '))) }} - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: actions: write contents: read @@ -52,7 +52,7 @@ jobs: cancel-pull-request-runs: name: 🚫 Cancel superseded pull request runs if: ${{ github.event_name == 'pull_request_target' }} - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: actions: write contents: read diff --git a/.github/workflows/entry-delete-branch.yml b/.github/workflows/entry-delete-branch.yml index 8b9113032e..b15f5c811d 100644 --- a/.github/workflows/entry-delete-branch.yml +++ b/.github/workflows/entry-delete-branch.yml @@ -20,7 +20,7 @@ permissions: jobs: cancel-branch-runs: if: ${{ github.event.ref_type == 'branch' }} - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: actions: write contents: read diff --git a/.github/workflows/entry-manual-mirror-cleanup.yml b/.github/workflows/entry-manual-mirror-cleanup.yml index efc372ad24..c4a6fc24ee 100644 --- a/.github/workflows/entry-manual-mirror-cleanup.yml +++ b/.github/workflows/entry-manual-mirror-cleanup.yml @@ -40,7 +40,7 @@ permissions: jobs: cleanup: name: πŸ—‘οΈ Delete private GHCR mirror packages - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: contents: read packages: write diff --git a/.github/workflows/entry-pr-change-orchestrate.yml b/.github/workflows/entry-pr-change-orchestrate.yml index 36d4aa63ee..2cf95bf625 100644 --- a/.github/workflows/entry-pr-change-orchestrate.yml +++ b/.github/workflows/entry-pr-change-orchestrate.yml @@ -29,7 +29,7 @@ jobs: detect-pr-scope: name: πŸ”Ž Detect PR scope if: ${{ github.event.pull_request.draft == false && (github.event_name == 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository) }} - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 outputs: scope: ${{ steps.resolve.outputs.scope }} run_ci_orchestrator: ${{ steps.resolve.outputs.run_ci_orchestrator }} @@ -59,7 +59,7 @@ jobs: - detect-pr-scope - validation if: ${{ needs.detect-pr-scope.outputs.run_ci_orchestrator == 'false' }} - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: βœ… Confirm lightweight PR scope shell: bash @@ -73,7 +73,7 @@ jobs: - detect-pr-scope - validation if: ${{ needs.detect-pr-scope.outputs.run_ci_orchestrator == 'true' }} - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 outputs: whitelist: ${{ steps.resolve.outputs.whitelist }} steps: @@ -124,7 +124,7 @@ jobs: - detect-pr-scope - validation if: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository && needs.detect-pr-scope.outputs.run_ci_orchestrator == 'true' }} - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 outputs: checkout_ref: ${{ steps.out.outputs.checkout_ref }} image_tag: ${{ steps.out.outputs.image_tag }} diff --git a/.github/workflows/entry-pr-closed-cancel-workflows.yml b/.github/workflows/entry-pr-closed-cancel-workflows.yml index c2f8e6ae12..f3d880ada1 100644 --- a/.github/workflows/entry-pr-closed-cancel-workflows.yml +++ b/.github/workflows/entry-pr-closed-cancel-workflows.yml @@ -15,7 +15,7 @@ permissions: jobs: cancel-pr-runs: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: actions: write contents: read diff --git a/.github/workflows/entry-pr-open-dependabot-close.yml b/.github/workflows/entry-pr-open-dependabot-close.yml index a3ced798c0..6d11bd73ce 100644 --- a/.github/workflows/entry-pr-open-dependabot-close.yml +++ b/.github/workflows/entry-pr-open-dependabot-close.yml @@ -19,7 +19,7 @@ jobs: close-when-disabled: name: 🚫 Close Dependabot PR while gated if: github.event.pull_request.user.login == 'dependabot[bot]' && vars.CI_ENABLE_AUTO_UPDATES != 'true' - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: πŸ“ͺ Close PR env: diff --git a/.github/workflows/entry-push-latest.yml b/.github/workflows/entry-push-latest.yml index a22ee0ca8b..7bbddb2caa 100644 --- a/.github/workflows/entry-push-latest.yml +++ b/.github/workflows/entry-push-latest.yml @@ -21,7 +21,7 @@ permissions: jobs: run-policy: name: 🧭 Decide CI policy - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: contents: read pull-requests: read @@ -59,7 +59,7 @@ jobs: name: ⏭️ Skip CI needs: run-policy if: needs.run-policy.outputs.should_run != 'true' - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: ⏭️ Explain skip shell: bash @@ -72,7 +72,7 @@ jobs: if: needs.run-policy.outputs.should_run == 'true' needs: run-policy name: 🏷️ Detect release tag - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 outputs: is_version: ${{ steps.pick.outputs.is_version }} version_tag: ${{ steps.pick.outputs.version_tag }} @@ -91,7 +91,7 @@ jobs: if: needs.run-policy.outputs.should_run == 'true' name: ⭐ Resolve new-role priority needs: run-policy - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 outputs: priority: ${{ steps.resolve.outputs.priority }} steps: @@ -132,7 +132,7 @@ jobs: - run-policy - detect-release - ci-orchestrator - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 outputs: allowed: ${{ steps.guard.outputs.allowed }} steps: diff --git a/docs/contributing/actions/debugging/ci.md b/docs/contributing/actions/debugging/ci.md index 9ee6075aff..bb84bf1450 100644 --- a/docs/contributing/actions/debugging/ci.md +++ b/docs/contributing/actions/debugging/ci.md @@ -49,11 +49,11 @@ Some CI failures (most notably OOM-kills such as [#162](https://github.com/infin ### GitHub-Hosted Runner Specs -All [workflows](../../../../.github/workflows/) target `runs-on: ubuntu-latest`, which maps to the GitHub-hosted standard Linux runner. The effective ceiling depends on the repository's visibility and plan: +All [workflows](../../../../.github/workflows/) target `runs-on: ubuntu-24.04`, the GitHub-hosted standard Linux runner pinned to one Ubuntu release. GitHub still refreshes that image weekly; moving to the next release is a deliberate change of the label, and `tests/lint/repository/test_workflow_runner_pinned.py` rejects `*-latest` labels. The effective ceiling depends on the repository's visibility and plan: | Repository / plan | vCPU | RAM | SSD | |------------------------------------------|------|--------|-------| -| Public repo (this repo, `ubuntu-latest`) | 4 | 16 GB | 14 GB | +| Public repo (this repo, `ubuntu-24.04`) | 4 | 16 GB | 14 GB | | Private repo on Free / Pro / Team | 2 | 7 GB | 14 GB | | Private repo on Enterprise | 4 | 16 GB | 14 GB | @@ -70,7 +70,7 @@ For the authoritative specs, see the upstream docs for [standard hosted runners] Pick the profile that matches the runner you want to mimic: ```sh -# Public repo runner (ubuntu-latest, 4 vCPU / 16 GB): the default CI target +# Public repo runner (ubuntu-24.04, 4 vCPU / 16 GB): the default CI target INFINITO_MEM_LIMIT=16g INFINITO_MEMSWAP_LIMIT=16g INFINITO_CPUS=4 make # Private repo runner on Free/Pro/Team (2 vCPU / 7 GB) diff --git a/tests/lint/repository/test_workflow_runner_pinned.py b/tests/lint/repository/test_workflow_runner_pinned.py new file mode 100644 index 0000000000..4aa1838c1c --- /dev/null +++ b/tests/lint/repository/test_workflow_runner_pinned.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +import unittest + +from utils.cache.yaml import load_yaml_any + +from . import PROJECT_ROOT + +_WORKFLOWS_DIR = PROJECT_ROOT / ".github" / "workflows" + + +class TestWorkflowRunnerPinned(unittest.TestCase): + def test_no_job_runs_on_a_latest_label(self) -> None: + offenders: list[str] = [] + for path in sorted(_WORKFLOWS_DIR.glob("*.y*ml")): + document = load_yaml_any(str(path), default_if_missing={}) or {} + for job_id, job in (document.get("jobs") or {}).items(): + if not isinstance(job, dict) or "runs-on" not in job: + continue + if "-latest" in str(job["runs-on"]): + offenders.append(f"{path.name}: {job_id}: {job['runs-on']}") + + if offenders: + self.fail( + f"{len(offenders)} job(s) run on a moving '*-latest' runner label. " + "GitHub moves that label to the next OS release on its own " + "schedule (ubuntu-latest jumped to 26.04 mid-run); pin the " + "release, e.g. 'ubuntu-24.04', and move it in a dedicated " + "change.\n" + "\n".join(offenders) + ) + + +if __name__ == "__main__": + unittest.main() From b055eece05efe352bc72e2cdf5a8cdd6866dbe6e Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Mon, 19 Oct 2026 16:50:05 +0200 Subject: [PATCH 05/28] feat(ci): keep GitHub-hosted runner labels current through the updater MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The workflows pin `runs-on: ubuntu-NN.NN`, so no job moves to a new Ubuntu release behind our back, but nothing raised the pin either. A sixth job of `πŸ”„ Update versions` closes that: `utils/update/runner.py` reads the image table of actions/runner-images, takes the newest x64 Ubuntu label whose image carries no preview or beta badge, and raises every older bare `runs-on: ubuntu-NN.NN` to it; arch-suffixed labels stay untouched. The job opens the change as one PR over `.github/workflows`, so its CI run tests the new image before it reaches main. This commit carries the updater's first result: every job now runs on ubuntu-26.04 (49 `runs-on`). actionlint 1.7.12, the newest release, does not know `ubuntu-26.04` yet (rhysd/actionlint#682, fix proposed in rhysd/actionlint#743), so `.github/actionlint.yaml` declares the label until a release knows it. act maps each `ubuntu-NN.NN` label the workflows use to the local platform image, derived from `.github/workflows`, instead of a fixed list that did not know ubuntu-26.04. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/actionlint.yaml | 3 + .github/workflows/call-images-build-ci.yml | 2 +- .../workflows/call-images-mirror-missing.yml | 2 +- .github/workflows/call-lint.yml | 2 +- .github/workflows/call-orchestrator.yml | 4 +- .github/workflows/call-release-version.yml | 4 +- .github/workflows/call-test-deploy.yml | 4 +- .github/workflows/call-test-dns.yml | 2 +- .github/workflows/call-test-install-make.yml | 2 +- .../workflows/call-test-install-pkgmgr.yml | 2 +- .github/workflows/call-test-workspace.yml | 4 +- .github/workflows/call-test.yml | 2 +- .github/workflows/call-validation.yml | 6 +- .github/workflows/cron-cleanup-stale.yml | 6 +- .github/workflows/cron-collection-source.yml | 2 +- .github/workflows/cron-images-cleanup-ci.yml | 2 +- .github/workflows/cron-images-mirror-all.yml | 2 +- .github/workflows/cron-release-highest.yml | 2 +- .github/workflows/cron-security-codeql.yml | 2 +- .github/workflows/cron-security-fuzz.yml | 2 +- .github/workflows/cron-security-scorecard.yml | 2 +- .github/workflows/cron-update.yml | 52 +++++++++++-- .github/workflows/entry-cancel-superseded.yml | 4 +- .github/workflows/entry-delete-branch.yml | 2 +- .../workflows/entry-manual-mirror-cleanup.yml | 2 +- .../workflows/entry-pr-change-orchestrate.yml | 8 +- .../entry-pr-closed-cancel-workflows.yml | 2 +- .../entry-pr-open-dependabot-close.yml | 2 +- .github/workflows/entry-push-latest.yml | 10 +-- cli/contributing/update/runner/__init__.py | 5 ++ cli/contributing/update/runner/__main__.py | 46 +++++++++++ docs/contributing/actions/debugging/ci.md | 6 +- scripts/github/update/runner_labels.sh | 8 ++ scripts/tests/deploy/act/workflow.sh | 8 +- tests/unit/python/utils/update/test_runner.py | 71 +++++++++++++++++ utils/update/runner.py | 77 +++++++++++++++++++ 36 files changed, 307 insertions(+), 55 deletions(-) create mode 100644 .github/actionlint.yaml create mode 100644 cli/contributing/update/runner/__init__.py create mode 100755 cli/contributing/update/runner/__main__.py create mode 100755 scripts/github/update/runner_labels.sh create mode 100644 tests/unit/python/utils/update/test_runner.py create mode 100644 utils/update/runner.py diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 0000000000..978712b9f1 --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,3 @@ +self-hosted-runner: + labels: + - ubuntu-26.04 diff --git a/.github/workflows/call-images-build-ci.yml b/.github/workflows/call-images-build-ci.yml index e88665756a..50e9aef161 100644 --- a/.github/workflows/call-images-build-ci.yml +++ b/.github/workflows/call-images-build-ci.yml @@ -43,7 +43,7 @@ concurrency: jobs: build: name: πŸ’Ώ Image - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: contents: read packages: write diff --git a/.github/workflows/call-images-mirror-missing.yml b/.github/workflows/call-images-mirror-missing.yml index 1fe1e5e252..0c6fa3bc08 100644 --- a/.github/workflows/call-images-mirror-missing.yml +++ b/.github/workflows/call-images-mirror-missing.yml @@ -78,7 +78,7 @@ concurrency: jobs: sync: name: πŸͺž Mirror - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: contents: read packages: write diff --git a/.github/workflows/call-lint.yml b/.github/workflows/call-lint.yml index fce0906703..679feb93f1 100644 --- a/.github/workflows/call-lint.yml +++ b/.github/workflows/call-lint.yml @@ -10,7 +10,7 @@ permissions: jobs: lint: name: 🧹 Lint - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 timeout-minutes: 45 env: INFINITO_LINT_RUNNER: host diff --git a/.github/workflows/call-orchestrator.yml b/.github/workflows/call-orchestrator.yml index 48453f6ab3..c00c41ab38 100644 --- a/.github/workflows/call-orchestrator.yml +++ b/.github/workflows/call-orchestrator.yml @@ -343,7 +343,7 @@ jobs: - test-deploy-chunk-2 - test-deploy-chunk-3 if: ${{ always() && github.ref == 'refs/heads/main' && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) }} - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: issues: write actions: read @@ -374,7 +374,7 @@ jobs: - lint - security-codeql if: ${{ always() }} - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 steps: - name: πŸ›‘ Fail if any upstream job failed or was cancelled if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }} diff --git a/.github/workflows/call-release-version.yml b/.github/workflows/call-release-version.yml index e16cfea5ca..3410e44a35 100644 --- a/.github/workflows/call-release-version.yml +++ b/.github/workflows/call-release-version.yml @@ -30,7 +30,7 @@ env: jobs: check-ci-images: name: πŸ”Ž Check CI images - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 outputs: missing: ${{ steps.check.outputs.missing }} ci_tag: ${{ steps.citag.outputs.ci_tag }} @@ -123,7 +123,7 @@ jobs: needs.build-ci-images-if-missing.result == 'success' || needs.build-ci-images-if-missing.result == 'skipped' ) - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: contents: read packages: write diff --git a/.github/workflows/call-test-deploy.yml b/.github/workflows/call-test-deploy.yml index 9fb7d25eff..ca454e0e46 100644 --- a/.github/workflows/call-test-deploy.yml +++ b/.github/workflows/call-test-deploy.yml @@ -163,7 +163,7 @@ env: jobs: discover: name: πŸ” Chunk ${{ inputs.index }}${{ inputs.marker }} - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 timeout-minutes: 60 outputs: apps: ${{ steps.discover.outputs.apps }} @@ -251,7 +251,7 @@ jobs: deploy: name: "${{ matrix.label }}" if: needs.discover.outputs.apps != '' && needs.discover.outputs.apps != '[]' - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 timeout-minutes: 355 needs: discover diff --git a/.github/workflows/call-test-dns.yml b/.github/workflows/call-test-dns.yml index b45c0aacca..d993811a4b 100644 --- a/.github/workflows/call-test-dns.yml +++ b/.github/workflows/call-test-dns.yml @@ -23,7 +23,7 @@ permissions: jobs: dns: name: 🌐 DNS - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 timeout-minutes: 30 steps: - name: 🧹 Free disk space diff --git a/.github/workflows/call-test-install-make.yml b/.github/workflows/call-test-install-make.yml index 36dcf5444d..080b6fcd22 100644 --- a/.github/workflows/call-test-install-make.yml +++ b/.github/workflows/call-test-install-make.yml @@ -10,7 +10,7 @@ permissions: jobs: test-install-make: name: πŸ› οΈ Makefile - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 timeout-minutes: 30 env: NIX_CONFIG: | diff --git a/.github/workflows/call-test-install-pkgmgr.yml b/.github/workflows/call-test-install-pkgmgr.yml index e37e2281ea..688e5ce28a 100644 --- a/.github/workflows/call-test-install-pkgmgr.yml +++ b/.github/workflows/call-test-install-pkgmgr.yml @@ -10,7 +10,7 @@ permissions: jobs: test-install-pkgmgr: name: πŸ“¦ PKGMGR - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 timeout-minutes: 30 env: NIX_CONFIG: | diff --git a/.github/workflows/call-test-workspace.yml b/.github/workflows/call-test-workspace.yml index f1021a8366..4c748b56e4 100644 --- a/.github/workflows/call-test-workspace.yml +++ b/.github/workflows/call-test-workspace.yml @@ -21,7 +21,7 @@ permissions: jobs: prepare: name: πŸ§‘β€πŸ’» Workspace Matrix - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 timeout-minutes: 5 outputs: matrix: ${{ steps.matrix.outputs.matrix }} @@ -44,7 +44,7 @@ jobs: test-workspace: name: ${{ matrix.icon }} Workspace (${{ matrix.label }}) - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 timeout-minutes: 345 needs: prepare diff --git a/.github/workflows/call-test.yml b/.github/workflows/call-test.yml index 47ed98878e..ae113752e1 100644 --- a/.github/workflows/call-test.yml +++ b/.github/workflows/call-test.yml @@ -10,7 +10,7 @@ permissions: jobs: test: name: πŸ§ͺ Test - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 timeout-minutes: 45 env: INFINITO_TEST_RUNNER: host diff --git a/.github/workflows/call-validation.yml b/.github/workflows/call-validation.yml index 6ff6dc2715..05e395339b 100644 --- a/.github/workflows/call-validation.yml +++ b/.github/workflows/call-validation.yml @@ -84,7 +84,7 @@ permissions: jobs: validation: name: βœ… Validation - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 steps: - name: ⬇️ Checkout the calling ref for its tooling uses: actions/checkout@v7.0.0 @@ -148,7 +148,7 @@ jobs: wait-fork-prereq-run: name: ⏳ Wait for fork run if: ${{ inputs.fork_prereqs }} - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 env: WAIT_REQUIRED: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository && !inputs.trusted_pr }} steps: @@ -194,7 +194,7 @@ jobs: name: 🚦 Gate Dependencies needs: wait-fork-prereq-run if: ${{ !cancelled() && inputs.fork_prereqs }} - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 steps: - uses: actions/checkout@v7.0.0 - name: βœ… Assert privileged prerequisites are ready diff --git a/.github/workflows/cron-cleanup-stale.yml b/.github/workflows/cron-cleanup-stale.yml index d340ec5255..68a1a1cf4c 100644 --- a/.github/workflows/cron-cleanup-stale.yml +++ b/.github/workflows/cron-cleanup-stale.yml @@ -12,7 +12,7 @@ permissions: jobs: stale: name: 🏷️ Close inactive issues and pull requests - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: issues: write pull-requests: write @@ -45,7 +45,7 @@ jobs: delete-stale-branches: name: 🌿 Delete inactive branches - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: contents: write issues: write @@ -65,7 +65,7 @@ jobs: cleanup-ghcr-ci-images: name: πŸ“¦ Delete old GHCR CI images - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: contents: read packages: write diff --git a/.github/workflows/cron-collection-source.yml b/.github/workflows/cron-collection-source.yml index be31adc04b..877415b27e 100644 --- a/.github/workflows/cron-collection-source.yml +++ b/.github/workflows/cron-collection-source.yml @@ -18,7 +18,7 @@ jobs: benchmark-collection-sources: name: βš–οΈ Time Galaxy against the git fallback if: vars.CI_ENABLE_AUTO_UPDATES == 'true' - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 env: HISTORY_PATH: build/collection-source-history.json steps: diff --git a/.github/workflows/cron-images-cleanup-ci.yml b/.github/workflows/cron-images-cleanup-ci.yml index 57f076f6f4..bdde2e46bd 100644 --- a/.github/workflows/cron-images-cleanup-ci.yml +++ b/.github/workflows/cron-images-cleanup-ci.yml @@ -22,7 +22,7 @@ concurrency: jobs: cleanup: name: 🧹 Delete old ci-* image versions - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: actions: read contents: read diff --git a/.github/workflows/cron-images-mirror-all.yml b/.github/workflows/cron-images-mirror-all.yml index 16427fd220..3f1619f873 100644 --- a/.github/workflows/cron-images-mirror-all.yml +++ b/.github/workflows/cron-images-mirror-all.yml @@ -61,7 +61,7 @@ concurrency: jobs: sync: name: πŸͺž Mirror - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: contents: read packages: write diff --git a/.github/workflows/cron-release-highest.yml b/.github/workflows/cron-release-highest.yml index 571e35e6cd..76f4a4ee35 100644 --- a/.github/workflows/cron-release-highest.yml +++ b/.github/workflows/cron-release-highest.yml @@ -19,7 +19,7 @@ env: jobs: check-and-trigger: name: πŸ”Ž Backfill highest missing release tag - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: contents: read packages: read diff --git a/.github/workflows/cron-security-codeql.yml b/.github/workflows/cron-security-codeql.yml index bc8c1cee3e..f5242ad964 100644 --- a/.github/workflows/cron-security-codeql.yml +++ b/.github/workflows/cron-security-codeql.yml @@ -34,7 +34,7 @@ jobs: # - https://gh.io/supported-runners-and-hardware-resources # - https://gh.io/using-larger-runners (GitHub.com only) # Consider using larger runners or machines with greater resources for possible analysis time improvements. - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: security-events: write packages: read diff --git a/.github/workflows/cron-security-fuzz.yml b/.github/workflows/cron-security-fuzz.yml index fb3a90cb17..ec8a3005ef 100644 --- a/.github/workflows/cron-security-fuzz.yml +++ b/.github/workflows/cron-security-fuzz.yml @@ -16,7 +16,7 @@ permissions: jobs: fuzz-yaml-bootstrap: name: 🧬 Fuzz the bootstrap YAML reader - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 timeout-minutes: 30 steps: - name: ⬇️ Checkout diff --git a/.github/workflows/cron-security-scorecard.yml b/.github/workflows/cron-security-scorecard.yml index 33eb5cebfc..144bd06067 100644 --- a/.github/workflows/cron-security-scorecard.yml +++ b/.github/workflows/cron-security-scorecard.yml @@ -26,7 +26,7 @@ permissions: read-all jobs: analysis: name: πŸ” Scorecard analysis - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 # `publish_results: true` only works when run from the default branch. conditional can be removed if disabled. if: github.event.repository.default_branch == github.ref_name || github.event_name == 'pull_request' permissions: diff --git a/.github/workflows/cron-update.yml b/.github/workflows/cron-update.yml index f946f46cac..a396a9d1f0 100644 --- a/.github/workflows/cron-update.yml +++ b/.github/workflows/cron-update.yml @@ -21,7 +21,7 @@ jobs: update-docker-image-versions: name: 🐳 Update Docker image versions if: vars.CI_ENABLE_AUTO_UPDATES == 'true' - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 steps: - name: ⬇️ Checkout uses: actions/checkout@v7.0.0 @@ -68,7 +68,7 @@ jobs: update-version-sources: name: πŸ”– Update declared version sources if: vars.CI_ENABLE_AUTO_UPDATES == 'true' - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 steps: - name: ⬇️ Checkout uses: actions/checkout@v7.0.0 @@ -115,7 +115,7 @@ jobs: update-pip-pins: name: 🐍 Update pinned pip requirements if: vars.CI_ENABLE_AUTO_UPDATES == 'true' - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 steps: - name: ⬇️ Checkout uses: actions/checkout@v7.0.0 @@ -162,7 +162,7 @@ jobs: update-galaxy-pins: name: 🌌 Update pinned Ansible collections if: vars.CI_ENABLE_AUTO_UPDATES == 'true' - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 steps: - name: ⬇️ Checkout uses: actions/checkout@v7.0.0 @@ -209,7 +209,7 @@ jobs: update-repository-refs: name: 🌿 Update repository refs if: vars.CI_ENABLE_AUTO_UPDATES == 'true' - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 steps: - name: ⬇️ Checkout uses: actions/checkout@v7.0.0 @@ -252,3 +252,45 @@ jobs: UPDATE_PR_BODY: "Automated update of semver-based git `ref:` values in `roles/*/meta/services.yml`. Upstream tags resolved via `git ls-remote --tags`." UPDATE_BASE_BRANCH: main run: bash scripts/github/update/open/role_prs.sh roles + + update-runner-labels: + name: πŸƒ Update GitHub-hosted runner labels + if: vars.CI_ENABLE_AUTO_UPDATES == 'true' + runs-on: ubuntu-26.04 + steps: + - name: ⬇️ Checkout + uses: actions/checkout@v7.0.0 + with: + ref: main + + - name: πŸ”§ Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.12" + + - name: πŸ”„ Update runner labels + run: bash scripts/github/update/runner_labels.sh + + - name: πŸ”Ž Check for changes + id: diff + run: bash scripts/github/resolve/check_git_changes.sh .github/workflows + + - name: βš™οΈ Generate app token + if: steps.diff.outputs.changed == 'true' + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ secrets.BOT_APP_CLIENT_ID }} + private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }} + + - name: πŸ“‚ Open PR if runner labels changed + if: steps.diff.outputs.changed == 'true' + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + APP_SLUG: ${{ steps.app-token.outputs.app-slug }} + UPDATE_BRANCH_PREFIX: update/runner-labels + UPDATE_COMMIT_MESSAGE: "update(ci): raise GitHub-hosted runner labels" + UPDATE_PR_TITLE: "update(ci): raise GitHub-hosted runner labels" + UPDATE_PR_BODY: "Automated raise of every `runs-on: ubuntu-NN.NN` in `.github/workflows/` to the newest GA x64 Ubuntu image listed in https://github.com/actions/runner-images. Its CI run is the proof that the stack works on the new image." + UPDATE_BASE_BRANCH: main + run: bash scripts/github/update/open/pr.sh .github/workflows diff --git a/.github/workflows/entry-cancel-superseded.yml b/.github/workflows/entry-cancel-superseded.yml index defc9a7e5d..85ed6ee1db 100644 --- a/.github/workflows/entry-cancel-superseded.yml +++ b/.github/workflows/entry-cancel-superseded.yml @@ -27,7 +27,7 @@ jobs: cancel-branch-runs: name: 🚫 Cancel superseded branch runs if: ${{ github.event_name == 'push' && (github.ref_name != 'main' || (github.ref_name == 'main' && startsWith(github.event.head_commit.message, 'Release version '))) }} - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: actions: write contents: read @@ -52,7 +52,7 @@ jobs: cancel-pull-request-runs: name: 🚫 Cancel superseded pull request runs if: ${{ github.event_name == 'pull_request_target' }} - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: actions: write contents: read diff --git a/.github/workflows/entry-delete-branch.yml b/.github/workflows/entry-delete-branch.yml index b15f5c811d..21499359ae 100644 --- a/.github/workflows/entry-delete-branch.yml +++ b/.github/workflows/entry-delete-branch.yml @@ -20,7 +20,7 @@ permissions: jobs: cancel-branch-runs: if: ${{ github.event.ref_type == 'branch' }} - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: actions: write contents: read diff --git a/.github/workflows/entry-manual-mirror-cleanup.yml b/.github/workflows/entry-manual-mirror-cleanup.yml index c4a6fc24ee..5f4436dbc7 100644 --- a/.github/workflows/entry-manual-mirror-cleanup.yml +++ b/.github/workflows/entry-manual-mirror-cleanup.yml @@ -40,7 +40,7 @@ permissions: jobs: cleanup: name: πŸ—‘οΈ Delete private GHCR mirror packages - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: contents: read packages: write diff --git a/.github/workflows/entry-pr-change-orchestrate.yml b/.github/workflows/entry-pr-change-orchestrate.yml index 2cf95bf625..0a3c46f036 100644 --- a/.github/workflows/entry-pr-change-orchestrate.yml +++ b/.github/workflows/entry-pr-change-orchestrate.yml @@ -29,7 +29,7 @@ jobs: detect-pr-scope: name: πŸ”Ž Detect PR scope if: ${{ github.event.pull_request.draft == false && (github.event_name == 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository) }} - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 outputs: scope: ${{ steps.resolve.outputs.scope }} run_ci_orchestrator: ${{ steps.resolve.outputs.run_ci_orchestrator }} @@ -59,7 +59,7 @@ jobs: - detect-pr-scope - validation if: ${{ needs.detect-pr-scope.outputs.run_ci_orchestrator == 'false' }} - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 steps: - name: βœ… Confirm lightweight PR scope shell: bash @@ -73,7 +73,7 @@ jobs: - detect-pr-scope - validation if: ${{ needs.detect-pr-scope.outputs.run_ci_orchestrator == 'true' }} - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 outputs: whitelist: ${{ steps.resolve.outputs.whitelist }} steps: @@ -124,7 +124,7 @@ jobs: - detect-pr-scope - validation if: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository && needs.detect-pr-scope.outputs.run_ci_orchestrator == 'true' }} - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 outputs: checkout_ref: ${{ steps.out.outputs.checkout_ref }} image_tag: ${{ steps.out.outputs.image_tag }} diff --git a/.github/workflows/entry-pr-closed-cancel-workflows.yml b/.github/workflows/entry-pr-closed-cancel-workflows.yml index f3d880ada1..e11ae6d0be 100644 --- a/.github/workflows/entry-pr-closed-cancel-workflows.yml +++ b/.github/workflows/entry-pr-closed-cancel-workflows.yml @@ -15,7 +15,7 @@ permissions: jobs: cancel-pr-runs: - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: actions: write contents: read diff --git a/.github/workflows/entry-pr-open-dependabot-close.yml b/.github/workflows/entry-pr-open-dependabot-close.yml index 6d11bd73ce..e6ea74258d 100644 --- a/.github/workflows/entry-pr-open-dependabot-close.yml +++ b/.github/workflows/entry-pr-open-dependabot-close.yml @@ -19,7 +19,7 @@ jobs: close-when-disabled: name: 🚫 Close Dependabot PR while gated if: github.event.pull_request.user.login == 'dependabot[bot]' && vars.CI_ENABLE_AUTO_UPDATES != 'true' - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 steps: - name: πŸ“ͺ Close PR env: diff --git a/.github/workflows/entry-push-latest.yml b/.github/workflows/entry-push-latest.yml index 7bbddb2caa..59070de57b 100644 --- a/.github/workflows/entry-push-latest.yml +++ b/.github/workflows/entry-push-latest.yml @@ -21,7 +21,7 @@ permissions: jobs: run-policy: name: 🧭 Decide CI policy - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: contents: read pull-requests: read @@ -59,7 +59,7 @@ jobs: name: ⏭️ Skip CI needs: run-policy if: needs.run-policy.outputs.should_run != 'true' - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 steps: - name: ⏭️ Explain skip shell: bash @@ -72,7 +72,7 @@ jobs: if: needs.run-policy.outputs.should_run == 'true' needs: run-policy name: 🏷️ Detect release tag - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 outputs: is_version: ${{ steps.pick.outputs.is_version }} version_tag: ${{ steps.pick.outputs.version_tag }} @@ -91,7 +91,7 @@ jobs: if: needs.run-policy.outputs.should_run == 'true' name: ⭐ Resolve new-role priority needs: run-policy - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 outputs: priority: ${{ steps.resolve.outputs.priority }} steps: @@ -132,7 +132,7 @@ jobs: - run-policy - detect-release - ci-orchestrator - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 outputs: allowed: ${{ steps.guard.outputs.allowed }} steps: diff --git a/cli/contributing/update/runner/__init__.py b/cli/contributing/update/runner/__init__.py new file mode 100644 index 0000000000..9b7a055ab0 --- /dev/null +++ b/cli/contributing/update/runner/__init__.py @@ -0,0 +1,5 @@ +"""CLI entry point package for the GitHub-hosted runner label update.""" + +from pathlib import Path + +PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] diff --git a/cli/contributing/update/runner/__main__.py b/cli/contributing/update/runner/__main__.py new file mode 100755 index 0000000000..1e6444aa45 --- /dev/null +++ b/cli/contributing/update/runner/__main__.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +from pathlib import Path + +from utils.update.fetch import document +from utils.update.runner import IMAGES_README, bump_workflows, newest_ubuntu_label + +from . import PROJECT_ROOT + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Raise every workflow's runs-on Ubuntu label to the newest GA " + "GitHub-hosted x64 image." + ) + ) + parser.add_argument( + "--repo-root", + type=Path, + default=PROJECT_ROOT, + help="Repository root whose .github/workflows are updated", + ) + args = parser.parse_args() + + label = newest_ubuntu_label(document(IMAGES_README)) + if label is None: + print(f"No GA x64 Ubuntu label found in {IMAGES_README}.") + return 1 + + repo_root = args.repo_root.resolve() + changed = bump_workflows(repo_root / ".github" / "workflows", label) + if not changed: + print(f"Every workflow already runs on {label} or newer.") + return 0 + + print(f"Raised runs-on to {label} in:") + for path in changed: + print(f"- {path.relative_to(repo_root)}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/docs/contributing/actions/debugging/ci.md b/docs/contributing/actions/debugging/ci.md index bb84bf1450..6fbb377978 100644 --- a/docs/contributing/actions/debugging/ci.md +++ b/docs/contributing/actions/debugging/ci.md @@ -49,11 +49,11 @@ Some CI failures (most notably OOM-kills such as [#162](https://github.com/infin ### GitHub-Hosted Runner Specs -All [workflows](../../../../.github/workflows/) target `runs-on: ubuntu-24.04`, the GitHub-hosted standard Linux runner pinned to one Ubuntu release. GitHub still refreshes that image weekly; moving to the next release is a deliberate change of the label, and `tests/lint/repository/test_workflow_runner_pinned.py` rejects `*-latest` labels. The effective ceiling depends on the repository's visibility and plan: +All [workflows](../../../../.github/workflows/) target `runs-on: ubuntu-26.04`, the GitHub-hosted standard Linux runner pinned to one Ubuntu release. GitHub still refreshes that image weekly. The `πŸƒ Update GitHub-hosted runner labels` job of [cron-update.yml](../../../../.github/workflows/cron-update.yml) opens a pull request when a newer GA x64 Ubuntu image is listed in [actions/runner-images](https://github.com/actions/runner-images), and `tests/lint/repository/test_workflow_runner_pinned.py` rejects `*-latest` labels. The effective ceiling depends on the repository's visibility and plan: | Repository / plan | vCPU | RAM | SSD | |------------------------------------------|------|--------|-------| -| Public repo (this repo, `ubuntu-24.04`) | 4 | 16 GB | 14 GB | +| Public repo (this repo, `ubuntu-26.04`) | 4 | 16 GB | 14 GB | | Private repo on Free / Pro / Team | 2 | 7 GB | 14 GB | | Private repo on Enterprise | 4 | 16 GB | 14 GB | @@ -70,7 +70,7 @@ For the authoritative specs, see the upstream docs for [standard hosted runners] Pick the profile that matches the runner you want to mimic: ```sh -# Public repo runner (ubuntu-24.04, 4 vCPU / 16 GB): the default CI target +# Public repo runner (ubuntu-26.04, 4 vCPU / 16 GB): the default CI target INFINITO_MEM_LIMIT=16g INFINITO_MEMSWAP_LIMIT=16g INFINITO_CPUS=4 make # Private repo runner on Free/Pro/Team (2 vCPU / 7 GB) diff --git a/scripts/github/update/runner_labels.sh b/scripts/github/update/runner_labels.sh new file mode 100755 index 0000000000..67e93be1b2 --- /dev/null +++ b/scripts/github/update/runner_labels.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../../.." && pwd)" + +cd "${REPO_ROOT}" +python3 -m cli.contributing.update.runner "$@" diff --git a/scripts/tests/deploy/act/workflow.sh b/scripts/tests/deploy/act/workflow.sh index 7be72c21a6..2f39f599ae 100755 --- a/scripts/tests/deploy/act/workflow.sh +++ b/scripts/tests/deploy/act/workflow.sh @@ -54,10 +54,10 @@ else fi cmd=(act "${ACT_EVENT}" -W "${_act_workflow}") -cmd+=(-P "ubuntu-latest=${ACT_PLATFORM_IMAGE}") -cmd+=(-P "ubuntu-24.04=${ACT_PLATFORM_IMAGE}") -cmd+=(-P "ubuntu-22.04=${ACT_PLATFORM_IMAGE}") -cmd+=(-P "ubuntu-20.04=${ACT_PLATFORM_IMAGE}") +_act_workflows_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)/.github/workflows" +while read -r _act_label; do + cmd+=(-P "${_act_label}=${ACT_PLATFORM_IMAGE}") +done < <(grep -rhE 'runs-on:' "${_act_workflows_dir}" | grep -oE 'ubuntu-[0-9]+\.[0-9]+' | sort -u) if [[ -n "${ACT_JOB}" ]]; then cmd+=(-j "${ACT_JOB}") diff --git a/tests/unit/python/utils/update/test_runner.py b/tests/unit/python/utils/update/test_runner.py new file mode 100644 index 0000000000..352c8f462e --- /dev/null +++ b/tests/unit/python/utils/update/test_runner.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +import tempfile +import unittest +from pathlib import Path + +from utils.cache.files import read_text +from utils.update.runner import bump_text, bump_workflows, newest_ubuntu_label + +_BADGE = "
![Endpoint Badge](https://img.shields.io/endpoint?url=x)" +_PREVIEW = "[![preview](https://img.shields.io/badge/preview-0969DA)](https://x)" +_README = "\n".join( + [ + "| Image | Architecture | YAML Label | Included Software |", + "| --------------------|--------------|---------------------|------------------|", + f"| Ubuntu 28.04 {_PREVIEW}{_BADGE} | x64 | `ubuntu-28.04` | [ubuntu-28.04] |", + f"| Ubuntu 26.04{_BADGE} | x64 | `ubuntu-26.04` | [ubuntu-26.04] |", + f"| Ubuntu 26.04 Arm64{_BADGE} | arm64 | `ubuntu-26.04-arm` | [x] |", + f"| Ubuntu 24.04{_BADGE} | x64 | `ubuntu-latest` or `ubuntu-24.04` | [x] |", + f"| Ubuntu Slim{_BADGE} | x64 | `ubuntu-slim` | [ubuntu-slim] |", + f"| macOS 26 Arm64{_BADGE} | arm64 | `macos-26` | [macOS-26-arm64] |", + ] +) + + +class TestNewestUbuntuLabel(unittest.TestCase): + def test_picks_newest_ga_x64_label(self) -> None: + self.assertEqual("ubuntu-26.04", newest_ubuntu_label(_README)) + + def test_none_without_an_ubuntu_row(self) -> None: + self.assertIsNone(newest_ubuntu_label("| Image | x |\n| macOS | arm64 |")) + + +class TestBumpText(unittest.TestCase): + def test_raises_older_and_keeps_newer(self) -> None: + text = ( + "jobs:\n" + " a:\n runs-on: ubuntu-22.04\n" + " b:\n runs-on: ubuntu-24.04\n" + " c:\n runs-on: ubuntu-28.04\n" + " d:\n runs-on: ubuntu-24.04-arm\n" + "# runs on ubuntu-24.04 elsewhere\n" + ) + self.assertEqual( + "jobs:\n" + " a:\n runs-on: ubuntu-26.04\n" + " b:\n runs-on: ubuntu-26.04\n" + " c:\n runs-on: ubuntu-28.04\n" + " d:\n runs-on: ubuntu-24.04-arm\n" + "# runs on ubuntu-24.04 elsewhere\n", + bump_text(text, "ubuntu-26.04"), + ) + + +class TestBumpWorkflows(unittest.TestCase): + def test_rewrites_only_changed_files(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + (root / "old.yml").write_text("runs-on: ubuntu-24.04\n", encoding="utf-8") + (root / "new.yml").write_text("runs-on: ubuntu-26.04\n", encoding="utf-8") + + changed = bump_workflows(root, "ubuntu-26.04") + + self.assertEqual([root / "old.yml"], changed) + self.assertEqual( + "runs-on: ubuntu-26.04\n", read_text(str(root / "old.yml")) + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/utils/update/runner.py b/utils/update/runner.py new file mode 100644 index 0000000000..326de8d007 --- /dev/null +++ b/utils/update/runner.py @@ -0,0 +1,77 @@ +"""Bump the pinned GitHub-hosted Ubuntu runner label of every workflow job. + +GitHub lists its hosted images in the runner-images README; the newest x64 +Ubuntu label whose image carries no preview or beta badge is the target. +""" + +from __future__ import annotations + +import re +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from pathlib import Path + +IMAGES_README = "https://raw.githubusercontent.com/actions/runner-images/main/README.md" + +_LABEL = re.compile(r"`ubuntu-(\d{2})\.(\d{2})`") +_RUNS_ON = re.compile(r"(runs-on:\s*)ubuntu-(\d{2})\.(\d{2})(?![\w.-])") +_PRERELEASE = re.compile(r"preview|beta", re.IGNORECASE) + + +def newest_ubuntu_label(readme: str) -> str | None: + """Return the newest GA x64 Ubuntu label listed in the images table. + + Args: + readme: text of the runner-images README. + """ + versions: list[tuple[int, int]] = [] + for row in readme.splitlines(): + cells = [cell.strip() for cell in row.split("|")] + if len(cells) < 5 or not cells[1].startswith("Ubuntu"): + continue + if _PRERELEASE.search(cells[1]) or cells[2] != "x64": + continue + versions += [ + (int(major), int(minor)) for major, minor in _LABEL.findall(cells[3]) + ] + if not versions: + return None + major, minor = max(versions) + return f"ubuntu-{major:02d}.{minor:02d}" + + +def bump_text(text: str, label: str) -> str: + """Raise every older ``runs-on: ubuntu-NN.NN`` in *text* to *label*. + + Args: + text: workflow source. + label: target label, ``ubuntu-NN.NN``. + """ + target = tuple(int(part) for part in label.removeprefix("ubuntu-").split(".")) + + def _raise(match: re.Match[str]) -> str: + if (int(match.group(2)), int(match.group(3))) < target: + return match.group(1) + label + return match.group(0) + + return _RUNS_ON.sub(_raise, text) + + +def bump_workflows(workflows_dir: Path, label: str) -> list[Path]: + """Rewrite the workflows under *workflows_dir* to *label*; return the changed files. + + Args: + workflows_dir: directory holding the workflow YAML files. + label: target label, ``ubuntu-NN.NN``. + """ + changed: list[Path] = [] + for path in sorted(workflows_dir.glob("*.y*ml")): + text = path.read_text( # nocheck: cache-read β€” read-then-write of the same file; cached read would go stale + encoding="utf-8" + ) + bumped = bump_text(text, label) + if bumped != text: + path.write_text(bumped, encoding="utf-8") + changed.append(path) + return changed From a085427236286c60e17263ef947819bc7032d437 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Mon, 19 Oct 2026 16:53:13 +0200 Subject: [PATCH 06/28] chore(svc-storage-nfs-server): capture ganesha's NFSv4 verdicts in debug runs On ubuntu-26.04 runners the swarm deploy fails at svc-storage-nfs-client's probe-write, right after a successful probe-mount, on every client node: `Destination directory /run/svc-storage-nfs-client/probe does not exist`. The rescue bundles cannot say why: ganesha writes to /var/log/ganesha/ganesha.log, which the swarm diagnostics never collected, and its NFS_V4 component logged nothing below EVENT. Debug runs now log NFS_V4 at DEBUG, and the diagnostics append the tail of ganesha.log to the nfs-boundary section. Co-Authored-By: Claude Opus 5.5 (1M context) --- roles/svc-storage-nfs-server/templates/ganesha.conf.j2 | 1 + scripts/tests/deploy/swarm/utils/collect/diagnostics.sh | 1 + 2 files changed, 2 insertions(+) diff --git a/roles/svc-storage-nfs-server/templates/ganesha.conf.j2 b/roles/svc-storage-nfs-server/templates/ganesha.conf.j2 index 44b076258f..deec4a6cf9 100644 --- a/roles/svc-storage-nfs-server/templates/ganesha.conf.j2 +++ b/roles/svc-storage-nfs-server/templates/ganesha.conf.j2 @@ -4,6 +4,7 @@ LOG { components { INIT = FULL_DEBUG; DISPATCH = DEBUG; + NFS_V4 = DEBUG; } } diff --git a/scripts/tests/deploy/swarm/utils/collect/diagnostics.sh b/scripts/tests/deploy/swarm/utils/collect/diagnostics.sh index 6fa5376a3e..327df86f04 100755 --- a/scripts/tests/deploy/swarm/utils/collect/diagnostics.sh +++ b/scripts/tests/deploy/swarm/utils/collect/diagnostics.sh @@ -88,6 +88,7 @@ dexec "${NFS_SERVER}" mountpoint "${INFINITO_SWARM_NFS_STATE_PATH:?}" 2>&1 dexec "${NFS_SERVER}" cat /proc/fs/nfsd/exports 2>&1 dexec "${NFS_SERVER}" cat /proc/fs/nfsd/versions 2>&1 dexec "${NFS_SERVER}" sh -c "journalctl -u nfs-server -u nfs-ganesha -u rpcbind --no-pager 2>&1 | tail -50" +dexec "${NFS_SERVER}" sh -c "tail -n 600 /var/log/ganesha/ganesha.log 2>&1" dexec "${NFS_SERVER}" sh -c "command -v ss >/dev/null 2>&1 && { ss -lntp | grep -E ':(2049|111)' || echo '(ss ran: nothing listening on 2049/111)'; } || echo '(ss not installed on this node)'" sep "ganesha-threads" "nfs-server: ganesha thread states (pins where a wedged startup blocks)" From fd596cf18820721f9e907d4d662b0dea6df6cf34 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Mon, 19 Oct 2026 22:36:32 +0200 Subject: [PATCH 07/28] fix(ci): tear down host AppArmor profiles on GitHub runners like on dev hosts ubuntu-26.04 runner images load per-binary AppArmor profiles (wg-quick, dig, who, fusermount3, ~150 upstream). Path-based attachment applies to every unconfined task, so the profiles also attach inside our privileged `apparmor=unconfined` containers. The upstream wg-quick profile only allows `/usr/bin/ip`; on CentOS and Fedora nodes `ip` lives at `/usr/sbin/ip`, so `svc-net-wireguard : Bring 'data' up` failed with rc=126: /usr/bin/wg-quick: line 32: /usr/sbin/ip: Permission denied apparmor="DENIED" operation="exec" profile="wg-quick" name="/usr/sbin/ip" (run 39268090162, Fediwall#0, CentOS swarm VPN). Dev hosts never see this: `environment-bootstrap` runs `security-apparmor-teardown`. The runners skipped that preparation. The deploy, workspace and DNS workflows now run the same target on the runner host before any container starts (skipped under act, whose job container cannot reach the host's policy), so CI matches the dev-host state for all profiles, not only the ones seen so far. A dedicated permissive profile for the containers was rejected: every AppArmor host without that profile loaded would refuse to start them, including dev hosts and act. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/call-test-deploy.yml | 5 +++++ .github/workflows/call-test-dns.yml | 5 +++++ .github/workflows/call-test-workspace.yml | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/.github/workflows/call-test-deploy.yml b/.github/workflows/call-test-deploy.yml index ca454e0e46..dc3e52ad1b 100644 --- a/.github/workflows/call-test-deploy.yml +++ b/.github/workflows/call-test-deploy.yml @@ -308,6 +308,11 @@ jobs: shell: bash run: ./scripts/github/runner/relocate_docker_dataroot.sh + - name: πŸ›‘οΈ Tear down host AppArmor profiles (dev-host parity) + if: ${{ !env.ACT }} + shell: bash + run: make security-apparmor-teardown + - name: πŸ” Resolve repository owner shell: bash run: | diff --git a/.github/workflows/call-test-dns.yml b/.github/workflows/call-test-dns.yml index d993811a4b..05c4c9ed3b 100644 --- a/.github/workflows/call-test-dns.yml +++ b/.github/workflows/call-test-dns.yml @@ -41,6 +41,11 @@ jobs: - name: ⬇️ Checkout uses: actions/checkout@v7.0.0 + - name: πŸ›‘οΈ Tear down host AppArmor profiles (dev-host parity) + if: ${{ !env.ACT }} + shell: bash + run: make security-apparmor-teardown + - name: πŸ”‘ Login to GHCR uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 with: diff --git a/.github/workflows/call-test-workspace.yml b/.github/workflows/call-test-workspace.yml index 4c748b56e4..a888feb280 100644 --- a/.github/workflows/call-test-workspace.yml +++ b/.github/workflows/call-test-workspace.yml @@ -88,6 +88,11 @@ jobs: - name: ⬇️ Checkout repository uses: actions/checkout@v7.0.0 + - name: πŸ›‘οΈ Tear down host AppArmor profiles (dev-host parity) + if: ${{ !env.ACT }} + shell: bash + run: make security-apparmor-teardown + - name: "πŸ“₯ Load the rescue paths into job env" shell: bash run: | From af82d6b4bf6b20a04f520e70d7cf55e6ffcfb079 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 20 Oct 2026 00:19:23 +0000 Subject: [PATCH 08/28] chore(deps-dev): bump eslint from 10.12.0 to 10.13.0 Bumps [eslint](https://github.com/eslint/eslint) from 10.12.0 to 10.13.0. - [Release notes](https://github.com/eslint/eslint/releases) - [Commits](https://github.com/eslint/eslint/compare/v10.12.0...v10.13.0) --- updated-dependencies: - dependency-name: eslint dependency-version: 10.13.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index d94cf2b236..07d86f4b59 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "devDependencies": { "@eslint/js": "^10.0.1", "@playwright/test": "^1.64.1", - "eslint": "^10.12.0", + "eslint": "^10.13.0", "eslint-plugin-playwright": "^2.12.1", "globals": "^17.13.0", "stylelint": "^17.16.0" @@ -848,9 +848,9 @@ } }, "node_modules/eslint": { - "version": "10.12.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.12.0.tgz", - "integrity": "sha512-npHjrHb2o4ehH3mE+YuxABR+0gyb0aVWCIolgJDArwCiHEIsonBm3ZKjel5b0U5+a+MbEvIyRCGCzD3eKKK2NA==", + "version": "10.13.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.13.0.tgz", + "integrity": "sha512-UohDjD4zSzEwhf58M3O7vc/ycc8O1I7dNlhtD0WxE8Px8EHNJKtUwzBqrzeGhisRuv2ag4sAYswszCJRgKXYUg==", "dev": true, "license": "MIT", "workspaces": [ diff --git a/package.json b/package.json index 5244fd5612..61ab507135 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "devDependencies": { "@eslint/js": "^10.0.1", "@playwright/test": "^1.64.1", - "eslint": "^10.12.0", + "eslint": "^10.13.0", "eslint-plugin-playwright": "^2.12.1", "globals": "^17.13.0", "stylelint": "^17.16.0" From a6c3c1cc91ac5b8dbd77f06aede55b21543abea3 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Tue, 20 Oct 2026 12:09:48 +0200 Subject: [PATCH 09/28] fix(apparmor): keep docker-default loaded when tearing AppArmor down `security-apparmor-teardown` ran `aa-teardown`, which unloads every profile including dockerd's in-memory `docker-default`. dockerd only loads that profile when it starts, so every container start afterwards that requests it fails until dockerd restarts. Since #1067 runs the teardown on the GitHub runners, BuildKit failed each RUN of the swarm node image three times (run 39332343490, 39 of 40 swarm jobs): runc run failed: unable to start container process: error during container init: unable to apply apparmor profile: apparmor failed to apply profile: write .../attr/apparmor/exec: no such file or directory The teardown now removes each loaded profile by name through securityfs and skips `docker-default` and child profiles, which leave with their parent. Dev hosts gain the same: no dockerd restart is needed after `make environment-bootstrap`. Co-Authored-By: Claude Opus 5.5 (1M context) --- scripts/system/apparmor/common.sh | 4 ++ scripts/system/apparmor/teardown.sh | 13 +++-- .../scripts/system/apparmor/__init__.py | 0 .../scripts/system/apparmor/test_common.py | 48 +++++++++++++++++++ 4 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 tests/unit/python/scripts/system/apparmor/__init__.py create mode 100644 tests/unit/python/scripts/system/apparmor/test_common.py diff --git a/scripts/system/apparmor/common.sh b/scripts/system/apparmor/common.sh index eae3bf51d5..d6c252267c 100755 --- a/scripts/system/apparmor/common.sh +++ b/scripts/system/apparmor/common.sh @@ -43,6 +43,10 @@ apparmor_should_manage() { apparmor_is_installed && apparmor_is_enabled } +apparmor_removable_profiles() { + sed -E 's/ \([a-z]+\)$//' | grep -v -E '//|^docker-default$' +} + systemd_is_operational() { command -v systemctl >/dev/null 2>&1 && [[ -d /run/systemd/system ]] } diff --git a/scripts/system/apparmor/teardown.sh b/scripts/system/apparmor/teardown.sh index 1b86288baa..f09ee0bf35 100755 --- a/scripts/system/apparmor/teardown.sh +++ b/scripts/system/apparmor/teardown.sh @@ -18,12 +18,15 @@ else echo "[apparmor] apparmor.service not available or systemd inactive; skipping service stop" fi -if command -v aa-teardown >/dev/null 2>&1; then - aa-teardown || apparmor_warn "[apparmor] aa-teardown returned non-zero; continuing" -elif command -v apparmor_parser >/dev/null 2>&1 && compgen -G '/etc/apparmor.d/*' >/dev/null; then - apparmor_parser -R /etc/apparmor.d/* || true # nocheck: shell-or-true -- grandfathered: worked in practice; TODO: sharpen to catch only the exact tolerated error +APPARMOR_FS=/sys/kernel/security/apparmor +if [[ -w "${APPARMOR_FS}/.remove" ]]; then + mapfile -t profiles < <(apparmor_removable_profiles <"${APPARMOR_FS}/profiles") + for profile in "${profiles[@]}"; do + printf '%s' "${profile}" >"${APPARMOR_FS}/.remove" + done + echo "[apparmor] unloaded ${#profiles[@]} profile(s); docker-default stays loaded" else - echo "[apparmor] no AppArmor profile tooling available; skipping profile unload" + echo "[apparmor] ${APPARMOR_FS}/.remove is not writable; skipping profile unload" fi echo "[apparmor] teardown complete" diff --git a/tests/unit/python/scripts/system/apparmor/__init__.py b/tests/unit/python/scripts/system/apparmor/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/python/scripts/system/apparmor/test_common.py b/tests/unit/python/scripts/system/apparmor/test_common.py new file mode 100644 index 0000000000..8b4440f1e4 --- /dev/null +++ b/tests/unit/python/scripts/system/apparmor/test_common.py @@ -0,0 +1,48 @@ +"""The AppArmor teardown must keep docker-default loaded. + +Run 39332343490 lost 39 swarm jobs: the runner's teardown unloaded every +profile, and BuildKit then failed each node image RUN step with +`apparmor failed to apply profile: write .../attr/apparmor/exec: no such file`. +""" + +from __future__ import annotations + +import subprocess +import unittest + +from utils.cache.files import PROJECT_ROOT + +COMMON = PROJECT_ROOT / "scripts" / "system" / "apparmor" / "common.sh" + +PROFILES = """wg-quick (enforce) +wg-quick//ip (enforce) +docker-default (enforce) +/usr/bin/man (enforce) +unprivileged_userns (unconfined) +docker-defaults (complain) +""" + + +class TestRemovableProfiles(unittest.TestCase): + def test_keeps_docker_default_and_child_profiles(self) -> None: + result = subprocess.run( + ["bash", "-c", f'source "{COMMON}"; apparmor_removable_profiles'], + input=PROFILES, + capture_output=True, + text=True, + check=True, + ) + + self.assertEqual( + [ + "wg-quick", + "/usr/bin/man", + "unprivileged_userns", + "docker-defaults", + ], + result.stdout.splitlines(), + ) + + +if __name__ == "__main__": + unittest.main() From 5238ec2b1378c64815f690e5a394e417c46949ed Mon Sep 17 00:00:00 2001 From: "infinito-nexus-updater[bot]" <286144613+infinito-nexus-updater[bot]@users.noreply.github.com> Date: Tue, 20 Oct 2026 10:15:19 +0000 Subject: [PATCH 10/28] update(versions): update pins with a declared source (web-app-n8n) --- roles/web-app-n8n/meta/services.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/web-app-n8n/meta/services.yml b/roles/web-app-n8n/meta/services.yml index 3465b87f28..b19d0978df 100644 --- a/roles/web-app-n8n/meta/services.yml +++ b/roles/web-app-n8n/meta/services.yml @@ -67,7 +67,7 @@ n8n: backup: no_stop_required: false image: docker.n8n.io/n8nio/n8n - version: "2.44.4" + version: "2.45.0" update: type: registry_tags image: docker.n8n.io/n8nio/n8n From e9e3326e4de229de88b750df16b1dcfeca27c8ee Mon Sep 17 00:00:00 2001 From: "infinito-nexus-updater[bot]" <286144613+infinito-nexus-updater[bot]@users.noreply.github.com> Date: Tue, 20 Oct 2026 10:16:09 +0000 Subject: [PATCH 11/28] update(images): update Docker image versions (svc-db-rabbitmq) --- roles/svc-db-rabbitmq/meta/services.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/svc-db-rabbitmq/meta/services.yml b/roles/svc-db-rabbitmq/meta/services.yml index b3183f721d..9a416080a5 100644 --- a/roles/svc-db-rabbitmq/meta/services.yml +++ b/roles/svc-db-rabbitmq/meta/services.yml @@ -12,7 +12,7 @@ rabbitmq: bond: 1 image: rabbitmq name: rabbitmq - version: "4.3-management-alpine" + version: "4.4-management-alpine" min_storage: 289MB backup: database_routine: true From d2146d9371d35df4f6a6c2b58fd6fa49ae1ae029 Mon Sep 17 00:00:00 2001 From: "infinito-nexus-updater[bot]" <286144613+infinito-nexus-updater[bot]@users.noreply.github.com> Date: Tue, 20 Oct 2026 10:16:13 +0000 Subject: [PATCH 12/28] update(images): update Docker image versions (web-app-opentalk) --- roles/web-app-opentalk/meta/services.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/web-app-opentalk/meta/services.yml b/roles/web-app-opentalk/meta/services.yml index 8640838ba9..97a1bfc355 100644 --- a/roles/web-app-opentalk/meta/services.yml +++ b/roles/web-app-opentalk/meta/services.yml @@ -142,7 +142,7 @@ rabbit: retries: 5 name: opentalk-rabbit image: rabbitmq - version: 4.3.6 + version: 4.4.0 min_storage: 314MB cpus: "0.5" mem_reservation: 256m From 760d6fee3f9677621063da4868aca8a342c3241d Mon Sep 17 00:00:00 2001 From: "infinito-nexus-updater[bot]" <286144613+infinito-nexus-updater[bot]@users.noreply.github.com> Date: Tue, 20 Oct 2026 10:16:17 +0000 Subject: [PATCH 13/28] update(images): update Docker image versions (web-app-pixelfed) --- roles/web-app-pixelfed/meta/services.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/web-app-pixelfed/meta/services.yml b/roles/web-app-pixelfed/meta/services.yml index b6e3bfb4da..84e36880c8 100644 --- a/roles/web-app-pixelfed/meta/services.yml +++ b/roles/web-app-pixelfed/meta/services.yml @@ -49,7 +49,7 @@ pixelfed: shared: true enabled: true # nocheck: dynamic-flag image: zknt/pixelfed - version: "20261019" + version: "20261020" name: pixelfed min_storage: 588MB backup: From 213d500fe3ca6935bf198143af247fcf717a1e98 Mon Sep 17 00:00:00 2001 From: "infinito-nexus-updater[bot]" <286144613+infinito-nexus-updater[bot]@users.noreply.github.com> Date: Tue, 20 Oct 2026 10:16:22 +0000 Subject: [PATCH 14/28] update(images): update Docker image versions (web-app-snipe-it) --- roles/web-app-snipe-it/meta/services.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/web-app-snipe-it/meta/services.yml b/roles/web-app-snipe-it/meta/services.yml index c8c526b175..ae261234af 100644 --- a/roles/web-app-snipe-it/meta/services.yml +++ b/roles/web-app-snipe-it/meta/services.yml @@ -58,7 +58,7 @@ snipe-it: enabled: true swarm: enabled: true - version: v8.8.0-alpine + version: v8.8.1-alpine custom: true name: snipe-it image: snipe/snipe-it From 4353fdbb123d7c7c8383780af2bdb73eee4d1e63 Mon Sep 17 00:00:00 2001 From: "infinito-nexus-updater[bot]" <286144613+infinito-nexus-updater[bot]@users.noreply.github.com> Date: Tue, 20 Oct 2026 10:16:26 +0000 Subject: [PATCH 15/28] update(images): update Docker image versions (web-app-socialhome) --- roles/web-app-socialhome/meta/services.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/web-app-socialhome/meta/services.yml b/roles/web-app-socialhome/meta/services.yml index a63a82eb63..536d8dce2e 100644 --- a/roles/web-app-socialhome/meta/services.yml +++ b/roles/web-app-socialhome/meta/services.yml @@ -13,7 +13,7 @@ socialhome: # nocheck: default-placement-manager embedded sqlite in a node-local volume; no clustered storage placement: manager image: ghcr.io/social-home-io/socialhome - version: "2026.10.17" + version: "2026.10.20" ports: local: http: 8089 From 916bc7e02503965a0756913ca4f49e2e091ce03e Mon Sep 17 00:00:00 2001 From: "infinito-nexus-updater[bot]" <286144613+infinito-nexus-updater[bot]@users.noreply.github.com> Date: Tue, 20 Oct 2026 10:16:31 +0000 Subject: [PATCH 16/28] update(images): update Docker image versions (web-app-taiga) --- roles/web-app-taiga/meta/services.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/roles/web-app-taiga/meta/services.yml b/roles/web-app-taiga/meta/services.yml index 4975dc4fad..54a2238584 100644 --- a/roles/web-app-taiga/meta/services.yml +++ b/roles/web-app-taiga/meta/services.yml @@ -112,7 +112,7 @@ async-rabbitmq: test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"] retries: 5 image: rabbitmq - version: "4.3-management-alpine" + version: "4.4-management-alpine" cpus: "1.0" mem_reservation: 1g mem_limit: 1.5g @@ -122,7 +122,7 @@ events-rabbitmq: test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"] retries: 5 image: rabbitmq - version: "4.3-management-alpine" + version: "4.4-management-alpine" cpus: "0.5" mem_reservation: 512m mem_limit: 1g From 310663ae8c7873bb4bdb6126eb1a95ed45d8b236 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Tue, 20 Oct 2026 13:39:36 +0200 Subject: [PATCH 17/28] feat(compose): recreate the stack network without losing volumes or worktree stacks When the IPAM config of a compose project's network changes (#999 added `ip_range`), compose recreates the network on the next `up`. With a foreign endpoint attached, as every worktree stack is on the primary's network, compose first stops the project's own containers, then fails with `error while removing network: ... has active endpoints`, and a repeated `up` or `up --force-recreate` does not recover. On 2026-10-19 this took the shared registry/package cache and coredns down for every local session. `make compose-network-recreate` (alias `i8conere`) runs the sequence that recovered it: detach every running container of another project from the network, `docker compose down --remove-orphans` without `-v` so the caches keep their volumes, the normal `up`, then attach the detached containers again, which draw addresses from the dynamic pool. If `up` fails, the still-detached containers are listed instead of reattached. Co-Authored-By: Claude Opus 5.5 (1M context) --- Makefile | 5 + aliases | 1 + cli/administration/deploy/development/cli.py | 1 + .../deploy/development/recreate.py | 74 ++++++++++++ .../deploy/development/test_recreate.py | 110 ++++++++++++++++++ 5 files changed, 191 insertions(+) create mode 100644 cli/administration/deploy/development/recreate.py create mode 100644 tests/unit/python/cli/administration/deploy/development/test_recreate.py diff --git a/Makefile b/Makefile index 46fd3d0ab0..eb19a6e74b 100644 --- a/Makefile +++ b/Makefile @@ -227,6 +227,11 @@ compose-inner-run: compose-inventory-refresh: @bash scripts/tests/deploy/local/reset/inventory.sh +.PHONY: compose-network-recreate +# Recreate the stack's default network after an IPAM change, keeping volumes and reattaching foreign stacks. +compose-network-recreate: install + @"$${PYTHON}" -m cli.administration.deploy.development recreate + .PHONY: compose-playwright # Rerun a role-local Playwright spec against the live running stack (no redeploy). # Usage: make compose-playwright role= [pw="--grep "] [keep=true] diff --git a/aliases b/aliases index 13f9873567..00d47d37a3 100644 --- a/aliases +++ b/aliases @@ -27,6 +27,7 @@ alias i8co2='m console' alias i8coape='m compose-app-exec' alias i8codo='m compose-down' alias i8cone='m compose-entity-purge' +alias i8conere='m compose-network-recreate' alias i8coni='m compose-inner-run' alias i8conire='m compose-inventory-refresh' alias i8copa='m compose-playwright' diff --git a/cli/administration/deploy/development/cli.py b/cli/administration/deploy/development/cli.py index 686cb52fe9..1c3fe3e3e8 100644 --- a/cli/administration/deploy/development/cli.py +++ b/cli/administration/deploy/development/cli.py @@ -53,6 +53,7 @@ def _build_parser(base_pkg: str) -> argparse.ArgumentParser: "run", "exec", "logs", + "recreate", ] for _, mod in _iter_command_modules(base_pkg, command_names): diff --git a/cli/administration/deploy/development/recreate.py b/cli/administration/deploy/development/recreate.py new file mode 100644 index 0000000000..24fc3e65c2 --- /dev/null +++ b/cli/administration/deploy/development/recreate.py @@ -0,0 +1,74 @@ +from __future__ import annotations + +import argparse +import json +import subprocess + +from .common import make_compose +from .up import handler as up_handler + + +def foreign_endpoints(rows: str, project: str) -> list[str]: + """Return the running containers of other projects on the network. + + Args: + rows: ``docker ps`` lines of ``\\t``. + project: compose project that owns the network. + """ + names = [] + for row in rows.splitlines(): + name, _, owner = row.partition("\t") + if name and owner != project: + names.append(name) + return names + + +def _docker(*args: str, capture: bool = False) -> str: + result = subprocess.run( + ["docker", *args], check=True, capture_output=capture, text=True + ) + return result.stdout if capture else "" + + +def add_parser(sub: argparse._SubParsersAction) -> None: + p = sub.add_parser( + "recreate", + help="Recreate the default network, keeping volumes and foreign stacks.", + ) + p.set_defaults(_handler=handler) + + +def handler(args: argparse.Namespace) -> int: + compose = make_compose() + rendered = compose.run(["config", "--format", "json"], capture=True).stdout + config = json.loads(rendered) + project = config["name"] + network = config["networks"]["default"]["name"] + + rows = _docker( + "ps", + "--filter", + f"network={network}", + "--format", + '{{.Names}}\t{{.Label "com.docker.compose.project"}}', + capture=True, + ) + detached = foreign_endpoints(rows, project) + for name in detached: + print(f">>> Detaching {name} from {network}") + _docker("network", "disconnect", network, name) + + rc = 1 + try: + compose.run(["down", "--remove-orphans"]) + rc = up_handler(argparse.Namespace(skip_entry_init=False, when_down=False)) + finally: + if rc != 0 and detached: + print(f">>> Still detached from {network}: {' '.join(detached)}") + if rc != 0: + return rc + + for name in detached: + print(f">>> Attaching {name} to {network}") + _docker("network", "connect", network, name) + return 0 diff --git a/tests/unit/python/cli/administration/deploy/development/test_recreate.py b/tests/unit/python/cli/administration/deploy/development/test_recreate.py new file mode 100644 index 0000000000..24a64956ef --- /dev/null +++ b/tests/unit/python/cli/administration/deploy/development/test_recreate.py @@ -0,0 +1,110 @@ +from __future__ import annotations + +import argparse +import json +import subprocess +import unittest +from unittest.mock import MagicMock, patch + +from cli.administration.deploy.development import recreate + +_CONFIG = json.dumps( + {"name": "infinito-nexus-core", "networks": {"default": {"name": "core_default"}}} +) +_ROWS = ( + "infinito_nexus_debian\tinfinito-nexus-core\n" + "infinito_nexus_fix_a\tfix-a\n" + "infinito-coredns\tinfinito-nexus-core\n" + "infinito_nexus_feature_b\tfeature-b\n" +) + + +class TestForeignEndpoints(unittest.TestCase): + def test_keeps_only_other_projects(self) -> None: + self.assertEqual( + ["infinito_nexus_fix_a", "infinito_nexus_feature_b"], + recreate.foreign_endpoints(_ROWS, "infinito-nexus-core"), + ) + + +class TestHandler(unittest.TestCase): + def _run(self, up_rc: int) -> tuple[int, list[tuple[str, ...]]]: + calls: list[tuple[str, ...]] = [] + compose = MagicMock() + + def compose_run(args, **_kwargs): + calls.append(("compose", *args)) + return subprocess.CompletedProcess(args, 0, stdout=_CONFIG) + + def docker(*args, capture=False): + calls.append(("docker", *args)) + return _ROWS if capture else "" + + def up(_args): + calls.append(("up",)) + return up_rc + + compose.run.side_effect = compose_run + with ( + patch.object(recreate, "make_compose", return_value=compose), + patch.object(recreate, "_docker", side_effect=docker), + patch.object(recreate, "up_handler", side_effect=up), + ): + rc = recreate.handler(argparse.Namespace()) + return rc, calls + + def test_detaches_recreates_without_volumes_and_reattaches(self) -> None: + rc, calls = self._run(up_rc=0) + + self.assertEqual(0, rc) + self.assertEqual( + [ + ( + "docker", + "network", + "disconnect", + "core_default", + "infinito_nexus_fix_a", + ), + ( + "docker", + "network", + "disconnect", + "core_default", + "infinito_nexus_feature_b", + ), + ("compose", "down", "--remove-orphans"), + ("up",), + ( + "docker", + "network", + "connect", + "core_default", + "infinito_nexus_fix_a", + ), + ( + "docker", + "network", + "connect", + "core_default", + "infinito_nexus_feature_b", + ), + ], + [ + c + for c in calls + if c[:2] != ("compose", "config") and c[:2] != ("docker", "ps") + ], + ) + + def test_failed_up_leaves_foreign_stacks_detached_and_reports(self) -> None: + rc, calls = self._run(up_rc=3) + + self.assertEqual(3, rc) + self.assertNotIn( + "connect", [c[2] for c in calls if c[:2] == ("docker", "network")] + ) + + +if __name__ == "__main__": + unittest.main() From 9b7f25f2feedd05926e3cfedcdca14f617c5b9f5 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Tue, 20 Oct 2026 16:28:34 +0200 Subject: [PATCH 18/28] fix(svc-storage-nfs-client): stop requesting directory delegations from ganesha < 9.11 Every swarm job on ubuntu-26.04 runners (kernel 7.0) died at the NFS probe write with `[Errno 121] Remote I/O error`. ganesha's NFSv4 debug log in the rescue bundles shows the cause on every client, the same second as the failure: Request 2: opcode 46 is OP_GET_DIR_DELEGATION Status of OP_GET_DIR_DELEGATION in position 2 = NFS4ERR_OP_ILLEGAL Linux 6.19 added directory delegations: after ACCESS, CREATE, REMOVE or RENAME the client adds GET_DIR_DELEGATION to the directory's next GETATTR (`should_request_dir_deleg`, gated per request by the nfsv4 parameter `directory_delegations`). nfs-ganesha before 9.11 routes op 46 to `nfs4_op_illegal`, whose reply carries opcode 10044; the client decoder expects 46 and fails the call with EREMOTEIO. Upstream fixed it in nfs-ganesha#1385 (98eb77c4, first in V9.11), which answers NOTSUPP; the client then drops the capability. The distro packages the role installs predate it: Debian trixie 6.5-5, Ubuntu 26.04 6.5-8. Every client and the controller now read the server's installed nfs-ganesha release and, below 9.11, set `directory_delegations=N` right after the mount and before the first access (the mount itself never sends op 46), plus `options nfsv4 directory_delegations=N` in /etc/modprobe.d for later module loads. No ganesha release grants directory delegations, so the end state equals a fixed server. The parameter is kernel-wide and is only ever set to N, so clients sharing one kernel cannot flip it back on each other. Servers on 9.11 or later and the kernel nfsd flavor keep the client default. Co-Authored-By: Claude Opus 5.5 (1M context) --- roles/svc-storage-nfs-client/README.md | 6 +++++ .../files/shell/directory_delegations.sh | 8 ++++++ .../tasks/directory_delegations.yml | 27 +++++++++++++++++++ roles/svc-storage-nfs-client/tasks/main.yml | 4 +++ .../tasks/03_controller_mount.yml | 4 +++ 5 files changed, 49 insertions(+) create mode 100644 roles/svc-storage-nfs-client/files/shell/directory_delegations.sh create mode 100644 roles/svc-storage-nfs-client/tasks/directory_delegations.yml diff --git a/roles/svc-storage-nfs-client/README.md b/roles/svc-storage-nfs-client/README.md index ec97c70692..b526991b84 100644 --- a/roles/svc-storage-nfs-client/README.md +++ b/roles/svc-storage-nfs-client/README.md @@ -50,6 +50,12 @@ Solid `1:1` edges are fixed relationships; dashed `0..1` edges are conditional ( Generations written to the share before the bind stay under the export's `backup/` directory, hidden from the nodes; remove them there by hand. +- **Directory delegations off against old ganesha:** When the server + runs nfs-ganesha older than 9.11, every client and the controller set + the kernel's `nfsv4` parameter `directory_delegations` to `N`, at + runtime and in `/etc/modprobe.d`. The parameter is kernel-wide, so on + a lab whose nodes share one kernel it covers every NFS mount of that + host until the module reloads. ## Quick Setup diff --git a/roles/svc-storage-nfs-client/files/shell/directory_delegations.sh b/roles/svc-storage-nfs-client/files/shell/directory_delegations.sh new file mode 100644 index 0000000000..2d95882dbd --- /dev/null +++ b/roles/svc-storage-nfs-client/files/shell/directory_delegations.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -eu +param=/sys/module/nfsv4/parameters/directory_delegations + +[ -e "${param}" ] || exit 0 +[ "$(cat "${param}")" = N ] && exit 0 +echo N >"${param}" +echo CHANGED diff --git a/roles/svc-storage-nfs-client/tasks/directory_delegations.yml b/roles/svc-storage-nfs-client/tasks/directory_delegations.yml new file mode 100644 index 0000000000..e7418ef8f2 --- /dev/null +++ b/roles/svc-storage-nfs-client/tasks/directory_delegations.yml @@ -0,0 +1,27 @@ +--- +- name: "πŸ” Read the NFS server's installed packages" + ansible.builtin.package_facts: + delegate_to: "{{ groups['svc-storage-nfs-server'] | first }}" + delegate_facts: true + run_once: true + +- name: "πŸ”§ Stop requesting directory delegations from a ganesha that rejects them" + when: >- + (hostvars[groups['svc-storage-nfs-server'] | first].ansible_facts.packages['nfs-ganesha'] | first).version + is version('9.11', '<') + block: + - name: "πŸ”§ Keep directory delegations off whenever nfsv4 loads" + ansible.builtin.lineinfile: + path: /etc/modprobe.d/infinito-nfs-directory-delegations.conf + line: "options nfsv4 directory_delegations=N" + create: true + owner: root + group: root + mode: "0644" + + - name: "πŸ”§ Turn directory delegations off in the running kernel" + ansible.builtin.script: + cmd: "{{ lookup('path_absolute', 'roles/svc-storage-nfs-client/files/shell/directory_delegations.sh') }}" + executable: /bin/bash + register: svc_storage_nfs_client_directory_delegations + changed_when: "'CHANGED' in svc_storage_nfs_client_directory_delegations.stdout" diff --git a/roles/svc-storage-nfs-client/tasks/main.yml b/roles/svc-storage-nfs-client/tasks/main.yml index 0fc67d391c..68241b326d 100644 --- a/roles/svc-storage-nfs-client/tasks/main.yml +++ b/roles/svc-storage-nfs-client/tasks/main.yml @@ -51,6 +51,10 @@ failed_when: - svc_storage_nfs_client_probe is failed +- name: "πŸ”§ Align the NFS client's directory delegations with the ganesha server" + ansible.builtin.include_tasks: directory_delegations.yml + when: lookup('nfs_flavor') == 'ganesha' + - name: "πŸ” Probe-write a marker to confirm the mount is RW" ansible.builtin.copy: dest: "{{ SVC_STORAGE_NFS_CLIENT_PROBE_DIR }}/.svc-storage-nfs-client-probe.{{ inventory_hostname }}" diff --git a/roles/svc-storage-nfs-server/tasks/03_controller_mount.yml b/roles/svc-storage-nfs-server/tasks/03_controller_mount.yml index fdaddf1a6b..62e83c01e4 100644 --- a/roles/svc-storage-nfs-server/tasks/03_controller_mount.yml +++ b/roles/svc-storage-nfs-server/tasks/03_controller_mount.yml @@ -59,3 +59,7 @@ retries: 12 delay: 5 until: _ctrl_nfs_mount is succeeded + +- name: "πŸ”§ Align the controller's directory delegations with the ganesha server" + ansible.builtin.include_tasks: "{{ lookup('path_absolute', 'roles/svc-storage-nfs-client/tasks/directory_delegations.yml') }}" + when: lookup('nfs_flavor') == 'ganesha' From 21eba1e88f5e9b7cd0542f06a3f4be685032f5f5 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Tue, 20 Oct 2026 18:09:07 +0200 Subject: [PATCH 19/28] fix(backup): carry hard links, ACLs and xattrs on every backup and recover hop baudolo 7.1.x writes volume generations with `rsync -aHAX`, so the producing host keeps `user.*` extended attributes (OpenCloud keeps its storage metadata in `user.oc.*`). Every later copy dropped them again, because none of the other rsync calls passed `-X` (nor `-A`/`-H`): - NFS export snapshots (svc-bkp-nfs-2-local `script.sh`), - the backup host's pull (svc-bkp-remote-2-local `pull_specific_host.py`), - the device sync (svc-bkp-local-2-device `script.py`), - every recover into a live target (`utils/recovery/base.py`, used by the volume, NFS and device recover scripts), - the `recover full` and `recover remote` tree pulls. A disaster recovery through the documented chain therefore restored OpenCloud without `user.oc.*`, silently: the copies succeeded and the dirval stamp does not cover attributes. All of these now run `rsync -aHAX` (`-aHAXP` for the device sync). A target filesystem without xattr support makes rsync exit non-zero as soon as a source file carries attributes, so the hop fails loudly instead of dropping them; there is no per-device opt-out. The single-image pull in `recover remote` keeps `-a`. The provider's ssh-wrapper needs no change: its glob accepts any server flags and its character guard allows them. Two tests write a `user.oc.id` attribute and assert it survives the NFS snapshot and the recover; with the old `-a` both fail with `OSError: [Errno 61] No data available`. Closes #1021 Co-Authored-By: Claude Opus 5.5 (1M context) --- cli/administration/recover/README.md | 2 +- cli/administration/recover/full.py | 2 +- cli/administration/recover/remote.py | 2 +- cli/administration/recover/schema.yml | 2 +- roles/svc-bkp-local-2-device/README.md | 2 +- roles/svc-bkp-local-2-device/files/python/recover.py | 2 +- roles/svc-bkp-local-2-device/files/python/script.py | 2 +- roles/svc-bkp-nfs-2-local/README.md | 4 ++-- roles/svc-bkp-nfs-2-local/files/python/recover.py | 2 +- roles/svc-bkp-nfs-2-local/files/shell/script.sh | 2 +- .../files/python/pull_specific_host.py | 2 +- roles/svc-bkp-volume-2-local/README.md | 4 ++-- roles/svc-bkp-volume-2-local/files/python/recover.py | 2 +- .../python/cli/administration/recover/test_full.py | 4 ++-- .../roles/svc-bkp-nfs-2-local/files/test_script.py | 8 ++++++++ tests/unit/python/utils/recovery/test_base.py | 10 ++++++++++ utils/recovery/base.py | 4 ++-- 17 files changed, 37 insertions(+), 19 deletions(-) diff --git a/cli/administration/recover/README.md b/cli/administration/recover/README.md index 64aa753e32..a11c05d2d9 100644 --- a/cli/administration/recover/README.md +++ b/cli/administration/recover/README.md @@ -78,7 +78,7 @@ expanding `volume` per docker volume. ## Safety -Each recover mirrors a snapshot with `rsync -a --delete` and first runs the +Each recover mirrors a snapshot with `rsync -aHAX --delete` and first runs the role's backup unit as a rollback point. `--no-safety-backup` skips that (only when the target holds nothing worth saving: fresh host / empty / disposable). `--preview` prints the exact commands without running them. diff --git a/cli/administration/recover/full.py b/cli/administration/recover/full.py index 63e90ea3f9..b73bbe888a 100644 --- a/cli/administration/recover/full.py +++ b/cli/administration/recover/full.py @@ -72,7 +72,7 @@ def plan(root: Path) -> tuple[list[tuple[str, str]], list[str]]: def _pull_cmd(source: str) -> list[str]: remote = source if ":" in source else f"{source}:{paths.BACKUP_ROOT}" - return ["rsync", "-a", f"{remote}/", f"{PULL_STAGE}/"] + return ["rsync", "-aHAX", f"{remote}/", f"{PULL_STAGE}/"] def _is_device(source: str) -> bool: diff --git a/cli/administration/recover/remote.py b/cli/administration/recover/remote.py index 44c7b72994..2626ea42f5 100644 --- a/cli/administration/recover/remote.py +++ b/cli/administration/recover/remote.py @@ -47,7 +47,7 @@ def commands( return ( [ ["ssh", host, shlex.join(remote)], - ["rsync", "-a", f"{host}:{REMOTE_ROOT}/", f"{LOCAL_ROOT}/"], + ["rsync", "-aHAX", f"{host}:{REMOTE_ROOT}/", f"{LOCAL_ROOT}/"], ], LOCAL_ROOT, ) diff --git a/cli/administration/recover/schema.yml b/cli/administration/recover/schema.yml index 8a14dda950..a854298a23 100644 --- a/cli/administration/recover/schema.yml +++ b/cli/administration/recover/schema.yml @@ -58,4 +58,4 @@ flags: skip the pre-recover safety backup of the current target (only when it holds nothing worth saving: fresh host / empty / disposable) -mechanism: "rsync -a --delete, preceded by a pre-recover safety backup by default" +mechanism: "rsync -aHAX --delete, preceded by a pre-recover safety backup by default" diff --git a/roles/svc-bkp-local-2-device/README.md b/roles/svc-bkp-local-2-device/README.md index aca5be1eb3..962a9a2ce9 100644 --- a/roles/svc-bkp-local-2-device/README.md +++ b/roles/svc-bkp-local-2-device/README.md @@ -97,7 +97,7 @@ Run `files/python/recover.py` on the host with the backup device attached: recover.py /dev/sdX1 /mnt/usb-recover /var/lib/infinito/backup [--snapshot ] ``` -The script opens the LUKS device (interactive passphrase prompt; `--passphrase-stdin` for automation), mounts it, picks the newest device snapshot across every machine hash (a fresh host after total loss has a new machine id), mirrors it into the target (`rsync -a --delete`) and unmounts/closes the device again. No pre-recover service backup runs: the device itself is the backup this role maintains. Afterwards restore individual applications from the backup root with the matching role's `recover.py` (`svc-bkp-volume-2-local` for docker volumes, `svc-bkp-nfs-2-local` for NFS exports). +The script opens the LUKS device (interactive passphrase prompt; `--passphrase-stdin` for automation), mounts it, picks the newest device snapshot across every machine hash (a fresh host after total loss has a new machine id), mirrors it into the target (`rsync -aHAX --delete`) and unmounts/closes the device again. No pre-recover service backup runs: the device itself is the backup this role maintains. Afterwards restore individual applications from the backup root with the matching role's `recover.py` (`svc-bkp-volume-2-local` for docker volumes, `svc-bkp-nfs-2-local` for NFS exports). ## Credits diff --git a/roles/svc-bkp-local-2-device/files/python/recover.py b/roles/svc-bkp-local-2-device/files/python/recover.py index beaff5b0b6..f7c21954df 100755 --- a/roles/svc-bkp-local-2-device/files/python/recover.py +++ b/roles/svc-bkp-local-2-device/files/python/recover.py @@ -8,7 +8,7 @@ Opens the LUKS device (interactive passphrase prompt, or stdin with ``--passphrase-stdin``), mounts it, picks the newest device snapshot (any machine hash: a fresh host after total loss has a new /etc/machine-id) and mirrors it into the target -(``rsync -a --delete``), then unmounts and closes the device again. +(``rsync -aHAX --delete``), then unmounts and closes the device again. No pre-recover service backup runs: the device itself is the backup this role maintains. diff --git a/roles/svc-bkp-local-2-device/files/python/script.py b/roles/svc-bkp-local-2-device/files/python/script.py index 222193d962..9e6a162635 100755 --- a/roles/svc-bkp-local-2-device/files/python/script.py +++ b/roles/svc-bkp-local-2-device/files/python/script.py @@ -53,7 +53,7 @@ def main(): try: rsync_command = [ "rsync", - "-aP", + "-aHAXP", "--numeric-ids", "--delete", "--delete-excluded", diff --git a/roles/svc-bkp-nfs-2-local/README.md b/roles/svc-bkp-nfs-2-local/README.md index 8b2168073c..0acd7775f5 100644 --- a/roles/svc-bkp-nfs-2-local/README.md +++ b/roles/svc-bkp-nfs-2-local/README.md @@ -44,7 +44,7 @@ flowchart TD UNIT["svc-bkp-nfs-2-local.<version>.<domain>.service"] --> LOCK["ExecStartPre: sys-lock against the manipulation group"] LOCK --> SCRIPT["ExecStart: script.sh <export_base> <backups_dir>
backup-nfs-to-local <state>/backup"] SCRIPT -->|export_base missing| ALARM["ERROR exit 1
OnFailure alarm"] - SCRIPT --> RSYNC["rsync -a --delete
--exclude /<state>/backup (no backups-in-backups)
--link-dest previous generation (hard links)"] + SCRIPT --> RSYNC["rsync -aHAX --delete
--exclude /<state>/backup (no backups-in-backups)
--link-dest previous generation (hard links)"] RSYNC -->|exit 24: live export churn| WARN["WARN, snapshot kept"] RSYNC -->|any other failure| CLEAN["generation removed, exit != 0"] RSYNC --> TREE["<backups_dir>/<sha256(machine-id)>/
backup-nfs-to-local/<YYYYmmddHHMMSS>/files/..."] @@ -105,7 +105,7 @@ recover.py //backup-nfs-to-local//files/`. -2. Run the script; it first starts the role's deployed backup unit (a fresh differential `backup-nfs-to-local` generation of the live export), then mirrors the snapshot into the target (`rsync -a --delete`, with the shared `/backup` root protected from deletion and never copied in). `--no-safety-backup` skips the unit run when the target holds nothing worth saving. +2. Run the script; it first starts the role's deployed backup unit (a fresh differential `backup-nfs-to-local` generation of the live export), then mirrors the snapshot into the target (`rsync -aHAX --delete`, with the shared `/backup` root protected from deletion and never copied in). `--no-safety-backup` skips the unit run when the target holds nothing worth saving. 3. Redeploy the stack; the NFS clients re-mount and pick up the restored state. The target subtree must already exist; the script refuses to create export subtrees implicitly. diff --git a/roles/svc-bkp-nfs-2-local/files/python/recover.py b/roles/svc-bkp-nfs-2-local/files/python/recover.py index ef159bed13..a2b0f7ebfe 100755 --- a/roles/svc-bkp-nfs-2-local/files/python/recover.py +++ b/roles/svc-bkp-nfs-2-local/files/python/recover.py @@ -6,7 +6,7 @@ Runs the role's deployed backup unit first (the usual differential ``backup-nfs-to-local`` generation of the live export), then mirrors the -snapshot into the target (``rsync -a --delete``). Run on the host +snapshot into the target (``rsync -aHAX --delete``). Run on the host serving the export with every consuming stack stopped; NFS clients re-mount on redeploy. diff --git a/roles/svc-bkp-nfs-2-local/files/shell/script.sh b/roles/svc-bkp-nfs-2-local/files/shell/script.sh index b353e816a3..415a7e893e 100644 --- a/roles/svc-bkp-nfs-2-local/files/shell/script.sh +++ b/roles/svc-bkp-nfs-2-local/files/shell/script.sh @@ -45,7 +45,7 @@ trap cleanup_failed_generation ERR mkdir -p "${DEST_DIR}" -rsync_args=(-a --numeric-ids --delete) +rsync_args=(-aHAX --numeric-ids --delete) if [[ -n "${EXCLUDE_REL}" ]]; then rsync_args+=(--exclude "/${EXCLUDE_REL#/}") fi diff --git a/roles/svc-bkp-remote-2-local/files/python/pull_specific_host.py b/roles/svc-bkp-remote-2-local/files/python/pull_specific_host.py index 3c57de188f..63553b9e81 100755 --- a/roles/svc-bkp-remote-2-local/files/python/pull_specific_host.py +++ b/roles/svc-bkp-remote-2-local/files/python/pull_specific_host.py @@ -120,7 +120,7 @@ def pull_backups(hostname: str, backups_dir: str) -> None: Path(local_backup_destination_path).mkdir(parents=True, exist_ok=True) rsync_command = ( - f"rsync -a --partial --info=stats2 " + f"rsync -aHAX --partial --info=stats2 " f"--numeric-ids --delete --delete-excluded --timeout=300 " f'-e "ssh {SSH_OPTS}" ' f'--rsync-path="sudo rsync" ' diff --git a/roles/svc-bkp-volume-2-local/README.md b/roles/svc-bkp-volume-2-local/README.md index 1a09e9415b..196385d452 100644 --- a/roles/svc-bkp-volume-2-local/README.md +++ b/roles/svc-bkp-volume-2-local/README.md @@ -78,7 +78,7 @@ flowchart TD TREE --> RECOVER["files/python/recover.py volume
resolves Mountpoint + Options"] RECOVER -->|"Options set"| PROBE{"mountpoint -q
(ssh for --docker-host)"} PROBE -->|unmounted| REFUSE["SystemExit: a restore would land
on the node disk and be shadowed
on the next mount"] - PROBE -->|mounted| RSYNC["rsync -a --delete through the
mounted export onto the server"] + PROBE -->|mounted| RSYNC["rsync -aHAX --delete through the
mounted export onto the server"] DRILL["DR drill [9/9]
mounts the export at the volume
mountpoint with the volume's own opts"] --> RECOVER RSYNC --> VERIFY["marker asserted on the
NFS server's disk"] ``` @@ -304,7 +304,7 @@ recover.py //backup-docker-to-local// ``` 1. Stop the consuming project (`docker compose down` / `docker stack rm `). -2. Run the script; it first starts the role's deployed backup unit (a fresh differential baudolo generation of every volume and database), resolves the volume's mountpoint and mirrors the snapshot into it (`rsync -a --delete`). `--no-safety-backup` skips the unit run when the target holds nothing worth saving. +2. Run the script; it first starts the role's deployed backup unit (a fresh differential baudolo generation of every volume and database), resolves the volume's mountpoint and mirrors the snapshot into it (`rsync -aHAX --delete`). `--no-safety-backup` skips the unit run when the target holds nothing worth saving. 3. Restore the databases with the `--databases` mode above **while the project is still down**, then start it again; on swarm, NFS-backed volumes are restored via `svc-bkp-nfs-2-local`'s `recover.py` instead (see below). ## Credits diff --git a/roles/svc-bkp-volume-2-local/files/python/recover.py b/roles/svc-bkp-volume-2-local/files/python/recover.py index b1ecdc5873..5d4d0829bf 100755 --- a/roles/svc-bkp-volume-2-local/files/python/recover.py +++ b/roles/svc-bkp-volume-2-local/files/python/recover.py @@ -9,7 +9,7 @@ Both modes run the role's deployed backup unit first, unless ``--no-safety-backup`` says the target holds nothing worth saving. Volume mode mirrors the snapshot into the volume's mountpoint -(``rsync -a --delete``); stop the consuming project first. With +(``rsync -aHAX --delete``); stop the consuming project first. With ``--docker-host ssh://user@host`` it works on a remote host. Database mode (``--databases``) replays every dump with diff --git a/tests/unit/python/cli/administration/recover/test_full.py b/tests/unit/python/cli/administration/recover/test_full.py index 6d980f1b5f..7395eef314 100644 --- a/tests/unit/python/cli/administration/recover/test_full.py +++ b/tests/unit/python/cli/administration/recover/test_full.py @@ -74,7 +74,7 @@ class PullCmdTest(unittest.TestCase): full._pull_cmd("user@srchost"), [ "rsync", - "-a", + "-aHAX", f"user@srchost:{paths.BACKUP_ROOT}/", f"{full.PULL_STAGE}/", ], @@ -83,7 +83,7 @@ class PullCmdTest(unittest.TestCase): def test_host_with_explicit_path(self): self.assertEqual( full._pull_cmd("host:/custom/root"), - ["rsync", "-a", "host:/custom/root/", f"{full.PULL_STAGE}/"], + ["rsync", "-aHAX", "host:/custom/root/", f"{full.PULL_STAGE}/"], ) diff --git a/tests/unit/python/roles/svc-bkp-nfs-2-local/files/test_script.py b/tests/unit/python/roles/svc-bkp-nfs-2-local/files/test_script.py index 452821bcec..acf56c75f5 100644 --- a/tests/unit/python/roles/svc-bkp-nfs-2-local/files/test_script.py +++ b/tests/unit/python/roles/svc-bkp-nfs-2-local/files/test_script.py @@ -74,6 +74,14 @@ class NfsBackupScriptTests(unittest.TestCase): ).read_text() # nocheck: cache-read tempdir fixture self.assertEqual(content, "hello") + def test_snapshot_keeps_user_xattrs(self): + (self.source / "a.txt").write_text("hello") + os.setxattr(self.source / "a.txt", "user.oc.id", b"space-1") + result = self._run() + self.assertEqual(result.returncode, 0, result.stderr) + snapshot = self._generations()[0] / "files" / "a.txt" + self.assertEqual(os.getxattr(snapshot, "user.oc.id"), b"space-1") + def test_second_run_hardlinks_unchanged_files(self): (self.source / "a.txt").write_text("hello") self.assertEqual(self._run(generation="20240101000000").returncode, 0) diff --git a/tests/unit/python/utils/recovery/test_base.py b/tests/unit/python/utils/recovery/test_base.py index b084fba0f6..e4672cc0e9 100644 --- a/tests/unit/python/utils/recovery/test_base.py +++ b/tests/unit/python/utils/recovery/test_base.py @@ -1,5 +1,6 @@ from __future__ import annotations +import os import tempfile import unittest import unittest.mock as mock @@ -55,6 +56,15 @@ class TestDirectoryRecovery(unittest.TestCase): self.assertEqual(text, "from-snapshot") self.assertFalse((target / "live.txt").exists()) + def test_restore_keeps_user_xattrs(self) -> None: + with tempfile.TemporaryDirectory() as td: + source, target = self._mk(td) + os.setxattr(source / "restored.txt", "user.oc.id", b"space-1") + _Recovery(str(source), str(target)).restore() + self.assertEqual( + os.getxattr(target / "restored.txt", "user.oc.id"), b"space-1" + ) + def test_missing_unit_refuses(self) -> None: with tempfile.TemporaryDirectory() as td: source, target = self._mk(td) diff --git a/utils/recovery/base.py b/utils/recovery/base.py index 3d7087c31e..6ca5a51ad1 100644 --- a/utils/recovery/base.py +++ b/utils/recovery/base.py @@ -1,7 +1,7 @@ """Abstract recover flow shared by every ``svc-bkp-*`` role. A recover mirrors a snapshot into a live target directory -(``rsync -a --delete``), so the target's current state would be lost +(``rsync -aHAX --delete``), so the target's current state would be lost without a net. The flow therefore first runs the role's deployed backup service (``systemctl start`` on the role's unit), which applies the usual differential backup logic to the live data before anything is @@ -87,7 +87,7 @@ class DirectoryRecovery(ABC): subprocess.run( [ "rsync", - "-a", + "-aHAX", "--numeric-ids", "--delete", *self.rsync_extra_args, From e2501cb1deed3ace0e03a2d64b1f6dedc5cd1545 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Tue, 20 Oct 2026 21:11:50 +0200 Subject: [PATCH 20/28] Release version 16.2.0 --- CHANGELOG.md | 33 +++ compose/package-cache-frontend/upstreams.conf | 205 +++++++++++------- packaging/arch/PKGBUILD | 2 +- packaging/debian/changelog | 35 +++ packaging/fedora/infinito-nexus.spec | 34 ++- pyproject.toml | 2 +- 6 files changed, 224 insertions(+), 87 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a1626fff7..b491b18d30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,38 @@ # Changelog +## [16.2.0] - 2026-10-20 + +Added: + +- Compose: *make compose-network-recreate* rebuilds the stack network safely +- CI: the updater keeps the GitHub-hosted runner labels current + +Fixed: + +- Backup: hard links, ACLs and xattrs survive every backup and recover hop +- NFS client: clients on Linux 6.19+ no longer fail writes on ganesha < 9.11 +- AppArmor: the teardown keeps docker-default, so containers keep starting +- Wazuh: hosts can be put into WireGuard meshes like every other role +- CI: GitHub-hosted runners are pinned to ubuntu-24.04 + +Version jumps (net since 16.1.0): + +- *svc-db-rabbitmq*: 4.3-management-alpine to 4.4-management-alpine +- *web-app-n8n*: 2.44.4 to 2.45.0 +- *web-app-pixelfed*: 20261019 to 20261020 +- *web-app-snipe-it*: v8.8.0-alpine to v8.8.1-alpine +- *web-app-taiga*: RabbitMQ 4.3-management-alpine to 4.4-management-alpine +- *eslint* (dev): ^10.12.0 to ^10.13.0 + +Upgrade notes: + +- Backup: a target without xattr support now fails instead of losing them +- Dev: worktree hosts apply the IP pool with *make compose-network-recreate* + +Contributors: + +- [Kevin Veen-Birkenbach](https://veen.world): backups, NFS, AppArmor, CI + ## [16.1.0] - 2026-10-19 Added: diff --git a/compose/package-cache-frontend/upstreams.conf b/compose/package-cache-frontend/upstreams.conf index b5c5ef08e7..9e0acd5a03 100644 --- a/compose/package-cache-frontend/upstreams.conf +++ b/compose/package-cache-frontend/upstreams.conf @@ -9,49 +9,8 @@ proxy_request_buffering off; proxy_read_timeout 300s; proxy_connect_timeout 30s; -# Docker embedded DNS, pinned to bypass image-specific resolv.conf order. resolver 127.0.0.11 ipv6=off valid=30s; -# Nexus answers 502 when a proxy's remote is unreachable (measured) and has no -# group repository type for apt, so the mirror failover sits here rather than in -# the repo layout. 5xx only: a 404 means the file genuinely is not in the suite, -# and apt probes for missing files often enough that retrying those would double -# the request rate. -# -# Nexus answers that 502 only after trying every address of the remote in turn, -# one connect timeout each; for a remote with several addresses that outlasts a -# client's own timeout before the mirror is ever asked. -# -# These stay prefix locations. A regex location forfeits nginx's implicit -# `/debian` -> `/debian/` redirect, and the URL-reachability test probes exactly -# that slash-less form through the runner's DNS hijack, so it turns into a 404. -server { - listen 80; - server_name deb.debian.org; - location /debian/ { - rewrite ^/debian/(.*)$ /repository/apt-debian/$1 break; - proxy_intercept_errors on; - proxy_read_timeout 8s; - error_page 502 503 504 = @debian_mirror; - proxy_pass http://package-cache:8081; - } - location @debian_mirror { - rewrite ^/repository/apt-debian/(.*)$ /repository/apt-debian-mirror/$1 break; - proxy_pass http://package-cache:8081; - } - location /debian-security/ { - rewrite ^/debian-security/(.*)$ /repository/apt-debian-security/$1 break; - proxy_intercept_errors on; - proxy_read_timeout 8s; - error_page 502 503 504 = @debian_security_mirror; - proxy_pass http://package-cache:8081; - } - location @debian_security_mirror { - rewrite ^/repository/apt-debian-security/(.*)$ /repository/apt-debian-security-mirror/$1 break; - proxy_pass http://package-cache:8081; - } -} - server { listen 80; server_name archive.ubuntu.com; @@ -59,26 +18,59 @@ server { rewrite ^/ubuntu/(.*)$ /repository/apt-ubuntu/$1 break; proxy_intercept_errors on; proxy_read_timeout 8s; - error_page 502 503 504 = @ubuntu_mirror; + error_page 502 503 504 = @apt_ubuntu_mirror; proxy_pass http://package-cache:8081; } - location @ubuntu_mirror { + location @apt_ubuntu_mirror { rewrite ^/repository/apt-ubuntu/(.*)$ /repository/apt-ubuntu-mirror/$1 break; proxy_pass http://package-cache:8081; } } + +server { + listen 443 ssl; + server_name argos-net.com; + ssl_certificate /etc/nginx/certs/argos-net.com.crt; + ssl_certificate_key /etc/nginx/certs/argos-net.com.key; + location / { + proxy_read_timeout 1800s; + proxy_pass http://package-cache:8081/repository/raw-argos-net/; + } +} + +server { + listen 443 ssl; + server_name codeload.github.com; + ssl_certificate /etc/nginx/certs/codeload.github.com.crt; + ssl_certificate_key /etc/nginx/certs/codeload.github.com.key; + location / { + proxy_pass http://package-cache:8081/repository/raw-codeload-github/; + } +} + server { listen 80; - server_name security.ubuntu.com; - location /ubuntu/ { - rewrite ^/ubuntu/(.*)$ /repository/apt-ubuntu-security/$1 break; + server_name deb.debian.org; + location /debian/ { + rewrite ^/debian/(.*)$ /repository/apt-debian/$1 break; proxy_intercept_errors on; proxy_read_timeout 8s; - error_page 502 503 504 = @ubuntu_security_mirror; + error_page 502 503 504 = @apt_debian_mirror; proxy_pass http://package-cache:8081; } - location @ubuntu_security_mirror { - rewrite ^/repository/apt-ubuntu-security/(.*)$ /repository/apt-ubuntu-security-mirror/$1 break; + location @apt_debian_mirror { + rewrite ^/repository/apt-debian/(.*)$ /repository/apt-debian-mirror/$1 break; + proxy_pass http://package-cache:8081; + } + location /debian-security/ { + rewrite ^/debian-security/(.*)$ /repository/apt-debian-security/$1 break; + proxy_intercept_errors on; + proxy_read_timeout 8s; + error_page 502 503 504 = @apt_debian_security_mirror; + proxy_pass http://package-cache:8081; + } + location @apt_debian_security_mirror { + rewrite ^/repository/apt-debian-security/(.*)$ /repository/apt-debian-security-mirror/$1 break; proxy_pass http://package-cache:8081; } } @@ -94,11 +86,11 @@ server { server { listen 443 ssl; - server_name pypi.org; - ssl_certificate /etc/nginx/certs/pypi.org.crt; - ssl_certificate_key /etc/nginx/certs/pypi.org.key; + server_name dl-cdn.alpinelinux.org; + ssl_certificate /etc/nginx/certs/dl-cdn.alpinelinux.org.crt; + ssl_certificate_key /etc/nginx/certs/dl-cdn.alpinelinux.org.key; location / { - proxy_pass http://package-cache:8081/repository/pypi-proxy/; + proxy_pass http://package-cache:8081/repository/raw-alpine/; } } @@ -114,28 +106,24 @@ server { server { listen 443 ssl; - server_name registry.npmjs.org; - ssl_certificate /etc/nginx/certs/registry.npmjs.org.crt; - ssl_certificate_key /etc/nginx/certs/registry.npmjs.org.key; - # Tarball URLs Nexus emits already carry /repository/npm-proxy/; pass them - # through as-is (no path on proxy_pass) so they aren't double-prefixed β†’ 404. - location /repository/npm-proxy/ { - proxy_pass http://package-cache:8081; - } + server_name huggingface.co; + ssl_certificate /etc/nginx/certs/huggingface.co.crt; + ssl_certificate_key /etc/nginx/certs/huggingface.co.key; location / { - proxy_pass http://package-cache:8081/repository/npm-proxy/; + proxy_pass http://package-cache:8081/repository/raw-huggingface/; } } server { listen 443 ssl; - server_name rubygems.org; - ssl_certificate /etc/nginx/certs/rubygems.org.crt; - ssl_certificate_key /etc/nginx/certs/rubygems.org.key; + server_name index.crates.io; + ssl_certificate /etc/nginx/certs/index.crates.io.crt; + ssl_certificate_key /etc/nginx/certs/index.crates.io.key; location / { - proxy_pass http://package-cache:8081/repository/gem-proxy/; + proxy_pass http://package-cache:8081/repository/raw-crates-index/; } } + server { listen 443 ssl; server_name index.rubygems.org; @@ -146,16 +134,6 @@ server { } } -server { - listen 443 ssl; - server_name repo.packagist.org; - ssl_certificate /etc/nginx/certs/repo.packagist.org.crt; - ssl_certificate_key /etc/nginx/certs/repo.packagist.org.key; - location / { - proxy_pass http://package-cache:8081/repository/raw-packagist/; - } -} - server { listen 443 ssl; server_name proxy.golang.org; @@ -168,11 +146,11 @@ server { server { listen 443 ssl; - server_name dl-cdn.alpinelinux.org; - ssl_certificate /etc/nginx/certs/dl-cdn.alpinelinux.org.crt; - ssl_certificate_key /etc/nginx/certs/dl-cdn.alpinelinux.org.key; + server_name pypi.org; + ssl_certificate /etc/nginx/certs/pypi.org.crt; + ssl_certificate_key /etc/nginx/certs/pypi.org.key; location / { - proxy_pass http://package-cache:8081/repository/raw-alpine/; + proxy_pass http://package-cache:8081/repository/pypi-proxy/; } } @@ -188,10 +166,69 @@ server { server { listen 443 ssl; - server_name codeload.github.com; - ssl_certificate /etc/nginx/certs/codeload.github.com.crt; - ssl_certificate_key /etc/nginx/certs/codeload.github.com.key; + server_name registry.npmjs.org; + ssl_certificate /etc/nginx/certs/registry.npmjs.org.crt; + ssl_certificate_key /etc/nginx/certs/registry.npmjs.org.key; + location /repository/npm-proxy/ { + proxy_pass http://package-cache:8081; + } location / { - proxy_pass http://package-cache:8081/repository/raw-codeload-github/; + proxy_pass http://package-cache:8081/repository/npm-proxy/; + } +} + +server { + listen 443 ssl; + server_name registry.ollama.ai; + ssl_certificate /etc/nginx/certs/registry.ollama.ai.crt; + ssl_certificate_key /etc/nginx/certs/registry.ollama.ai.key; + location / { + proxy_pass http://package-cache:8081/repository/raw-ollama-registry/; + } +} + +server { + listen 443 ssl; + server_name repo.packagist.org; + ssl_certificate /etc/nginx/certs/repo.packagist.org.crt; + ssl_certificate_key /etc/nginx/certs/repo.packagist.org.key; + location / { + proxy_pass http://package-cache:8081/repository/raw-packagist/; + } +} + +server { + listen 443 ssl; + server_name rubygems.org; + ssl_certificate /etc/nginx/certs/rubygems.org.crt; + ssl_certificate_key /etc/nginx/certs/rubygems.org.key; + location / { + proxy_pass http://package-cache:8081/repository/gem-proxy/; + } +} + +server { + listen 80; + server_name security.ubuntu.com; + location /ubuntu/ { + rewrite ^/ubuntu/(.*)$ /repository/apt-ubuntu-security/$1 break; + proxy_intercept_errors on; + proxy_read_timeout 8s; + error_page 502 503 504 = @apt_ubuntu_security_mirror; + proxy_pass http://package-cache:8081; + } + location @apt_ubuntu_security_mirror { + rewrite ^/repository/apt-ubuntu-security/(.*)$ /repository/apt-ubuntu-security-mirror/$1 break; + proxy_pass http://package-cache:8081; + } +} + +server { + listen 443 ssl; + server_name static.crates.io; + ssl_certificate /etc/nginx/certs/static.crates.io.crt; + ssl_certificate_key /etc/nginx/certs/static.crates.io.key; + location / { + proxy_pass http://package-cache:8081/repository/raw-crates-static/; } } diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD index 32b4c239a6..56668d0805 100644 --- a/packaging/arch/PKGBUILD +++ b/packaging/arch/PKGBUILD @@ -1,7 +1,7 @@ # Maintainer: Kevin Veen-Birkenbach pkgname=infinito-nexus -pkgver=16.1.0 +pkgver=16.2.0 pkgrel=1 pkgdesc="Meta package for Infinito.Nexus host dependencies (dev + CI)." arch=('any') diff --git a/packaging/debian/changelog b/packaging/debian/changelog index 7130893af6..3d1e3dfd16 100644 --- a/packaging/debian/changelog +++ b/packaging/debian/changelog @@ -1,3 +1,38 @@ +infinito-nexus (16.2.0-1) unstable; urgency=medium + + Added: + + - Compose: *make compose-network-recreate* rebuilds the stack network safely + - CI: the updater keeps the GitHub-hosted runner labels current + + Fixed: + + - Backup: hard links, ACLs and xattrs survive every backup and recover hop + - NFS client: clients on Linux 6.19+ no longer fail writes on ganesha < 9.11 + - AppArmor: the teardown keeps docker-default, so containers keep starting + - Wazuh: hosts can be put into WireGuard meshes like every other role + - CI: GitHub-hosted runners are pinned to ubuntu-24.04 + + Version jumps (net since 16.1.0): + + - *svc-db-rabbitmq*: 4.3-management-alpine to 4.4-management-alpine + - *web-app-n8n*: 2.44.4 to 2.45.0 + - *web-app-pixelfed*: 20261019 to 20261020 + - *web-app-snipe-it*: v8.8.0-alpine to v8.8.1-alpine + - *web-app-taiga*: RabbitMQ 4.3-management-alpine to 4.4-management-alpine + - *eslint* (dev): ^10.12.0 to ^10.13.0 + + Upgrade notes: + + - Backup: a target without xattr support now fails instead of losing them + - Dev: worktree hosts apply the IP pool with *make compose-network-recreate* + + Contributors: + + - [Kevin Veen-Birkenbach](https://veen.world): backups, NFS, AppArmor, CI + + -- Kevin Veen-Birkenbach Tue, 20 Oct 2026 21:11:50 +0200 + infinito-nexus (16.1.0-1) unstable; urgency=medium Added: diff --git a/packaging/fedora/infinito-nexus.spec b/packaging/fedora/infinito-nexus.spec index fa0b42a559..666506d68d 100644 --- a/packaging/fedora/infinito-nexus.spec +++ b/packaging/fedora/infinito-nexus.spec @@ -1,5 +1,5 @@ Name: infinito-nexus -Version: 16.1.0 +Version: 16.2.0 Release: 1%{?dist} Summary: Meta package for Infinito.Nexus host dependencies @@ -58,6 +58,38 @@ install -d %{buildroot}%{_docdir}/%{name} %doc %{_docdir}/%{name}/DEPENDENCIES %changelog +* Tue Oct 20 2026 Kevin Veen-Birkenbach - 16.2.0-1 +- Added: + +- - Compose: *make compose-network-recreate* rebuilds the stack network safely +- - CI: the updater keeps the GitHub-hosted runner labels current + +- Fixed: + +- - Backup: hard links, ACLs and xattrs survive every backup and recover hop +- - NFS client: clients on Linux 6.19+ no longer fail writes on ganesha < 9.11 +- - AppArmor: the teardown keeps docker-default, so containers keep starting +- - Wazuh: hosts can be put into WireGuard meshes like every other role +- - CI: GitHub-hosted runners are pinned to ubuntu-24.04 + +- Version jumps (net since 16.1.0): + +- - *svc-db-rabbitmq*: 4.3-management-alpine to 4.4-management-alpine +- - *web-app-n8n*: 2.44.4 to 2.45.0 +- - *web-app-pixelfed*: 20261019 to 20261020 +- - *web-app-snipe-it*: v8.8.0-alpine to v8.8.1-alpine +- - *web-app-taiga*: RabbitMQ 4.3-management-alpine to 4.4-management-alpine +- - *eslint* (dev): ^10.12.0 to ^10.13.0 + +- Upgrade notes: + +- - Backup: a target without xattr support now fails instead of losing them +- - Dev: worktree hosts apply the IP pool with *make compose-network-recreate* + +- Contributors: + +- - [Kevin Veen-Birkenbach](https://veen.world): backups, NFS, AppArmor, CI + * Mon Oct 19 2026 Kevin Veen-Birkenbach - 16.1.0-1 - Added: diff --git a/pyproject.toml b/pyproject.toml index 91d16d1233..90ee16c53a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "infinito-nexus" -version = "16.1.0" +version = "16.2.0" description = "Infinito.Nexus" readme = "README.md" requires-python = ">=3.11" From d48f8e9897c00dc2dd9294235666599ff7e32ed9 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Tue, 20 Oct 2026 21:35:29 +0200 Subject: [PATCH 21/28] fix(bcrypt_hash): derive the seeded salt from the seed alone The seeded salt was the first 16 bytes of sha256(seed + secret). bcrypt stores its salt in clear inside the hash, so every seeded hash published a fast sha256 digest of the password under a seed that is public in the repository (`web-app-wazuh:indexer-admin`). Anyone holding the hash could test password guesses at sha256 speed instead of paying bcrypt's cost. The salt is now sha256(seed)[:16]. The hash stays deterministic for one password, still changes when the password rotates, and its salt carries nothing computed from the secret. Existing Wazuh deployments get a new hash once; `securityadmin.sh` re-applies `internal_users.yml` on every deploy and the hash still verifies the unchanged password. Co-Authored-By: Claude Opus 5.5 (1M context) --- plugins/filter/bcrypt_hash.py | 11 +++++------ tests/unit/python/plugins/filter/test_bcrypt_hash.py | 7 +++++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/plugins/filter/bcrypt_hash.py b/plugins/filter/bcrypt_hash.py index 2f9d5e91e1..3eee1aae25 100644 --- a/plugins/filter/bcrypt_hash.py +++ b/plugins/filter/bcrypt_hash.py @@ -23,18 +23,17 @@ _STD_TO_BCRYPT_B64 = bytes.maketrans( ) -def _seeded_salt(seed: str, secret: bytes) -> bytes: - """A fixed bcrypt salt derived from the seed and the secret. +def _seeded_salt(seed: str) -> bytes: + """A fixed bcrypt salt derived from the seed alone. Args: seed: caller-chosen, non-empty string that separates otherwise equal secrets. - secret: the UTF-8 encoded secret being hashed. Returns: ``$2b$12$`` plus 22 bcrypt-base64 characters encoding the first 16 - bytes of sha256(seed + secret). + bytes of sha256(seed). """ - digest = hashlib.sha256(seed.encode("utf-8") + secret).digest()[:16] + digest = hashlib.sha256(seed.encode("utf-8")).digest()[:16] encoded = base64.b64encode(digest)[:22].translate(_STD_TO_BCRYPT_B64) return b"$2b$12$" + encoded @@ -61,7 +60,7 @@ class FilterModule: if salt_seed is None: salt = bcrypt.gensalt() elif isinstance(salt_seed, str) and salt_seed: - salt = _seeded_salt(salt_seed, secret) + salt = _seeded_salt(salt_seed) else: raise ValueError( "bcrypt_hash: salt_seed must be a non-empty string when given" diff --git a/tests/unit/python/plugins/filter/test_bcrypt_hash.py b/tests/unit/python/plugins/filter/test_bcrypt_hash.py index de6de1ae67..dbec112808 100644 --- a/tests/unit/python/plugins/filter/test_bcrypt_hash.py +++ b/tests/unit/python/plugins/filter/test_bcrypt_hash.py @@ -70,6 +70,13 @@ class TestBcryptHash(unittest.TestCase): self.f("password-two", "app:indexer-admin"), ) + def test_seeded_salt_carries_nothing_of_the_secret(self): + salt_end = len("$2b$12$") + 22 + self.assertEqual( + self.f("password-one", "app:indexer-admin")[:salt_end], + self.f("password-two", "app:indexer-admin")[:salt_end], + ) + def test_seeded_hash_verifies_against_the_original_password(self): password = "correct horse battery staple" hashed = self.f(password, "app:indexer-admin") From 6e776a578036119b66530898051687c4b59ff035 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Tue, 20 Oct 2026 21:40:56 +0200 Subject: [PATCH 22/28] refactor(python): clear the open CodeQL code-quality alerts - Declare `__all__` in every package `__init__.py` whose `PROJECT_ROOT` is imported by its modules (163 packages), so the constant counts as exported; list every public name where the package defines more. - Empty the 15 package `__init__.py` files whose `PROJECT_ROOT` no module imports. - Declare the public API of `utils.github.variant.axes` in `__all__`, re-exports included, instead of silencing them with `noqa: F401`; the list matches the one `feature/store-i18n` already carries. - Use `import unittest.mock as mock` in `test_applications.py`, as the rest of the suite does. - Spell `_NON_PRINTABLE` in `utils/yaml_bootstrap.py` as the equivalent BMP-only positive class; it matches exactly the same 2111 code points as the old negated class across U+0000..U+10FFFF. Co-Authored-By: Claude Opus 5.5 (1M context) --- cli/__init__.py | 2 ++ cli/administration/deploy/runner/__init__.py | 2 ++ cli/build/docs/integration_matrix/__init__.py | 2 ++ cli/build/tree/__init__.py | 9 +++++++++ .../update/collection_source/__init__.py | 2 ++ cli/contributing/update/docker/__init__.py | 2 ++ cli/contributing/update/galaxy/__init__.py | 2 ++ cli/contributing/update/pip/__init__.py | 2 ++ .../update/repository/__init__.py | 2 ++ cli/contributing/update/runner/__init__.py | 2 ++ cli/contributing/update/source/__init__.py | 2 ++ cli/core/__init__.py | 2 ++ cli/meta/domains/__init__.py | 2 ++ cli/meta/env/__init__.py | 2 ++ .../roles/applications/invokable/__init__.py | 4 ++-- .../resolution/combined/__init__.py | 2 ++ .../resolution/dependencies/__init__.py | 2 ++ .../resolution/run_after/__init__.py | 2 ++ .../roles/applications/ressources/__init__.py | 2 ++ .../sufficient_storage/__init__.py | 2 ++ cli/meta/roles/applications/type/__init__.py | 4 ++-- cli/meta/runtime/__init__.py | 2 ++ plugins/filter/__init__.py | 3 --- plugins/lookup/__init__.py | 2 ++ tasks/utils/migrate_meta_layout/__init__.py | 2 ++ tests/external/repository/__init__.py | 2 ++ tests/external/roles/__init__.py | 2 ++ tests/external/update/__init__.py | 3 --- tests/external/update/addons/__init__.py | 2 ++ tests/external/update/docker/__init__.py | 2 ++ tests/external/update/pip/__init__.py | 2 ++ tests/external/update/repository/__init__.py | 2 ++ tests/external/update/source/__init__.py | 2 ++ tests/integration/ansible/__init__.py | 3 --- .../ansible/action_plugins/__init__.py | 2 ++ tests/integration/ansible/filters/__init__.py | 2 ++ .../ansible/group_vars/__init__.py | 2 ++ .../integration/ansible/handlers/__init__.py | 2 ++ tests/integration/ansible/jinja/__init__.py | 2 ++ tests/integration/ansible/lookups/__init__.py | 2 ++ .../ansible/lookups/config/__init__.py | 2 ++ .../integration/ansible/playbooks/__init__.py | 2 ++ tests/integration/ansible/vars/__init__.py | 2 ++ tests/integration/iam/__init__.py | 3 --- tests/integration/iam/passwords/__init__.py | 2 ++ tests/integration/iam/sso/__init__.py | 2 ++ tests/integration/iam/sso/rbac/__init__.py | 2 ++ tests/integration/infrastructure/__init__.py | 3 --- .../infrastructure/compose/__init__.py | 2 ++ .../infrastructure/docker/__init__.py | 2 ++ .../infrastructure/networks/__init__.py | 2 ++ .../infrastructure/services/__init__.py | 2 ++ tests/integration/meta/__init__.py | 3 --- tests/integration/meta/categories/__init__.py | 2 ++ tests/integration/meta/cli/__init__.py | 2 ++ tests/integration/meta/domains/__init__.py | 2 ++ tests/integration/meta/env/__init__.py | 2 ++ tests/integration/meta/workflows/__init__.py | 2 ++ tests/integration/roles/__init__.py | 2 ++ .../roles/applications/__init__.py | 2 ++ .../roles/applications/id/__init__.py | 2 ++ tests/integration/roles/includes/__init__.py | 2 ++ tests/integration/roles/meta/__init__.py | 2 ++ .../roles/meta/dependencies/__init__.py | 2 ++ .../integration/roles/meta/server/__init__.py | 2 ++ .../roles/meta/services/__init__.py | 2 ++ .../meta/services/prometheus/__init__.py | 2 ++ .../meta/services/repositories/__init__.py | 2 ++ .../roles/meta/services/run_after/__init__.py | 2 ++ .../roles/meta/variants/__init__.py | 2 ++ tests/integration/roles/run_once/__init__.py | 2 ++ tests/integration/roles/when/__init__.py | 2 ++ tests/lint/ansible/__init__.py | 2 ++ tests/lint/ansible/jinja/__init__.py | 2 ++ tests/lint/ansible/modules/__init__.py | 2 ++ tests/lint/ansible/roles/__init__.py | 2 ++ tests/lint/ansible/roles/meta/__init__.py | 2 ++ .../roles/web-app/integration/__init__.py | 2 ++ .../roles/web-app/playwright/__init__.py | 2 ++ .../web-app/playwright/persona/__init__.py | 2 ++ tests/lint/ansible/services/__init__.py | 2 ++ tests/lint/ansible/shell/__init__.py | 2 ++ tests/lint/ansible/sql/__init__.py | 2 ++ tests/lint/ansible/structure/__init__.py | 2 ++ tests/lint/ansible/swarm/__init__.py | 2 ++ tests/lint/ansible/tasks/__init__.py | 2 ++ tests/lint/ansible/variables/__init__.py | 2 ++ tests/lint/docker/dockerfile/__init__.py | 2 ++ tests/lint/docker/dockerfile/apt/__init__.py | 2 ++ tests/lint/filesystem/__init__.py | 2 ++ tests/lint/repository/__init__.py | 2 ++ tests/performance/__init__.py | 2 ++ .../deploy/development/__init__.py | 2 ++ .../cli/administration/inventory/__init__.py | 2 ++ .../administration/inventory/mesh/__init__.py | 2 ++ .../inventory/validate/__init__.py | 2 ++ tests/unit/python/cli/build/__init__.py | 2 ++ tests/unit/python/cli/gha/__init__.py | 2 ++ tests/unit/python/library/__init__.py | 2 ++ tests/unit/python/plugins/filter/__init__.py | 2 ++ .../python/plugins/filter/get/__init__.py | 3 --- tests/unit/python/plugins/lookup/__init__.py | 2 ++ tests/unit/python/roles/agents/__init__.py | 2 ++ .../roles/svc-ai-litellm/files/__init__.py | 2 ++ .../svc-ai-litellm/templates/__init__.py | 2 ++ .../svc-ai-mcp-adapter/files/__init__.py | 2 ++ .../svc-bkp-nfs-2-local/files/__init__.py | 2 ++ .../svc-bkp-remote-2-local/files/__init__.py | 2 ++ .../svc-bkp-volume-2-local/files/__init__.py | 2 ++ .../library/__init__.py | 2 ++ .../roles/svc-net-tor-smtp/files/__init__.py | 2 ++ .../roles/svc-net-tor/files/__init__.py | 2 ++ .../roles/svc-net-wireguard/__init__.py | 3 --- .../filter_plugins/__init__.py | 2 ++ .../svc-opt-keyboard-color/files/__init__.py | 2 ++ .../roles/sys-ctl-cln-bkps/files/__init__.py | 2 ++ .../roles/sys-ctl-hlth-csp/files/__init__.py | 2 ++ .../lookup_plugins/__init__.py | 2 ++ .../sys-ctl-hlth-disc-space/files/__init__.py | 2 ++ .../lookup_plugins/__init__.py | 2 ++ .../sys-ctl-hlth-webserver/files/__init__.py | 2 ++ .../filter_plugins/__init__.py | 2 ++ .../files/__init__.py | 2 ++ .../files/__init__.py | 2 ++ .../filter_plugins/__init__.py | 2 ++ .../filter_plugins/__init__.py | 2 ++ .../python/roles/sys-lock/files/__init__.py | 2 ++ .../sys-svc-compose-ca/files/__init__.py | 2 ++ .../roles/sys-svc-compose/files/__init__.py | 2 ++ .../sys-svc-compose/files/swarm/__init__.py | 2 ++ .../lookup_plugins/__init__.py | 2 ++ .../roles/sys-svc-container/files/__init__.py | 2 ++ .../roles/sys-svc-proxy/templates/__init__.py | 2 ++ .../roles/sys-svc-rdbms/files/__init__.py | 2 ++ .../roles/sys-util-git-pull/files/__init__.py | 2 ++ .../roles/test-e2e-cli/files/__init__.py | 3 --- .../test-e2e-cli/files/shared/__init__.py | 3 --- .../test-e2e-cli/files/shared/mcp/__init__.py | 2 ++ .../test-e2e-cli/filter_plugins/__init__.py | 2 ++ .../filter_plugins/__init__.py | 2 ++ .../roles/web-app-baserow/files/__init__.py | 2 ++ .../web-app-baserow/files/sso/__init__.py | 2 ++ .../web-app-bookwyrm/files/sso/__init__.py | 2 ++ .../filter_plugins/__init__.py | 2 ++ .../roles/web-app-flowise/files/__init__.py | 3 --- .../files/reconcile/__init__.py | 2 ++ .../roles/web-app-jellyfin/files/__init__.py | 2 ++ .../web-app-keycloak/templates/__init__.py | 2 ++ .../filter_plugins/__init__.py | 2 ++ .../web-app-mailu/lookup_plugins/__init__.py | 2 ++ .../web-app-matrix/lookup_plugins/__init__.py | 2 ++ .../roles/web-app-n8n/files/__init__.py | 3 --- .../web-app-n8n/files/provision/__init__.py | 2 ++ .../filter_plugins/__init__.py | 2 ++ .../lookup_plugins/__init__.py | 2 ++ .../roles/web-app-openwebui/files/__init__.py | 2 ++ .../web-app-openwebui/templates/__init__.py | 3 --- .../filter_plugins/__init__.py | 2 ++ .../roles/web-app-stalwart/files/__init__.py | 2 ++ .../lookup_plugins/__init__.py | 2 ++ .../lookup_plugins/__init__.py | 2 ++ .../web-svc-logout/filter_plugins/__init__.py | 2 ++ .../python/roles/web_app_keycloak/__init__.py | 2 ++ tests/unit/python/utils/__init__.py | 3 --- tests/unit/python/utils/cache/__init__.py | 2 ++ .../python/utils/cache/test_applications.py | 2 +- .../unit/python/utils/diagnostics/__init__.py | 2 ++ tests/unit/python/utils/github/__init__.py | 3 --- tests/unit/python/utils/mail/__init__.py | 2 ++ tests/unit/python/utils/recovery/__init__.py | 2 ++ tests/unit/python/utils/roles/__init__.py | 2 ++ .../unit/python/utils/templating/__init__.py | 2 ++ tests/utils/__init__.py | 2 ++ utils/__init__.py | 2 ++ utils/claude/__init__.py | 2 ++ utils/domains/__init__.py | 2 ++ utils/github/variant/axes.py | 20 +++++++++++++++++-- utils/meta/__init__.py | 2 ++ utils/roles/__init__.py | 2 ++ utils/roles/validation/__init__.py | 2 ++ utils/yaml_bootstrap.py | 2 +- 181 files changed, 353 insertions(+), 53 deletions(-) diff --git a/cli/__init__.py b/cli/__init__.py index 9f4a6a3289..29a52ec495 100644 --- a/cli/__init__.py +++ b/cli/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[1] + +__all__ = ["PROJECT_ROOT"] diff --git a/cli/administration/deploy/runner/__init__.py b/cli/administration/deploy/runner/__init__.py index 14b7e7361a..0be6d2a841 100644 --- a/cli/administration/deploy/runner/__init__.py +++ b/cli/administration/deploy/runner/__init__.py @@ -3,3 +3,5 @@ from __future__ import annotations from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/cli/build/docs/integration_matrix/__init__.py b/cli/build/docs/integration_matrix/__init__.py index 1d48518cec..3622064c5e 100644 --- a/cli/build/docs/integration_matrix/__init__.py +++ b/cli/build/docs/integration_matrix/__init__.py @@ -10,3 +10,5 @@ from __future__ import annotations from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/cli/build/tree/__init__.py b/cli/build/tree/__init__.py index 189667f78b..ba83f47252 100644 --- a/cli/build/tree/__init__.py +++ b/cli/build/tree/__init__.py @@ -4,6 +4,15 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[3] +__all__ = [ + "PROJECT_ROOT", + "build_mappings", + "find_roles", + "main", + "output_graph", + "process_role", +] + from . import __main__ as _main # noqa: E402 build_mappings = _main.build_mappings diff --git a/cli/contributing/update/collection_source/__init__.py b/cli/contributing/update/collection_source/__init__.py index ab2c0b6137..6e89eae234 100644 --- a/cli/contributing/update/collection_source/__init__.py +++ b/cli/contributing/update/collection_source/__init__.py @@ -3,3 +3,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/cli/contributing/update/docker/__init__.py b/cli/contributing/update/docker/__init__.py index b9b716f02d..0d4e8d4485 100644 --- a/cli/contributing/update/docker/__init__.py +++ b/cli/contributing/update/docker/__init__.py @@ -3,3 +3,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/cli/contributing/update/galaxy/__init__.py b/cli/contributing/update/galaxy/__init__.py index f79db0b735..9a92af7804 100644 --- a/cli/contributing/update/galaxy/__init__.py +++ b/cli/contributing/update/galaxy/__init__.py @@ -3,3 +3,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/cli/contributing/update/pip/__init__.py b/cli/contributing/update/pip/__init__.py index 427a508803..0edb5a2c18 100644 --- a/cli/contributing/update/pip/__init__.py +++ b/cli/contributing/update/pip/__init__.py @@ -3,3 +3,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/cli/contributing/update/repository/__init__.py b/cli/contributing/update/repository/__init__.py index d4057cef8d..55857f43cc 100644 --- a/cli/contributing/update/repository/__init__.py +++ b/cli/contributing/update/repository/__init__.py @@ -3,3 +3,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/cli/contributing/update/runner/__init__.py b/cli/contributing/update/runner/__init__.py index 9b7a055ab0..247a64576a 100644 --- a/cli/contributing/update/runner/__init__.py +++ b/cli/contributing/update/runner/__init__.py @@ -3,3 +3,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/cli/contributing/update/source/__init__.py b/cli/contributing/update/source/__init__.py index 0f6478c67a..bbd236ccce 100644 --- a/cli/contributing/update/source/__init__.py +++ b/cli/contributing/update/source/__init__.py @@ -3,3 +3,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/cli/core/__init__.py b/cli/core/__init__.py index a11dbb1745..8125c38905 100644 --- a/cli/core/__init__.py +++ b/cli/core/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[2] + +__all__ = ["PROJECT_ROOT"] diff --git a/cli/meta/domains/__init__.py b/cli/meta/domains/__init__.py index 78e3cae448..ff4b515c73 100644 --- a/cli/meta/domains/__init__.py +++ b/cli/meta/domains/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[3] + +__all__ = ["PROJECT_ROOT"] diff --git a/cli/meta/env/__init__.py b/cli/meta/env/__init__.py index 3b325d12fb..dae468a3c0 100644 --- a/cli/meta/env/__init__.py +++ b/cli/meta/env/__init__.py @@ -9,3 +9,5 @@ resulting `.env` at the repo root. Implementation lives in from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[3] + +__all__ = ["PROJECT_ROOT"] diff --git a/cli/meta/roles/applications/invokable/__init__.py b/cli/meta/roles/applications/invokable/__init__.py index e9ac86d477..f5da6fa522 100644 --- a/cli/meta/roles/applications/invokable/__init__.py +++ b/cli/meta/roles/applications/invokable/__init__.py @@ -1,7 +1,7 @@ from __future__ import annotations -__all__: list[str] = [] - from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[5] + +__all__ = ["PROJECT_ROOT"] diff --git a/cli/meta/roles/applications/resolution/combined/__init__.py b/cli/meta/roles/applications/resolution/combined/__init__.py index 9f69c70b47..087c384b57 100644 --- a/cli/meta/roles/applications/resolution/combined/__init__.py +++ b/cli/meta/roles/applications/resolution/combined/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/cli/meta/roles/applications/resolution/dependencies/__init__.py b/cli/meta/roles/applications/resolution/dependencies/__init__.py index 9f69c70b47..087c384b57 100644 --- a/cli/meta/roles/applications/resolution/dependencies/__init__.py +++ b/cli/meta/roles/applications/resolution/dependencies/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/cli/meta/roles/applications/resolution/run_after/__init__.py b/cli/meta/roles/applications/resolution/run_after/__init__.py index 9f69c70b47..087c384b57 100644 --- a/cli/meta/roles/applications/resolution/run_after/__init__.py +++ b/cli/meta/roles/applications/resolution/run_after/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/cli/meta/roles/applications/ressources/__init__.py b/cli/meta/roles/applications/ressources/__init__.py index 8ee16d7567..aa7a30d2e5 100644 --- a/cli/meta/roles/applications/ressources/__init__.py +++ b/cli/meta/roles/applications/ressources/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[5] + +__all__ = ["PROJECT_ROOT"] diff --git a/cli/meta/roles/applications/sufficient_storage/__init__.py b/cli/meta/roles/applications/sufficient_storage/__init__.py index 8ee16d7567..aa7a30d2e5 100644 --- a/cli/meta/roles/applications/sufficient_storage/__init__.py +++ b/cli/meta/roles/applications/sufficient_storage/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[5] + +__all__ = ["PROJECT_ROOT"] diff --git a/cli/meta/roles/applications/type/__init__.py b/cli/meta/roles/applications/type/__init__.py index e9ac86d477..f5da6fa522 100644 --- a/cli/meta/roles/applications/type/__init__.py +++ b/cli/meta/roles/applications/type/__init__.py @@ -1,7 +1,7 @@ from __future__ import annotations -__all__: list[str] = [] - from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[5] + +__all__ = ["PROJECT_ROOT"] diff --git a/cli/meta/runtime/__init__.py b/cli/meta/runtime/__init__.py index 105048b62c..f3cd3eb0f6 100644 --- a/cli/meta/runtime/__init__.py +++ b/cli/meta/runtime/__init__.py @@ -5,6 +5,8 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[3] +__all__ = ["PROJECT_ROOT", "detect_runtime"] + def detect_runtime() -> str: """ diff --git a/plugins/filter/__init__.py b/plugins/filter/__init__.py index a11dbb1745..e69de29bb2 100644 --- a/plugins/filter/__init__.py +++ b/plugins/filter/__init__.py @@ -1,3 +0,0 @@ -from pathlib import Path - -PROJECT_ROOT: Path = Path(__file__).resolve().parents[2] diff --git a/plugins/lookup/__init__.py b/plugins/lookup/__init__.py index a11dbb1745..8125c38905 100644 --- a/plugins/lookup/__init__.py +++ b/plugins/lookup/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[2] + +__all__ = ["PROJECT_ROOT"] diff --git a/tasks/utils/migrate_meta_layout/__init__.py b/tasks/utils/migrate_meta_layout/__init__.py index dd8a02b6d1..bc79b7740f 100644 --- a/tasks/utils/migrate_meta_layout/__init__.py +++ b/tasks/utils/migrate_meta_layout/__init__.py @@ -10,3 +10,5 @@ Idempotent per role: re-running on an already-migrated tree is a no-op. from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[3] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/external/repository/__init__.py b/tests/external/repository/__init__.py index 78e3cae448..ff4b515c73 100644 --- a/tests/external/repository/__init__.py +++ b/tests/external/repository/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[3] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/external/roles/__init__.py b/tests/external/roles/__init__.py index 78e3cae448..ff4b515c73 100644 --- a/tests/external/roles/__init__.py +++ b/tests/external/roles/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[3] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/external/update/__init__.py b/tests/external/update/__init__.py index 78e3cae448..e69de29bb2 100644 --- a/tests/external/update/__init__.py +++ b/tests/external/update/__init__.py @@ -1,3 +0,0 @@ -from pathlib import Path - -PROJECT_ROOT: Path = Path(__file__).resolve().parents[3] diff --git a/tests/external/update/addons/__init__.py b/tests/external/update/addons/__init__.py index 88f288ec1f..eec6d4835a 100644 --- a/tests/external/update/addons/__init__.py +++ b/tests/external/update/addons/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/external/update/docker/__init__.py b/tests/external/update/docker/__init__.py index 88f288ec1f..eec6d4835a 100644 --- a/tests/external/update/docker/__init__.py +++ b/tests/external/update/docker/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/external/update/pip/__init__.py b/tests/external/update/pip/__init__.py index 88f288ec1f..eec6d4835a 100644 --- a/tests/external/update/pip/__init__.py +++ b/tests/external/update/pip/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/external/update/repository/__init__.py b/tests/external/update/repository/__init__.py index 88f288ec1f..eec6d4835a 100644 --- a/tests/external/update/repository/__init__.py +++ b/tests/external/update/repository/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/external/update/source/__init__.py b/tests/external/update/source/__init__.py index 88f288ec1f..eec6d4835a 100644 --- a/tests/external/update/source/__init__.py +++ b/tests/external/update/source/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/integration/ansible/__init__.py b/tests/integration/ansible/__init__.py index 78e3cae448..e69de29bb2 100644 --- a/tests/integration/ansible/__init__.py +++ b/tests/integration/ansible/__init__.py @@ -1,3 +0,0 @@ -from pathlib import Path - -PROJECT_ROOT: Path = Path(__file__).resolve().parents[3] diff --git a/tests/integration/ansible/action_plugins/__init__.py b/tests/integration/ansible/action_plugins/__init__.py index 88f288ec1f..eec6d4835a 100644 --- a/tests/integration/ansible/action_plugins/__init__.py +++ b/tests/integration/ansible/action_plugins/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/integration/ansible/filters/__init__.py b/tests/integration/ansible/filters/__init__.py index 88f288ec1f..eec6d4835a 100644 --- a/tests/integration/ansible/filters/__init__.py +++ b/tests/integration/ansible/filters/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/integration/ansible/group_vars/__init__.py b/tests/integration/ansible/group_vars/__init__.py index 88f288ec1f..eec6d4835a 100644 --- a/tests/integration/ansible/group_vars/__init__.py +++ b/tests/integration/ansible/group_vars/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/integration/ansible/handlers/__init__.py b/tests/integration/ansible/handlers/__init__.py index 88f288ec1f..eec6d4835a 100644 --- a/tests/integration/ansible/handlers/__init__.py +++ b/tests/integration/ansible/handlers/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/integration/ansible/jinja/__init__.py b/tests/integration/ansible/jinja/__init__.py index 88f288ec1f..eec6d4835a 100644 --- a/tests/integration/ansible/jinja/__init__.py +++ b/tests/integration/ansible/jinja/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/integration/ansible/lookups/__init__.py b/tests/integration/ansible/lookups/__init__.py index 88f288ec1f..eec6d4835a 100644 --- a/tests/integration/ansible/lookups/__init__.py +++ b/tests/integration/ansible/lookups/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/integration/ansible/lookups/config/__init__.py b/tests/integration/ansible/lookups/config/__init__.py index 8ee16d7567..aa7a30d2e5 100644 --- a/tests/integration/ansible/lookups/config/__init__.py +++ b/tests/integration/ansible/lookups/config/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[5] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/integration/ansible/playbooks/__init__.py b/tests/integration/ansible/playbooks/__init__.py index 88f288ec1f..eec6d4835a 100644 --- a/tests/integration/ansible/playbooks/__init__.py +++ b/tests/integration/ansible/playbooks/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/integration/ansible/vars/__init__.py b/tests/integration/ansible/vars/__init__.py index 88f288ec1f..eec6d4835a 100644 --- a/tests/integration/ansible/vars/__init__.py +++ b/tests/integration/ansible/vars/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/integration/iam/__init__.py b/tests/integration/iam/__init__.py index 78e3cae448..e69de29bb2 100644 --- a/tests/integration/iam/__init__.py +++ b/tests/integration/iam/__init__.py @@ -1,3 +0,0 @@ -from pathlib import Path - -PROJECT_ROOT: Path = Path(__file__).resolve().parents[3] diff --git a/tests/integration/iam/passwords/__init__.py b/tests/integration/iam/passwords/__init__.py index 88f288ec1f..eec6d4835a 100644 --- a/tests/integration/iam/passwords/__init__.py +++ b/tests/integration/iam/passwords/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/integration/iam/sso/__init__.py b/tests/integration/iam/sso/__init__.py index 88f288ec1f..eec6d4835a 100644 --- a/tests/integration/iam/sso/__init__.py +++ b/tests/integration/iam/sso/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/integration/iam/sso/rbac/__init__.py b/tests/integration/iam/sso/rbac/__init__.py index 8ee16d7567..aa7a30d2e5 100644 --- a/tests/integration/iam/sso/rbac/__init__.py +++ b/tests/integration/iam/sso/rbac/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[5] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/integration/infrastructure/__init__.py b/tests/integration/infrastructure/__init__.py index 78e3cae448..e69de29bb2 100644 --- a/tests/integration/infrastructure/__init__.py +++ b/tests/integration/infrastructure/__init__.py @@ -1,3 +0,0 @@ -from pathlib import Path - -PROJECT_ROOT: Path = Path(__file__).resolve().parents[3] diff --git a/tests/integration/infrastructure/compose/__init__.py b/tests/integration/infrastructure/compose/__init__.py index 88f288ec1f..eec6d4835a 100644 --- a/tests/integration/infrastructure/compose/__init__.py +++ b/tests/integration/infrastructure/compose/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/integration/infrastructure/docker/__init__.py b/tests/integration/infrastructure/docker/__init__.py index 88f288ec1f..eec6d4835a 100644 --- a/tests/integration/infrastructure/docker/__init__.py +++ b/tests/integration/infrastructure/docker/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/integration/infrastructure/networks/__init__.py b/tests/integration/infrastructure/networks/__init__.py index 88f288ec1f..eec6d4835a 100644 --- a/tests/integration/infrastructure/networks/__init__.py +++ b/tests/integration/infrastructure/networks/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/integration/infrastructure/services/__init__.py b/tests/integration/infrastructure/services/__init__.py index 88f288ec1f..eec6d4835a 100644 --- a/tests/integration/infrastructure/services/__init__.py +++ b/tests/integration/infrastructure/services/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/integration/meta/__init__.py b/tests/integration/meta/__init__.py index 78e3cae448..e69de29bb2 100644 --- a/tests/integration/meta/__init__.py +++ b/tests/integration/meta/__init__.py @@ -1,3 +0,0 @@ -from pathlib import Path - -PROJECT_ROOT: Path = Path(__file__).resolve().parents[3] diff --git a/tests/integration/meta/categories/__init__.py b/tests/integration/meta/categories/__init__.py index 88f288ec1f..eec6d4835a 100644 --- a/tests/integration/meta/categories/__init__.py +++ b/tests/integration/meta/categories/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/integration/meta/cli/__init__.py b/tests/integration/meta/cli/__init__.py index 88f288ec1f..eec6d4835a 100644 --- a/tests/integration/meta/cli/__init__.py +++ b/tests/integration/meta/cli/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/integration/meta/domains/__init__.py b/tests/integration/meta/domains/__init__.py index 88f288ec1f..eec6d4835a 100644 --- a/tests/integration/meta/domains/__init__.py +++ b/tests/integration/meta/domains/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/integration/meta/env/__init__.py b/tests/integration/meta/env/__init__.py index 88f288ec1f..eec6d4835a 100644 --- a/tests/integration/meta/env/__init__.py +++ b/tests/integration/meta/env/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/integration/meta/workflows/__init__.py b/tests/integration/meta/workflows/__init__.py index 88f288ec1f..eec6d4835a 100644 --- a/tests/integration/meta/workflows/__init__.py +++ b/tests/integration/meta/workflows/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/integration/roles/__init__.py b/tests/integration/roles/__init__.py index 78e3cae448..ff4b515c73 100644 --- a/tests/integration/roles/__init__.py +++ b/tests/integration/roles/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[3] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/integration/roles/applications/__init__.py b/tests/integration/roles/applications/__init__.py index 88f288ec1f..eec6d4835a 100644 --- a/tests/integration/roles/applications/__init__.py +++ b/tests/integration/roles/applications/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/integration/roles/applications/id/__init__.py b/tests/integration/roles/applications/id/__init__.py index 8ee16d7567..aa7a30d2e5 100644 --- a/tests/integration/roles/applications/id/__init__.py +++ b/tests/integration/roles/applications/id/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[5] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/integration/roles/includes/__init__.py b/tests/integration/roles/includes/__init__.py index 88f288ec1f..eec6d4835a 100644 --- a/tests/integration/roles/includes/__init__.py +++ b/tests/integration/roles/includes/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/integration/roles/meta/__init__.py b/tests/integration/roles/meta/__init__.py index 88f288ec1f..eec6d4835a 100644 --- a/tests/integration/roles/meta/__init__.py +++ b/tests/integration/roles/meta/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/integration/roles/meta/dependencies/__init__.py b/tests/integration/roles/meta/dependencies/__init__.py index 8ee16d7567..aa7a30d2e5 100644 --- a/tests/integration/roles/meta/dependencies/__init__.py +++ b/tests/integration/roles/meta/dependencies/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[5] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/integration/roles/meta/server/__init__.py b/tests/integration/roles/meta/server/__init__.py index 8ee16d7567..aa7a30d2e5 100644 --- a/tests/integration/roles/meta/server/__init__.py +++ b/tests/integration/roles/meta/server/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[5] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/integration/roles/meta/services/__init__.py b/tests/integration/roles/meta/services/__init__.py index 8ee16d7567..aa7a30d2e5 100644 --- a/tests/integration/roles/meta/services/__init__.py +++ b/tests/integration/roles/meta/services/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[5] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/integration/roles/meta/services/prometheus/__init__.py b/tests/integration/roles/meta/services/prometheus/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/integration/roles/meta/services/prometheus/__init__.py +++ b/tests/integration/roles/meta/services/prometheus/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/integration/roles/meta/services/repositories/__init__.py b/tests/integration/roles/meta/services/repositories/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/integration/roles/meta/services/repositories/__init__.py +++ b/tests/integration/roles/meta/services/repositories/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/integration/roles/meta/services/run_after/__init__.py b/tests/integration/roles/meta/services/run_after/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/integration/roles/meta/services/run_after/__init__.py +++ b/tests/integration/roles/meta/services/run_after/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/integration/roles/meta/variants/__init__.py b/tests/integration/roles/meta/variants/__init__.py index 8ee16d7567..aa7a30d2e5 100644 --- a/tests/integration/roles/meta/variants/__init__.py +++ b/tests/integration/roles/meta/variants/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[5] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/integration/roles/run_once/__init__.py b/tests/integration/roles/run_once/__init__.py index 88f288ec1f..eec6d4835a 100644 --- a/tests/integration/roles/run_once/__init__.py +++ b/tests/integration/roles/run_once/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/integration/roles/when/__init__.py b/tests/integration/roles/when/__init__.py index 88f288ec1f..eec6d4835a 100644 --- a/tests/integration/roles/when/__init__.py +++ b/tests/integration/roles/when/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/lint/ansible/__init__.py b/tests/lint/ansible/__init__.py index 78e3cae448..ff4b515c73 100644 --- a/tests/lint/ansible/__init__.py +++ b/tests/lint/ansible/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[3] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/lint/ansible/jinja/__init__.py b/tests/lint/ansible/jinja/__init__.py index 8b57ab2838..4e9f2cd497 100644 --- a/tests/lint/ansible/jinja/__init__.py +++ b/tests/lint/ansible/jinja/__init__.py @@ -2,6 +2,8 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] +__all__ = ["COMPOSE_TEMPLATE", "PROJECT_ROOT", "is_main_compose_template"] + COMPOSE_TEMPLATE = "compose.yml.j2" diff --git a/tests/lint/ansible/modules/__init__.py b/tests/lint/ansible/modules/__init__.py index 88f288ec1f..eec6d4835a 100644 --- a/tests/lint/ansible/modules/__init__.py +++ b/tests/lint/ansible/modules/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/lint/ansible/roles/__init__.py b/tests/lint/ansible/roles/__init__.py index 88f288ec1f..eec6d4835a 100644 --- a/tests/lint/ansible/roles/__init__.py +++ b/tests/lint/ansible/roles/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/lint/ansible/roles/meta/__init__.py b/tests/lint/ansible/roles/meta/__init__.py index 8ee16d7567..aa7a30d2e5 100644 --- a/tests/lint/ansible/roles/meta/__init__.py +++ b/tests/lint/ansible/roles/meta/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[5] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/lint/ansible/roles/web-app/integration/__init__.py b/tests/lint/ansible/roles/web-app/integration/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/lint/ansible/roles/web-app/integration/__init__.py +++ b/tests/lint/ansible/roles/web-app/integration/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/lint/ansible/roles/web-app/playwright/__init__.py b/tests/lint/ansible/roles/web-app/playwright/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/lint/ansible/roles/web-app/playwright/__init__.py +++ b/tests/lint/ansible/roles/web-app/playwright/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/lint/ansible/roles/web-app/playwright/persona/__init__.py b/tests/lint/ansible/roles/web-app/playwright/persona/__init__.py index 86efabd459..6290ad0f99 100644 --- a/tests/lint/ansible/roles/web-app/playwright/persona/__init__.py +++ b/tests/lint/ansible/roles/web-app/playwright/persona/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[7] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/lint/ansible/services/__init__.py b/tests/lint/ansible/services/__init__.py index 88f288ec1f..eec6d4835a 100644 --- a/tests/lint/ansible/services/__init__.py +++ b/tests/lint/ansible/services/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/lint/ansible/shell/__init__.py b/tests/lint/ansible/shell/__init__.py index 88f288ec1f..eec6d4835a 100644 --- a/tests/lint/ansible/shell/__init__.py +++ b/tests/lint/ansible/shell/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/lint/ansible/sql/__init__.py b/tests/lint/ansible/sql/__init__.py index 88f288ec1f..eec6d4835a 100644 --- a/tests/lint/ansible/sql/__init__.py +++ b/tests/lint/ansible/sql/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/lint/ansible/structure/__init__.py b/tests/lint/ansible/structure/__init__.py index 88f288ec1f..eec6d4835a 100644 --- a/tests/lint/ansible/structure/__init__.py +++ b/tests/lint/ansible/structure/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/lint/ansible/swarm/__init__.py b/tests/lint/ansible/swarm/__init__.py index 88f288ec1f..eec6d4835a 100644 --- a/tests/lint/ansible/swarm/__init__.py +++ b/tests/lint/ansible/swarm/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/lint/ansible/tasks/__init__.py b/tests/lint/ansible/tasks/__init__.py index 88f288ec1f..eec6d4835a 100644 --- a/tests/lint/ansible/tasks/__init__.py +++ b/tests/lint/ansible/tasks/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/lint/ansible/variables/__init__.py b/tests/lint/ansible/variables/__init__.py index 88f288ec1f..eec6d4835a 100644 --- a/tests/lint/ansible/variables/__init__.py +++ b/tests/lint/ansible/variables/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/lint/docker/dockerfile/__init__.py b/tests/lint/docker/dockerfile/__init__.py index 88f288ec1f..eec6d4835a 100644 --- a/tests/lint/docker/dockerfile/__init__.py +++ b/tests/lint/docker/dockerfile/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/lint/docker/dockerfile/apt/__init__.py b/tests/lint/docker/dockerfile/apt/__init__.py index 8ee16d7567..aa7a30d2e5 100644 --- a/tests/lint/docker/dockerfile/apt/__init__.py +++ b/tests/lint/docker/dockerfile/apt/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[5] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/lint/filesystem/__init__.py b/tests/lint/filesystem/__init__.py index 78e3cae448..ff4b515c73 100644 --- a/tests/lint/filesystem/__init__.py +++ b/tests/lint/filesystem/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[3] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/lint/repository/__init__.py b/tests/lint/repository/__init__.py index 78e3cae448..ff4b515c73 100644 --- a/tests/lint/repository/__init__.py +++ b/tests/lint/repository/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[3] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/performance/__init__.py b/tests/performance/__init__.py index a11dbb1745..8125c38905 100644 --- a/tests/performance/__init__.py +++ b/tests/performance/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[2] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/cli/administration/deploy/development/__init__.py b/tests/unit/python/cli/administration/deploy/development/__init__.py index 86efabd459..6290ad0f99 100644 --- a/tests/unit/python/cli/administration/deploy/development/__init__.py +++ b/tests/unit/python/cli/administration/deploy/development/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[7] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/cli/administration/inventory/__init__.py b/tests/unit/python/cli/administration/inventory/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/cli/administration/inventory/__init__.py +++ b/tests/unit/python/cli/administration/inventory/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/cli/administration/inventory/mesh/__init__.py b/tests/unit/python/cli/administration/inventory/mesh/__init__.py index 86efabd459..6290ad0f99 100644 --- a/tests/unit/python/cli/administration/inventory/mesh/__init__.py +++ b/tests/unit/python/cli/administration/inventory/mesh/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[7] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/cli/administration/inventory/validate/__init__.py b/tests/unit/python/cli/administration/inventory/validate/__init__.py index 86efabd459..6290ad0f99 100644 --- a/tests/unit/python/cli/administration/inventory/validate/__init__.py +++ b/tests/unit/python/cli/administration/inventory/validate/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[7] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/cli/build/__init__.py b/tests/unit/python/cli/build/__init__.py index 8ee16d7567..aa7a30d2e5 100644 --- a/tests/unit/python/cli/build/__init__.py +++ b/tests/unit/python/cli/build/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[5] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/cli/gha/__init__.py b/tests/unit/python/cli/gha/__init__.py index 29d44ed803..f5da6fa522 100644 --- a/tests/unit/python/cli/gha/__init__.py +++ b/tests/unit/python/cli/gha/__init__.py @@ -3,3 +3,5 @@ from __future__ import annotations from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[5] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/library/__init__.py b/tests/unit/python/library/__init__.py index 88f288ec1f..eec6d4835a 100644 --- a/tests/unit/python/library/__init__.py +++ b/tests/unit/python/library/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/plugins/filter/__init__.py b/tests/unit/python/plugins/filter/__init__.py index 8ee16d7567..aa7a30d2e5 100644 --- a/tests/unit/python/plugins/filter/__init__.py +++ b/tests/unit/python/plugins/filter/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[5] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/plugins/filter/get/__init__.py b/tests/unit/python/plugins/filter/get/__init__.py index 9f69c70b47..e69de29bb2 100644 --- a/tests/unit/python/plugins/filter/get/__init__.py +++ b/tests/unit/python/plugins/filter/get/__init__.py @@ -1,3 +0,0 @@ -from pathlib import Path - -PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] diff --git a/tests/unit/python/plugins/lookup/__init__.py b/tests/unit/python/plugins/lookup/__init__.py index 8ee16d7567..aa7a30d2e5 100644 --- a/tests/unit/python/plugins/lookup/__init__.py +++ b/tests/unit/python/plugins/lookup/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[5] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/agents/__init__.py b/tests/unit/python/roles/agents/__init__.py index 8ee16d7567..aa7a30d2e5 100644 --- a/tests/unit/python/roles/agents/__init__.py +++ b/tests/unit/python/roles/agents/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[5] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/svc-ai-litellm/files/__init__.py b/tests/unit/python/roles/svc-ai-litellm/files/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/svc-ai-litellm/files/__init__.py +++ b/tests/unit/python/roles/svc-ai-litellm/files/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/svc-ai-litellm/templates/__init__.py b/tests/unit/python/roles/svc-ai-litellm/templates/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/svc-ai-litellm/templates/__init__.py +++ b/tests/unit/python/roles/svc-ai-litellm/templates/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/svc-ai-mcp-adapter/files/__init__.py b/tests/unit/python/roles/svc-ai-mcp-adapter/files/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/svc-ai-mcp-adapter/files/__init__.py +++ b/tests/unit/python/roles/svc-ai-mcp-adapter/files/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/svc-bkp-nfs-2-local/files/__init__.py b/tests/unit/python/roles/svc-bkp-nfs-2-local/files/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/svc-bkp-nfs-2-local/files/__init__.py +++ b/tests/unit/python/roles/svc-bkp-nfs-2-local/files/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/svc-bkp-remote-2-local/files/__init__.py b/tests/unit/python/roles/svc-bkp-remote-2-local/files/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/svc-bkp-remote-2-local/files/__init__.py +++ b/tests/unit/python/roles/svc-bkp-remote-2-local/files/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/svc-bkp-volume-2-local/files/__init__.py b/tests/unit/python/roles/svc-bkp-volume-2-local/files/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/svc-bkp-volume-2-local/files/__init__.py +++ b/tests/unit/python/roles/svc-bkp-volume-2-local/files/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/svc-bkp-volume-2-local/library/__init__.py b/tests/unit/python/roles/svc-bkp-volume-2-local/library/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/svc-bkp-volume-2-local/library/__init__.py +++ b/tests/unit/python/roles/svc-bkp-volume-2-local/library/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/svc-net-tor-smtp/files/__init__.py b/tests/unit/python/roles/svc-net-tor-smtp/files/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/svc-net-tor-smtp/files/__init__.py +++ b/tests/unit/python/roles/svc-net-tor-smtp/files/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/svc-net-tor/files/__init__.py b/tests/unit/python/roles/svc-net-tor/files/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/svc-net-tor/files/__init__.py +++ b/tests/unit/python/roles/svc-net-tor/files/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/svc-net-wireguard/__init__.py b/tests/unit/python/roles/svc-net-wireguard/__init__.py index 8ee16d7567..e69de29bb2 100644 --- a/tests/unit/python/roles/svc-net-wireguard/__init__.py +++ b/tests/unit/python/roles/svc-net-wireguard/__init__.py @@ -1,3 +0,0 @@ -from pathlib import Path - -PROJECT_ROOT: Path = Path(__file__).resolve().parents[5] diff --git a/tests/unit/python/roles/svc-net-wireguard/filter_plugins/__init__.py b/tests/unit/python/roles/svc-net-wireguard/filter_plugins/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/svc-net-wireguard/filter_plugins/__init__.py +++ b/tests/unit/python/roles/svc-net-wireguard/filter_plugins/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/svc-opt-keyboard-color/files/__init__.py b/tests/unit/python/roles/svc-opt-keyboard-color/files/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/svc-opt-keyboard-color/files/__init__.py +++ b/tests/unit/python/roles/svc-opt-keyboard-color/files/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/sys-ctl-cln-bkps/files/__init__.py b/tests/unit/python/roles/sys-ctl-cln-bkps/files/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/sys-ctl-cln-bkps/files/__init__.py +++ b/tests/unit/python/roles/sys-ctl-cln-bkps/files/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/sys-ctl-hlth-csp/files/__init__.py b/tests/unit/python/roles/sys-ctl-hlth-csp/files/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/sys-ctl-hlth-csp/files/__init__.py +++ b/tests/unit/python/roles/sys-ctl-hlth-csp/files/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/sys-ctl-hlth-csp/lookup_plugins/__init__.py b/tests/unit/python/roles/sys-ctl-hlth-csp/lookup_plugins/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/sys-ctl-hlth-csp/lookup_plugins/__init__.py +++ b/tests/unit/python/roles/sys-ctl-hlth-csp/lookup_plugins/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/sys-ctl-hlth-disc-space/files/__init__.py b/tests/unit/python/roles/sys-ctl-hlth-disc-space/files/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/sys-ctl-hlth-disc-space/files/__init__.py +++ b/tests/unit/python/roles/sys-ctl-hlth-disc-space/files/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/sys-ctl-hlth-msmtp/lookup_plugins/__init__.py b/tests/unit/python/roles/sys-ctl-hlth-msmtp/lookup_plugins/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/sys-ctl-hlth-msmtp/lookup_plugins/__init__.py +++ b/tests/unit/python/roles/sys-ctl-hlth-msmtp/lookup_plugins/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/sys-ctl-hlth-webserver/files/__init__.py b/tests/unit/python/roles/sys-ctl-hlth-webserver/files/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/sys-ctl-hlth-webserver/files/__init__.py +++ b/tests/unit/python/roles/sys-ctl-hlth-webserver/files/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/sys-ctl-hlth-webserver/filter_plugins/__init__.py b/tests/unit/python/roles/sys-ctl-hlth-webserver/filter_plugins/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/sys-ctl-hlth-webserver/filter_plugins/__init__.py +++ b/tests/unit/python/roles/sys-ctl-hlth-webserver/filter_plugins/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/sys-ctl-rpr-container-hard/files/__init__.py b/tests/unit/python/roles/sys-ctl-rpr-container-hard/files/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/sys-ctl-rpr-container-hard/files/__init__.py +++ b/tests/unit/python/roles/sys-ctl-rpr-container-hard/files/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/sys-ctl-rpr-container-soft/files/__init__.py b/tests/unit/python/roles/sys-ctl-rpr-container-soft/files/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/sys-ctl-rpr-container-soft/files/__init__.py +++ b/tests/unit/python/roles/sys-ctl-rpr-container-soft/files/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/sys-dns-wildcards/filter_plugins/__init__.py b/tests/unit/python/roles/sys-dns-wildcards/filter_plugins/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/sys-dns-wildcards/filter_plugins/__init__.py +++ b/tests/unit/python/roles/sys-dns-wildcards/filter_plugins/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/sys-front-inj-all/filter_plugins/__init__.py b/tests/unit/python/roles/sys-front-inj-all/filter_plugins/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/sys-front-inj-all/filter_plugins/__init__.py +++ b/tests/unit/python/roles/sys-front-inj-all/filter_plugins/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/sys-lock/files/__init__.py b/tests/unit/python/roles/sys-lock/files/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/sys-lock/files/__init__.py +++ b/tests/unit/python/roles/sys-lock/files/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/sys-svc-compose-ca/files/__init__.py b/tests/unit/python/roles/sys-svc-compose-ca/files/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/sys-svc-compose-ca/files/__init__.py +++ b/tests/unit/python/roles/sys-svc-compose-ca/files/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/sys-svc-compose/files/__init__.py b/tests/unit/python/roles/sys-svc-compose/files/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/sys-svc-compose/files/__init__.py +++ b/tests/unit/python/roles/sys-svc-compose/files/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/sys-svc-compose/files/swarm/__init__.py b/tests/unit/python/roles/sys-svc-compose/files/swarm/__init__.py index 86efabd459..6290ad0f99 100644 --- a/tests/unit/python/roles/sys-svc-compose/files/swarm/__init__.py +++ b/tests/unit/python/roles/sys-svc-compose/files/swarm/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[7] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/sys-svc-compose/lookup_plugins/__init__.py b/tests/unit/python/roles/sys-svc-compose/lookup_plugins/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/sys-svc-compose/lookup_plugins/__init__.py +++ b/tests/unit/python/roles/sys-svc-compose/lookup_plugins/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/sys-svc-container/files/__init__.py b/tests/unit/python/roles/sys-svc-container/files/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/sys-svc-container/files/__init__.py +++ b/tests/unit/python/roles/sys-svc-container/files/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/sys-svc-proxy/templates/__init__.py b/tests/unit/python/roles/sys-svc-proxy/templates/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/sys-svc-proxy/templates/__init__.py +++ b/tests/unit/python/roles/sys-svc-proxy/templates/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/sys-svc-rdbms/files/__init__.py b/tests/unit/python/roles/sys-svc-rdbms/files/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/sys-svc-rdbms/files/__init__.py +++ b/tests/unit/python/roles/sys-svc-rdbms/files/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/sys-util-git-pull/files/__init__.py b/tests/unit/python/roles/sys-util-git-pull/files/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/sys-util-git-pull/files/__init__.py +++ b/tests/unit/python/roles/sys-util-git-pull/files/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/test-e2e-cli/files/__init__.py b/tests/unit/python/roles/test-e2e-cli/files/__init__.py index 9f69c70b47..e69de29bb2 100644 --- a/tests/unit/python/roles/test-e2e-cli/files/__init__.py +++ b/tests/unit/python/roles/test-e2e-cli/files/__init__.py @@ -1,3 +0,0 @@ -from pathlib import Path - -PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] diff --git a/tests/unit/python/roles/test-e2e-cli/files/shared/__init__.py b/tests/unit/python/roles/test-e2e-cli/files/shared/__init__.py index 86efabd459..e69de29bb2 100644 --- a/tests/unit/python/roles/test-e2e-cli/files/shared/__init__.py +++ b/tests/unit/python/roles/test-e2e-cli/files/shared/__init__.py @@ -1,3 +0,0 @@ -from pathlib import Path - -PROJECT_ROOT: Path = Path(__file__).resolve().parents[7] diff --git a/tests/unit/python/roles/test-e2e-cli/files/shared/mcp/__init__.py b/tests/unit/python/roles/test-e2e-cli/files/shared/mcp/__init__.py index 1f90564319..0bd565f9d2 100644 --- a/tests/unit/python/roles/test-e2e-cli/files/shared/mcp/__init__.py +++ b/tests/unit/python/roles/test-e2e-cli/files/shared/mcp/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[8] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/test-e2e-cli/filter_plugins/__init__.py b/tests/unit/python/roles/test-e2e-cli/filter_plugins/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/test-e2e-cli/filter_plugins/__init__.py +++ b/tests/unit/python/roles/test-e2e-cli/filter_plugins/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/test-e2e-playwright/filter_plugins/__init__.py b/tests/unit/python/roles/test-e2e-playwright/filter_plugins/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/test-e2e-playwright/filter_plugins/__init__.py +++ b/tests/unit/python/roles/test-e2e-playwright/filter_plugins/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/web-app-baserow/files/__init__.py b/tests/unit/python/roles/web-app-baserow/files/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/web-app-baserow/files/__init__.py +++ b/tests/unit/python/roles/web-app-baserow/files/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/web-app-baserow/files/sso/__init__.py b/tests/unit/python/roles/web-app-baserow/files/sso/__init__.py index 86efabd459..6290ad0f99 100644 --- a/tests/unit/python/roles/web-app-baserow/files/sso/__init__.py +++ b/tests/unit/python/roles/web-app-baserow/files/sso/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[7] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/web-app-bookwyrm/files/sso/__init__.py b/tests/unit/python/roles/web-app-bookwyrm/files/sso/__init__.py index 86efabd459..6290ad0f99 100644 --- a/tests/unit/python/roles/web-app-bookwyrm/files/sso/__init__.py +++ b/tests/unit/python/roles/web-app-bookwyrm/files/sso/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[7] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/web-app-dashboard/filter_plugins/__init__.py b/tests/unit/python/roles/web-app-dashboard/filter_plugins/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/web-app-dashboard/filter_plugins/__init__.py +++ b/tests/unit/python/roles/web-app-dashboard/filter_plugins/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/web-app-flowise/files/__init__.py b/tests/unit/python/roles/web-app-flowise/files/__init__.py index 9f69c70b47..e69de29bb2 100644 --- a/tests/unit/python/roles/web-app-flowise/files/__init__.py +++ b/tests/unit/python/roles/web-app-flowise/files/__init__.py @@ -1,3 +0,0 @@ -from pathlib import Path - -PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] diff --git a/tests/unit/python/roles/web-app-flowise/files/reconcile/__init__.py b/tests/unit/python/roles/web-app-flowise/files/reconcile/__init__.py index 86efabd459..6290ad0f99 100644 --- a/tests/unit/python/roles/web-app-flowise/files/reconcile/__init__.py +++ b/tests/unit/python/roles/web-app-flowise/files/reconcile/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[7] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/web-app-jellyfin/files/__init__.py b/tests/unit/python/roles/web-app-jellyfin/files/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/web-app-jellyfin/files/__init__.py +++ b/tests/unit/python/roles/web-app-jellyfin/files/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/web-app-keycloak/templates/__init__.py b/tests/unit/python/roles/web-app-keycloak/templates/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/web-app-keycloak/templates/__init__.py +++ b/tests/unit/python/roles/web-app-keycloak/templates/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/web-app-littlejs/filter_plugins/__init__.py b/tests/unit/python/roles/web-app-littlejs/filter_plugins/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/web-app-littlejs/filter_plugins/__init__.py +++ b/tests/unit/python/roles/web-app-littlejs/filter_plugins/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/web-app-mailu/lookup_plugins/__init__.py b/tests/unit/python/roles/web-app-mailu/lookup_plugins/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/web-app-mailu/lookup_plugins/__init__.py +++ b/tests/unit/python/roles/web-app-mailu/lookup_plugins/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/web-app-matrix/lookup_plugins/__init__.py b/tests/unit/python/roles/web-app-matrix/lookup_plugins/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/web-app-matrix/lookup_plugins/__init__.py +++ b/tests/unit/python/roles/web-app-matrix/lookup_plugins/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/web-app-n8n/files/__init__.py b/tests/unit/python/roles/web-app-n8n/files/__init__.py index 9f69c70b47..e69de29bb2 100644 --- a/tests/unit/python/roles/web-app-n8n/files/__init__.py +++ b/tests/unit/python/roles/web-app-n8n/files/__init__.py @@ -1,3 +0,0 @@ -from pathlib import Path - -PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] diff --git a/tests/unit/python/roles/web-app-n8n/files/provision/__init__.py b/tests/unit/python/roles/web-app-n8n/files/provision/__init__.py index 86efabd459..6290ad0f99 100644 --- a/tests/unit/python/roles/web-app-n8n/files/provision/__init__.py +++ b/tests/unit/python/roles/web-app-n8n/files/provision/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[7] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/web-app-nextcloud/filter_plugins/__init__.py b/tests/unit/python/roles/web-app-nextcloud/filter_plugins/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/web-app-nextcloud/filter_plugins/__init__.py +++ b/tests/unit/python/roles/web-app-nextcloud/filter_plugins/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/web-app-nextcloud/lookup_plugins/__init__.py b/tests/unit/python/roles/web-app-nextcloud/lookup_plugins/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/web-app-nextcloud/lookup_plugins/__init__.py +++ b/tests/unit/python/roles/web-app-nextcloud/lookup_plugins/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/web-app-openwebui/files/__init__.py b/tests/unit/python/roles/web-app-openwebui/files/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/web-app-openwebui/files/__init__.py +++ b/tests/unit/python/roles/web-app-openwebui/files/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/web-app-openwebui/templates/__init__.py b/tests/unit/python/roles/web-app-openwebui/templates/__init__.py index 9f69c70b47..e69de29bb2 100644 --- a/tests/unit/python/roles/web-app-openwebui/templates/__init__.py +++ b/tests/unit/python/roles/web-app-openwebui/templates/__init__.py @@ -1,3 +0,0 @@ -from pathlib import Path - -PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] diff --git a/tests/unit/python/roles/web-app-peertube/filter_plugins/__init__.py b/tests/unit/python/roles/web-app-peertube/filter_plugins/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/web-app-peertube/filter_plugins/__init__.py +++ b/tests/unit/python/roles/web-app-peertube/filter_plugins/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/web-app-stalwart/files/__init__.py b/tests/unit/python/roles/web-app-stalwart/files/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/web-app-stalwart/files/__init__.py +++ b/tests/unit/python/roles/web-app-stalwart/files/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/web-app-stalwart/lookup_plugins/__init__.py b/tests/unit/python/roles/web-app-stalwart/lookup_plugins/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/web-app-stalwart/lookup_plugins/__init__.py +++ b/tests/unit/python/roles/web-app-stalwart/lookup_plugins/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/web-opt-rdr-www/lookup_plugins/__init__.py b/tests/unit/python/roles/web-opt-rdr-www/lookup_plugins/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/web-opt-rdr-www/lookup_plugins/__init__.py +++ b/tests/unit/python/roles/web-opt-rdr-www/lookup_plugins/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/web-svc-logout/filter_plugins/__init__.py b/tests/unit/python/roles/web-svc-logout/filter_plugins/__init__.py index 9f69c70b47..087c384b57 100644 --- a/tests/unit/python/roles/web-svc-logout/filter_plugins/__init__.py +++ b/tests/unit/python/roles/web-svc-logout/filter_plugins/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[6] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/roles/web_app_keycloak/__init__.py b/tests/unit/python/roles/web_app_keycloak/__init__.py index 8ee16d7567..aa7a30d2e5 100644 --- a/tests/unit/python/roles/web_app_keycloak/__init__.py +++ b/tests/unit/python/roles/web_app_keycloak/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[5] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/utils/__init__.py b/tests/unit/python/utils/__init__.py index 88f288ec1f..e69de29bb2 100644 --- a/tests/unit/python/utils/__init__.py +++ b/tests/unit/python/utils/__init__.py @@ -1,3 +0,0 @@ -from pathlib import Path - -PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] diff --git a/tests/unit/python/utils/cache/__init__.py b/tests/unit/python/utils/cache/__init__.py index 8ee16d7567..aa7a30d2e5 100644 --- a/tests/unit/python/utils/cache/__init__.py +++ b/tests/unit/python/utils/cache/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[5] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/utils/cache/test_applications.py b/tests/unit/python/utils/cache/test_applications.py index 66502c19d9..2f719500f8 100644 --- a/tests/unit/python/utils/cache/test_applications.py +++ b/tests/unit/python/utils/cache/test_applications.py @@ -15,8 +15,8 @@ import sys import tempfile import textwrap import unittest +import unittest.mock as mock from pathlib import Path -from unittest import mock from utils.cache import _reset_cache_for_tests from utils.cache import applications as cache_apps diff --git a/tests/unit/python/utils/diagnostics/__init__.py b/tests/unit/python/utils/diagnostics/__init__.py index 8ee16d7567..aa7a30d2e5 100644 --- a/tests/unit/python/utils/diagnostics/__init__.py +++ b/tests/unit/python/utils/diagnostics/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[5] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/utils/github/__init__.py b/tests/unit/python/utils/github/__init__.py index 8ee16d7567..e69de29bb2 100644 --- a/tests/unit/python/utils/github/__init__.py +++ b/tests/unit/python/utils/github/__init__.py @@ -1,3 +0,0 @@ -from pathlib import Path - -PROJECT_ROOT: Path = Path(__file__).resolve().parents[5] diff --git a/tests/unit/python/utils/mail/__init__.py b/tests/unit/python/utils/mail/__init__.py index 8ee16d7567..aa7a30d2e5 100644 --- a/tests/unit/python/utils/mail/__init__.py +++ b/tests/unit/python/utils/mail/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[5] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/utils/recovery/__init__.py b/tests/unit/python/utils/recovery/__init__.py index 8ee16d7567..aa7a30d2e5 100644 --- a/tests/unit/python/utils/recovery/__init__.py +++ b/tests/unit/python/utils/recovery/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[5] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/utils/roles/__init__.py b/tests/unit/python/utils/roles/__init__.py index 8ee16d7567..aa7a30d2e5 100644 --- a/tests/unit/python/utils/roles/__init__.py +++ b/tests/unit/python/utils/roles/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[5] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/unit/python/utils/templating/__init__.py b/tests/unit/python/utils/templating/__init__.py index 8ee16d7567..aa7a30d2e5 100644 --- a/tests/unit/python/utils/templating/__init__.py +++ b/tests/unit/python/utils/templating/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[5] + +__all__ = ["PROJECT_ROOT"] diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py index a11dbb1745..8125c38905 100644 --- a/tests/utils/__init__.py +++ b/tests/utils/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[2] + +__all__ = ["PROJECT_ROOT"] diff --git a/utils/__init__.py b/utils/__init__.py index 9f4a6a3289..29a52ec495 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[1] + +__all__ = ["PROJECT_ROOT"] diff --git a/utils/claude/__init__.py b/utils/claude/__init__.py index a11dbb1745..8125c38905 100644 --- a/utils/claude/__init__.py +++ b/utils/claude/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[2] + +__all__ = ["PROJECT_ROOT"] diff --git a/utils/domains/__init__.py b/utils/domains/__init__.py index 5578db5dde..1612edbb60 100644 --- a/utils/domains/__init__.py +++ b/utils/domains/__init__.py @@ -3,3 +3,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[2] + +__all__ = ["PROJECT_ROOT"] diff --git a/utils/github/variant/axes.py b/utils/github/variant/axes.py index f3cd6ed26d..a5c0b7d79c 100644 --- a/utils/github/variant/axes.py +++ b/utils/github/variant/axes.py @@ -40,13 +40,13 @@ import os from typing import TYPE_CHECKING from utils.github.variant import instructions -from utils.github.variant.label import ( # noqa: F401 +from utils.github.variant.label import ( LABEL_RE, LOCAL_GLYPH, Label, parse_label, ) -from utils.github.variant.pools import ( # noqa: F401 +from utils.github.variant.pools import ( DISTROS, FILESYSTEMS, rotate, @@ -67,6 +67,22 @@ from utils.github.variant.vpn import ( from utils.roles.display import VARIANT_SEPARATOR, display_names from utils.symbol_glossary import to_emoji +__all__ = [ + "DISTROS", + "FILESYSTEMS", + "LABEL_RE", + "LOCAL_GLYPH", + "MODES", + "Label", + "artifact_slug", + "assign", + "check_pins", + "parse_label", + "pick_mode", + "resolve_sweep", + "sort_key", +] + if TYPE_CHECKING: from collections.abc import Mapping, Sequence from typing import Any diff --git a/utils/meta/__init__.py b/utils/meta/__init__.py index a11dbb1745..8125c38905 100644 --- a/utils/meta/__init__.py +++ b/utils/meta/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[2] + +__all__ = ["PROJECT_ROOT"] diff --git a/utils/roles/__init__.py b/utils/roles/__init__.py index a11dbb1745..8125c38905 100644 --- a/utils/roles/__init__.py +++ b/utils/roles/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[2] + +__all__ = ["PROJECT_ROOT"] diff --git a/utils/roles/validation/__init__.py b/utils/roles/validation/__init__.py index 78e3cae448..ff4b515c73 100644 --- a/utils/roles/validation/__init__.py +++ b/utils/roles/validation/__init__.py @@ -1,3 +1,5 @@ from pathlib import Path PROJECT_ROOT: Path = Path(__file__).resolve().parents[3] + +__all__ = ["PROJECT_ROOT"] diff --git a/utils/yaml_bootstrap.py b/utils/yaml_bootstrap.py index 18a1cd289f..16183d8266 100644 --- a/utils/yaml_bootstrap.py +++ b/utils/yaml_bootstrap.py @@ -25,7 +25,7 @@ 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]" + r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x84\x86-\x9f\ud800-\udfffοΏΎοΏΏ]" ) From b0856dcf7b7b764602712d07c978f98c71c41a24 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Tue, 20 Oct 2026 22:10:19 +0200 Subject: [PATCH 23/28] fix(web-app-snipe-it): bridge the SSO header past v8.8.1's HTTP_ refusal Snipe-IT v8.8.1 refuses any `login_remote_user_header_name` matching `/^HTTP_/i`, so the configured `HTTP_X_FORWARDED_PREFERRED_USERNAME` stopped minting sessions behind oauth2-proxy. Apache in the image now copies `X-Forwarded-Preferred-Username` into the server variable `INFINITO_SSO_USER`, and only when the request also carries `X-Infinito-Sso-Proxy` equal to the new per-inventory `sso_proxy_secret`; an empty secret keeps the bridge closed. openresty sends that header on every proxied request, so a client reaching the container directly cannot log in by forging the identity header. A deploy-time check asserts both directions (forged 200, with secret 302). Co-Authored-By: Claude Opus 5.5 (1M context) --- roles/web-app-snipe-it/README.md | 11 ++++++++--- roles/web-app-snipe-it/files/Dockerfile | 2 ++ .../files/php/apply/sso_config.php | 8 -------- roles/web-app-snipe-it/meta/secrets.yml | 6 ++++++ roles/web-app-snipe-it/tasks/00_core.yml | 1 + roles/web-app-snipe-it/tasks/04_sso.yml | 15 +++++++++++++++ roles/web-app-snipe-it/templates/env.j2 | 1 + roles/web-app-snipe-it/vars/main.yml | 4 +++- 8 files changed, 36 insertions(+), 12 deletions(-) diff --git a/roles/web-app-snipe-it/README.md b/roles/web-app-snipe-it/README.md index 147f417cab..e05d00a44e 100644 --- a/roles/web-app-snipe-it/README.md +++ b/roles/web-app-snipe-it/README.md @@ -88,9 +88,14 @@ Solid `1:1` edges are fixed relationships; dashed `0..1` edges are conditional ( When the oauth2-proxy SSO flavor is active, Snipe-IT's native `loginViaRemoteUser()` is enabled via the `Setting` model (`login_remote_user_enabled`, `login_remote_user_header_name = - HTTP_X_FORWARDED_PREFERRED_USERNAME`). nginx gates `/login` through - oauth2-proxy/Keycloak and injects the verified - `X-Forwarded-Preferred-Username` header, which Snipe-IT matches against + INFINITO_SSO_USER`). nginx gates `/login` through + oauth2-proxy/Keycloak, injects the verified + `X-Forwarded-Preferred-Username` header and sends the + `sso_proxy_secret` credential as `X-Infinito-Sso-Proxy` with every + request. Apache in the + container copies the username into the `INFINITO_SSO_USER` server + variable only when that secret matches `INFINITO_SSO_PROXY_SECRET`. + Snipe-IT matches the username against `users.username` (the user must pre-exist and be activated β€” sync them via LDAP) to mint a native `snipeit_session`. Visitors without a matching activated user silently fall through to the normal login form (no error). diff --git a/roles/web-app-snipe-it/files/Dockerfile b/roles/web-app-snipe-it/files/Dockerfile index 43255da7e0..094cce7ccc 100644 --- a/roles/web-app-snipe-it/files/Dockerfile +++ b/roles/web-app-snipe-it/files/Dockerfile @@ -3,3 +3,5 @@ ARG SNIPE_IT_VERSION FROM ${SNIPE_IT_IMAGE}:${SNIPE_IT_VERSION} RUN sed -i 's#php artisan migrate --force#php artisan migrate --force --isolated#' /startup.sh + +RUN printf 'SetEnvIf X-Forwarded-Preferred-Username "^(.+)$" INFINITO_SSO_USER=$1\nSetEnvIfExpr "-z env(\047INFINITO_SSO_PROXY_SECRET\047) || req(\047X-Infinito-Sso-Proxy\047) != env(\047INFINITO_SSO_PROXY_SECRET\047)" !INFINITO_SSO_USER\n' > /etc/apache2/conf.d/infinito-sso.conf diff --git a/roles/web-app-snipe-it/files/php/apply/sso_config.php b/roles/web-app-snipe-it/files/php/apply/sso_config.php index 77a67e1441..2fba9bddfb 100644 --- a/roles/web-app-snipe-it/files/php/apply/sso_config.php +++ b/roles/web-app-snipe-it/files/php/apply/sso_config.php @@ -2,14 +2,6 @@ // nocheck: mirrored-unit-test - boots Snipe-IT's Laravel kernel through // bootstrap/app.php and writes the trusted-reverse-proxy login settings through // App\Models\Setting; the models only exist once the install has booted -// Configure Snipe-IT trusted-reverse-proxy login via the Laravel `Setting` -// model (same path the UI uses). The oauth2-proxy in front of Snipe-IT -// authenticates the user against Keycloak and nginx injects the verified -// identity into the `X-Forwarded-Preferred-Username` request header, which -// PHP exposes as $_SERVER['HTTP_X_FORWARDED_PREFERRED_USERNAME']. -// LoginController::loginViaRemoteUser() then matches users.username and mints -// a native snipeit_session. The password/LDAP form stays available as a -// fallback (login_common_disabled is kept at 0). require "vendor/autoload.php"; $app = require "bootstrap/app.php"; $app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap(); diff --git a/roles/web-app-snipe-it/meta/secrets.yml b/roles/web-app-snipe-it/meta/secrets.yml index 7e665aca46..43316dcfca 100644 --- a/roles/web-app-snipe-it/meta/secrets.yml +++ b/roles/web-app-snipe-it/meta/secrets.yml @@ -10,3 +10,9 @@ credentials: algorithm: alphanumeric validation: "^[A-Za-z0-9]{32,}$" length: 48 + sso_proxy_secret: + description: "Shared secret the reverse proxy sends with every request; Apache in the Snipe-IT container only turns the trusted identity header into the login + variable when it matches." + algorithm: alphanumeric + validation: "^[A-Za-z0-9]{32,}$" + length: 48 diff --git a/roles/web-app-snipe-it/tasks/00_core.yml b/roles/web-app-snipe-it/tasks/00_core.yml index a7d4c2cc1b..7f582896aa 100644 --- a/roles/web-app-snipe-it/tasks/00_core.yml +++ b/roles/web-app-snipe-it/tasks/00_core.yml @@ -13,6 +13,7 @@ name: sys-stk-full vars: compose_handlers_flush: true + webserver_location_extra_configuration: 'proxy_set_header {{ SNIPE_IT_SSO_PROXY_HEADER }} "{{ SNIPE_IT_SSO_PROXY_SECRET }}";' - name: "πŸ‘” Snipe-IT manager-side operations (container exec)" include_tasks: 01_install/manager_ops.yml diff --git a/roles/web-app-snipe-it/tasks/04_sso.yml b/roles/web-app-snipe-it/tasks/04_sso.yml index 5dda964562..c08514b0d0 100644 --- a/roles/web-app-snipe-it/tasks/04_sso.yml +++ b/roles/web-app-snipe-it/tasks/04_sso.yml @@ -26,3 +26,18 @@ ansible.builtin.shell: cmd: "container exec {{ SNIPE_IT_CONTAINER_ADDRESS }} php artisan optimize:clear" changed_when: true + +- name: "πŸ”’ snipe-it: the SSO header bridge logs in only proxy-secret requests" + ansible.builtin.shell: + cmd: >- + container exec {{ SNIPE_IT_CONTAINER_ADDRESS }} sh -c + '{{ 30 | curl }} -s -o /dev/null -w "%{http_code}" -H "X-Forwarded-Preferred-Username: $1" {{ item.proxy_header }} http://localhost/login' + _ {{ lookup('users', 'administrator').username | quote }} + loop: + - { label: "forged header without the proxy secret", proxy_header: "", expected: "200" } + - { label: "header with the proxy secret", proxy_header: '-H "{{ SNIPE_IT_SSO_PROXY_HEADER }}: $INFINITO_SSO_PROXY_SECRET"', expected: "302" } + loop_control: + label: "{{ item.label }}" + register: snipe_it_sso_bridge_probe + changed_when: false + failed_when: snipe_it_sso_bridge_probe.stdout != item.expected diff --git a/roles/web-app-snipe-it/templates/env.j2 b/roles/web-app-snipe-it/templates/env.j2 index a725042808..63d28cec03 100644 --- a/roles/web-app-snipe-it/templates/env.j2 +++ b/roles/web-app-snipe-it/templates/env.j2 @@ -78,6 +78,7 @@ SECURE_COOKIES={{ SNIPE_IT_TLS_ENABLED | string | lower }} API_TOKEN_EXPIRATION_YEARS=40 APP_TRUSTED_PROXIES=** +INFINITO_SSO_PROXY_SECRET={{ SNIPE_IT_SSO_PROXY_SECRET }} ALLOW_IFRAMING=false REFERRER_POLICY=same-origin ENABLE_CSP=false diff --git a/roles/web-app-snipe-it/vars/main.yml b/roles/web-app-snipe-it/vars/main.yml index 56966c32ef..689acd417d 100644 --- a/roles/web-app-snipe-it/vars/main.yml +++ b/roles/web-app-snipe-it/vars/main.yml @@ -18,7 +18,9 @@ SNIPE_IT_LDAP_ENABLED: "{{ lookup('config', application_id, 'services.ldap.enabl SNIPE_IT_TLS_ENABLED: "{{ lookup('tls', application_id, 'enabled') | bool }}" SNIPE_IT_SSO_ENABLED: "{{ lookup('sso', application_id, 'is_proxy_gated') }}" -SNIPE_IT_SSO_HEADER_NAME: "HTTP_X_FORWARDED_PREFERRED_USERNAME" +SNIPE_IT_SSO_HEADER_NAME: "INFINITO_SSO_USER" +SNIPE_IT_SSO_PROXY_HEADER: "X-Infinito-Sso-Proxy" +SNIPE_IT_SSO_PROXY_SECRET: "{{ lookup('config', application_id, 'secrets.credentials.sso_proxy_secret') }}" SNIPE_IT_SSO_LOGOUT_URL: "{{ OIDC.CLIENT.LOGOUT_URL }}" SNIPE_IT_LDAP_FILTER_FRAGMENT: "&(objectClass=inetOrgPerson)" From bc56beb7e9357345503b3d99e2f84e75bfddb159 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Tue, 20 Oct 2026 22:30:34 +0200 Subject: [PATCH 24/28] fix(package-cache-frontend): restore upstreams.conf from before 16.2.0 The 16.2.0 release commit (e2501cb1de) shipped a locally generated `compose/package-cache-frontend/upstreams.conf`, the output of the cache generator that only exists on the feature branches (230d9a1e5e). It adds TLS server blocks for argos-net.com, huggingface.co, index.crates.io, registry.ollama.ai and static.crates.io, but main's `cert-gen.sh` issues no certificates for them. nginx then dies on start with cannot load certificate "/etc/nginx/certs/argos-net.com.crt" the frontend stays unhealthy, pypi.org on the runner refuses connections, and `make install-lint` fails in every workspace job (run 39482585010). Restore the file byte for byte to its state before the release (blob b5c5ef08e7), the version every workspace job passed with in run 39443109355. Co-Authored-By: Claude Opus 5.5 (1M context) --- compose/package-cache-frontend/upstreams.conf | 229 ++++++++---------- 1 file changed, 96 insertions(+), 133 deletions(-) diff --git a/compose/package-cache-frontend/upstreams.conf b/compose/package-cache-frontend/upstreams.conf index 9e0acd5a03..b5c5ef08e7 100644 --- a/compose/package-cache-frontend/upstreams.conf +++ b/compose/package-cache-frontend/upstreams.conf @@ -9,8 +9,49 @@ proxy_request_buffering off; proxy_read_timeout 300s; proxy_connect_timeout 30s; +# Docker embedded DNS, pinned to bypass image-specific resolv.conf order. resolver 127.0.0.11 ipv6=off valid=30s; +# Nexus answers 502 when a proxy's remote is unreachable (measured) and has no +# group repository type for apt, so the mirror failover sits here rather than in +# the repo layout. 5xx only: a 404 means the file genuinely is not in the suite, +# and apt probes for missing files often enough that retrying those would double +# the request rate. +# +# Nexus answers that 502 only after trying every address of the remote in turn, +# one connect timeout each; for a remote with several addresses that outlasts a +# client's own timeout before the mirror is ever asked. +# +# These stay prefix locations. A regex location forfeits nginx's implicit +# `/debian` -> `/debian/` redirect, and the URL-reachability test probes exactly +# that slash-less form through the runner's DNS hijack, so it turns into a 404. +server { + listen 80; + server_name deb.debian.org; + location /debian/ { + rewrite ^/debian/(.*)$ /repository/apt-debian/$1 break; + proxy_intercept_errors on; + proxy_read_timeout 8s; + error_page 502 503 504 = @debian_mirror; + proxy_pass http://package-cache:8081; + } + location @debian_mirror { + rewrite ^/repository/apt-debian/(.*)$ /repository/apt-debian-mirror/$1 break; + proxy_pass http://package-cache:8081; + } + location /debian-security/ { + rewrite ^/debian-security/(.*)$ /repository/apt-debian-security/$1 break; + proxy_intercept_errors on; + proxy_read_timeout 8s; + error_page 502 503 504 = @debian_security_mirror; + proxy_pass http://package-cache:8081; + } + location @debian_security_mirror { + rewrite ^/repository/apt-debian-security/(.*)$ /repository/apt-debian-security-mirror/$1 break; + proxy_pass http://package-cache:8081; + } +} + server { listen 80; server_name archive.ubuntu.com; @@ -18,59 +59,26 @@ server { rewrite ^/ubuntu/(.*)$ /repository/apt-ubuntu/$1 break; proxy_intercept_errors on; proxy_read_timeout 8s; - error_page 502 503 504 = @apt_ubuntu_mirror; + error_page 502 503 504 = @ubuntu_mirror; proxy_pass http://package-cache:8081; } - location @apt_ubuntu_mirror { + location @ubuntu_mirror { rewrite ^/repository/apt-ubuntu/(.*)$ /repository/apt-ubuntu-mirror/$1 break; proxy_pass http://package-cache:8081; } } - -server { - listen 443 ssl; - server_name argos-net.com; - ssl_certificate /etc/nginx/certs/argos-net.com.crt; - ssl_certificate_key /etc/nginx/certs/argos-net.com.key; - location / { - proxy_read_timeout 1800s; - proxy_pass http://package-cache:8081/repository/raw-argos-net/; - } -} - -server { - listen 443 ssl; - server_name codeload.github.com; - ssl_certificate /etc/nginx/certs/codeload.github.com.crt; - ssl_certificate_key /etc/nginx/certs/codeload.github.com.key; - location / { - proxy_pass http://package-cache:8081/repository/raw-codeload-github/; - } -} - server { listen 80; - server_name deb.debian.org; - location /debian/ { - rewrite ^/debian/(.*)$ /repository/apt-debian/$1 break; + server_name security.ubuntu.com; + location /ubuntu/ { + rewrite ^/ubuntu/(.*)$ /repository/apt-ubuntu-security/$1 break; proxy_intercept_errors on; proxy_read_timeout 8s; - error_page 502 503 504 = @apt_debian_mirror; + error_page 502 503 504 = @ubuntu_security_mirror; proxy_pass http://package-cache:8081; } - location @apt_debian_mirror { - rewrite ^/repository/apt-debian/(.*)$ /repository/apt-debian-mirror/$1 break; - proxy_pass http://package-cache:8081; - } - location /debian-security/ { - rewrite ^/debian-security/(.*)$ /repository/apt-debian-security/$1 break; - proxy_intercept_errors on; - proxy_read_timeout 8s; - error_page 502 503 504 = @apt_debian_security_mirror; - proxy_pass http://package-cache:8081; - } - location @apt_debian_security_mirror { - rewrite ^/repository/apt-debian-security/(.*)$ /repository/apt-debian-security-mirror/$1 break; + location @ubuntu_security_mirror { + rewrite ^/repository/apt-ubuntu-security/(.*)$ /repository/apt-ubuntu-security-mirror/$1 break; proxy_pass http://package-cache:8081; } } @@ -86,11 +94,11 @@ server { server { listen 443 ssl; - server_name dl-cdn.alpinelinux.org; - ssl_certificate /etc/nginx/certs/dl-cdn.alpinelinux.org.crt; - ssl_certificate_key /etc/nginx/certs/dl-cdn.alpinelinux.org.key; + server_name pypi.org; + ssl_certificate /etc/nginx/certs/pypi.org.crt; + ssl_certificate_key /etc/nginx/certs/pypi.org.key; location / { - proxy_pass http://package-cache:8081/repository/raw-alpine/; + proxy_pass http://package-cache:8081/repository/pypi-proxy/; } } @@ -104,71 +112,13 @@ server { } } -server { - listen 443 ssl; - server_name huggingface.co; - ssl_certificate /etc/nginx/certs/huggingface.co.crt; - ssl_certificate_key /etc/nginx/certs/huggingface.co.key; - location / { - proxy_pass http://package-cache:8081/repository/raw-huggingface/; - } -} - -server { - listen 443 ssl; - server_name index.crates.io; - ssl_certificate /etc/nginx/certs/index.crates.io.crt; - ssl_certificate_key /etc/nginx/certs/index.crates.io.key; - location / { - proxy_pass http://package-cache:8081/repository/raw-crates-index/; - } -} - -server { - listen 443 ssl; - server_name index.rubygems.org; - ssl_certificate /etc/nginx/certs/index.rubygems.org.crt; - ssl_certificate_key /etc/nginx/certs/index.rubygems.org.key; - location / { - proxy_pass http://package-cache:8081/repository/gem-proxy/; - } -} - -server { - listen 443 ssl; - server_name proxy.golang.org; - ssl_certificate /etc/nginx/certs/proxy.golang.org.crt; - ssl_certificate_key /etc/nginx/certs/proxy.golang.org.key; - location / { - proxy_pass http://package-cache:8081/repository/go-proxy/; - } -} - -server { - listen 443 ssl; - server_name pypi.org; - ssl_certificate /etc/nginx/certs/pypi.org.crt; - ssl_certificate_key /etc/nginx/certs/pypi.org.key; - location / { - proxy_pass http://package-cache:8081/repository/pypi-proxy/; - } -} - -server { - listen 443 ssl; - server_name raw.githubusercontent.com; - ssl_certificate /etc/nginx/certs/raw.githubusercontent.com.crt; - ssl_certificate_key /etc/nginx/certs/raw.githubusercontent.com.key; - location / { - proxy_pass http://package-cache:8081/repository/raw-githubusercontent/; - } -} - server { listen 443 ssl; server_name registry.npmjs.org; ssl_certificate /etc/nginx/certs/registry.npmjs.org.crt; ssl_certificate_key /etc/nginx/certs/registry.npmjs.org.key; + # Tarball URLs Nexus emits already carry /repository/npm-proxy/; pass them + # through as-is (no path on proxy_pass) so they aren't double-prefixed β†’ 404. location /repository/npm-proxy/ { proxy_pass http://package-cache:8081; } @@ -179,11 +129,20 @@ server { server { listen 443 ssl; - server_name registry.ollama.ai; - ssl_certificate /etc/nginx/certs/registry.ollama.ai.crt; - ssl_certificate_key /etc/nginx/certs/registry.ollama.ai.key; + server_name rubygems.org; + ssl_certificate /etc/nginx/certs/rubygems.org.crt; + ssl_certificate_key /etc/nginx/certs/rubygems.org.key; location / { - proxy_pass http://package-cache:8081/repository/raw-ollama-registry/; + proxy_pass http://package-cache:8081/repository/gem-proxy/; + } +} +server { + listen 443 ssl; + server_name index.rubygems.org; + ssl_certificate /etc/nginx/certs/index.rubygems.org.crt; + ssl_certificate_key /etc/nginx/certs/index.rubygems.org.key; + location / { + proxy_pass http://package-cache:8081/repository/gem-proxy/; } } @@ -199,36 +158,40 @@ server { server { listen 443 ssl; - server_name rubygems.org; - ssl_certificate /etc/nginx/certs/rubygems.org.crt; - ssl_certificate_key /etc/nginx/certs/rubygems.org.key; + server_name proxy.golang.org; + ssl_certificate /etc/nginx/certs/proxy.golang.org.crt; + ssl_certificate_key /etc/nginx/certs/proxy.golang.org.key; location / { - proxy_pass http://package-cache:8081/repository/gem-proxy/; - } -} - -server { - listen 80; - server_name security.ubuntu.com; - location /ubuntu/ { - rewrite ^/ubuntu/(.*)$ /repository/apt-ubuntu-security/$1 break; - proxy_intercept_errors on; - proxy_read_timeout 8s; - error_page 502 503 504 = @apt_ubuntu_security_mirror; - proxy_pass http://package-cache:8081; - } - location @apt_ubuntu_security_mirror { - rewrite ^/repository/apt-ubuntu-security/(.*)$ /repository/apt-ubuntu-security-mirror/$1 break; - proxy_pass http://package-cache:8081; + proxy_pass http://package-cache:8081/repository/go-proxy/; } } server { listen 443 ssl; - server_name static.crates.io; - ssl_certificate /etc/nginx/certs/static.crates.io.crt; - ssl_certificate_key /etc/nginx/certs/static.crates.io.key; + server_name dl-cdn.alpinelinux.org; + ssl_certificate /etc/nginx/certs/dl-cdn.alpinelinux.org.crt; + ssl_certificate_key /etc/nginx/certs/dl-cdn.alpinelinux.org.key; location / { - proxy_pass http://package-cache:8081/repository/raw-crates-static/; + proxy_pass http://package-cache:8081/repository/raw-alpine/; + } +} + +server { + listen 443 ssl; + server_name raw.githubusercontent.com; + ssl_certificate /etc/nginx/certs/raw.githubusercontent.com.crt; + ssl_certificate_key /etc/nginx/certs/raw.githubusercontent.com.key; + location / { + proxy_pass http://package-cache:8081/repository/raw-githubusercontent/; + } +} + +server { + listen 443 ssl; + server_name codeload.github.com; + ssl_certificate /etc/nginx/certs/codeload.github.com.crt; + ssl_certificate_key /etc/nginx/certs/codeload.github.com.key; + location / { + proxy_pass http://package-cache:8081/repository/raw-codeload-github/; } } From 66eb3c70825302560c929921e61d0fb4da5b9cc8 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Tue, 20 Oct 2026 22:35:05 +0200 Subject: [PATCH 25/28] Release version 16.2.1 --- CHANGELOG.md | 21 +++++++++++++++++++++ packaging/arch/PKGBUILD | 2 +- packaging/debian/changelog | 23 +++++++++++++++++++++++ packaging/fedora/infinito-nexus.spec | 22 +++++++++++++++++++++- pyproject.toml | 2 +- 5 files changed, 67 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b491b18d30..98d225c765 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## [16.2.1] - 2026-10-20 + +Fixed: + +- Cache: the package cache frontend starts again; 16.2.0 broke its nginx +- Snipe-IT: SSO login works again on v8.8.1 behind oauth2-proxy +- Snipe-IT: the SSO header is only trusted with the proxy's shared secret +- Wazuh: seeded bcrypt salts no longer leak a fast password digest + +Version jumps (net since 16.2.0): + +- *web-app-socialhome*: 2026.10.17 to 2026.10.20 + +Upgrade notes: + +- Snipe-IT: re-run *infinito administration inventory provision* once + +Contributors: + +- [Kevin Veen-Birkenbach](https://veen.world): cache, Snipe-IT SSO, bcrypt + ## [16.2.0] - 2026-10-20 Added: diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD index 56668d0805..3d0f24b702 100644 --- a/packaging/arch/PKGBUILD +++ b/packaging/arch/PKGBUILD @@ -1,7 +1,7 @@ # Maintainer: Kevin Veen-Birkenbach pkgname=infinito-nexus -pkgver=16.2.0 +pkgver=16.2.1 pkgrel=1 pkgdesc="Meta package for Infinito.Nexus host dependencies (dev + CI)." arch=('any') diff --git a/packaging/debian/changelog b/packaging/debian/changelog index 3d1e3dfd16..103fd7dd93 100644 --- a/packaging/debian/changelog +++ b/packaging/debian/changelog @@ -1,3 +1,26 @@ +infinito-nexus (16.2.1-1) unstable; urgency=medium + + Fixed: + + - Cache: the package cache frontend starts again; 16.2.0 broke its nginx + - Snipe-IT: SSO login works again on v8.8.1 behind oauth2-proxy + - Snipe-IT: the SSO header is only trusted with the proxy's shared secret + - Wazuh: seeded bcrypt salts no longer leak a fast password digest + + Version jumps (net since 16.2.0): + + - *web-app-socialhome*: 2026.10.17 to 2026.10.20 + + Upgrade notes: + + - Snipe-IT: re-run *infinito administration inventory provision* once + + Contributors: + + - [Kevin Veen-Birkenbach](https://veen.world): cache, Snipe-IT SSO, bcrypt + + -- Kevin Veen-Birkenbach Tue, 20 Oct 2026 22:35:05 +0200 + infinito-nexus (16.2.0-1) unstable; urgency=medium Added: diff --git a/packaging/fedora/infinito-nexus.spec b/packaging/fedora/infinito-nexus.spec index 666506d68d..198d8b1f29 100644 --- a/packaging/fedora/infinito-nexus.spec +++ b/packaging/fedora/infinito-nexus.spec @@ -1,5 +1,5 @@ Name: infinito-nexus -Version: 16.2.0 +Version: 16.2.1 Release: 1%{?dist} Summary: Meta package for Infinito.Nexus host dependencies @@ -58,6 +58,26 @@ install -d %{buildroot}%{_docdir}/%{name} %doc %{_docdir}/%{name}/DEPENDENCIES %changelog +* Tue Oct 20 2026 Kevin Veen-Birkenbach - 16.2.1-1 +- Fixed: + +- - Cache: the package cache frontend starts again; 16.2.0 broke its nginx +- - Snipe-IT: SSO login works again on v8.8.1 behind oauth2-proxy +- - Snipe-IT: the SSO header is only trusted with the proxy's shared secret +- - Wazuh: seeded bcrypt salts no longer leak a fast password digest + +- Version jumps (net since 16.2.0): + +- - *web-app-socialhome*: 2026.10.17 to 2026.10.20 + +- Upgrade notes: + +- - Snipe-IT: re-run *infinito administration inventory provision* once + +- Contributors: + +- - [Kevin Veen-Birkenbach](https://veen.world): cache, Snipe-IT SSO, bcrypt + * Tue Oct 20 2026 Kevin Veen-Birkenbach - 16.2.0-1 - Added: diff --git a/pyproject.toml b/pyproject.toml index 90ee16c53a..a00a1d1f0b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "infinito-nexus" -version = "16.2.0" +version = "16.2.1" description = "Infinito.Nexus" readme = "README.md" requires-python = ">=3.11" From 701931eb92541cb2adbc8989b5980a3f77a91f31 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Tue, 20 Oct 2026 23:27:18 +0200 Subject: [PATCH 26/28] docs(svc-bkp-local-2-device): state the device filesystem requirement The device backup runs `rsync -aHAXP` since #1090, which needs the mounted filesystem to store owners, POSIX ACLs and extended attributes. The role never creates that filesystem, so name the requirement where the operator reads how the device is used. Co-Authored-By: Claude Opus 5.5 (1M context) --- roles/svc-bkp-local-2-device/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/roles/svc-bkp-local-2-device/README.md b/roles/svc-bkp-local-2-device/README.md index 962a9a2ce9..ebfa53fc13 100644 --- a/roles/svc-bkp-local-2-device/README.md +++ b/roles/svc-bkp-local-2-device/README.md @@ -9,6 +9,7 @@ The transfer is hard-link deduplicated against the previous snapshot, so each pl This role asserts that the mount, source, and target configuration values are present, deploys the rsync-driver script, and installs a systemd mount unit plus a oneshot service. The oneshot service is triggered automatically when the configured mount path appears, so the only operator action is to physically plug the device in. +The filesystem mounted at the configured mount path must store owners, POSIX ACLs and extended attributes (for example ext4 or xfs). The backup runs `rsync -aHAXP`, which fails on FAT or exFAT. ## Cosmos From b1c7e98d3a8d79d8d4d81bee3598503852a1be97 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Tue, 20 Oct 2026 23:27:39 +0200 Subject: [PATCH 27/28] fix(test-e2e-playwright): bound the shared Playwright image pull The shared `container pull` of the Playwright image had no timeout. A wedged registry-proxy tunnel kept a local deploy on that task for 18 minutes with no traffic until the pull was killed by hand; the task's `retries: 3` never got a chance because the first attempt never returned. Run the pull under `timeout -k 60 `, the same shape the compose build handlers use. A stuck pull now exits with 124 and the existing retry loop starts a fresh connection. Co-Authored-By: Claude Opus 5.5 (1M context) --- roles/test-e2e-playwright/tasks/01_setup.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/roles/test-e2e-playwright/tasks/01_setup.yml b/roles/test-e2e-playwright/tasks/01_setup.yml index 6d9b548091..1d52eced18 100644 --- a/roles/test-e2e-playwright/tasks/01_setup.yml +++ b/roles/test-e2e-playwright/tasks/01_setup.yml @@ -53,6 +53,10 @@ - name: ⬇️ Pull the Playwright image once for every role of this run ansible.builtin.command: argv: + - timeout + - -k + - "60" + - "{{ lookup('timeout', 1800) | int }}" - container - pull - "{{ TEST_E2E_PLAYWRIGHT_IMAGE }}" From d7c4abab95aafa30870a48ec139241e7ab369f91 Mon Sep 17 00:00:00 2001 From: "infinito-nexus-updater[bot]" <286144613+infinito-nexus-updater[bot]@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:41:36 +0000 Subject: [PATCH 28/28] update(images): update Docker image versions (svc-runner) --- roles/svc-runner/meta/services.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/svc-runner/meta/services.yml b/roles/svc-runner/meta/services.yml index 17dddd36b2..8e61f10154 100644 --- a/roles/svc-runner/meta/services.yml +++ b/roles/svc-runner/meta/services.yml @@ -30,7 +30,7 @@ buildkit: enabled: false shared: false image: moby/buildkit - version: "v0.34.0" + version: "v0.34.1" # nocheck: dynamic-flag act-runner: enabled: false