diff --git a/src/entrypoint.sh b/src/entrypoint.sh index 6d1ff4a..1fedf36 100644 --- a/src/entrypoint.sh +++ b/src/entrypoint.sh @@ -36,30 +36,79 @@ SOURCE_REPO_PREFIX="https://${SOURCE_REPO_HOSTNAME}/" # Functions ################################################ +####################################### +# doing the ssh setup. +# Arguments: +# ssh_private_key_src +# source_repo_hostname +# Changes: +# SOURCE_REPO_PREFIX +# Exports: +# SRC_SSH_PRIVATEKEY_ABS_PATH +####################################### function ssh_setup() { echo "::group::ssh setup" info "prepare ssh" - SRC_SSH_FILE_DIR="/tmp/.ssh" - SRC_SSH_PRIVATEKEY_FILE_NAME="id_rsa_actions_template_sync" - export SRC_SSH_PRIVATEKEY_ABS_PATH="${SRC_SSH_FILE_DIR}/${SRC_SSH_PRIVATEKEY_FILE_NAME}" + + local src_ssh_file_dir="/tmp/.ssh" + local src_ssh_private_key_file_name="id_rsa_actions_template_sync" + + local ssh_private_key_src=$1 + local source_repo_hostname=$2 + + if [[ -z "${ssh_private_key_src}" ]] &>/dev/null; then + err "Missing variable 'ssh_private_key_src'."; + exit 1; + fi + + if [[ -z "${source_repo_hostname}" ]]; then + err "Missing variable 'source_repo_hostname'."; + exit 1; + fi + + # exporting SRC_SSH_PRIVATEKEY_ABS_PATH to be used later + export SRC_SSH_PRIVATEKEY_ABS_PATH="${src_ssh_file_dir}/${src_ssh_private_key_file_name}" + debug "We are using SSH within a private source repo" - mkdir -p "${SRC_SSH_FILE_DIR}" + mkdir -p "${src_ssh_file_dir}" # use cat <<< instead of echo to swallow output of the private key - cat <<< "${SSH_PRIVATE_KEY_SRC}" | sed 's/\\n/\n/g' > "${SRC_SSH_PRIVATEKEY_ABS_PATH}" + cat <<< "${ssh_private_key_src}" | sed 's/\\n/\n/g' > "${SRC_SSH_PRIVATEKEY_ABS_PATH}" chmod 600 "${SRC_SSH_PRIVATEKEY_ABS_PATH}" - SOURCE_REPO_PREFIX="git@${SOURCE_REPO_HOSTNAME}:" + + # adjusting outer variable source repo prefix + SOURCE_REPO_PREFIX="git@${source_repo_hostname}:" echo "::endgroup::" } +####################################### +# doing the gpg setup. +# Arguments: +# gpg_private_key +# git_user_email +####################################### function gpg_setup() { echo "::group::gpg setup" info "start prepare gpg" - echo -e "$GPG_PRIVATE_KEY" | gpg --import --batch - for fpr in $(gpg --list-key --with-colons "${GIT_USER_EMAIL}" | awk -F: '/fpr:/ {print $10}' | sort -u); do echo -e "5\ny\n" | gpg --no-tty --command-fd 0 --expert --edit-key "$fpr" trust; done - KEY_ID="$(gpg --list-secret-key --with-colons "${GIT_USER_EMAIL}" | awk -F: '/sec:/ {print $5}')" + local gpg_private_key=$1 + local git_user_email=$2 + + if [[ -z "${gpg_private_key}" ]] &>/dev/null; then + err "Missing variable 'gpg_private_key'."; + exit 1; + fi + + if [[ -z "${git_user_email}" ]]; then + err "Missing variable 'git_user_email'."; + exit 1; + fi + + echo -e "${gpg_private_key}" | gpg --import --batch + for fpr in $(gpg --list-key --with-colons "${git_user_email}" | awk -F: '/fpr:/ {print $10}' | sort -u); do echo -e "5\ny\n" | gpg --no-tty --command-fd 0 --expert --edit-key "$fpr" trust; done + + KEY_ID="$(gpg --list-secret-key --with-colons "${git_user_email}" | awk -F: '/sec:/ {print $5}')" git config --global user.signingkey "${KEY_ID}" git config --global commit.gpgsign true git config --global gpg.program /bin/gpg_no_tty.sh @@ -68,23 +117,36 @@ function gpg_setup() { echo "::endgroup::" } + +####################################### +# doing the git setup. +# Arguments: +# git_user_email +# git_user_name +# source_repo_hostname +####################################### function git_init() { echo "::group::git init" info "set git global configuration" - git config --global user.email "${GIT_USER_EMAIL}" - git config --global user.name "${GIT_USER_NAME}" + local git_user_email=$1 + local git_user_name=$2 + local source_repo_hostname=$3 + + git config --global user.email "${git_user_email}" + git config --global user.name "${git_user_name}" git config --global pull.rebase false git config --global --add safe.directory /github/workspace + # TODO(anau) think about git lfs git lfs install if [[ "${IS_NOT_SOURCE_GITHUB}" == 'true' ]]; then info "the source repository is not located within GitHub." - ssh-keyscan -t rsa "${SOURCE_REPO_HOSTNAME}" >> /root/.ssh/known_hosts + ssh-keyscan -t rsa "${source_repo_hostname}" >> /root/.ssh/known_hosts else info "the source repository is located within GitHub." - gh auth setup-git --hostname "${SOURCE_REPO_HOSTNAME}" - gh auth status --hostname "${SOURCE_REPO_HOSTNAME}" + gh auth setup-git --hostname "${source_repo_hostname}" + gh auth status --hostname "${source_repo_hostname}" fi echo "::endgroup::" } @@ -95,17 +157,17 @@ function git_init() { # Forward to /dev/null to swallow the output of the private key if [[ -n "${SSH_PRIVATE_KEY_SRC}" ]] &>/dev/null; then - ssh_setup + ssh_setup "${SSH_PRIVATE_KEY_SRC}" "${SOURCE_REPO_HOSTNAME}" elif [[ "${SOURCE_REPO_HOSTNAME}" != "${DEFAULT_REPO_HOSTNAME}" ]]; then gh auth login --git-protocol "https" --hostname "${SOURCE_REPO_HOSTNAME}" --with-token <<< "${GITHUB_TOKEN}" fi export SOURCE_REPO="${SOURCE_REPO_PREFIX}${SOURCE_REPO_PATH}" -git_init +git_init "${GIT_USER_EMAIL}" "${GIT_USER_NAME}" "${SOURCE_REPO_HOSTNAME}" if [[ -n "${GPG_PRIVATE_KEY}" ]] &>/dev/null; then - gpg_setup + gpg_setup "${GPG_PRIVATE_KEY}" "${GIT_USER_EMAIL}" fi # shellcheck source=src/sync_template.sh diff --git a/src/sync_template.sh b/src/sync_template.sh index 28b54e6..3e7c8dc 100644 --- a/src/sync_template.sh +++ b/src/sync_template.sh @@ -44,10 +44,6 @@ GIT_REMOTE_PULL_PARAMS="${GIT_REMOTE_PULL_PARAMS:---allow-unrelated-histories -- cmd_from_yml "install" -LOCAL_CURRENT_GIT_HASH=$(git rev-parse HEAD) - -info "current git hash: ${LOCAL_CURRENT_GIT_HASH}" - TEMPLATE_SYNC_IGNORE_FILE_PATH=".templatesyncignore" TEMPLATE_REMOTE_GIT_HASH=$(git ls-remote "${SOURCE_REPO}" HEAD | awk '{print $1}') NEW_TEMPLATE_GIT_HASH=$(git rev-parse --short "${TEMPLATE_REMOTE_GIT_HASH}") @@ -55,51 +51,124 @@ NEW_BRANCH="${PR_BRANCH_NAME_PREFIX}_${NEW_TEMPLATE_GIT_HASH}" PR_BODY="${PR_BODY:-Merge ${SOURCE_REPO_PATH} ${NEW_TEMPLATE_GIT_HASH}}" debug "new Git HASH ${NEW_TEMPLATE_GIT_HASH}" -echo "::group::Check new changes" +# Check if the Ignore File exists inside .github folder or if it doesn't exist at all +if [[ -f ".github/${TEMPLATE_SYNC_IGNORE_FILE_PATH}" || ! -f "${TEMPLATE_SYNC_IGNORE_FILE_PATH}" ]]; then + debug "using ignore file as in .github folder" + TEMPLATE_SYNC_IGNORE_FILE_PATH=".github/${TEMPLATE_SYNC_IGNORE_FILE_PATH}" +fi ##################################################### # Functions ##################################################### +####################################### +# set the gh action outputs if run with github action. +# Arguments: +# pr_branch +####################################### function set_github_action_outputs() { echo "::group::set gh action outputs" + + local pr_branch=$1 + info "set github action outputs" + if [[ -z "${GITHUB_RUN_ID}" ]]; then info "env var 'GITHUB_RUN_ID' is empty -> no github action workflow" else # https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-output-parameter - echo "pr_branch=${NEW_BRANCH}" >> "$GITHUB_OUTPUT" + echo "pr_branch=${pr_branch}" >> "$GITHUB_OUTPUT" fi echo "::endgroup::" } - +####################################### +# Check if the branch exists remote. +# Arguments: +# pr_branch +####################################### function check_branch_remote_existing() { - git ls-remote --exit-code --heads origin "${NEW_BRANCH}" || BRANCH_DOES_NOT_EXIST=true - if [[ "${BRANCH_DOES_NOT_EXIST}" != true ]]; then - warn "Git branch '${NEW_BRANCH}' exists in the remote repository" - set_github_action_outputs + local branch_to_check=$1 + + info "check if the remote branch ${branch_to_check} exists. Exit if so" + + git ls-remote --exit-code --heads origin "${branch_to_check}" || branch_does_not_exist=true + + if [[ "${branch_does_not_exist}" != true ]]; then + warn "Git branch '${branch_to_check}' exists in the remote repository" + set_github_action_outputs "${branch_to_check}" + exit 0 + fi +} + +####################################### +# Check if the commit is already in history. +# exit 0 if so +# Arguments: +# template_remote_git_hash +####################################### +function check_if_commit_already_in_hist_graceful_exit() { + info "check if commit already in history" + + local template_remote_git_hash=$1 + + git cat-file -e "${template_remote_git_hash}" || commit_not_in_hist=true + if [ "${commit_not_in_hist}" != true ] ; then + warn "repository is up to date!" + exit 0 + fi + +} + +########################################## +# check if there are staged files. +# exit if not +########################################## +function check_staged_files_available_graceful_exit() { + if git diff --quiet && git diff --staged --quiet; then + info "nothing to commit" exit 0 fi } +####################################### +# force source file deletion if they had been deleted +####################################### function force_delete_files() { - echo "::group::force file deletion" + info "force delete files" warn "force file deletion is enabled. Deleting files which are deleted within the target repository" - FILES_TO_DELETE=$(git log --diff-filter D --pretty="format:" --name-only "${LOCAL_CURRENT_GIT_HASH}"..HEAD | sed '/^$/d') - warn "files to delete: ${FILES_TO_DELETE}" - if [[ -n "${FILES_TO_DELETE}" ]]; then - echo "${FILES_TO_DELETE}" | xargs rm - fi + local_current_git_hash=$(git rev-parse HEAD) - echo "::endgroup::" + info "current git hash: ${local_current_git_hash}" + + files_to_delete=$(git log --diff-filter D --pretty="format:" --name-only "${local_current_git_hash}"..HEAD | sed '/^$/d') + warn "files to delete: ${files_to_delete}" + if [[ -n "${files_to_delete}" ]]; then + echo "${files_to_delete}" | xargs rm + fi } +####################################### +# cleanup older prs based on labels. +# Arguments: +# upstream_branch +# pr_labels +####################################### function cleanup_older_prs () { + info "cleanup older prs" + + local upstream_branch=$1 + local pr_labels=$2 + + if [[ -z "${pr_labels}" ]]; then + warn "env var 'PR_LABELS' is empty. Skipping older prs cleanup" + return 0 + fi + older_prs=$(gh pr list \ - --base "${UPSTREAM_BRANCH}" \ + --base "${upstream_branch}" \ --state open \ - --label "${PR_LABELS}" \ + --label "${pr_labels}" \ --json number \ --template '{{range .}}{{printf "%v" .number}}{{"\n"}}{{end}}') @@ -110,8 +179,41 @@ function cleanup_older_prs () { done } -function maybe_create_labels () { - readarray -t labels_array < <(awk -F',' '{ for( i=1; i<=NF; i++ ) print $i }' <<<"${PR_LABELS}") +################################## +# pull source changes +# Arguments: +# source_repo +# git_remote_pull_params +################################## +function pull_source_changes() { + info "pull changes from source repository" + local source_repo=$1 + local git_remote_pull_params=$2 + + eval "git pull ${source_repo} ${git_remote_pull_params}" || pull_has_issues=true + + if [ "$pull_has_issues" == true ] ; then + warn "There had been some git pull issues." + warn "Maybe a merge issue." + warn "We go on but it is likely that you need to fix merge issues within the created PR." + fi +} + +####################################### +# eventual create labels (if they are not existent). +# Arguments: +# pr_labels +####################################### +function eventual_create_labels () { + local pr_labels=$1 + info "eventual create labels ${pr_labels}" + + if [[ -z "${pr_labels}" ]]; then + info "'pr_labels' is empty. Skipping labels check" + retun 0 + fi + + readarray -t labels_array < <(awk -F',' '{ for( i=1; i<=NF; i++ ) print $i }' <<<"${pr_labels}") for label in "${labels_array[@]}" do search_result=$(gh label list \ @@ -132,142 +234,177 @@ function maybe_create_labels () { done } +############################## +# push the changes +# Arguments: +# branch +############################## function push () { - debug "push changes" - git push --set-upstream origin "${NEW_BRANCH}" + info "push changes" + local branch=$1 + git push --set-upstream origin "${branch}" } +#################################### +# creates a pr +# Arguments: +# title +# body +# branch +# labels +# reviewers +################################### function create_pr () { + info "create pr" + local title=$1 + local body=$2 + local branch=$3 + local labels=$4 + local reviewers=$5 + gh pr create \ - --title "${PR_TITLE}" \ - --body "${PR_BODY}" \ - --base "${UPSTREAM_BRANCH}" \ - --label "${PR_LABELS}" \ - --reviewer "${PR_REVIEWERS}" + --title "${title}" \ + --body "${body}" \ + --base "${branch}" \ + --label "${labels}" \ + --reviewer "${reviewers}" +} + +######################################### +# restore the .templatesyncignore file +# Arguments: +# template_sync_ignore_file_path +########################################### +function restore_templatesyncignore_file() { + info "restore the ignore file" + local template_sync_ignore_file_path=$1 + if [ -s "${template_sync_ignore_file_path}" ]; then + git reset "${template_sync_ignore_file_path}" + git checkout -- "${template_sync_ignore_file_path}" || warn "not able to checkout the former .templatesyncignore file. Most likely the file was not present" + fi +} + +######################################### +# reset all files within the .templatesyncignore file +# Arguments: +# template_sync_ignore_file_path +########################################### +function handle_templatesyncignore() { + info "handle .templatesyncignore" + local template_sync_ignore_file_path=$1 + # we are checking the ignore file if it exists or is empty + # -s is true if the file contains whitespaces + if [ -s "${template_sync_ignore_file_path}" ]; then + debug "unstage files from template sync ignore" + git reset --pathspec-from-file="${template_sync_ignore_file_path}" + + debug "clean untracked files" + git clean -df + + debug "discard all unstaged changes" + git checkout -- . + fi } ######################################################## # Logic ####################################################### -check_branch_remote_existing +function prechecks() { + info "prechecks" + echo "::group::force file deletion" + check_branch_remote_existing "${NEW_BRANCH}" -git cat-file -e "${TEMPLATE_REMOTE_GIT_HASH}" || COMMIT_NOT_IN_HIST=true -if [ "$COMMIT_NOT_IN_HIST" != true ] ; then - warn "repository is up to date!" - exit 0 -fi + check_if_commit_already_in_hist_graceful_exit "${TEMPLATE_REMOTE_GIT_HASH}" -echo "::endgroup::" + echo "::endgroup::" +} -cmd_from_yml "prepull" -echo "::group::Pull template" +function checkout_branch_and_pull() { + info "checkout branch and pull" + cmd_from_yml "prepull" -debug "create new branch from default branch with name ${NEW_BRANCH}" -git checkout -b "${NEW_BRANCH}" -debug "pull changes from template" + echo "::group::checkout branch and pull" -eval "git pull ${SOURCE_REPO} ${GIT_REMOTE_PULL_PARAMS}" || PULL_HAS_ISSUES=true + debug "create new branch from default branch with name ${NEW_BRANCH}" + git checkout -b "${NEW_BRANCH}" + debug "pull changes from template" -if [ "$PULL_HAS_ISSUES" == true ] ; then - warn "There had been some git pull issues." - warn "Maybe a merge issue." - warn "We go on but it is likely that you need to fix merge issues within the created PR." -fi + pull_source_changes "${SOURCE_REPO}" "${GIT_REMOTE_PULL_PARAMS}" -echo "::endgroup::" + restore_templatesyncignore_file "${TEMPLATE_SYNC_IGNORE_FILE_PATH}" -# Check if the Ignore File exists inside .github folder or if it doesn't exist at all -if [[ -f ".github/${TEMPLATE_SYNC_IGNORE_FILE_PATH}" || ! -f "${TEMPLATE_SYNC_IGNORE_FILE_PATH}" ]]; then - debug "using ignore file as in .github folder" - TEMPLATE_SYNC_IGNORE_FILE_PATH=".github/${TEMPLATE_SYNC_IGNORE_FILE_PATH}" -fi + if [ "$IS_FORCE_DELETION" == "true" ]; then + force_delete_files + fi -if [ -s "${TEMPLATE_SYNC_IGNORE_FILE_PATH}" ]; then - echo "::group::restore ignore file" - info "restore the ignore file" - git reset "${TEMPLATE_SYNC_IGNORE_FILE_PATH}" - git checkout -- "${TEMPLATE_SYNC_IGNORE_FILE_PATH}" || warn "not able to checkout the former .templatesyncignore file. Most likely the file was not present" echo "::endgroup::" -fi +} -if [ "$IS_FORCE_DELETION" == "true" ]; then - force_delete_files -fi -cmd_from_yml "precommit" +function commit() { + info "commit" -echo "::group::commit changes" + cmd_from_yml "precommit" -git add . + echo "::group::commit changes" -# we are checking the ignore file if it exists or is empty -# -s is true if the file contains whitespaces -if [ -s "${TEMPLATE_SYNC_IGNORE_FILE_PATH}" ]; then - debug "unstage files from template sync ignore" - git reset --pathspec-from-file="${TEMPLATE_SYNC_IGNORE_FILE_PATH}" + git add . - debug "clean untracked files" - git clean -df + handle_templatesyncignore "${TEMPLATE_SYNC_IGNORE_FILE_PATH}" - debug "discard all unstaged changes" - git checkout -- . -fi + check_staged_files_available_graceful_exit -if git diff --quiet && git diff --staged --quiet; then - info "nothing to commit" - exit 0 -fi + git commit --signoff -m "${PR_COMMIT_MSG}" + + echo "::endgroup::" +} -git commit --signoff -m "${PR_COMMIT_MSG}" -echo "::endgroup::" +function push_prepare_pr_create_pr() { + info "push_prepare_pr_create_pr" + if [ "$IS_DRY_RUN" == "true" ]; then + warn "dry_run option is set to on. skipping labels check, cleanup older PRs, push and create pr" + return 0 + fi + echo "::group::check for missing labels" -echo "::group::cleanup older PRs" + eventual_create_labels "${PR_LABELS}" + + echo "::endgroup::" -if [ "$IS_DRY_RUN" != "true" ]; then + echo "::group::cleanup older PRs" if [ "$IS_PR_CLEANUP" != "false" ]; then if [[ -z "${PR_LABELS}" ]]; then - warn "env var 'PR_LABELS' is empty. Skipping older prs cleanup" + warn "env var 'PR_LABELS' is empty. Skipping older prs cleanup" else cmd_from_yml "precleanup" - cleanup_older_prs + cleanup_older_prs "${UPSTREAM_BRANCH}" "${PR_LABELS}" fi else warn "is_pr_cleanup option is set to off. Skipping older prs cleanup" fi -else - warn "dry_run option is set to off. Skipping older prs cleanup" -fi -echo "::endgroup::" + echo "::endgroup::" -echo "::group::check for missing labels" + echo "::group::push changes and create PR" -if [[ -z "${PR_LABELS}" ]]; then - info "env var 'PR_LABELS' is empty. Skipping labels check" -else - if [ "$IS_DRY_RUN" != "true" ]; then - maybe_create_labels - else - warn "dry_run option is set to off. Skipping labels check" - fi -fi + cmd_from_yml "prepush" + push "${NEW_BRANCH}" + cmd_from_yml "prepr" + create_pr "${PR_TITLE}" "${PR_BODY}" "${UPSTREAM_BRANCH}" "${PR_LABELS}" "${PR_REVIEWERS}" + + echo "::endgroup::" +} -echo "::endgroup::" -echo "::group::push changes and create PR" +prechecks -if [ "$IS_DRY_RUN" != "true" ]; then - cmd_from_yml "prepush" - push - cmd_from_yml "prepr" - create_pr -else - warn "dry_run option is set to off. Skipping push changes and skip create pr" -fi +checkout_branch_and_pull + +commit -echo "::endgroup::" +push_prepare_pr_create_pr -set_github_action_outputs +set_github_action_outputs "${NEW_BRANCH}"