Skip to content

Build

Build #63098

Workflow file for this run

# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
name: Build
on:
workflow_dispatch:
inputs:
force_build:
description: "Force run build job when manually triggered"
required: false
default: "true"
docker_only:
description: "Only rebuild and push Docker images"
required: false
default: "false"
schedule:
- cron: '*/30 * * * *'
# A full third party build runs for hours while the schedule fires every 30
# minutes. Without a concurrency group the runs stack up and race each other on
# the same release tag.
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false
jobs:
prerelease:
name: Prerelease
# This job only resolves a hash and pre-fetches sources, so it does not need
# a macOS runner. ubuntu-latest also ships GNU md5sum, which keeps archive
# verification on rather than at the mercy of what the runner happens to have.
runs-on: ubuntu-latest
env:
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MACOS_X86_MAX_AGE_DAYS: ${{ vars.MACOS_X86_MAX_AGE_DAYS || '7' }}
permissions:
contents: write
outputs:
should_release: ${{ steps.check_diff.outputs.should_release }}
should_update_docker: ${{ steps.check_diff.outputs.should_update_docker }}
should_build_macos_x86: ${{ steps.check_diff.outputs.should_build_macos_x86 }}
thirdparty_commit_hash: ${{ steps.check_diff.outputs.thirdparty_commit_hash }}
doris_version: ${{ steps.check_diff.outputs.doris_version }}
attempt: ${{ steps.check_diff.outputs.attempt }}
steps:
- name: Checkout
uses: actions/checkout@v5
with:
repository: 'apache/doris'
fetch-depth: 0
- name: Check Diff
id: check_diff
env:
MANUAL_FORCE_BUILD: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.force_build == 'true' }}
MANUAL_DOCKER_ONLY: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.docker_only == 'true' }}
run: |
if [[ -z "$(gh release list)" ]] ||
! gh release list | awk -F "\t" '{ print $3 }' | grep 'automation' >/dev/null; then
gh release create -t 'Apache Doris Third Party Prebuilt' automation
fi
max_attempts=3
# The release note is the only state this pipeline keeps. Read it once
# so the three fields below cannot come from three different revisions.
note="$(gh release view automation)"
last_version="$(echo "${note}" | sed -n -E 's/Doris Version: \*(.*)\*.*/\1/p')"
last_status="$(echo "${note}" | sed -n -E 's/Status: \*(.*)\*.*/\1/p')"
last_attempt="$(echo "${note}" | sed -n -E 's/Attempts: \*([0-9]+)\*.*/\1/p')"
# A note without a usable counter must not enable retries: treating it
# as attempt 0 is what turns every scheduled run into a full rebuild.
[[ "${last_attempt}" =~ ^[0-9]+$ ]] || last_attempt="${max_attempts}"
current_version="$(git log -1 --format='%H')"
thirdparty_commit_hash="$(git log -1 --format='%H' -- thirdparty)"
echo "Last Version: ${last_version}"
echo "Last Status: ${last_status}"
echo "Last Attempt: ${last_attempt}"
echo "Current Version: ${current_version}"
echo "Thirdparty Commit: ${thirdparty_commit_hash}"
should_release=false
should_update_docker=false
attempt=1
if [[ -z "${last_version}" ]]; then
echo "The first release was detected."
should_release=true
should_update_docker=true
elif [[ "${last_version}" != "${current_version}" ]]; then
changed_files="$(git diff --name-only "${last_version}" "${current_version}")"
echo -e "Changed files:\n${changed_files}"
if grep -qE '^thirdparty/' <<<"${changed_files}"; then
should_release=true
should_update_docker=true
fi
if grep -qx 'docker/compilation/Dockerfile' <<<"${changed_files}"; then
should_update_docker=true
fi
elif [[ "${last_status}" == 'FAILURE' ]] && [[ "${last_attempt}" -lt "${max_attempts}" ]]; then
# Downloads from third party mirrors fail often enough that a single
# red run should not park master until the next thirdparty/ commit
# lands. Retry a bounded number of times, never forever.
attempt=$((last_attempt + 1))
echo "Previous build failed, retrying (attempt ${attempt}/${max_attempts})."
should_release=true
should_update_docker=true
fi
# docker_only takes precedence over force_build because force_build
# defaults to true for backwards compatibility with existing callers.
if [[ "${MANUAL_DOCKER_ONLY}" == 'true' ]]; then
echo 'Manual Docker-only build requested.'
should_release=false
should_update_docker=true
elif [[ "${MANUAL_FORCE_BUILD}" == 'true' ]]; then
echo 'Manual full build requested.'
should_release=true
should_update_docker=true
fi
# Docker-only failures are deterministic image/workflow failures. Do
# not turn their scheduled retry into an expensive full thirdparty build.
if [[ "${should_release}" == 'false' ]] && [[ "${should_update_docker}" == 'true' ]]; then
attempt="${max_attempts}"
fi
# The macOS x86_64 leg is by far the slowest of the four - 4h22m41s of the
# 5h01m run 31988123966 took, against 1h27m54s for Linux arm64 - and it is
# already allowed to fail without holding up a release. Rebuild it only once
# its published archive has gone stale rather than on every thirdparty change.
should_build_macos_x86="${should_release}"
if [[ "${should_build_macos_x86}" == 'true' ]]; then
macos_x86_asset='doris-thirdparty-prebuilt-darwin-x86_64.tar.xz'
asset_updated_at="$(gh release view automation --json assets \
--jq ".assets[] | select(.name == \"${macos_x86_asset}\") | .updatedAt")" || asset_updated_at=''
if [[ -n "${asset_updated_at}" ]]; then
age_days="$(( ($(date -u +%s) - $(date -u -d "${asset_updated_at}" +%s)) / 86400 ))"
echo "macOS x86_64 archive is ${age_days} day(s) old, rebuild threshold is ${MACOS_X86_MAX_AGE_DAYS} day(s)"
if [[ "${age_days}" -lt "${MACOS_X86_MAX_AGE_DAYS}" ]]; then
should_build_macos_x86=false
fi
else
echo "No macOS x86_64 archive published yet, building it"
fi
fi
if "${should_release}" || "${should_update_docker}"; then
echo -ne "Update Time: *$(date)*\nDoris Version: *${current_version}*\nStatus: *BUILDING*\nAttempts: *${attempt}*" >release_note.md
else
gh release view automation | sed -n '/--/,$p' | awk '{ if (NR > 1) print $0 }' | sed "{
s/Update Time:.*/Update Time: *$(date)*/
s/Doris Version:.*/Doris Version: *${current_version}*/
}" >release_note.md
fi
gh release edit -F release_note.md automation
{
echo "should_release=${should_release}"
echo "should_update_docker=${should_update_docker}"
echo "should_build_macos_x86=${should_build_macos_x86}"
echo "thirdparty_commit_hash=${thirdparty_commit_hash}"
echo "doris_version=${current_version}"
echo "attempt=${attempt}"
} >> "${GITHUB_OUTPUT}"
- name: Download Source and Upload
if: steps.check_diff.outputs.should_release == 'true'
run: |
cd thirdparty
sed '/# unpacking thirdpart archives/,$d' download-thirdparty.sh | bash -
tar -zcvf doris-thirdparty-source.tgz src
# Drop the old asset first: `gh release upload --clobber` intermittently
# exits 1 with "release not found" while deleting the asset it is
# replacing, even though the upload itself went through.
gh release delete-asset automation doris-thirdparty-source.tgz --yes || true
gh release upload --clobber automation doris-thirdparty-source.tgz
# One job per platform instead of one matrix job, so that `update-docker` can depend
# on the Linux x86_64 archive alone. As a matrix it had to wait for all four: in run
# 31988123966 the archive it consumes was published at 05:19 and the Docker job did
# not start until 06:58, held up by macOS x86_64.
build-linux-x86_64:
name: Build
needs: prerelease
if: needs.prerelease.outputs.should_release == 'true'
permissions:
contents: write
uses: ./.github/workflows/build-target.yml
with:
target: Linux
runs_on: ubuntu-22.04
thirdparty_commit_hash: ${{ needs.prerelease.outputs.thirdparty_commit_hash }}
build-linux-arm64:
name: Build
needs: prerelease
if: needs.prerelease.outputs.should_release == 'true'
permissions:
contents: write
uses: ./.github/workflows/build-target.yml
with:
target: Linux-arm64
runs_on: ubuntu-22.04-arm
thirdparty_commit_hash: ${{ needs.prerelease.outputs.thirdparty_commit_hash }}
build-macos-arm64:
name: Build
needs: prerelease
if: needs.prerelease.outputs.should_release == 'true'
permissions:
contents: write
uses: ./.github/workflows/build-target.yml
with:
target: macOS-arm64
runs_on: macos-14
thirdparty_commit_hash: ${{ needs.prerelease.outputs.thirdparty_commit_hash }}
# Skipped while its published archive is still fresh enough - see the prerelease job.
build-macos-x86_64:
name: Build
needs: prerelease
if: needs.prerelease.outputs.should_build_macos_x86 == 'true'
permissions:
contents: write
uses: ./.github/workflows/build-target.yml
with:
target: macOS-x86_64
runs_on: macos-15-intel
tolerate_failure: true
thirdparty_commit_hash: ${{ needs.prerelease.outputs.thirdparty_commit_hash }}
update-docker:
name: Update Docker Image
needs: [prerelease, build-linux-x86_64]
# Only the Linux x86_64 archive goes into the image, so nothing else is waited for.
if: |
always() &&
needs.build-linux-x86_64.result != 'cancelled' &&
needs.prerelease.outputs.should_update_docker == 'true'
runs-on: ubuntu-latest
env:
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Verify Linux x86_64 prebuilt provenance
run: |
expected_commit="${{ needs.prerelease.outputs.thirdparty_commit_hash }}"
if [[ -z "${expected_commit}" ]]; then
echo 'Expected thirdparty commit hash is empty' >&2
exit 1
fi
archive='doris-thirdparty-prebuilt-linux-x86_64.tar.xz'
gh release download automation --pattern "${archive}"
actual_commit="$(tar -xOf "${archive}" installed/_doris_thirdparty_commit_)"
if [[ "${actual_commit}" != "${expected_commit}" ]]; then
echo "Linux prebuilt provenance mismatch: expected ${expected_commit}, got ${actual_commit}" >&2
exit 1
fi
echo "Verified Linux prebuilt thirdparty commit: ${actual_commit}"
rm -f "${archive}"
- name: Checkout docker/setup-buildx-action
run: |
rm -rf ./.github/actions/setup-buildx-action
git clone https://github.com/docker/setup-buildx-action .github/actions/setup-buildx-action
pushd .github/actions/setup-buildx-action &>/dev/null
git checkout 8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
popd &>/dev/null
- name: Checkout docker/login-action
run: |
rm -rf ./.github/actions/login-action
git clone https://github.com/docker/login-action .github/actions/login-action
pushd .github/actions/login-action &>/dev/null
git checkout c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
popd &>/dev/null
- name: Checkout docker/build-push-action
run: |
rm -rf ./.github/actions/build-push-action
git clone https://github.com/docker/build-push-action .github/actions/build-push-action
pushd .github/actions/build-push-action &>/dev/null
git checkout ca052bb54ab0790a636c9b5f226502c73d547a25 # v5
popd &>/dev/null
- name: Set up Docker Buildx
uses: ./.github/actions/setup-buildx-action
- name: Login to Docker Hub
uses: ./.github/actions/login-action
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Prepare Dockerfiles
run: |
# Bind the image to the exact Doris revision selected by prerelease.
curl -fsSL "https://raw.githubusercontent.com/apache/doris/${{ needs.prerelease.outputs.doris_version }}/docker/compilation/Dockerfile" \
-o Dockerfile.upstream
python3 - << 'PYEOF'
import sys, re
with open('Dockerfile.upstream') as f:
content = f.read()
# Patch 1: replace "clone & build thirdparty" block with downloading
# our prebuilt artifact. The block starts with "# clone lastest source
# code" comment and ends with "rm -rf ${DEFAULT_DIR}/doris".
prebuilt_block = (
'# Download prebuilt thirdparty from GitHub Release (built by doris-thirdparty automation)\n'
'ARG GITHUB_REPOSITORY\n'
'RUN mkdir -p /var/local/thirdparty \\\n'
' && wget -q "https://github.com/${GITHUB_REPOSITORY}/releases/download/automation/doris-thirdparty-prebuilt-linux-x86_64.tar.xz" \\\n'
' -O /tmp/prebuilt.tar.xz \\\n'
' && tar -xf /tmp/prebuilt.tar.xz -C /var/local/thirdparty \\\n'
' && rm /tmp/prebuilt.tar.xz\n'
)
patched_2 = re.sub(
r'# clone lastest source code.*?rm -rf \$\{DEFAULT_DIR\}/doris\n',
prebuilt_block,
content, flags=re.DOTALL
)
assert patched_2 != content, "Patch 1 was a no-op: 'clone lastest source code' block not found in upstream Dockerfile"
patched = patched_2
# Write normal image Dockerfile
with open('Dockerfile.patched', 'w') as f:
f.write(patched)
# Patch 2 (no-avx2): add USE_AVX2=0 to the ENV block in builder stage.
# The ENV block ends with PATH=... just before "# install ccache".
old_avx2 = 'PATH="/var/local/ldb-toolchain/bin/:$PATH"\n # USE_AVX2=0'
new_avx2 = 'PATH="/var/local/ldb-toolchain/bin/:$PATH" \\\n USE_AVX2=0'
assert old_avx2 in patched, "Patch 2 failed: '# USE_AVX2=0' comment not found; upstream Dockerfile may have changed"
noavx2 = patched.replace(old_avx2, new_avx2)
assert noavx2 != patched, "Patch 2 was a no-op: no-avx2 Dockerfile is identical to normal Dockerfile"
with open('Dockerfile.patched-noavx2', 'w') as f:
f.write(noavx2)
print("=== normal: check ===")
for line in open('Dockerfile.patched'):
if any(k in line for k in ['COPY doris', 'build-thirdparty', 'ARG GITHUB', 'prebuilt', 'USE_AVX2']):
print(line, end='')
print("=== noavx2: check ===")
for line in open('Dockerfile.patched-noavx2'):
if any(k in line for k in ['COPY doris', 'build-thirdparty', 'ARG GITHUB', 'prebuilt', 'USE_AVX2']):
print(line, end='')
PYEOF
- name: Build and Push Docker Image (normal)
uses: ./.github/actions/build-push-action
with:
context: .
file: Dockerfile.patched
build-args: |
GITHUB_REPOSITORY=${{ github.repository }}
push: true
tags: ${{ vars.DOCKER_TAGS || 'apache/doris:build-env-ldb-toolchain-latest' }}
platforms: linux/amd64
- name: Build and Push Docker Image (no-avx2)
uses: ./.github/actions/build-push-action
with:
context: .
file: Dockerfile.patched-noavx2
build-args: |
GITHUB_REPOSITORY=${{ github.repository }}
push: true
tags: ${{ vars.DOCKER_TAGS_NOAVX2 || 'apache/doris:build-env-ldb-toolchain-no-avx2-latest' }}
platforms: linux/amd64
success:
name: Success
needs: [prerelease, build-linux-x86_64, build-linux-arm64, build-macos-arm64, build-macos-x86_64, update-docker]
# `!cancelled() && !failure()` rather than the default: build-macos-x86_64 is
# skipped on most runs, and a skipped dependency would otherwise skip this job and
# leave the release note stuck at BUILDING forever.
if: |
!cancelled() && !failure() &&
(needs.prerelease.outputs.should_release == 'true' || needs.prerelease.outputs.should_update_docker == 'true')
runs-on: ubuntu-latest
env:
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DORIS_VERSION: ${{ needs.prerelease.outputs.doris_version }}
permissions:
contents: write
steps:
- name: Update Checksums
run: |
gh release download automation
# Write the version this run actually built rather than parsing it back
# out of the note, so the terminal state never depends on the note
# having survived intact.
echo -ne "Update Time: *$(date)*\nDoris Version: *${DORIS_VERSION}*\nStatus: *SUCCESS*\n\n## SHA256 Checksums\n\`\`\`\n$(sha256sum *)\n\`\`\`" >release_note.md
gh release edit --latest -F release_note.md automation
failure:
name: Failure
needs: [prerelease, build-linux-x86_64, build-linux-arm64, build-macos-arm64, build-macos-x86_64, update-docker]
# Only report on a build that actually ran. If prerelease itself failed there
# is no version to record, and writing an empty one is exactly what used to
# make the next scheduled run rebuild from scratch, forever.
if: always() && failure() && needs.prerelease.result == 'success'
runs-on: ubuntu-latest
env:
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DORIS_VERSION: ${{ needs.prerelease.outputs.doris_version }}
ATTEMPT: ${{ needs.prerelease.outputs.attempt }}
permissions:
contents: write
steps:
- name: Update Checksums
run: |
gh release download automation
# Keep Doris Version: the prerelease job keys off it, and dropping it
# made every scheduled run look like a first release. #405 guarded this
# by reading the line back out of the note and bailing out when it was
# missing, which protects an intact note but cannot repair one that is
# already broken -- and a note that has lost the line is exactly the
# state that keeps the rebuild loop running. Take the value from the
# prerelease job instead, so it is always available.
echo -ne "Update Time: *$(date)*\nDoris Version: *${DORIS_VERSION}*\nStatus: *FAILURE*\nAttempts: *${ATTEMPT}*\n\n## SHA256 Checksums\n\`\`\`\n$(sha256sum *)\n\`\`\`" >release_note.md
gh release edit --latest -F release_note.md automation