Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 205 additions & 0 deletions .github/workflows/sync-vercel-detect-agent-upstream.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
# Sync Python port with upstream Vercel `packages/detect-agent` when it changes.
#
# Setup:
# - Repository secret: CURSOR_API_KEY (https://github.com/marketplace/actions/cursor-action)
# - No GitHub PAT: the final `gh` step uses the built-in GITHUB_TOKEN (see permissions below).
# - Uses actions/cache to remember the last synced upstream commit for that path.
# - First successful run bootstraps cache only (no agent); the next drift runs the agent + publish steps.
# - Cursor only edits files and runs checks; this workflow commits, pushes, and opens/updates the PR.
#
# Test on your PR (same repo only; fork PRs are ignored):
# 1. Create a repo label named `test-upstream-sync` (Settings -> Labels) if it does not exist.
# 2. Open your PR, add the `test-upstream-sync` label — this workflow runs once (labeled event).
# 3. PR runs use a separate Actions cache key so they do not overwrite the mainline upstream SHA.
# 4. Remove the label when done (optional).
#
# Manual run: Actions -> "Sync Vercel detect-agent upstream" -> Run workflow (pick your branch to test workflow_dispatch changes).

name: Sync Vercel detect-agent upstream

on:
schedule:
# Daily 14:05 UTC — adjust as you like
- cron: "5 14 * * *"
workflow_dispatch:
pull_request:
types: [labeled]

concurrency:
group: sync-vercel-detect-agent-${{ github.ref }}
cancel-in-progress: false

jobs:
sync:
if: >-
github.event_name != 'pull_request' ||
(github.event_name == 'pull_request' &&
github.event.label.name == 'test-upstream-sync' &&
github.event.pull_request.head.repo.full_name == github.repository)
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
env:
# runner.* is not allowed in job-level env; use stable paths (fresh VM per hosted job).
UPSTREAM_STATE_DIR: /tmp/detect-agent-upstream-state
UPSTREAM_VERCEL_MIRROR: /tmp/vercel-detect-agent-bare.git
UPSTREAM_DIFF_FILE: /tmp/vercel-packages-detect-agent.diff
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: true
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}

- uses: actions/cache@v4
id: vercel_mirror_cache
with:
path: /tmp/vercel-detect-agent-bare.git
key: vercel-github-bare-mirror-v1-${{ runner.os }}

- uses: actions/cache@v4
id: upstream_state_cache
with:
path: /tmp/detect-agent-upstream-state
key: vercel-detect-agent-upstream-state-v1-${{ runner.os }}-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || 'default' }}

- name: Compute upstream drift
id: drift
shell: bash
run: ./scripts/vercel_upstream_drift.sh

- name: Prepare default branch for sync
if: steps.drift.outputs.should_sync == 'true'
env:
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
run: |
set -euo pipefail
db="${DEFAULT_BRANCH:-main}"
git fetch origin
git checkout "$db"
git pull --ff-only "origin/$db"

- name: Configure git committer (actions bot)
if: steps.drift.outputs.should_sync == 'true'
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

- uses: astral-sh/setup-uv@v5
if: steps.drift.outputs.should_sync == 'true'
with:
enable-cache: true
python-version: "3.9"

- name: Install Python deps (uv)
if: steps.drift.outputs.should_sync == 'true'
run: uv sync --extra dev

- name: Run Cursor agent (port + verify)
id: cursor
if: steps.drift.outputs.should_sync == 'true'
uses: PunGrumpy/cursor-action@v1.0.0
with:
api-key: ${{ secrets.CURSOR_API_KEY }}
model: composer-2
working-directory: .
permissions: read-write
timeout: "3600"
prompt: |
You are updating this repository's Python port of Vercel's `packages/detect-agent` to match upstream changes.

The workflow has already checked out the default branch and pulled latest; stay on the current branch. Do not run any `git` commands and do not use `gh`.

Upstream facts:
- merge_base: ${{ steps.drift.outputs.merge_base }}
- last_recorded_upstream_tip: ${{ steps.drift.outputs.last_recorded }}
- new_upstream_tip: ${{ steps.drift.outputs.new_sha }}
- compare URL: ${{ steps.drift.outputs.compare_url }}

Read the unified diff from this absolute path. Do not edit that path; it is CI scratch only:
${{ steps.drift.outputs.diff_path }}

Your job:
1. Port upstream logic into `detect_agent/__init__.py` and `tests/` as needed. Mirror detection order and semantics from the TypeScript sources; keep typing style consistent with the existing Python file.
2. Update `README.md` / `CHANGELOG.md` here if upstream docs/changelog warrant it.
3. Run: `uv sync --extra dev && uv run pytest && uv run ruff check . && uv run ruff format --check .`

If the diff file is empty but the SHAs differ, use the compare URL and current upstream `main` tree for that package to align the port.

- name: Commit, push, and open or update PR
id: publish
if: steps.drift.outputs.should_sync == 'true' && steps.cursor.outcome == 'success'
env:
GH_TOKEN: ${{ github.token }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
MERGE_BASE: ${{ steps.drift.outputs.merge_base }}
NEW_SHA: ${{ steps.drift.outputs.new_sha }}
COMPARE_URL: ${{ steps.drift.outputs.compare_url }}
BRANCH_SLUG: ${{ steps.drift.outputs.branch_slug }}
shell: bash
run: |
set -euo pipefail
db="${DEFAULT_BRANCH:-main}"
BRANCH_NAME="sync/vercel-detect-agent-${BRANCH_SLUG}"

git branch -D "$BRANCH_NAME" 2>/dev/null || true
git checkout -b "$BRANCH_NAME"

git add detect_agent tests README.md CHANGELOG.md pyproject.toml uv.lock 2>/dev/null || true

if git diff --staged --quiet; then
echo "::notice::No staged changes after agent; skipping commit and PR."
exit 0
fi

short_mb="${MERGE_BASE:0:7}"
short_new="${NEW_SHA:0:7}"
git commit -m "sync: port vercel detect-agent ${short_mb}..${short_new}"

git push -u origin "$BRANCH_NAME" --force

EXISTING_PR="$(gh pr list --state open --head "$BRANCH_NAME" --json number --jq '.[0].number // empty' 2>/dev/null || true)"

PR_TITLE="sync: port vercel detect-agent (${short_mb}..${short_new})"
PR_BODY="$(printf '%s\n\n%s\n\n%s\n' "Ports [\`packages/detect-agent\`](https://github.com/vercel/vercel/tree/main/packages/detect-agent) from [vercel/vercel](https://github.com/vercel/vercel)." "Compare: ${COMPARE_URL}" "cc @blainekasten")"

if [ -n "${EXISTING_PR}" ]; then
gh pr edit "${EXISTING_PR}" --title "${PR_TITLE}" --body "${PR_BODY}"
gh pr edit "${EXISTING_PR}" --add-assignee blainekasten 2>/dev/null || true
echo "Updated existing PR #${EXISTING_PR}"
else
gh pr create \
--title "${PR_TITLE}" \
--body "${PR_BODY}" \
--base "${db}" \
--head "${BRANCH_NAME}" \
--assignee blainekasten
fi

- name: Record synced upstream SHA
if: success() && steps.drift.outputs.should_sync == 'true' && steps.cursor.outcome == 'success' && steps.publish.outcome == 'success'
shell: bash
env:
NEW_SHA: ${{ steps.drift.outputs.new_sha }}
run: |
set -euo pipefail
mkdir -p "${UPSTREAM_STATE_DIR}"
printf '%s\n' "${NEW_SHA}" >"${UPSTREAM_STATE_DIR}/last_packages_detect_agent_commit.txt"

- name: Job summary
if: always()
shell: bash
run: |
set -euo pipefail
{
echo "## Upstream sync"
echo "- should_sync: ${{ steps.drift.outputs.should_sync }}"
echo "- new_sha: ${{ steps.drift.outputs.new_sha }}"
echo "- compare: ${{ steps.drift.outputs.compare_url }}"
} >>"${GITHUB_STEP_SUMMARY}"
if [[ "${{ steps.drift.outputs.should_sync }}" == "true" ]]; then
echo "- cursor exit: ${{ steps.cursor.outputs.exit-code }}" >>"${GITHUB_STEP_SUMMARY}"
echo "- publish outcome: ${{ steps.publish.outcome }}" >>"${GITHUB_STEP_SUMMARY}"
fi
107 changes: 107 additions & 0 deletions scripts/vercel_upstream_drift.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#!/usr/bin/env bash
# Compute whether vercel/vercel changed packages/detect-agent since the last run.
# Intended for GitHub Actions; optionally writes key=value lines to GITHUB_OUTPUT.
#
# Required env:
# UPSTREAM_STATE_DIR - directory holding last_packages_detect_agent_commit.txt
# UPSTREAM_VERCEL_MIRROR - path to (or parent of) bare clone of github.com/vercel/vercel.git
# UPSTREAM_DIFF_FILE - where to write unified diff when drift is detected
#
# Optional:
# GITHUB_OUTPUT - when set, writes should_sync, new_sha, merge_base, last_recorded,
# diff_path, compare_url (booleans as true/false strings)

set -euo pipefail

UPSTREAM_STATE_DIR="${UPSTREAM_STATE_DIR:?UPSTREAM_STATE_DIR is required}"
UPSTREAM_VERCEL_MIRROR="${UPSTREAM_VERCEL_MIRROR:?UPSTREAM_VERCEL_MIRROR is required}"
UPSTREAM_DIFF_FILE="${UPSTREAM_DIFF_FILE:?UPSTREAM_DIFF_FILE is required}"

path='packages/detect-agent'
LAST_SHA_FILE="${UPSTREAM_STATE_DIR}/last_packages_detect_agent_commit.txt"

append_kv() {
if [[ -z "${GITHUB_OUTPUT:-}" ]]; then
return 0
fi
printf '%s=%s\n' "$1" "$2" >>"$GITHUB_OUTPUT"
}

mkdir -p "$UPSTREAM_STATE_DIR"

if [[ ! -d "$UPSTREAM_VERCEL_MIRROR" ]]; then
git clone --mirror https://github.com/vercel/vercel.git "$UPSTREAM_VERCEL_MIRROR"
fi

git -C "$UPSTREAM_VERCEL_MIRROR" fetch --prune origin

# Mirror clones expose the default branch as refs/heads/main, not origin/main.
main_ref="main"
if ! git -C "$UPSTREAM_VERCEL_MIRROR" rev-parse -q --verify "${main_ref}^{commit}" >/dev/null 2>&1; then
main_ref="origin/main"
fi
if ! git -C "$UPSTREAM_VERCEL_MIRROR" rev-parse -q --verify "${main_ref}^{commit}" >/dev/null 2>&1; then
echo "failed to resolve main branch tip (tried main, origin/main) in ${UPSTREAM_VERCEL_MIRROR}" >&2
exit 1
fi

NEW_SHA="$(git -C "$UPSTREAM_VERCEL_MIRROR" log -1 --format=%H "$main_ref" -- "$path")"
if [[ -z "$NEW_SHA" ]]; then
echo "failed to resolve latest commit for $path on ${main_ref}" >&2
exit 1
fi

if [[ ! -f "$LAST_SHA_FILE" ]]; then
printf '%s\n' "$NEW_SHA" >"$LAST_SHA_FILE"
echo "bootstrapped upstream state (no sync): $NEW_SHA" >&2
append_kv should_sync false
append_kv new_sha "$NEW_SHA"
exit 0
fi

LAST_SHA="$(tr -d '[:space:]' <"$LAST_SHA_FILE" || true)"
if [[ -z "$LAST_SHA" ]]; then
printf '%s\n' "$NEW_SHA" >"$LAST_SHA_FILE"
echo "repaired empty state file -> $NEW_SHA" >&2
append_kv should_sync false
append_kv new_sha "$NEW_SHA"
exit 0
fi

if [[ "$LAST_SHA" == "$NEW_SHA" ]]; then
echo "no upstream drift: $NEW_SHA" >&2
append_kv should_sync false
append_kv new_sha "$NEW_SHA"
exit 0
fi

echo "upstream drift: $LAST_SHA -> $NEW_SHA" >&2

if ! git -C "$UPSTREAM_VERCEL_MIRROR" cat-file -e "${LAST_SHA}^{commit}" 2>/dev/null; then
echo "widening fetch for stored sha" >&2
git -C "$UPSTREAM_VERCEL_MIRROR" fetch --prune origin "${LAST_SHA}" || true
fi

if ! git -C "$UPSTREAM_VERCEL_MIRROR" cat-file -e "${LAST_SHA}^{commit}" 2>/dev/null; then
echo "stored sha $LAST_SHA not found; delete ${LAST_SHA_FILE} to re-bootstrap" >&2
exit 1
fi

MB="$(git -C "$UPSTREAM_VERCEL_MIRROR" merge-base "$LAST_SHA" "$NEW_SHA" 2>/dev/null || true)"
if [[ -z "$MB" ]]; then
MB="$LAST_SHA"
fi

git -C "$UPSTREAM_VERCEL_MIRROR" diff "${MB}..${NEW_SHA}" -- "$path" >"$UPSTREAM_DIFF_FILE" || true

COMPARE_URL="https://github.com/vercel/vercel/compare/${MB}...${NEW_SHA}"

append_kv should_sync true
append_kv new_sha "$NEW_SHA"
append_kv branch_slug "${NEW_SHA:0:7}"
append_kv merge_base "$MB"
append_kv last_recorded "$LAST_SHA"
append_kv diff_path "$UPSTREAM_DIFF_FILE"
append_kv compare_url "$COMPARE_URL"

exit 0
Loading