66 workflow_dispatch :
77 inputs :
88 cpython_tag :
9- description : ' CPython tag to sync against (e.g. 3.14)'
9+ description : ' CPython tag/branch to sync against (e.g. 3.14)'
1010 required : false
1111 default : ' 3.14'
1212
1313permissions :
1414 contents : write
1515 issues : write
1616
17+ # Shared with maintenance.yml so the two never push at the same time.
1718concurrency :
18- group : sync
19+ group : repo-writes
1920 cancel-in-progress : false
2021
2122jobs :
2223 sync :
2324 runs-on : ubuntu-latest
25+ timeout-minutes : 45
2426 env :
2527 CPYTHON_TAG : ${{ github.event.inputs.cpython_tag || '3.14' }}
2628 steps :
@@ -29,115 +31,149 @@ jobs:
2931 with :
3032 fetch-depth : 0
3133
34+ # --- Skip the expensive build if upstream hasn't moved -------------
35+ - name : Get upstream head
36+ id : upstream
37+ run : |
38+ sha=$(git ls-remote https://github.com/python/cpython \
39+ "refs/heads/$CPYTHON_TAG" "refs/tags/$CPYTHON_TAG" \
40+ | head -1 | cut -f1)
41+ echo "sha=$sha" >> "$GITHUB_OUTPUT"
42+ # Marker lives outside the repo so `git add --all` never sees it.
43+ echo "$sha" > "$RUNNER_TEMP/sync-marker"
44+
45+ - name : Check whether this upstream head was already synced
46+ id : seen
47+ # Fail open: no SHA -> no lookup -> full run.
48+ if : steps.upstream.outputs.sha != ''
49+ uses : actions/cache/restore@v4
50+ with :
51+ path : ${{ runner.temp }}/sync-marker
52+ key : synced-${{ env.CPYTHON_TAG }}-${{ steps.upstream.outputs.sha }}
53+ lookup-only : true
54+
55+ - name : Decide whether to run
56+ run : |
57+ if [ "${{ steps.seen.outputs.cache-hit }}" = "true" ] \
58+ && [ "${{ github.event_name }}" != "workflow_dispatch" ]; then
59+ echo "Upstream ${{ steps.upstream.outputs.sha }} already synced; skipping."
60+ echo "SKIP=true" >> "$GITHUB_ENV"
61+ fi
62+
63+ # --- Sync ------------------------------------------------------------
3264 - name : Set up Python
65+ if : env.SKIP != 'true'
3366 uses : actions/setup-python@v4
3467 with :
3568 python-version : ' 3.12'
3669
3770 - name : Install gettext
71+ if : env.SKIP != 'true'
3872 run : sudo apt-get install -y gettext
3973
4074 - name : Install Python dependencies
75+ if : env.SKIP != 'true'
76+ # Consider installing from CPython's Doc/requirements.txt instead of
77+ # unpinned sphinx, so Sphinx upgrades can't change msgids on their own.
4178 run : pip install sphinx sphinx-intl
4279
43- # Item 4: fetch/build/merge/validate now all live in one shared
44- # module (scripts/po_sync.py) instead of inline bash/awk here, so
45- # this workflow can't drift from update_python_version.py the way
46- # it did before (missing --no-location --no-wrap on one side).
4780 - name : Sync msgids (fetch, build gettext, merge, validate)
81+ if : env.SKIP != 'true'
4882 run : python scripts/po_sync.py sync-only "$CPYTHON_TAG" --create-new
4983
50- - name : Ensure translation label exists
51- env :
52- GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
53- run : |
54- gh label create "translation" \
55- --color "0075ca" \
56- --description "Translation review needed" \
57- --repo ${{ github.repository }} \
58- 2>/dev/null || true
84+ - name : Stage changes
85+ if : env.SKIP != 'true'
86+ run : git add --all
5987
60- - name : Open issue for new and fuzzy strings
61- env :
62- GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
88+ - name : Unstage POT-Creation-Date-only changes
89+ if : env.SKIP != 'true'
90+ run : python scripts/unstage_cosmetic.py
91+
92+ # --- Build the report from what is actually staged -------------------
93+ - name : Build review report (new files + newly fuzzy strings)
94+ if : env.SKIP != 'true'
6395 run : |
64- tmpfile=$(mktemp)
96+ report="$RUNNER_TEMP/report.md"
97+ : > "$report"
98+ work=$(mktemp -d)
6599
66- # --- New PO files (untracked, since nothing is staged yet) ---
67- new_files=$(git ls-files --others --exclude-standard -- '*.po' \
68- ':(exclude).cpython-src' ':(exclude).pot-templates')
100+ # New PO files
101+ new_files=$(git diff --staged --name-only --diff-filter=A -- '*.po')
69102 if [ -n "$new_files" ]; then
70- new_count=$(echo "$new_files" | wc -l)
71103 {
72- echo "## 🆕 New PO files ($new_count )"
104+ echo "## 🆕 New PO files ($(echo "$new_files" | wc -l) )"
73105 echo "$new_files" | sed 's/^/- `/; s/$/`/'
74106 echo ""
75- } >> "$tmpfile "
107+ } >> "$report "
76108 fi
77109
78- # --- Fuzzy strings ---
79- fuzzy_out=$(mktemp)
110+ # Fuzzy entries that are new in this run (present now, absent at HEAD)
111+ fuzzy_out="$work/fuzzy.md"
112+ : > "$fuzzy_out"
113+ git diff --staged --name-only --diff-filter=M -- '*.po' |
80114 while IFS= read -r f; do
81- fuzzy=$(msgattrib --only-fuzzy --no-obsolete "$f" 2>/dev/null)
82- if [ -n "$fuzzy" ]; then
83- entries=$(echo "$fuzzy" | awk '
84- /^msgid / {
85- first=$0; msg=$0"\n"; lines=1; incapture=1; next
86- }
87- incapture && /^"/ {
88- msg = msg $0 "\n"; lines++; next
89- }
90- incapture && /^msgstr/ {
91- incapture=0
92- if (!(first == "msgid \"\"" && lines==1)) {
93- printf "- %s\n", msg
94- }
95- next
96- }
97- ')
98- count=$(echo "$entries" | grep -c '^- ' || true)
99- if [ "${count:-0}" -gt 0 ]; then
100- echo "### $f ($count fuzzy)" >> "$fuzzy_out"
101- echo "$entries" | head -20 >> "$fuzzy_out"
102- echo "" >> "$fuzzy_out"
103- fi
115+ git show "HEAD:$f" > "$work/old_full.po"
116+ msgattrib --only-fuzzy --no-obsolete --no-wrap "$work/old_full.po" > "$work/old.po" 2>/dev/null || : > "$work/old.po"
117+ msgattrib --only-fuzzy --no-obsolete --no-wrap "$f" > "$work/new.po" 2>/dev/null || : > "$work/new.po"
118+ # entries in exactly one of old/new, then keep those also in new
119+ msgcomm --less-than=2 --no-wrap "$work/old.po" "$work/new.po" > "$work/sym.po" 2>/dev/null || : > "$work/sym.po"
120+ msgcomm --no-wrap "$work/new.po" "$work/sym.po" > "$work/added.po" 2>/dev/null || : > "$work/added.po"
121+ count=$(grep -c '^#, .*fuzzy' "$work/added.po" || true)
122+ if [ "${count:-0}" -gt 0 ]; then
123+ {
124+ echo "### \`$f\` ($count new fuzzy)"
125+ grep -E '^msgid ".+"' "$work/added.po" | sed 's/^msgid /- /' | head -10
126+ [ "$count" -gt 10 ] && echo "- … and $((count - 10)) more"
127+ echo ""
128+ } >> "$fuzzy_out"
104129 fi
105- done < <(find . -name "*.po" \
106- -not -path "./.git/*" \
107- -not -path "./.cpython-src/*" \
108- -not -path "./.pot-templates/*")
130+ done
109131
110132 if [ -s "$fuzzy_out" ]; then
111- {
112- echo "## 🔍 Fuzzy strings"
113- cat "$fuzzy_out"
114- } >> "$tmpfile"
115- fi
116-
117- # --- One issue for both ---
118- if [ -s "$tmpfile" ]; then
119- echo "Triggered by sync with \`$CPYTHON_TAG\`." >> "$tmpfile"
120- gh issue create \
121- --title "🔍 Translation review needed after CPython sync ($(date +%Y-%m-%d))" \
122- --body-file "$tmpfile" \
123- --label "translation" \
124- --repo ${{ github.repository }}
133+ { echo "## 🔍 Newly fuzzy strings"; cat "$fuzzy_out"; } >> "$report"
125134 fi
126- rm -f "$tmpfile" "$fuzzy_out"
127-
128- - name : Stage changes
129- run : git add --all
130135
131- - name : Unstage POT-Creation-Date-only changes
132- run : python scripts/unstage_cosmetic.py
133-
134- - name : Commit if changed
136+ # --- Commit + push ---------------------------------------------------
137+ - name : Commit and push if changed
138+ id : commit
139+ if : env.SKIP != 'true'
135140 run : |
136141 git config user.name "github-actions[bot]"
137142 git config user.email "github-actions[bot]@users.noreply.github.com"
138143 if git diff --staged --quiet; then
139- echo "Nothing to commit, skipping."
144+ echo "Nothing to commit."
145+ echo "committed=false" >> "$GITHUB_OUTPUT"
140146 exit 0
141147 fi
142148 git commit -m "chore: sync msgids with CPython $CPYTHON_TAG"
149+ git pull --rebase
143150 git push
151+ echo "committed=true" >> "$GITHUB_OUTPUT"
152+
153+ # Only after a successful push, so a failed run can't create duplicates.
154+ - name : Open issue for new files / newly fuzzy strings
155+ if : env.SKIP != 'true' && steps.commit.outputs.committed == 'true'
156+ env :
157+ GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
158+ run : |
159+ report="$RUNNER_TEMP/report.md"
160+ if [ -s "$report" ]; then
161+ gh label create "translation" \
162+ --color "0075ca" \
163+ --description "Translation review needed" \
164+ --repo "${{ github.repository }}" 2>/dev/null || true
165+ echo "Triggered by sync with \`$CPYTHON_TAG\` (commit $(git rev-parse --short HEAD))." >> "$report"
166+ gh issue create \
167+ --title "🔍 Translation review needed after CPython sync ($(date +%Y-%m-%d))" \
168+ --body-file "$report" \
169+ --label "translation" \
170+ --repo "${{ github.repository }}"
171+ fi
172+
173+ # Record the upstream head as synced (only reached if everything above passed).
174+ - name : Remember synced upstream head
175+ if : env.SKIP != 'true' && steps.upstream.outputs.sha != ''
176+ uses : actions/cache/save@v4
177+ with :
178+ path : ${{ runner.temp }}/sync-marker
179+ key : synced-${{ env.CPYTHON_TAG }}-${{ steps.upstream.outputs.sha }}
0 commit comments