44 workflow_call :
55 inputs :
66 template_repo :
7- description : ' HTTPS URL or owner/repo of the Cookiecutter template (e.g., https://github.com/your-org/Project.Cookiecutter or your-org/Project.Cookiecutter) '
7+ description : ' URL of the Cookiecutter template repo '
88 required : true
99 type : string
1010 repo_branch :
11- description : ' Branch of the caller repo to update (e.g., main) '
11+ description : ' Branch of the project repo to update'
1212 required : true
1313 type : string
1414
@@ -17,253 +17,118 @@ permissions:
1717 pull-requests : write
1818
1919jobs :
20- update :
21- name : Update from Cookiecutter Template
20+ template-update :
2221 runs-on : ubuntu-latest
23- env :
24- TEMPLATE_TOKEN : ${{ secrets.TEMPLATE_REPO_TOKEN != '' && secrets.TEMPLATE_REPO_TOKEN || github.token }}
2522
2623 steps :
27- - name : Checkout caller repository
28- uses : actions/checkout@v4
29- with :
30- ref : ${{ inputs.repo_branch }}
31- fetch-depth : 0
32-
33- - name : Set up Python
34- uses : actions/setup-python@v5
35- with :
36- python-version : ' 3.x'
37-
38- - name : Install Cookiecutter (and jq if missing)
39- shell : bash
40- run : |
41- set -euo pipefail
42- python -m pip install --upgrade pip
43- pip install "cookiecutter==2.6.0"
44- if ! command -v jq >/dev/null 2>&1; then
45- sudo apt-get update
46- sudo apt-get install -y jq
47- fi
48-
49- - name : Verify tools
50- shell : bash
51- run : |
52- set -euo pipefail
53- cookiecutter --version
54- jq --version
55- git --version
56-
57- - name : Read OLD template SHA from .cookiecutter.json
58- id : old_sha
59- shell : bash
60- run : |
61- set -euo pipefail
62- if [ ! -f ".cookiecutter.json" ]; then
63- echo "::error ::.cookiecutter.json not found at repo root."
64- exit 1
65- fi
66- OLD_SHA="$(jq -r '.cookiecutter.template_sha // empty' .cookiecutter.json)"
67- if [ -z "$OLD_SHA" ] || [ "$OLD_SHA" = "null" ]; then
68- echo "::error ::.cookiecutter.json missing cookiecutter.template_sha"
69- exit 1
70- fi
71- echo "sha=$OLD_SHA" >> "$GITHUB_OUTPUT"
72- echo "Detected OLD template sha: $OLD_SHA"
73-
74- - name : Parse template repo slug
75- id : repo
76- shell : bash
77- run : |
78- set -euo pipefail
79- INPUT="${{ inputs.template_repo }}"
80- if [[ "$INPUT" =~ ^https?://github\.com/ ]]; then
81- # Extract owner/repo from URL (strip optional .git)
82- SLUG="$(echo "$INPUT" | sed -E 's#^https?://github\.com/([^/]+/[^/]+)(\.git)?$#\1#')"
83- else
84- SLUG="$INPUT"
85- fi
86- if [[ ! "$SLUG" =~ ^[^/]+/[^/]+$ ]]; then
87- echo "::error ::Unable to parse owner/repo from '${{ inputs.template_repo }}'"
88- exit 1
89- fi
90- echo "slug=$SLUG" >> "$GITHUB_OUTPUT"
91- echo "Template repo slug: $SLUG"
92-
93- - name : Checkout template at OLD SHA (base-template)
94- uses : actions/checkout@v4
95- with :
96- repository : ${{ steps.repo.outputs.slug }}
97- ref : ${{ steps.old_sha.outputs.sha }}
98- token : ${{ env.TEMPLATE_TOKEN }}
99- path : base-template
100-
101- - name : Checkout template at default branch HEAD (new-template)
102- uses : actions/checkout@v4
103- with :
104- repository : ${{ steps.repo.outputs.slug }}
105- token : ${{ env.TEMPLATE_TOKEN }}
106- path : new-template
107-
108- - name : Discover NEW template SHA from new-template HEAD
109- id : new_sha
110- shell : bash
111- run : |
112- set -euo pipefail
113- NEW_SHA="$(git -C new-template rev-parse HEAD)"
114- echo "sha=$NEW_SHA" >> "$GITHUB_OUTPUT"
115- echo "Discovered NEW template sha: $NEW_SHA"
116-
117- - name : Compare SHAs
118- shell : bash
119- run : |
120- if [ "${{ steps.old_sha.outputs.sha }}" = "${{ steps.new_sha.outputs.sha }}" ]; then
121- echo "SHA_CHANGED=false" >> "$GITHUB_ENV"
122- echo "⏭️ Template SHA unchanged."
123- else
124- echo "SHA_CHANGED=true" >> "$GITHUB_ENV"
125- echo "✅ Template SHA changed."
126- fi
127-
128- - name : Exit early if SHA unchanged
129- if : env.SHA_CHANGED == 'false'
130- run : echo "Nothing to do."
131-
132- - name : Extract cookiecutter extra context
133- if : env.SHA_CHANGED == 'true'
134- id : ctx
135- shell : bash
136- run : |
137- set -euo pipefail
138- CTX="$(jq -c '.cookiecutter' .cookiecutter.json)"
139- if [ -z "$CTX" ] || [ "$CTX" = "null" ]; then
140- echo "::error ::Unable to read cookiecutter context from .cookiecutter.json"
141- exit 1
142- fi
143- echo "json=$CTX" >> "$GITHUB_OUTPUT"
144-
145- - name : Render OLD template tree
146- if : env.SHA_CHANGED == 'true'
147- shell : bash
148- run : |
149- set -euo pipefail
150- rm -rf render-old template-base
151- mkdir -p render-old
152- cookiecutter --no-input -f base-template --output-dir render-old --extra-context '${{ steps.ctx.outputs.json }}'
153- shopt -s dotglob nullglob
154- GEN=(render-old/*)
155- if [ "${#GEN[@]}" -ne 1 ]; then
156- echo "::error ::Expected exactly one rendered folder for OLD template."
157- exit 1
158- fi
159- mkdir -p template-base
160- mv "${GEN[0]}"/* template-base/
161-
162- - name : Render NEW template tree
163- if : env.SHA_CHANGED == 'true'
164- shell : bash
165- run : |
166- set -euo pipefail
167- rm -rf render-new template-new
168- mkdir -p render-new
169- cookiecutter --no-input -f new-template --output-dir render-new --extra-context '${{ steps.ctx.outputs.json }}'
170- shopt -s dotglob nullglob
171- GEN=(render-new/*)
172- if [ "${#GEN[@]}" -ne 1 ]; then
173- echo "::error ::Expected exactly one rendered folder for NEW template."
24+ - name : Checkout project repo
25+ uses : actions/checkout@v4
26+ with :
27+ repository : ${{ github.repository }}
28+ ref : ${{ inputs.repo_branch }}
29+ fetch-depth : 0
30+
31+ - name : Set up Python
32+ uses : actions/setup-python@v5
33+ with :
34+ python-version : ' 3.x'
35+
36+ - name : Install dependencies
37+ run : pip install cookiecutter jq
38+
39+ - name : Read old template SHA
40+ id : old_sha
41+ run : |
42+ SHA=$(jq -r '.cookiecutter.template_sha // empty' .cookiecutter.json)
43+ if [ -z "$SHA" ]; then
44+ echo "::error ::.cookiecutter.json is missing cookiecutter.template_sha"
45+ exit 1
46+ fi
47+ echo "sha=$SHA" >> "$GITHUB_OUTPUT"
48+
49+ - name : Fetch new template SHA
50+ id : new_sha
51+ run : |
52+ echo "sha=$(git ls-remote '${{ inputs.template_repo }}' HEAD | cut -f1)" >> "$GITHUB_OUTPUT"
53+
54+ - name : Determine if Template was updated
55+ run : |
56+ if [ "${{ steps.old_sha.outputs.sha }}" = "${{ steps.new_sha.outputs.sha }}" ]; then
57+ echo "SHA_CHANGED=false" >> "$GITHUB_ENV"
58+ else
59+ echo "SHA_CHANGED=true" >> "$GITHUB_ENV"
60+ fi
61+
62+ - name : Exit if SHA unchanged
63+ if : env.SHA_CHANGED == 'false'
64+ run : echo "✂️ SHA unchanged; nothing to update."
65+
66+ - name : Clone template at OLD SHA
67+ if : env.SHA_CHANGED == 'true'
68+ run : |
69+ git clone '${{ inputs.template_repo }}' base-template
70+ git -C base-template checkout '${{ steps.old_sha.outputs.sha }}'
71+
72+ - name : Clone template at NEW SHA
73+ if : env.SHA_CHANGED == 'true'
74+ run : git clone '${{ inputs.template_repo }}' template-source
75+
76+ - name : Render OLD template (base-template → template-base)
77+ if : env.SHA_CHANGED == 'true'
78+ run : |
79+ rm -rf ~/.cookiecutters/*
80+ mkdir -p template-base
81+ # jq '.cookiecutter.templates = "main"' .cookiecutter.json > tmp && mv tmp .cookiecutter.json # <-- uncomment if keeping the variable
82+ cookiecutter base-template \
83+ --replay-file .cookiecutter.json \
84+ --overwrite-if-exists \
85+ --output-dir template-base
86+
87+ - name : Render NEW template (template-source → template-new)
88+ if : env.SHA_CHANGED == 'true'
89+ run : |
90+ rm -rf ~/.cookiecutters/*
91+ mkdir -p template-new
92+ # jq '.cookiecutter.templates = "main"' .cookiecutter.json > tmp && mv tmp .cookiecutter.json # <-- same note
93+ cookiecutter template-source \
94+ --replay-file .cookiecutter.json \
95+ --overwrite-if-exists \
96+ --output-dir template-new
97+
98+ - name : Apply patch & raise PR
99+ if : env.SHA_CHANGED == 'true'
100+ shell : bash
101+ run : |
102+ diff -ruN template-base template-new > update.patch || true
103+
104+ if [ ! -s update.patch ]; then
105+ echo "ℹ️ No template diffs; only SHA bump will occur."
106+ else
107+ if ! git apply --index --3way update.patch; then
108+ echo "::error ::Merge conflicts detected; aborting."
174109 exit 1
175110 fi
176- mkdir -p template-new
177- mv "${GEN[0]}"/* template-new/
178-
179- - name : Build update patch
180- if : env.SHA_CHANGED == 'true'
181- shell : bash
182- run : |
183- set -euo pipefail
184- git config core.autocrlf false
185- git config apply.whitespace nowarn
186-
187- git diff --no-index --binary --full-index \
188- --src-prefix=a/ --dst-prefix=b/ \
189- template-base template-new > update.patch || true
190-
191- if [ ! -s update.patch ]; then
192- echo "NO_DIFF=true" >> "$GITHUB_ENV"
193- echo "ℹ️ No template diffs; only SHA bump will occur."
194- fi
195-
196- - name : Apply patch (3-way with fallback to --reject)
197- if : env.SHA_CHANGED == 'true' && env.NO_DIFF != 'true'
198- shell : bash
199- run : |
200- set -euo pipefail
201- echo "::group::Try 3-way apply"
202- if git apply --3way --index --whitespace=fix update.patch; then
203- echo "APPLY_MODE=threeway" >> "$GITHUB_ENV"
204- echo "✅ Applied template changes (3-way)"
205- else
206- echo "::endgroup::"
207- echo "::warning::3-way apply failed; trying --reject fallback..."
208- if git apply --reject --whitespace=fix update.patch; then
209- echo "APPLY_MODE=rejects" >> "$GITHUB_ENV"
210- echo "⚠️ Patch applied with rejects; .rej files generated."
211- else
212- echo "::error ::Patch could not be applied even with --reject."
213- exit 1
214- fi
215- fi
216- echo "::endgroup::"
217- git add -A
218-
219- - name : Bump cookiecutter.template_sha
220- if : env.SHA_CHANGED == 'true'
221- shell : bash
222- run : |
223- set -euo pipefail
224- jq ".cookiecutter.template_sha = \"${{ steps.new_sha.outputs.sha }}\"" \
225- .cookiecutter.json > tmp && mv tmp .cookiecutter.json
226- git add .cookiecutter.json
227- echo "Updated .cookiecutter.json template_sha to ${{ steps.new_sha.outputs.sha }}"
228-
229- - name : Commit & push update branch
230- if : env.SHA_CHANGED == 'true'
231- shell : bash
232- run : |
233- set -euo pipefail
234- BRANCH="template-update-${{ steps.new_sha.outputs.sha }}"
235- git switch -c "$BRANCH"
236- git config user.name "GitHub Actions Bot"
237- git config user.email "actions@github.com"
238- git commit -m "chore: merge template updates ${{ steps.old_sha.outputs.sha }} → ${{ steps.new_sha.outputs.sha }}"
239- git push -u origin "$BRANCH"
240- echo "BRANCH=$BRANCH" >> "$GITHUB_ENV"
241-
242- - name : Upload rejects (if any)
243- if : env.SHA_CHANGED == 'true' && env.APPLY_MODE == 'rejects'
244- uses : actions/upload-artifact@v4
245- with :
246- name : template-update-rejects
247- path : |
248- **/*.rej
249- update.patch
250-
251- - name : Create pull request
252- if : env.SHA_CHANGED == 'true'
253- env :
254- GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
255- shell : bash
256- run : |
257- set -euo pipefail
258- BODY="Automated template update.
259-
260- - Apply mode: ${APPLY_MODE:-threeway}
261- - Old template SHA: \`${{ steps.old_sha.outputs.sha }}\`
262- - New template SHA: \`${{ steps.new_sha.outputs.sha }}\`
263-
264- If apply mode was 'rejects', download the 'template-update-rejects' artifact to resolve \`.rej\` files, then push fixes to this branch."
265- gh pr create \
266- --title "chore: merge template updates ${{ steps.old_sha.outputs.sha }} → ${{ steps.new_sha.outputs.sha }}" \
267- --body "$BODY" \
268- --base '${{ inputs.repo_branch }}' \
269- --head "$BRANCH"
111+ echo "✅ Applied template changes"
112+ fi
113+
114+ jq ".cookiecutter.template_sha = \"${{ steps.new_sha.outputs.sha }}\"" \
115+ .cookiecutter.json > tmp && mv tmp .cookiecutter.json
116+ git add .cookiecutter.json
117+
118+ BRANCH="template-update-${{ steps.new_sha.outputs.sha }}"
119+ git checkout -b "$BRANCH"
120+ git config user.name "GitHub Actions Bot"
121+ git config user.email "actions@github.com"
122+ git commit -am "chore: merge template updates ${{ steps.old_sha.outputs.sha }} → ${{ steps.new_sha.outputs.sha }}"
123+ git push origin "$BRANCH"
124+
125+ - name : Create pull request
126+ if : env.SHA_CHANGED == 'true'
127+ env :
128+ GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
129+ run : |
130+ gh pr create \
131+ --title "chore: merge template updates ${{ steps.old_sha.outputs.sha }} → ${{ steps.new_sha.outputs.sha }}" \
132+ --body "Updates cookiecutter.template_sha from ${{ steps.old_sha.outputs.sha }} to ${{ steps.new_sha.outputs.sha }}" \
133+ --base '${{ inputs.repo_branch }}' \
134+ --head "$BRANCH"
0 commit comments