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 6167740b27..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-latest + 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 3127bc68ef..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-latest + 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 d0d54bbff1..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-latest + 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 7bf5546484..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-latest + runs-on: ubuntu-26.04 permissions: issues: write actions: read @@ -374,7 +374,7 @@ jobs: - lint - security-codeql if: ${{ always() }} - runs-on: ubuntu-latest + 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 c6cc449a20..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-latest + 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-latest + 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 b1da0d71dd..dc3e52ad1b 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-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-latest + runs-on: ubuntu-26.04 timeout-minutes: 355 needs: discover @@ -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 133b4194bf..05c4c9ed3b 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-26.04 timeout-minutes: 30 steps: - name: 🧹 Free disk space @@ -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-install-make.yml b/.github/workflows/call-test-install-make.yml index 06acb0304f..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-latest + 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 836eb87a31..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-latest + 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 102f25ae7d..a888feb280 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-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-latest + runs-on: ubuntu-26.04 timeout-minutes: 345 needs: prepare @@ -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: | diff --git a/.github/workflows/call-test.yml b/.github/workflows/call-test.yml index ac26225993..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-latest + 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 5a1c32c004..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-latest + 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-latest + 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-latest + 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 35a5d0168e..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-latest + 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-latest + 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-latest + 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 50dc753b10..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-latest + 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 6f54549d13..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-latest + 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 6b84158dfc..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-latest + 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 8e17fadbae..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-latest + 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 35f47e7b7f..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: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + 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 56e4a1e9f1..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-latest + 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 69eb85746f..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-latest + 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 57f610fc05..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-latest + 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-latest + 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-latest + 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-latest + 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-latest + 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 dfd1df7907..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-latest + 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-latest + 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 8b9113032e..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-latest + 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 efc372ad24..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-latest + 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 36d4aa63ee..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-latest + 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-latest + 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-latest + 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-latest + 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 c2f8e6ae12..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-latest + 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 a3ced798c0..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-latest + 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 a22ee0ca8b..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-latest + 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-latest + 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-latest + 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-latest + 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-latest + runs-on: ubuntu-26.04 outputs: allowed: ${{ steps.guard.outputs.allowed }} steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a1626fff7..98d225c765 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,59 @@ # 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: + +- 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/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/__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/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/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/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/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 new file mode 100644 index 0000000000..247a64576a --- /dev/null +++ b/cli/contributing/update/runner/__init__.py @@ -0,0 +1,7 @@ +"""CLI entry point package for the GitHub-hosted runner label update.""" + +from pathlib import Path + +PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] + +__all__ = ["PROJECT_ROOT"] 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/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/docs/contributing/actions/debugging/ci.md b/docs/contributing/actions/debugging/ci.md index 9ee6075aff..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-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-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-latest`) | 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-latest, 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/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" diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD index 32b4c239a6..3d0f24b702 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.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 7130893af6..103fd7dd93 100644 --- a/packaging/debian/changelog +++ b/packaging/debian/changelog @@ -1,3 +1,61 @@ +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: + + - 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..198d8b1f29 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.1 Release: 1%{?dist} Summary: Meta package for Infinito.Nexus host dependencies @@ -58,6 +58,58 @@ 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: + +- - 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/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/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/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/pyproject.toml b/pyproject.toml index 91d16d1233..a00a1d1f0b 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.1" description = "Infinito.Nexus" readme = "README.md" requires-python = ">=3.11" diff --git a/roles/svc-bkp-local-2-device/README.md b/roles/svc-bkp-local-2-device/README.md index aca5be1eb3..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 @@ -97,7 +98,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/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 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 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' 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/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 }}" 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 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 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: 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/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 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)" 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 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 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 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 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/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/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/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)" 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/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() 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/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() 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/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/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/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") 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-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/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/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() 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/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/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/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/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/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, 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/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 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οΏΎοΏΏ]" )