diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 76fe386861..fdb5c9d48f 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -18,7 +18,8 @@ jobs: id-token: write # needed for signing the images with GitHub OIDC Token packages: write # needed for publishing packages to GHCR env: - TEST_TAG: vectorim/element-web:test + TEST_TAG: ghcr.io/element-hq/element-web:test + TEST_TAG_MODULES: ghcr.io/element-hq/element-web/modules:test steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: @@ -38,16 +39,18 @@ jobs: install: true - name: Build and load - id: test-build - uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7 + uses: docker/bake-action@d3418bd7d0e9324001bca92fa8ba175ea7e6dc9b # v7 with: - context: . - file: apps/web/Dockerfile + source: . + files: ./docker-bake.hcl load: true + set: | + element-web.tags=${{ env.TEST_TAG }} + element-web-modules.tags=${{ env.TEST_TAG_MODULES }} - name: Test the image env: - IMAGEID: ${{ steps.test-build.outputs.imageid }} + IMAGE: ${{ env.TEST_TAG }} timeout-minutes: 2 run: | set -x @@ -65,7 +68,7 @@ jobs: -e "ELEMENT_WEB_PORT=$ELEMENT_WEB_PORT" \ -dp "$ELEMENT_WEB_PORT:$ELEMENT_WEB_PORT" \ -v $(pwd)/test_modules:/modules \ - "$IMAGEID" \ + "$IMAGE" \ ) # Run some smoke tests @@ -81,6 +84,41 @@ jobs: # Clean up docker stop "$CONTAINER_ID" + - name: Test the modules image + env: + IMAGE: ${{ env.TEST_TAG_MODULES }} + timeout-minutes: 2 + run: | + set -x + + ELEMENT_WEB_PORT=8182 + CONTAINER_ID=$( + docker run \ + --rm \ + -e "ELEMENT_WEB_PORT=$ELEMENT_WEB_PORT" \ + -dp "$ELEMENT_WEB_PORT:$ELEMENT_WEB_PORT" \ + "$IMAGE" \ + ) + + # Every module pinned in the Dockerfile should be listed in the config and served + MODULES=$(grep -oE '^ARG MODULE_[A-Z0-9_]+_VERSION=' apps/web/Dockerfile | + sed -E 's/^ARG MODULE_(.*)_VERSION=$/\1/' | tr '[:upper:]_' '[:lower:]-') + test -n "$MODULES" + + CONFIG=$(wget --retry-connrefused --tries=5 -q --wait=3 -O - "http://localhost:$ELEMENT_WEB_PORT/config.json") + for MODULE in $MODULES; do + echo "$CONFIG" | jq -e --arg m "/modules/$MODULE/index.js" '.modules | index($m)' + wget -q --spider "http://localhost:$ELEMENT_WEB_PORT/modules/$MODULE/index.js" + done + + # Check healthcheck + until test "$(docker inspect -f '{{.State.Health.Status}}' "$CONTAINER_ID")" == "healthy"; do + sleep 1 + done + + # Clean up + docker stop "$CONTAINER_ID" + - name: Docker meta id: meta uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6 @@ -95,6 +133,20 @@ jobs: type=ref,event=tag flavor: | latest=${{ contains(github.ref_name, '-rc.') && 'false' || 'auto' }} + bake-target: docker-metadata-action + + - name: Docker meta (modules) + id: meta-modules + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6 + if: github.event_name != 'pull_request' + with: + images: ghcr.io/element-hq/element-web/modules + tags: | + type=ref,event=branch + type=ref,event=tag + flavor: | + latest=${{ contains(github.ref_name, '-rc.') && 'false' || 'auto' }} + bake-target: docker-metadata-action-modules - name: Login to Docker Hub uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4 @@ -147,28 +199,37 @@ jobs: username: ${{ steps.import-secrets.outputs.OCI_USERNAME }} password: ${{ steps.import-secrets.outputs.OCI_PASSWORD }} + # Both targets are built in a single bake so that the app is only compiled once. - name: Build and push id: build-and-push - uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7 + uses: docker/bake-action@d3418bd7d0e9324001bca92fa8ba175ea7e6dc9b # v7 if: github.event_name != 'pull_request' with: - context: . - file: apps/web/Dockerfile + source: . + files: | + ./docker-bake.hcl + ${{ steps.meta.outputs.bake-file }} + ${{ steps.meta-modules.outputs.bake-file }} push: true - platforms: linux/amd64,linux/arm64 - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} + vars: | + PLATFORMS=linux/amd64,linux/arm64 - name: Sign the images with GitHub OIDC Token env: - DIGEST: ${{ steps.build-and-push.outputs.digest }} + METADATA: ${{ steps.build-and-push.outputs.metadata }} TAGS: ${{ steps.meta.outputs.tags }} + TAGS_MODULES: ${{ steps.meta-modules.outputs.tags }} if: github.event_name != 'pull_request' run: | images="" - for tag in ${TAGS}; do - images+="${tag}@${DIGEST} " - done + sign_target() { + digest=$(echo "$METADATA" | jq -er --arg t "$1" '.[$t]."containerimage.digest"') + for tag in $2; do + images+="${tag}@${digest} " + done + } + sign_target element-web "$TAGS" + sign_target element-web-modules "$TAGS_MODULES" cosign sign --yes ${images} - name: Update repo description diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile index 2c3a135bd7..67f7bd8450 100644 --- a/apps/web/Dockerfile +++ b/apps/web/Dockerfile @@ -30,7 +30,7 @@ RUN --mount=type=bind,source=.git,target=/src/.git /src/scripts/docker-package.s RUN cp /src/apps/web/config.sample.json /src/apps/web/webapp/config.json # App -FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:22f839c5fb4007dc24d203a170a9e03fc185d660bfefc34ac6823a7aef085cbc +FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:22f839c5fb4007dc24d203a170a9e03fc185d660bfefc34ac6823a7aef085cbc AS element_web # Need root user to install packages & manipulate the usr directory USER root @@ -55,3 +55,39 @@ USER nginx ENV ELEMENT_WEB_PORT=80 HEALTHCHECK --start-period=5s CMD wget -q --spider http://localhost:$ELEMENT_WEB_PORT/config.json + +# Modules are consumed as prebuilt release artifacts rather than built from source. +# Each module is pinned to a version and the sha256 of its release archive. +FROM --platform=$BUILDPLATFORM alpine:3.22@sha256:14358309a308569c32bdc37e2e0e9694be33a9d99e68afb0f5ff33cc1f695dce AS modules + +ARG MODULE_BANNER_VERSION=v1.0.0 +ADD --checksum=sha256:8aabd9d43d40ffb499050246f7968323a08895b6f53bd48a71d4c7f0daf96634 \ + https://github.com/element-hq/element-web/releases/download/module%2Fbanner%2F${MODULE_BANNER_VERSION}/banner-${MODULE_BANNER_VERSION}.zip \ + /tmp/modules/banner.zip + +ARG MODULE_RESTRICTED_GUESTS_VERSION=v1.0.0 +ADD --checksum=sha256:d883327469ae78504a4e0aa8ebf2bbf525a9407ab10430ef42cf9338cc0821bb \ + https://github.com/element-hq/element-web/releases/download/module%2Frestricted-guests%2F${MODULE_RESTRICTED_GUESTS_VERSION}/restricted-guests-${MODULE_RESTRICTED_GUESTS_VERSION}.zip \ + /tmp/modules/restricted-guests.zip + +ARG MODULE_WIDGET_LIFECYCLE_VERSION=v1.0.0 +ADD --checksum=sha256:125e5a7a045e3cebee2c82ca30a477ebc8e31ee3bd139ae46e177612c25cc988 \ + https://github.com/element-hq/element-web/releases/download/module%2Fwidget-lifecycle%2F${MODULE_WIDGET_LIFECYCLE_VERSION}/widget-lifecycle-${MODULE_WIDGET_LIFECYCLE_VERSION}.zip \ + /tmp/modules/widget-lifecycle.zip + +ARG MODULE_WIDGET_TOGGLES_VERSION=v1.0.0 +ADD --checksum=sha256:27b0d0d9d803c41855aa94f02493ce214321f8d0c65af500d85875a3ef20efb0 \ + https://github.com/element-hq/element-web/releases/download/module%2Fwidget-toggles%2F${MODULE_WIDGET_TOGGLES_VERSION}/widget-toggles-${MODULE_WIDGET_TOGGLES_VERSION}.zip \ + /tmp/modules/widget-toggles.zip + +# Unpack the modules +RUN apk add --no-cache unzip && \ + for archive in /tmp/modules/*.zip; do \ + name=$(basename "$archive" .zip); \ + mkdir -p "/modules/$name" && unzip -q "$archive" -d "/modules/$name"; \ + done + +# Target with element_web + `/modules` copied in +FROM element_web AS element_web_modules + +COPY --from=modules /modules /modules diff --git a/docker-bake.hcl b/docker-bake.hcl new file mode 100644 index 0000000000..6e764e59d4 --- /dev/null +++ b/docker-bake.hcl @@ -0,0 +1,37 @@ +# Builds the Element Web images. Both targets share the `builder` and `element_web` stages, +# building them together in a single bake invocation compiles the app only once. +# +# docker buildx bake # both images, for the local platform +# docker buildx bake element-web # just the base image +# PLATFORMS=linux/amd64,linux/arm64 docker buildx bake --push + +variable "PLATFORMS" { + default = "" +} + +# Populated by docker/metadata-action in CI; empty locally. +target "docker-metadata-action" {} +target "docker-metadata-action-modules" {} + +target "_common" { + context = "." + dockerfile = "apps/web/Dockerfile" + platforms = PLATFORMS == "" ? null : split(",", PLATFORMS) +} + +# The stock image, with no modules bundled. +target "element-web" { + inherits = ["_common", "docker-metadata-action"] + target = "element_web" +} + +# The same image with the released Element modules baked into /modules, +# where the entrypoint picks them up and adds them to config.json. +target "element-web-modules" { + inherits = ["_common", "docker-metadata-action-modules"] + target = "element_web_modules" +} + +group "default" { + targets = ["element-web", "element-web-modules"] +} diff --git a/modules/Dockerfile b/modules/Dockerfile deleted file mode 100644 index 0950c353bb..0000000000 --- a/modules/Dockerfile +++ /dev/null @@ -1,24 +0,0 @@ -ARG ELEMENT_VERSION=latest@sha256:9f9b546280e67e492d56f6e775c449e74356bffb98daaf211abc755276713fad - -FROM --platform=$BUILDPLATFORM node:lts-alpine@sha256:f70403e87646dc51b45295f4b8b70cdad0b63d2297c4c9899119b03f7af7a6b3 AS builder - -ARG BUILD_CONTEXT - -RUN apk add --no-cache jq - -WORKDIR /app -COPY package.json yarn.lock ./ -# Copy the package.json files of all modules & packages to ensure the frozen workspace lockfile holds up -RUN --mount=type=bind,target=/docker-context \ - cd /docker-context/; \ - find . -path ./node_modules -prune -o -name "package.json" -mindepth 0 -maxdepth 4 -exec cp --parents "{}" /app/ \; -RUN yarn install --frozen-lockfile --ignore-scripts -COPY tsconfig.json ./ -COPY ./$BUILD_CONTEXT ./$BUILD_CONTEXT -RUN cd $BUILD_CONTEXT && yarn vite build -RUN mkdir /modules -RUN cp -r ./$BUILD_CONTEXT/lib/ /modules/$(jq -r '"\(.name)-v\(.version)"' ./$BUILD_CONTEXT/package.json) - -FROM ghcr.io/element-hq/element-web:${ELEMENT_VERSION} - -COPY --from=builder /modules /modules/ \ No newline at end of file