diff --git a/.github/workflows/sync-upstream.yaml b/.github/workflows/sync-upstream.yaml new file mode 100644 index 0000000000..207601e9fb --- /dev/null +++ b/.github/workflows/sync-upstream.yaml @@ -0,0 +1,242 @@ +name: Sync upstream + +on: + # TEMPORARY: testing on feature branch — remove before merging to cohere. + push: + branches: + - feat/sync-upstream-workflow + schedule: + # Every Monday at 09:00 UTC + - cron: "0 9 * * 1" + workflow_dispatch: + inputs: + upstream_ref: + description: "Upstream ref to sync from (default: main)" + required: false + default: "main" + cohere_branch: + description: "Cohere branch to merge into" + required: false + default: "cohere" + +# Deny all permissions at the workflow level; jobs opt in explicitly below. +permissions: {} + +# Only one sync workflow runs at a time — prevents racing pushes to main or +# duplicate sync PRs if a manual dispatch overlaps the cron schedule. +concurrency: + group: sync-upstream + cancel-in-progress: false + +env: + UPSTREAM_REPO: https://github.com/confidential-containers/cloud-api-adaptor.git + UPSTREAM_REF: ${{ inputs.upstream_ref || 'main' }} + COHERE_BRANCH: ${{ inputs.cohere_branch || 'cohere' }} + +jobs: + # ── Step 1: fast-forward origin/main to match upstream/main ────────────── + sync-main: + name: Fast-forward main from upstream + runs-on: ubuntu-latest + permissions: + contents: write # push the fast-forwarded main branch + outputs: + new_commits: ${{ steps.ff.outputs.new_commits }} + upstream_sha: ${{ steps.ff.outputs.upstream_sha }} + # GH_TOKEN must be available to every step — `gh auth setup-git` + # registers `gh` as a git credential helper, and that helper shells + # out to `gh` at push-time, which reads $GH_TOKEN from the environment. + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - name: Checkout main + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: main + fetch-depth: 0 + # zizmor: token is set up explicitly below via `gh auth setup-git` + # so it isn't persisted in .git/config after this step. + persist-credentials: false + + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + gh auth setup-git + + - name: Fast-forward main to upstream + id: ff + run: | + git remote add upstream "$UPSTREAM_REPO" || true + git fetch upstream "$UPSTREAM_REF" + + BEHIND=$(git rev-list --count "HEAD..upstream/$UPSTREAM_REF") + UPSTREAM_SHA=$(git rev-parse --short "upstream/$UPSTREAM_REF") + echo "upstream_sha=$UPSTREAM_SHA" >> "$GITHUB_OUTPUT" + + if [ "$BEHIND" -eq 0 ]; then + echo "main is already up to date with upstream." + echo "new_commits=0" >> "$GITHUB_OUTPUT" + exit 0 + fi + + AHEAD=$(git rev-list --count "upstream/$UPSTREAM_REF..HEAD") + if [ "$AHEAD" -ne 0 ]; then + echo "::error::origin/main is $AHEAD commits AHEAD of upstream — cannot fast-forward." + echo "::error::Remove the extra commits first (rebase or reset) before syncing." + exit 1 + fi + + git merge --ff-only "upstream/$UPSTREAM_REF" + git push origin main + echo "new_commits=$BEHIND" >> "$GITHUB_OUTPUT" + echo "Fast-forwarded main by $BEHIND commits to $UPSTREAM_SHA" + + # ── Step 2: merge updated main into the cohere branch via PR ───────────── + sync-cohere: + name: Merge main into cohere via PR + needs: sync-main + runs-on: ubuntu-latest + permissions: + contents: write # push the sync/* branch + pull-requests: write # create the sync PR via `gh pr create` + # GH_TOKEN must be available to every step — `gh auth setup-git` + # registers `gh` as a git credential helper, and that helper shells + # out to `gh` at push-time, which reads $GH_TOKEN from the environment. + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - name: Checkout cohere branch + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ env.COHERE_BRANCH }} + fetch-depth: 0 + # zizmor: token is set up explicitly below via `gh auth setup-git` + # so it isn't persisted in .git/config after this step. + persist-credentials: false + + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + gh auth setup-git + + - name: Check if cohere is behind main + id: check + run: | + git fetch origin main + BEHIND=$(git rev-list --count HEAD..origin/main) + echo "behind=$BEHIND" >> "$GITHUB_OUTPUT" + echo "Cohere branch is $BEHIND commits behind origin/main" + + DATE=$(date +%Y-%m-%d) + SHORT_SHA=$(git rev-parse --short origin/main) + echo "sync_branch=sync/upstream-${DATE}-${SHORT_SHA}" >> "$GITHUB_OUTPUT" + + - name: Skip if up to date + if: steps.check.outputs.behind == '0' + run: echo "Cohere branch is already up to date with main. Nothing to do." + + - name: Create sync branch and check for conflicts + if: steps.check.outputs.behind != '0' + id: merge + env: + SYNC_BRANCH: ${{ steps.check.outputs.sync_branch }} + run: | + # The sync branch is created at origin/main so it's always ahead of + # cohere by $BEHIND commits. The PR merges this into cohere via the + # GitHub UI, which handles clean merges and surfaces conflicts + # natively — the merge commit is never pushed from CI. + # (If we instead tried to pre-merge in CI and aborted on conflict, + # the sync branch would equal cohere HEAD and `gh pr create` would + # fail with "No commits between cohere and ".) + git branch "$SYNC_BRANCH" origin/main + + # Preflight merge — purely to report conflict status in the PR body. + # Never committed or pushed; aborted either way. + if git merge --no-commit --no-ff origin/main; then + echo "conflict=false" >> "$GITHUB_OUTPUT" + else + echo "conflict=true" >> "$GITHUB_OUTPUT" + echo "" + echo "=== CONFLICT: listing conflicted files ===" + git diff --name-only --diff-filter=U + fi + git merge --abort 2>/dev/null || true + + - name: Push sync branch + if: steps.check.outputs.behind != '0' + env: + SYNC_BRANCH: ${{ steps.check.outputs.sync_branch }} + run: git push origin "$SYNC_BRANCH" + + - name: Create pull request (clean merge) + if: steps.check.outputs.behind != '0' && steps.merge.outputs.conflict == 'false' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BEHIND: ${{ steps.check.outputs.behind }} + UPSTREAM_SHA: ${{ needs.sync-main.outputs.upstream_sha }} + SYNC_BRANCH: ${{ steps.check.outputs.sync_branch }} + run: | + gh pr create \ + --base "$COHERE_BRANCH" \ + --head "$SYNC_BRANCH" \ + --title "Sync upstream ($UPSTREAM_SHA) — $BEHIND new commits" \ + --body "$(cat <