Metadata-Version: 2.4
Name: lcdp-back-merge
Version: 1.0.5
Summary: Cross-platform automated back-merge bot (Bitbucket / GitHub) with AI-assisted conflict resolution
Author: Le Comptoir Des Pharmacies
Author-email: webmaster@lecomptoirdespharmacies.fr
Requires-Python: >=3.9,<4.0
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: 3.15
Requires-Dist: anthropic (>=0.40.0)
Requires-Dist: pyjwt[crypto] (>=2.4.0)
Requires-Dist: requests (>=2.31.0,<3.0.0)
Description-Content-Type: text/markdown

# lcdp-back-merge

Bot de back-merge automatique multi-plateforme (Bitbucket Cloud / GitHub) avec résolution IA des conflits.

## Quoi ça fait

Pour chaque branche divergente de la branche source courante :

1. Crée une branche `auto-merge/<source>-into-<target>`
2. Tente le merge ; si conflit, demande à Claude une proposition de résolution par fichier
3. Ouvre une PR avec une description riche (statut, fichiers en conflit, suggestions IA)
4. **Auto-merge** si pas de conflit OU si l'IA a résolu tous les fichiers avec confiance `high`
5. **Notifie Slack** sinon (review requise)

> Les commits créés par le bot sur la branche `auto-merge/*` portent `[skip ci]`
> pour éviter une CI redondante sur cette branche éphémère :
> - **GitHub Actions** : saute les workflows sur `push` et `pull_request`.
> - **Bitbucket** : saute les *builds de branche* uniquement — **pas** les
>   pipelines `pull-requests:` ([BCLOUD-17676](https://jira.atlassian.com/browse/BCLOUD-17676), limite Bitbucket sans contournement).
>
> Sans effet sur la branche de destination : le bot merge en vrai merge commit
> (`merge_commit` / `merge`), dont le message ne contient pas le tag. La CI de
> `develop` tourne donc normalement après le merge.

> **Auto-merge et mergeabilité asynchrone (GitHub).** GitHub calcule `mergeable`
> après coup : une PR qui vient d'être créée renvoie `null`, et un `PUT /merge`
> immédiat est rejeté en `405 Base branch was modified`. Le bot attend donc que
> `mergeable` soit tranché avant de merger, et retente un 405/409 sur le merge
> lui-même. Un `mergeable: false` est un verdict — conflit réel, pas de retry.
> Sans cette attente, l'auto-merge perdait la course et laissait la PR ouverte
> avec un simple WARN : sur `lcdp-inventorist-app`, le back-merge de v2.2.0 est
> resté ouvert sept semaines et a fait échouer la release v2.2.1 sur le conflit
> correspondant. **Corollaire** : ce WARN reste le seul signal si
> `SLACK_WEBHOOK_URL` n'est pas fourni — le passer aux jobs qui lancent le bot.

## Tests

```bash
poetry install --with dev
poetry run pytest tests
```

## Configuration

### Variables d'environnement requises

| Variable | Plateforme | Description |
|---|---|---|
| `BB_AUTH_STRING` | Bitbucket | `client_id:client_secret` OAuth pour l'API Bitbucket |
| `GITHUB_APP_ID` | GitHub | App ID du GitHub App (auth App uniquement, pas de PAT) |
| `GITHUB_APP_PRIVATE_KEY` | GitHub | Clé privée PEM du GitHub App. Permissions requises : `contents:write` + `pull-requests:write` |

La plateforme est détectée automatiquement via les variables CI standard (`BITBUCKET_BRANCH`, `GITHUB_ACTIONS`).

> GitHub : l'auth se fait **exclusivement via GitHub App** (le script génère lui-même un installation token court). L'App doit être **installée sur le repo** cible.

### Variables d'environnement optionnelles

| Variable | Défaut | Description |
|---|---|---|
| `ANTHROPIC_API_KEY` | — | Active la résolution IA des conflits |
| `ANTHROPIC_MODEL` | `claude-sonnet-4-6` | Modèle Claude utilisé pour la résolution |
| `SLACK_WEBHOOK_URL` | — | Active les notifications Slack |
| `AUTO_MERGE_TARGET_PATTERNS` | (toutes) | Globs séparés par virgule : `develop,release/*` |
| `AUTO_MERGE_BRANCH_MAP` | — | Mapping source→patterns : `master:develop;develop:feature/*` |
| `AUTO_MERGE_DEFAULT_REVIEWERS` | — | Reviewers fallback en CSV (UUIDs Bitbucket ou logins GitHub) |
| `AUTO_MERGE_PR_RUN_LIMIT` | -1 (illimité) | Nombre max de PR créées par run |
| `AUTO_MERGE_SOURCE_BRANCH` | branche CI | Branche à back-merger, à la place de celle qui a déclenché la CI |

> **GitHub : ne pas surcharger `GITHUB_*` pour changer la source.** Actions ignore
> en silence un `env: GITHUB_REF_NAME: main` posé sur un step. Un workflow lancé
> depuis `release/vX` qui doit back-merger `main` pose `AUTO_MERGE_SOURCE_BRANCH: main`.
> Le log de démarrage affiche la source effective et la variable lue :
> `Source branch: main (from AUTO_MERGE_SOURCE_BRANCH)`.

## Utilisation en pipeline

### Bitbucket Pipelines

```yaml
branches:
  "{master}":
    - step:
        name: Back merge
        # image uv officielle (debian bookworm) → uv + git déjà présents
        image: ghcr.io/astral-sh/uv:python3.11-bookworm
        clone:
          depth: full          # toutes les branches divergentes
        script:
          - export AUTO_MERGE_TARGET_PATTERNS="develop"
          - uvx lcdp-back-merge
        # Variables (repository/deployment) : BB_AUTH_STRING, ANTHROPIC_API_KEY, SLACK_WEBHOOK_URL
```

### GitHub Actions

```yaml
- uses: actions/checkout@v4
  with:
    fetch-depth: 0          # toutes les branches divergentes
    persist-credentials: false   # le script gère l'auth git via le token App
- uses: astral-sh/setup-uv@v5
- name: Back-merge
  run: uvx lcdp-back-merge
  env:
    # Noms des secrets GitHub explicites (à gauche = ce que lit le script,
    # à droite = le secret/variable côté repo, nommé sans ambiguïté).
    GITHUB_APP_ID: ${{ vars.LCDP_BACKMERGE_APP_ID }}
    GITHUB_APP_PRIVATE_KEY: ${{ secrets.LCDP_BACKMERGE_APP_PRIVATE_KEY }}
    ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
    SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
```

## Architecture

```
lcdp_back_merge/
├── cli.py              # argparse, entry point
├── runner.py           # boucle principale, orchestration
├── ai_resolver.py      # résolution de conflits via Claude
├── slack.py            # notifications Slack
├── git_utils.py        # wrappers subprocess git
└── platforms/
    ├── base.py         # abstract Platform
    ├── bitbucket.py    # adaptateur Bitbucket Cloud
    └── github.py       # adaptateur GitHub
```

Ajouter une plateforme = ajouter une sous-classe de `Platform` dans `platforms/`, l'enregistrer dans `platforms.__init__.detect_platform()`.

