diff --git a/.github/workflows/ci-orchestrator.yml b/.github/workflows/ci-orchestrator.yml index 720572ce1b..6fd1b5987a 100644 --- a/.github/workflows/ci-orchestrator.yml +++ b/.github/workflows/ci-orchestrator.yml @@ -28,7 +28,7 @@ on: type: string default: "false" trusted_pr: - description: "True when the PR carries the maintainer trusted-pr label; skips the fork-prereq wait." + description: "True when the PR carries the maintainer '🛡️ Trusted' label; skips the fork-prereq wait." required: false type: boolean default: false @@ -55,7 +55,7 @@ jobs: shell: bash run: | set -euo pipefail - echo "Fork prerequisites are implicitly ready (non-fork PR or trusted-pr label)." + echo "Fork prerequisites are implicitly ready (non-fork PR or '🛡️ Trusted' label)." - name: Checkout trusted workflow repo (helpers) if: ${{ env.WAIT_REQUIRED == 'true' }} diff --git a/.github/workflows/cleanup-stale.yml b/.github/workflows/cleanup-stale.yml index 7de697df62..f70268a104 100644 --- a/.github/workflows/cleanup-stale.yml +++ b/.github/workflows/cleanup-stale.yml @@ -18,7 +18,7 @@ jobs: steps: - name: 🏷️ Mark stale issues and pull requests - uses: actions/stale@v9 + uses: actions/stale@v10 with: # Issues days-before-issue-stale: 90 @@ -50,19 +50,13 @@ jobs: steps: - name: 🌿 Delete branches inactive for 360 days - uses: crs-k/stale-branches@v8 + uses: crs-k/stale-branches@v9.0.1 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - days-before-stale: 360 - days-before-delete: 0 + days-before-stale: 350 + days-before-delete: 360 - protected-branches: | - main - master - develop - dev - release/** - hotfix/** + branches-filter-regex: '^(?!(?:main|master|develop|dev)$)(?!release/)(?!hotfix/).*' compare-branches: info dry-run: false diff --git a/.github/workflows/entry-pull-request-change.yml b/.github/workflows/entry-pull-request-change.yml index b0a267eaf3..8e033ef679 100644 --- a/.github/workflows/entry-pull-request-change.yml +++ b/.github/workflows/entry-pull-request-change.yml @@ -130,7 +130,7 @@ jobs: distros: "arch debian ubuntu fedora centos" whitelist: ${{ needs.detect-affected-roles.outputs.whitelist }} roles_only: ${{ needs.detect-affected-roles.outputs.roles_only }} - trusted_pr: ${{ contains(github.event.pull_request.labels.*.name, 'trusted-pr') }} + trusted_pr: ${{ contains(github.event.pull_request.labels.*.name, '🛡️ Trusted') }} prepare-fork-merge-ref: needs: @@ -141,7 +141,7 @@ jobs: outputs: checkout_ref: ${{ steps.out.outputs.checkout_ref }} image_tag: ${{ steps.out.outputs.image_tag }} - trusted: ${{ contains(github.event.pull_request.labels.*.name, 'trusted-pr') }} + trusted: ${{ contains(github.event.pull_request.labels.*.name, '🛡️ Trusted') }} steps: - name: Checkout uses: actions/checkout@v7 @@ -162,7 +162,7 @@ jobs: source_repository: ${{ needs.prepare-fork-merge-ref.outputs.trusted == 'true' && github.event.pull_request.head.repo.full_name || github.repository }} image_tag: ${{ needs.prepare-fork-merge-ref.outputs.image_tag }} concurrency_channel: pr-target-build-producer - secrets: inherit + allow_unsafe_checkout: true images-mirror-missing-fork: needs: prepare-fork-merge-ref @@ -174,4 +174,5 @@ jobs: source_path: _pr_source concurrency_channel: pr-target-mirror-producer concurrency_key: pr-${{ github.event.pull_request.number }} + allow_unsafe_checkout: true secrets: inherit diff --git a/.github/workflows/images-build-ci.yml b/.github/workflows/images-build-ci.yml index c90d0d7e24..617b5f4c01 100644 --- a/.github/workflows/images-build-ci.yml +++ b/.github/workflows/images-build-ci.yml @@ -27,6 +27,11 @@ on: required: false type: string default: "default" + allow_unsafe_checkout: + description: "Allow actions/checkout to fetch PR head/merge code under pull_request_target. Only set for maintainer-trusted fork PRs." + required: false + type: boolean + default: false permissions: contents: read @@ -77,20 +82,30 @@ jobs: ref: ${{ inputs.checkout_ref || github.ref }} path: _src persist-credentials: false + allow-unsafe-pr-checkout: ${{ inputs.allow_unsafe_checkout }} - name: Disk pressure (start) shell: bash run: bash scripts/github/runner/disk_pressure_report.sh START + - name: Pre-pull buildkit image (retry 7x, 20s) + shell: bash + env: + MAX_ATTEMPTS: "7" + RETRY_DELAY_SECONDS: "20" + run: bash scripts/github/runner/pull_with_retry.sh moby/buildkit:buildx-stable-1 + - name: Set up Docker Buildx uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + with: + driver-opts: image=moby/buildkit:buildx-stable-1 - name: Login to GHCR uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ghcr.io username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + password: ${{ github.token }} - name: Disk pressure (pre-build) shell: bash @@ -112,7 +127,7 @@ jobs: MATRIX_DISTRO: ${{ matrix.distro }} IMAGE_TAG: ${{ needs.prepare.outputs.image_tag }} USE_NIX_TOKEN: ${{ !(github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository) }} - NIX_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NIX_GITHUB_TOKEN: ${{ github.token }} MAX_ATTEMPTS: "7" RETRY_DELAY_SECONDS: "20" run: ./scripts/image/push.sh diff --git a/.github/workflows/images-mirror-missing.yml b/.github/workflows/images-mirror-missing.yml index e7080b63ce..8ba7217f51 100644 --- a/.github/workflows/images-mirror-missing.yml +++ b/.github/workflows/images-mirror-missing.yml @@ -32,6 +32,11 @@ on: required: false type: string default: "_pr_source" + allow_unsafe_checkout: + description: "Allow actions/checkout to fetch PR head/merge code under pull_request_target. Only set for maintainer-trusted fork PRs." + required: false + type: boolean + default: false images_per_hour: description: "Optional throttle: max images mirrored per hour" required: false @@ -103,6 +108,7 @@ jobs: path: ${{ inputs.source_path }} fetch-depth: 1 persist-credentials: false + allow-unsafe-pr-checkout: ${{ inputs.allow_unsafe_checkout }} - name: Setup Python uses: actions/setup-python@v6 diff --git a/.github/workflows/release-version.yml b/.github/workflows/release-version.yml index 1fac74ee03..315e558ec2 100644 --- a/.github/workflows/release-version.yml +++ b/.github/workflows/release-version.yml @@ -138,8 +138,17 @@ jobs: ref: ${{ github.sha }} persist-credentials: false + - name: Pre-pull buildkit image (retry 7x, 20s) + shell: bash + env: + MAX_ATTEMPTS: "7" + RETRY_DELAY_SECONDS: "20" + run: bash scripts/github/runner/pull_with_retry.sh moby/buildkit:buildx-stable-1 + - name: Set up Docker Buildx uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + with: + driver-opts: image=moby/buildkit:buildx-stable-1 - name: Login to GHCR uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 diff --git a/.github/workflows/update.yml b/.github/workflows/update.yml index 20f5ff2a67..386d9e5a53 100644 --- a/.github/workflows/update.yml +++ b/.github/workflows/update.yml @@ -20,6 +20,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v7 + with: + ref: main - name: Set up Python uses: actions/setup-python@v6 @@ -56,7 +58,7 @@ jobs: UPDATE_PR_TITLE: "update(images): update Docker image versions" UPDATE_PR_BODY: "Automated update of semver-based Docker image versions in `roles/*/meta/services.yml`." UPDATE_BASE_BRANCH: main - run: bash scripts/github/update/open_pr.sh roles + run: bash scripts/github/update/open_role_prs.sh roles update-repository-refs: name: 🌿 Update repository refs @@ -65,6 +67,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v7 + with: + ref: main - name: Set up Python uses: actions/setup-python@v6 @@ -101,7 +105,7 @@ jobs: UPDATE_PR_TITLE: "update(repos): bump git refs to latest semver tags" 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_pr.sh roles + run: bash scripts/github/update/open_role_prs.sh roles update-skills: name: 🧠 Update skills-lock.json @@ -110,6 +114,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v7 + with: + ref: main - name: Set up Node.js uses: actions/setup-node@v6 diff --git a/CHANGELOG.md b/CHANGELOG.md index ca89ad64e4..731845b2fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## [11.2.0] - 2026-06-28 + +* Per-role update PRs: the Docker-image-version and repository-ref updaters now open one pull request per affected role instead of a single combined PR, and they recompute the diff against the latest *main* at run time — the update jobs check out *main* rather than the triggering commit, so a queued or stale run no longer proposes already-merged changes or bundles unrelated roles. Each role gets its own branch (*update/---*) and a role-scoped duplicate check, while the *skills-lock.json* updater deliberately stays a single PR. See [the update workflow catalog](docs/contributing/tools/github/actions/workflows.md). + +* Fork-PR CI hardening (follow-up to the 11.1.0 trusted-fork flow): the privileged *pull_request_target* image producer and mirror now set *allow-unsafe-pr-checkout* so *actions/checkout* stops refusing the merged fork ref it had begun rejecting. Untrusted fork builds run without organization secrets — they authenticate to GHCR with the per-job *GITHUB_TOKEN* only and mirror Docker Hub anonymously — while the maintainer-trusted path keeps full credentials and builds the raw PR head. The trust switch moved to the *🛡️ Trusted* label. See [the fork-PR pipeline](docs/contributing/artefact/git/pipeline.md) and [the GHCR authentication reference](docs/contributing/tools/ghcr/authentication.md). + +* Buildx setup resilience: the CI image build pre-pulls and pins the *moby/buildkit* image with a retry loop before *docker/setup-buildx-action*, so a transient *registry-1.docker.io* timeout during the buildkit bootstrap no longer aborts the build — buildx falls back to the locally cached image once it is present. + +* Cleanup-workflow fixes (follow-up to the 11.1.0 cleanup automation): the *crs-k/stale-branches* action is pinned to *v9.0.1* (the previous *@v8* was unresolvable and failed the job) and its non-existent *protected-branches* input is replaced with the action's real *branches-filter-regex*; the GHCR CI-image cleanup script now reports what it scans, keeps and deletes with a per-package summary and a *DRY_RUN* mode, so a no-op run is no longer indistinguishable from a broken one. + +* Routine maintenance: YAML normalization of the *web-app-pihole* role's *meta/services.yml* (document start, list indentation) and an autoformat pass. + +* Image and dependency version jumps (net since 11.1.0): + * *web-app-matomo*: 5.3.2 to 5.11.2 + * *web-app-mattermost*: 11.8.1 to 11.8.2 + * *web-app-opentalk* (LiveKit): v1.13.1 to v1.13.2 + +**Contributors** + +* [Kevin Veen-Birkenbach](https://veen.world): per-role update PRs, fork-PR CI hardening, buildx retry, cleanup-workflow fixes and version maintenance + ## [11.1.0] - 2026-06-27 * Trusted fork PRs: a maintainer-applied *trusted-pr* label runs a fork PR as deliberately released code — the orchestrator skips the fork-prerequisite wait and the privileged *pull_request_target* build switches its source from the base merge ref to the raw PR head, while the built image keeps the merge-SHA tag so every downstream consumer is unchanged. The label is the only switch (applying a label is collaborator-only on GitHub, so fork authors cannot grant it), build-orchestration scripts always come from the base repository, and both unlabeled fork PRs and same-repository PRs are untouched. See [the fork-PR pipeline](docs/contributing/artefact/git/pipeline.md) and [the contributor fork workflow](docs/contributing/artefact/git/pull-request.md). diff --git a/docs/contributing/artefact/git/pipeline.md b/docs/contributing/artefact/git/pipeline.md index 9b04245ee8..8ad064fee5 100644 --- a/docs/contributing/artefact/git/pipeline.md +++ b/docs/contributing/artefact/git/pipeline.md @@ -32,7 +32,7 @@ The orchestrator runs the following stages in order: ### 1. Fork Prerequisites ⏳ -For fork PRs, CI waits for privileged images to be built by the `pull_request_target` event before proceeding. This ensures untrusted code never runs with elevated permissions. A maintainer-applied `trusted-pr` label opts the PR out of this wait — see [Trusted fork PRs](#trusted-fork-prs-). +For fork PRs, CI waits for privileged images built by the `pull_request_target` event before proceeding. For an untrusted fork that build runs from the safe base merge ref **without organization secrets** (only the per-job `GITHUB_TOKEN`); a maintainer-applied `🛡️ Trusted` label switches the build source to the PR head, unlocks the mirror credentials, and skips the wait — see [Trusted fork PRs](#trusted-fork-prs-). ### 2. Security 🛡️ @@ -62,7 +62,7 @@ The `code-quality-gate` requires linting, code tests, and security checks to all [images-mirror-missing.yml](../../../../.github/workflows/images-mirror-missing.yml) mirrors any missing upstream images in parallel with the DNS tests so later deploy jobs pull from GHCR instead of Docker Hub, MCR, or other external registries. This shields CI from upstream rate limits, geo-blocking, and transient registry outages. -Fork PRs cannot publish mirror packages directly, so their untrusted runs wait for the trusted mirror producer before continuing. See [mirror.md](../image/mirror.md) for the mirroring architecture and naming convention. +Fork PRs cannot publish mirror packages directly, so their runs wait for the `pull_request_target` mirror producer before continuing. The producer mirrors with the Docker Hub credentials only for trusted PRs ([🛡️ Trusted](#trusted-fork-prs-)); untrusted forks mirror anonymously (no organization secrets). See [mirror.md](../image/mirror.md) for the mirroring architecture and naming convention. GHCR publication uses the workflow `GITHUB_TOKEN`; optional Docker Hub secrets are used only to reduce source-side Docker Hub rate limits while mirroring. See [authentication.md](../../tools/ghcr/authentication.md). @@ -108,22 +108,23 @@ See [configuration.md](../../tools/github/actions/configuration.md) for all repo ## Fork PRs 🍴 -Fork PRs run under the `pull_request` event without write permissions. Privileged steps (image builds, package writes) run via `pull_request_target` after a maintainer review. The fork CI waits for those privileged images before proceeding. See [pull-request.md](pull-request.md) for the contributor-facing fork workflow. +Fork PRs run under the `pull_request` event without write permissions. Privileged steps (image builds, package writes) run via `pull_request_target`. The fork CI waits for those privileged images before proceeding. See [pull-request.md](pull-request.md) for the contributor-facing fork workflow. -By default the privileged `pull_request_target` build checks out the **base merge ref** (`refs/pull//merge`) from the base repository — the PR's changes merged onto base, with all workflow and build-orchestration scripts coming from base, never from the fork. +By default — for an **untrusted** fork PR — the privileged `pull_request_target` build checks out the **base merge ref** (`refs/pull//merge`) from the base repository: the PR's changes merged onto base, with all workflow and build-orchestration scripts coming from base, never from the fork. Because `actions/checkout` otherwise refuses to fetch a PR ref under `pull_request_target`, the source checkout sets `allow-unsafe-pr-checkout: true`. The untrusted build runs **without organization secrets** — `secrets: inherit` is not passed and it authenticates to GHCR with the per-job `GITHUB_TOKEN` only, so a malicious fork cannot read repository/organization secrets. (The `pull_request_target` token still carries `packages: write`, so the `🛡️ Trusted` label remains the real trust boundary for anything beyond image publication.) ### Trusted fork PRs 🔓 -When a maintainer applies the `trusted-pr` label, the fork PR is treated as deliberately released code: +When a maintainer applies the `🛡️ Trusted` label, the fork PR is treated as deliberately released code: - The orchestrator's `wait-fork-prereq-run` job **skips the wait** for the privileged fork images. - The privileged `pull_request_target` build switches its **build source** from the base merge ref to the **raw PR head** (`head.repo.full_name` @ `head.sha`). +- The trusted build additionally receives the Docker Hub mirror credentials (passed only when the label is present); untrusted builds mirror anonymously. - The built image **keeps the merge-SHA tag** (`ci-`) so the `pull_request` orchestrator finds it unchanged — no consumer workflow has to know about the label. Security gates: -- Only the `trusted-pr` label flips the build source; without it the safe base merge ref is used. Restrict who may set the label via repository label permissions / branch protection so only maintainers can grant it. +- The `🛡️ Trusted` label flips the build source to the PR head and unlocks the Docker Hub credentials; without it the safe base merge ref is built with no organization secrets. Restrict who may set the label via repository label permissions / branch protection so only maintainers can grant it. - Build-orchestration scripts (`scripts/image/push.sh` and the helper checkout) always come from the base repo at `github.sha`; only the Docker build **context** (`_src`) is the fork head. `persist-credentials: false` keeps the token out of the source checkout, and `USE_NIX_TOKEN` stays disabled for fork-sourced builds. - Fork `pull_request` runs always receive a read-only token (GitHub-enforced); the label never grants write tokens or secrets to the untrusted event. -Both events carry a `labeled` trigger, so applying the `trusted-pr` label starts fresh runs that already observe it: the `pull_request_target` build switches to the PR head and the `pull_request` orchestrator skips the wait — no extra push is needed. (Re-running an existing run does not pick up a new label, because GitHub replays the original event payload.) The trade-off is that any label change starts a fresh PR CI run. +Both events carry a `labeled` trigger, so applying the `🛡️ Trusted` label starts fresh runs that already observe it: the `pull_request_target` build switches to the PR head and the `pull_request` orchestrator skips the wait — no extra push is needed. (Re-running an existing run does not pick up a new label, because GitHub replays the original event payload.) The trade-off is that any label change starts a fresh PR CI run. diff --git a/docs/contributing/tools/ghcr/authentication.md b/docs/contributing/tools/ghcr/authentication.md index abf0c430c3..eb87158772 100644 --- a/docs/contributing/tools/ghcr/authentication.md +++ b/docs/contributing/tools/ghcr/authentication.md @@ -39,11 +39,11 @@ These are used only for pulling source images from Docker Hub and are not requir ## Fork Pull Requests 🍴 -Secrets are NOT available in `pull_request` workflows triggered by forks. This is a GitHub security restriction. The mirror workflow handles this transparently: +Secrets are NOT available in `pull_request` workflows triggered by forks. This is a GitHub security restriction. The mirror and image build run via `pull_request_target` instead: -- Fork PRs trigger a `pull_request_target` run with the base repository's trusted `GITHUB_TOKEN`. -- That trusted run mirrors any new images needed by the fork. -- The fork PR's CI then waits for those images to appear on GHCR before proceeding. +- A fork PR triggers a `pull_request_target` run that builds and mirrors any new images needed by the fork, authenticating to GHCR with the per-job `GITHUB_TOKEN`. +- The `pull_request` orchestrator then waits for and consumes those images. +- Untrusted fork builds run **without** organization secrets (`secrets: inherit` is not passed); Docker Hub mirror credentials are passed only for maintainer-trusted PRs (the `🛡️ Trusted` label). See [pipeline.md](../../artefact/git/pipeline.md#fork-prs-). ## Troubleshooting 🔧 diff --git a/docs/contributing/tools/github/actions/workflows.md b/docs/contributing/tools/github/actions/workflows.md index 93b480eaec..ee4e5dda10 100644 --- a/docs/contributing/tools/github/actions/workflows.md +++ b/docs/contributing/tools/github/actions/workflows.md @@ -25,7 +25,7 @@ Trigger column legend: **auto** = fires automatically (push, pull_request, sched | Workflow | Description | Trigger | Inputs | |---|---|---|---| -| [entry-pull-request-change.yml](../../../../../.github/workflows/entry-pull-request-change.yml): `⚡ CI: Pull Request` | Detects PR scope from changed files + branch prefix, then conditionally calls the orchestrator. A maintainer `trusted-pr` label builds the privileged images from the PR head — see [pipeline.md](../../../artefact/git/pipeline.md#trusted-fork-prs-). | auto (`pull_request` & `pull_request_target`: opened, synchronize, reopened, ready_for_review, labeled) | none | +| [entry-pull-request-change.yml](../../../../../.github/workflows/entry-pull-request-change.yml): `⚡ CI: Pull Request` | Detects PR scope from changed files + branch prefix, then conditionally calls the orchestrator. A maintainer `🛡️ Trusted` label builds the privileged images from the PR head — see [pipeline.md](../../../artefact/git/pipeline.md#trusted-fork-prs-). | auto (`pull_request` & `pull_request_target`: opened, synchronize, reopened, ready_for_review, labeled) | none | | [entry-push-latest.yml](../../../../../.github/workflows/entry-push-latest.yml): `⚡ CI: Push` | Syncs `main` from the configured source repository, then runs the orchestrator on pushes to `main`, `feature/**`, `hotfix/**`, `fix/**`, `chore/**`, `alert-autofix-*`; on tagged `main` it also calls `release-version.yml`. Respects the `CI_CANCEL_IN_PROGRESS`, `CI_SYNC_MAIN_SOURCE_REPOSITORY`, and `CI_RUN_ON_MAIN` repository variables, see [configuration.md](configuration.md). | auto (`push`) | none | | [entry-manual.yml](../../../../../.github/workflows/entry-manual.yml): `⚡ CI: Manual` | Manual dispatch of the full orchestrator for a chosen distro set and app whitelist. The `whitelist` input accepts the `__ALL__` sentinel for forced full deploy; see the "Diff-driven app selection" subsection in [pipeline.md](../../../artefact/git/pipeline.md). | manual | `distros` (default `debian`), `whitelist` (optional; `__ALL__` forces full deploy, empty triggers diff derivation, any other value is verbatim) | | [entry-pull-request-cancel.yml](../../../../../.github/workflows/entry-pull-request-cancel.yml): `🚫 Cancel: PR Runs on Close` | Cancels active workflow runs for a PR when it is closed or converted to draft. | auto (`pull_request_target`: closed, converted_to_draft) | none | diff --git a/package-lock.json b/package-lock.json index d20850efe3..8034569054 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "devDependencies": { "@eslint/js": "^10.0.1", "@playwright/test": "^1.61.1", - "eslint": "^10.5.0", + "eslint": "^10.6.0", "eslint-plugin-playwright": "^2.10.4", "globals": "^17.7.0" } @@ -363,9 +363,9 @@ } }, "node_modules/eslint": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.5.0.tgz", - "integrity": "sha512-1y+7C+vi12bUK1IpZeaV3gsH9fHLBmPvYmPx42pvT/E9yG0IC8g3PUZZgp0+JLJl7ZDK0flc2gc+Aw9dpCvIsQ==", + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.6.0.tgz", + "integrity": "sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==", "dev": true, "license": "MIT", "workspaces": [ diff --git a/package.json b/package.json index 8c2a7b6b59..d963e8e7d6 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "devDependencies": { "@eslint/js": "^10.0.1", "@playwright/test": "^1.61.1", - "eslint": "^10.5.0", + "eslint": "^10.6.0", "eslint-plugin-playwright": "^2.10.4", "globals": "^17.7.0" } diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD index 85bea14468..a730875b7c 100644 --- a/packaging/arch/PKGBUILD +++ b/packaging/arch/PKGBUILD @@ -1,7 +1,7 @@ # Maintainer: Kevin Veen-Birkenbach pkgname=infinito-nexus -pkgver=11.1.0 +pkgver=11.2.0 pkgrel=1 pkgdesc="Meta package for Infinito.Nexus host dependencies (dev + CI)." arch=('any') diff --git a/packaging/debian/changelog b/packaging/debian/changelog index f1bb9858e6..3e9078d710 100644 --- a/packaging/debian/changelog +++ b/packaging/debian/changelog @@ -1,3 +1,26 @@ +infinito-nexus (11.2.0-1) unstable; urgency=medium + + * Per-role update PRs: the Docker-image-version and repository-ref updaters now open one pull request per affected role instead of a single combined PR, and they recompute the diff against the latest *main* at run time — the update jobs check out *main* rather than the triggering commit, so a queued or stale run no longer proposes already-merged changes or bundles unrelated roles. Each role gets its own branch (*update/---*) and a role-scoped duplicate check, while the *skills-lock.json* updater deliberately stays a single PR. See [the update workflow catalog](docs/contributing/tools/github/actions/workflows.md). + + * Fork-PR CI hardening (follow-up to the 11.1.0 trusted-fork flow): the privileged *pull_request_target* image producer and mirror now set *allow-unsafe-pr-checkout* so *actions/checkout* stops refusing the merged fork ref it had begun rejecting. Untrusted fork builds run without organization secrets — they authenticate to GHCR with the per-job *GITHUB_TOKEN* only and mirror Docker Hub anonymously — while the maintainer-trusted path keeps full credentials and builds the raw PR head. The trust switch moved to the *🛡️ Trusted* label. See [the fork-PR pipeline](docs/contributing/artefact/git/pipeline.md) and [the GHCR authentication reference](docs/contributing/tools/ghcr/authentication.md). + + * Buildx setup resilience: the CI image build pre-pulls and pins the *moby/buildkit* image with a retry loop before *docker/setup-buildx-action*, so a transient *registry-1.docker.io* timeout during the buildkit bootstrap no longer aborts the build — buildx falls back to the locally cached image once it is present. + + * Cleanup-workflow fixes (follow-up to the 11.1.0 cleanup automation): the *crs-k/stale-branches* action is pinned to *v9.0.1* (the previous *@v8* was unresolvable and failed the job) and its non-existent *protected-branches* input is replaced with the action's real *branches-filter-regex*; the GHCR CI-image cleanup script now reports what it scans, keeps and deletes with a per-package summary and a *DRY_RUN* mode, so a no-op run is no longer indistinguishable from a broken one. + + * Routine maintenance: YAML normalization of the *web-app-pihole* role's *meta/services.yml* (document start, list indentation) and an autoformat pass. + + * Image and dependency version jumps (net since 11.1.0): + * *web-app-matomo*: 5.3.2 to 5.11.2 + * *web-app-mattermost*: 11.8.1 to 11.8.2 + * *web-app-opentalk* (LiveKit): v1.13.1 to v1.13.2 + + **Contributors** + + * [Kevin Veen-Birkenbach](https://veen.world): per-role update PRs, fork-PR CI hardening, buildx retry, cleanup-workflow fixes and version maintenance + + -- Kevin Veen-Birkenbach Sun, 28 Jun 2026 20:38:36 +0200 + infinito-nexus (11.1.0-1) unstable; urgency=medium * Trusted fork PRs: a maintainer-applied *trusted-pr* label runs a fork PR as deliberately released code — the orchestrator skips the fork-prerequisite wait and the privileged *pull_request_target* build switches its source from the base merge ref to the raw PR head, while the built image keeps the merge-SHA tag so every downstream consumer is unchanged. The label is the only switch (applying a label is collaborator-only on GitHub, so fork authors cannot grant it), build-orchestration scripts always come from the base repository, and both unlabeled fork PRs and same-repository PRs are untouched. See [the fork-PR pipeline](docs/contributing/artefact/git/pipeline.md) and [the contributor fork workflow](docs/contributing/artefact/git/pull-request.md). diff --git a/packaging/fedora/infinito-nexus.spec b/packaging/fedora/infinito-nexus.spec index a88a50618b..a5444c0b97 100644 --- a/packaging/fedora/infinito-nexus.spec +++ b/packaging/fedora/infinito-nexus.spec @@ -1,5 +1,5 @@ Name: infinito-nexus -Version: 11.1.0 +Version: 11.2.0 Release: 1%{?dist} Summary: Meta package for Infinito.Nexus host dependencies @@ -58,6 +58,26 @@ install -d %{buildroot}%{_docdir}/%{name} %doc %{_docdir}/%{name}/DEPENDENCIES %changelog +* Sun Jun 28 2026 Kevin Veen-Birkenbach - 11.2.0-1 +- * Per-role update PRs: the Docker-image-version and repository-ref updaters now open one pull request per affected role instead of a single combined PR, and they recompute the diff against the latest *main* at run time — the update jobs check out *main* rather than the triggering commit, so a queued or stale run no longer proposes already-merged changes or bundles unrelated roles. Each role gets its own branch (*update/---*) and a role-scoped duplicate check, while the *skills-lock.json* updater deliberately stays a single PR. See [the update workflow catalog](docs/contributing/tools/github/actions/workflows.md). + +- * Fork-PR CI hardening (follow-up to the 11.1.0 trusted-fork flow): the privileged *pull_request_target* image producer and mirror now set *allow-unsafe-pr-checkout* so *actions/checkout* stops refusing the merged fork ref it had begun rejecting. Untrusted fork builds run without organization secrets — they authenticate to GHCR with the per-job *GITHUB_TOKEN* only and mirror Docker Hub anonymously — while the maintainer-trusted path keeps full credentials and builds the raw PR head. The trust switch moved to the *🛡️ Trusted* label. See [the fork-PR pipeline](docs/contributing/artefact/git/pipeline.md) and [the GHCR authentication reference](docs/contributing/tools/ghcr/authentication.md). + +- * Buildx setup resilience: the CI image build pre-pulls and pins the *moby/buildkit* image with a retry loop before *docker/setup-buildx-action*, so a transient *registry-1.docker.io* timeout during the buildkit bootstrap no longer aborts the build — buildx falls back to the locally cached image once it is present. + +- * Cleanup-workflow fixes (follow-up to the 11.1.0 cleanup automation): the *crs-k/stale-branches* action is pinned to *v9.0.1* (the previous *@v8* was unresolvable and failed the job) and its non-existent *protected-branches* input is replaced with the action's real *branches-filter-regex*; the GHCR CI-image cleanup script now reports what it scans, keeps and deletes with a per-package summary and a *DRY_RUN* mode, so a no-op run is no longer indistinguishable from a broken one. + +- * Routine maintenance: YAML normalization of the *web-app-pihole* role's *meta/services.yml* (document start, list indentation) and an autoformat pass. + +- * Image and dependency version jumps (net since 11.1.0): +- * *web-app-matomo*: 5.3.2 to 5.11.2 +- * *web-app-mattermost*: 11.8.1 to 11.8.2 +- * *web-app-opentalk* (LiveKit): v1.13.1 to v1.13.2 + +- **Contributors** + +- * [Kevin Veen-Birkenbach](https://veen.world): per-role update PRs, fork-PR CI hardening, buildx retry, cleanup-workflow fixes and version maintenance + * Sat Jun 27 2026 Kevin Veen-Birkenbach - 11.1.0-1 - * Trusted fork PRs: a maintainer-applied *trusted-pr* label runs a fork PR as deliberately released code — the orchestrator skips the fork-prerequisite wait and the privileged *pull_request_target* build switches its source from the base merge ref to the raw PR head, while the built image keeps the merge-SHA tag so every downstream consumer is unchanged. The label is the only switch (applying a label is collaborator-only on GitHub, so fork authors cannot grant it), build-orchestration scripts always come from the base repository, and both unlabeled fork PRs and same-repository PRs are untouched. See [the fork-PR pipeline](docs/contributing/artefact/git/pipeline.md) and [the contributor fork workflow](docs/contributing/artefact/git/pull-request.md). diff --git a/pyproject.toml b/pyproject.toml index db350c33e8..6abd3b3a0f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "infinito-nexus" -version = "11.1.0" +version = "11.2.0" description = "Infinito.Nexus" readme = "README.md" requires-python = ">=3.11" diff --git a/roles/pkgmgr/meta/services.yml b/roles/pkgmgr/meta/services.yml index 2f8c698b72..8250fcc1d9 100644 --- a/roles/pkgmgr/meta/services.yml +++ b/roles/pkgmgr/meta/services.yml @@ -3,4 +3,4 @@ pkgmgr: lifecycle: beta repository: https://github.com/kevinveenbirkenbach/package-manager.git - ref: v1.15.2 + ref: v1.16.0 diff --git a/roles/web-app-matomo/meta/services.yml b/roles/web-app-matomo/meta/services.yml index d3a06ab4b5..32c7b074ac 100644 --- a/roles/web-app-matomo/meta/services.yml +++ b/roles/web-app-matomo/meta/services.yml @@ -20,7 +20,7 @@ matomo: enabled: true shared: true image: matomo - version: "5.3.2" + version: "5.11.2" name: matomo min_storage: 5GB backup: diff --git a/roles/web-app-opentalk/meta/services.yml b/roles/web-app-opentalk/meta/services.yml index bee0b3892a..36b9bcc285 100644 --- a/roles/web-app-opentalk/meta/services.yml +++ b/roles/web-app-opentalk/meta/services.yml @@ -82,7 +82,7 @@ controller: livekit: name: opentalk-livekit image: livekit/livekit-server - version: v1.13.1 + version: v1.13.2 min_storage: 1GB cpus: "2.0" mem_reservation: 512m diff --git a/roles/web-app-pihole/meta/server.yml b/roles/web-app-pihole/meta/server.yml index bb1b156256..6cb4ca4002 100644 --- a/roles/web-app-pihole/meta/server.yml +++ b/roles/web-app-pihole/meta/server.yml @@ -1,6 +1,7 @@ +--- domains: canonical: - - pihole.{{ DOMAIN_PRIMARY }} + - pihole.{{ DOMAIN_PRIMARY }} aliases: [] status_codes: default: diff --git a/roles/web-app-pihole/meta/services.yml b/roles/web-app-pihole/meta/services.yml index 8c6a7f035f..0c93572e39 100644 --- a/roles/web-app-pihole/meta/services.yml +++ b/roles/web-app-pihole/meta/services.yml @@ -1,3 +1,4 @@ +--- # nocheck: playwright-service-flag logout: bond: 1 @@ -67,5 +68,5 @@ pihole: http: 8078 sso: 16508 run_after: - - web-app-keycloak + - web-app-keycloak lifecycle: beta diff --git a/scripts/github/cleanup/ghcr_ci_images.sh b/scripts/github/cleanup/ghcr_ci_images.sh index a1a35b45d0..16bd2f6d02 100755 --- a/scripts/github/cleanup/ghcr_ci_images.sh +++ b/scripts/github/cleanup/ghcr_ci_images.sh @@ -14,6 +14,7 @@ source "${REPO_ROOT}/scripts/meta/env/load.sh" package_prefix="${PACKAGE_PREFIX:-core}" max_age_days="${MAX_AGE_DAYS:-30}" tag_prefix="${TAG_PREFIX:-ci-}" +dry_run="${DRY_RUN:-false}" if [[ ! "${max_age_days}" =~ ^[0-9]+$ ]]; then echo "ERROR: MAX_AGE_DAYS must be a non-negative integer." >&2 @@ -25,15 +26,12 @@ if [[ -z "${tag_prefix}" ]]; then exit 1 fi -if ! command -v gh >/dev/null 2>&1; then - echo "ERROR: gh CLI not found." >&2 - exit 1 -fi - -if ! command -v jq >/dev/null 2>&1; then - echo "ERROR: jq not found." >&2 - exit 1 -fi +for tool in gh jq; do + if ! command -v "${tool}" >/dev/null 2>&1; then + echo "ERROR: ${tool} not found." >&2 + exit 1 + fi +done encode_package_name() { local package="${1}" @@ -53,24 +51,43 @@ resolve_packages() { done } -is_deletable_ci_version() { - local version="${1}" - local tags +classify_version() { + local version="${1}" cutoff_ts="${2}" + local tags updated_ts tags="$(jq -r '.tags[]?' <<<"${version}")" + [[ -n "${tags}" ]] || { + echo "keep:untagged" + return 0 + } + grep -Eq "^(latest|v[0-9].*)$" <<<"${tags}" && { + echo "keep:release-tag" + return 0 + } + grep -q "^${tag_prefix}" <<<"${tags}" || { + echo "keep:no-ci-tag" + return 0 + } - [[ -n "${tags}" ]] || return 1 + updated_ts="$(date -u -d "$(jq -r '.updated_at' <<<"${version}")" +%s)" + [[ "${updated_ts}" -lt "${cutoff_ts}" ]] || { + echo "keep:too-new" + return 0 + } - # Keep release tags even when the version also has ci-* tags. - grep -q "^${tag_prefix}" <<<"${tags}" || return 1 - grep -Eq "^(latest|v.*)$" <<<"${tags}" && return 1 - grep -vq "^${tag_prefix}" <<<"${tags}" && return 1 - - return 0 + echo "delete" } cutoff="$(date -u -d "${max_age_days} days ago" +%s)" +echo "GHCR CI image cleanup" +echo " org=${ORG} tag-prefix=${tag_prefix} max-age=${max_age_days}d dry-run=${dry_run}" +echo " cutoff=$(date -u -d "@${cutoff}" +%Y-%m-%dT%H:%M:%SZ) (delete ${tag_prefix}* versions without a semver/latest tag, older than this)" + +total_scanned=0 +total_deleted=0 +declare -A kept_reasons=() + while IFS= read -r package; do package="${package#"${package%%[![:space:]]*}"}" package="${package%"${package##*[![:space:]]}"}" @@ -78,24 +95,51 @@ while IFS= read -r package; do encoded_package="$(encode_package_name "${package}")" - gh api --paginate \ - "/orgs/${ORG}/packages/container/${encoded_package}/versions" \ - --jq '.[] | {id: .id, updated_at: .updated_at, tags: .metadata.container.tags}' | - jq -c '.' | - while IFS= read -r version; do - id="$(jq -r '.id' <<<"${version}")" - updated_at="$(jq -r '.updated_at' <<<"${version}")" - updated_ts="$(date -u -d "${updated_at}" +%s)" + pkg_scanned=0 + pkg_deleted=0 - if [[ "${updated_ts}" -ge "${cutoff}" ]]; then - continue - fi + echo "== ${package} ==" - is_deletable_ci_version "${version}" || continue + while IFS= read -r version; do + [[ -n "${version}" ]] || continue + pkg_scanned=$((pkg_scanned + 1)) - echo "Deleting ${package} version ${id} updated at ${updated_at}" + decision="$(classify_version "${version}" "${cutoff}")" + + if [[ "${decision}" != "delete" ]]; then + kept_reasons["${decision}"]=$((${kept_reasons["${decision}"]:-0} + 1)) + continue + fi + + id="$(jq -r '.id' <<<"${version}")" + updated_at="$(jq -r '.updated_at' <<<"${version}")" + tags_csv="$(jq -r '[.tags[]?] | join(",")' <<<"${version}")" + + if [[ "${dry_run}" == "true" ]]; then + echo " would delete id=${id} tags=${tags_csv} updated=${updated_at}" + else + echo " deleting id=${id} tags=${tags_csv} updated=${updated_at}" gh api \ --method DELETE \ "/orgs/${ORG}/packages/container/${encoded_package}/versions/${id}" - done + fi + pkg_deleted=$((pkg_deleted + 1)) + done < <(gh api --paginate \ + "/orgs/${ORG}/packages/container/${encoded_package}/versions" \ + --jq '.[] | {id: .id, updated_at: .updated_at, tags: .metadata.container.tags}' | + jq -c '.') + + echo " -> scanned ${pkg_scanned}, deleted ${pkg_deleted}" + total_scanned=$((total_scanned + pkg_scanned)) + total_deleted=$((total_deleted + pkg_deleted)) done < <(resolve_packages) + +echo "== Summary ==" +echo " scanned ${total_scanned} versions, deleted ${total_deleted}" +for reason in "${!kept_reasons[@]}"; do + echo " kept ${kept_reasons[${reason}]} (${reason})" +done + +if [[ "${total_deleted}" -eq 0 ]]; then + echo " no versions matched the deletion criteria" +fi diff --git a/scripts/github/runner/pull_with_retry.sh b/scripts/github/runner/pull_with_retry.sh new file mode 100755 index 0000000000..68191d0a13 --- /dev/null +++ b/scripts/github/runner/pull_with_retry.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -euo pipefail + +image="${1:?Usage: pull_with_retry.sh }" +max_attempts="${MAX_ATTEMPTS:-7}" +retry_delay_seconds="${RETRY_DELAY_SECONDS:-20}" +attempt=1 + +while true; do + echo "=== docker pull ${image} attempt ${attempt}/${max_attempts} ===" + if docker pull "${image}"; then + echo "Pulled ${image} on attempt ${attempt}/${max_attempts}." + break + fi + + if [[ "${attempt}" -ge "${max_attempts}" ]]; then + echo "Failed to pull ${image} after ${max_attempts} attempts." >&2 + exit 1 + fi + + echo "Attempt ${attempt} failed. Retrying in ${retry_delay_seconds}s..." + sleep "${retry_delay_seconds}" + attempt=$((attempt + 1)) +done diff --git a/scripts/github/update/open_pr.sh b/scripts/github/update/open_pr.sh index b3d56aaa16..f093369356 100755 --- a/scripts/github/update/open_pr.sh +++ b/scripts/github/update/open_pr.sh @@ -9,6 +9,7 @@ set -euo pipefail UPDATE_BASE_BRANCH="${UPDATE_BASE_BRANCH:-master}" UPDATE_BRANCH_SUFFIX="${UPDATE_BRANCH_SUFFIX:-$(date +%Y%m%d)}" +UPDATE_DEDUPE_PREFIX="${UPDATE_DEDUPE_PREFIX:-${UPDATE_BRANCH_PREFIX}-}" if ! command -v gh >/dev/null 2>&1; then echo "ERROR: gh CLI not found." >&2 @@ -70,7 +71,7 @@ mapfile -t OPEN_PRS < <( --base "${UPDATE_BASE_BRANCH}" \ --limit 100 \ --json number,headRefName \ - --jq ".[] | select(.headRefName | startswith(\"${UPDATE_BRANCH_PREFIX}-\")) | \"\(.number)\t\(.headRefName)\"" + --jq ".[] | select(.headRefName | startswith(\"${UPDATE_DEDUPE_PREFIX}\")) | \"\(.number)\t\(.headRefName)\"" ) DUPLICATE_PR="" diff --git a/scripts/github/update/open_role_prs.sh b/scripts/github/update/open_role_prs.sh new file mode 100755 index 0000000000..576424d38a --- /dev/null +++ b/scripts/github/update/open_role_prs.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Opens one update PR per top-level entry that changed under the given root +# (e.g. one PR per role under roles/), instead of a single combined PR. +# The actual dedup/push/PR logic is reused from open_pr.sh; this wrapper only +# isolates one entry's changes per call. + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" + +ROOT="${1:?Usage: open_role_prs.sh (e.g. roles)}" + +: "${GH_TOKEN:?Missing GH_TOKEN}" +: "${UPDATE_BRANCH_PREFIX:?Missing UPDATE_BRANCH_PREFIX}" +: "${UPDATE_COMMIT_MESSAGE:?Missing UPDATE_COMMIT_MESSAGE}" +: "${UPDATE_PR_TITLE:?Missing UPDATE_PR_TITLE}" +: "${UPDATE_PR_BODY:?Missing UPDATE_PR_BODY}" + +date_suffix="$(date +%Y%m%d)" + +# A throwaway identity is enough for the scratch commit below; open_pr.sh sets +# the real bot identity for each published commit. +git config user.name >/dev/null 2>&1 || git config user.name "github-actions[bot]" +git config user.email >/dev/null 2>&1 || git config user.email "github-actions[bot]@users.noreply.github.com" + +base="$(git rev-parse HEAD)" + +git add -A -- "${ROOT}" +if git diff --cached --quiet; then + echo "No ${ROOT} changes to split into per-entry PRs." + exit 0 +fi +git commit --quiet -m "scratch: ${ROOT} updates" +scratch="$(git rev-parse HEAD)" +git reset --quiet --hard "${base}" + +mapfile -t entries < <( + git diff --name-only "${base}" "${scratch}" -- "${ROOT}/" | + sed -E "s#^${ROOT}/([^/]+)/.*#\1#;t;d" | + sort -u +) + +echo "Changed ${ROOT} entries: ${entries[*]:-(none)}" + +for entry in "${entries[@]}"; do + [[ -z "${entry}" ]] && continue + echo "=== Opening PR for ${ROOT}/${entry} ===" + + git checkout --quiet --force --detach "${base}" + git checkout --quiet "${scratch}" -- "${ROOT}/${entry}/" + + UPDATE_BRANCH_SUFFIX="${entry}-${date_suffix}" \ + UPDATE_DEDUPE_PREFIX="${UPDATE_BRANCH_PREFIX}-${entry}-" \ + UPDATE_COMMIT_MESSAGE="${UPDATE_COMMIT_MESSAGE} (${entry})" \ + UPDATE_PR_TITLE="${UPDATE_PR_TITLE} (${entry})" \ + UPDATE_PR_BODY="${UPDATE_PR_BODY}"$'\n\n'"Scope: \`${ROOT}/${entry}\`." \ + bash "${SCRIPT_DIR}/open_pr.sh" "${ROOT}/${entry}" +done + +git checkout --quiet --force --detach "${base}" || true