#!/bin/bash # If the envvar PUSH_TO_REPO=yes (the default), then stuff is pushed to remote # locations set -euo pipefail SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" stage_from_version() { if [[ "$1" =~ -beta[0-9]$ ]]; then echo -n "beta" elif [[ "$1" =~ -post$ ]]; then echo -n "test" else echo -n "final" fi } parse_args_and_release() { local new_version=$($SCRIPT_DIR/current-version) local push=yes local stage= local build_all=yes local build_docker_image=no local build_deb_package=no local build_rpm_package=no local build_bundle=no local build_deployments=no local build_pypi=no local push_windows=no local force=no local allow_uncommitted=no local github_username="${GITHUB_USERNAME-}" local github_token="${GITHUB_TOKEN-}" local github_release=no local new_image= local image_base= local image_digest= while [ -n "${1-}" ]; do case $1 in --stage) stage="$2" case $stage in final|beta|test) ;; *) echo "stage must be 'final', 'beta' or 'test'" >&2 && exit 1 ;; esac shift 1 ;; --new-version) new_version="$2" if [[ ! "$new_version" =~ [0-9]\.[0-9]\.[0-9](-beta[0-9])?(-post)? ]]; then echo "Version $new_version is in an unexpected format" >&2 exit 1 fi shift 1 ;; --github-user) github_username="$2" shift 1 ;; --github-token) github_token="$2" shift 1 ;; --push) push=yes ;; --no-push) push=no ;; --force) force=yes ;; --allow-uncommitted-files) allow_uncommitted=yes ;; --component) case "$2" in docker) build_docker_image=yes build_all=no ;; deb) build_deb_package=yes build_all=no ;; rpm) build_rpm_package=yes build_all=no ;; bundle) build_bundle=yes build_all=no ;; deployments) build_deployments=yes build_all=no ;; windows) push_windows=yes build_all=no ;; pypi) build_pypi=yes build_all=no ;; github) github_release=yes build_all=no ;; *) echo "component "$1" not recognized, quitting" >&2 && exit 1 ;; esac shift 1 ;; *) echo "Unknown option $1" >&2 usage exit 1 ;; esac shift 1 done if [[ -z "$stage" ]]; then stage=$(stage_from_version $new_version) fi image_base="$(docker_repo_from_stage $stage)" new_image="$image_base:$new_version" # fail immediately if releasing to Github and either the username or token is not set if [[ "$push" == "yes" ]] && [[ "$stage" == "final" ]] && [[ "$build_all" == "yes" || "$github_release" == "yes" || "$build_bundle" == "yes" || "$push_windows" == "yes" ]]; then if [[ -z "$github_username" ]]; then echo "Github username is required when releasing to Github." >&2 echo "Set the GITHUB_USERNAME env var or use the '--github-user ' option." >&2 exit 1 fi if [[ -z "$github_token" ]]; then echo "Github token is required when releasing to Github." >&2 echo "Set the GITHUB_TOKEN env var or use the '--github-token ' option." >&2 exit 1 fi fi # exit/fail immediately if only creating a Github release and the requirements are not met if [[ "$github_release" == "yes" ]]; then if [[ "$push" == "no" ]]; then echo "Nothing to do" exit 0 fi if [[ "$stage" != "final" ]]; then echo "Release stage must be 'final' in order to create a Github release." >&2 exit 1 fi if ! docker pull $new_image; then echo "$new_image must already be built and pushed in order to create a Github release." >&2 exit 1 fi fi if [[ "$stage" != "test" ]] && ! git diff --exit-code && [[ "$allow_uncommitted" != "yes" ]]; then echo "You are making a non-test release and have changes in your local workspace. Stash them first for a pristine build." >&2 exit 1 fi if [[ "$stage" != "test" ]] && [[ $(git symbolic-ref HEAD) != "refs/heads/master" ]] && [[ "$force" != "yes" ]]; then echo "You are releasing a beta/final release from a branch other than master, which is not allowed." >&2 exit 1 fi read -p "This is a $stage release of version $new_version, please confirm: [y/N] " [[ ! "$REPLY" =~ ^[Yy]$ ]] && echo "Cancelling release" && exit 1 if [[ "$push_windows" == "yes" ]]; then push_windows_bundle "$new_version" "$stage" "$push" "$github_username" "$github_token" exit 0 fi if [[ "$build_all" == "yes" ]] || [[ "$build_docker_image" == "yes" ]]; then echo "Building docker image..." build_docker_image "$stage" "$new_version" if [[ "$push" == "yes" ]]; then echo "Pushing docker image" docker push $new_image if [[ "$stage" == "final" ]]; then update_floating_docker_tags "$new_version" "$new_image" "$image_base" fi fi fi if [[ "$build_all" == "yes" ]] || [[ "$build_deb_package" == "yes" ]]; then echo "Building and pushing deb package" build_and_push_package "deb" "$stage" "$push" if [[ "$push" == "yes" ]]; then $SCRIPT_DIR/invalidate-cloudfront "/debs/signalfx-agent/$stage/*" fi fi if [[ "$build_all" == "yes" ]] || [[ "$build_rpm_package" == "yes" ]]; then echo "Building and pushing rpm package" build_and_push_package "rpm" "$stage" "$push" $SCRIPT_DIR/invalidate-cloudfront "/rpms/signalfx-agent/$stage/*" fi if [[ "$stage" != "test" ]] && [[ "$($SCRIPT_DIR/current-version)" != "$new_version" ]]; then create_and_push_tag $new_version fi if [[ "$stage" == "final" ]] && [[ "$build_all" == "yes" || "$github_release" == "yes" ]]; then image_digest=$(get_image_digest $new_image) ensure_github_release "v$new_version" "$github_username" "$github_token" add_digest_to_release "v$new_version" "$new_image" "$image_digest" "$github_username" "$github_token" fi if [[ "$build_all" == "yes" ]] || [[ "$build_bundle" == "yes" ]]; then echo "Making bundle tar.gz" make_bundle "$new_version" if [[ "$push" == "yes" ]] && [[ "$stage" == "final" ]]; then echo "Pushing bundle to Github" push_bundle_to_github "$new_version" "$github_username" "$github_token" fi fi # Do these updates after everything has been pushed so that nobody ever sees # versions in the master branch that haven't been released. if [[ "$stage" == "final" ]] && ([[ "$build_deployments" == "yes" ]] || [[ "$build_all" == "yes" ]]); then update_deployment_files "$new_version" "$force" fi if [[ "$build_all" == "yes" ]] || [[ "$build_pypi" == "yes" ]]; then echo "Pushing python extensions to PyPi" if python_code_has_changed "$new_version"; then release_python_to_pypi fi fi echo "Successfully released $new_version" } usage() { cat < The new version to release. If not specified, will be inferred from the latest git tag Note that the version should not include a leading 'v'! --component can be docker, deb, rpm, bundle, github, or windows Releases only the selected component if specified, otherwise does everything except windows If "github" is selected, the release stage must be "final", and the agent docker image must already be built and pushed --[no-]push Whether to push the components to remote sources or not (default, yes) --force Ignore checks for uncommited local changes and package repo confirmation --allow-uncommitted-files Whether to ignore uncommitted files in the current directory --stage test|beta|final What kind of release this is. If not specified, will be inferred from the version --github-user Github username of a user that has permisssions to manage releases --github-token Github API token for the given user EOH } update_deployment_files() { local new_version=$1 $SCRIPT_DIR/update-deployments-version $new_version # Don't do anything if there aren't any changed files if ! git --no-pager diff --exit-code deployments; then if [[ $force != "yes" ]]; then read -p "Above is the diff for deployment files for $new_version, please confirm: [y/N] " [[ ! "$REPLY" =~ ^[Yy]$ ]] && echo "Not pushing deployment updates" >&2 && return 1 fi git add deployments Dockerfile.rhel git commit -m"Update deployment versions to ${new_version}" git push origin fi } create_and_push_tag() { local new_version=$1 new_tag="v${new_version}" echo "Tagging repo with ${new_tag}" # This will prompt the user for a tag message, which will act as the changelog # for this version git tag -a "$new_tag" if [[ $($SCRIPT_DIR/current-version) != "$new_version" ]]; then echo "Something is off, $($SCRIPT_DIR/current_version) should now be $new_version" >&2 exit 1 fi git push --tags echo "Tag pushed" } ensure_github_release() { local tag=$1 local username=$2 local token=$3 . $SCRIPT_DIR/github-releases.sh if ! get_github_release "$tag" "$username" "$token"; then echo "Creating Github release..." new_github_release $tag $username $token else echo "Github release already exists" fi } ## Docker image build and push docker_repo_from_stage() { local stage=$1 if [[ "$stage" != "final" && "$stage" != "beta" ]]; then echo -n "quay.io/signalfx/signalfx-agent-dev" else echo -n "quay.io/signalfx/signalfx-agent" fi } build_docker_image() { local image_name="$(docker_repo_from_stage $1)" local new_version="$2" echo "Building image $image_name:$new_version" AGENT_IMAGE_NAME=$image_name make -C $SCRIPT_DIR/.. image } update_floating_docker_tags() { local new_version="$1" local new_image="$2" local image_base="$3" major_version=$(echo -n "$new_version" | cut -d. -f1) floating_tag="$image_base:$major_version" docker tag "$new_image" "$floating_tag" docker push $floating_tag } # Deb package build and push build_and_push_package() { local package_type=$1 local stage=$2 local push=$3 PUSH_TO_REPO=$push make ${package_type}-$stage-package } make_bundle() { local new_version="$1" local expected_output="$SCRIPT_DIR/../signalfx-agent-${new_version}.tar.gz" make bundle test -f "$expected_output" echo "Bundle is built at $expected_output" } push_bundle_to_github() { local new_version="$1" local username="$2" local token="$3" local tag="v$new_version" local bundle_path="$SCRIPT_DIR/../signalfx-agent-${new_version}.tar.gz" . $SCRIPT_DIR/github-releases.sh if ! get_github_release "$tag" "$username" "$token"; then echo "Github release $tag not found!" exit 1 fi upload_asset_to_release v$new_version $bundle_path application/gzip $username $token } push_windows_bundle() { local new_version="$1" local stage="$2" local push="$3" local username="$4" local token="$5" local src_dir="$SCRIPT_DIR/../build" local dest_dir="s3://public-downloads--signalfuse-com/windows/$stage/zip" local bundle_path="$src_dir/SignalFxAgent-${new_version}-win64.zip" local tag="v$new_version" if [[ ! -f "$bundle_path" ]]; then echo "$bundle_path not found!" echo "The $new_version Windows agent bundle must be manually built." exit 1 fi # ensure that latest.txt is created and has the same version of the bundle echo -n "$new_version" > "$src_dir/latest.txt" if [[ "$push" == "yes" ]]; then if [[ "$stage" == "final" ]]; then . $SCRIPT_DIR/github-releases.sh if ! get_github_release "$tag" "$username" "$token"; then echo "Github release $tag not found!" echo "You must release the other components before releasing the Windows bundle." exit 1 fi upload_asset_to_release "$tag" "$bundle_path" "application/zip" "$username" "$token" fi if aws s3 ls "$dest_dir/SignalFxAgent-${new_version}-win64.zip"; then read -p "SignalFxAgent-${new_version}-win64.zip already exists in $dest_dir. Overwrite? [y/N] " [[ ! "$REPLY" =~ ^[Yy]$ ]] && echo "Not pushing windows bundle" >&2 && return 1 fi aws s3 cp "$bundle_path" "$dest_dir/SignalFxAgent-${new_version}-win64.zip" aws s3 cp "$src_dir/latest.txt" "${dest_dir}/latest/latest.txt" $SCRIPT_DIR/invalidate-cloudfront "/windows/$stage/zip/*" else echo "Nothing to do" fi } latest_pypi_release() { echo -n "curl https://pypi.org/pypi/sfxpython/json | jq -r .info.version" || true } python_code_has_changed() { local new_version=$1 if ! git diff --exit-code v$(scripts/latest-final-release v${new_version}~) -- $SCRIPT_DIR/../python; then echo "Python code changed since last release" last_release=$(latest_pypi_release) current_release=$(python $SCRIPT_DIR/../python/setup.py --version) if [[ -n "$last_release" ]] && [[ "$last_release" == "$current_version" ]]; then echo "ERROR: Python release has changed since last agent release but you didn't increment the Python release version from $current_release" >&2 exit 7 fi echo "Last release of Python package was version $last_release" else echo "Python package didn't change since last release" return 1 fi } release_python_to_pypi() { pushd $SCRIPT_DIR/../python python3 setup.py sdist bdist_wheel python3 -m twine upload dist/* popd } get_image_digest() { local image=$1 local digest= digest=$(docker inspect --format='{{.RepoDigests}}' $image | sed "s|\[.*@\(sha256:.*\)\]|\1|") if [[ ! "$digest" =~ ^sha256:[A-Fa-f0-9]{64}$ ]]; then echo "Failed to get manifest digest for $image!" >&2 return 1 fi echo $digest } add_digest_to_release() { local tag=$1 local image=$2 local digest=$3 local username=$4 local token=$5 local release_id= local body= . $SCRIPT_DIR/github-releases.sh release_id=$(github_request "$username" "$token" "GET" "releases" | jq --arg tag "$tag" '.[] | select(.tag_name==$tag) | .id') if [[ ! "$release_id" =~ ^[0-9]+$ ]]; then echo "Failed to get release id for $tag!" >&2 exit 1 fi # get original release message and append the agent image digest body=$(github_request "$username" "$token" "GET" "releases" | jq --argjson id $release_id '.[] | select(.id==$id) | .body' | tr -d '"') if [[ -z "$body" ]]; then echo "Failed to get release message for $tag!" >&2 exit 1 fi body="""$(echo -en $body) > Docker Image: \`$image\` (digest: \`$digest\`) """ local tmpfile=$(mktemp) echo "$tmpfile" cat < $tmpfile { "body": $(jq -n --arg body "$body" '$body') } EOH github_request "$username" "$token" "PATCH" "releases/$release_id" $tmpfile } parse_args_and_release $@