Skip to content

Commit b77fee5

Browse files
committed
feat: automate template sync with GitHub Actions workflow
Adds a scheduled workflow that merges template changes weekly via git merge (preserving line-level 3-way merge across all files). Improves the manual script with error handling, early exit when already up to date, cleanup trap, and automatic PR creation. https://claude.ai/code/session_01UrErV4mLhMCJHmnPd3VpYE
1 parent 8389c8f commit b77fee5

File tree

2 files changed

+164
-12
lines changed

2 files changed

+164
-12
lines changed
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
name: Sync from Template
2+
3+
on:
4+
schedule:
5+
- cron: "0 9 * * 1" # every Monday at 9am UTC
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: write
10+
pull-requests: write
11+
12+
jobs:
13+
sync:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- name: Checkout
17+
uses: actions/checkout@v6
18+
with:
19+
fetch-depth: 0 # full history needed for merge
20+
21+
- uses: prefix-dev/setup-pixi@v0.9.4
22+
with:
23+
cache: true
24+
25+
- name: Setup git merge driver for pixi.lock
26+
run: git config merge.ourslock.driver true
27+
28+
- name: Configure git identity
29+
run: |
30+
git config user.name "github-actions[bot]"
31+
git config user.email "github-actions[bot]@users.noreply.github.com"
32+
33+
- name: Add template remote and fetch
34+
run: |
35+
git remote add template https://github.com/blooop/python_template.git
36+
git fetch template main
37+
38+
- name: Check for changes
39+
id: check
40+
run: |
41+
if git diff HEAD...template/main --quiet 2>/dev/null; then
42+
echo "has_changes=false" >> "$GITHUB_OUTPUT"
43+
echo "No changes from template."
44+
else
45+
echo "has_changes=true" >> "$GITHUB_OUTPUT"
46+
echo "Template has new changes to merge."
47+
fi
48+
49+
- name: Merge template changes
50+
if: steps.check.outputs.has_changes == 'true'
51+
id: merge
52+
run: |
53+
git checkout -B feature/update_from_template
54+
55+
# Real git merge — line-level 3-way merge across ALL files.
56+
# Git remembers the merge base so conflicts only need resolving once.
57+
if git merge template/main --allow-unrelated-histories -m 'feat: pull changes from remote template'; then
58+
echo "conflicts=false" >> "$GITHUB_OUTPUT"
59+
else
60+
echo "conflicts=true" >> "$GITHUB_OUTPUT"
61+
fi
62+
63+
- name: Regenerate pixi.lock
64+
if: steps.check.outputs.has_changes == 'true' && steps.merge.outputs.conflicts == 'false'
65+
run: |
66+
git checkout --ours pixi.lock 2>/dev/null || true
67+
pixi update
68+
git add pixi.lock
69+
git diff --cached --quiet || git commit -m 'chore: update pixi.lock after template merge'
70+
71+
- name: Push and create PR
72+
if: steps.check.outputs.has_changes == 'true' && steps.merge.outputs.conflicts == 'false'
73+
run: |
74+
git push --force-with-lease -u origin feature/update_from_template
75+
76+
# Create PR if one doesn't already exist
77+
EXISTING=$(gh pr list --head feature/update_from_template --state open --json number -q '.[0].number' || true)
78+
if [ -z "$EXISTING" ]; then
79+
gh pr create \
80+
--title "feat: sync updates from template repo" \
81+
--body "Automated pull of changes from the [template repository](https://github.com/blooop/python_template) using \`git merge\`."
82+
else
83+
echo "PR #$EXISTING already exists."
84+
fi
85+
env:
86+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
87+
88+
- name: Warn on conflicts
89+
if: steps.check.outputs.has_changes == 'true' && steps.merge.outputs.conflicts == 'true'
90+
run: |
91+
git merge --abort
92+
echo "::warning::Template merge has conflicts. Run 'pixi run update-from-template-repo' locally to resolve them."

scripts/update_from_template.sh

Lines changed: 72 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,78 @@
1-
#! /bin/bash
1+
#!/bin/bash
2+
set -euo pipefail
3+
4+
TEMPLATE_REPO="${TEMPLATE_REPO:-https://github.com/blooop/python_template.git}"
5+
BRANCH="feature/update_from_template"
6+
7+
# Ensure clean working tree
8+
if ! git diff --quiet || ! git diff --cached --quiet; then
9+
echo "Error: working tree is dirty. Commit or stash changes first."
10+
exit 1
11+
fi
212

313
git config --global pull.rebase false
4-
git remote add template https://github.com/blooop/python_template.git
5-
git fetch --all
6-
git checkout main && git pull origin main
7-
git checkout -B feature/update_from_template; git pull
8-
git merge template/main --allow-unrelated-histories -m 'feat: pull changes from remote template'
9-
git checkout --ours pixi.lock
10-
11-
# regenerate lockfile to match merged pyproject.toml
14+
15+
# Setup merge driver for pixi.lock (idempotent)
16+
git config merge.ourslock.driver true
17+
18+
# Add template remote (remove first if leftover from a failed run)
19+
git remote remove template 2>/dev/null || true
20+
git remote add template "$TEMPLATE_REPO"
21+
22+
cleanup() {
23+
git remote remove template 2>/dev/null || true
24+
}
25+
trap cleanup EXIT
26+
27+
git fetch template main
28+
29+
# Check if there are any changes to merge
30+
if git diff HEAD...template/main --quiet 2>/dev/null; then
31+
echo "No changes from template. Already up to date."
32+
exit 0
33+
fi
34+
35+
# Get to a clean main/master
36+
DEFAULT_BRANCH=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@' || echo "main")
37+
git checkout "$DEFAULT_BRANCH" && git pull origin "$DEFAULT_BRANCH"
38+
39+
# Create or reset the update branch
40+
git checkout -B "$BRANCH"
41+
42+
# Merge template changes — this is a real git merge so:
43+
# - 3-way merge at line level across ALL files
44+
# - git remembers the merge base, so conflicts only appear once
45+
# - --allow-unrelated-histories is needed for the first merge, harmless after
46+
if ! git merge template/main --allow-unrelated-histories -m 'feat: pull changes from remote template'; then
47+
echo ""
48+
echo "============================================="
49+
echo " Merge conflicts detected — resolve them,"
50+
echo " then run: pixi update && git add pixi.lock"
51+
echo " and commit to finish the merge."
52+
echo "============================================="
53+
exit 1
54+
fi
55+
56+
# Resolve pixi.lock: keep ours, then regenerate from merged pyproject.toml
57+
git checkout --ours pixi.lock 2>/dev/null || true
1258
pixi update
1359
git add pixi.lock
1460
git diff --cached --quiet || git commit -m 'chore: update pixi.lock after template merge'
1561

16-
git remote remove template
17-
git push --set-upstream origin feature/update_from_template
18-
git checkout main
62+
git push --set-upstream origin "$BRANCH"
63+
64+
# Create a PR if gh is available and no open PR exists
65+
if command -v gh &>/dev/null; then
66+
EXISTING=$(gh pr list --head "$BRANCH" --state open --json number -q '.[0].number' 2>/dev/null || true)
67+
if [ -z "$EXISTING" ]; then
68+
gh pr create \
69+
--title "feat: sync updates from template repo" \
70+
--body "Automated pull of changes from [$TEMPLATE_REPO]($TEMPLATE_REPO) using \`git merge\`." \
71+
--fill 2>/dev/null || echo "PR creation skipped (gh not authenticated or not a GitHub repo)."
72+
else
73+
echo "PR #$EXISTING already open for $BRANCH."
74+
fi
75+
fi
76+
77+
git checkout "$DEFAULT_BRANCH"
78+
echo "Done. Review the PR for branch $BRANCH."

0 commit comments

Comments
 (0)