Repository navigation
243 lines (219 loc) · 10.3 KB
/
Copy pathupstream-sync.yml
File metadata and controls
243 lines (219 loc) · 10.3 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
239
240
241
242
243
name: Upstream Release Sync
on:
schedule:
# 00:30 Asia/Shanghai = 16:30 UTC on the previous day.
- cron: '30 16 * * *'
workflow_dispatch:
permissions:
actions: write
contents: write
pull-requests: write
# The waiter reads the combined commit status. main's previous workflow
# file does not grant this; the script treats that 403 as "unreadable"
# and falls back to workflow job results.
statuses: read
concurrency:
group: upstream-release-sync
cancel-in-progress: false
env:
UPSTREAM_REPOSITORY: james-6-23/codex2api
TARGET_BRANCH: codex2api-custom
jobs:
prepare-main-pr:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Checkout release baseline
uses: actions/checkout@v5
with:
ref: main
fetch-depth: 0
- name: Create upstream baseline PR
env:
GH_TOKEN: ${{ github.token }}
# Optional. A fine-grained PAT or GitHub App token with contents,
# pull requests, and actions write. When set, the push is not made
# with GITHUB_TOKEN, so pull_request workflows run without the
# approval gate. When unset, workflow_dispatch results are mirrored
# onto commit statuses.
UPSTREAM_SYNC_TOKEN: ${{ secrets.UPSTREAM_SYNC_TOKEN }}
shell: bash
run: |
set -euo pipefail
git remote add upstream "https://github.com/${UPSTREAM_REPOSITORY}.git"
git fetch --no-tags origin \
"refs/heads/main:refs/remotes/origin/main" \
"refs/heads/${TARGET_BRANCH}:refs/remotes/origin/${TARGET_BRANCH}"
# Preserve and baseline lists are owned by codex2api-custom. main's
# copy of this script is the previous sync and would miss paths
# added on custom since then.
catalog=$(mktemp)
git show "refs/remotes/origin/${TARGET_BRANCH}:.github/scripts/dispatch-pr-checks.sh" > "$catalog"
# shellcheck disable=SC1090
source "$catalog"
rm -f "$catalog"
configure_upstream_sync_token
expected_main_sha=$(git rev-parse refs/remotes/origin/main)
expected_custom_sha=$(git rev-parse "refs/remotes/origin/${TARGET_BRANCH}")
git reset --hard "$expected_main_sha"
read_marker() {
git show "$1:.upstream-release" 2>/dev/null | sed -n '1p' || true
}
current_main_tag=$(read_marker "$expected_main_sha")
current_custom_tag=$(read_marker "$expected_custom_sha")
latest_tag=$(git ls-remote --tags --refs upstream 'v[0-9]*' \
| awk -F/ '{print $3}' \
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \
| sort -V \
| tail -n 1 || true)
if [[ -z "$latest_tag" ]]; then
echo "No semver release tag was found upstream."
exit 0
fi
echo "current_main_release=${current_main_tag:-<none>}"
echo "current_custom_release=${current_custom_tag:-<none>}"
echo "latest_release=${latest_tag}"
marker_ahead_of_latest() {
local marker="$1"
[[ -z "$marker" ]] && return 1
[[ "$marker" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || return 1
[[ "$marker" != "$latest_tag" && "$(printf '%s\n' "$latest_tag" "$marker" | sort -V | tail -n 1)" == "$marker" ]]
}
if marker_ahead_of_latest "$current_main_tag" || marker_ahead_of_latest "$current_custom_tag"; then
echo "A release marker is newer than upstream ${latest_tag}; refusing to roll branches back." >&2
exit 1
fi
if [[ "$latest_tag" == "$current_main_tag" && "$latest_tag" == "$current_custom_tag" ]]; then
{
echo "### Upstream release sync"
echo
echo "No new upstream release is available."
echo "- Current release: \`${latest_tag}\`"
} >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
git fetch --no-tags upstream \
"refs/tags/${latest_tag}:refs/tags/upstream-${latest_tag}"
upstream_sha=$(git rev-parse "refs/tags/upstream-${latest_tag}^{commit}")
# Main follows the upstream tree, with maintained automation files
# and a narrow source-map-js security patch preserved.
preserve_dir=$(mktemp -d)
trap 'rm -rf "$preserve_dir"' EXIT
for path in "${custom_baseline_paths[@]}"; do
mkdir -p "$preserve_dir/$(dirname "$path")"
git show "refs/remotes/origin/${TARGET_BRANCH}:${path}" > "$preserve_dir/$path"
done
baseline_branch="sync/baseline-${latest_tag}"
git switch --create "$baseline_branch" "$expected_main_sha"
git read-tree --reset -u "$upstream_sha"
for path in "${custom_baseline_paths[@]}"; do
mkdir -p "$(dirname "$path")"
cp "$preserve_dir/$path" "$path"
done
node .github/scripts/patch-source-map-js-lock.mjs frontend/package-lock.json
printf '%s\n' "$latest_tag" > .upstream-release
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add -A
if git diff --cached --quiet; then
echo "The upstream release tree produces no baseline changes."
exit 0
fi
existing_pr=$(gh pr list \
--repo "$GITHUB_REPOSITORY" \
--state open \
--base main \
--head "$baseline_branch" \
--json number \
--jq '.[0].number // empty')
# Same tree as the open baseline branch: do not force-push a new
# commit. A nightly rerun was invalidating green checks and timing
# out again before auto-merge could be enabled.
bash .github/scripts/dispatch-pr-checks.sh --reuse-or-push \
"$baseline_branch" \
"chore: sync upstream release ${latest_tag} into main"
if [[ -z "$existing_pr" ]]; then
pr_body_file=$(mktemp)
trap 'rm -f "$pr_body_file"; rm -rf "$preserve_dir"' EXIT
cat > "$pr_body_file" <<EOF
## Upstream baseline sync
This PR updates \`main\` to upstream release ${latest_tag}.
- The upstream release tree is the main branch baseline.
- The scheduler and protected CI workflow files are retained.
- The custom branch is not changed by this PR.
- After merge, the custom synchronization workflow will create the PR into \`codex2api-custom\`.
EOF
pr_url=$(gh pr create \
--repo "$GITHUB_REPOSITORY" \
--base main \
--head "$baseline_branch" \
--title "chore: sync upstream release ${latest_tag} into main" \
--body-file "$pr_body_file")
else
pr_url="https://github.com/${GITHUB_REPOSITORY}/pull/${existing_pr}"
fi
pr_number=${pr_url##*/}
expected_head_sha=$(git rev-parse "$baseline_branch")
pr_head_sha=""
for attempt in $(seq 1 20); do
pr_head_sha=$(gh pr view "$pr_number" --repo "$GITHUB_REPOSITORY" --json headRefOid --jq '.headRefOid' 2>/dev/null || true)
if [[ "$pr_head_sha" == "$expected_head_sha" ]]; then
break
fi
sleep 2
done
if [[ "$pr_head_sha" != "$expected_head_sha" ]]; then
echo "Baseline PR #${pr_number} head does not match the prepared upstream commit after waiting for GitHub to converge." >&2
echo "expected=${expected_head_sha} actual=${pr_head_sha:-<empty>}" >&2
exit 1
fi
bash .github/scripts/dispatch-pr-checks.sh "$baseline_branch" "$pr_head_sha" main "$pr_number"
# --delete-branch removes this PR's head (sync/baseline-*) only.
# The base is main and is not deleted. cleanup-sync-branch.yml
# repeats the delete for any same-repo head the setting misses,
# and it refuses main and codex2api-custom.
gh pr merge "$pr_number" --repo "$GITHUB_REPOSITORY" --auto --merge --delete-branch
merged=false
for attempt in $(seq 1 100); do
pr_json=$(gh pr view "$pr_url" --json state,mergedAt,statusCheckRollup)
pr_state=$(jq -r '.state // ""' <<< "$pr_json")
if [[ "$pr_state" == "MERGED" ]]; then
merged=true
break
fi
# Abort only when a required context failed. Branch protection
# still blocks the merge. An approval-gated workflow shell has
# no required name, and an unrelated failing check must not
# cancel auto-merge. Pending required checks keep this loop
# waiting until GitHub merges or the attempt budget ends.
failed_checks=$(bash .github/scripts/dispatch-pr-checks.sh --failed-required-checks "$pr_number" main)
if [[ -n "$failed_checks" ]]; then
echo "Upstream baseline PR has failed checks: ${failed_checks}" >&2
exit 1
fi
if [[ "$pr_state" == "CLOSED" ]]; then
echo "The upstream baseline PR closed without merging: ${pr_url}" >&2
exit 1
fi
sleep 15
done
if [[ "$merged" != true ]]; then
echo "Timed out waiting for the upstream baseline PR to merge: ${pr_url}" >&2
exit 1
fi
# A bot-created PR merge does not reliably emit pull_request:closed
# for the next workflow. workflow_dispatch is an explicit trigger
# supported by GITHUB_TOKEN, so dispatch the custom stage after the
# protected main merge is confirmed.
gh workflow run 'Merge Upstream Baseline Into Custom' \
--repo "$GITHUB_REPOSITORY" \
--ref main
{
echo "### Upstream baseline sync"
echo
echo "Prepared upstream release \`${latest_tag}\` as a protected main PR."
echo "- baseline branch: \`${baseline_branch}\`"
echo "- PR: ${pr_url}"
echo "- main remains unchanged until the required checks pass"
echo "- custom synchronization was dispatched after the protected main merge"
} >> "$GITHUB_STEP_SUMMARY"