# Copyright 2026 RustFS Team # SPDX-License-Identifier: Apache-2.0 name: Gateway Integration Sync # Merges `main` into the long-lived `gateway/integration` branch every weekday # so the s3s -> gateway migration (rustfs/backlog#2734, task T0.1 in #2736) # never accumulates merge debt. A clean merge lands as a `--no-ff` merge commit # pushed straight to the branch. A conflicting merge is aborted and fails the # run after its summary records the source and target SHAs, run URL, and exact # conflicting paths. It creates no sync branch or pull request. Resolve the # merge in an isolated worktree based on the latest target, preserve the # two-parent merge commit, and push only after the required checks pass and the # target still matches the merge's first parent. If the target moves, restart # from its new head. # # Write scope: only `gateway/integration` is ever pushed. The push uses a # one-hour installation token of the organisation's GitHub App (contents: # write) because that app is the only non-admin bypass actor of the # `gateway/integration` ruleset; GITHUB_TOKEN cannot be a ruleset bypass actor, # so it cannot push through the required reviews and checks. GITHUB_TOKEN is # read-only here. # # `schedule` fires only from the default branch, so the cron is live once this # file exists on `main`. `workflow_dispatch` runs from any ref that carries the # file and always acts on `gateway/integration`. on: schedule: - cron: "0 2 * * 1-5" # Weekdays 02:00 UTC workflow_dispatch: inputs: source_ref: description: >- Branch to merge into gateway/integration. The schedule always merges main; only the conflict acceptance test (rustfs/backlog#2736) sets another branch. required: false default: main type: string permissions: contents: read concurrency: group: ${{ github.workflow }} cancel-in-progress: false env: TARGET_BRANCH: gateway/integration jobs: sync: name: Merge main into gateway/integration if: github.repository == 'rustfs/rustfs' runs-on: ubuntu-latest timeout-minutes: 15 steps: - name: Create the branch push token id: push-token uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3 with: app-id: ${{ vars.CLA_BOT_APP_ID }} private-key: ${{ secrets.CLA_BOT_APP_PRIVATE_KEY }} owner: ${{ github.repository_owner }} repositories: rustfs permission-contents: write - name: Checkout gateway/integration uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7 with: # persist-credentials-exempt: this checkout's token IS the push # credential — the merge commit and the sync branch are pushed with it # below. It is a one-hour installation token scoped to contents. ref: gateway/integration fetch-depth: 0 token: ${{ steps.push-token.outputs.token }} - name: Configure the bot identity run: | git config user.name "rustfs-cla-assistant[bot]" git config user.email "273121464+rustfs-cla-assistant[bot]@users.noreply.github.com" - name: Merge the source branch id: merge env: SOURCE_REF: ${{ inputs.source_ref || 'main' }} RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} run: | set -euo pipefail if ! [[ "$SOURCE_REF" =~ ^[A-Za-z0-9][A-Za-z0-9._/-]*$ ]] \ || [[ "$SOURCE_REF" == *..* ]] \ || [[ "$SOURCE_REF" == "$TARGET_BRANCH" ]]; then echo "::error::refusing source_ref '${SOURCE_REF}'" exit 1 fi git fetch origin "refs/heads/${SOURCE_REF}:refs/remotes/origin/${SOURCE_REF}" source_sha=$(git rev-parse "origin/${SOURCE_REF}") target_sha=$(git rev-parse HEAD) { echo "source_ref=${SOURCE_REF}" echo "source_sha=${source_sha}" echo "target_sha=${target_sha}" } >> "$GITHUB_OUTPUT" if git merge --no-ff --no-edit "origin/${SOURCE_REF}"; then if [[ "$(git rev-parse HEAD)" == "$target_sha" ]]; then echo "result=up-to-date" >> "$GITHUB_OUTPUT" echo "\`${TARGET_BRANCH}\` already contains \`${SOURCE_REF}\` (${source_sha}); nothing to push." >> "$GITHUB_STEP_SUMMARY" else echo "result=merged" >> "$GITHUB_OUTPUT" fi exit 0 fi conflicts=$(git diff --name-only --diff-filter=U) if [[ -z "$conflicts" ]]; then git merge --abort || true echo "::error::git merge failed without reporting conflicting paths; this is not a conflict" exit 1 fi { echo "## Gateway integration sync stopped on conflicts" echo echo "- Run: ${RUN_URL}" echo "- Source: \`${SOURCE_REF}\` at ${source_sha}" echo "- Target: \`${TARGET_BRANCH}\` at ${target_sha}" echo echo "Conflicting files:" printf '%s\n' "$conflicts" | sed 's/^/- /' } >> "$GITHUB_STEP_SUMMARY" git merge --abort echo "::error::${SOURCE_REF} cannot be merged cleanly into ${TARGET_BRANCH}; no branch or pull request was created" exit 1 - name: Push the merge commit if: steps.merge.outputs.result == 'merged' env: SOURCE_REF: ${{ steps.merge.outputs.source_ref }} SOURCE_SHA: ${{ steps.merge.outputs.source_sha }} run: | set -euo pipefail git push origin HEAD:refs/heads/gateway/integration echo "Merged \`${SOURCE_REF}\` (${SOURCE_SHA}) into \`${TARGET_BRANCH}\` as $(git rev-parse HEAD)." >> "$GITHUB_STEP_SUMMARY" alert-on-failure: name: Alert on scheduled failure needs: [sync] # Same ci-8 mechanism audit.yml and coverage.yml use: a scheduled run that # could not push or encountered merge conflicts must not stay visible only # in the Actions tab. if: github.repository == 'rustfs/rustfs' && (always() && github.event_name == 'schedule' && contains(needs.*.result, 'failure')) runs-on: ubuntu-latest timeout-minutes: 10 permissions: contents: read issues: write steps: - uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7 with: persist-credentials: false - name: Open or update failure-tracking issue uses: ./.github/actions/schedule-failure-issue with: github-token: ${{ secrets.BACKLOG_ISSUE_TOKEN }}