Sync SDK repos #1448
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: Sync SDK repos | |
| # Keeps the staging and production trunks in sync and the config repo's tracking files | |
| # fresh. Each job self-routes by repo + event, so this one file can live in both repos and | |
| # only the right job runs. The dispatch jobs are eager-only; scheduled polls cover them. | |
| on: | |
| schedule: | |
| # back-sync poll: a cheap pure-git check, twice hourly. | |
| - cron: '7,37 * * * *' | |
| workflow_dispatch: {} | |
| repository_dispatch: | |
| types: [prod-released] | |
| release: | |
| types: [published] | |
| push: | |
| branches: [main] | |
| jobs: | |
| back-sync: | |
| # Fast-forward production main back onto staging so the trunks stay identical. | |
| runs-on: ${{ vars.STLC_RUNNER || 'ubuntu-latest' }} | |
| if: >- | |
| github.repository == 'privy-io/node-sdk-private' && | |
| (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch') | |
| environment: builder-blobby-sdk-sync-and-seal | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: stlc-back-sync | |
| cancel-in-progress: true | |
| env: | |
| PRODUCTION_REPO: privy-io/node-sdk | |
| steps: | |
| - name: Check out staging | |
| uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Mint staging-repo-scoped Builder Blobby token | |
| id: staging-token | |
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 | |
| with: | |
| app-id: ${{ secrets.BUILDER_BLOBBY_APP_ID }} | |
| private-key: ${{ secrets.BUILDER_BLOBBY_APP_PRIVATE_KEY }} | |
| owner: privy-io | |
| repositories: node-sdk-private | |
| permission-contents: write | |
| - name: Fetch production main | |
| env: | |
| PRODUCTION_REPO_TOKEN: ${{ secrets.PRODUCTION_REPO_TOKEN }} | |
| run: | | |
| # Public production reads with no credential; a private production | |
| # repo needs PRODUCTION_REPO_TOKEN (the same token the promote uses). | |
| if [ -n "${PRODUCTION_REPO_TOKEN:-}" ]; then | |
| git remote add production "https://x-access-token:${PRODUCTION_REPO_TOKEN}@github.com/${PRODUCTION_REPO}.git" | |
| else | |
| git remote add production "https://github.com/${PRODUCTION_REPO}.git" | |
| fi | |
| # actions/checkout persists this repo's token as an auth header, which | |
| # outranks the remote URL credential; blank it for this fetch only. | |
| git -c "http.https://github.com/.extraheader=" fetch production main | |
| - name: Check whether production has content staging lacks | |
| id: diff | |
| run: | | |
| # Content compare: would merging production into staging change its tree? | |
| MERGED=$(git merge-tree --write-tree origin/main production/main) || MERGED=conflict | |
| STAGING_TREE=$(git rev-parse 'origin/main^{tree}') | |
| if [ "$MERGED" = "$STAGING_TREE" ]; then | |
| echo "Staging already has production's content. Nothing to pull back." | |
| echo "behind=false" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "behind=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Sync production to staging (fast-forward) | |
| if: steps.diff.outputs.behind == 'true' | |
| env: | |
| STAGING_REPO_TOKEN: ${{ steps.staging-token.outputs.token }} | |
| run: | | |
| # Refuse unless staging is an ancestor of production: otherwise the | |
| # trunks have forked and a fast-forward would be unsafe. | |
| if ! git merge-base --is-ancestor origin/main production/main; then | |
| echo "::error title=Back-sync blocked::staging main is not an ancestor of production/main." | |
| exit 1 | |
| fi | |
| git remote set-url origin "https://x-access-token:${STAGING_REPO_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" | |
| git push origin production/main:refs/heads/main | |
| echo "Fast-forwarded staging/main to production/main." | |
| notify-back-sync: | |
| # On a published release, tell staging to back-sync now instead of waiting for the | |
| # poll. The app token is scoped to staging with the Contents:write permission that | |
| # repository_dispatch requires, and this job uses it only for that dispatch. | |
| runs-on: ${{ vars.STLC_RUNNER || 'ubuntu-latest' }} | |
| if: >- | |
| github.repository == 'privy-io/node-sdk' && | |
| (github.event_name == 'release' || github.event_name == 'workflow_dispatch') | |
| environment: builder-blobby-sdk-release-and-sync | |
| permissions: | |
| contents: read | |
| env: | |
| STAGING_REPO: privy-io/node-sdk-private | |
| steps: | |
| - name: Mint staging-repo-scoped Builder Blobby token | |
| id: app-token | |
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 | |
| with: | |
| app-id: ${{ secrets.BUILDER_BLOBBY_APP_ID }} | |
| private-key: ${{ secrets.BUILDER_BLOBBY_APP_PRIVATE_KEY }} | |
| owner: privy-io | |
| repositories: node-sdk-private | |
| permission-contents: write | |
| - name: Dispatch back-sync to staging | |
| env: | |
| DISPATCH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| REF_NAME: ${{ github.ref_name }} | |
| run: | | |
| set -euo pipefail | |
| payload=$(jq -n --arg ref "$REF_NAME" '{event_type:"prod-released",client_payload:{ref:$ref}}') | |
| code=$(curl -sS -o /tmp/dispatch.txt -w '%{http_code}' -X POST \ | |
| -H "Authorization: Bearer ${DISPATCH_TOKEN}" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| "https://api.github.com/repos/${STAGING_REPO}/dispatches" \ | |
| -d "$payload") | |
| if [ "$code" = "204" ]; then | |
| echo "Back-sync dispatched to ${STAGING_REPO}." | |
| else | |
| echo "Dispatch failed (HTTP $code)" >&2; cat /tmp/dispatch.txt >&2; exit 1 | |
| fi | |
| seal-dispatch: | |
| # When out-of-band custom code lands on staging main, tell the config repo to re-seal | |
| # now instead of waiting for its scheduled sync. The loop guards skip stlc's own | |
| # pushes, so the bot's commits can't trigger a re-seal loop. Auth is a short-lived | |
| # Builder Blobby app token (from the builder-blobby-sdk-sync-and-seal environment), | |
| # downscoped to the config repo — no long-lived PAT needed. | |
| runs-on: ${{ vars.STLC_RUNNER || 'ubuntu-latest' }} | |
| if: >- | |
| github.repository == 'privy-io/node-sdk-private' && | |
| github.event_name == 'push' | |
| environment: builder-blobby-sdk-sync-and-seal | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: seal-dispatch-${{ github.ref }} | |
| cancel-in-progress: false | |
| env: | |
| CONFIG_REPO: privy-io/stainless-sdks-config | |
| steps: | |
| - name: Loop-guard | |
| id: guard | |
| env: | |
| HEAD_MSG: ${{ github.event.head_commit.message }} | |
| HEAD_AUTHOR_NAME: ${{ github.event.head_commit.author.name }} | |
| run: | | |
| set -euo pipefail | |
| # Loop guard 1: skip the stlc "Build SDK" squash commit (Stainless-Generated-From trailer). | |
| if printf '%s' "$HEAD_MSG" | grep -q 'Stainless-Generated-From'; then | |
| echo "Head commit is an stlc build — skipping re-seal dispatch." | |
| echo "proceed=false" >> "$GITHUB_OUTPUT"; exit 0 | |
| fi | |
| # Loop guard 2: skip the build bot's own commits (verify the [bot] name matches | |
| # stlc-build.yml's git identity / the GitHub App slug). | |
| if [ "$HEAD_AUTHOR_NAME" = "builder-blobby[bot]" ]; then | |
| echo "Head commit authored by the build bot — skipping re-seal dispatch." | |
| echo "proceed=false" >> "$GITHUB_OUTPUT"; exit 0 | |
| fi | |
| echo "proceed=true" >> "$GITHUB_OUTPUT" | |
| - name: Check for Builder Blobby credentials | |
| id: creds | |
| if: steps.guard.outputs.proceed == 'true' | |
| env: | |
| APP_ID: ${{ secrets.BUILDER_BLOBBY_APP_ID }} | |
| APP_KEY: ${{ secrets.BUILDER_BLOBBY_APP_PRIVATE_KEY }} | |
| run: | | |
| set -euo pipefail | |
| # Eager re-seal is optional; the config repo's scheduled poll is the backstop. | |
| # No-op (don't fail the push) when the app isn't wired up on this repo. | |
| if [ -n "${APP_ID:-}" ] && [ -n "${APP_KEY:-}" ]; then | |
| echo "present=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "::notice::Builder Blobby app credentials not configured — skipping the eager re-seal. The config repo's scheduled sync covers this." | |
| echo "present=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Mint a config-repo-scoped dispatch token | |
| id: app-token | |
| if: steps.guard.outputs.proceed == 'true' && steps.creds.outputs.present == 'true' | |
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 | |
| with: | |
| app-id: ${{ secrets.BUILDER_BLOBBY_APP_ID }} | |
| private-key: ${{ secrets.BUILDER_BLOBBY_APP_PRIVATE_KEY }} | |
| owner: privy-io | |
| # Downscope to only the config repo + only Contents:write (all repository_dispatch | |
| # needs), so this token is no broader than a single-purpose PAT would be. | |
| repositories: stainless-sdks-config | |
| permission-contents: write | |
| - name: Send re-seal dispatch | |
| if: steps.guard.outputs.proceed == 'true' && steps.creds.outputs.present == 'true' | |
| env: | |
| DISPATCH_TOKEN: ${{ steps.app-token.outputs.token }} | |
| SHA: ${{ github.sha }} | |
| REPO: ${{ github.repository }} | |
| run: | | |
| set -euo pipefail | |
| payload=$(jq -n --arg sha "$SHA" --arg repo "$REPO" \ | |
| '{event_type:"seal-custom-code",client_payload:{target:"all",sha:$sha,repo:$repo}}') | |
| code=$(curl -sS -o /tmp/dispatch.txt -w '%{http_code}' -X POST \ | |
| -H "Authorization: Bearer ${DISPATCH_TOKEN}" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| "https://api.github.com/repos/${CONFIG_REPO}/dispatches" \ | |
| -d "$payload") | |
| if [ "$code" = "204" ]; then | |
| echo "Re-seal dispatched to ${CONFIG_REPO}." | |
| else | |
| echo "Dispatch failed (HTTP $code)" >&2; cat /tmp/dispatch.txt >&2; exit 1 | |
| fi |