From a2fbaa8c3b0f3556eadb9e2bbc0ce7b7e359a20d Mon Sep 17 00:00:00 2001 From: "Cathy J. Fitzpatrick" Date: Fri, 8 Nov 2024 15:38:57 -0800 Subject: [PATCH] Automate the release process --- .gitignore | 3 +- CMakeLists.txt | 16 +++ Makefile | 7 +- src/meta/download-source.sh | 24 +++-- src/meta/github-release.md.m4 | 10 ++ src/meta/make-universal.sh | 4 +- src/meta/upload-to-github.sh | 185 ++++++++++++++++++++++++++++++++++ 7 files changed, 235 insertions(+), 14 deletions(-) create mode 100644 src/meta/github-release.md.m4 create mode 100755 src/meta/upload-to-github.sh diff --git a/.gitignore b/.gitignore index 1f6b490..1c5728a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,9 +2,8 @@ .DS_Store .homebrew .vscode -bin +arm64 build -objects universal testing/*.log \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index f7a933c..235a2c3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -203,6 +203,22 @@ add_executable(pinentry-wrapper "src/pinentry-wrapper.cpp") target_codesign(pinentry-wrapper) +############################################################################### +# dependency-sources.zip + +add_custom_command( + OUTPUT + "dependency-sources.zip" + "dependency-sources.zip.txt" + COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/src/meta/download-source.sh" ARGS + ${CMAKE_BINARY_DIR} + VERBATIM + USES_TERMINAL +) + +add_custom_target("dependency-sources" + DEPENDS "dependency-sources.zip" "dependency-sources.zip.txt") + ############################################################################### # keychain-interpose.app diff --git a/Makefile b/Makefile index eaf76b9..39162ea 100644 --- a/Makefile +++ b/Makefile @@ -15,9 +15,12 @@ notarize : universal/keychain-interpose.app release : universal/keychain-interpose.app @make notarize - src/meta/download-source.sh + @cmake --build "$( # SPDX-License-Identifier: GPL-3.0-or-later -set -o pipefail +set -efuC -o pipefail fastfail() { "$@" || kill -- "-$$" } -source_dir=$(dirname "$(fastfail readlink -f "$0")") -base_dir="${source_dir}/../.." -pkg_info_dir="${base_dir}/universal/keychain-interpose.app/Contents/Resources/pkg-info" +if [[ -d "${1:-}" ]]; then + base_dir=${1} +else + source_dir=$(dirname "$(fastfail readlink -f "$0")") + base_dir="${source_dir}/../../universal" +fi + +pkg_info_dir="${base_dir}/keychain-interpose.app/Contents/Resources/pkg-info" readonly source_dir base_dir pkg_info_dir [[ -d ${pkg_info_dir} ]] || exit 1 @@ -19,7 +24,7 @@ while IFS= read -r -d $'\0' pkg; do packages+=( "$(basename "${pkg}")" ) done < <(fastfail find -L "${pkg_info_dir}" -mindepth 1 -type directory -print0) -target_dir="${base_dir}/universal/sources" +target_dir="${base_dir}/sources" [[ -d ${target_dir} ]] && rm -R "${target_dir}" mkdir -p "${target_dir}" @@ -39,11 +44,14 @@ done < <( fastfail brew info --json "${packages[@]}" | \ fastfail yq '.[] | (.name + ":" + .versions.stable + ":" + .urls.stable.url)') +# Remove the final newline from ${release_message}. +release_message="${release_message::-1}" + echo "Creating archive of dependency source code:" zip_basename='dependency-sources.zip' -zip_path="${base_dir}/universal/${zip_basename}" +zip_path="${base_dir}/${zip_basename}" /usr/bin/ditto -ckV --keepParent "${target_dir}" "${zip_path}" rm -R "${target_dir}" du -sh "$(fastfail readlink -f "${zip_path}")" echo "The ${zip_basename} file contains the source code of the following packages:" -sort <(echo -n "${release_message}") \ No newline at end of file +echo -n "${release_message}" | sort | tee "${zip_path}.txt" \ No newline at end of file diff --git a/src/meta/github-release.md.m4 b/src/meta/github-release.md.m4 new file mode 100644 index 0000000..e8d7ace --- /dev/null +++ b/src/meta/github-release.md.m4 @@ -0,0 +1,10 @@ +changequote(`[', `]')dnl +All releases of `keychain-interpose` so far, including this one, are primarily intended for my own use. + +The release binaries are available in the `keychain-interpose-RELEASE_VERSION.zip` file. + +This release contains binary versions of the following dependencies, signed with my developer key: + +RELEASE_DEPENDENCY_LIST + +The source code of the dependencies is available in the `keychain-interpose-RELEASE_VERSION-dependency-sources.zip` file. \ No newline at end of file diff --git a/src/meta/make-universal.sh b/src/meta/make-universal.sh index ee7080c..641d921 100755 --- a/src/meta/make-universal.sh +++ b/src/meta/make-universal.sh @@ -122,5 +122,5 @@ chmod -R go-rwx arm64/keychain-interpose.app "${IDENTITY:?}" "--entitlements arm64/migrate-keys-entitlements.plist" rm -Rf universal x64 -mv arm64 universal -echo "Moved \`arm64\` to \`universal\`." \ No newline at end of file +ln -f -s arm64 universal +echo "Symlinked \`arm64\` to \`universal\`." \ No newline at end of file diff --git a/src/meta/upload-to-github.sh b/src/meta/upload-to-github.sh new file mode 100755 index 0000000..4f93527 --- /dev/null +++ b/src/meta/upload-to-github.sh @@ -0,0 +1,185 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright 2023 Cathy J. Fitzpatrick +# SPDX-License-Identifier: GPL-3.0-or-later +set -efuC -o pipefail +shopt -s inherit_errexit + +fastfail() { + "${@}" || { + /bin/kill -- "-$(/bin/ps -o pgid= "${$}")" "${$}" > /dev/null 2>&1 + } +} + +prompt_yn() { + local __yes='y' __no='n' __default="${2:-}" + if [[ ${__default,,} == 'y' ]]; then + __yes='Y' + elif [[ ${__default,,} == 'n' ]]; then + __no='N' + elif [[ -n "${__default}" ]]; then + echo "prompt_yn: Unknown default: ${__default,,}" 1>&2 + return 2 + fi + echo -n "${1:?} [${__yes}/${__no}] " + local __prompt_yn= + while [[ (${__prompt_yn,,} != 'y') && (${__prompt_yn,,} != 'n') ]]; do + read -r -s -N 1 __prompt_yn + if [[ (-n "${__default}") && (${__prompt_yn} == $'\n') ]]; then + __prompt_yn=${__default,,} + fi + done + echo "${__prompt_yn,,}" + local __status=0 + [[ ${__prompt_yn,,} == 'y' ]] || __status=1 + return "${__status}" +} + +__is_overwrite_required() { + local -a refs + mapfile -t refs < <( + fastfail git show-ref -s -d -- \ + "refs/heads/${__branch}" "refs/tags/${1:?}" | \ + fastfail cut -d ' ' -f 1 + ) + local __status=0 + [[ ${refs[0]} != "${refs[2]}" ]] || __status=1 + return "${__status}" +} + +declare __force_push_required __skip_tag_creation +__inner_prompt_version() { + version= + while [[ -z "${version}" ]]; do + __force_push_required=0 __skip_tag_creation=0 + IFS= read -r -p 'Enter new version to release: ' version + if [[ ${version:0:1} != 'v' ]]; then + version="v${version}" + fi + # shellcheck disable=SC2310 + if [[ ! ${version} =~ ^v[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then + echo 'Supplied version does not match the required pattern (X.Y(.Z)?). Try again.' + version= + elif [[ -n "$(fastfail git tag -l "${version}")" ]]; then + # shellcheck disable=SC2310 + if __is_overwrite_required "${version}"; then + local warning1 warning2 + warning1=$( + echo -n $'\u2757'' Warning: Supplied version ('"${version}"') exists ' + echo -n 'and is not the same revision. '$'\u2757' + ) + warning2=$'\u2757'' It will be overwritten if you proceed.' + printf "%s\n%s%$(( ${#warning1} - ${#warning2} - 1 ))s%s\n" \ + "${warning1}" "${warning2}" '' $'\u2757' + __force_push_required=1 + else + __skip_tag_creation=1 + fi + fi + done +} + +prompt_version() { + local __confirmed_version= + while [[ -z "${__confirmed_version}" ]]; do + __inner_prompt_version + local message='Use this version ('"${version}"')?' + local default='y' + if [[ ${__force_push_required} -eq 1 ]]; then + message="${message::-1} even though it will overwrite existing version?" + default='n' + fi + # shellcheck disable=SC2310 + if prompt_yn "${message}" "${default}"; then + # shellcheck disable=SC2016 + message='This will require `git push -f`. Are you sure?' + if [[ ${__force_push_required} -eq 0 ]] || prompt_yn "${message}" 'n'; then + __confirmed_version=1 + fi + fi + done +} + +declare __branch +__branch=$(git branch --show-current) +[[ -n "${__branch}" ]] || { + echo 'Failed to determine current git branch. Aborting.' + exit 1 +} + +declare build_directory="${1:-universal}" +# shellcheck disable=SC2310 +prompt_yn 'Build directory "'"${build_directory}"'" will be used. Is this okay?' 'y' || { + echo 'Aborting.' + exit 1 +} + +echo 'Existing versions: ' +git tag -ln + +declare version release_message +prompt_version +release_message=$( + m4 "src/meta/github-release.md.m4" -E \ + -D "RELEASE_VERSION=${version}" \ + -D "RELEASE_DEPENDENCY_LIST=$(< "${build_directory}/dependency-sources.zip.txt")" +) + +if [[ ${__skip_tag_creation} -ne 1 ]]; then + declare remote + remote=$(git config get "branch.${__branch}.remote") + [[ -n "${remote}" ]] || { + echo 'Failed to determine default git remote. Aborting.' + exit 1 + } + declare -a git_args=() + if [[ ${__force_push_required} -eq 1 ]]; then + git_args+=( -f ) + fi + echo 'Signing a tag for the release...' + git tag "${git_args[@]}" -s "${version}" -m "version ${version:1}" + echo 'Pushing the signed tag...' + git push "${git_args[@]}" "${remote}" "${version}" +fi + +declare releases +releases=$( + gh release list --json tagName,isDraft \ + -q '.[] | select(.isDraft == true and .tagName == "'"${version}"'") | .tagName' +) +if [[ -n "${releases}" ]]; then + echo 'One or more GitHub draft releases already exist for this version ('"${version}"').' + declare num_releases=$(( $(wc -l <<< "${releases}") )) + echo 'You can delete all '"${num_releases}"' of the drafts if you want.' + # shellcheck disable=SC2310 + if prompt_yn $'\u2757'' Delete all drafts for version '"${version}"'?'; then + declare i + for ((i = 0; i < num_releases; ++i)); do + gh release delete -y "${version}" + done + fi +fi + +declare -A artifacts=( + ["keychain-interpose.app.zip"]="keychain-interpose-${version}.zip" + ["dependency-sources.zip"]="keychain-interpose-${version}-dependency-sources.zip" +) + +declare i +for i in "${!artifacts[@]}"; do + artifacts[${i}]="${build_directory}/${artifacts[${i}]}" + ln -f "${build_directory}/${i}" "${artifacts[${i}]}" +done + +echo 'Uploading artifacts...' +declare uri +uri=$( + echo -n "${release_message}" | + gh release create "${version}" --title "${version}" --notes-file - \ + --draft --verify-tag "${artifacts[@]}" +) +echo 'You can finish publishing the release at this URI:' +echo ' '"${uri}" +# shellcheck disable=SC2310 +if prompt_yn 'Open this in your browser now?' 'y'; then + open "${uri}" +fi \ No newline at end of file