From a086c88266613f09b86597fc2b704379e075f2b2 Mon Sep 17 00:00:00 2001 From: coseto6125 <80243681+coseto6125@users.noreply.github.com> Date: Sun, 24 May 2026 00:30:10 +0800 Subject: [PATCH 1/2] refactor(ci): replace Mergify with GitHub-native auto-merge + auto-update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Delete .mergify.yml + Mergify GitHub App dependency. Replace with two small GitHub Actions workflows that together cover Mergify's value for this repo's actual scale (single-author, low PR volume): 1. .github/workflows/auto-merge.yml On non-draft PR events, enable GitHub native auto-merge via `gh pr merge --auto --squash --delete-branch`. GitHub then waits for required checks + up-to-date branch, merges automatically. 2. .github/workflows/auto-update-pr-branches.yml On push to main (and 15-min cron safety net), iterate eligible open PRs and call /update-branch API — programmatic equivalent of the 'Update branch' button. Conflicts → one-time marker comment + job failure so GitHub emails the author. What we lose vs Mergify: - Speculative trial (PR diff + main tip CI before merge) — substituted by 'Require branches to be up to date' + auto-update. - Batched trial (multiple PRs in one CI run) — never exercised on this repo; batch_size config was aspirational. - Per-area parallel queue UI categorization — ecp:area-* labels (set by ecp-pr-analyze.yml) survive and remain readable for humans / LLM agents. What we gain: - No more 'Mergify Merge Queue: neutral / Merge queue is ready' noise blocking dogfood PRs (see today's stuck #386 #387 #388). - One less third-party service in the merge path. - Workflows readable end-to-end in the repo, no Mergify dashboard hop. Also bundle 2 small hardcode fixes in release.yml: - owner: coseto6125 → owner: ${{ github.repository_owner }} - coseto6125/homebrew-tap → env.HOMEBREW_TAP_REPO (single source of truth) --- .github/workflows/auto-merge.yml | 43 +++++++ .github/workflows/auto-update-pr-branches.yml | 85 +++++++++++++ .github/workflows/release.yml | 9 +- .mergify.yml | 119 ------------------ 4 files changed, 135 insertions(+), 121 deletions(-) create mode 100644 .github/workflows/auto-merge.yml create mode 100644 .github/workflows/auto-update-pr-branches.yml delete mode 100644 .mergify.yml diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml new file mode 100644 index 00000000..f40ebded --- /dev/null +++ b/.github/workflows/auto-merge.yml @@ -0,0 +1,43 @@ +name: Auto-merge ready PRs +# Enables GitHub native auto-merge on any non-draft PR. GitHub then waits +# for: +# - All required status checks (defined in branch protection) to pass +# - The PR branch to be up-to-date with main (handled by +# auto-update-pr-branches.yml) +# …and merges automatically when both are satisfied. +# +# Per-area serialization (Mergify's queue_rules equivalent) is not enforced +# at workflow level — GitHub's auto-merge engine processes PRs first-ready- +# first-merged. For single-author / low-volume repos this matches Mergify's +# practical behavior because batch_size never exercised. If parallel same- +# area PRs become common, add a `concurrency:` group keyed off the area +# label here. +on: + pull_request: + types: [opened, ready_for_review, synchronize, reopened] + +permissions: + contents: write + pull-requests: write + +jobs: + enable: + if: github.event.pull_request.draft == false + runs-on: ubuntu-latest + steps: + - name: Enable auto-merge + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + PR: ${{ github.event.pull_request.number }} + run: | + set -euo pipefail + if gh pr merge "$PR" --repo "$REPO" --squash --auto --delete-branch 2>&1; then + echo "✓ auto-merge enabled for #$PR" + else + # Common no-op causes: PR already merged, conflict, branch + # protection requires review we don't have. Surface as warning + # not failure — the workflow can be re-fired by the next + # synchronize event when the underlying issue resolves. + echo "::warning::could not enable auto-merge on #$PR (already merged? conflict? unmet protection?)" + fi diff --git a/.github/workflows/auto-update-pr-branches.yml b/.github/workflows/auto-update-pr-branches.yml new file mode 100644 index 00000000..1de95245 --- /dev/null +++ b/.github/workflows/auto-update-pr-branches.yml @@ -0,0 +1,85 @@ +name: Auto-update PR branches +# Whenever main moves, refresh every eligible open PR's branch via +# GitHub's update-branch API (the programmatic "Update branch" button). +# Refreshed branches re-trigger CI; once CI is green and auto-merge is +# enabled (see auto-merge.yml), GitHub merges without manual clicks. +# +# Eligibility: non-draft, targeting main, has auto-merge already armed +# (`.autoMergeRequest != null` from `gh pr list`). Drafts and explicitly +# un-armed PRs are skipped on purpose — author hasn't opted in. +# +# Conflict handling: if update-branch returns a merge-conflict response, +# the PR gets a one-time marker comment (``) +# and the workflow exits non-zero so GitHub emails the author its usual +# CI-failure notification. +on: + push: + branches: [main] + schedule: + - cron: '*/15 * * * *' # safety net for missed push events / token blips + +permissions: + contents: write + pull-requests: write + +concurrency: + group: auto-update-branches + cancel-in-progress: false + +jobs: + update: + runs-on: ubuntu-latest + steps: + - name: Update eligible PR branches + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + TRIGGER_SHA: ${{ github.sha }} + run: | + set -euo pipefail + prs=$(gh pr list --repo "$REPO" --state open --base main \ + --json number,isDraft,autoMergeRequest,headRefName \ + --jq '.[] | select(.isDraft == false and .autoMergeRequest != null) | "\(.number)\t\(.headRefName)"') + + if [ -z "$prs" ]; then + echo "no eligible PRs to update" + exit 0 + fi + + conflicts=() + while IFS=$'\t' read -r pr branch; do + response=$(gh api -X PUT "repos/$REPO/pulls/$pr/update-branch" 2>&1 || true) + case "$response" in + *successfully*) + echo "✓ updated #$pr ($branch)" + ;; + *"already up"*|*"up to date"*) + echo "= #$pr ($branch) already current" + ;; + *conflict*|*"Merge conflict"*) + echo "✗ #$pr ($branch) conflict — author needed" + conflicts+=("$pr") + # Post a one-time marker comment so author gets a GitHub + # notification. Marker survives across re-runs so we don't + # spam on every push to main. + marker="" + existing=$(gh pr view "$pr" --repo "$REPO" --json comments \ + --jq ".comments[] | select(.body | startswith(\"$marker\")) | .id" | head -1) + if [ -z "$existing" ]; then + # printf keeps the multi-line body inside one bash var so + # YAML's block-scalar indentation rules stay happy. + body=$(printf '%s\n\n🚨 **Auto-update-branch failed** — merge conflict with `main` (trigger commit `%s`). Please rebase / merge manually. This comment posts only once per conflict; delete it to let the bot re-notify on the next event.' "$marker" "$TRIGGER_SHA") + gh pr comment "$pr" --repo "$REPO" --body "$body" || true + fi + ;; + *) + echo "✗ #$pr ($branch) unexpected response: $response" + conflicts+=("$pr") + ;; + esac + done <<< "$prs" + + if [ "${#conflicts[@]}" -gt 0 ]; then + echo "::error::PRs needing manual intervention: ${conflicts[*]}" + exit 1 + fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b5a8d625..355a8130 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,6 +23,11 @@ env: CARGO_TERM_COLOR: always CARGO_NET_RETRY: 10 RUSTUP_MAX_RETRIES: 10 + # Homebrew tap lives in a separate repo (not auto-derivable from + # github.repository). Centralized here so the publish/notes/clone steps + # below all reference one source of truth — change the tap location + # without grepping the file. + HOMEBREW_TAP_REPO: coseto6125/homebrew-tap BIN_NAME: ecp ECP_BUILD_VERBOSE: 1 @@ -376,7 +381,7 @@ jobs: with: app-id: ${{ vars.HOMEBREW_TAP_APP_ID }} private-key: ${{ secrets.HOMEBREW_TAP_APP_PRIVATE_KEY }} - owner: coseto6125 + owner: ${{ github.repository_owner }} repositories: homebrew-tap - name: Generate Homebrew formula @@ -410,7 +415,7 @@ jobs: GH_APP_TOKEN: ${{ steps.app-token.outputs.token }} shell: bash run: | - git clone "https://x-access-token:${GH_APP_TOKEN}@github.com/coseto6125/homebrew-tap.git" tap + git clone "https://x-access-token:${GH_APP_TOKEN}@github.com/${HOMEBREW_TAP_REPO}.git" tap mkdir -p tap/Formula cp generated/Formula/egent-code-plexus.rb tap/Formula/egent-code-plexus.rb diff --git a/.mergify.yml b/.mergify.yml deleted file mode 100644 index 767939bc..00000000 --- a/.mergify.yml +++ /dev/null @@ -1,119 +0,0 @@ -# Mergify config. -# -# Routing pattern (modern Mergify, post-2024): -# - Each queue_rule carries its own `queue_conditions` — labels, base, and -# required-check list a PR must satisfy to auto-enter THIS queue. -# - `merge_conditions` is what gates the actual merge once embarked. -# - No `pull_request_rules` needed for routing: Mergify checks each PR -# against every queue's `queue_conditions` and embarks it into the first -# match. Order queue_rules from most-specific to least-specific. -# -# Why not pull_request_rules.queue? -# - That action exists for EXPLICIT enqueuing (commands, advanced filters), -# not auto-routing. With only pull_request_rules, the Mergify Merge Queue -# check stays at "Merge queue is ready" without ever embarking — confirmed -# on PR #386 (manual `@Mergifyio queue` worked, auto did not). -# -# The required-check list is duplicated across queue_conditions — YAML -# anchors splice nested lists which Mergify rejects, so DRY isn't available. -# Keep the list synced with `gh api repos/.../branches/main/protection`. -# -# Where does `ecp/cross-pr-conflict` belong? -# queue_conditions, NOT merge_conditions. Mergify's speculative trial -# creates a fresh commit (PR diff + main tip) — ecp-pr-analyze.yml -# triggers on `pull_request` events only, so the trial commit has no -# cross-pr-conflict status reported against it. Anything required in -# merge_conditions would thus block the trial indefinitely → dequeue -# (PR #386 was the live repro). Keeping the check in queue_conditions -# evaluates it on the PR HEAD where the status actually exists. - -queue_rules: - - name: main-test-only - queue_conditions: - - base=main - - label=merge-queue - - label=ecp:area-test - - check-success=Test (all platforms) - - check-success=Code Quality (Linting & Formatting) - - check-success=Supply-chain (audit & deny) - - check-success=Lint GitHub Actions workflows - - check-success=CodeQL - - check-success=dependency-review - - status-success=ecp/cross-pr-conflict - merge_conditions: - - base=main - batch_size: 10 - batch_max_wait_time: 30s - - - name: main-parser-changes - queue_conditions: - - base=main - - label=merge-queue - - label=ecp:area-parser - - check-success=Test (all platforms) - - check-success=Code Quality (Linting & Formatting) - - check-success=Supply-chain (audit & deny) - - check-success=Lint GitHub Actions workflows - - check-success=CodeQL - - check-success=dependency-review - - status-success=ecp/cross-pr-conflict - merge_conditions: - - base=main - batch_size: 1 - - - name: main-cli-changes - queue_conditions: - - base=main - - label=merge-queue - - label=ecp:area-cli - - check-success=Test (all platforms) - - check-success=Code Quality (Linting & Formatting) - - check-success=Supply-chain (audit & deny) - - check-success=Lint GitHub Actions workflows - - check-success=CodeQL - - check-success=dependency-review - - status-success=ecp/cross-pr-conflict - merge_conditions: - - base=main - batch_size: 3 - batch_max_wait_time: 2m - - - name: main-docs-only - queue_conditions: - - base=main - - label=merge-queue - - label=ecp:area-docs - - check-success=Test (all platforms) - - check-success=Code Quality (Linting & Formatting) - - check-success=Supply-chain (audit & deny) - - check-success=Lint GitHub Actions workflows - - check-success=CodeQL - - check-success=dependency-review - - status-success=ecp/cross-pr-conflict - merge_conditions: - - base=main - batch_size: 20 - batch_max_wait_time: 10s - - # Fallback queue — catches PRs with `merge-queue` label but no area label. - # Must come last; queue_conditions match via negation on the area-label set. - - name: main-default - queue_conditions: - - base=main - - label=merge-queue - - -label~=^ecp:area- - - check-success=Test (all platforms) - - check-success=Code Quality (Linting & Formatting) - - check-success=Supply-chain (audit & deny) - - check-success=Lint GitHub Actions workflows - - check-success=CodeQL - - check-success=dependency-review - - status-success=ecp/cross-pr-conflict - merge_conditions: - - base=main - batch_size: 2 - -# Priority overrides: deferred to FU-2026-05-23-037. -# Modern Mergify exposes priority via `queue_rules.priority_rules` (per-queue -# nested rule list). Adding it cleanly requires duplicating priority_rules -# across all 5 queue_rules. Implement when same-area queue actually backs up. From 44ab5af15a88621afd674e28e9e43693c276a6e9 Mon Sep 17 00:00:00 2001 From: coseto6125 <80243681+coseto6125@users.noreply.github.com> Date: Sun, 24 May 2026 00:50:01 +0800 Subject: [PATCH 2/2] fix(ci): dedupe overlapping case patterns + silence false SC2016 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit actionlint surfaced 3 shellcheck warnings on auto-update-pr-branches.yml: - SC2221: `*"already up"*` always overrides `*"up to date"*` (the former matches "already up to date" too) → drop the redundant pattern. - SC2222: `*conflict*` always wins over `*"Merge conflict"*` (subset) → drop the redundant pattern. - SC2016: backticks inside the printf single-quoted format string look like un-expanded command substitution, but they are intentional Markdown formatting (`main`, `%s`). False positive → disable with shellcheck directive + comment explaining why. --- .github/workflows/auto-update-pr-branches.yml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/auto-update-pr-branches.yml b/.github/workflows/auto-update-pr-branches.yml index 1de95245..1c394e43 100644 --- a/.github/workflows/auto-update-pr-branches.yml +++ b/.github/workflows/auto-update-pr-branches.yml @@ -53,10 +53,15 @@ jobs: *successfully*) echo "✓ updated #$pr ($branch)" ;; - *"already up"*|*"up to date"*) + *"up to date"*) + # Includes "already up to date" and "up-to-date" variants; + # earlier explicit *"already up"* pattern was redundant + # (shellcheck SC2221). echo "= #$pr ($branch) already current" ;; - *conflict*|*"Merge conflict"*) + *conflict*) + # *"Merge conflict"* was a strict subset of *conflict* + # and so could never fire on its own (shellcheck SC2222). echo "✗ #$pr ($branch) conflict — author needed" conflicts+=("$pr") # Post a one-time marker comment so author gets a GitHub @@ -67,7 +72,11 @@ jobs: --jq ".comments[] | select(.body | startswith(\"$marker\")) | .id" | head -1) if [ -z "$existing" ]; then # printf keeps the multi-line body inside one bash var so - # YAML's block-scalar indentation rules stay happy. + # YAML's block-scalar indentation rules stay happy. The + # backticks in the format string are intentional Markdown + # code formatting — shellcheck SC2016 is a false positive + # because printf treats them as literal characters. + # shellcheck disable=SC2016 body=$(printf '%s\n\n🚨 **Auto-update-branch failed** — merge conflict with `main` (trigger commit `%s`). Please rebase / merge manually. This comment posts only once per conflict; delete it to let the bot re-notify on the next event.' "$marker" "$TRIGGER_SHA") gh pr comment "$pr" --repo "$REPO" --body "$body" || true fi