diff --git a/anaconda/build.sh b/anaconda/build.sh index 3577ea9..dae01b6 100644 --- a/anaconda/build.sh +++ b/anaconda/build.sh @@ -1 +1,4 @@ -${PYTHON} setup.py install --single-version-externally-managed --record=record.txt +# Run setup.py in isolated mode (-I) so the build does not pick up stray +# modules from the working directory. -I is available on all supported +# Pythons (>=3.4), unlike -P/PYTHONSAFEPATH which require Python >=3.11. +${PYTHON} -I setup.py install --single-version-externally-managed --record=record.txt diff --git a/build.sh b/build.sh index a325391..c36c83f 100755 --- a/build.sh +++ b/build.sh @@ -3,10 +3,12 @@ # This script builds snowflake-telemetry-python for release. It is called from # a Jenkins job called SnowflakeTelemetryPythonPackageBuilder. # -# Prequisites: +# Prerequisites: +# - run from a pristine git checkout (this script refuses to build from a +# workspace with modified, untracked, or ignored files; see +# scripts/release_lib.sh) # - activate a conda environment # - have conda-build installed, e.g. `conda install conda-build` -# - have conda-verify installed, e.g. `conda install conda-verify` # # Then, run this script. # @@ -18,12 +20,30 @@ # conda activate my-conda-build-environment # # cd into the snowflake-telemetry-python git root dir # export SNOWFLAKE_TELEMETRY_DIR=$(pwd) +# +# For local test builds only, the workspace hygiene check can be bypassed with: +# export SNOWFLAKE_TELEMETRY_ALLOW_DIRTY_WORKSPACE=1 +# Never set this in the Jenkins release job. + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${REPO_ROOT}/scripts/release_lib.sh" + +# Remove our own previous build output so it does not trip the hygiene gate. +rm -rf ./anaconda/dist + +# Refuse to build unless the checkout is pristine (no modified, untracked, or +# ignored files), so release artifacts contain only committed sources. The +# explicit exit keeps this guard effective even if the script is invoked as +# `bash build.sh` without -e. +assert_clean_workspace "${REPO_ROOT}" || exit 1 make_conda_build () { # set default build number to 0, if SNOWFLAKE_TELEMETRY_BUILD_NUMBER is not set echo "Start building snowflake-telemetry-python package with build_number: ${SNOWFLAKE_TELEMETRY_BUILD_NUMBER:=0}" - # conda build takes GIT_HASH as environment variable and embed it into the build package name - GIT_HASH=$(git rev-parse --short HEAD) SNOWFLAKE_TELEMETRY_BUILD_NUMBER=${SNOWFLAKE_TELEMETRY_BUILD_NUMBER:=0} conda build ./anaconda --output-folder ./anaconda/dist + # conda build takes GIT_HASH as environment variable and embed it into the build package name. + # Record the full HEAD commit so the artifact can be traced back to an exact, + # immutable revision (a short hash is ambiguous enough to collide). + GIT_HASH=$(git -C "${REPO_ROOT}" rev-parse HEAD) SNOWFLAKE_TELEMETRY_BUILD_NUMBER=${SNOWFLAKE_TELEMETRY_BUILD_NUMBER:=0} conda build ./anaconda --output-folder ./anaconda/dist } BUILD_CONDA_FORMAT=false @@ -37,8 +57,8 @@ done # set up private channel to make opentelemetry dependencies available conda config --add channels https://repo.anaconda.com/pkgs/snowflake/ -# clean up the dist directory -rm -rf ./anaconda/dist +# create a clean dist directory (previous output was removed before the +# workspace hygiene gate above) mkdir -p ./anaconda/dist if [ "$BUILD_CONDA_FORMAT" = true ] ; then @@ -52,6 +72,10 @@ else make_conda_build fi +# Bind the build outputs to SHA-256 digests so downstream release steps can +# verify that what gets published is what this build produced. +write_sha256_manifest ./anaconda/dist || exit 1 + # clean up the conda environment conda config --remove channels https://repo.anaconda.com/pkgs/snowflake/ conda build purge diff --git a/pypi-build.sh b/pypi-build.sh index fb3d864..9a970af 100755 --- a/pypi-build.sh +++ b/pypi-build.sh @@ -18,6 +18,22 @@ # source .venv/bin/activate # # cd into the snowflake-telemetry-python git root dir # export SNOWFLAKE_TELEMETRY_DIR=$(pwd) +# +# For local test builds only, the workspace hygiene check can be bypassed with: +# export SNOWFLAKE_TELEMETRY_ALLOW_DIRTY_WORKSPACE=1 +# Never set this in the Jenkins release job. + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${REPO_ROOT}/scripts/release_lib.sh" + +# Remove our own previous build outputs so they do not trip the hygiene gate. +rm -rf ./dist ./build venv_* src/*.egg-info + +# Refuse to build unless the checkout is pristine (no modified, untracked, or +# ignored files), so release artifacts contain only committed sources. The +# explicit exit keeps this guard effective even if the script is invoked as +# `bash pypi-build.sh` without -e. +assert_clean_workspace "${REPO_ROOT}" || exit 1 VENV_DIR=venv_$(date +%s) python3 -m venv ${VENV_DIR} @@ -35,5 +51,9 @@ mkdir ./dist echo "Start building snowflake-telemetry-python package with build_number: ${SNOWFLAKE_TELEMETRY_BUILD_NUMBER:=0}" SNOWFLAKE_TELEMETRY_BUILD_NUMBER=${SNOWFLAKE_TELEMETRY_BUILD_NUMBER:=0} python3 -m build +# Bind the build outputs to SHA-256 digests so downstream release steps can +# verify that what gets published is what this build produced. +write_sha256_manifest ./dist || exit 1 + deactivate rm -rf ${VENV_DIR} diff --git a/scripts/release_lib.sh b/scripts/release_lib.sh new file mode 100644 index 0000000..2c27a28 --- /dev/null +++ b/scripts/release_lib.sh @@ -0,0 +1,81 @@ +# Guards for the Jenkins release build scripts (build.sh and pypi-build.sh). +# +# This file is sourced by those scripts. It must only define functions and +# have no side effects when sourced. + +# assert_clean_workspace +# +# Return 0 only when the git working tree at is pristine: no +# modified, staged, untracked, or ignored files. Otherwise print the offending +# entries to stderr and return 1. +# +# Release builds must produce artifacts from committed sources only, so the +# build refuses to run from a checkout with any leftover or unreviewed files. +# +# For local test builds only, set SNOWFLAKE_TELEMETRY_ALLOW_DIRTY_WORKSPACE=1 +# to bypass this check. Never set it in the Jenkins release job. +assert_clean_workspace() { + local repo_root="${1:-}" + + if [ -z "$repo_root" ]; then + echo "assert_clean_workspace: no repository path given; refusing to build." >&2 + return 1 + fi + + if [ "${SNOWFLAKE_TELEMETRY_ALLOW_DIRTY_WORKSPACE:-0}" = "1" ]; then + echo "WARNING: SNOWFLAKE_TELEMETRY_ALLOW_DIRTY_WORKSPACE=1 is set; skipping the workspace hygiene check." >&2 + echo "WARNING: never set this for official release builds." >&2 + return 0 + fi + + local status + if ! status="$(git -C "$repo_root" status --porcelain --untracked-files=all --ignored=matching 2>&1)"; then + echo "Unable to determine the git state of $repo_root; refusing to build." >&2 + echo "$status" >&2 + return 1 + fi + + if [ -n "$status" ]; then + echo "Refusing to build release artifacts from a non-pristine workspace: $repo_root" >&2 + echo "The following modified, untracked, or ignored (!!) files are present:" >&2 + echo "$status" >&2 + echo "Release builds must run from a pristine checkout so that artifacts" >&2 + echo "contain only committed sources." >&2 + echo "For local builds, set SNOWFLAKE_TELEMETRY_ALLOW_DIRTY_WORKSPACE=1 to skip this check." >&2 + return 1 + fi +} + +# write_sha256_manifest +# +# Write /SHA256SUMS containing the SHA-256 digest of every file +# under , so downstream release steps can verify that the artifacts +# they publish are bit-for-bit what this build produced. Fails if no artifacts +# are present. +write_sha256_manifest() { + local dist_dir="${1:-}" + + if [ -z "$dist_dir" ] || [ ! -d "$dist_dir" ]; then + echo "write_sha256_manifest: '$dist_dir' is not a directory." >&2 + return 1 + fi + + local sha256_cmd + if command -v sha256sum >/dev/null 2>&1; then + sha256_cmd="sha256sum" + else + # macOS (local test builds) ships shasum instead of sha256sum. + sha256_cmd="shasum -a 256" + fi + + local files + files="$(cd "$dist_dir" && find . -type f ! -name SHA256SUMS -print | sort)" + if [ -z "$files" ]; then + echo "No build artifacts found under $dist_dir" >&2 + return 1 + fi + + (cd "$dist_dir" && printf '%s\n' "$files" | while IFS= read -r artifact; do + $sha256_cmd "$artifact" + done > SHA256SUMS) +}