Update from Template #15
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name: Update from Template | |
# This workflow keeps the repo up to date with changes from the template repo (REMOTE_URL) | |
# It duplicates the REMOTE_BRANCH (into UPDATE_BRANCH) and tries to merge it into | |
# this repos default branch (which is checked out here) | |
# Note that this requires a PAT (Personal Access Token) - at best from a servicing account | |
# PAT permissions: read:discussion, read:org, repo, workflow | |
# Also note that you should have at least once merged the template repo into the current repo manually | |
# otherwise a "refusing to merge unrelated histories" error might occur. | |
on: | |
schedule: | |
- cron: '55 2 * * 1' | |
workflow_dispatch: | |
inputs: | |
no_automatic_merge: | |
type: boolean | |
description: 'No automatic merge' | |
default: false | |
env: | |
UPDATE_BRANCH: update-from-template | |
UPDATE_BRANCH_MERGED: update-from-template-merged | |
REMOTE_URL: https://github.com/xdev-software/standard-maven-template.git | |
REMOTE_BRANCH: master | |
permissions: | |
contents: write | |
pull-requests: write | |
jobs: | |
update: | |
runs-on: ubuntu-latest | |
outputs: | |
update_branch_merged_commit: ${{ steps.manage-branches.outputs.update_branch_merged_commit }} | |
create_update_branch_merged_pr: ${{ steps.manage-branches.outputs.create_update_branch_merged_pr }} | |
steps: | |
- uses: actions/checkout@v4 | |
with: | |
# Required because otherwise there are always changes detected when executing diff/rev-list | |
fetch-depth: 0 | |
# If no PAT is used the following error occurs on a push: | |
# refusing to allow a GitHub App to create or update workflow `.github/workflows/xxx.yml` without `workflows` permission | |
token: ${{ secrets.UPDATE_FROM_TEMPLATE_PAT }} | |
- name: Init Git | |
run: | | |
git config --global user.email "[email protected]" | |
git config --global user.name "XDEV Bot" | |
- name: Manage branches | |
id: manage-branches | |
run: | | |
echo "Adding remote template-repo" | |
git remote add template ${{ env.REMOTE_URL }} | |
echo "Fetching remote template repo" | |
git fetch template | |
echo "Deleting local branches that will contain the updates - if present" | |
git branch -D ${{ env.UPDATE_BRANCH }} || true | |
git branch -D ${{ env.UPDATE_BRANCH_MERGED }} || true | |
echo "Checking if the remote template repo has new commits" | |
git rev-list ..template/${{ env.REMOTE_BRANCH }} | |
if [ $(git rev-list --count ..template/${{ env.REMOTE_BRANCH }}) -eq 0 ]; then | |
echo "There are no commits new commits on the template repo" | |
echo "Deleting origin branch(es) that contain the updates - if present" | |
git push -f origin --delete ${{ env.UPDATE_BRANCH }} || true | |
git push -f origin --delete ${{ env.UPDATE_BRANCH_MERGED }} || true | |
echo "create_update_branch_pr=0" >> $GITHUB_OUTPUT | |
echo "create_update_branch_merged_pr=0" >> $GITHUB_OUTPUT | |
exit 0 | |
fi | |
echo "Found new commits on the template repo" | |
echo "Creating update branch" | |
git branch ${{ env.UPDATE_BRANCH }} template/${{ env.REMOTE_BRANCH }} | |
git branch --unset-upstream ${{ env.UPDATE_BRANCH }} | |
echo "Pushing update branch" | |
git push -f -u origin ${{ env.UPDATE_BRANCH }} | |
echo "Getting base branch" | |
base_branch=$(git branch --show-current) | |
echo "Base branch is $base_branch" | |
echo "base_branch=$base_branch" >> $GITHUB_OUTPUT | |
echo "Trying to create auto-merged branch ${{ env.UPDATE_BRANCH_MERGED }}" | |
git branch ${{ env.UPDATE_BRANCH_MERGED }} ${{ env.UPDATE_BRANCH }} | |
git checkout ${{ env.UPDATE_BRANCH_MERGED }} | |
echo "Merging branch $base_branch into ${{ env.UPDATE_BRANCH_MERGED }}" | |
git merge $base_branch && merge_exit_code=$? || merge_exit_code=$? | |
if [ $merge_exit_code -ne 0 ]; then | |
echo "Auto merge failed! Manual merge required" | |
echo "::notice ::Auto merge failed - Manual merge required" | |
echo "Cleaning up failed merge" | |
git merge --abort | |
git checkout $base_branch | |
git branch -D ${{ env.UPDATE_BRANCH_MERGED }} || true | |
echo "Deleting auto-merge branch - if present" | |
git push -f origin --delete ${{ env.UPDATE_BRANCH_MERGED }} || true | |
echo "create_update_branch_pr=1" >> $GITHUB_OUTPUT | |
echo "create_update_branch_merged_pr=0" >> $GITHUB_OUTPUT | |
exit 0 | |
fi | |
echo "Post processing: Trying to automatically fill in template variables" | |
find . -type f \ | |
-not -path "./.git/**" \ | |
-not -path "./.github/workflows/update-from-template.yml" -print0 \ | |
| xargs -0 sed -i "s/template-placeholder/${GITHUB_REPOSITORY#*/}/g" | |
git status | |
git add --all | |
if [[ "$(git status --porcelain)" != "" ]]; then | |
echo "Filled in template; Committing" | |
git commit -m "Fill in template" | |
fi | |
echo "Pushing auto-merged branch" | |
git push -f -u origin ${{ env.UPDATE_BRANCH_MERGED }} | |
echo "update_branch_merged_commit=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT | |
echo "Restoring base branch $base_branch" | |
git checkout $base_branch | |
echo "create_update_branch_pr=0" >> $GITHUB_OUTPUT | |
echo "create_update_branch_merged_pr=1" >> $GITHUB_OUTPUT | |
echo "try_close_update_branch_pr=1" >> $GITHUB_OUTPUT | |
- name: Create/Update PR update_branch | |
if: steps.manage-branches.outputs.create_update_branch_pr == 1 | |
env: | |
GH_TOKEN: ${{ secrets.UPDATE_FROM_TEMPLATE_PAT }} | |
run: | | |
gh_pr_up() { | |
gh pr create -H "${{ env.UPDATE_BRANCH }}" "$@" || (git checkout "${{ env.UPDATE_BRANCH }}" && gh pr edit "$@") | |
} | |
gh_pr_up -B "${{ steps.manage-branches.outputs.base_branch }}" \ | |
--title "Update from template" \ | |
--body "An automated PR to sync changes from the template into this repo" | |
# Ensure that only a single PR is open (otherwise confusion and spam) | |
- name: Close PR update_branch | |
if: steps.manage-branches.outputs.try_close_update_branch_pr == 1 | |
env: | |
GH_TOKEN: ${{ secrets.UPDATE_FROM_TEMPLATE_PAT }} | |
run: | | |
gh pr close "${{ env.UPDATE_BRANCH }}" || true | |
- name: Create/Update PR update_branch_merged | |
if: steps.manage-branches.outputs.create_update_branch_merged_pr == 1 | |
env: | |
GH_TOKEN: ${{ secrets.UPDATE_FROM_TEMPLATE_PAT }} | |
run: | | |
gh_pr_up() { | |
gh pr create -H "${{ env.UPDATE_BRANCH_MERGED }}" "$@" || (git checkout "${{ env.UPDATE_BRANCH_MERGED }}" && gh pr edit "$@") | |
} | |
gh_pr_up -B "${{ steps.manage-branches.outputs.base_branch }}" \ | |
--title "Update from template (auto-merged)" \ | |
--body "An automated PR to sync changes from the template into this repo" | |
# Wait a moment so that checks of PR have higher prio than following job | |
sleep 3 | |
# Split into two jobs to help with executor starvation | |
auto-merge: | |
needs: [update] | |
if: needs.update.outputs.create_update_branch_merged_pr == 1 | |
runs-on: ubuntu-latest | |
steps: | |
- uses: actions/checkout@v4 | |
with: | |
# Required because otherwise there are always changes detected when executing diff/rev-list | |
fetch-depth: 0 | |
# If no PAT is used the following error occurs on a push: | |
# refusing to allow a GitHub App to create or update workflow `.github/workflows/xxx.yml` without `workflows` permission | |
token: ${{ secrets.UPDATE_FROM_TEMPLATE_PAT }} | |
- name: Init Git | |
run: | | |
git config --global user.email "[email protected]" | |
git config --global user.name "XDEV Bot" | |
- name: Checking if auto-merge for PR update_branch_merged can be done | |
id: auto-merge-check | |
env: | |
GH_TOKEN: ${{ secrets.UPDATE_FROM_TEMPLATE_PAT }} | |
run: | | |
not_failed_conclusion="skipped|neutral|success" | |
not_relevant_app_slug="dependabot|github-pages|sonarcloud" | |
echo "Waiting for checks to start..." | |
sleep 40s | |
for i in {1..20}; do | |
echo "Checking if PR can be auto-merged. Try: $i" | |
echo "Checking if update-branch-merged exists" | |
git fetch | |
if [[ $(git rev-parse origin/${{ env.UPDATE_BRANCH_MERGED }}) ]]; then | |
echo "Branch still exists; Continuing..." | |
else | |
echo "Branch origin/${{ env.UPDATE_BRANCH_MERGED }} is missing" | |
exit 0 | |
fi | |
echo "Fetching checks" | |
cs_response=$(curl -sL \ | |
--fail-with-body \ | |
--connect-timeout 60 \ | |
--max-time 120 \ | |
-H "Accept: application/vnd.github+json" \ | |
-H "Authorization: Bearer $GH_TOKEN" \ | |
-H "X-GitHub-Api-Version: 2022-11-28" \ | |
https://api.github.com/repos/${{ github.repository }}/commits/${{ needs.update.outputs.update_branch_merged_commit }}/check-suites) | |
cs_data=$(echo $cs_response | jq '.check_suites[] | { conclusion: .conclusion, slug: .app.slug, check_runs_url: .check_runs_url }') | |
echo $cs_data | |
if [[ -z "$cs_data" ]]; then | |
echo "No check suite data - Assuming that there are no checks to run" | |
echo "perform=1" >> $GITHUB_OUTPUT | |
exit 0 | |
fi | |
cs_failed=$(echo $cs_data | jq --arg x "$not_failed_conclusion" 'select ((.conclusion == null or (.conclusion | test($x))) | not)') | |
if [[ -z "$cs_failed" ]]; then | |
echo "No check failed so far; Checking if relevant checks are still running" | |
cs_relevant_still_running=$(echo $cs_data | jq --arg x "$not_relevant_app_slug" 'select (.conclusion == null and (.slug | test($x) | not))') | |
if [[ -z $cs_relevant_still_running ]]; then | |
echo "All relevant checks finished - PR can be merged" | |
echo "perform=1" >> $GITHUB_OUTPUT | |
exit 0 | |
else | |
echo "Relevant checks are still running" | |
echo $cs_relevant_still_running | |
fi | |
else | |
echo "Detected failed check" | |
echo $cs_failed | |
echo "perform=0" >> $GITHUB_OUTPUT | |
exit 0 | |
fi | |
echo "Waiting before next run..." | |
sleep 30s | |
done | |
echo "Timed out - Assuming executor starvation - Forcing merge" | |
echo "perform=1" >> $GITHUB_OUTPUT | |
- name: Auto-merge update_branch_merged | |
if: steps.auto-merge-check.outputs.perform == 1 | |
run: | | |
echo "Getting base branch" | |
base_branch=$(git branch --show-current) | |
echo "Base branch is $base_branch" | |
echo "Fetching..." | |
git fetch | |
if [[ $(git rev-parse origin/${{ env.UPDATE_BRANCH_MERGED }}) ]]; then | |
echo "Branch still exists; Continuing..." | |
else | |
echo "Branch origin/${{ env.UPDATE_BRANCH_MERGED }} is missing" | |
exit 0 | |
fi | |
expected_commit="${{ needs.update.outputs.update_branch_merged_commit }}" | |
actual_commit=$(git rev-parse origin/${{ env.UPDATE_BRANCH_MERGED }}) | |
if [[ "$expected_commit" != "$actual_commit" ]]; then | |
echo "Branch ${{ env.UPDATE_BRANCH_MERGED }} contains unexpected commit $actual_commit" | |
echo "Expected: $expected_commit" | |
exit 0 | |
fi | |
echo "Ensuring that current branch $base_branch is up-to-date" | |
git pull | |
echo "Merging origin/${{ env.UPDATE_BRANCH_MERGED }} into $base_branch" | |
git merge origin/${{ env.UPDATE_BRANCH_MERGED }} && merge_exit_code=$? || merge_exit_code=$? | |
if [ $merge_exit_code -ne 0 ]; then | |
echo "Unexpected merge failure $merge_exit_code - Requires manual resolution" | |
exit 0 | |
fi | |
if [[ "${{ inputs.no_automatic_merge }}" == "true" ]]; then | |
echo "Exiting due no_automatic_merge" | |
exit 0 | |
fi | |
echo "Pushing" | |
git push | |
echo "Cleaning up" | |
git branch -D ${{ env.UPDATE_BRANCH }} || true | |
git branch -D ${{ env.UPDATE_BRANCH_MERGED }} || true | |
git push -f origin --delete ${{ env.UPDATE_BRANCH }} || true | |
git push -f origin --delete ${{ env.UPDATE_BRANCH_MERGED }} || true |