api-sync #172
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |