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..1c394e43 --- /dev/null +++ b/.github/workflows/auto-update-pr-branches.yml @@ -0,0 +1,94 @@ +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)" + ;; + *"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"* 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 + # 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. 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 + ;; + *) + 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.