Sync SDK repos #264
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. | |
| # | |
| on: | |
| schedule: | |
| # back-sync poll: a cheap pure-git check, twice hourly so an unsynced | |
| # production change (release commits, a community PR) can't hold codegen | |
| # for long — the config repo's production-ahead guard holds builds while | |
| # production is ahead. | |
| - cron: '7,37 * * * *' | |
| workflow_dispatch: {} | |
| repository_dispatch: | |
| types: [prod-released] | |
| release: | |
| types: [published] | |
| push: | |
| # main only. stlc preview/integrated/codegen branches never push to main. | |
| branches: [main] | |
| jobs: | |
| back-sync: | |
| # Fast-forward production main back onto staging so the trunks stay | |
| # identical. The staging push uses the App token, not GITHUB_TOKEN — | |
| # GITHUB_TOKEN pushes never create workflow runs, which would leave | |
| # seal-dispatch below dead for back-synced release commits. | |
| runs-on: ubuntu-latest | |
| if: >- | |
| github.repository == 'muxinc/mux-ts-staging' && | |
| (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch') | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: stlc-back-sync | |
| cancel-in-progress: true | |
| env: | |
| PRODUCTION_REPO: muxinc/mux-ts | |
| steps: | |
| - name: Mint mux-sdks-ci App token (production read + staging push) | |
| id: mint | |
| uses: actions/create-github-app-token@v2 | |
| with: | |
| app-id: ${{ vars.STLC_APP_ID }} | |
| private-key: ${{ secrets.STLC_APP_PRIVATE_KEY }} | |
| owner: muxinc | |
| repositories: mux-ts,mux-ts-staging | |
| - name: Check out staging | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| - name: Fetch production main | |
| env: | |
| PRODUCTION_REPO_TOKEN: ${{ steps.mint.outputs.token }} | |
| run: | | |
| # Authenticated fetch works whether production is public or private. | |
| git remote add production "https://x-access-token:${PRODUCTION_REPO_TOKEN}@github.com/${PRODUCTION_REPO}.git" | |
| # 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? | |
| # If not, staging already has production's content (release-please commits). | |
| 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_PUSH_TOKEN: ${{ steps.mint.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 | |
| # App-token push (not the checkout's GITHUB_TOKEN) so seal-dispatch | |
| # fires — see the job comment. Header blanked for the same reason as | |
| # the production fetch. | |
| git -c "http.https://github.com/.extraheader=" push \ | |
| "https://x-access-token:${STAGING_PUSH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" \ | |
| production/main:refs/heads/main | |
| echo "Fast-forwarded staging/main to production/main." | |
| - name: Alert on failure | |
| if: failure() | |
| env: | |
| ALERT_WEBHOOK_URL: ${{ secrets.STLC_ALERT_WEBHOOK_URL }} | |
| run: | | |
| run_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" | |
| msg="stlc back-sync (sync from production) failed in ${{ github.repository }}. A stalled back-sync lets custom-code tracking drift, which later builds refuse on — investigate before the next build. Run: $run_url" | |
| echo "::error title=stlc workflow failed::$msg" | |
| { echo "### ⚠️ stlc workflow failed"; echo ""; echo "$msg"; } >> "$GITHUB_STEP_SUMMARY" | |
| if [ -n "${ALERT_WEBHOOK_URL:-}" ]; then | |
| curl -sS -X POST -H 'Content-Type: application/json' \ | |
| -d "$(jq -n --arg text "$msg" '{text:$text}')" "$ALERT_WEBHOOK_URL" \ | |
| || echo "::warning::Alert webhook POST failed" | |
| fi | |
| notify-back-sync: | |
| # On a published release, tell staging to back-sync now instead of waiting | |
| # for the poll. Dispatch-only: it cannot write production or staging | |
| # contents, and the production repo deliberately holds no key that could — | |
| # without STAGING_DISPATCH_TOKEN this no-ops and the poll covers it. | |
| runs-on: ubuntu-latest | |
| if: >- | |
| github.repository == 'muxinc/mux-ts' && | |
| (github.event_name == 'release' || github.event_name == 'workflow_dispatch') | |
| permissions: | |
| contents: read | |
| env: | |
| STAGING_REPO: muxinc/mux-ts-staging | |
| steps: | |
| - name: Dispatch back-sync to staging | |
| env: | |
| DISPATCH_TOKEN: ${{ secrets.STAGING_DISPATCH_TOKEN }} | |
| REF_NAME: ${{ github.ref_name }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "${DISPATCH_TOKEN:-}" ]; then | |
| echo "::notice::STAGING_DISPATCH_TOKEN not configured — skipping the eager back-sync notify. The staging repo's twice-hourly poll covers this." | |
| exit 0 | |
| fi | |
| 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 | |
| - name: Alert on failure | |
| if: failure() | |
| env: | |
| ALERT_WEBHOOK_URL: ${{ secrets.STLC_ALERT_WEBHOOK_URL }} | |
| run: | | |
| run_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" | |
| msg="stlc release back-sync trigger failed in ${{ github.repository }} — the staging repo was NOT notified to back-sync this release (likely an expired STAGING_DISPATCH_TOKEN). Staging catches up on its next poll, but verify the token. Run: $run_url" | |
| echo "::error title=stlc workflow failed::$msg" | |
| { echo "### ⚠️ stlc workflow failed"; echo ""; echo "$msg"; } >> "$GITHUB_STEP_SUMMARY" | |
| if [ -n "${ALERT_WEBHOOK_URL:-}" ]; then | |
| curl -sS -X POST -H 'Content-Type: application/json' \ | |
| -d "$(jq -n --arg text "$msg" '{text:$text}')" "$ALERT_WEBHOOK_URL" \ | |
| || echo "::warning::Alert webhook POST failed" | |
| fi | |
| seal-dispatch: | |
| # When out-of-band changes land on staging main (a back-synced release, a | |
| # community contribution, a manual commit), tell the config repo to | |
| # re-seal tracking now instead of waiting for its scheduled sync. The | |
| # loop guards skip stlc's own build pushes, so CI commits can't trigger a | |
| # re-seal loop. Back-synced commits reach this job only because the | |
| # back-sync above pushes with the App token — a GITHUB_TOKEN push creates | |
| # no workflow runs and would leave this job dead for its primary trigger. | |
| runs-on: ubuntu-latest | |
| if: >- | |
| github.repository == 'muxinc/mux-ts-staging' && | |
| github.event_name == 'push' | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: seal-dispatch-${{ github.ref }} | |
| cancel-in-progress: false | |
| env: | |
| CONFIG_REPO: muxinc/openapi-specification | |
| 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 "skip=true" >> "$GITHUB_OUTPUT"; exit 0 | |
| fi | |
| # Loop guard 2: skip CI-authored commits (generate/sync workflow pushes). | |
| if [ "$HEAD_AUTHOR_NAME" = "mux-sdks-ci[bot]" ]; then | |
| echo "Head commit authored by mux-sdks-ci[bot] — skipping re-seal dispatch." | |
| echo "skip=true" >> "$GITHUB_OUTPUT"; exit 0 | |
| fi | |
| echo "skip=false" >> "$GITHUB_OUTPUT" | |
| - name: Mint mux-sdks-ci App token (config-repo dispatch) | |
| if: steps.guard.outputs.skip == 'false' | |
| id: mint | |
| uses: actions/create-github-app-token@v2 | |
| with: | |
| app-id: ${{ vars.STLC_APP_ID }} | |
| private-key: ${{ secrets.STLC_APP_PRIVATE_KEY }} | |
| owner: muxinc | |
| repositories: openapi-specification | |
| - name: Send re-seal dispatch | |
| if: steps.guard.outputs.skip == 'false' | |
| env: | |
| DISPATCH_TOKEN: ${{ steps.mint.outputs.token }} | |
| SHA: ${{ github.sha }} | |
| REPO: ${{ github.repository }} | |
| run: | | |
| set -euo pipefail | |
| # event_type matches the config repo's dispatch listener. | |
| 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 | |
| - name: Alert on failure | |
| if: failure() | |
| env: | |
| ALERT_WEBHOOK_URL: ${{ secrets.STLC_ALERT_WEBHOOK_URL }} | |
| run: | | |
| run_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" | |
| msg="stlc seal-dispatch failed in ${{ github.repository }} — the config repo was NOT notified to re-seal. Its twice-daily tracking sync is the backstop. Run: $run_url" | |
| echo "::error title=stlc workflow failed::$msg" | |
| { echo "### ⚠️ stlc workflow failed"; echo ""; echo "$msg"; } >> "$GITHUB_STEP_SUMMARY" | |
| if [ -n "${ALERT_WEBHOOK_URL:-}" ]; then | |
| curl -sS -X POST -H 'Content-Type: application/json' \ | |
| -d "$(jq -n --arg text "$msg" '{text:$text}')" "$ALERT_WEBHOOK_URL" \ | |
| || echo "::warning::Alert webhook POST failed" | |
| fi |