diff --git a/.github/actions/get-latest-upstream/action.yml b/.github/actions/get-latest-upstream/action.yml index 6588e5e47..2e20f52a9 100644 --- a/.github/actions/get-latest-upstream/action.yml +++ b/.github/actions/get-latest-upstream/action.yml @@ -51,7 +51,11 @@ runs: fi fi ASSET=$(curl -s -H "$AUTH_HEADER" https://api.github.com/repos/${AUTHORS}/selenium/releases/tags/${RELEASE} | jq -r '[.assets[] | select(.name | endswith(".jar"))] | last') - jar_file=$(echo "${ASSET}" | jq -r '.name') + jar_file=$(echo "${ASSET}" | jq -r '.name // empty') + if [ -z "${jar_file}" ]; then + echo "Release ${RELEASE} has no server jar asset" + exit 1 + fi echo "Server package: ${jar_file}" VERSION=$(echo $jar_file | sed 's/selenium-server-//;s/\.jar//') # What that jar actually IS, not just what it is called. The nightly jar @@ -60,13 +64,19 @@ runs: # version alone cannot tell two nightly cores apart. Anything keyed on the # core needs this, or it will treat last week's nightly as today's. ASSET_DIGEST=$(echo "${ASSET}" | jq -r '.digest // empty') + DIGEST_SOURCE="asset digest" if [ -z "${ASSET_DIGEST}" ] || [ "${ASSET_DIGEST}" = "null" ]; then # Older assets predate the digest field; identity then comes from the # asset id and the time it was last uploaded, which move together. ASSET_DIGEST=$(echo "${ASSET}" | jq -r '"\(.id)-\(.updated_at)"') + DIGEST_SOURCE="asset id and upload time (no digest field)" fi echo "BASE_RELEASE=${RELEASE} | BASE_VERSION=${VERSION} | VERSION=${VERSION}" - echo "Server package digest: ${ASSET_DIGEST}" + # Which of the two it is matters: only a real sha256 can be verified + # against the jar the build downloads, and if the digest field ever came + # back constant, everything keyed on the core would quietly go back to + # treating every nightly as the same one with nothing to notice it. + echo "Server package digest: ${ASSET_DIGEST} (from ${DIGEST_SOURCE})" echo "BASE_RELEASE=${RELEASE}" >> $GITHUB_ENV echo "BASE_VERSION=${VERSION}" >> $GITHUB_ENV echo "BASE_ASSET_DIGEST=${ASSET_DIGEST}" >> $GITHUB_ENV diff --git a/.github/workflows/build-images.yml b/.github/workflows/build-images.yml index f0a393e2f..2b050c890 100644 --- a/.github/workflows/build-images.yml +++ b/.github/workflows/build-images.yml @@ -53,6 +53,9 @@ on: base-release: description: 'Upstream release tag that core came from' value: ${{ jobs.decide.outputs.base-release }} + base-asset-digest: + description: 'sha256 of the server jar that core resolved to' + value: ${{ jobs.decide.outputs.base-asset-digest }} src-hash: description: 'Content hash the tag is keyed on' value: ${{ jobs.decide.outputs.src-hash }} @@ -73,6 +76,7 @@ jobs: src-hash: ${{ steps.decide.outputs.src-hash }} base-version: ${{ steps.decide.outputs.base-version }} base-release: ${{ steps.decide.outputs.base-release }} + base-asset-digest: ${{ steps.decide.outputs.base-asset-digest }} registry: ${{ steps.decide.outputs.registry }} steps: - name: Checkout code @@ -162,6 +166,9 @@ jobs: echo "src-hash=${SRC_HASH}" >> "$GITHUB_OUTPUT" echo "base-version=${BASE_VERSION}" >> "$GITHUB_OUTPUT" echo "base-release=${BASE_RELEASE}" >> "$GITHUB_OUTPUT" + # The build has to end up with the jar this hash was computed from, not + # merely with the same release tag - the tag's asset is replaced nightly. + echo "base-asset-digest=${BASE_ASSET_DIGEST}" >> "$GITHUB_OUTPUT" { echo "### Images" echo @@ -246,15 +253,22 @@ jobs: command: make setup_dev_env # Use exactly the core decide hashed on, so the tag and the image agree. - # All three matter: Base/Dockerfile builds its download URL from the - # release tag AND the version + # All four matter. Base/Dockerfile builds its download URL from the release + # tag AND the version # .../releases/download/${RELEASE}/selenium-server-${VERSION}.jar # so carrying only BASE_VERSION leaves BASE_RELEASE on the Makefile default - # and asks for a jar that does not exist. + # and asks for a jar that does not exist. And that URL names the asset, not + # its contents - the nightly one is replaced every night - so the digest is + # the only part of this that pins the build to the jar the decision was + # actually made about. Base/Dockerfile fails the build when what it + # downloads does not match, which also stops the two architecture jobs - + # they download independently, minutes apart - from putting different cores + # into one manifest. - name: Use the core decide resolved run: | echo "BASE_VERSION=${{ needs.decide.outputs.base-version }}" >> $GITHUB_ENV echo "BASE_RELEASE=${{ needs.decide.outputs.base-release }}" >> $GITHUB_ENV + echo "BASE_ASSET_DIGEST=${{ needs.decide.outputs.base-asset-digest }}" >> $GITHUB_ENV echo "VERSION=${{ needs.decide.outputs.base-version }}" >> $GITHUB_ENV # Native, single platform. Edge and Chrome for Testing are amd64-only and @@ -275,7 +289,9 @@ jobs: timeout_minutes: 180 max_attempts: 3 retry_wait_seconds: 60 - command: PLATFORMS="${PLATFORMS}" VERSION="${BASE_VERSION}" BUILD_DATE="${BUILD_DATE}" make build + command: | + PLATFORMS="${PLATFORMS}" VERSION="${BASE_VERSION}" BUILD_DATE="${BUILD_DATE}" \ + BASE_ASSET_DIGEST="${BASE_ASSET_DIGEST}" make build # Also tag pr-, so cleanup-pr-images.yml has something to delete when the # pull request closes. The src-* tag is shared and must outlive any one diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 59812a0ba..e8e0e0d34 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -347,8 +347,13 @@ jobs: with: github_token: ${{ secrets.SELENIUM_CI_TOKEN }} branch: trunk + # The release publishes -, which is what the notes read by + # default. GHCR_NAMESPACE has to be passed though: the mirror above is + # keyed on the repository owner, not on the script's SeleniumHQ default. - name: Create release notes (release_notes.md) - run: ./generate_release_notes.sh ${LATEST_TAG} origin/trunk ${GRID_VERSION} ${BUILD_DATE} + run: | + GHCR_NAMESPACE="ghcr.io/$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')" \ + ./generate_release_notes.sh ${LATEST_TAG} origin/trunk ${GRID_VERSION} ${BUILD_DATE} - name: Upload release notes uses: actions/upload-artifact@main with: diff --git a/.github/workflows/k8s-scaling-test.yml b/.github/workflows/k8s-scaling-test.yml index f5f112a80..2c01835bb 100644 --- a/.github/workflows/k8s-scaling-test.yml +++ b/.github/workflows/k8s-scaling-test.yml @@ -201,8 +201,14 @@ jobs: echo "AUTHORS=${AUTHORS}" >> $GITHUB_ENV env: AUTHORS: ${{ vars.AUTHORS || github.repository_owner }} + # A pull request from a fork gets no secrets, so these are empty and the + # login exits 1 - failing the job before a single test runs. The other test + # workflows that log in here already tolerate that; this one was missed, + # which is why a fork pull request saw only the scaling jobs go red. The + # login is an anonymous-pull-limit courtesy, not a requirement: every image + # the cluster needs is either built in this job or public. - name: Login Docker Hub - run: docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD" + run: docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD" || true env: DOCKER_USERNAME: ${{secrets.DOCKER_USERNAME}} DOCKER_PASSWORD: ${{secrets.DOCKER_PASSWORD}} diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 13cf76272..653636256 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -51,6 +51,14 @@ jobs: make setup_dev_env - name: Output Docker info run: docker info + # The release notes read the arm64 browser versions by running the arm64 + # image. On the build path the multi-architecture build registered the + # emulators as a side effect; the promotion path builds nothing, so nothing + # would. + - name: Set up QEMU + uses: docker/setup-qemu-action@main + with: + platforms: arm64 - name: Set Selenium base version uses: ./.github/actions/get-latest-upstream with: @@ -156,16 +164,28 @@ jobs: command: GHCR_NAMESPACE="ghcr.io/$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')" VERSION="${GRID_VERSION}" BUILD_DATE=${BUILD_DATE} make release_ghcr_nightly # - name: Update package versions # run: make generate_nightly_sbom + # The changelog is the range ...trunk. An empty + # LATEST_TAG silently turns that into HEAD...trunk - nothing - so stop here + # rather than publishing notes with no changelog in them. - name: Get current latest tag - run: echo "LATEST_TAG=$(git describe --tags --abbrev=0 --exclude=nightly --exclude=selenium-grid*)" >> $GITHUB_ENV + run: | + set -euo pipefail + LATEST_TAG=$(git describe --tags --abbrev=0 --exclude=nightly --exclude='selenium-grid*') + echo "LATEST_TAG=${LATEST_TAG}" >> $GITHUB_ENV - name: Display latest tag run: echo ${LATEST_TAG} - name: Sets env var for nightly tag run: | echo "NEXT_TAG=nightly" >> $GITHUB_ENV - echo "FILTER_IMAGE_TAG=nightly" >> $GITHUB_ENV + # :nightly is the only tag the nightly publishes - the promotion never + # writes a - tag, and on that path nothing is in the local + # image store either. IMAGE_TAG is what the notes read the versions and the + # published manifests from. - name: Create release notes (release_notes.md) - run: ./generate_release_notes.sh ${LATEST_TAG} origin/trunk ${GRID_VERSION} ${BUILD_DATE} + run: | + IMAGE_TAG=nightly VIDEO_IMAGE_TAG=nightly \ + GHCR_NAMESPACE="ghcr.io/$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')" \ + ./generate_release_notes.sh ${LATEST_TAG} origin/trunk ${GRID_VERSION} ${BUILD_DATE} - name: Set up Python uses: actions/setup-python@main with: diff --git a/Base/Dockerfile b/Base/Dockerfile index 8c601d4bb..098d1c500 100644 --- a/Base/Dockerfile +++ b/Base/Dockerfile @@ -6,6 +6,13 @@ LABEL org.opencontainers.image.source="https://github.com/${AUTHORS}/docker-sele # Arguments to define the version of dependencies to download ARG VERSION ARG RELEASE=selenium-${VERSION} +# sha256 of the server jar this build is required to contain, as CI resolved it +# before deciding to build. The download URL below is keyed on the release tag +# and the version alone, and the nightly release keeps one asset name for a whole +# snapshot cycle while its contents are replaced every night - so the URL is a +# moving target and the two are not enough to say which core an image holds. +# Empty on a local build, which takes whatever the URL currently serves. +ARG SERVER_JAR_DIGEST="" # Default value should be aligned with upstream Selenium (https://github.com/SeleniumHQ/selenium/blob/trunk/MODULE.bazel) ARG OPENTELEMETRY_VERSION=1.65.0 ARG GRPC_VERSION=1.83.1 @@ -26,6 +33,10 @@ ARG JRE_VERSION=21 ARG TARGETARCH ARG TARGETVARIANT +# Recorded so a published image can be traced back to the core it was built +# against without pulling it apart, and inherited by every image built FROM base. +LABEL org.seleniumhq.selenium.core.digest="${SERVER_JAR_DIGEST}" + USER root ENV DEBIAN_FRONTEND=noninteractive \ @@ -130,6 +141,16 @@ RUN if [ "${TARGETARCH}" = "arm" ] && [ "${TARGETVARIANT}" = "v7" ]; then \ && chmod -R 775 /opt/selenium /var/run/supervisor /var/log/supervisor /etc/passwd ${HOME} ${VIDEO_FOLDER} \ && wget --no-verbose https://github.com/${AUTHORS}/selenium/releases/download/${RELEASE}/selenium-server-${VERSION}.jar \ -O /opt/selenium/selenium-server.jar \ + # CI decides whether to build from the digest it resolved before the build + # started. If the jar moved in between, the image would be published under a + # tag naming a core it does not contain, and the two architecture builds - + # which download independently - could even end up with different cores in one + # manifest. Fail here instead. Anything that is not a sha256 (a local build, or + # an upstream asset that predates the digest field) skips the check and says so. + && case "${SERVER_JAR_DIGEST}" in \ + sha256:*) echo "${SERVER_JAR_DIGEST#sha256:} /opt/selenium/selenium-server.jar" | sha256sum -c - ;; \ + *) echo "Server jar digest not verified (SERVER_JAR_DIGEST=${SERVER_JAR_DIGEST:-none})" ;; \ + esac \ && chgrp -R 0 /opt/selenium ${HOME} ${VIDEO_FOLDER} /opt/selenium/assets /var/run/supervisor /var/log/supervisor \ && chmod -R g=u /opt/selenium ${HOME} ${VIDEO_FOLDER} /opt/selenium/assets /var/run/supervisor /var/log/supervisor \ && setfacl -Rm u:${SEL_USER}:rwx /opt /opt/selenium ${HOME} ${VIDEO_FOLDER} /opt/selenium/assets /var/run/supervisor /var/log/supervisor \ diff --git a/Makefile b/Makefile index 5ae11e015..4e4b796b1 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,11 @@ BASE_VERSION := $(or $(BASE_VERSION),$(BASE_VERSION),4.48.0) BINDING_VERSION := $(or $(BINDING_VERSION),$(BINDING_VERSION),4.48.0) BASE_RELEASE_NIGHTLY := $(or $(BASE_RELEASE_NIGHTLY),$(BASE_RELEASE_NIGHTLY),nightly) BASE_VERSION_NIGHTLY := $(or $(BASE_VERSION_NIGHTLY),$(BASE_VERSION_NIGHTLY),4.49.0-SNAPSHOT) +# sha256 of the server jar the build must end up with, as get-latest-upstream +# resolved it. Empty locally, where the build simply takes whatever the release +# URL serves; CI passes it so a core that moved mid-build fails the build instead +# of shipping under a tag that names the core it was supposed to have. +BASE_ASSET_DIGEST := $(or $(BASE_ASSET_DIGEST),$(BASE_ASSET_DIGEST),) VERSION := $(or $(VERSION),$(VERSION),4.48.0) MVN_SELENIUM_VERSION := $(or $(MVN_SELENIUM_VERSION),$(MVN_SELENIUM_VERSION),latest.release) TAG_VERSION := $(VERSION)-$(BUILD_DATE) @@ -90,6 +95,12 @@ SKIP_BUILD_TARGETS := $(or $(SKIP_BUILD_TARGETS),base hub distributor router ses standalone_all_browsers standalone_docker standalone_kubernetes \ video ffmpeg keda_external_scaler update_go) +# The same list, for the scripts that have to iterate it. generate_release_notes.sh +# lists what a release published, and a second copy of these names in a shell +# script is a copy that drifts. +print_ci_images: + @echo $(CI_IMAGES) + # Push what was just built, so the rest of the run can reuse it. # video does not carry the grid tag: it is built as # $(NAME)/video:$(FFMPEG_TAG_VERSION)-$(BUILD_DATE), and the compose files read it @@ -488,6 +499,7 @@ gen_certs: base: update_go prepare_resources gen_certs cd ./Base && SEL_PASSWD=$(SEL_PASSWD) docker buildx build --platform $(PLATFORMS) $(BUILD_ARGS) --build-arg VERSION=$(BASE_VERSION) --build-arg RELEASE=$(BASE_RELEASE) --build-arg AUTHORS=$(AUTHORS) \ + --build-arg SERVER_JAR_DIGEST=$(BASE_ASSET_DIGEST) \ --secret id=SEL_PASSWD --sbom=true --attest type=provenance,mode=max -t $(NAME)/base:$(TAG_VERSION) . base_nightly: diff --git a/generate_release_notes.sh b/generate_release_notes.sh index b558100fd..698048428 100755 --- a/generate_release_notes.sh +++ b/generate_release_notes.sh @@ -7,29 +7,107 @@ BUILD_DATE=$4 NAMESPACE=${NAME:-selenium} FFMPEG_TAG_VERSION=$(grep FFMPEG_TAG_VERSION Makefile | sed 's/.*,\([^)]*\))/\1/p' | head -n 1) AUTHORS=${AUTHORS:-"SeleniumHQ"} +GHCR_NAMESPACE=${GHCR_NAMESPACE:-ghcr.io/$(echo "${AUTHORS}" | tr '[:upper:]' '[:lower:]')} TAG_VERSION=${GRID_VERSION}-${BUILD_DATE} -echo "" >>release_notes.md +# The tag the images were actually published under. A release publishes +# -, which is the default; the nightly publishes +# :nightly and nothing else, so nightly.yml passes IMAGE_TAG=nightly. Reading a +# tag that was never published is what left every cell of the table below blank. +# +# Video is the one image that does not carry the grid tag on the release path - +# it is built as - - so it gets a tag of its own. +IMAGE_TAG=${IMAGE_TAG:-${TAG_VERSION}} +VIDEO_IMAGE_TAG=${VIDEO_IMAGE_TAG:-${FFMPEG_TAG_VERSION}-${BUILD_DATE}} + +if [ -z "${LATEST_TAG}" ] || [ -z "${HEAD_BRANCH}" ] || [ -z "${GRID_VERSION}" ] || [ -z "${BUILD_DATE}" ]; then + echo "usage: $0 " >&2 + exit 1 +fi + +# `git log ...` with an empty left-hand side is `HEAD...`, which +# on a checkout of that very branch is empty - a silent, changelog-shaped hole in +# the notes. Resolve both ends up front and say which one is missing instead. +for rev in "${LATEST_TAG}" "${HEAD_BRANCH}"; do + if ! git rev-parse --verify --quiet "${rev}^{commit}" >/dev/null; then + echo "cannot resolve '${rev}': the changelog would be empty" >&2 + exit 1 + fi +done + echo "### Changelog" >release_notes.md git --no-pager log "${LATEST_TAG}...${HEAD_BRANCH}" --pretty=format:"* [\`%h\`](http://github.com/seleniumhq/docker-selenium/commit/%H) - %s :: %an" --reverse >>release_notes.md -GRID_REVISION=$(docker run --entrypoint="" --rm ${NAMESPACE}/base:${TAG_VERSION} java -jar /opt/selenium/selenium-server.jar info --version | awk '{print $5}') -CHROME_VERSION=$(docker run --rm ${NAMESPACE}/node-chrome:${TAG_VERSION} google-chrome --version | awk '{print $3}') -CFT_VERSION=$(docker run --rm ${NAMESPACE}/node-chrome-for-testing:${TAG_VERSION} google-chrome --version | awk '{print $5}') -EDGE_VERSION=$(docker run --rm ${NAMESPACE}/node-edge:${TAG_VERSION} microsoft-edge --version | awk '{print $3}') -CHROMEDRIVER_VERSION=$(docker run --rm ${NAMESPACE}/node-chrome:${TAG_VERSION} chromedriver --version | awk '{print $2}') -EDGEDRIVER_VERSION=$(docker run --rm ${NAMESPACE}/node-edge:${TAG_VERSION} msedgedriver --version | awk '{print $4}') -FIREFOX_VERSION=$(docker run --rm ${NAMESPACE}/node-firefox:${TAG_VERSION} firefox --version | awk '{print $3}') -GECKODRIVER_VERSION=$(docker run --rm ${NAMESPACE}/node-firefox:${TAG_VERSION} geckodriver --version | awk 'NR==1{print $2}') -FFMPEG_VERSION=$(docker run --entrypoint="" --rm ${NAMESPACE}/video:${FFMPEG_TAG_VERSION}-${BUILD_DATE} ffmpeg -version | awk '{print $3}' | head -n 1) -RCLONE_VERSION=$(docker run --entrypoint="" --rm ${NAMESPACE}/video:${FFMPEG_TAG_VERSION}-${BUILD_DATE} rclone version | head -n 1 | awk '{print $2}' | tr -d 'v') -JRE_VERSION=$(docker run --entrypoint="" --rm ${NAMESPACE}/base:${TAG_VERSION} java --version | grep -oP '\b\d+\.\d+\.\d+\b' | head -1) -OS_VERSION=$(docker run --entrypoint="" --rm ${NAMESPACE}/base:${TAG_VERSION} cat /etc/os-release | grep PRETTY_NAME | cut -d '"' -f 2) -FIREFOX_ARM64_VERSION=$(docker run --rm --platform linux/arm64 ${NAMESPACE}/node-firefox:${TAG_VERSION} firefox --version | awk '{print $3}') -CHROME_ARM64_VERSION=$(docker run --rm --platform linux/arm64 ${NAMESPACE}/node-chrome:${TAG_VERSION} google-chrome --version | awk '{print $3}') -CHROMEDRIVER_ARM64_VERSION=$(docker run --rm --platform linux/arm64 ${NAMESPACE}/node-chrome:${TAG_VERSION} chromedriver --version | awk '{print $2}') -CHROMIUM_VERSION=$(docker run --rm ${NAMESPACE}/node-chromium:${TAG_VERSION} chromium --version | awk '{print $2}') +# --- versions, read out of the published images ------------------------------- +# +# On the promotion path the deploy job builds nothing, so none of these images is +# in the local store and the `docker run` that reads one pulls it. The browser +# images together are far larger than the runner has free, so each is dropped +# again as soon as its values have been read. Anything that was already in the +# store came from a local build and is left where the rest of the job expects it. +PRESENT_BEFORE=" $(docker images --format '{{.Repository}}:{{.Tag}}' | tr '\n' ' ')" + +drop_image() { + case "${PRESENT_BEFORE}" in + *" $1 "*) return 0 ;; + esac + docker rmi -f "$1" >/dev/null 2>&1 || true +} + +BASE_IMAGE=${NAMESPACE}/base:${IMAGE_TAG} +GRID_REVISION=$(docker run --entrypoint="" --rm ${BASE_IMAGE} java -jar /opt/selenium/selenium-server.jar info --version | awk '{print $5}') +JRE_VERSION=$(docker run --entrypoint="" --rm ${BASE_IMAGE} java --version | grep -oP '\b\d+\.\d+\.\d+\b' | head -1) +OS_VERSION=$(docker run --entrypoint="" --rm ${BASE_IMAGE} cat /etc/os-release | grep PRETTY_NAME | cut -d '"' -f 2) +drop_image ${BASE_IMAGE} + +CHROME_IMAGE=${NAMESPACE}/node-chrome:${IMAGE_TAG} +CHROME_VERSION=$(docker run --rm ${CHROME_IMAGE} google-chrome --version | awk '{print $3}') +CHROMEDRIVER_VERSION=$(docker run --rm ${CHROME_IMAGE} chromedriver --version | awk '{print $2}') +CHROME_ARM64_VERSION=$(docker run --rm --platform linux/arm64 ${CHROME_IMAGE} google-chrome --version | awk '{print $3}') +CHROMEDRIVER_ARM64_VERSION=$(docker run --rm --platform linux/arm64 ${CHROME_IMAGE} chromedriver --version | awk '{print $2}') +drop_image ${CHROME_IMAGE} + +CFT_IMAGE=${NAMESPACE}/node-chrome-for-testing:${IMAGE_TAG} +CFT_VERSION=$(docker run --rm ${CFT_IMAGE} google-chrome --version | awk '{print $5}') +drop_image ${CFT_IMAGE} + +CHROMIUM_IMAGE=${NAMESPACE}/node-chromium:${IMAGE_TAG} +CHROMIUM_VERSION=$(docker run --rm ${CHROMIUM_IMAGE} chromium --version | awk '{print $2}') +drop_image ${CHROMIUM_IMAGE} + +EDGE_IMAGE=${NAMESPACE}/node-edge:${IMAGE_TAG} +EDGE_VERSION=$(docker run --rm ${EDGE_IMAGE} microsoft-edge --version | awk '{print $3}') +EDGEDRIVER_VERSION=$(docker run --rm ${EDGE_IMAGE} msedgedriver --version | awk '{print $4}') +drop_image ${EDGE_IMAGE} + +FIREFOX_IMAGE=${NAMESPACE}/node-firefox:${IMAGE_TAG} +FIREFOX_VERSION=$(docker run --rm ${FIREFOX_IMAGE} firefox --version | awk '{print $3}') +GECKODRIVER_VERSION=$(docker run --rm ${FIREFOX_IMAGE} geckodriver --version | awk 'NR==1{print $2}') +FIREFOX_ARM64_VERSION=$(docker run --rm --platform linux/arm64 ${FIREFOX_IMAGE} firefox --version | awk '{print $3}') +drop_image ${FIREFOX_IMAGE} + +VIDEO_IMAGE=${NAMESPACE}/video:${VIDEO_IMAGE_TAG} +FFMPEG_VERSION=$(docker run --entrypoint="" --rm ${VIDEO_IMAGE} ffmpeg -version | awk '{print $3}' | head -n 1) +RCLONE_VERSION=$(docker run --entrypoint="" --rm ${VIDEO_IMAGE} rclone version | head -n 1 | awk '{print $2}' | tr -d 'v') +drop_image ${VIDEO_IMAGE} + +# A version that came back empty means the image it was read from was not +# published under ${IMAGE_TAG}, or could not be run. Publishing the table anyway +# is how a release ends up looking empty, so name what failed and stop - the +# release step has not run yet, so the previous notes stay up. +missing="" +for version in GRID_REVISION JRE_VERSION OS_VERSION CHROME_VERSION CHROMEDRIVER_VERSION \ + CHROME_ARM64_VERSION CHROMEDRIVER_ARM64_VERSION CFT_VERSION CHROMIUM_VERSION \ + EDGE_VERSION EDGEDRIVER_VERSION FIREFOX_VERSION GECKODRIVER_VERSION \ + FIREFOX_ARM64_VERSION FFMPEG_VERSION RCLONE_VERSION; do + [ -n "${!version}" ] || missing="${missing} ${version}" +done +if [ -n "${missing}" ]; then + echo "could not read from the images published as '${IMAGE_TAG}':${missing}" >&2 + exit 1 +fi + if [[ "${GRID_VERSION}" == *"SNAPSHOT"* ]]; then GRID_RELEASE_TAG="nightly" else @@ -55,25 +133,77 @@ echo "| rclone | ${RCLONE_VERSION} | ${RCLONE_VERSION} |" >>release_notes.md echo "| Java Runtime | ${JRE_VERSION} | ${JRE_VERSION} |" >>release_notes.md echo "| OS | ${OS_VERSION} | ${OS_VERSION} |" >>release_notes.md +# --- what was published ------------------------------------------------------- +# +# `docker images` listed the local store, which the promotion path never fills, +# so both sections below came out as a bare table header. Ask the registry +# instead: the digest it answers with is the one users pull, and it is the same +# check for a build and for a promotion. +# +# The image list comes from the Makefile so the two cannot drift apart. +PUBLISHED_IMAGES=$(make --no-print-directory print_ci_images) +if [ -z "${PUBLISHED_IMAGES}" ]; then + echo "could not read the image list from the Makefile" >&2 + exit 1 +fi + +list_published_images() { + local registry=$1 + local image ref raw digest platforms row + local -a rows=() + local ref_width=5 platform_width=9 + + for image in ${PUBLISHED_IMAGES}; do + if [ "${image}" = "video" ]; then + ref="${registry}/${image}:${VIDEO_IMAGE_TAG}" + else + ref="${registry}/${image}:${IMAGE_TAG}" + fi + if ! raw=$(docker buildx imagetools inspect --raw "${ref}" 2>/dev/null); then + echo "not published: ${ref}" >&2 + continue + fi + # The architectures the manifest list actually covers, so an image that + # quietly lost one is visible in the notes. `unknown` is the attestation + # entry buildx attaches alongside the real ones. + platforms=$(printf '%s' "${raw}" | jq -r '[.manifests[]? | select(.platform.os != "unknown") | "\(.platform.os)/\(.platform.architecture)"] | unique | join(",")') + digest=$(docker buildx imagetools inspect --format '{{.Manifest.Digest}}' "${ref}" 2>/dev/null) + [ ${#ref} -gt ${ref_width} ] && ref_width=${#ref} + [ ${#platforms} -gt ${platform_width} ] && platform_width=${#platforms} + rows+=("${ref}"$'\t'"${platforms:--}"$'\t'"${digest:--}") + done + + if [ ${#rows[@]} -eq 0 ]; then + echo "none of the images are published in ${registry}" >&2 + return 1 + fi + + printf "%-${ref_width}s %-${platform_width}s %s\n" "IMAGE" "PLATFORMS" "DIGEST" + for row in "${rows[@]}"; do + IFS=$'\t' read -r ref platforms digest <<<"${row}" + printf "%-${ref_width}s %-${platform_width}s %s\n" "${ref}" "${platforms}" "${digest}" + done +} + echo "" >>release_notes.md echo "### Published Docker images on [Docker Hub](https://hub.docker.com/u/${NAMESPACE})" >>release_notes.md echo "
" >>release_notes.md echo "Click to see published Docker images" >>release_notes.md echo "" >>release_notes.md echo '```' >>release_notes.md -docker images --filter=reference=${NAMESPACE}'/*:'${FILTER_IMAGE_TAG:-"*"} --format "table {{.ID}}\t{{.Repository}}\t{{.Tag}}\t{{.Size}}" >>release_notes.md +list_published_images "${NAMESPACE}" >>release_notes.md || exit 1 echo '```' >>release_notes.md echo "" >>release_notes.md echo "
" >>release_notes.md echo "" >>release_notes.md -echo "### Published Docker images on [GitHub Container Registry](https://github.com/orgs/SeleniumHQ/packages?repo_name=docker-selenium)" >>release_notes.md +echo "### Published Docker images on [GitHub Container Registry](https://github.com/orgs/${AUTHORS}/packages?repo_name=docker-selenium)" >>release_notes.md echo "
" >>release_notes.md echo "Click to see published Docker images" >>release_notes.md echo "" >>release_notes.md echo '```' >>release_notes.md -docker images --filter=reference=ghcr.io/seleniumhq'/*:'${FILTER_IMAGE_TAG:-"*"} --format "table {{.ID}}\t{{.Repository}}\t{{.Tag}}\t{{.Size}}" >>release_notes.md +list_published_images "${GHCR_NAMESPACE}" >>release_notes.md || exit 1 echo '```' >>release_notes.md echo "" >>release_notes.md echo "
" >>release_notes.md