Skip to content

auto-sync-upstream

auto-sync-upstream #2

Workflow file for this run

name: auto-sync-upstream
# Automatically syncs the fork with upstream google/adk-python.
#
# Flow:
# 1. Merge upstream into main (the integration branch).
# 2. If the merge conflicts -> stop, open an issue, leave `stable` untouched.
# 3. If clean -> install + run the model tests.
# 4. Tests pass -> fast-forward `stable` to the new commit (the new baseline).
# 5. Tests fail -> stop, open an issue, leave `stable` untouched.
#
# Consumers pin `@stable`, so they only ever receive green syncs and are
# automatically held at the last working version when a sync fails.
#
# 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 # push main and stable
issues: write # open the failure-report issue
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
token: ${{ secrets.GITHUB_TOKEN }}
- name: Configure git
run: |
git config user.name "adk-fork-bot"
git config user.email "adk-fork-bot@users.noreply.github.com"
- name: Merge upstream
id: merge
run: |
git remote add upstream "$UPSTREAM_REPO"
git fetch upstream main
BEFORE=$(git rev-parse HEAD)
echo "before=$BEFORE" >> "$GITHUB_OUTPUT"
if git merge --no-edit upstream/main; then
AFTER=$(git rev-parse HEAD)
echo "after=$AFTER" >> "$GITHUB_OUTPUT"
if [ "$BEFORE" = "$AFTER" ]; then
echo "result=up-to-date" >> "$GITHUB_OUTPUT"
else
echo "result=merged" >> "$GITHUB_OUTPUT"
fi
else
git merge --abort
echo "result=conflict" >> "$GITHUB_OUTPUT"
fi
- 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: |
git push origin HEAD:"$INTEGRATION_BRANCH"
git push origin HEAD:"$STABLE_BRANCH"
echo "Advanced $STABLE_BRANCH to $(git rev-parse --short HEAD)"
# --- 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."
# --- always: refresh the "last synced / next sync" block in the README ---
# Runs on every path. Edits a fresh checkout of origin/main so it never
# carries merge residue from a failed run. Skips the commit if unchanged.
- name: Update sync timestamps in README
if: always()
run: |
if [ "${{ steps.merge.outputs.result }}" = "conflict" ] || \
[ "${{ steps.tests.outcome }}" = "failure" ]; then
OUTCOME="⚠️ last attempt failed — see the open auto-sync issue"
else
OUTCOME="✅ last sync succeeded"
fi
NOW=$(date -u '+%Y-%m-%d %H:%M UTC')
NEXT=$(date -u -d 'tomorrow 06:00' '+%Y-%m-%d %H:%M UTC')
TMP=$(mktemp -d)
git clone --depth 1 --branch main \
"https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git" "$TMP"
cd "$TMP"
python3 - "$NOW" "$NEXT" "$OUTCOME" <<'PY'
import sys, re, pathlib
now, nxt, outcome = sys.argv[1], sys.argv[2], sys.argv[3]
block = (
"> <!-- SYNC-TIMES-START -->\n"
f"> _Last sync attempt: {now} ({outcome})._\n"
f"> _Next scheduled sync: {nxt}._\n"
"> <!-- SYNC-TIMES-END -->"
)
p = pathlib.Path("README.md")
text = p.read_text()
new = re.sub(
r"> <!-- SYNC-TIMES-START -->.*?> <!-- SYNC-TIMES-END -->",
block.replace("\\", "\\\\"), text, flags=re.S,
)
if new != text:
p.write_text(new)
print("README updated")
else:
print("README unchanged")
PY
if ! git diff --quiet README.md; then
git config user.name "adk-fork-bot"
git config user.email "adk-fork-bot@users.noreply.github.com"
git add README.md
git commit -m "chore: update sync timestamps [skip ci]"
git push origin main
fi
# --- failure path: conflict or red tests -> hold + report ---
- name: Report sync failure
if: >-
steps.merge.outputs.result == 'conflict' ||
(steps.merge.outputs.result == 'merged' && steps.tests.outcome != 'success')
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REASON: ${{ steps.merge.outputs.result == 'conflict' && 'merge conflict' || 'test failure' }}
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
)
EXISTING=$(gh issue list --state open --label auto-sync --json number --jq '.[0].number')
if [ -n "$EXISTING" ]; then
gh issue comment "$EXISTING" --body "$BODY"
else
gh label create auto-sync --color B60205 --description "Automated upstream sync" 2>/dev/null || true
gh issue create --title "$TITLE" --body "$BODY" --label auto-sync
fi
exit 1