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
102 changes: 98 additions & 4 deletions .github/workflows/nextcloud-development.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Validate Nextcloud master image
name: Validate and publish Nextcloud master image

on:
pull_request:
Expand All @@ -10,6 +10,10 @@ on:
- 'scripts/scan-images.sh'
- 'trivy.yaml'
- '.github/workflows/nextcloud-development.yml'
- 'scripts/resolve-nextcloud-upstream.sh'
- 'scripts/push-master-architecture.sh'
- 'scripts/publish-master-manifest.sh'
- 'tests/nextcloud-development-image.bats'
push:
branches:
- main
Expand All @@ -19,19 +23,56 @@ on:
- 'scripts/scan-images.sh'
- 'trivy.yaml'
- '.github/workflows/nextcloud-development.yml'
- 'scripts/resolve-nextcloud-upstream.sh'
- 'scripts/push-master-architecture.sh'
- 'scripts/publish-master-manifest.sh'
- 'tests/nextcloud-development-image.bats'
schedule:
- cron: '17 3 * * *'
workflow_dispatch:

permissions:
contents: read

concurrency:
group: nextcloud-master-validation-${{ github.ref }}
group: nextcloud-master-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: true

env:
APP_IMAGE: ghcr.io/librecodecoop/nextcloud-docker-app
NEXTCLOUD_DAILY_URL: https://download.nextcloud.com/server/daily/latest-master.tar.bz2
NEXTCLOUD_BASE_TAG: stable-fpm

jobs:
script-tests:
name: Test development image scripts
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Setup Bats
uses: bats-core/bats-action@77d6fb60505b4d0d1d73e48bd035b55074bbfb43 # 4.0.0
with:
support-install: false
assert-install: false
detik-install: false
file-install: false

- name: Test development image scripts
run: bats tests/nextcloud-development-image.bats

validate:
needs:
- script-tests
name: master-fpm / linux/${{ matrix.arch }}
runs-on: ubuntu-latest
permissions:
contents: read
packages: write

strategy:
fail-fast: false
Expand Down Expand Up @@ -62,16 +103,31 @@ jobs:
detik-install: false
file-install: false

- name: Resolve upstream inputs
id: upstream
shell: bash
run: bash scripts/resolve-nextcloud-upstream.sh "${NEXTCLOUD_BASE_TAG}" "${NEXTCLOUD_DAILY_URL}" >> "${GITHUB_OUTPUT}"

- name: Build Nextcloud master app image
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with:
context: .docker/app
platforms: linux/${{ matrix.arch }}
load: true
build-args: |
NEXTCLOUD_BASE_IMAGE=nextcloud:stable-fpm
NEXTCLOUD_BASE_IMAGE=${{ steps.upstream.outputs.base_image }}
NEXTCLOUD_SOURCE=daily
NEXTCLOUD_DAILY_URL=https://download.nextcloud.com/server/daily/latest-master.tar.bz2
NEXTCLOUD_DAILY_URL=${{ env.NEXTCLOUD_DAILY_URL }}
labels: |
org.opencontainers.image.source=https://github.com/${{ github.repository }}
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.created=${{ steps.upstream.outputs.created }}
org.opencontainers.image.version=master-fpm
coop.librecode.nextcloud.channel=master
coop.librecode.nextcloud.source=${{ env.NEXTCLOUD_DAILY_URL }}
coop.librecode.nextcloud.source.sha512=${{ steps.upstream.outputs.source_sha512 }}
coop.librecode.nextcloud.base.digest=${{ steps.upstream.outputs.base_digest }}
coop.librecode.runtime.variant=fpm
tags: scan/master:${{ matrix.arch }}
cache-from: type=gha,scope=master-${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=master-${{ matrix.arch }}
Expand All @@ -95,3 +151,41 @@ jobs:
env:
APP_IMAGE: scan/master:${{ matrix.arch }}
run: bats tests/app-image.bats

- name: Log in to GitHub Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Push validated architecture image
if: github.event_name != 'pull_request'
shell: bash
run: bash scripts/push-master-architecture.sh "${APP_IMAGE}" "scan/master:${{ matrix.arch }}" "${GITHUB_SHA}" "${{ matrix.arch }}"

publish:
name: Publish master-fpm manifest
if: github.event_name != 'pull_request'
needs:
- validate
runs-on: ubuntu-latest
permissions:
contents: read
packages: write

steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3

- name: Log in to GitHub Container Registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Publish multi-platform master-fpm
shell: bash
run: bash scripts/publish-master-manifest.sh "${APP_IMAGE}" "${GITHUB_SHA}"
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ COMPOSE ?= docker compose
GARAGES3_COMPOSE_FILE ?= docker-compose-garages3.yml
APP_TEST_IMAGE ?= nextcloud-app:acceptance

.PHONY: up-garages3 down-garages3 bootstrap-garages3 garage-status-garages3 start-garages3 wait-nextcloud-garages3 setup-garages3 test-scan-images test-ncdd test-app-image test-current-app-image scan-images
.PHONY: up-garages3 down-garages3 bootstrap-garages3 garage-status-garages3 start-garages3 wait-nextcloud-garages3 setup-garages3 test-scan-images test-development-image-scripts test-ncdd test-app-image test-current-app-image scan-images

up-garages3:
$(COMPOSE) -f $(GARAGES3_COMPOSE_FILE) up -d garage
Expand Down Expand Up @@ -30,6 +30,9 @@ setup-garages3:
test-scan-images:
bash tests/test-scan-images.sh

test-development-image-scripts:
bats tests/nextcloud-development-image.bats

test-ncdd:
bats tests/ncdd.bats

Expand Down
18 changes: 16 additions & 2 deletions docs/images.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,20 @@ The repository Compose environment keeps `NEXTCLOUD_VERSION` as its user-facing

The app image remains LibreSign-ready while being generic across Nextcloud versions. It keeps repository-level runtime requirements such as Poppler, UTF-8 locale support, and the PHP bz2 extension, but does not reinstall extensions already supplied by the official Nextcloud base image such as Imagick. Dependabot owns Docker and GitHub Actions updates in this repository. Renovate is deliberately restricted to custom regex-managed values that Dependabot cannot see, currently the explicit Trivy binary version used by CI. The two bots must not manage the same dependency.

## Development publication

The rolling Nextcloud Server `master` image is published as:

```
ghcr.io/librecodecoop/nextcloud-docker-app:master-fpm
```

Pull requests build, scan, and run runtime acceptance for both amd64 and arm64 without publishing. Pushes to `main`, the daily scheduled run, and manual workflow dispatches may publish only after both architectures pass those gates.

The workflow resolves the official `nextcloud:stable-fpm` base to an OCI digest and records that digest in image metadata. It also records the SHA-512 of the exact upstream `latest-master.tar.bz2` artifact used for the build. This provides exact upstream artifact traceability without inventing a Nextcloud Git commit that the daily archive does not expose.

Architecture-specific staging tags are implementation details used to assemble the multi-platform manifest. The public development-channel contract is `:master-fpm`.

## Runtime acceptance

The app image has a runtime acceptance test based on the same behavioral checks used by the official Nextcloud container projects.
Expand All @@ -192,8 +206,8 @@ This document defines the target contract and the generic app-image foundation n

Remaining work in #47 must continue incrementally. In particular:

- publish development images from the generic foundation using the documented `:master-fpm` contract;
- add the remaining OCI traceability metadata to published images;
- keep the `:master-fpm` publication workflow aligned with the generic app-image foundation;
- extend traceability metadata when upstream exposes stronger revision identifiers;
- keep LibreSign-specific behavior out of the generic runtime;
- preserve scan and runtime-acceptance gates before publication;
- preserve the existing Compose environment until a replacement is explicitly validated.
13 changes: 13 additions & 0 deletions scripts/publish-master-manifest.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/env bash

set -euo pipefail

app_image=${1:?Usage: publish-master-manifest.sh <app-image> <revision>}
revision=${2:?Usage: publish-master-manifest.sh <app-image> <revision>}

docker buildx imagetools create \
--tag "${app_image}:master-fpm" \
"${app_image}:master-fpm-${revision}-amd64" \
"${app_image}:master-fpm-${revision}-arm64"

docker buildx imagetools inspect "${app_image}:master-fpm"
20 changes: 20 additions & 0 deletions scripts/push-master-architecture.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/usr/bin/env bash

set -euo pipefail

app_image=${1:?Usage: push-master-architecture.sh <app-image> <source-tag> <revision> <arch>}
source_tag=${2:?Usage: push-master-architecture.sh <app-image> <source-tag> <revision> <arch>}
revision=${3:?Usage: push-master-architecture.sh <app-image> <source-tag> <revision> <arch>}
arch=${4:?Usage: push-master-architecture.sh <app-image> <source-tag> <revision> <arch>}

case "${arch}" in
amd64|arm64) ;;
*)
echo "Unsupported architecture: ${arch}" >&2
exit 2
;;
esac

staging_tag="master-fpm-${revision}-${arch}"
docker tag "${source_tag}" "${app_image}:${staging_tag}"
docker push "${app_image}:${staging_tag}"
43 changes: 43 additions & 0 deletions scripts/resolve-nextcloud-upstream.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/usr/bin/env bash

set -euo pipefail

base_tag=${1:?Usage: resolve-nextcloud-upstream.sh <base-tag> <daily-url>}
daily_url=${2:?Usage: resolve-nextcloud-upstream.sh <base-tag> <daily-url>}

token_response="$(curl -fsSL "https://auth.docker.io/token?service=registry.docker.io&scope=repository:library/nextcloud:pull")"
token="$(printf '%s' "${token_response}" | sed -n 's/.*"token"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
if [ -z "${token}" ]; then
echo "Could not obtain Docker Hub token" >&2
exit 1
fi

headers="$(mktemp)"
checksum_file="$(mktemp)"
trap 'rm -f "$headers" "$checksum_file"' EXIT

curl -fsSLI \
-H "Authorization: Bearer ${token}" \
-H 'Accept: application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.list.v2+json' \
-D "${headers}" \
-o /dev/null \
"https://registry-1.docker.io/v2/library/nextcloud/manifests/${base_tag}"

base_digest="$(awk 'BEGIN { IGNORECASE=1 } /^docker-content-digest:/ { gsub("\r", "", $2); print $2; exit }' "${headers}")"
if [[ ! "${base_digest}" =~ ^sha256:[0-9a-f]{64}$ ]]; then
echo "Could not resolve nextcloud:${base_tag} digest" >&2
exit 1
fi

archive_name="$(basename "${daily_url}")"
curl -fsSL "${daily_url}.sha512" -o "${checksum_file}"
source_sha512="$(awk -v archive="${archive_name}" '$2 == archive { print $1; exit }' "${checksum_file}")"
if [[ ! "${source_sha512}" =~ ^[0-9a-fA-F]{128}$ ]]; then
echo "Could not resolve SHA-512 for ${archive_name}" >&2
exit 1
fi

printf 'base_image=nextcloud@%s\n' "${base_digest}"
printf 'base_digest=%s\n' "${base_digest}"
printf 'source_sha512=%s\n' "${source_sha512,,}"
printf 'created=%s\n' "$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
115 changes: 115 additions & 0 deletions tests/nextcloud-development-image.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
#!/usr/bin/env bats

setup() {
TEST_ROOT="$(mktemp -d)"
BIN_DIR="$TEST_ROOT/bin"
mkdir -p "$BIN_DIR"
export PATH="$BIN_DIR:$PATH"
}

teardown() {
rm -rf "$TEST_ROOT"
}

@test "resolve upstream emits validated metadata" {
cat > "$BIN_DIR/curl" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
args="$*"
if [[ "$args" == *"auth.docker.io/token"* ]]; then
printf '{"token":"test-token"}'
elif [[ "$args" == *"registry-1.docker.io"* ]]; then
headers=""
while [ "$#" -gt 0 ]; do
if [ "$1" = "-D" ]; then
headers=$2
shift 2
else
shift
fi
done
printf 'docker-content-digest: sha256:%064d\r\n' 0 > "$headers"
elif [[ "$args" == *".sha512"* ]]; then
output=""
while [ "$#" -gt 0 ]; do
if [ "$1" = "-o" ]; then
output=$2
shift 2
else
shift
fi
done
printf '%0128d latest-master.tar.bz2\n' 0 > "$output"
else
exit 1
fi
EOF
chmod +x "$BIN_DIR/curl"

run bash scripts/resolve-nextcloud-upstream.sh stable-fpm https://download.nextcloud.com/server/daily/latest-master.tar.bz2

[ "$status" -eq 0 ]
[[ "$output" == *"base_image=nextcloud@sha256:"* ]]
[[ "$output" == *"source_sha512="* ]]
[[ "$output" == *"created="* ]]
}

@test "resolve upstream rejects an invalid base digest" {
cat > "$BIN_DIR/curl" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
args="$*"
if [[ "$args" == *"auth.docker.io/token"* ]]; then
printf '{"token":"test-token"}'
elif [[ "$args" == *"registry-1.docker.io"* ]]; then
while [ "$#" -gt 0 ]; do
if [ "$1" = "-D" ]; then
printf 'docker-content-digest: invalid\r\n' > "$2"
exit 0
fi
shift
done
fi
EOF
chmod +x "$BIN_DIR/curl"

run bash scripts/resolve-nextcloud-upstream.sh stable-fpm https://download.nextcloud.com/server/daily/latest-master.tar.bz2

[ "$status" -ne 0 ]
[[ "$output" == *"Could not resolve nextcloud:stable-fpm digest"* ]]
}

@test "architecture publication uses only the requested staging tag" {
cat > "$BIN_DIR/docker" <<EOF
#!/usr/bin/env bash
printf '%s\n' "\$*" >> "$TEST_ROOT/docker.log"
EOF
chmod +x "$BIN_DIR/docker"

run bash scripts/push-master-architecture.sh ghcr.io/example/app scan/master:amd64 abc123 amd64

[ "$status" -eq 0 ]
grep -Fx "tag scan/master:amd64 ghcr.io/example/app:master-fpm-abc123-amd64" "$TEST_ROOT/docker.log"
grep -Fx "push ghcr.io/example/app:master-fpm-abc123-amd64" "$TEST_ROOT/docker.log"
}

@test "architecture publication rejects unsupported architectures" {
run bash scripts/push-master-architecture.sh ghcr.io/example/app scan/master:s390x abc123 s390x

[ "$status" -eq 2 ]
[[ "$output" == *"Unsupported architecture: s390x"* ]]
}

@test "manifest publication combines exactly amd64 and arm64" {
cat > "$BIN_DIR/docker" <<EOF
#!/usr/bin/env bash
printf '%s\n' "\$*" >> "$TEST_ROOT/docker.log"
EOF
chmod +x "$BIN_DIR/docker"

run bash scripts/publish-master-manifest.sh ghcr.io/example/app abc123

[ "$status" -eq 0 ]
grep -Fx "buildx imagetools create --tag ghcr.io/example/app:master-fpm ghcr.io/example/app:master-fpm-abc123-amd64 ghcr.io/example/app:master-fpm-abc123-arm64" "$TEST_ROOT/docker.log"
grep -Fx "buildx imagetools inspect ghcr.io/example/app:master-fpm" "$TEST_ROOT/docker.log"
}
Loading