diff --git a/.github/workflows/push_docker.yml b/.github/workflows/push_docker.yml index bfa70b65..39ea2833 100644 --- a/.github/workflows/push_docker.yml +++ b/.github/workflows/push_docker.yml @@ -65,7 +65,7 @@ jobs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - name: Docker Hub Description - uses: peter-evans/dockerhub-description@v3 + uses: peter-evans/dockerhub-description@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index ea215907..e9c04d0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## [1.5.0](https://github.com/AndreasAugustin/actions-template-sync/compare/v1.4.0...v1.5.0) (2024-02-04) + + +### Features + +* :sparkles: ([#401](https://github.com/AndreasAugustin/actions-template-sync/issues/401)) add option to force file deletion ([#435](https://github.com/AndreasAugustin/actions-template-sync/issues/435)) ([e68941f](https://github.com/AndreasAugustin/actions-template-sync/commit/e68941ff0fc9695be3fe480ab739ebeb318dca0d)) + +## [1.4.0](https://github.com/AndreasAugustin/actions-template-sync/compare/v1.3.0...v1.4.0) (2024-01-25) + + +### Features + +* allow for pruning of older PRs ([#438](https://github.com/AndreasAugustin/actions-template-sync/issues/438)) ([0e51714](https://github.com/AndreasAugustin/actions-template-sync/commit/0e51714bd42e4ce8223a641d7435c220a99aad51)) + ## [1.3.0](https://github.com/AndreasAugustin/actions-template-sync/compare/v1.2.0...v1.3.0) (2024-01-18) diff --git a/Dockerfile b/Dockerfile index 4852e4f5..a69b45d3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ ####################################### # image for dev build environment ###################################### -FROM alpine:3.19.0 as dev +FROM alpine:3.19.1 as dev ARG GH_CLI_VER=2.34.0 diff --git a/README.md b/README.md index d14d82aa..779a38d3 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ jobs: github_token: ${{ secrets.GITHUB_TOKEN }} source_repo_path: upstream_branch: # defaults to main - pr_labels: ,[,...] # optional, no default + pr_labels: ,[,...] # defaults to template_sync ``` You will receive a pull request within your repository if there are some changes available in the template. @@ -139,7 +139,7 @@ jobs: github_token: ${{ steps.generate_token.outputs.token }} source_repo_path: upstream_branch: # defaults to main - pr_labels: ,[,...] # optional, no default + pr_labels: ,[,...] # defaults to template_sync ``` #### 2. Using SSH @@ -175,7 +175,7 @@ jobs: github_token: ${{ secrets.GITHUB_TOKEN }} source_repo_path: ${{ secrets.SOURCE_REPO_PATH }} # , should be within secrets upstream_branch: ${{ secrets.TARGET_BRANCH }} # # defaults to main - pr_labels: ,[,...] # optional, no default + pr_labels: ,[,...] # defaults to template_sync source_repo_ssh_private_key: ${{ secrets.SOURCE_REPO_SSH_PRIVATE_KEY }} # contains the private ssh key of the private repository ``` @@ -232,24 +232,26 @@ jobs: ### Configuration parameters -| Variable | Description | Required | `[Default]` | -|----|----|----|----| -| github_token | Token for the repo. Can be passed in using `$\{{ secrets.GITHUB_TOKEN }}` | `true` | | -| source_repo_path | Repository path of the template | `true` | | -| upstream_branch | The target branch | `false` | `` | -| source_repo_ssh_private_key | `[optional]` private ssh key for the source repository. [see](#private-template-repository)| `false` | | -| pr_branch_name_prefix | `[optional]` the prefix of branches created by this action | `false` | `chore/template_sync` | -| pr_title | `[optional]` the title of PRs opened by this action. Must be already created. | `false` | `upstream merge template repository` | -| pr_labels | `[optional]` comma separated list. [pull request labels][pr-labels]. Must be already created. | `false` | | -| pr_reviewers | `[optional]` comma separated list of pull request reviewers. | `false` | | -| pr_commit_msg | `[optional]` commit message in the created pull request | `false` | `chore(template): merge template changes :up:` | -| 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` | -| is_not_source_github | `[optional]` set to `true` if the source git provider is not GitHub | `false` | `false` | -| git_user_name | `[optional]` set the committer git user.name | `false` | `${GITHUB_ACTOR}` | -| git_user_email | `[optional]` set the committer git user.email | `false` | `github-action@actions-template-sync.noreply.${SOURCE_REPO_HOSTNAME}` | -| git_remote_pull_params |`[optional]` set remote pull parameters | `false` | `--allow-unrelated-histories --squash --strategy=recursive -X theirs` | +| Variable | Description | Required | `[Default]` | +|-----------------------------|---------------------------------------------------------------------------------------------------------------|----------|-----------------------------------------------------------------------| +| github_token | Token for the repo. Can be passed in using `$\{{ secrets.GITHUB_TOKEN }}` | `true` | | +| source_repo_path | Repository path of the template | `true` | | +| upstream_branch | The target branch | `false` | `` | +| source_repo_ssh_private_key | `[optional]` private ssh key for the source repository. [see](#private-template-repository) | `false` | | +| pr_branch_name_prefix | `[optional]` the prefix of branches created by this action | `false` | `chore/template_sync` | +| pr_title | `[optional]` the title of PRs opened by this action. Must be already created. | `false` | `upstream merge template repository` | +| pr_labels | `[optional]` comma separated list. [pull request labels][pr-labels]. | `false` | `sync_template` | +| pr_reviewers | `[optional]` comma separated list of pull request reviewers. | `false` | | +| pr_commit_msg | `[optional]` commit message in the created pull request | `false` | `chore(template): merge template changes :up:` | +| 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` | +| 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` | +| git_user_name | `[optional]` set the committer git user.name | `false` | `${GITHUB_ACTOR}` | +| git_user_email | `[optional]` set the committer git user.email | `false` | `github-action@actions-template-sync.noreply.${SOURCE_REPO_HOSTNAME}` | +| git_remote_pull_params | `[optional]` set remote pull parameters | `false` | `--allow-unrelated-histories --squash --strategy=recursive -X theirs` | | gpg_private_key | `[optional]` set if you want to sign commits | `false` | | | gpg_passphrase | `[optional]` set if your optionial gpg private key has a passphrase | `false` | | @@ -364,6 +366,7 @@ The following hooks are supported (please check [docs/ARCHITECTURE.md](docs/ARCH * `prepull` is executed before the code is pulled from the source repository * `precommit` is executed before the code is commited * `prepush` is executed before the push is executed, right after the commit +* `precleanup` is executed before older PRs targeting the same branch are closed * `prepr` is executed before the PR is done **Remark** The underlying OS is defined by an Alpine container. @@ -389,12 +392,36 @@ hooks: commands: - echo 'hi, we are within the prepush phase' - echo 'maybe you want to add further changes and commits' + precleanup: + commands: + - echo 'hi, we are within the precleanup phase' + - echo 'maybe you want to interact with older PRs before they are closed' prepr: commands: - echo 'hi, we are within the prepr phase' - echo 'maybe you want to change the code a bit and do another push before creating the pr' ``` +## Labels creation + +By default, generated PRs will be labeled with the `template_sync` label. +If that label doesn't exist in your repository, it will be created automatically unless you specify your own existing labels. +Associating a label with the generated PRs helps keeping track of them and allows for features like automatic PR cleanup. + +## Pull request cleanup + +Depending on your way of working, you may end up with multiple pull requests related to template syncing pointing to the same branch. +If you want to avoid this situation, you can instruct this action to clean up older PRs (search based on labels defined with the `pr_labels` config parameter). + +:warning: this feature will close all pull requests with labels configured with `pr_labels` config parameter. + +## Force deletion + +This feature will force delete files if those are deelted within the source repository. + +:warning: it is highly related to the `git_remote_pull_params` config parameter and won't work with the default. +You need to change the default one e.g. to `git_remote_pull_params: --allow-unrelated-histories --strategy=recursive --no-edit`. + ## Troubleshooting * refusing to allow a GitHub App to create or update workflow `.github/workflows/******.yml` without `workflows` permission diff --git a/action.yml b/action.yml index a6e72f9e..eb1d071d 100644 --- a/action.yml +++ b/action.yml @@ -23,6 +23,7 @@ inputs: default: "upstream merge template repository" pr_labels: description: "[optional] comma separated list of pull request labels" + default: "template_sync" pr_reviewers: description: "[optional] comma separated list of pull request reviewers" pr_commit_msg: @@ -36,9 +37,15 @@ inputs: is_allow_hooks: description: "[optional] set to true if you want to allow hooks. Use this functionality with caution!" default: "false" + is_pr_cleanup: + description: "[optional] set to true if you want to cleanup older PRs targeting the same branch." + default: "false" is_not_source_github: description: "[optional] set to true if the source repository is not a github related repository. Useful e.q. if the source is GitLab" default: "false" + is_force_deletion: + description: "[optional] set to true if you want to force delete files which are deleted within the source repository even if they contain changes" + default: "false" git_user_name: description: "[optional] set the committer git user.name for the merge commit" git_user_email: @@ -65,7 +72,9 @@ runs: HOSTNAME: ${{ inputs.hostname }} IS_DRY_RUN: ${{ inputs.is_dry_run }} IS_ALLOW_HOOKS: ${{ inputs.is_allow_hooks }} + IS_PR_CLEANUP: ${{ inputs.is_pr_cleanup}} IS_NOT_SOURCE_GITHUB: ${{ inputs.is_not_source_github }} + IS_FORCE_DELETION: ${{ inputs.is_force_deletion }} GIT_USER_NAME: ${{ inputs.git_user_name }} GIT_USER_EMAIL: ${{ inputs.git_user_email }} GIT_REMOTE_PULL_PARAMS: ${{ inputs.git_remote_pull_params }} diff --git a/src/Dockerfile b/src/Dockerfile index c50b17e0..34fd8acb 100644 --- a/src/Dockerfile +++ b/src/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.19.0 +FROM alpine:3.19.1 ARG GH_CLI_VER=2.34.0 diff --git a/src/sync_template.sh b/src/sync_template.sh index 2a07ff93..d4cb6921 100644 --- a/src/sync_template.sh +++ b/src/sync_template.sh @@ -36,6 +36,10 @@ GIT_REMOTE_PULL_PARAMS="${GIT_REMOTE_PULL_PARAMS:---allow-unrelated-histories -- cmd_from_yml_file "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}") @@ -66,6 +70,7 @@ echo "::endgroup::" cmd_from_yml_file "prepull" echo "::group::Pull template" + debug "create new branch from default branch with name ${NEW_BRANCH}" git checkout -b "${NEW_BRANCH}" debug "pull changes from template" @@ -94,9 +99,26 @@ 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" echo "::group::commit changes" + git add . # we are checking the ignore file if it exists or is empty @@ -121,26 +143,96 @@ git commit --signoff -m "${PR_COMMIT_MSG}" echo "::endgroup::" -function push_and_create_pr () { - cmd_from_yml_file "prepush" - if [ "$IS_DRY_RUN" != "true" ]; then +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 + if [ "$IS_PR_CLEANUP" != "false" ]; then + if [[ -z "${PR_LABELS}" ]]; then + warn "env var 'PR_LABELS' is empty. Skipping older prs cleanup" + else + cmd_from_yml_file "precleanup" + cleanup_older_prs + 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 "::group::push changes and create PR" - debug "push changes" - git push --set-upstream origin "${NEW_BRANCH}" +echo "::endgroup::" - cmd_from_yml_file "prepr" - gh pr create \ - --title "${PR_TITLE}" \ - --body "Merge ${SOURCE_REPO_PATH} ${NEW_TEMPLATE_GIT_HASH}" \ - --base "${UPSTREAM_BRANCH}" \ - --label "${PR_LABELS}" \ - --reviewer "${PR_REVIEWERS}" - echo "::endgroup::" +function maybe_create_labels () { + all_labels=${PR_LABELS//,/$'\n'} + for label in $all_labels + do + search_result=$(gh label list \ + --search "${label}" \ + --limit 1 \ + --json name \ + --template '{{range .}}{{printf "%v" .name}}{{"\n"}}{{end}}') + + if [ "${search_result}" = "${label}" ]; then + info "label '${label}' was found in the repository" + else + gh label create "${label}" + info "label '${label}' was missing and has been created" + fi + done +} + +echo "::group::check for missing labels" + +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 push changes and skip create pr" + warn "dry_run option is set to off. Skipping labels check" fi +fi + +echo "::endgroup::" + +function push () { + debug "push changes" + git push --set-upstream origin "${NEW_BRANCH}" } -push_and_create_pr +function create_pr () { + gh pr create \ + --title "${PR_TITLE}" \ + --body "Merge ${SOURCE_REPO_PATH} ${NEW_TEMPLATE_GIT_HASH}" \ + --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" + push + cmd_from_yml_file "prepr" + create_pr +else + warn "dry_run option is set to off. Skipping push changes and skip create pr" +fi + +echo "::endgroup::"