Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/automerge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Auto-merge

# Enables GitHub's native auto-merge on every non-draft PR.
# GitHub will merge automatically once all required status checks pass
# (as configured in branch protection → required status checks → CI Gate).
#
# Prerequisites:
# 1. "Allow auto-merge" must be enabled in repo Settings → General
# 2. Branch protection for main must have "CI Gate" as a required check
# with "Allow administrators to bypass" unchecked

on:
pull_request:
types: [opened, synchronize, ready_for_review]

jobs:
enable-automerge:
name: Enable auto-merge
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
permissions:
pull-requests: write
contents: write
steps:
- name: Enable auto-merge (squash)
run: gh pr merge --auto --squash "${{ github.event.pull_request.number }}"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
18 changes: 18 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ jobs:
- run: ruff check .
- run: ruff format --check .
- run: bash -n scripts/*
- name: Validate pre-commit config
run: pre-commit validate-config .pre-commit-config.yaml
- run: pre-commit run --all-files --show-diff-on-failure
- name: Validate version consistency
run: |
Expand Down Expand Up @@ -159,3 +161,19 @@ jobs:
else
echo "CHANGELOG.md is up to date."
fi

gate:
name: CI Gate
runs-on: ubuntu-latest
needs: [test, lint, package]
if: always()
steps:
- name: All required checks passed
run: |
if [[ "${{ needs.test.result }}" != "success" ]] ||
[[ "${{ needs.lint.result }}" != "success" ]] ||
[[ "${{ needs.package.result }}" != "success" ]]; then
echo "::error::One or more required checks failed — merge blocked."
exit 1
fi
echo "All required checks passed."
20 changes: 19 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ git clone https://github.com/fusionAIze/faigate.git faigate
cd faigate
python3 -m venv venv && source venv/bin/activate
pip install -e ".[dev]"
pre-commit install # enforce hooks before every commit — required
```

`pre-commit install` installs git hooks that run the full hook suite (ruff, bandit,
trailing-whitespace, etc.) before each commit. This catches ~80 % of CI failures
locally before they ever reach GitHub. **Do not skip this step.**

To run all hooks manually at any time:

```bash
pre-commit run --all-files
```

## Running Tests
Expand Down Expand Up @@ -56,9 +67,16 @@ Important: Never score the system prompt for keywords. See ClawRouter's insight
1. Fork the repo
2. Create a `feature/<topic>-<date>` branch
3. Add tests for new functionality
4. Ensure `pytest` and `ruff check` pass
4. Ensure `pre-commit run --all-files` and `pytest` pass
5. Open a PR with a clear description

PRs are merged automatically by the CI Gate bot once all required checks pass
(test, lint, package). You do not need to manually trigger a merge. If checks
fail the bot will not merge — fix the issues and push again.

See [docs/process/ci-safeguards.md](./docs/process/ci-safeguards.md) for full
details on the CI enforcement model.

Use the repository templates when possible:

- bug reports via `.github/ISSUE_TEMPLATE/bug_report.yml`
Expand Down
150 changes: 150 additions & 0 deletions docs/process/ci-safeguards.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# CI Safeguards

Three complementary mechanisms enforce code quality gates in this repo:

1. **Local pre-commit hooks** — catch issues before they reach GitHub
2. **CI Gate job** — single required check that cannot be bypassed
3. **Auto-merge bot** — merges automatically once the gate passes

Together they eliminate the "merge and fix later" cycle.

---

## 1. Local Pre-commit Hooks

### Setup (one-time, per clone)

```bash
pip install -e ".[dev]"
pre-commit install
```

### What it runs on every `git commit`

| Hook | Purpose |
|------|---------|
| `trailing-whitespace` | Strips trailing whitespace |
| `end-of-file-fixer` | Ensures files end with a newline |
| `check-yaml` | Validates YAML syntax (catches duplicate keys) |
| `check-merge-conflict` | Detects unresolved conflict markers |
| `detect-private-key` | Blocks accidental credential commits |
| `ruff` | Lints Python (auto-fixes where possible) |
| `ruff-format` | Formats Python |
| `bandit` | Security scan on `faigate/` package |

### Manual run (scan all files)

```bash
pre-commit run --all-files
```

### Validate hook config itself

```bash
pre-commit validate-config .pre-commit-config.yaml
```

CI runs this step before executing the hooks. It catches typos in hook IDs
(e.g. `check-merge-conflicts` vs. `check-merge-conflict`) before they silently
disable entire hook groups.

---

## 2. CI Gate Job

### How it works

`ci.yml` defines a `gate` job that depends on `test`, `lint`, and `package`:

```yaml
gate:
name: CI Gate
runs-on: ubuntu-latest
needs: [test, lint, package]
if: always()
steps:
- name: All required checks passed
run: |
if [[ "${{ needs.test.result }}" != "success" ]] ||
[[ "${{ needs.lint.result }}" != "success" ]] ||
[[ "${{ needs.package.result }}" != "success" ]]; then
echo "::error::One or more required checks failed — merge blocked."
exit 1
fi
```

`if: always()` ensures the gate job runs even when upstream jobs fail — without
this, a failed `test` job would cause `gate` to be skipped, which would count
as "not run" rather than "failed" in branch protection.

### Branch protection setup (one-time, per repo)

In **Settings → Branches → Branch protection rules** for `main`:

1. Enable **"Require status checks to pass before merging"**
2. Add **`CI Gate`** as the required check (only this one — not individual jobs)
3. **Uncheck "Allow administrators to bypass branch protection rules"**

Step 3 is the critical one. With admin bypass disabled, `gh pr merge --admin`
no longer works. The only path to merge is through the gate.

> **If CI itself is broken** (e.g. a hook config typo): fix the CI config,
> push the fix as a PR, let the gate pass, and merge normally. Do not add
> temporary admin bypass — fix the root cause.

### Why a single gate check instead of multiple required checks?

Adding individual jobs (`test`, `lint`) as required checks means you have to
update branch protection settings every time you rename or add a job. The gate
job is a stable indirection layer: update `needs:` in the workflow, not GitHub
settings.

---

## 3. Auto-merge Bot

### How it works

`.github/workflows/automerge.yml` enables GitHub's native auto-merge on every
non-draft PR when it is opened or updated:

```yaml
- name: Enable auto-merge (squash)
run: gh pr merge --auto --squash "${{ github.event.pull_request.number }}"
```

Once enabled, GitHub automatically merges the PR the moment all required status
checks pass. No manual merge step needed.

### Prerequisites

Enable **"Allow auto-merge"** in **Settings → General** (under Pull Requests).
This is a one-time repo setting.

### Workflow

```
PR opened/pushed
├─► automerge.yml enables --auto on the PR
└─► CI runs: test + lint + package
├─► gate passes → GitHub merges automatically
└─► gate fails → PR stays open, author fixes and pushes
```

No manual `gh pr merge` calls. No `--admin` overrides.

---

## Troubleshooting

| Symptom | Cause | Fix |
|---------|-------|-----|
| Gate job skipped (not run) | `if: always()` missing from gate job | Add `if: always()` |
| `--admin` merge still works | Admin bypass not disabled in branch protection | Uncheck it in Settings |
| Auto-merge not triggering | "Allow auto-merge" disabled in repo settings | Enable in Settings → General |
| `pre-commit validate-config` fails | Typo in hook ID or wrong `rev` | Fix `.pre-commit-config.yaml` |
| `check-merge-conflict` unknown | Hook name typo (trailing `s`) or wrong version | Use `check-merge-conflict` with `rev: v4.6.0` |
Loading