From d67cd07d73460267c6b34b8343d32ba0eaec8a50 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 22 Oct 2026 11:31:03 -0300 Subject: [PATCH 1/3] fix(ci): pin GitHub Actions to immutable release commits Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .github/actions/build-and-scan/action.yml | 12 +++---- .../workflows/discover-nextcloud-versions.yml | 2 +- .github/workflows/docker-image.yml | 26 +++++++------- Makefile | 1 + docs/maintenance.md | 2 +- templates/workflows/ncdd-tests.yml | 2 +- tests/check-action-pins.py | 34 +++++++++++++++++++ 7 files changed, 57 insertions(+), 22 deletions(-) create mode 100644 tests/check-action-pins.py diff --git a/.github/actions/build-and-scan/action.yml b/.github/actions/build-and-scan/action.yml index 558ea74..f16457c 100644 --- a/.github/actions/build-and-scan/action.yml +++ b/.github/actions/build-and-scan/action.yml @@ -25,16 +25,16 @@ runs: file-install: false - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 with: platforms: arm64 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 - name: Build app image (linux/amd64) if: inputs.component == 'app' - uses: docker/build-push-action@v6 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 with: context: .docker/app platforms: linux/amd64 @@ -47,7 +47,7 @@ runs: - name: Build app image (linux/arm64) if: inputs.component == 'app' - uses: docker/build-push-action@v6 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 with: context: .docker/app platforms: linux/arm64 @@ -60,7 +60,7 @@ runs: - name: Build web image (linux/amd64) if: inputs.component == 'web' - uses: docker/build-push-action@v6 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 with: context: .docker/web platforms: linux/amd64 @@ -71,7 +71,7 @@ runs: - name: Build web image (linux/arm64) if: inputs.component == 'web' - uses: docker/build-push-action@v6 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 with: context: .docker/web platforms: linux/arm64 diff --git a/.github/workflows/discover-nextcloud-versions.yml b/.github/workflows/discover-nextcloud-versions.yml index ca6ff2d..4dd0521 100644 --- a/.github/workflows/discover-nextcloud-versions.yml +++ b/.github/workflows/discover-nextcloud-versions.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout reviewed branch - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 602e0c6..c802473 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -26,7 +26,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false @@ -58,7 +58,7 @@ jobs: matrix: ${{ steps.approved.outputs.matrix }} steps: - name: Checkout repository - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false @@ -79,7 +79,7 @@ jobs: security-events: write steps: - name: Checkout repository - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false @@ -92,14 +92,14 @@ jobs: - name: Upload amd64 security findings if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' - uses: github/codeql-action/upload-sarif@v3 + uses: github/codeql-action/upload-sarif@4bfa242aa49c5f7c6770b98670d354a7dfac05f8 # v3.39.1 with: sarif_file: trivy-results/app-linux-amd64.sarif category: trivy-app-linux-amd64-${{ matrix.tag }} - name: Upload arm64 security findings if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' - uses: github/codeql-action/upload-sarif@v3 + uses: github/codeql-action/upload-sarif@4bfa242aa49c5f7c6770b98670d354a7dfac05f8 # v3.39.1 with: sarif_file: trivy-results/app-linux-arm64.sarif category: trivy-app-linux-arm64-${{ matrix.tag }} @@ -113,7 +113,7 @@ jobs: - name: Log in to GHCR if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' - uses: docker/login-action@v4.6.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: ${{ github.actor }} @@ -133,7 +133,7 @@ jobs: security-events: write steps: - name: Checkout repository - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false @@ -144,14 +144,14 @@ jobs: - name: Upload amd64 security findings if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' - uses: github/codeql-action/upload-sarif@v3 + uses: github/codeql-action/upload-sarif@4bfa242aa49c5f7c6770b98670d354a7dfac05f8 # v3.39.1 with: sarif_file: trivy-results/web-linux-amd64.sarif category: trivy-web-linux-amd64 - name: Upload arm64 security findings if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' - uses: github/codeql-action/upload-sarif@v3 + uses: github/codeql-action/upload-sarif@4bfa242aa49c5f7c6770b98670d354a7dfac05f8 # v3.39.1 with: sarif_file: trivy-results/web-linux-arm64.sarif category: trivy-web-linux-arm64 @@ -165,7 +165,7 @@ jobs: - name: Log in to GHCR if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' - uses: docker/login-action@v4.6.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: ${{ github.actor }} @@ -185,15 +185,15 @@ jobs: packages: write steps: - name: Checkout repository - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 + uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 - name: Log in to GHCR - uses: docker/login-action@v4.6.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: ${{ github.actor }} diff --git a/Makefile b/Makefile index 8f73fa7..5698ac9 100644 --- a/Makefile +++ b/Makefile @@ -31,6 +31,7 @@ test-scan-images: bash tests/test-scan-images.sh test-ci-scripts: + python3 tests/check-action-pins.py bats tests/nextcloud-versions.bats tests/publish-images.bats test-development-image-scripts: diff --git a/docs/maintenance.md b/docs/maintenance.md index e05e51c..b472bec 100644 --- a/docs/maintenance.md +++ b/docs/maintenance.md @@ -91,7 +91,7 @@ Image metadata should use standard OCI labels wherever possible, including the s Keep tag naming consistent with the official Nextcloud model. Do not introduce `:dev`, `:dev-`, bare `:`, or `:nc-` as new public conventions. The legacy development tag `:35` is not a required compatibility alias; see the consumer analysis in #52. -Dependabot manages Docker and GitHub Actions dependencies. Renovate is limited to regex-managed values Dependabot cannot discover (currently the explicitly pinned Trivy binary version), so the two bots do not manage the same dependency. +Dependabot manages Docker and GitHub Actions references, including commit SHA pins and their release-version comments. Renovate handles only regex-managed values (Nextcloud image digests and the Trivy binary version), so the bots do not manage the same dependency. The CI rejects mutable external Actions references. The NCDD reusable-workflow example pins a source-branch snapshot; refresh its commit when updating that template. The remaining implementation constraints tracked under #47 are to improve metadata when upstream identifiers become available, keep the stable and master publishing contracts aligned, preserve runtime and security gates, and maintain deployment compatibility until replacement approaches are validated. diff --git a/templates/workflows/ncdd-tests.yml b/templates/workflows/ncdd-tests.yml index d9009f3..2d008f1 100644 --- a/templates/workflows/ncdd-tests.yml +++ b/templates/workflows/ncdd-tests.yml @@ -22,7 +22,7 @@ permissions: jobs: ncdd: - uses: LibreCodeCoop/nextcloud-docker/.github/workflows/test-nextcloud-app.yml@main + uses: LibreCodeCoop/nextcloud-docker/.github/workflows/test-nextcloud-app.yml@80dfe8632a448436ae8c5adb61e845d9e73dcadc # main with: suite: ${{ inputs.suite || 'phpunit' }} target: ${{ inputs.target || '' }} diff --git a/tests/check-action-pins.py b/tests/check-action-pins.py new file mode 100644 index 0000000..7d681e9 --- /dev/null +++ b/tests/check-action-pins.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +"""Reject mutable or unversioned remote uses references in GitHub Actions.""" +from pathlib import Path +import re +import sys + +uses = re.compile(r"^\s*(?:-\s*)?uses:\s*(\S+)(.*)$") +pinned = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)*@[a-f0-9]{40}$") +version = re.compile(r"^#\s+v?[0-9]+\.[0-9]+\.[0-9]+$") +checked = 0 +errors = [] + +for directory in (Path(".github"), Path("templates/workflows")): + for path in sorted((*directory.rglob("*.yml"), *directory.rglob("*.yaml"))): + for lineno, line in enumerate(path.read_text().splitlines(), 1): + match = uses.match(line) + if not match: + continue + ref, comment = match.groups() + if ref.startswith("./"): + continue + checked += 1 + # The distributed example pins a branch snapshot, not a tagged Action. + is_template = ref.startswith("LibreCodeCoop/nextcloud-docker/.github/workflows/") + valid_comment = bool(version.fullmatch(comment.strip())) + if is_template: + valid_comment = comment.strip() == "# main" + if not pinned.fullmatch(ref) or not valid_comment: + errors.append(f"{path}:{lineno}: {ref}{comment}") + +if errors: + print("Unpinned or unversioned GitHub Actions references:", *errors, sep="\n", file=sys.stderr) + sys.exit(1) +print(f"Validated {checked} SHA-pinned external Actions/workflow references") From 2d6c540205da2f8bc0c67d2b5d9751d0f3f56a93 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 22 Oct 2026 12:30:00 -0300 Subject: [PATCH 2/3] test(ci): enforce immutable action pins with Bats Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- Makefile | 3 +-- tests/check-action-pins.bats | 49 ++++++++++++++++++++++++++++++++++++ tests/check-action-pins.py | 34 ------------------------- 3 files changed, 50 insertions(+), 36 deletions(-) create mode 100644 tests/check-action-pins.bats delete mode 100644 tests/check-action-pins.py diff --git a/Makefile b/Makefile index 5698ac9..fdcd6de 100644 --- a/Makefile +++ b/Makefile @@ -31,8 +31,7 @@ test-scan-images: bash tests/test-scan-images.sh test-ci-scripts: - python3 tests/check-action-pins.py - bats tests/nextcloud-versions.bats tests/publish-images.bats + bats tests/check-action-pins.bats tests/nextcloud-versions.bats tests/publish-images.bats test-development-image-scripts: bats tests/nextcloud-development-image.bats diff --git a/tests/check-action-pins.bats b/tests/check-action-pins.bats new file mode 100644 index 0000000..04262bb --- /dev/null +++ b/tests/check-action-pins.bats @@ -0,0 +1,49 @@ +#!/usr/bin/env bats + +setup() { + cd "$BATS_TEST_DIRNAME/.." +} + +check_pins() { + local file line ref count=0 + local use_ref='^[[:space:]]*(-[[:space:]]*)?uses:[[:space:]]*([^[:space:]]+)' + local release='^[[:space:]]*(-[[:space:]]*)?uses:[[:space:]]*[a-zA-Z0-9_.-]+(/[a-zA-Z0-9_.-]+)+@[a-f0-9]{40}[[:space:]]+#[[:space:]]+v?[0-9]+\.[0-9]+\.[0-9]+[[:space:]]*$' + local template='^[[:space:]]*(-[[:space:]]*)?uses:[[:space:]]*LibreCodeCoop/nextcloud-docker/\.github/workflows/[a-zA-Z0-9_.-]+@[a-f0-9]{40}[[:space:]]+#[[:space:]]+main[[:space:]]*$' + + for file in "$@"; do + while IFS= read -r line || [[ -n $line ]]; do + [[ $line =~ $use_ref ]] || continue + ref="${BASH_REMATCH[2]}" + [[ $ref == ./* ]] && continue + ((count += 1)) + if [[ ! $line =~ $release && ! $line =~ $template ]]; then + echo "$file: $line" >&2 + return 1 + fi + done < "$file" + done + + (( count > 0 )) +} + +@test "all external Actions and reusable workflows are pinned to full SHAs" { + local file + local -a files=() + while IFS= read -r -d '' file; do + files+=("$file") + done < <(find .github templates/workflows -type f \( -name '*.yml' -o -name '*.yaml' \) -print0) + + check_pins "${files[@]}" +} + +@test "mutable tags and missing version comments are rejected" { + local fixture="$BATS_TEST_TMPDIR/unpinned.yml" + printf ' - uses: actions/checkout@v7\n' > "$fixture" + run check_pins "$fixture" + [ "$status" -ne 0 ] + [[ $output == *'actions/checkout@v7'* ]] + + printf ' - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1\n' > "$fixture" + run check_pins "$fixture" + [ "$status" -ne 0 ] +} diff --git a/tests/check-action-pins.py b/tests/check-action-pins.py deleted file mode 100644 index 7d681e9..0000000 --- a/tests/check-action-pins.py +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env python3 -"""Reject mutable or unversioned remote uses references in GitHub Actions.""" -from pathlib import Path -import re -import sys - -uses = re.compile(r"^\s*(?:-\s*)?uses:\s*(\S+)(.*)$") -pinned = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)*@[a-f0-9]{40}$") -version = re.compile(r"^#\s+v?[0-9]+\.[0-9]+\.[0-9]+$") -checked = 0 -errors = [] - -for directory in (Path(".github"), Path("templates/workflows")): - for path in sorted((*directory.rglob("*.yml"), *directory.rglob("*.yaml"))): - for lineno, line in enumerate(path.read_text().splitlines(), 1): - match = uses.match(line) - if not match: - continue - ref, comment = match.groups() - if ref.startswith("./"): - continue - checked += 1 - # The distributed example pins a branch snapshot, not a tagged Action. - is_template = ref.startswith("LibreCodeCoop/nextcloud-docker/.github/workflows/") - valid_comment = bool(version.fullmatch(comment.strip())) - if is_template: - valid_comment = comment.strip() == "# main" - if not pinned.fullmatch(ref) or not valid_comment: - errors.append(f"{path}:{lineno}: {ref}{comment}") - -if errors: - print("Unpinned or unversioned GitHub Actions references:", *errors, sep="\n", file=sys.stderr) - sys.exit(1) -print(f"Validated {checked} SHA-pinned external Actions/workflow references") From 9f0c6091c4efc638c400b8deb092c874521056c1 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 22 Oct 2026 12:35:28 -0300 Subject: [PATCH 3/3] style(ci): wrap Bats targets in Makefile Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- Makefile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index fdcd6de..875aab6 100644 --- a/Makefile +++ b/Makefile @@ -31,7 +31,9 @@ test-scan-images: bash tests/test-scan-images.sh test-ci-scripts: - bats tests/check-action-pins.bats tests/nextcloud-versions.bats tests/publish-images.bats + bats tests/check-action-pins.bats \ + tests/nextcloud-versions.bats \ + tests/publish-images.bats test-development-image-scripts: bats tests/nextcloud-development-image.bats