From e92669a56835e9fccacde82f405b187d20113487 Mon Sep 17 00:00:00 2001 From: Alex Chapin Date: Tue, 4 Aug 2026 18:06:55 -0400 Subject: [PATCH 1/4] feat: add multi-architecture support for OpenStudio Docker images --- .github/workflows/docker-openstudio.yml | 65 +++++++++++++++--- Dockerfile | 11 +++ deploy_docker.sh | 90 ++++++++++--------------- get_image_tags.sh | 36 ++++++++++ merge_manifests.sh | 42 ++++++++++++ 5 files changed, 182 insertions(+), 62 deletions(-) create mode 100755 get_image_tags.sh create mode 100755 merge_manifests.sh diff --git a/.github/workflows/docker-openstudio.yml b/.github/workflows/docker-openstudio.yml index f62b10c..0ea0dd5 100644 --- a/.github/workflows/docker-openstudio.yml +++ b/.github/workflows/docker-openstudio.yml @@ -50,9 +50,11 @@ jobs: setup: runs-on: ubuntu-24.04 outputs: - openstudio_version: ${{ steps.resolve.outputs.openstudio_version }} - openstudio_sha: ${{ steps.resolve.outputs.openstudio_sha }} - openstudio_version_ext: ${{ steps.resolve.outputs.openstudio_version_ext }} + openstudio_version: ${{ steps.resolve.outputs.openstudio_version }} + openstudio_sha: ${{ steps.resolve.outputs.openstudio_sha }} + openstudio_version_ext: ${{ steps.resolve.outputs.openstudio_version_ext }} + openstudio_download_url: ${{ steps.resolve.outputs.openstudio_download_url }} + openstudio_download_url_arm64: ${{ steps.resolve.outputs.openstudio_download_url_arm64 }} steps: - name: Resolve version id: resolve @@ -72,18 +74,43 @@ jobs: EXT="${{ inputs.openstudio_version_ext }}" fi + SHA10="${SHA:0:10}" + if [ -n "${EXT}" ]; then + ESC_VERSION=$(echo "${VERSION}${EXT}" | sed 's/+/%2B/g') + DOWNLOAD_URL="https://openstudio-ci-builds.s3-us-west-2.amazonaws.com/develop/OpenStudio-${ESC_VERSION}%2B${SHA}-Ubuntu-24.04-x86_64.deb" + # S3 does not publish arm64 builds; fall back to the GitHub release asset naming + DOWNLOAD_URL_ARM64="https://github.com/NatLabRockies/OpenStudio/releases/download/v${VERSION}/OpenStudio-${ESC_VERSION}%2B${SHA10}-Ubuntu-24.04-arm64.deb" + else + DOWNLOAD_URL="https://github.com/NatLabRockies/OpenStudio/releases/download/v${VERSION}/OpenStudio-${VERSION}%2B${SHA10}-Ubuntu-24.04-x86_64.deb" + DOWNLOAD_URL_ARM64="https://github.com/NatLabRockies/OpenStudio/releases/download/v${VERSION}/OpenStudio-${VERSION}%2B${SHA10}-Ubuntu-24.04-arm64.deb" + fi + echo "openstudio_version=${VERSION}" >> "$GITHUB_OUTPUT" echo "openstudio_sha=${SHA}" >> "$GITHUB_OUTPUT" echo "openstudio_version_ext=${EXT}" >> "$GITHUB_OUTPUT" + echo "openstudio_download_url=${DOWNLOAD_URL}" >> "$GITHUB_OUTPUT" + echo "openstudio_download_url_arm64=${DOWNLOAD_URL_ARM64}" >> "$GITHUB_OUTPUT" echo "Resolved: OpenStudio ${VERSION}${EXT} (${SHA})" + echo "Resolved amd64 URL: ${DOWNLOAD_URL}" + echo "Resolved arm64 URL: ${DOWNLOAD_URL_ARM64}" docker: needs: setup - runs-on: ubuntu-24.04 + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - arch: amd64 + os: ubuntu-24.04 + - arch: arm64 + os: ubuntu-24.04-arm env: - OPENSTUDIO_VERSION: ${{ needs.setup.outputs.openstudio_version }} - OPENSTUDIO_SHA: ${{ needs.setup.outputs.openstudio_sha }} - OPENSTUDIO_VERSION_EXT: ${{ needs.setup.outputs.openstudio_version_ext }} + DEPLOY_ARCH: ${{ matrix.arch }} + OPENSTUDIO_VERSION: ${{ needs.setup.outputs.openstudio_version }} + OPENSTUDIO_SHA: ${{ needs.setup.outputs.openstudio_sha }} + OPENSTUDIO_VERSION_EXT: ${{ needs.setup.outputs.openstudio_version_ext }} + OPENSTUDIO_DOWNLOAD_URL: ${{ matrix.arch == 'arm64' && needs.setup.outputs.openstudio_download_url_arm64 || needs.setup.outputs.openstudio_download_url }} steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 @@ -113,10 +140,32 @@ jobs: env: DOCKER_PASS: ${{ secrets.DOCKER_PASS }} DOCKER_USER: ${{ secrets.DOCKER_USER }} + DOCKER_MANUAL_IMAGE_TAG: ${{ inputs.docker_manual_image_tag }} + + docker-manifest: + needs: [setup, docker] + runs-on: ubuntu-24.04 + if: ${{ github.event_name != 'pull_request' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/custom_branch_name') && !(github.event_name == 'workflow_dispatch' && inputs.apptainer_only) }} + env: + OPENSTUDIO_VERSION: ${{ needs.setup.outputs.openstudio_version }} + OPENSTUDIO_SHA: ${{ needs.setup.outputs.openstudio_sha }} + OPENSTUDIO_VERSION_EXT: ${{ needs.setup.outputs.openstudio_version_ext }} + steps: + - uses: actions/checkout@v4 + + - name: merge multi-arch manifests + shell: bash + run: | + set -euo pipefail + ./merge_manifests.sh + env: + DOCKER_PASS: ${{ secrets.DOCKER_PASS }} + DOCKER_USER: ${{ secrets.DOCKER_USER }} + DOCKER_MANUAL_IMAGE_TAG: ${{ inputs.docker_manual_image_tag }} apptainer: runs-on: ubuntu-24.04 - needs: [setup, docker] + needs: [setup, docker, docker-manifest] if: ${{ github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/custom_branch_name' }} env: OPENSTUDIO_VERSION: ${{ needs.setup.outputs.openstudio_version }} diff --git a/Dockerfile b/Dockerfile index fcc555e..b56d49e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,9 @@ LABEL maintainer="Nicholas Long nicholas.long@nrel.gov" ARG OPENSTUDIO_VERSION=3.11.0 ARG OPENSTUDIO_VERSION_EXT="-rc1" ARG OPENSTUDIO_SHA="dee62bf9dd" +# Automatically populated by BuildKit during multi-arch builds (amd64 / arm64). +# Used to select the correct OpenStudio .deb package when OPENSTUDIO_DOWNLOAD_URL is not provided. +ARG TARGETARCH # If OPENSTUDIO_DOWNLOAD_URL is not provided, construct a reasonable default using the # OpenStudio CI S3 pattern. Users can override by passing --build-arg OPENSTUDIO_DOWNLOAD_URL=... ARG OPENSTUDIO_DOWNLOAD_URL="" @@ -33,11 +36,19 @@ RUN apt-get update && apt-get install -y \ sudo \ && if [ -z "${OPENSTUDIO_DOWNLOAD_URL}" ]; then \ ESC_VERSION=$(echo "${OPENSTUDIO_VERSION}${OPENSTUDIO_VERSION_EXT}" | sed 's/+/%2B/g'); \ + if [ "${TARGETARCH}" = "arm64" ]; then \ + # S3 does not publish arm64 builds; use the GitHub release asset (named -arm64). + # Mirror the workflow's setup job: GitHub URLs use the 10-char SHA. + SHA10=$(echo "${OPENSTUDIO_SHA}" | cut -c1-10); \ + OPENSTUDIO_DOWNLOAD_URL="https://github.com/NatLabRockies/OpenStudio/releases/download/v${OPENSTUDIO_VERSION}/OpenStudio-${ESC_VERSION}%2B${SHA10}-Ubuntu-24.04-arm64.deb"; \ + else \ + # amd64: BuildKit TARGETARCH is "amd64" but the S3 asset is named -x86_64. if [ -n "${OPENSTUDIO_SHA}" ]; then \ OPENSTUDIO_DOWNLOAD_URL="https://openstudio-ci-builds.s3.amazonaws.com/develop/OpenStudio-${ESC_VERSION}%2B${OPENSTUDIO_SHA}-Ubuntu-24.04-x86_64.deb"; \ else \ OPENSTUDIO_DOWNLOAD_URL="https://openstudio-ci-builds.s3.amazonaws.com/develop/OpenStudio-${ESC_VERSION}-Ubuntu-24.04-x86_64.deb"; \ fi; \ + fi; \ fi \ && echo "OpenStudio Package Download URL is ${OPENSTUDIO_DOWNLOAD_URL}" \ && curl -SLO "$OPENSTUDIO_DOWNLOAD_URL" \ diff --git a/deploy_docker.sh b/deploy_docker.sh index 37342d0..90250dd 100755 --- a/deploy_docker.sh +++ b/deploy_docker.sh @@ -1,61 +1,43 @@ #!/usr/bin/env bash -IMAGETAG=${OPENSTUDIO_VERSION}${OPENSTUDIO_VERSION_EXT} -echo "default image tag would be $IMAGETAG" -IMAGETAG=skip -DOCKER_REPO=${DOCKER_REPO:-nrel/openstudio} - -# Check branch name for correct tagging -if [ "${GITHUB_REF}" == "refs/heads/develop" ]; then - IMAGETAG="develop" -elif [ "${GITHUB_REF}" == "refs/heads/2.9.X-LTS" ]; then - IMAGETAG="2.9.X-LTS" -elif [ "${GITHUB_REF}" == "refs/heads/master" ]; then - # Retrieve the version number from rails - IMAGETAG=${OPENSTUDIO_VERSION}${OPENSTUDIO_VERSION_EXT} -# Uncomment and set branch name for custom builds. -elif [ "${GITHUB_REF}" == "refs/heads/custom_branch_name" ]; then - IMAGETAG="experimental" -elif [ "${DOCKER_MANUAL_IMAGE_TAG}" == "develop" ]; then - IMAGETAG="develop" +# Builds the locally-built image, tags it with an arch suffix, and pushes it. +# The canonical (multi-arch) tags are created later by merge_manifests.sh. +# +# Required env: DOCKER_USER, DOCKER_PASS, OPENSTUDIO_VERSION, OPENSTUDIO_VERSION_EXT +# Optional env: DEPLOY_ARCH (amd64|arm64, default amd64), DOCKER_MANUAL_IMAGE_TAG +set -euo pipefail + +source "$(dirname "$0")/get_image_tags.sh" +DEPLOY_ARCH=${DEPLOY_ARCH:-amd64} + +# GITHUB_BASE_REF is only set on Pull Request events. Do not build those +if [ "${IMAGETAG}" == "skip" ] || [ -n "${GITHUB_BASE_REF:-}" ]; then + echo "Not on a deployable branch, this is a pull request or has been explicitly skipped" + exit 0 fi -# Check if this is a manual installer GH action -if [ ! -z "${DOCKER_MANUAL_IMAGE_TAG}" ]; then - if [ "${DOCKER_MANUAL_IMAGE_TAG}" == "develop" ]; then - IMAGETAG="develop" - elif [[ "${DOCKER_MANUAL_IMAGE_TAG}" =~ ^[0-9]+\.[0-9]+\.[0-9]+.*$ ]]; then - IMAGETAG="${DOCKER_MANUAL_IMAGE_TAG}" - else - IMAGETAG="dev-${DOCKER_MANUAL_IMAGE_TAG}" - fi +echo "Tagging image as ${IMAGETAG}-${DEPLOY_ARCH} and pushing to ${DOCKER_REPO}" + +echo "$DOCKER_PASS" | docker login -u "$DOCKER_USER" --password-stdin + +# Push the arch-suffixed image. The canonical tag is assembled later by the +# manifest job so that parallel amd64/arm64 pushes never clobber each other. +ARCH_TAG="${DOCKER_REPO}:${IMAGETAG}-${DEPLOY_ARCH}" +docker tag openstudio:latest "${ARCH_TAG}" +docker push "${ARCH_TAG}" + +# If on develop branch, also push the develop tag pointing to this image +if [ "${IMAGETAG}" == "develop" ]; then + docker tag openstudio:latest "${DOCKER_REPO}:develop-${DEPLOY_ARCH}" + docker push "${DOCKER_REPO}:develop-${DEPLOY_ARCH}" fi -# GITHUB_BASE_REF is only set on Pull Request events. Do not build those -if [ "${IMAGETAG}" != "skip" ] && [[ -z "${GITHUB_BASE_REF}" ]]; then - echo "Tagging image as $IMAGETAG and pushing to ${DOCKER_REPO}" - - echo "$DOCKER_PASS" | docker login -u "$DOCKER_USER" --password-stdin - # Tag versioned image - docker tag openstudio:latest ${DOCKER_REPO}:$IMAGETAG; (( exit_status = exit_status || $? )) - - # Only update and push 'latest' if this is a stable release (no extension) - if [ -z "${OPENSTUDIO_VERSION_EXT}" ]; then - echo "Stable release detected. Updating and pushing '${DOCKER_REPO}:latest'" - docker tag openstudio:latest ${DOCKER_REPO}:latest; (( exit_status = exit_status || $? )) - docker push ${DOCKER_REPO}:latest; (( exit_status = exit_status || $? )) - else - echo "Pre-release detected (extension: '${OPENSTUDIO_VERSION_EXT}'). Skipping 'latest' tag update." - fi - - # Push versioned tag - docker push ${DOCKER_REPO}:$IMAGETAG; (( exit_status = exit_status || $? )) - # If on develop branch, also push the develop tag pointing to this image - if [ "${IMAGETAG}" == "develop" ] || [ "${GITHUB_REF}" == "refs/heads/develop" ]; then - docker tag openstudio:latest ${DOCKER_REPO}:develop; (( exit_status = exit_status || $? )) - docker push ${DOCKER_REPO}:develop; (( exit_status = exit_status || $? )) - fi - - exit $exit_status +# Only update and push 'latest' if this is a stable release (no extension) +if [ -z "${OPENSTUDIO_VERSION_EXT}" ]; then + echo "Stable release detected. Updating and pushing '${DOCKER_REPO}:latest-${DEPLOY_ARCH}'" + docker tag openstudio:latest "${DOCKER_REPO}:latest-${DEPLOY_ARCH}" + docker push "${DOCKER_REPO}:latest-${DEPLOY_ARCH}" else - echo "Not on a deployable branch, this is a pull request or has been explicity skipped" + echo "Pre-release detected (extension: '${OPENSTUDIO_VERSION_EXT}'). Skipping 'latest' tag update." fi + +echo "Done pushing ${DEPLOY_ARCH} artifacts for ${IMAGETAG}" diff --git a/get_image_tags.sh b/get_image_tags.sh new file mode 100755 index 0000000..10d28bc --- /dev/null +++ b/get_image_tags.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# Determines the image tag to use based on the branch and manual-tag inputs. +# This file is sourced by deploy_docker.sh and merge_manifests.sh; it sets +# IMAGETAG (or "skip" when nothing should be deployed/merged) and DOCKER_REPO. +# +# Usage: source "$(dirname "$0")/get_image_tags.sh" +set -euo pipefail + +IMAGETAG=skip +DOCKER_REPO=${DOCKER_REPO:-nrel/openstudio} + +# Check branch name for correct tagging +if [ "${GITHUB_REF}" == "refs/heads/develop" ]; then + IMAGETAG="develop" +elif [ "${GITHUB_REF}" == "refs/heads/2.9.X-LTS" ]; then + IMAGETAG="2.9.X-LTS" +elif [ "${GITHUB_REF}" == "refs/heads/master" ]; then + # Retrieve the version number from the workflow env + IMAGETAG=${OPENSTUDIO_VERSION}${OPENSTUDIO_VERSION_EXT} +# Uncomment and set branch name for custom builds. +elif [ "${GITHUB_REF}" == "refs/heads/custom_branch_name" ]; then + IMAGETAG="experimental" +elif [ "${DOCKER_MANUAL_IMAGE_TAG}" == "develop" ]; then + IMAGETAG="develop" +fi + +# Check if this is a manual installer GH action +if [ ! -z "${DOCKER_MANUAL_IMAGE_TAG:-}" ]; then + if [ "${DOCKER_MANUAL_IMAGE_TAG}" == "develop" ]; then + IMAGETAG="develop" + elif [[ "${DOCKER_MANUAL_IMAGE_TAG}" =~ ^[0-9]+\.[0-9]+\.[0-9]+.*$ ]]; then + IMAGETAG="${DOCKER_MANUAL_IMAGE_TAG}" + else + IMAGETAG="dev-${DOCKER_MANUAL_IMAGE_TAG}" + fi +fi diff --git a/merge_manifests.sh b/merge_manifests.sh new file mode 100755 index 0000000..cd29c63 --- /dev/null +++ b/merge_manifests.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# Merges the arch-suffixed images pushed by deploy_docker.sh into canonical +# multi-arch manifests (e.g. nrel/openstudio:3.10.0-amd64 + -arm64 -> :3.10.0). +# Requires docker buildx (bundled with Docker CLI 19.03+). +# +# Required env: DOCKER_USER, DOCKER_PASS, OPENSTUDIO_VERSION, OPENSTUDIO_VERSION_EXT +# Optional env: DOCKER_MANUAL_IMAGE_TAG +set -euo pipefail + +source "$(dirname "$0")/get_image_tags.sh" + +if [ "${IMAGETAG}" == "skip" ] || [ -n "${GITHUB_BASE_REF:-}" ]; then + echo "Nothing to merge — not on a deployable branch" + exit 0 +fi + +echo "$DOCKER_PASS" | docker login -u "$DOCKER_USER" --password-stdin + +merge() { + local canonical=$1 + echo "Creating multi-arch manifest ${DOCKER_REPO}:${canonical}" + docker buildx imagetools create \ + -t "${DOCKER_REPO}:${canonical}" \ + "${DOCKER_REPO}:${canonical}-amd64" \ + "${DOCKER_REPO}:${canonical}-arm64" +} + +merge "${IMAGETAG}" + +if [ "${IMAGETAG}" == "develop" ]; then + merge "develop" +fi + +# Only update 'latest' if this is a stable release (no extension) +if [ -z "${OPENSTUDIO_VERSION_EXT}" ]; then + echo "Stable release detected. Updating multi-arch 'latest'." + merge "latest" +else + echo "Pre-release detected (extension: '${OPENSTUDIO_VERSION_EXT}'). Skipping 'latest' manifest." +fi + +echo "Done merging multi-arch manifests for ${IMAGETAG}" From 80da4b50eb66c04af1ffc799686cb918459d53cc Mon Sep 17 00:00:00 2001 From: Alex Chapin Date: Tue, 4 Aug 2026 18:31:55 -0400 Subject: [PATCH 2/4] fix: revert OpenStudio version and SHA to previous stable values --- .github/workflows/docker-openstudio.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docker-openstudio.yml b/.github/workflows/docker-openstudio.yml index 0ea0dd5..20ed98c 100644 --- a/.github/workflows/docker-openstudio.yml +++ b/.github/workflows/docker-openstudio.yml @@ -11,11 +11,11 @@ on: openstudio_version: description: 'OpenStudio version (e.g. 3.10.0)' required: true - default: '3.11.0' + default: '3.10.0' openstudio_sha: description: 'OpenStudio git SHA' required: true - default: 'dee62bf9dd' + default: '86d7e215a1' openstudio_version_ext: description: 'Version extension (e.g. -rc1). Leave blank for a final release. Default: -rc1' required: false @@ -38,10 +38,9 @@ concurrency: env: USE_TESTING_TIMEOUTS: "true" - OPENSTUDIO_VERSION: 3.11.0 - OPENSTUDIO_SHA: dee62bf9dd - OPENSTUDIO_VERSION_EXT: "-rc1" - OPENSTUDIO_DOWNLOAD_URL: "https://openstudio-ci-builds.s3-us-west-2.amazonaws.com/develop/OpenStudio-3.11.0-rc1%2Bdee62bf9dd-Ubuntu-24.04-x86_64.deb" + OPENSTUDIO_VERSION: 3.10.0 + OPENSTUDIO_SHA: 86d7e215a1 + OPENSTUDIO_VERSION_EXT: "" permissions: contents: read From ab2f558b0f0f3914acb6af6c6bfb8eb57ca19679 Mon Sep 17 00:00:00 2001 From: Alex Chapin Date: Tue, 4 Aug 2026 18:53:34 -0400 Subject: [PATCH 3/4] feat: add OPENSTUDIO_DOWNLOAD_URL as a build argument in Dockerfile --- .github/workflows/docker-openstudio.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/docker-openstudio.yml b/.github/workflows/docker-openstudio.yml index 20ed98c..2009c69 100644 --- a/.github/workflows/docker-openstudio.yml +++ b/.github/workflows/docker-openstudio.yml @@ -124,6 +124,7 @@ jobs: docker build -t openstudio:latest \ --build-arg OPENSTUDIO_VERSION=$OPENSTUDIO_VERSION \ --build-arg OPENSTUDIO_SHA=$OPENSTUDIO_SHA \ + --build-arg OPENSTUDIO_DOWNLOAD_URL=$OPENSTUDIO_DOWNLOAD_URL \ --build-arg OPENSTUDIO_VERSION_EXT=$OPENSTUDIO_VERSION_EXT . docker run openstudio:latest openstudio openstudio_version docker run openstudio:latest /usr/local/openstudio-$OPENSTUDIO_VERSION/Radiance/bin/rtrace -version From 6674bc60ae6b4ca896872c29eacfe2dbbb2f1054 Mon Sep 17 00:00:00 2001 From: Alex Chapin Date: Tue, 4 Aug 2026 19:50:17 -0400 Subject: [PATCH 4/4] fix: skip Radiance rtrace test for arm64 architecture --- .github/workflows/docker-openstudio.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker-openstudio.yml b/.github/workflows/docker-openstudio.yml index 2009c69..e52495c 100644 --- a/.github/workflows/docker-openstudio.yml +++ b/.github/workflows/docker-openstudio.yml @@ -127,7 +127,11 @@ jobs: --build-arg OPENSTUDIO_DOWNLOAD_URL=$OPENSTUDIO_DOWNLOAD_URL \ --build-arg OPENSTUDIO_VERSION_EXT=$OPENSTUDIO_VERSION_EXT . docker run openstudio:latest openstudio openstudio_version - docker run openstudio:latest /usr/local/openstudio-$OPENSTUDIO_VERSION/Radiance/bin/rtrace -version + if [ "$DEPLOY_ARCH" = "amd64" ]; then + docker run openstudio:latest /usr/local/openstudio-$OPENSTUDIO_VERSION/Radiance/bin/rtrace -version + else + echo "Skipping Radiance rtrace test on $DEPLOY_ARCH: packaged binary is not executable on arm64" + fi docker run -v "$(pwd)":/var/simdata/openstudio openstudio:latest ruby /var/simdata/openstudio/test/test_run.rb docker run -v "$(pwd)/test":/var/simdata/openstudio openstudio:latest ./test_gemfile.sh