Repository navigation
170 lines (150 loc) · 7.41 KB
/
Copy pathapi-sync.yml
File metadata and controls
170 lines (150 loc) · 7.41 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
name: API Sync
on:
repository_dispatch:
types: [api-sync]
permissions:
contents: write
pull-requests: write
id-token: write
jobs:
sync:
name: Sync SDK with API changes
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.2.18
# Fails loudly rather than syncing against a wrong, truncated, or already-superseded spec:
# the delivery branch must carry both spec-current.json and a delivery.json manifest whose
# spec_sha256 actually hashes to spec-current.json's bytes, and (when the dispatch payload
# carries its own spec_sha256) the manifest must agree with what triggered this run.
- name: Fetch and verify the delivered spec manifest
env:
DISPATCH_SHA256: ${{ github.event.client_payload.spec_sha256 }}
run: |
set -e
git fetch origin api-sync-data
mkdir -p /tmp/api-sync
if ! git show origin/api-sync-data:.api-sync/spec-current.json > /tmp/api-sync/spec-current.json; then
echo "::error::.api-sync/spec-current.json is missing on the api-sync-data branch."
exit 1
fi
if ! git show origin/api-sync-data:.api-sync/delivery.json > /tmp/api-sync/delivery.json; then
echo "::error::.api-sync/delivery.json is missing on the api-sync-data branch."
exit 1
fi
MANIFEST_SHA256=$(jq -r '.spec_sha256' /tmp/api-sync/delivery.json)
ACTUAL_SHA256=$(sha256sum /tmp/api-sync/spec-current.json | cut -d' ' -f1)
if [ "$MANIFEST_SHA256" != "$ACTUAL_SHA256" ]; then
echo "::error::spec-current.json sha256 ($ACTUAL_SHA256) does not match delivery.json's spec_sha256 ($MANIFEST_SHA256)."
exit 1
fi
if [ -n "$DISPATCH_SHA256" ] && [ "$DISPATCH_SHA256" != "$MANIFEST_SHA256" ]; then
echo "::error::stale delivery: the dispatch payload's spec_sha256 ($DISPATCH_SHA256) differs from delivery.json's ($MANIFEST_SHA256)."
exit 1
fi
echo "Verified spec-current.json (sha256 $ACTUAL_SHA256) against delivery.json:"
jq -r '" source_repository: \(.source_repository)\n source_sha: \(.source_sha)\n generated_at: \(.generated_at)"' /tmp/api-sync/delivery.json
- name: Check for existing api-sync PR
id: check-pr
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PR_NUMBER=$(gh pr list --head api-sync --json number --jq '.[0].number // empty')
if [ -n "$PR_NUMBER" ]; then
echo "existing_pr=$PR_NUMBER" >> "$GITHUB_OUTPUT"
echo "Found existing api-sync PR: #$PR_NUMBER"
else
echo "existing_pr=" >> "$GITHUB_OUTPUT"
echo "No existing api-sync PR found"
fi
- name: Create or checkout api-sync branch
run: |
git fetch origin api-sync 2>/dev/null || true
if git rev-parse --verify origin/api-sync >/dev/null 2>&1; then
git checkout api-sync
git reset --hard origin/main
else
git checkout -b api-sync
fi
- name: Install dependencies
run: bun install --frozen-lockfile
# The entire "what changed, and can this repo express it without a human" decision is a
# pure, deterministic script: scripts/api-sync/. It reconciles the SDK's declared surface
# against the delivered spec state (not an event diff — see reconcile.ts for why), applies
# only enum-member and optional-field additions, and hard-fails on anything else: a
# removal, a required-ness or type change, a new operation or schema, or a mapped
# symbol/anchor it can no longer find. No LLM, no best-guessing.
- name: Run the deterministic patcher
id: sync
run: |
set +e
bun scripts/api-sync/index.ts --apply --spec /tmp/api-sync/spec-current.json --report /tmp/sync-report.json
echo "exit_code=$?" >> "$GITHUB_OUTPUT"
set -e
echo "bump=$(jq -r '.bump' /tmp/sync-report.json)" >> "$GITHUB_OUTPUT"
echo "new_version=$(jq -r '.newVersion // empty' /tmp/sync-report.json)" >> "$GITHUB_OUTPUT"
echo "applied_count=$(jq -r '.applied | length' /tmp/sync-report.json)" >> "$GITHUB_OUTPUT"
# A precise, actionable failure (removal, type change, new operation/schema, missing
# anchor) fails the run loudly instead of silently applying nothing. If the repo also has
# an alerting hook wired to failed runs of this workflow, that is what pages a human.
- name: Fail loudly on a needs-human finding
if: steps.sync.outputs.exit_code != '0'
run: |
echo "::error::api-sync found change(s) it cannot apply automatically. See needsHuman below."
jq -r '.needsHuman[] | "- " + .' /tmp/sync-report.json
exit 1
- name: Exit quietly when there is nothing to apply
if: steps.sync.outputs.applied_count == '0'
run: echo "No pending drift; nothing to sync."
- name: Commit and push
id: commit
if: steps.sync.outputs.applied_count != '0'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add -A
git commit -m "$(printf 'feat: sync SDK with API changes (v%s)\n\nApplied:\n%s\n' \
"${{ steps.sync.outputs.new_version }}" \
"$(jq -r '.applied[] | "- " + .' /tmp/sync-report.json)")"
git push --force-with-lease origin api-sync
# The PAT, not GITHUB_TOKEN: a merge performed with GITHUB_TOKEN produces a
# push that starts no workflow, so the sync would land on main and never
# publish to npm.
- name: Create or update PR
if: steps.commit.outcome == 'success'
env:
GH_TOKEN: ${{ secrets.SDK_SYNC_PAT }}
run: |
BUMP="${{ steps.sync.outputs.bump }}"
VERSION="${{ steps.sync.outputs.new_version }}"
if [ "$BUMP" = "minor" ]; then
PREFIX="feat"
else
PREFIX="fix"
fi
TITLE="${PREFIX}: sync SDK with API changes (v${VERSION})"
EXISTING_PR="${{ steps.check-pr.outputs.existing_pr }}"
{
echo "Automated, script-generated SDK update from an API spec change."
echo "No LLM was involved in producing this diff; see scripts/api-sync/ in this repo."
echo
echo "### Applied"
jq -r '.applied[] | "- " + .' /tmp/sync-report.json
echo
echo "### Known divergences (recorded, not applied)"
jq -r '.knownDivergences[] | "- " + .' /tmp/sync-report.json
echo
echo "Version bump: \`${BUMP}\` -> v${VERSION}"
} > /tmp/pr-body.md
if [ -n "$EXISTING_PR" ]; then
gh pr edit "$EXISTING_PR" --title "$TITLE" --body-file /tmp/pr-body.md
PR_NUMBER="$EXISTING_PR"
else
gh pr create --title "$TITLE" --body-file /tmp/pr-body.md --base main --head api-sync --label api-sync
PR_NUMBER=$(gh pr list --head api-sync --json number --jq '.[0].number')
fi
# Enable auto-merge so the required checks (sync:check, determinism, tests, lint,
# contract-check, etc.) are the only gate; a needs-human run never reaches this step.
gh pr merge "$PR_NUMBER" --auto --squash