Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .docker/web/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM nginx:alpine
FROM nginx:alpine@sha256:892bb2152464f0c5d8972593a5f5afaf8054552580b699d95f8ff4af08c2d33d

RUN apk upgrade --no-cache

Expand Down
10 changes: 7 additions & 3 deletions .github/actions/build-and-scan/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ inputs:
description: Either app or web
required: true
nextcloud_version:
description: Official Nextcloud release tag for app builds (e.g. 35.0.2-fpm)
description: Official Nextcloud release tag for cache keys (e.g. 35.0.2-fpm)
required: false
default: ''
nextcloud_base_image:
description: Immutable approved Nextcloud image (tag and digest)
required: false
default: ''
runs:
Expand Down Expand Up @@ -36,7 +40,7 @@ runs:
platforms: linux/amd64
load: true
build-args: |
NEXTCLOUD_BASE_IMAGE=nextcloud:${{ inputs.nextcloud_version }}
NEXTCLOUD_BASE_IMAGE=${{ inputs.nextcloud_base_image }}
tags: scan/app:amd64
cache-from: type=gha,scope=app-${{ inputs.nextcloud_version }}-amd64
cache-to: type=gha,mode=max,scope=app-${{ inputs.nextcloud_version }}-amd64
Expand All @@ -49,7 +53,7 @@ runs:
platforms: linux/arm64
load: true
build-args: |
NEXTCLOUD_BASE_IMAGE=nextcloud:${{ inputs.nextcloud_version }}
NEXTCLOUD_BASE_IMAGE=${{ inputs.nextcloud_base_image }}
tags: scan/app:arm64
cache-from: type=gha,scope=app-${{ inputs.nextcloud_version }}-arm64
cache-to: type=gha,mode=max,scope=app-${{ inputs.nextcloud_version }}-arm64
Expand Down
4 changes: 1 addition & 3 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@ updates:
- package-ecosystem: docker
directory: /.docker/web
schedule:
interval: weekly
cooldown:
default-days: 7
interval: daily

- package-ecosystem: github-actions
directory: /
Expand Down
6 changes: 3 additions & 3 deletions .github/nextcloud-supported.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@
"versions": [
{
"major": 33,
"release": "33.0.10",
"image": "nextcloud:33.0.10-fpm@sha256:9b564a1f88b8ab80319fcdcb3982de3063d8c66e136121f2ed03274126d6858e",
"eol": "2027-02"
},
{
"major": 34,
"release": "34.0.5",
"image": "nextcloud:34.0.5-fpm@sha256:debf3a90c0efc3a31f3c48e3c211ee3148983361c3d10a6a6bb8c13d7610463d",
"eol": "2027-06"
},
{
"major": 35,
"release": "35.0.2",
"image": "nextcloud:35.0.2-fpm@sha256:2556e045d57fff50d2b50ddd160d2d0730e6d5b6f71c33b903d491baa8444f03",
"eol": "2027-09"
}
]
Expand Down
3 changes: 1 addition & 2 deletions .github/workflows/docker-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ on:
branches:
- main
workflow_dispatch:
schedule:
- cron: '13 4 * * 2'

permissions:
contents: read
Expand Down Expand Up @@ -90,6 +88,7 @@ jobs:
with:
component: app
nextcloud_version: ${{ matrix.release }}-fpm
nextcloud_base_image: ${{ matrix.image }}

- name: Upload amd64 security findings
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
Expand Down
19 changes: 18 additions & 1 deletion docs/maintenance.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ Publication produces:

Expired EOL months in the approved JSON stop the release workflow instead of silently rebuilding an unsupported version. Existing EOL tags are **not deleted automatically**; they stop being rebuilt after the reviewed inventory changes.

Stable publication runs on pushes to `main`, a weekly rebuild schedule, or manual dispatch on `main`. Pull requests and non-`main` dispatches validate without publishing.
Stable publication runs on pushes to `main` or manual dispatch on `main`. Pull requests and non-`main` dispatches validate without publishing.

## Development image `master-fpm`

Expand Down Expand Up @@ -94,3 +94,20 @@ Keep tag naming consistent with the official Nextcloud model. Do not introduce `
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.

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.


## Automatic Docker base updates

The supported Nextcloud JSON is the source of truth for the immutable Docker
base references. Renovate updates each supported image tag and digest in a
reviewable pull request. New major versions and EOL transitions remain managed
by the existing maintenance-wiki discovery workflow, which preserves the
Renovate-managed references for already-supported versions.

Dependabot manages the independent Nginx Dockerfile (also digest-pinned) and
GitHub Actions. The stable Docker image workflow runs on pull requests and
reviewed merges to main. It has no scheduled rebuild.

The separate maintenance-wiki discovery cron is only for changes to the
Nextcloud support inventory; it does not build or publish images. Docker base
updates alone do not detect changes in unpinned APT/APK packages.
26 changes: 26 additions & 0 deletions renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,32 @@
"# renovate: datasource=(?<datasource>\\S+) depName=(?<depName>\\S+)\\s+version: (?<currentValue>v?\\d+\\.\\d+\\.\\d+)"
],
"versioningTemplate": "semver-coerced"
},
{
"customType": "regex",
"managerFilePatterns": [
"/^\\.github\\/nextcloud-supported\\.json$/"
],
"matchStrings": [
"\"image\"\\s*:\\s*\"nextcloud:(?<currentValue>\\d+\\.\\d+\\.\\d+-fpm)@(?<currentDigest>sha256:[a-f0-9]{64})\""
],
"datasourceTemplate": "docker",
"depNameTemplate": "nextcloud",
"versioningTemplate": "docker"
}
],
"packageRules": [
{
"matchManagers": [
"custom.regex"
],
"matchPackageNames": [
"nextcloud"
],
"matchUpdateTypes": [
"major"
],
"enabled": false
}
]
}
22 changes: 15 additions & 7 deletions scripts/nextcloud-versions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@ validate_manifest() {
($root.versions | type == "array" and length >= 1 and length <= 5) and
($root.versions | all(.[];
type == "object" and
(keys == ["eol","major","release"]) and
(keys == ["eol","image","major"]) and
(.major | type == "number" and . >= 20 and . <= 99 and . == floor) and
(.release | type == "string" and test("^[0-9]{2,3}[.][0-9]+[.][0-9]+$")) and
((.release | split(".")[0] | tonumber) == .major) and
(.image | type == "string" and test("^nextcloud:[0-9]{2,3}[.][0-9]+[.][0-9]+-fpm@sha256:[a-f0-9]{64}$")) and
((.image | capture("^nextcloud:(?<major>[0-9]{2,3})[.]").major | tonumber) == .major) and
(.eol | type == "string" and test("^20[0-9]{2}-(0[1-9]|1[0-2])$") and . >= $month)
)) and
([$root.versions[].major] as $majors |
Expand Down Expand Up @@ -87,12 +87,13 @@ verify_release() {
(.images | any(.[]; .os == "linux" and .architecture == "amd64")) and
(.images | any(.[]; .os == "linux" and .architecture == "arm64"))
' <<< "$docker_tag" > /dev/null || error "Official image nextcloud:${release}-fpm lacks amd64 or arm64"
jq -er '.digest | select(test("^sha256:[a-f0-9]{64}$"))' <<< "$docker_tag" || error "No valid digest for nextcloud:${release}-fpm"
}

case "$command_name" in
matrix)
validate_manifest "$manifest"
matrix=$(jq -c '{include: [.versions[] | {major, tag: ((.major | tostring) + "-fpm"), release}]}' "$manifest")
matrix=$(jq -c '{include: [.versions[] | {major, tag: ((.major | tostring) + "-fpm"), image, release: (.image | split(":")[1] | split("-fpm@")[0])}]}' "$manifest")
if [[ -n $github_output ]]; then
printf 'matrix=%s\n' "$matrix" >> "$github_output"
else
Expand All @@ -116,14 +117,21 @@ case "$command_name" in
[[ $released =~ ^20[0-9]{2}-[0-9]{2}-[0-9]{2}$ && $released < $today || $released == "$today" ]] \
|| error "Nextcloud $major release date is invalid or in the future: $released"
[[ $(date -u -d "$released" +%F 2>/dev/null) == "$released" ]] || error "Invalid release date: $released"
if [[ $skip_upstream == false ]]; then verify_release "$release"; fi
digest="sha256:$(printf "%064d" 0)"
if [[ $skip_upstream == false ]]; then digest=$(verify_release "$release"); fi
image=''
if [[ -f $manifest ]]; then
image=$(jq -r --argjson major "$major" '.versions[]? | select(.major == $major) | .image // empty' "$manifest")
fi
if [[ -z $image ]]; then image="nextcloud:${release}-fpm@${digest}"; fi
printf '%s\t%s\t%s\n' "$major" "$image" "$eol" >> "$tmpdir/images.tsv"
done < "$tmpdir/releases.tsv"

jq -Rn --arg source "$source_url" '
{schema_version: 1, source: $source,
versions: ([inputs | split("\t") |
{major: (.[0] | tonumber), release: .[1], eol: .[2]}] | sort_by(.major))}
' < "$tmpdir/releases.tsv" > "$tmpdir/manifest.json"
{major: (.[0] | tonumber), image: .[1], eol: .[2]}] | sort_by(.major))}
' < "$tmpdir/images.tsv" > "$tmpdir/manifest.json"

validate_manifest "$tmpdir/manifest.json"
if [[ ! -f $manifest ]] || ! cmp -s "$tmpdir/manifest.json" "$manifest"; then
Expand Down
2 changes: 1 addition & 1 deletion scripts/publish-images.sh
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ promote() {
publish "$app_image" "${major}-fpm" "$prefix"
publish "$app_image" "${release}-fpm" "$prefix"
latest_prefix=$prefix
done < <(jq -r '.versions[] | [.major, .release] | @tsv' "$manifest_file")
done < <(jq -r '.versions[] | [.major, (.image | split(":")[1] | split("-fpm@")[0])] | @tsv' "$manifest_file")

[[ -n ${latest_prefix:-} ]] || fail "No approved stable Nextcloud major"
publish "$app_image" stable-fpm "$latest_prefix"
Expand Down
26 changes: 24 additions & 2 deletions tests/nextcloud-versions.bats
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ discover_fixture() {
run discover_fixture 2026-supported 2026-10-20
[ "$status" -eq 0 ]
[ "$(jq -r '[.versions[].major] | join(",")' "$inventory")" = 33,34,35 ]
[ "$(jq -r '.versions[-1].release' "$inventory")" = 35.0.2 ]
[[ "$(jq -r '.versions[-1].image' "$inventory")" == nextcloud:35.0.2-fpm@sha256:* ]]
}
@test "historical EOL formatting variations do not change the supported majors" {
run discover_fixture 2025-eol-structure 2025-10-27
Expand Down Expand Up @@ -103,7 +103,7 @@ case "$url" in
;;
*hub.docker.com*)
tag="${url##*/}"
printf '{"name":"%s","images":[{"os":"linux","architecture":"amd64"},{"os":"linux","architecture":"arm64"}]}\n' "$tag"
printf '{"name":"%s","digest":"sha256:%064d","images":[{"os":"linux","architecture":"amd64"},{"os":"linux","architecture":"arm64"}]}\n' "$tag" 0
;;
*) exit 1 ;;
esac
Expand Down Expand Up @@ -132,3 +132,25 @@ MOCK
[[ "$output" == *'lacks amd64 or arm64'* ]]
[ ! -f "$inventory" ]
}

@test "the manifest rejects an altered or incomplete Docker digest" {
discover_fixture 2026-supported 2026-10-20
jq '.versions[1].image = "nextcloud:34.0.5-fpm@sha256:invalid"' "$inventory" > "$TEST_ROOT/invalid.json"
run bash "$script" matrix --file "$TEST_ROOT/invalid.json" --today 2026-10-20
[ "$status" -ne 0 ]
}

@test "wiki discovery preserves Renovate-updated image refs" {
discover_fixture 2026-supported 2026-10-20
jq '.versions[2].image = "nextcloud:35.0.3-fpm@sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"' "$inventory" > "$TEST_ROOT/renovate.json"
cp "$TEST_ROOT/renovate.json" "$inventory"
discover_fixture 2026-supported 2026-10-20
[ "$(jq -r '.versions[2].image' "$inventory")" = "nextcloud:35.0.3-fpm@sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" ]
run bash "$script" matrix --file "$inventory" --today 2026-10-20
[ "$status" -eq 0 ]
}

@test "web Dockerfile uses a complete SHA256 digest" {
run grep -Eq '^FROM nginx:alpine@sha256:[a-f0-9]{64}$' .docker/web/Dockerfile
[ "$status" -eq 0 ]
}
2 changes: 1 addition & 1 deletion tests/publish-images.bats
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ teardown() {
grep -Fx "buildx imagetools create --tag ${WEB_IMAGE}:latest ${WEB_IMAGE}:sha-${SHA}-web-amd64 ${WEB_IMAGE}:sha-${SHA}-web-arm64" "$DOCKER_COMMAND_LOG"
}
@test "latest follows Nextcloud 36 when approved, without code changes" {
jq '.versions = (.versions[1:] + [{"major":36,"release":"36.0.0","eol":"2099-12"}])' \
jq '.versions = (.versions[1:] + [{"major":36,"image":"nextcloud:36.0.0-fpm@sha256:0000000000000000000000000000000000000000000000000000000000000000","eol":"2099-12"}])' \
"$INVENTORY" > "$TEST_ROOT/next.json"
run bash "$script" promote "$APP_IMAGE" "$WEB_IMAGE" "$SHA" "$TEST_ROOT/next.json"
[ "$status" -eq 0 ]
Expand Down
Loading