diff --git a/README.md b/README.md index 76b907a6..26b6a6e9 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,25 @@ 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` | +| 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` | ### Docker @@ -304,6 +305,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. @@ -329,12 +331,28 @@ 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. + ## 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 a4a23b5d..760b5786 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,6 +37,9 @@ 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" @@ -61,6 +65,7 @@ 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 }} GIT_USER_NAME: ${{ inputs.git_user_name }} GIT_USER_EMAIL: ${{ inputs.git_user_email }} diff --git a/src/sync_template.sh b/src/sync_template.sh index e114ef4d..cb819987 100644 --- a/src/sync_template.sh +++ b/src/sync_template.sh @@ -66,6 +66,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" @@ -97,6 +98,7 @@ 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 +123,96 @@ git commit -m "${PR_COMMIT_MSG}" echo "::endgroup::" -push_and_create_pr () { - cmd_from_yml_file "prepush" - if [ "$IS_DRY_RUN" != "true" ]; then +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 "::endgroup::" - echo "::group::push changes and create PR" - debug "push changes" - git push --set-upstream origin "${NEW_BRANCH}" - cmd_from_yml_file "prepr" +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" - 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::" +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::" + +push () { + debug "push changes" + git push --set-upstream origin "${NEW_BRANCH}" +} + +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}" } -push_and_create_pr +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::"