diff --git a/CHANGELOG.md b/CHANGELOG.md index f771a62..b519ccd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [1.10.0](https://github.com/AndreasAugustin/actions-template-sync/compare/v1.9.0...v1.10.0) (2024-03-05) + + +### Features + +* **#467:** :sparkles: hooks now within action inputs ([#489](https://github.com/AndreasAugustin/actions-template-sync/issues/489)) ([0e55c08](https://github.com/AndreasAugustin/actions-template-sync/commit/0e55c08f95f9a83c60f809fa6b49785187ec7623)) + ## [1.9.0](https://github.com/AndreasAugustin/actions-template-sync/compare/v1.8.1...v1.9.0) (2024-03-05) diff --git a/README.md b/README.md index 0043e66..cf43869 100644 --- a/README.md +++ b/README.md @@ -246,6 +246,7 @@ jobs: | hostname | `[optional]` the hostname of the repository | `false` | `github.com` | | is_dry_run | `[optional]` set to `true` if you do not want to push the changes and not want to create a PR | `false` | | | is_allow_hooks | `[optional]` set to `true` if you want to enable lifecycle hooks. Use this with caution! | `false` | `false` | +| hooks | `[optional]` please check the lifecycle hooks section below | `false` | | | is_pr_cleanup | `[optional]` set to `true` if you want to cleanup older PRs targeting the same branch. Use this with caution! | `false` | `false` | | is_not_source_github | `[optional]` set to `true` if the source git provider is not GitHub | `false` | `false` | | is_force_deletion | `[optional]` set to `true` if you want to force delete files which are deleted within the source repository even if they contain changes. You need to also adjust `git_remote_pull_params` (see below for details) | `false` | `false` | @@ -374,7 +375,8 @@ jobs: Different lifecycle hooks are supported. You need to enable the functionality with the option `is_allow_hooks` and set it to `true` :warning: use this functionality with caution. You can use one of the available docker images to test it out. **With great power comes great responsibility**. -In addition, you need a configuration file with the name `templatesync.yml` within the root of the target repository. +In addition, you need either a configuration file with the name `templatesync.yml` within the root of the target repository +or you set the hooks input parameter within the action definition with a related yaml string The following hooks are supported (please check [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for a better understanding of the lifecycles). @@ -388,9 +390,7 @@ The following hooks are supported (please check [docs/ARCHITECTURE.md](docs/ARCH **Remark** The underlying OS is defined by an Alpine container. E.q. for the installation phase you need to use commands like `apk add --update --no-cache python3` -Schema and example for the `templatesync.yml` - -**Remark** It is possible to use environment variables within the github action definition usable within the command configuration, e.g. +### Example for the hooks input parameter ```yml - name: Test action step @@ -398,7 +398,25 @@ Schema and example for the `templatesync.yml` env: MY_VAR: "foo" # possible to define envrionment variables with: - github_token: ${{ secrets.GITHUB_TOKEN }} + source_repo_path: AndreasAugustin/template.git + upstream_branch: main + is_dry_run: true + is_allow_hooks: true + hooks: > + prepull: + commands: + - echo 'hi, we are within the prepull phase' + - echo 'maybe you want to do adjustments on the local code' +``` + +### Schema and example for the `templatesync.yml` + +**Remark** It is possible to use environment variables within the github action definition usable within the command configuration, e.g. + +```yml +- name: Test action step + uses: AndreasAugustin/actions-template-sync@v1 + with: source_repo_path: AndreasAugustin/template.git upstream_branch: main is_dry_run: true diff --git a/action.yml b/action.yml index 89fa245..41f8b6b 100644 --- a/action.yml +++ b/action.yml @@ -40,6 +40,8 @@ inputs: is_allow_hooks: description: "[optional] set to true if you want to allow hooks. Use this functionality with caution!" default: "false" + hooks: + description: "[optional] define the hooks as yaml string input" is_pr_cleanup: description: "[optional] set to true if you want to cleanup older PRs targeting the same branch." default: "false" @@ -76,6 +78,7 @@ runs: HOSTNAME: ${{ inputs.hostname }} IS_DRY_RUN: ${{ inputs.is_dry_run }} IS_ALLOW_HOOKS: ${{ inputs.is_allow_hooks }} + HOOKS: ${{ inputs.hooks }} IS_PR_CLEANUP: ${{ inputs.is_pr_cleanup}} IS_NOT_SOURCE_GITHUB: ${{ inputs.is_not_source_github }} IS_FORCE_DELETION: ${{ inputs.is_force_deletion }} diff --git a/src/entrypoint.sh b/src/entrypoint.sh index 4d788e1..6d1ff4a 100644 --- a/src/entrypoint.sh +++ b/src/entrypoint.sh @@ -6,6 +6,10 @@ set -e # shellcheck source=src/sync_common.sh source sync_common.sh +########################################### +# Precheks +########################################## + if [[ -z "${GITHUB_TOKEN}" ]]; then err "Missing input 'github_token: \${{ secrets.GITHUB_TOKEN }}'."; exit 1; @@ -16,6 +20,10 @@ if [[ -z "${SOURCE_REPO_PATH}" ]]; then exit 1 fi +############################################ +# Variables +############################################ + DEFAULT_REPO_HOSTNAME="github.com" SOURCE_REPO_HOSTNAME="${HOSTNAME:-${DEFAULT_REPO_HOSTNAME}}" GIT_USER_NAME="${GIT_USER_NAME:-${GITHUB_ACTOR}}" @@ -24,6 +32,10 @@ GIT_USER_EMAIL="${GIT_USER_EMAIL:-github-action@actions-template-sync.noreply.${ # In case of ssh template repository this will be overwritten SOURCE_REPO_PREFIX="https://${SOURCE_REPO_HOSTNAME}/" +################################################ +# Functions +################################################ + function ssh_setup() { echo "::group::ssh setup" @@ -53,18 +65,9 @@ function gpg_setup() { git config --global gpg.program /bin/gpg_no_tty.sh info "done prepare gpg" - echo "::endgroup::"for fpr in + echo "::endgroup::" } -# Forward to /dev/null to swallow the output of the private key -if [[ -n "${SSH_PRIVATE_KEY_SRC}" ]] &>/dev/null; then - ssh_setup -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}" - function git_init() { echo "::group::git init" info "set git global configuration" @@ -86,6 +89,19 @@ function git_init() { echo "::endgroup::" } +################################################### +# Logic +################################################### + +# Forward to /dev/null to swallow the output of the private key +if [[ -n "${SSH_PRIVATE_KEY_SRC}" ]] &>/dev/null; then + ssh_setup +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 if [[ -n "${GPG_PRIVATE_KEY}" ]] &>/dev/null; then diff --git a/src/sync_common.sh b/src/sync_common.sh index 7d29036..3a26f99 100755 --- a/src/sync_common.sh +++ b/src/sync_common.sh @@ -41,15 +41,15 @@ function info() { } ####################################### -# Executes commands defined within yml file +# Executes commands defined within yml file or env variable # Arguments: # hook -> the hook to use # ####################################3# -function cmd_from_yml_file() { +function cmd_from_yml() { local FILE_NAME="templatesync.yml" local HOOK=$1 - local YML_PATH=".hooks.${HOOK}.commands" + local YML_PATH_SUFF=".${HOOK}.commands" if [ "$IS_ALLOW_HOOKS" != "true" ]; then debug "execute cmd hooks not enabled" @@ -60,7 +60,19 @@ function cmd_from_yml_file() { err "yaml query yq is not installed. 'https://mikefarah.gitbook.io/yq/'"; exit 1; fi - readarray cmd_Arr < <(yq "${YML_PATH} | .[]" "${FILE_NAME}") + + if [[ -n "${HOOKS}" ]]; then + debug "hooks input variable is set. Using the variable" + echo "${HOOKS}" > "tmp.${FILE_NAME}" + YML_PATH="${YML_PATH_SUFF}" + else + cp ${FILE_NAME} "tmp.${FILE_NAME}" + YML_PATH=".hooks${YML_PATH_SUFF}" + fi + + readarray cmd_Arr < <(yq "${YML_PATH} | .[]" "tmp.${FILE_NAME}") + + rm "tmp.${FILE_NAME}" for key in "${cmd_Arr[@]}"; do echo "${key}" | bash; done fi diff --git a/src/sync_template.sh b/src/sync_template.sh index 25f4db6..28b54e6 100644 --- a/src/sync_template.sh +++ b/src/sync_template.sh @@ -7,6 +7,10 @@ set -e # shellcheck source=src/sync_template.sh source sync_common.sh +############################################ +# Prechecks +############################################ + if [[ -z "${PR_COMMIT_MSG}" ]]; then err "Missing env variable 'PR_COMMIT_MSG'"; exit 1; @@ -22,6 +26,10 @@ if ! [ -x "$(command -v gh)" ]; then exit 1; fi +######################################################## +# Variables +######################################################## + if [[ -z "${UPSTREAM_BRANCH}" ]]; then UPSTREAM_BRANCH="$(git remote show origin | awk '/HEAD branch/ {print $NF}')" info "Missing env variable 'UPSTREAM_BRANCH' setting to remote default ${UPSTREAM_BRANCH}"; @@ -34,7 +42,7 @@ fi GIT_REMOTE_PULL_PARAMS="${GIT_REMOTE_PULL_PARAMS:---allow-unrelated-histories --squash --strategy=recursive -X theirs}" -cmd_from_yml_file "install" +cmd_from_yml "install" LOCAL_CURRENT_GIT_HASH=$(git rev-parse HEAD) @@ -49,6 +57,10 @@ debug "new Git HASH ${NEW_TEMPLATE_GIT_HASH}" echo "::group::Check new changes" +##################################################### +# Functions +##################################################### + function set_github_action_outputs() { echo "::group::set gh action outputs" if [[ -z "${GITHUB_RUN_ID}" ]]; then @@ -71,6 +83,73 @@ function check_branch_remote_existing() { fi } +function force_delete_files() { + echo "::group::force file deletion" + 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 + + echo "::endgroup::" +} + +function cleanup_older_prs () { + older_prs=$(gh pr list \ + --base "${UPSTREAM_BRANCH}" \ + --state open \ + --label "${PR_LABELS}" \ + --json number \ + --template '{{range .}}{{printf "%v" .number}}{{"\n"}}{{end}}') + + for older_pr in $older_prs + do + gh pr close "$older_pr" + debug "Closed PR #${older_pr}" + done +} + +function maybe_create_labels () { + 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 \ + --search "${label}" \ + --limit 1 \ + --json name \ + --template '{{range .}}{{printf "%v" .name}}{{"\n"}}{{end}}') + + if [ "${search_result}" = "${label##[[:space:]]}" ]; then + info "label '${label##[[:space:]]}' was found in the repository" + else + if gh label create "${label}"; then + info "label '${label}' was missing and has been created" + else + warn "label creation did not work. For any reason the former check sometimes is failing" + fi + fi + done +} + +function push () { + debug "push changes" + git push --set-upstream origin "${NEW_BRANCH}" +} + +function create_pr () { + gh pr create \ + --title "${PR_TITLE}" \ + --body "${PR_BODY}" \ + --base "${UPSTREAM_BRANCH}" \ + --label "${PR_LABELS}" \ + --reviewer "${PR_REVIEWERS}" +} + +######################################################## +# Logic +####################################################### + check_branch_remote_existing git cat-file -e "${TEMPLATE_REMOTE_GIT_HASH}" || COMMIT_NOT_IN_HIST=true @@ -81,7 +160,7 @@ fi echo "::endgroup::" -cmd_from_yml_file "prepull" +cmd_from_yml "prepull" echo "::group::Pull template" @@ -113,23 +192,11 @@ if [ -s "${TEMPLATE_SYNC_IGNORE_FILE_PATH}" ]; then echo "::endgroup::" fi -function force_delete_files() { - echo "::group::force file deletion" - 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 - - echo "::endgroup::" -} - if [ "$IS_FORCE_DELETION" == "true" ]; then force_delete_files fi -cmd_from_yml_file "precommit" +cmd_from_yml "precommit" echo "::group::commit changes" @@ -157,20 +224,6 @@ git commit --signoff -m "${PR_COMMIT_MSG}" echo "::endgroup::" -function cleanup_older_prs () { - older_prs=$(gh pr list \ - --base "${UPSTREAM_BRANCH}" \ - --state open \ - --label "${PR_LABELS}" \ - --json number \ - --template '{{range .}}{{printf "%v" .number}}{{"\n"}}{{end}}') - - for older_pr in $older_prs - do - gh pr close "$older_pr" - debug "Closed PR #${older_pr}" - done -} echo "::group::cleanup older PRs" if [ "$IS_DRY_RUN" != "true" ]; then @@ -178,7 +231,7 @@ if [ "$IS_DRY_RUN" != "true" ]; then if [[ -z "${PR_LABELS}" ]]; then warn "env var 'PR_LABELS' is empty. Skipping older prs cleanup" else - cmd_from_yml_file "precleanup" + cmd_from_yml "precleanup" cleanup_older_prs fi else @@ -190,29 +243,6 @@ fi echo "::endgroup::" - -function maybe_create_labels () { - 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 \ - --search "${label}" \ - --limit 1 \ - --json name \ - --template '{{range .}}{{printf "%v" .name}}{{"\n"}}{{end}}') - - if [ "${search_result}" = "${label##[[:space:]]}" ]; then - info "label '${label##[[:space:]]}' was found in the repository" - else - if gh label create "${label}"; then - info "label '${label}' was missing and has been created" - else - warn "label creation did not work. For any reason the former check sometimes is failing" - fi - fi - done -} - echo "::group::check for missing labels" if [[ -z "${PR_LABELS}" ]]; then @@ -227,26 +257,12 @@ fi echo "::endgroup::" -function push () { - debug "push changes" - git push --set-upstream origin "${NEW_BRANCH}" -} - -function create_pr () { - gh pr create \ - --title "${PR_TITLE}" \ - --body "${PR_BODY}" \ - --base "${UPSTREAM_BRANCH}" \ - --label "${PR_LABELS}" \ - --reviewer "${PR_REVIEWERS}" -} - echo "::group::push changes and create PR" if [ "$IS_DRY_RUN" != "true" ]; then - cmd_from_yml_file "prepush" + cmd_from_yml "prepush" push - cmd_from_yml_file "prepr" + cmd_from_yml "prepr" create_pr else warn "dry_run option is set to off. Skipping push changes and skip create pr"