-
Notifications
You must be signed in to change notification settings - Fork 0
271 lines (238 loc) · 13 KB
/
Copy pathsync-from-docs.yml
File metadata and controls
271 lines (238 loc) · 13 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
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
name: Sync from Docs
on:
repository_dispatch:
types: [docs-updated]
workflow_dispatch:
permissions:
contents: write
issues: write
pull-requests: write
jobs:
create-task:
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
issue_number: ${{ steps.create-issue.outputs.issue_number }}
steps:
- name: Validate sync token
env:
SYNC_GH_TOKEN: ${{ secrets.ADMIN_GITHUB_TOKEN || secrets.BOT_GITHUB_TOKEN }}
run: |
if [ -z "$SYNC_GH_TOKEN" ]; then
echo "::error::Missing GitHub token. Set ADMIN_GITHUB_TOKEN or BOT_GITHUB_TOKEN."
exit 1
fi
- name: Checkout Docs
uses: actions/checkout@v4
with:
repository: AceDataCloud/Docs
path: _docs
token: ${{ secrets.ADMIN_GITHUB_TOKEN || secrets.BOT_GITHUB_TOKEN }}
fetch-depth: 5
- name: Get Docs changes
id: docs
run: |
cd _docs
commit_info=$(git log -1 --pretty=format:"%h %s" 2>/dev/null || echo "unknown")
echo "commit_info=$commit_info" >> "$GITHUB_OUTPUT"
recent_changes=$(git log --oneline -5 2>/dev/null || echo "unknown")
echo "recent_changes<<EOF" >> "$GITHUB_OUTPUT"
echo "$recent_changes" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
changed_services="${{ github.event.client_payload.changed_services }}"
if [ -z "$changed_services" ]; then
changed_services="all"
fi
echo "changed_services=$changed_services" >> "$GITHUB_OUTPUT"
- name: Close stale sync issues and orphaned Copilot PRs
env:
GH_TOKEN: ${{ secrets.ADMIN_GITHUB_TOKEN || secrets.BOT_GITHUB_TOKEN }}
REPO: ${{ github.repository }}
run: |
set +e
# 1) Close prior auto-sync issues — a new dispatch supersedes them.
gh issue list --repo "$REPO" --label "auto-sync" --state open --json number --jq '.[].number' | while read -r num; do
[ -z "$num" ] && continue
gh issue close "$num" --repo "$REPO" --comment "Superseded by new sync trigger." 2>/dev/null || true
done
# 2) Close every leftover open copilot/* PR — they are stale by definition once a new
# Docs commit lands. Each new dispatch triggers a fresh Copilot session that will
# open its own PR; old drafts (including 0-commit zombies) just clutter the queue.
gh pr list --repo "$REPO" --state open --json number,headRefName \
--jq '.[] | select(.headRefName | startswith("copilot/")) | .number' | while read -r num; do
[ -z "$num" ] && continue
gh pr close "$num" --repo "$REPO" --delete-branch \
--comment "Superseded by new sync trigger; Copilot will open a fresh PR for the latest Docs state." 2>/dev/null || true
done
exit 0
- name: Create issue for Copilot
id: create-issue
env:
GH_TOKEN: ${{ secrets.ADMIN_GITHUB_TOKEN || secrets.BOT_GITHUB_TOKEN }}
COMMIT_INFO: ${{ steps.docs.outputs.commit_info }}
RECENT_CHANGES: ${{ steps.docs.outputs.recent_changes }}
CHANGED_SERVICES: ${{ steps.docs.outputs.changed_services }}
REPO: ${{ github.repository }}
run: |
body=$(jq -n \
--arg changes "$RECENT_CHANGES" \
--arg services "$CHANGED_SERVICES" \
'"The upstream Docs have been updated. Ensure the SDK in this repo is in sync with the latest API specs.\n\n## Changed Services\n\n`" + $services + "`\n\n## Recent Docs Changes\n\n```\n" + $changes + "\n```\n\n## Instructions\n\nSee `.github/copilot-instructions.md` for sync rules.\n\nThis is a monorepo with `typescript/` and `python/` subdirectories.\n\nCheck the AceDataCloud/Docs repo OpenAPI specs at `openapi/<service>.json` for each changed service.\n\nCompare models, endpoints, parameters, and provider lists against the SDK code. Fix any differences.\n\nIf everything is already in sync, close this issue."')
ISSUE_JSON=$(jq -n \
--arg title "sync: update from Docs ($COMMIT_INFO)" \
--argjson body "$body" \
'{title: $title, body: $body, labels: ["auto-sync"]}' \
| gh api \
--method POST \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
/repos/"$REPO"/issues \
--input -)
ISSUE_NUM=$(echo "$ISSUE_JSON" | jq -r '.number')
ISSUE_NODE=$(echo "$ISSUE_JSON" | jq -r '.node_id')
echo "Created issue #$ISSUE_NUM (node=$ISSUE_NODE)"
OWNER="${REPO%/*}"
NAME="${REPO#*/}"
COPILOT_ID=$(gh api graphql \
-f query='query($owner:String!,$name:String!){ repository(owner:$owner,name:$name){ suggestedActors(capabilities:[CAN_BE_ASSIGNED], first:50){ nodes { __typename ... on Bot { login id } ... on User { login id } } } } }' \
-f owner="$OWNER" -f name="$NAME" \
--jq '[.data.repository.suggestedActors.nodes[] | select(.login=="copilot-swe-agent" or .login=="Copilot")] | .[0].id // empty')
if [ -z "$COPILOT_ID" ]; then
echo "::error::Copilot not found in suggestedActors. ADMIN_GITHUB_TOKEN must belong to a user with a Copilot seat in this org."
gh issue comment "$ISSUE_NUM" --repo "$REPO" --body "Auto-assignment failed: Copilot not visible to ADMIN_GITHUB_TOKEN. Please assign Copilot manually." || true
exit 1
fi
gh api graphql \
-f query='mutation($a:ID!,$b:[ID!]!){ replaceActorsForAssignable(input:{assignableId:$a, actorIds:$b}){ assignable { ... on Issue { number assignees(first:5){ nodes { login } } } } } }' \
-f a="$ISSUE_NODE" -f b="$COPILOT_ID"
echo "issue_number=$ISSUE_NUM" >> "$GITHUB_OUTPUT"
wait-and-merge:
needs: create-task
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Wait for Copilot to finish
id: wait
env:
GH_TOKEN: ${{ secrets.ADMIN_GITHUB_TOKEN || secrets.BOT_GITHUB_TOKEN }}
ISSUE_NUMBER: ${{ needs.create-task.outputs.issue_number }}
REPO: ${{ github.repository }}
run: |
set +e
echo "Waiting for Copilot to process issue #$ISSUE_NUMBER..."
PR_NUMBER=""
PR_FOUND_AT=0
ZOMBIE_GRACE_SECONDS=1800 # close PR after 30 min if it still has 0 commits
# 110 polls × 30s = 55 min, fits inside the 60-min job timeout.
for i in $(seq 1 110); do
now=$(date +%s)
echo "Poll attempt $i/110 (elapsed=$((i * 30))s)..."
# Fast path: Copilot closed the issue itself (no changes needed)
ISSUE_STATE=$(gh issue view "$ISSUE_NUMBER" --repo "$REPO" --json state --jq '.state' 2>/dev/null)
if [ "$ISSUE_STATE" = "CLOSED" ]; then
echo "Issue #$ISSUE_NUMBER closed by Copilot — no changes needed."
echo "result=no-op" >> "$GITHUB_OUTPUT"
exit 0
fi
# Locate the PR linked to THIS issue. Copilot puts "#<issue>" or "Closes #<issue>"
# in the PR body when assigned to an issue.
CANDIDATE=$(gh pr list --repo "$REPO" --state open \
--json number,headRefName,body,isDraft,additions,deletions,createdAt \
--jq "[.[] | select(.headRefName | startswith(\"copilot/\")) | select(.body // \"\" | test(\"#${ISSUE_NUMBER}( |$|\\\\b)\"))] | .[0] // empty" \
2>/dev/null)
# Fallback: if no PR mentions this issue but exactly one copilot/* PR exists,
# take it. (Stale ones were already closed in create-task.)
if [ -z "$CANDIDATE" ]; then
CANDIDATE=$(gh pr list --repo "$REPO" --state open \
--json number,headRefName,body,isDraft,additions,deletions,createdAt \
--jq '[.[] | select(.headRefName | startswith("copilot/"))] | if length == 1 then .[0] else empty end' \
2>/dev/null)
fi
if [ -n "$CANDIDATE" ]; then
PR_NUMBER=$(echo "$CANDIDATE" | jq -r '.number')
IS_DRAFT=$(echo "$CANDIDATE" | jq -r '.isDraft')
ADDITIONS=$(echo "$CANDIDATE" | jq -r '.additions')
DELETIONS=$(echo "$CANDIDATE" | jq -r '.deletions')
CREATED_AT=$(echo "$CANDIDATE" | jq -r '.createdAt')
if [ "$PR_FOUND_AT" -eq 0 ]; then
PR_FOUND_AT=$now
echo "Tracking PR #$PR_NUMBER (draft=$IS_DRAFT, +$ADDITIONS/-$DELETIONS)"
fi
# Reliable completion signal: Copilot marks the PR ready-for-review
# itself when its agent finishes successfully.
if [ "$IS_DRAFT" = "false" ]; then
echo "PR #$PR_NUMBER is ready for review — Copilot finished."
echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
echo "result=ready" >> "$GITHUB_OUTPUT"
exit 0
fi
# Zombie guard: if PR has been open >30 min with 0 commits, give up.
if [ "$ADDITIONS" = "0" ] && [ "$DELETIONS" = "0" ]; then
created_epoch=$(date -d "$CREATED_AT" +%s 2>/dev/null || python3 -c "import datetime,sys; print(int(datetime.datetime.fromisoformat(sys.argv[1].replace('Z','+00:00')).timestamp()))" "$CREATED_AT")
age=$((now - created_epoch))
if [ "$age" -gt "$ZOMBIE_GRACE_SECONDS" ]; then
echo "::warning::PR #$PR_NUMBER has 0 commits after ${age}s — closing as zombie."
gh pr close "$PR_NUMBER" --repo "$REPO" --delete-branch \
--comment "Auto-closed: Copilot opened this PR but produced no commits within ${ZOMBIE_GRACE_SECONDS}s. Triggering workflow can be re-dispatched if a sync is still needed." 2>/dev/null || true
echo "result=zombie-closed" >> "$GITHUB_OUTPUT"
exit 1
fi
fi
fi
sleep 30
done
echo "::warning::Timed out waiting for Copilot to mark PR ready for review."
if [ -n "$PR_NUMBER" ]; then
echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
echo "result=timeout-with-pr" >> "$GITHUB_OUTPUT"
else
echo "result=timeout-no-pr" >> "$GITHUB_OUTPUT"
fi
exit 1
- name: Merge PR
if: steps.wait.outputs.pr_number != ''
env:
GH_TOKEN: ${{ secrets.ADMIN_GITHUB_TOKEN || secrets.BOT_GITHUB_TOKEN }}
PR_NUMBER: ${{ steps.wait.outputs.pr_number }}
ISSUE_NUMBER: ${{ needs.create-task.outputs.issue_number }}
REPO: ${{ github.repository }}
run: |
set +e
echo "Preparing to merge PR #$PR_NUMBER..."
# Idempotent: makes draft → ready-for-review. Harmless if already ready.
gh pr ready "$PR_NUMBER" --repo "$REPO" 2>/dev/null || true
# Wait briefly for required checks (validate.yml) to settle. We use --admin
# at merge time so this is best-effort: we surface check failures but don't
# let a single transient flake block the sync.
for i in $(seq 1 24); do
CHECKS=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json statusCheckRollup \
--jq '[.statusCheckRollup[]? | {name, status, conclusion}]' 2>/dev/null)
FAILING=$(echo "$CHECKS" | jq '[.[] | select(.conclusion == "failure" or .conclusion == "cancelled" or .conclusion == "timed_out")] | length' 2>/dev/null || echo 0)
PENDING=$(echo "$CHECKS" | jq '[.[] | select(.status != "completed")] | length' 2>/dev/null || echo 0)
TOTAL=$(echo "$CHECKS" | jq 'length' 2>/dev/null || echo 0)
echo "Checks: total=$TOTAL pending=$PENDING failing=$FAILING"
if [ "${FAILING:-0}" -gt 0 ]; then
echo "::warning::PR #$PR_NUMBER has $FAILING failing check(s) — proceeding with --admin merge anyway. Investigate after merge."
break
fi
if [ "${TOTAL:-0}" -gt 0 ] && [ "${PENDING:-0}" -eq 0 ]; then
echo "All checks settled."
break
fi
sleep 15
done
# Try the merge up to 3 times — covers transient API hiccups.
for attempt in 1 2 3; do
echo "Merge attempt $attempt/3..."
if gh pr merge "$PR_NUMBER" --repo "$REPO" --squash --admin --delete-branch \
--subject "sync: update from Docs [automated] (#$PR_NUMBER)" \
--body "Closes #$ISSUE_NUMBER"; then
echo "::notice::Merged PR #$PR_NUMBER"
exit 0
fi
echo "Merge attempt $attempt failed, retrying in 20s..."
sleep 20
done
echo "::error::Failed to merge PR #$PR_NUMBER after 3 attempts. Manual intervention required."
gh pr comment "$PR_NUMBER" --repo "$REPO" --body "Auto-merge failed after 3 attempts. Please review and merge manually." 2>/dev/null || true
exit 1