diff --git a/.dockerignore b/.dockerignore index 8797cc5274e..05dc17082f2 100644 --- a/.dockerignore +++ b/.dockerignore @@ -8,3 +8,4 @@ test/certs/webpki test/certs/.softhsm-tokens .git .gocache +.github diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 61c710e7c1c..1587aec37f6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,16 +10,24 @@ on: - '**' jobs: - push-release: + build-artifacts: strategy: fail-fast: false matrix: - GO_VERSION: - - "1.25.0" - runs-on: ubuntu-24.04 + include: + - GO_VERSION: "1.25.0" + ARCH: "amd64" + RUNNER: "ubuntu-24.04" + - GO_VERSION: "1.25.0" + ARCH: "arm64" + RUNNER: "ubuntu-24.04-arm" + runs-on: ${{ matrix.RUNNER }} permissions: contents: write packages: write + outputs: + version: ${{ steps.version.outputs.version }} + go_version: ${{ matrix.GO_VERSION }} steps: - uses: actions/checkout@v4 with: @@ -29,20 +37,63 @@ jobs: - name: Verify release ancestry run: ./tools/verify-release-ancestry.sh "$GITHUB_SHA" + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Set version output + id: version + run: | + # Use commit timestamp for reproducible builds + COMMIT_TIMESTAMP="$(git show -s --format=%ct HEAD)" + VERSION="${{ matrix.GO_VERSION }}.${COMMIT_TIMESTAMP}" + echo "version=${VERSION}" >> $GITHUB_OUTPUT + - name: Build Boulder container and .deb id: build env: GO_VERSION: ${{ matrix.GO_VERSION }} + DOCKER_DEFAULT_PLATFORM: linux/${{ matrix.ARCH }} run: ./tools/container-build.sh - - name: Tag Boulder container - run: docker tag boulder "ghcr.io/letsencrypt/boulder:${{ github.ref_name }}-go${{ matrix.GO_VERSION }}" + - name: Export container image for multi-platform + run: | + VERSION="${{ steps.version.outputs.version }}" + docker save "boulder:${VERSION}-${{ matrix.ARCH }}" | gzip > "boulder-image-${{ matrix.ARCH }}.tar.gz" + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: boulder-${{ matrix.ARCH }} + path: | + boulder*.deb + boulder*.tar.gz + boulder-image-${{ matrix.ARCH }}.tar.gz + retention-days: 1 - - name: Compute checksums - id: checksums - # The files listed on this line must be identical to the files uploaded - # in the last step. - run: sha256sum boulder*.deb boulder*.tar.gz >| boulder-${{ matrix.GO_VERSION }}.$(date +%s)-$(git rev-parse --short=8 HEAD).checksums.txt + create-release: + needs: build-artifacts + runs-on: ubuntu-24.04 + permissions: + contents: write + packages: write + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + fetch-depth: '0' + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts/ + + - name: Prepare release files + run: | + # Move all .deb and .tar.gz files to current directory + find artifacts/ -name "*.deb" -o -name "*.tar.gz" | grep -v "boulder-image-" | xargs -I {} cp {} . + + # Compute checksums for release files only + sha256sum boulder*.deb boulder*.tar.gz >| boulder-${{ needs.build-artifacts.outputs.version }}.$(git rev-parse --short=8 HEAD).checksums.txt - name: Create release env: @@ -57,16 +108,68 @@ jobs: # https://cli.github.com/manual/gh_release_upload run: gh release upload "${GITHUB_REF_NAME}" boulder*.deb boulder*.tar.gz boulder*.checksums.txt - - name: Build ct-test-srv container - run: docker buildx build . --build-arg "GO_VERSION=${{ matrix.GO_VERSION }}" -f test/ct-test-srv/Dockerfile -t "ghcr.io/letsencrypt/ct-test-srv:${{ github.ref_name }}-go${{ matrix.GO_VERSION }}" + push-images: + needs: build-artifacts + runs-on: ubuntu-24.04 + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Download container images + uses: actions/download-artifact@v4 + with: + path: artifacts/ + + - name: Load and tag images + run: | + # Load architecture-specific images + docker load < artifacts/boulder-amd64/boulder-image-amd64.tar.gz + docker load < artifacts/boulder-arm64/boulder-image-arm64.tar.gz + + VERSION="${{ needs.build-artifacts.outputs.version }}" + BASE_TAG="ghcr.io/${{ github.repository_owner }}/boulder:${{ github.ref_name }}-go${VERSION}" + + # Tag with architecture-specific tags for manifest creation + docker tag "boulder:${VERSION}-amd64" "${BASE_TAG}-amd64" + docker tag "boulder:${VERSION}-arm64" "${BASE_TAG}-arm64" - name: Login to ghcr.io run: printenv GITHUB_TOKEN | docker login ghcr.io -u "${{ github.actor }}" --password-stdin env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Push Boulder container - run: docker push "ghcr.io/letsencrypt/boulder:${{ github.ref_name }}-go${{ matrix.GO_VERSION }}" + - name: Push architecture-specific images + run: | + VERSION="${{ needs.build-artifacts.outputs.version }}" + BASE_TAG="ghcr.io/${{ github.repository_owner }}/boulder:${{ github.ref_name }}-go${VERSION}" + docker push "${BASE_TAG}-amd64" + docker push "${BASE_TAG}-arm64" + + - name: Create and push multi-platform manifest + run: | + VERSION="${{ needs.build-artifacts.outputs.version }}" + BASE_TAG="ghcr.io/${{ github.repository_owner }}/boulder:${{ github.ref_name }}-go${VERSION}" + + docker buildx imagetools create -t "${BASE_TAG}" \ + "${BASE_TAG}-amd64" \ + "${BASE_TAG}-arm64" - - name: Push ct-test-srv container - run: docker push "ghcr.io/letsencrypt/ct-test-srv:${{ github.ref_name }}-go${{ matrix.GO_VERSION }}" + - name: Build and push ct-test-srv multi-platform + run: | + VERSION="${{ needs.build-artifacts.outputs.version }}" + docker buildx build . \ + --build-arg "GO_VERSION=${{ needs.build-artifacts.outputs.go_version }}" \ + -f test/ct-test-srv/Dockerfile \ + --platform linux/amd64,linux/arm64 \ + -t "ghcr.io/${{ github.repository_owner }}/ct-test-srv:${{ github.ref_name }}-go${VERSION}" \ + --push diff --git a/.github/workflows/try-release.yml b/.github/workflows/try-release.yml index 1cd6270b105..86a3d4a93d0 100644 --- a/.github/workflows/try-release.yml +++ b/.github/workflows/try-release.yml @@ -18,18 +18,27 @@ jobs: strategy: fail-fast: false matrix: - GO_VERSION: - - "1.25.0" - runs-on: ubuntu-24.04 + include: + - GO_VERSION: "1.25.0" + ARCH: "amd64" + RUNNER: "ubuntu-24.04" + - GO_VERSION: "1.25.0" + ARCH: "arm64" + RUNNER: "ubuntu-24.04-arm" + runs-on: ${{ matrix.RUNNER }} steps: - uses: actions/checkout@v4 with: persist-credentials: false + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Build Boulder container and .deb id: build env: GO_VERSION: ${{ matrix.GO_VERSION }} + DOCKER_DEFAULT_PLATFORM: linux/${{ matrix.ARCH }} run: ./tools/container-build.sh - name: Compute checksums @@ -47,4 +56,4 @@ jobs: run: cat boulder*.checksums.txt - name: Build ct-test-srv container - run: docker buildx build . --build-arg "GO_VERSION=${{ matrix.GO_VERSION }}" -f test/ct-test-srv/Dockerfile -t "ghcr.io/letsencrypt/ct-test-srv:${{ github.sha }}-go${{ matrix.GO_VERSION }}" + run: docker buildx build . --build-arg "GO_VERSION=${{ matrix.GO_VERSION }}" -f test/ct-test-srv/Dockerfile -t "ghcr.io/${{ github.repository_owner }}/ct-test-srv:${{ github.sha }}-go${{ matrix.GO_VERSION }}-${{ matrix.ARCH }}" --load diff --git a/.gitignore b/.gitignore index 769b54767cf..28c283ed383 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ *.a *.so *.pyc +boulder-*.deb +boulder-*.tar.gz # Folders _obj diff --git a/Containerfile b/Containerfile index 92276aa6d30..be6017ba5e5 100644 --- a/Containerfile +++ b/Containerfile @@ -1,18 +1,27 @@ -# This builds Boulder in a Docker container, then creates an image -# containing just the built Boulder binaries plus some ancillary -# files that are useful for predeployment testing. -FROM docker.io/ubuntu:24.04 AS builder +# This multi-stage build first builds Boulder in a container, then +# creates a minimal image containing the built Boulder binaries and +# ancillary files for pre-deployment testing. +ARG BUILDER_BASE=docker.io/ubuntu:24.04 +ARG FINAL_BASE=docker.io/ubuntu:24.04 + +FROM ${BUILDER_BASE} AS builder ARG COMMIT_ID ARG GO_VERSION ARG VERSION +ARG TARGETPLATFORM ENV DEBIAN_FRONTEND=noninteractive RUN apt-get --assume-yes --no-install-recommends --update install \ ca-certificates curl gcc git gnupg2 libc6-dev COPY tools/fetch-and-verify-go.sh /tmp -RUN /tmp/fetch-and-verify-go.sh ${GO_VERSION} +RUN case "${TARGETPLATFORM}" in \ + "linux/amd64") PLATFORM="linux-amd64" ;; \ + "linux/arm64") PLATFORM="linux-arm64" ;; \ + *) echo "Unsupported platform: ${TARGETPLATFORM}" && exit 1 ;; \ + esac && \ + /tmp/fetch-and-verify-go.sh ${GO_VERSION} ${PLATFORM} RUN tar -C /opt -xzf go.tar.gz ENV PATH="/opt/go/bin:${PATH}" @@ -26,7 +35,7 @@ RUN go install \ -mod=vendor \ ./... -FROM docker.io/ubuntu:24.04 +FROM ${FINAL_BASE} ARG VERSION diff --git a/tools/container-build.sh b/tools/container-build.sh index 735332b6a67..627a399710d 100755 --- a/tools/container-build.sh +++ b/tools/container-build.sh @@ -7,36 +7,61 @@ set -ex -# Without this, `go install` produces: -# # runtime/cgo -# gcc: error: unrecognized command-line option '-m64' -if [ "$(uname -m)" = "arm64" ]; then - export DOCKER_DEFAULT_PLATFORM=linux/amd64 -fi - if [ -z "${GO_VERSION}" ] ; then echo "GO_VERSION not set" exit 1 fi -ARCH="$(uname -m)" COMMIT_ID="$(git rev-parse --short=8 HEAD)" -VERSION="${GO_VERSION}.$(date +%s)" +# Use commit timestamp for reproducible builds +COMMIT_TIMESTAMP="$(git show -s --format=%ct HEAD)" +VERSION="${GO_VERSION}.${COMMIT_TIMESTAMP}" + +# Determine what architecture to build for +if [ -n "${DOCKER_DEFAULT_PLATFORM}" ]; then + # User specified a platform override + PLATFORM="${DOCKER_DEFAULT_PLATFORM}" +else + # No override - detect from current machine + MACHINE_ARCH=$(uname -m) + case "${MACHINE_ARCH}" in + x86_64) PLATFORM="linux/amd64" ;; + aarch64) PLATFORM="linux/arm64" ;; + arm64) PLATFORM="linux/arm64" ;; + *) echo "Unsupported machine architecture: ${MACHINE_ARCH}" && exit 1 ;; + esac +fi + +# Convert platform to short architecture name for file naming +case "${PLATFORM}" in + linux/amd64) ARCH="amd64" ;; + linux/arm64) ARCH="arm64" ;; + *) echo "Unsupported platform: ${PLATFORM}" && exit 1 ;; +esac +# Create platform-specific image +# Keep generic tags for standalone use docker buildx build \ --file Containerfile \ + --platform "$PLATFORM" \ --build-arg "COMMIT_ID=${COMMIT_ID}" \ --build-arg "GO_VERSION=${GO_VERSION}" \ --build-arg "VERSION=${VERSION}" \ + --tag "boulder:${VERSION}-${ARCH}" \ --tag "boulder:${VERSION}" \ - --tag "boulder:${COMMIT_ID}" \ - --tag boulder \ + --tag "boulder" \ + --load \ + --progress=plain \ . -docker run boulder tar -C /opt/boulder -cpz . > "./boulder-${VERSION}-${COMMIT_ID}.${ARCH}.tar.gz" . -# Produces e.g. boulder-1.25.0.1754519595-591c0545.x86_64.deb +# Create tarball +docker run "boulder" tar -C /opt/boulder -cpz . \ + > "./boulder-${VERSION}-${COMMIT_ID}.${ARCH}.tar.gz" + +# Create .deb package docker run -v .:/boulderrepo \ - -e "COMMIT_ID=$(git rev-parse --short=8 HEAD)" \ - -e "VERSION=${VERSION}" \ - boulder \ - /boulderrepo/tools/make-deb.sh + -e "COMMIT_ID=${COMMIT_ID}" \ + -e "VERSION=${VERSION}" \ + -e "ARCH=${ARCH}" \ + "boulder" \ + /boulderrepo/tools/make-deb.sh diff --git a/tools/make-deb.sh b/tools/make-deb.sh index 56d0a4ae899..d39ff165f65 100755 --- a/tools/make-deb.sh +++ b/tools/make-deb.sh @@ -1,27 +1,30 @@ #!/usr/bin/env bash # -# Produce a .deb from a built Boulder plus helper files. +# Produce a .deb package from a built Boulder container. # -# This script expects to run on Ubuntu, as configured on GitHub Actions runners -# (with curl, make, and git installed). +# This script is executed inside the Boulder Docker container by container-build.sh. +# It packages the Boulder binary and assets into a Debian package for distribution. # # -e Stops execution in the instance of a command or pipeline error. # -u Treat unset variables as an error and exit immediately. set -eu cd "$(realpath -- "$(dirname -- "$0")")/.." -BUILD="$(mktemp -d)" +if [ -z "${VERSION:-}" ]; then echo "VERSION not set"; exit 1; fi +if [ -z "${COMMIT_ID:-}" ]; then echo "COMMIT_ID not set"; exit 1; fi +if [ -z "${ARCH:-}" ]; then echo "ARCH not set"; exit 1; fi +BUILD="$(mktemp -d)" mkdir -p "${BUILD}/opt" cp -a /opt/boulder "${BUILD}/opt/boulder" mkdir -p "${BUILD}/DEBIAN" -cat > "${BUILD}/DEBIAN/control" <<-EOF +cat >"${BUILD}/DEBIAN/control" <<-EOF Package: boulder Version: 1:${VERSION} License: Mozilla Public License v2.0 Vendor: ISRG -Architecture: amd64 +Architecture: ${ARCH} Maintainer: Community Section: default Priority: extra @@ -29,4 +32,4 @@ Homepage: https://github.com/letsencrypt/boulder Description: Boulder is an ACME-compatible X.509 Certificate Authority EOF -dpkg-deb -Zgzip -b "${BUILD}" "boulder-${VERSION}-${COMMIT_ID}.x86_64.deb" +dpkg-deb -Zgzip -b "${BUILD}" "boulder-${VERSION}-${COMMIT_ID}.${ARCH}.deb"