auto-sync-upstream #63
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: auto-sync-upstream | |
| # Automatically syncs the fork with upstream google/adk-python. | |
| # | |
| # Flow (delegated to functions in scripts/lib/sync-core.sh): | |
| # 1. Merge upstream into main. Content conflicts under .github/workflows/** | |
| # auto-resolve via .gitattributes; modify/delete conflicts there | |
| # auto-resolve in sync_merge_upstream. Real conflicts stop the merge. | |
| # 2. Protect the two fork-owned workflow files (belt-and-braces, in case | |
| # .gitattributes itself got rewritten by the same merge). | |
| # 3. Install + run the model tests. | |
| # 4. Tests pass -> publish (advance both main and stable). | |
| # 5. Re-disable inherited upstream workflows that may have re-activated. | |
| # 6. Any failure -> open (or comment on the existing) auto-sync issue. | |
| # | |
| # Consumers pin `@stable`, so they only ever receive green syncs and are | |
| # automatically held at the last working version when a sync fails. | |
| # | |
| # WHY THE SHARED LIBRARY: this workflow AND scripts/update-fork.sh both | |
| # source scripts/lib/sync-core.sh, so manual and automated recoveries | |
| # execute mechanically-identical logic. Missing that parity caused an | |
| # outage on 2026-07-02: the re-disable step lived only in this YAML, so | |
| # a manual recovery via update-fork.sh left Continuous Integration | |
| # active, which then failed on lint against our patch. | |
| # | |
| # Authentication model: | |
| # Checkout + push use SYNC_TOKEN (a fine-grained PAT with `workflow: write` | |
| # scope) instead of the built-in GITHUB_TOKEN. The built-in token cannot | |
| # push changes under `.github/workflows/**` (GitHub gates that behind the | |
| # `workflow` OAuth scope, which only PATs carry). | |
| # | |
| # Security: only static commands and trusted context values (github.run_id, | |
| # github.repository, github.server_url) plus this workflow's own step outputs | |
| # are used. No untrusted github.event.* input is interpolated into run steps. | |
| on: | |
| schedule: | |
| - cron: '0 6 * * *' # daily 06:00 UTC | |
| workflow_dispatch: {} | |
| permissions: | |
| contents: write # governs GITHUB_TOKEN (used by the failure-report step). | |
| issues: write # The actual push uses SYNC_TOKEN, which is independent | |
| actions: write # of these permissions. | |
| concurrency: | |
| group: auto-sync | |
| cancel-in-progress: false | |
| env: | |
| INTEGRATION_BRANCH: main | |
| STABLE_BRANCH: stable | |
| UPSTREAM_REPO: https://github.com/google/adk-python.git | |
| jobs: | |
| sync: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout integration branch (full history) | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: main | |
| fetch-depth: 0 | |
| # PAT, not GITHUB_TOKEN: needed so the later push step can write | |
| # changes under .github/workflows/**. | |
| token: ${{ secrets.SYNC_TOKEN }} | |
| - name: Configure git and merge drivers | |
| run: | | |
| . "$GITHUB_WORKSPACE/scripts/lib/sync-core.sh" | |
| sync_configure_git | |
| - name: Merge upstream | |
| id: merge | |
| run: | | |
| . "$GITHUB_WORKSPACE/scripts/lib/sync-core.sh" | |
| sync_ensure_upstream_remote | |
| sync_merge_upstream | |
| echo "before=$SYNC_BEFORE" >> "$GITHUB_OUTPUT" | |
| echo "after=$SYNC_AFTER" >> "$GITHUB_OUTPUT" | |
| echo "result=$SYNC_RESULT" >> "$GITHUB_OUTPUT" | |
| # Belt-and-braces: if the same merge that changed our workflow files | |
| # ALSO rewrote .gitattributes, merge=ours might not have been in | |
| # effect. Re-restore our version of the two we own from pre-merge. | |
| - name: Protect fork-owned workflows | |
| if: steps.merge.outputs.result == 'merged' | |
| env: | |
| BEFORE: ${{ steps.merge.outputs.before }} | |
| run: | | |
| . "$GITHUB_WORKSPACE/scripts/lib/sync-core.sh" | |
| sync_protect_fork_workflows "$BEFORE" | |
| - name: Set up Python | |
| if: steps.merge.outputs.result == 'merged' | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install | |
| if: steps.merge.outputs.result == 'merged' | |
| run: pip install -e ".[test]" | |
| - name: Run model tests | |
| id: tests | |
| if: steps.merge.outputs.result == 'merged' | |
| run: python -m pytest tests/unittests/models/ -q | |
| # --- success path: merge clean + tests green -> advance both branches --- | |
| - name: Publish new baseline | |
| if: steps.merge.outputs.result == 'merged' && steps.tests.outcome == 'success' | |
| run: | | |
| . "$GITHUB_WORKSPACE/scripts/lib/sync-core.sh" | |
| sync_publish_baseline | |
| # Re-disable any inherited upstream workflows that may have re-activated. | |
| # Runs on BOTH the merged-and-green path AND the up-to-date path so an | |
| # inherited workflow that quietly re-activated between our syncs (e.g. | |
| # because someone pushed to main directly, touching workflow files) still | |
| # gets caught. See sync-core.sh's UNWANTED_INHERITED_WORKFLOWS list. | |
| - name: Re-disable inherited workflows | |
| if: >- | |
| steps.merge.outputs.result == 'up-to-date' || | |
| (steps.merge.outputs.result == 'merged' && steps.tests.outcome == 'success') | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| REPO: ${{ github.repository }} | |
| run: | | |
| . "$GITHUB_WORKSPACE/scripts/lib/sync-core.sh" | |
| sync_redisable_inherited_workflows | |
| # --- nothing to do --- | |
| - name: Up to date | |
| if: steps.merge.outputs.result == 'up-to-date' | |
| run: echo "Already in sync with upstream. Nothing to do." | |
| # --- failure path: any failure (conflict, red tests, push rejection) -> hold + report --- | |
| - name: Report sync failure | |
| if: >- | |
| failure() || | |
| steps.merge.outputs.result == 'conflict' || | |
| (steps.merge.outputs.result == 'merged' && steps.tests.outcome != 'success') | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # Pin every gh call to THIS repo. We added `upstream` as a git remote | |
| # earlier in the job; the gh CLI infers its default repo from git | |
| # remotes and may pick the wrong one (a recent run targeted | |
| # google/adk-python's labels API and got a 403). | |
| REPO: ${{ github.repository }} | |
| REASON: >- | |
| ${{ steps.merge.outputs.result == 'conflict' && 'merge conflict' || | |
| (steps.merge.outputs.result == 'merged' && steps.tests.outcome != 'success') && 'test failure' || | |
| 'publish failed (see run log)' }} | |
| RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| run: | | |
| STABLE_SHA=$(git rev-parse --short "origin/$STABLE_BRANCH") | |
| TITLE="Upstream sync failed ($REASON) — holding stable at $STABLE_SHA" | |
| BODY=$(cat <<EOF | |
| Automated upstream sync could not complete. | |
| - **Reason:** $REASON | |
| - **\`$STABLE_BRANCH\` is held at:** \`$STABLE_SHA\` (last known-good — consumers are safe) | |
| - **Run log:** $RUN_URL | |
| ### To recover | |
| \`\`\` | |
| ./scripts/update-fork.sh | |
| \`\`\` | |
| Resolve the conflict or fix the failing test, push \`$INTEGRATION_BRANCH\`, | |
| then fast-forward \`$STABLE_BRANCH\` to it. | |
| EOF | |
| ) | |
| # Ensure the label exists BEFORE any gh call that filters on it. | |
| if ! gh label list --repo "$REPO" --search auto-sync --json name --jq '.[].name' | grep -qx auto-sync; then | |
| gh label create auto-sync --repo "$REPO" --color B60205 --description "Automated upstream sync" | |
| fi | |
| EXISTING=$(gh issue list --repo "$REPO" --state open --label auto-sync --json number --jq '.[0].number') | |
| if [ -n "$EXISTING" ]; then | |
| gh issue comment "$EXISTING" --repo "$REPO" --body "$BODY" | |
| else | |
| gh issue create --repo "$REPO" --title "$TITLE" --body "$BODY" --label auto-sync | |
| fi | |
| exit 1 |