diff --git a/.github/workflows/auto-retarget-main-pr-to-dev.yml b/.github/workflows/auto-retarget-main-pr-to-dev.yml deleted file mode 100644 index 3732a72359..0000000000 --- a/.github/workflows/auto-retarget-main-pr-to-dev.yml +++ /dev/null @@ -1,73 +0,0 @@ -name: auto-retarget-main-pr-to-dev - -on: - pull_request_target: - types: - - opened - - reopened - - edited - branches: - - main - -permissions: - contents: read - issues: write - pull-requests: write - -jobs: - retarget: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-latest - steps: - - name: Retarget PR base to dev - uses: actions/github-script@v7 - with: - script: | - const pr = context.payload.pull_request; - const prNumber = pr.number; - const { owner, repo } = context.repo; - - const baseRef = pr.base?.ref; - const headRef = pr.head?.ref; - const desiredBase = "dev"; - - if (baseRef !== "main") { - core.info(`PR #${prNumber} base is ${baseRef}; nothing to do.`); - return; - } - - if (headRef === desiredBase) { - core.info(`PR #${prNumber} is ${desiredBase} -> main; skipping retarget.`); - return; - } - - core.info(`Retargeting PR #${prNumber} base from ${baseRef} to ${desiredBase}.`); - - try { - await github.rest.pulls.update({ - owner, - repo, - pull_number: prNumber, - base: desiredBase, - }); - } catch (error) { - core.setFailed(`Failed to retarget PR #${prNumber} to ${desiredBase}: ${error.message}`); - return; - } - - const body = [ - `This pull request targeted \`${baseRef}\`.`, - "", - `The base branch has been automatically changed to \`${desiredBase}\`.`, - ].join("\n"); - - try { - await github.rest.issues.createComment({ - owner, - repo, - issue_number: prNumber, - body, - }); - } catch (error) { - core.warning(`Failed to comment on PR #${prNumber}: ${error.message}`); - } diff --git a/.github/workflows/auto-sync-upstream-copilot.yml b/.github/workflows/auto-sync-upstream-copilot.yml new file mode 100644 index 0000000000..5af4fda4d7 --- /dev/null +++ b/.github/workflows/auto-sync-upstream-copilot.yml @@ -0,0 +1,171 @@ +name: auto-sync-upstream-copilot + +on: + schedule: + # daily at 02:30 UTC (10:30 Asia/Singapore) — offset 30m from auto-sync-upstream + - cron: '30 2 * * *' + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + issues: write + actions: write + +concurrency: + group: auto-sync-upstream-copilot + cancel-in-progress: false + +env: + UPSTREAM_URL: https://github.com/router-for-me/CLIProxyAPI.git + UPSTREAM_BRANCH: main + TARGET_BRANCH: feat/copilot + PR_BRANCH: auto-sync/upstream-main-copilot + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - name: Checkout target branch + uses: actions/checkout@v4 + with: + ref: ${{ env.TARGET_BRANCH }} + fetch-depth: 0 + + - name: Configure git identity + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Add upstream remote and fetch + run: | + git remote add upstream "$UPSTREAM_URL" + git fetch upstream "$UPSTREAM_BRANCH" + + - name: Check if behind upstream + id: check + run: | + BEHIND=$(git rev-list --count "HEAD..upstream/${UPSTREAM_BRANCH}") + echo "behind=$BEHIND" >> "$GITHUB_OUTPUT" + if [ "$BEHIND" = "0" ]; then + echo "Already up to date with upstream/${UPSTREAM_BRANCH}." + else + echo "$BEHIND new commits from upstream:" + git log --oneline "HEAD..upstream/${UPSTREAM_BRANCH}" + fi + + - name: Build merge commit message + if: steps.check.outputs.behind != '0' + run: | + { + echo "Merge upstream/${UPSTREAM_BRANCH} (auto-sync feat/copilot)" + echo + git log --reverse --pretty=format:'- %h %s' "HEAD..upstream/${UPSTREAM_BRANCH}" + echo + } > /tmp/merge-msg.txt + cat /tmp/merge-msg.txt + + - name: Try clean merge + if: steps.check.outputs.behind != '0' + id: merge + run: | + set +e + git merge --no-ff -F /tmp/merge-msg.txt "upstream/${UPSTREAM_BRANCH}" + rc=$? + if [ "$rc" = "0" ]; then + echo "clean=true" >> "$GITHUB_OUTPUT" + else + echo "clean=false" >> "$GITHUB_OUTPUT" + git merge --abort || true + fi + exit 0 + + - name: Set up Go + if: steps.merge.outputs.clean == 'true' + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: Verify build + if: steps.merge.outputs.clean == 'true' + id: build + run: | + set +e + go build -o /tmp/cli-proxy-api ./cmd/server + rc=$? + rm -f /tmp/cli-proxy-api + if [ "$rc" = "0" ]; then + echo "ok=true" >> "$GITHUB_OUTPUT" + else + echo "ok=false" >> "$GITHUB_OUTPUT" + fi + exit 0 + + - name: Push merged changes to target branch + if: steps.merge.outputs.clean == 'true' && steps.build.outputs.ok == 'true' + run: git push origin "${TARGET_BRANCH}" + + - name: Trigger downstream image build + if: steps.merge.outputs.clean == 'true' && steps.build.outputs.ok == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh workflow run ghcr-feat-copilot.yml --repo "${{ github.repository }}" --ref "${TARGET_BRANCH}" + + - name: Prepare PR branch (build failure case) + if: steps.merge.outputs.clean == 'true' && steps.build.outputs.ok != 'true' + run: | + # HEAD is the (clean) merge commit — push it as the PR branch so reviewers + # see exactly what would land, and pr-test-build.yml runs against it. + git push --force origin "HEAD:refs/heads/${PR_BRANCH}" + + - name: Prepare PR branch (conflict case) + if: steps.check.outputs.behind != '0' && steps.merge.outputs.clean != 'true' + run: | + # Reset working tree, then push raw upstream tip as the PR branch so GitHub + # surfaces the conflict against feat/copilot and offers web-based resolution. + git reset --hard "origin/${TARGET_BRANCH}" + git checkout -B "${PR_BRANCH}" "upstream/${UPSTREAM_BRANCH}" + git push --force origin "${PR_BRANCH}" + + - name: Open or update sync PR + if: steps.check.outputs.behind != '0' && (steps.merge.outputs.clean != 'true' || steps.build.outputs.ok != 'true') + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + run: | + set -e + + if [ "${{ steps.merge.outputs.clean }}" != "true" ]; then + REASON='conflicts with `'"${TARGET_BRANCH}"'`' + else + REASON='merge clean but `go build` failed' + fi + + { + echo "Auto-sync from \`upstream/${UPSTREAM_BRANCH}\` (router-for-me/CLIProxyAPI) into \`${TARGET_BRANCH}\`." + echo + echo "**Reason for manual review:** ${REASON}" + echo + echo "### New commits" + git log --reverse --pretty=format:'- %h %s' "origin/${TARGET_BRANCH}..upstream/${UPSTREAM_BRANCH}" + } > /tmp/pr-body.md + + # Idempotently ensure the label exists. + gh label create auto-sync-copilot --color FBCA04 --description "Automated upstream sync (feat/copilot)" || true + + EXISTING=$(gh pr list --head "${PR_BRANCH}" --base "${TARGET_BRANCH}" --state open --json number --jq '.[0].number' || true) + + TITLE="auto-sync(copilot): merge upstream/${UPSTREAM_BRANCH} (${REASON})" + + if [ -n "$EXISTING" ]; then + gh pr edit "$EXISTING" --title "$TITLE" --body-file /tmp/pr-body.md + echo "Updated existing PR #$EXISTING" + else + gh pr create \ + --base "${TARGET_BRANCH}" \ + --head "${PR_BRANCH}" \ + --title "$TITLE" \ + --body-file /tmp/pr-body.md \ + --label auto-sync-copilot + fi