Skip to content

api-sync

api-sync #172

Workflow file for this run

name: API Sync
on:
repository_dispatch:
types: [api-sync]
permissions:
contents: write
pull-requests: write
id-token: write
jobs:
sync:
name: Run deterministic spec-sync patcher
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Fetch the newly delivered spec + manifest from api-sync-data
run: |
git fetch origin api-sync-data
git show origin/api-sync-data:.api-sync/spec-current.json > .api-sync/spec-current.json
git show origin/api-sync-data:.api-sync/delivery.json > /tmp/delivery.json
- name: Verify delivery manifest
env:
DISPATCH_SPEC_SHA256: ${{ github.event.client_payload.spec_sha256 }}
run: |
set -euo pipefail
if [ ! -s .api-sync/spec-current.json ]; then
echo "::error::spec-current.json is missing or empty on api-sync-data. Aborting."
exit 1
fi
if [ ! -s /tmp/delivery.json ]; then
echo "::error::delivery.json is missing or empty on api-sync-data. Aborting."
exit 1
fi
MANIFEST_SHA256=$(jq -r '.spec_sha256 // empty' /tmp/delivery.json)
MANIFEST_SOURCE_REPO=$(jq -r '.source_repository // empty' /tmp/delivery.json)
MANIFEST_SOURCE_SHA=$(jq -r '.source_sha // empty' /tmp/delivery.json)
MANIFEST_GENERATED_AT=$(jq -r '.generated_at // empty' /tmp/delivery.json)
if [ -z "$MANIFEST_SHA256" ] || [ -z "$MANIFEST_SOURCE_REPO" ] || [ -z "$MANIFEST_SOURCE_SHA" ] || [ -z "$MANIFEST_GENERATED_AT" ]; then
echo "::error::delivery.json is missing one or more required fields (source_repository, source_sha, spec_sha256, generated_at)."
exit 1
fi
if [ "$MANIFEST_SOURCE_REPO" != "blindpaylabs/blindpay-v2" ]; then
echo "::error::delivery.json source_repository is '$MANIFEST_SOURCE_REPO', expected 'blindpaylabs/blindpay-v2'."
exit 1
fi
ACTUAL_SHA256=$(sha256sum .api-sync/spec-current.json | cut -d' ' -f1)
if [ "$ACTUAL_SHA256" != "$MANIFEST_SHA256" ]; then
echo "::error::spec-current.json sha256 ($ACTUAL_SHA256) does not match delivery.json's spec_sha256 ($MANIFEST_SHA256). The delivered spec is corrupt or does not match its manifest."
exit 1
fi
if [ -n "${DISPATCH_SPEC_SHA256:-}" ] && [ "$DISPATCH_SPEC_SHA256" != "$MANIFEST_SHA256" ]; then
echo "::error::Stale delivery: the repository_dispatch payload's spec_sha256 ($DISPATCH_SPEC_SHA256) does not match the manifest currently on api-sync-data ($MANIFEST_SHA256). A newer delivery has landed since this dispatch fired -- re-trigger."
exit 1
fi
echo "Delivery manifest verified: source_sha=$MANIFEST_SOURCE_SHA generated_at=$MANIFEST_GENERATED_AT spec_sha256=$MANIFEST_SHA256"
- name: Create or reset the api-sync branch from main
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: Run the patcher
id: patch
continue-on-error: true
run: python3 .api-sync/sync.py --apply --report /tmp/api-sync-report.json
- name: Show report
if: always()
run: |
if [ -f /tmp/api-sync-report.json ]; then
cat /tmp/api-sync-report.json
else
echo "(no report written -- the patcher failed before producing one, see the 'Run the patcher' step)"
fi
- name: Print coverage report (non-blocking)
if: always()
run: python3 .api-sync/sync.py --coverage || true
- name: Fail loudly if the patcher could not apply cleanly
if: steps.patch.outcome == 'failure'
run: |
echo "::error::api-sync patcher could not apply the new spec cleanly. This needs a human: see the 'Run the patcher' and 'Show report' step output above for the exact NEEDS_HUMAN reason(s)."
exit 1
- name: Determine whether there is anything to commit
id: bump
run: |
python3 - <<'PY' >> "$GITHUB_OUTPUT"
import json
with open("/tmp/api-sync-report.json") as f:
report = json.load(f)
applied = report.get("applied", [])
bump = report.get("bump")
prefix = {"minor": "feat", "patch": "fix"}.get(bump, "chore")
print(f"has_changes={'true' if applied else 'false'}")
print(f"bump={bump or ''}")
print(f"prefix={prefix}")
PY
- name: Commit and push
if: steps.bump.outputs.has_changes == 'true'
run: |
git remote set-url origin "https://x-access-token:${{ secrets.SDK_SYNC_PAT }}@github.com/${{ github.repository }}.git"
# Defense in depth: the patcher's own spec-map.json never points at
# .github/workflows, but never let the automated flow commit changes
# there even if something upstream of this step somehow produced one.
git checkout -- .github/workflows/ 2>/dev/null || true
git add -A
git reset HEAD .github/workflows/ 2>/dev/null || true
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git commit -m "${{ steps.bump.outputs.prefix }}: sync SDK with API changes"
git push --force-with-lease origin api-sync
- name: Check for existing api-sync PR
if: steps.bump.outputs.has_changes == 'true'
id: check-pr
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PR_NUMBER=$(gh pr list --head api-sync --json number --jq '.[0].number // empty')
echo "existing_pr=$PR_NUMBER" >> "$GITHUB_OUTPUT"
- name: Create or update PR and enable auto-merge
if: steps.bump.outputs.has_changes == 'true'
env:
GH_TOKEN: ${{ secrets.SDK_SYNC_PAT }}
run: |
set -e
EXISTING_PR="${{ steps.check-pr.outputs.existing_pr }}"
TITLE="${{ steps.bump.outputs.prefix }}: sync SDK with API changes"
BODY="Automated, deterministic SDK sync (.api-sync/sync.py --apply). Bump: ${{ steps.bump.outputs.bump }}. See the workflow run's 'Show report' step for the exact applied changes."
if [ -n "$EXISTING_PR" ]; then
gh pr edit "$EXISTING_PR" --title "$TITLE" --body "$BODY"
PR_NUMBER="$EXISTING_PR"
else
gh pr create --title "$TITLE" --body "$BODY" --base main --head api-sync --label api-sync
PR_NUMBER=$(gh pr list --head api-sync --json number --jq '.[0].number')
fi
gh pr merge "$PR_NUMBER" --auto --squash