1111 description : ' Branch of the project repo to update'
1212 required : true
1313 type : string
14+ update_workflows :
15+ description : ' Include .github/workflows updates from template?'
16+ required : false
17+ type : boolean
18+ default : false
1419
1520permissions :
1621 contents : write
@@ -25,164 +30,191 @@ jobs:
2530 new_sha : ${{ steps.new_sha.outputs.sha }}
2631
2732 steps :
28- - name : Checkout project repo
29- uses : actions/checkout@v4
30- with :
31- repository : ${{ github.repository }}
32- ref : ${{ inputs.repo_branch }}
33- sparse-checkout : .cookiecutter.json
34- sparse-checkout-cone-mode : false
35-
36- - name : Read old template SHA
37- id : old_sha
38- run : |
39- SHA=$(jq -r '.cookiecutter.template_sha // empty' .cookiecutter.json)
40- if [ -z "$SHA" ]; then
41- echo "::error ::.cookiecutter.json is missing cookiecutter.template_sha"
42- exit 1
43- fi
44- echo "sha=$SHA" >> "$GITHUB_OUTPUT"
45-
46- - name : Fetch new template SHA
47- id : new_sha
48- run : |
49- echo "sha=$(git ls-remote '${{ inputs.template_repo }}' HEAD | cut -f1)" >> "$GITHUB_OUTPUT"
50-
51- - name : Compare SHAs
52- id : compare
53- run : |
54- if [ "${{ steps.old_sha.outputs.sha }}" = "${{ steps.new_sha.outputs.sha }}" ]; then
55- echo "✂️ SHA unchanged; nothing to update."
56- echo "sha_changed=false" >> "$GITHUB_OUTPUT"
57- else
58- echo "sha_changed=true" >> "$GITHUB_OUTPUT"
59- fi
33+ - name : Checkout project repo
34+ uses : actions/checkout@v4
35+ with :
36+ repository : ${{ github.repository }}
37+ ref : ${{ inputs.repo_branch }}
38+ sparse-checkout : .cookiecutter.json
39+ sparse-checkout-cone-mode : false
40+
41+ - name : Read old template SHA
42+ id : old_sha
43+ run : |
44+ SHA=$(jq -r '.cookiecutter.template_sha // empty' .cookiecutter.json)
45+ if [ -z "$SHA" ]; then
46+ echo "::error ::.cookiecutter.json is missing cookiecutter.template_sha"
47+ exit 1
48+ fi
49+ echo "sha=$SHA" >> "$GITHUB_OUTPUT"
50+
51+ - name : Fetch new template SHA
52+ id : new_sha
53+ run : |
54+ echo "sha=$(git ls-remote '${{ inputs.template_repo }}' HEAD | cut -f1)" >> "$GITHUB_OUTPUT"
55+
56+ - name : Compare SHAs
57+ id : compare
58+ run : |
59+ if [ "${{ steps.old_sha.outputs.sha }}" = "${{ steps.new_sha.outputs.sha }}" ]; then
60+ echo "✂️ SHA unchanged; nothing to update."
61+ echo "sha_changed=false" >> "$GITHUB_OUTPUT"
62+ else
63+ echo "sha_changed=true" >> "$GITHUB_OUTPUT"
64+ fi
6065
6166 template-update :
6267 needs : check-update
6368 if : needs.check-update.outputs.sha_changed == 'true'
6469 runs-on : ubuntu-latest
6570
6671 steps :
67- - name : Checkout project repo
68- uses : actions/checkout@v4
69- with :
70- repository : ${{ github.repository }}
71- ref : ${{ inputs.repo_branch }}
72- fetch-depth : 0
73- submodules : false
74-
75- - name : Set up Python
76- uses : actions/setup-python@v5
77- with :
78- python-version : ' 3.x'
79-
80- - name : Install dependencies
81- run : pip install cookiecutter
82-
83- - name : Check if update branch already exists
84- run : |
85- BRANCH="template-update-${{ needs.check-update.outputs.new_sha }}"
86- if git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null 2>&1; then
87- echo "::error ::Branch '$BRANCH' already exists. A template update PR may already be open."
88- exit 1
89- fi
90-
91- - name : Clone template at new SHA
92- run : git clone '${{ inputs.template_repo }}' /tmp/template-source
93-
94- - name : Run cookiecutter replay
95- run : |
96- rm -rf ~/.cookiecutters/*
97- cookiecutter /tmp/template-source \
98- --replay-file .cookiecutter.json \
99- --overwrite-if-exists \
100- --output-dir /tmp/rendered
101-
102- - name : Get rendered project directory name
103- id : rendered_dir
104- run : |
105- DIR=$(ls /tmp/rendered)
106- echo "name=$DIR" >> "$GITHUB_OUTPUT"
107-
108- - name : Create template branch and merge
109- id : merge
110- shell : bash
111- env :
112- GH_TOKEN : ${{ secrets.token || github.token }}
113- run : |
114- set -euo pipefail
115-
116- RENDERED="/tmp/rendered/${{ steps.rendered_dir.outputs.name }}"
117- OLD_SHA="${{ needs.check-update.outputs.old_sha }}"
118- NEW_SHA="${{ needs.check-update.outputs.new_sha }}"
119- BRANCH="template-update-${NEW_SHA}"
120-
121- git config user.name "GitHub Actions Bot"
122- git config user.email "actions@github.com"
123-
124- # Save current branch name
125- CURRENT_BRANCH="${{ inputs.repo_branch }}"
126-
127- # Create an orphan branch with the rendered template content
128- git checkout --orphan template-rendered
129- git rm -rf --cached .
130- rm -rf $(ls -A | grep -v '^\.git$')
131-
132- # Copy rendered content
133- cp -r "$RENDERED"/. .
134-
135- # Preserve .cookiecutter.json from original branch with updated SHA
136- git show "${CURRENT_BRANCH}:.cookiecutter.json" > .cookiecutter.json
137- jq ".cookiecutter.template_sha = \"${NEW_SHA}\"" \
138- .cookiecutter.json > tmp.json && mv tmp.json .cookiecutter.json
139-
140- git add -A
141- git commit -m "Template render at ${NEW_SHA}"
142-
143- # Save the commit SHA of template-rendered
144- TEMPLATE_COMMIT=$(git rev-parse HEAD)
145-
146- # Switch back to repo branch and create update branch
147- git checkout "${CURRENT_BRANCH}"
148- git checkout -b "$BRANCH"
149-
150- # Merge using the commit SHA instead of branch name
151- if git merge "$TEMPLATE_COMMIT" --allow-unrelated-histories --no-edit -m "chore: update from template ${OLD_SHA} → ${NEW_SHA}"; then
152- echo "merge_had_conflicts=false" >> "$GITHUB_OUTPUT"
153- else
154- echo "::warning ::Merge had conflicts. They will be included in the PR for manual resolution."
155- echo "merge_had_conflicts=true" >> "$GITHUB_OUTPUT"
72+ - name : Checkout project repo
73+ uses : actions/checkout@v4
74+ with :
75+ repository : ${{ github.repository }}
76+ ref : ${{ inputs.repo_branch }}
77+ fetch-depth : 0
78+ submodules : false
79+
80+ - name : Capture base SHA
81+ shell : bash
82+ run : |
83+ echo "BASE_SHA=$(git rev-parse HEAD)" >> "$GITHUB_ENV"
84+
85+ - name : Set up Python
86+ uses : actions/setup-python@v5
87+ with :
88+ python-version : ' 3.x'
89+
90+ - name : Install dependencies
91+ run : pip install cookiecutter
92+
93+ - name : Check if update branch already exists
94+ run : |
95+ BRANCH="template-update-${{ needs.check-update.outputs.new_sha }}"
96+ if git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null 2>&1; then
97+ echo "::error ::Branch '$BRANCH' already exists. A template update PR may already be open."
98+ exit 1
99+ fi
100+
101+ - name : Clone template at new SHA
102+ run : git clone '${{ inputs.template_repo }}' /tmp/template-source
103+
104+ - name : Run cookiecutter replay
105+ run : |
106+ rm -rf ~/.cookiecutters/*
107+ cookiecutter /tmp/template-source \
108+ --replay-file .cookiecutter.json \
109+ --overwrite-if-exists \
110+ --output-dir /tmp/rendered
111+
112+ - name : Get rendered project directory name
113+ id : rendered_dir
114+ run : |
115+ DIR=$(ls /tmp/rendered)
116+ echo "name=$DIR" >> "$GITHUB_OUTPUT"
117+
118+ - name : Create template branch, merge, and push
119+ id : merge
120+ shell : bash
121+ env :
122+ GH_TOKEN : ${{ secrets.WORKFLOW_TOKEN || github.token }}
123+ run : |
124+ set -euo pipefail
125+
126+ RENDERED="/tmp/rendered/${{ steps.rendered_dir.outputs.name }}"
127+ OLD_SHA="${{ needs.check-update.outputs.old_sha }}"
128+ NEW_SHA="${{ needs.check-update.outputs.new_sha }}"
129+ BRANCH="template-update-${NEW_SHA}"
130+
131+ git config user.name "GitHub Actions Bot"
132+ git config user.email "actions@github.com"
133+
134+ CURRENT_BRANCH="${{ inputs.repo_branch }}"
135+
136+ # Create an orphan branch with the rendered template content
137+ git checkout --orphan template-rendered
138+ git rm -rf --cached . >/dev/null 2>&1 || true
139+ rm -rf $(ls -A | grep -v '^\.git$') || true
140+
141+ # Copy rendered content
142+ cp -r "$RENDERED"/. .
143+
144+ # Preserve .cookiecutter.json from original branch with updated SHA
145+ git show "${CURRENT_BRANCH}:.cookiecutter.json" > .cookiecutter.json
146+ jq ".cookiecutter.template_sha = \"${NEW_SHA}\"" \
147+ .cookiecutter.json > tmp.json && mv tmp.json .cookiecutter.json
148+
156149 git add -A
157- git commit -m "chore: update from template ${OLD_SHA} → ${NEW_SHA} (with conflicts)"
158- fi
159-
160- if ! git remote get-url origin >/dev/null 2>&1; then
161- git remote add origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
162- fi
163-
164- git push origin "$BRANCH"
165- echo "branch=$BRANCH" >> "$GITHUB_OUTPUT"
166-
167- - name : Create pull request
168- env :
169- GITHUB_TOKEN : ${{ secrets.token || github.token }}
170- run : |
171- OLD_SHA="${{ needs.check-update.outputs.old_sha }}"
172- NEW_SHA="${{ needs.check-update.outputs.new_sha }}"
173- BRANCH="${{ steps.merge.outputs.branch }}"
174- MERGE_HAD_CONFLICTS="${{ steps.merge.outputs.merge_had_conflicts }}"
175-
176- if [ "$MERGE_HAD_CONFLICTS" = "true" ]; then
177- BODY="Updates from template SHA \`${OLD_SHA}\` → \`${NEW_SHA}\`
178-
179- ⚠️ **Merge conflicts detected**: This PR contains conflict markers that need manual resolution. Search for \`<<<<<<<\` in the changed files."
180- else
181- BODY="Updates from template SHA \`${OLD_SHA}\` → \`${NEW_SHA}\`"
182- fi
183-
184- gh pr create \
185- --title "chore: update from template ${OLD_SHA} → ${NEW_SHA}" \
186- --body "$BODY" \
187- --base '${{ inputs.repo_branch }}' \
188- --head "$BRANCH"
150+ git commit -m "Template render at ${NEW_SHA}"
151+
152+ TEMPLATE_COMMIT=$(git rev-parse HEAD)
153+
154+ # Switch back to repo branch and create update branch
155+ git checkout "${CURRENT_BRANCH}"
156+ git checkout -b "$BRANCH"
157+
158+ MERGE_HAD_CONFLICTS=false
159+
160+ # Attempt the merge; if it fails, keep conflict markers in working tree
161+ if git merge "$TEMPLATE_COMMIT" --allow-unrelated-histories --no-edit -m "chore: update from template ${OLD_SHA} → ${NEW_SHA}"; then
162+ MERGE_HAD_CONFLICTS=false
163+ else
164+ echo "::warning ::Merge had conflicts. They will be included in the PR for manual resolution."
165+ MERGE_HAD_CONFLICTS=true
166+ fi
167+
168+ # Conditionally revert .github/workflows back to base (omit workflow updates)
169+ if [ "${{ inputs.update_workflows }}" != "true" ]; then
170+ echo "update_workflows=false → reverting .github/workflows to base commit: $BASE_SHA"
171+ if [ -d ".github/workflows" ]; then
172+ git restore --source="$BASE_SHA" --staged --worktree .github/workflows || true
173+ fi
174+ fi
175+
176+ # If merge had conflicts, commit AFTER optional workflow revert
177+ if [ "$MERGE_HAD_CONFLICTS" = "true" ]; then
178+ git add -A
179+ git commit -m "chore: update from template ${OLD_SHA} → ${NEW_SHA} (with conflicts)"
180+ else
181+ # Merge succeeded and created a merge commit already.
182+ # If we reverted workflows (or made any post-merge edits), commit those changes.
183+ if ! git diff --cached --quiet; then
184+ git add -A
185+ git commit -m "chore: omit workflow updates"
186+ fi
187+ fi
188+
189+ # Ensure origin exists
190+ if ! git remote get-url origin >/dev/null 2>&1; then
191+ git remote add origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
192+ fi
193+
194+ git push origin "$BRANCH"
195+
196+ echo "branch=$BRANCH" >> "$GITHUB_OUTPUT"
197+ echo "merge_had_conflicts=$MERGE_HAD_CONFLICTS" >> "$GITHUB_OUTPUT"
198+
199+ - name : Create pull request
200+ env :
201+ GITHUB_TOKEN : ${{ secrets.WORKFLOW_TOKEN || github.token }}
202+ run : |
203+ OLD_SHA="${{ needs.check-update.outputs.old_sha }}"
204+ NEW_SHA="${{ needs.check-update.outputs.new_sha }}"
205+ BRANCH="${{ steps.merge.outputs.branch }}"
206+ MERGE_HAD_CONFLICTS="${{ steps.merge.outputs.merge_had_conflicts }}"
207+
208+ if [ "$MERGE_HAD_CONFLICTS" = "true" ]; then
209+ BODY="Updates from template SHA \`${OLD_SHA}\` → \`${NEW_SHA}\`
210+
211+ ⚠️ **Merge conflicts detected**: This PR contains conflict markers that need manual resolution. Search for \`<<<<<<<\` in the changed files."
212+ else
213+ BODY="Updates from template SHA \`${OLD_SHA}\` → \`${NEW_SHA}\`"
214+ fi
215+
216+ gh pr create \
217+ --title "chore: update from template ${OLD_SHA} → ${NEW_SHA}" \
218+ --body "$BODY" \
219+ --base '${{ inputs.repo_branch }}' \
220+ --head "$BRANCH"
0 commit comments