Repository navigation
238 lines (222 loc) · 11.1 KB
/
Copy pathtemplate-sync.yml
File metadata and controls
238 lines (222 loc) · 11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
# Ships to every instance of this template. Diffs the instance against the
# template's latest tagged release (per ../template-sync-manifest.yml's
# tiers) and opens a PR with the result -- never a direct push, never
# auto-merged. See ../workflows/README.md's "Template sync" section.
name: Template sync
on:
schedule:
# Cadence itself (weekly vs. monthly) is a repository variable, since
# cron can't read one to pick its own interval -- see check-cadence below.
- cron: "17 4 * * 1"
workflow_dispatch:
inputs:
initial_sync_tag:
description: >-
One-time bootstrap only: seed template-sync-state.json at this tag
instead of running a sync. Use for an instance that predates this
workflow and has no state file yet. Requires template_repo too.
required: false
template_repo:
description: >-
Bootstrap only: the template's "owner/repo" this instance was
created from, e.g. "myorg/template-fastapi".
required: false
permissions:
contents: write
pull-requests: write
env:
STATE_FILE: .github/template-sync-state.json
jobs:
sync:
# renovate: datasource=github-runners depName=ubuntu
runs-on: ${{ vars.CI_RUNNER || 'ubuntu-24.04' }}
steps:
# TEMPLATE_SYNC_TOKEN, not the default GITHUB_TOKEN: checkout stores
# this token for the later `git push`, and GITHUB_TOKEN can never
# create or update files under .github/workflows/ -- which most
# syncs touch. Falls back to GITHUB_TOKEN for a sync that doesn't.
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
token: ${{ secrets.TEMPLATE_SYNC_TOKEN || github.token }}
# Belt-and-suspenders against a self-sync loop: this workflow ships
# inside the template repo itself (craftainer/template-fastapi),
# which never has a state file, so "Read current sync state" below
# already no-ops there. This step additionally refuses the one path
# that could still set template_repo to this same repo -- a
# workflow_dispatch bootstrap run fired against the template repo by
# mistake -- so that mistake fails loudly instead of quietly seeding
# a state file that would sync the template to itself forever after.
- name: Refuse to sync a repo against itself
if: inputs.template_repo != '' && inputs.template_repo == github.repository
env:
INPUT_TEMPLATE_REPO: ${{ inputs.template_repo }}
run: |
echo "::error::template_repo ($INPUT_TEMPLATE_REPO) is this same repository -- refusing to bootstrap a self-sync loop." >&2
exit 1
- name: Check sync cadence
id: cadence
if: github.event_name == 'schedule'
env:
SYNC_INTERVAL: ${{ vars.TEMPLATE_SYNC_INTERVAL || 'weekly' }}
run: |
if ! python3 .github/scripts/template_sync.py check-cadence \
--interval "$SYNC_INTERVAL"; then
echo "Not this interval's scheduled week -- no-op."
echo "skip=true" >> "$GITHUB_OUTPUT"
fi
- name: Bootstrap state file
if: steps.cadence.outputs.skip != 'true' && inputs.initial_sync_tag != ''
env:
# Same token checkout used to persist for the push below.
GH_TOKEN: ${{ secrets.TEMPLATE_SYNC_TOKEN || github.token }}
INPUT_TEMPLATE_REPO: ${{ inputs.template_repo }}
INPUT_SYNC_TAG: ${{ inputs.initial_sync_tag }}
run: |
# Checkout doesn't persist credentials; git push uses gh's helper.
gh auth setup-git
python3 .github/scripts/template_sync.py write-state \
--state-file "$STATE_FILE" \
--template-repo "$INPUT_TEMPLATE_REPO" \
--synced-tag "$INPUT_SYNC_TAG"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
branch="template-sync/bootstrap-$INPUT_SYNC_TAG"
git checkout -b "$branch"
git add "$STATE_FILE"
git commit -m "chore(template-sync): bootstrap state at $INPUT_SYNC_TAG"
git push -u origin "$branch"
gh pr create \
--title "chore(template-sync): bootstrap state at $INPUT_SYNC_TAG" \
--body "Seeds \`$STATE_FILE\` pointing \`template_repo\` at \`$INPUT_TEMPLATE_REPO\` @ \`$INPUT_SYNC_TAG\`. No other files are touched by this bootstrap run -- the next scheduled or manual sync diffs against newer tags and opens its own PR."
- name: Read current sync state
id: state
if: steps.cadence.outputs.skip != 'true' && inputs.initial_sync_tag == ''
run: |
if ! python3 .github/scripts/template_sync.py read-state --state-file "$STATE_FILE" \
>> "$GITHUB_OUTPUT"; then
echo "::warning::no $STATE_FILE yet -- run this workflow manually with initial_sync_tag set."
echo "skip=true" >> "$GITHUB_OUTPUT"
fi
- name: Guard against a self-referential state file
if: steps.state.outputs.skip != 'true' && steps.cadence.outputs.skip != 'true' && steps.state.outputs.template_repo == github.repository
run: |
echo "::error::$STATE_FILE points template_repo at this same repository -- refusing to sync." >&2
exit 1
- name: Fetch template tags
id: tags
if: steps.state.outputs.skip != 'true' && steps.cadence.outputs.skip != 'true' && inputs.initial_sync_tag == ''
env:
GH_TOKEN: ${{ secrets.TEMPLATE_SYNC_TOKEN || github.token }}
TEMPLATE_REPO: ${{ steps.state.outputs.template_repo }}
SYNC_CHANNEL: ${{ vars.TEMPLATE_SYNC_CHANNEL || 'stable' }}
SYNCED_TAG: ${{ steps.state.outputs.synced_tag }}
run: |
gh api "repos/$TEMPLATE_REPO/tags" --paginate --jq '.[].name' \
> /tmp/template-tags.txt
new_tag=$(python3 .github/scripts/template_sync.py select-tag \
--channel "$SYNC_CHANNEL" \
< /tmp/template-tags.txt | cut -d= -f2)
echo "new_tag=$new_tag" >> "$GITHUB_OUTPUT"
if [ "$new_tag" = "$SYNCED_TAG" ]; then
echo "Already synced to $new_tag -- no-op."
echo "skip=true" >> "$GITHUB_OUTPUT"
fi
- name: Check out template at both tags
if: inputs.initial_sync_tag == '' && steps.tags.outputs.skip != 'true' && steps.state.outputs.skip != 'true' && steps.cadence.outputs.skip != 'true'
env:
GH_TOKEN: ${{ secrets.TEMPLATE_SYNC_TOKEN || github.token }}
TEMPLATE_REPO: ${{ steps.state.outputs.template_repo }}
SYNCED_TAG: ${{ steps.state.outputs.synced_tag }}
NEW_TAG: ${{ steps.tags.outputs.new_tag }}
run: |
gh repo clone "$TEMPLATE_REPO" /tmp/template-old -- \
--depth 1 --branch "$SYNCED_TAG"
gh repo clone "$TEMPLATE_REPO" /tmp/template-new -- \
--depth 1 --branch "$NEW_TAG"
- name: Apply replace and merge tiers
id: apply
if: inputs.initial_sync_tag == '' && steps.tags.outputs.skip != 'true' && steps.state.outputs.skip != 'true' && steps.cadence.outputs.skip != 'true'
run: |
cd /tmp/template-new/.github/scripts
set +e
python3 template_sync.py apply \
--manifest /tmp/template-new/.github/template-sync-manifest.yml \
--old-ref /tmp/template-old \
--new-ref /tmp/template-new \
--instance-root "$GITHUB_WORKSPACE" \
> /tmp/apply-summary.json
rc=$?
set -e
# Exit codes: 0 clean, 1 merge conflicts, 2 error. A crash before
# apply's own error handler (an import-time SyntaxError) also exits
# 1, so check-apply trusts 0 or 1 only when the summary agrees. It
# fails the job here, before the sync state is advanced, and the
# summary is printed as valid only once it has agreed.
# Only the validated `conflicts=0|1` line reaches $GITHUB_OUTPUT.
check_rc=0
check_out=$(python3 template_sync.py check-apply --exit-code "$rc" \
--summary /tmp/apply-summary.json) || check_rc=$?
case "$check_out" in
conflicts=0 | conflicts=1) [ "$check_rc" -ne 0 ] || echo "$check_out" >> "$GITHUB_OUTPUT" ;;
*)
if [ "$check_rc" -eq 0 ]; then
echo "::error::check-apply printed an unexpected line; not trusting it." >&2
check_rc=1
fi
;;
esac
if [ "$rc" -gt 1 ] || [ "$check_rc" -ne 0 ]; then
echo "::group::apply stdout (not validated)"
cat /tmp/apply-summary.json || true
echo "::endgroup::"
echo "::error::template_sync.py apply failed (exit $rc, check-apply exit $check_rc); not advancing the sync state."
# Keep apply's own status when it is an error (2, or e.g. 137 for
# a kill); a 0 or 1 that check-apply rejected is an error too.
if [ "$rc" -gt 1 ]; then
exit "$rc"
fi
exit "$check_rc"
fi
cat /tmp/apply-summary.json
- name: Update sync state
if: inputs.initial_sync_tag == '' && steps.tags.outputs.skip != 'true' && steps.state.outputs.skip != 'true' && steps.cadence.outputs.skip != 'true'
env:
TEMPLATE_REPO: ${{ steps.state.outputs.template_repo }}
NEW_TAG: ${{ steps.tags.outputs.new_tag }}
run: |
python3 .github/scripts/template_sync.py write-state \
--state-file "$STATE_FILE" \
--template-repo "$TEMPLATE_REPO" \
--synced-tag "$NEW_TAG"
- name: Open sync PR
if: inputs.initial_sync_tag == '' && steps.tags.outputs.skip != 'true' && steps.state.outputs.skip != 'true' && steps.cadence.outputs.skip != 'true'
env:
GH_TOKEN: ${{ secrets.TEMPLATE_SYNC_TOKEN || github.token }}
TEMPLATE_REPO: ${{ steps.state.outputs.template_repo }}
SYNCED_TAG: ${{ steps.state.outputs.synced_tag }}
NEW_TAG: ${{ steps.tags.outputs.new_tag }}
run: |
# Checkout doesn't persist credentials; git push uses gh's helper.
gh auth setup-git
if git diff --quiet && git diff --cached --quiet && [ -z "$(git status --porcelain)" ]; then
echo "Nothing changed -- no-op."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
branch="template-sync/$NEW_TAG"
git checkout -b "$branch"
git add -A
git commit -m "chore(template-sync): sync to $NEW_TAG"
git push -u origin "$branch"
python3 /tmp/template-new/.github/scripts/template_sync.py pr-body \
--summary /tmp/apply-summary.json \
--old-tag "$SYNCED_TAG" \
--new-tag "$NEW_TAG" \
--template-repo "$TEMPLATE_REPO" \
> /tmp/pr-body.md
gh pr create \
--title "chore(template-sync): sync to $NEW_TAG" \
--body-file /tmp/pr-body.md