lcdp-back-merge 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,114 @@
1
+ Metadata-Version: 2.4
2
+ Name: lcdp-back-merge
3
+ Version: 1.0.0
4
+ Summary: Cross-platform automated back-merge bot (Bitbucket / GitHub) with AI-assisted conflict resolution
5
+ Author: Le Comptoir Des Pharmacies
6
+ Author-email: webmaster@lecomptoirdespharmacies.fr
7
+ Requires-Python: >=3.9,<4.0
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.9
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Requires-Dist: anthropic (>=0.40.0)
16
+ Requires-Dist: pyjwt[crypto] (>=2.4.0)
17
+ Requires-Dist: requests (>=2.31.0,<3.0.0)
18
+ Description-Content-Type: text/markdown
19
+
20
+ # lcdp-back-merge
21
+
22
+ Bot de back-merge automatique multi-plateforme (Bitbucket Cloud / GitHub) avec résolution IA des conflits.
23
+
24
+ ## Quoi ça fait
25
+
26
+ Pour chaque branche divergente de la branche source courante :
27
+
28
+ 1. Crée une branche `auto-merge/<source>-into-<target>`
29
+ 2. Tente le merge ; si conflit, demande à Claude une proposition de résolution par fichier
30
+ 3. Ouvre une PR avec une description riche (statut, fichiers en conflit, suggestions IA)
31
+ 4. **Auto-merge** si pas de conflit OU si l'IA a résolu tous les fichiers avec confiance `high`
32
+ 5. **Notifie Slack** sinon (review requise)
33
+
34
+ ## Configuration
35
+
36
+ ### Variables d'environnement requises
37
+
38
+ | Variable | Plateforme | Description |
39
+ |---|---|---|
40
+ | `BB_AUTH_STRING` | Bitbucket | `client_id:client_secret` OAuth pour l'API Bitbucket |
41
+ | `GITHUB_APP_ID` | GitHub | App ID du GitHub App (auth App uniquement, pas de PAT) |
42
+ | `GITHUB_APP_PRIVATE_KEY` | GitHub | Clé privée PEM du GitHub App. Permissions requises : `contents:write` + `pull-requests:write` |
43
+
44
+ La plateforme est détectée automatiquement via les variables CI standard (`BITBUCKET_BRANCH`, `GITHUB_ACTIONS`).
45
+
46
+ > 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.
47
+
48
+ ### Variables d'environnement optionnelles
49
+
50
+ | Variable | Défaut | Description |
51
+ |---|---|---|
52
+ | `ANTHROPIC_API_KEY` | — | Active la résolution IA des conflits |
53
+ | `ANTHROPIC_MODEL` | `claude-sonnet-4-6` | Modèle Claude utilisé pour la résolution |
54
+ | `SLACK_WEBHOOK_URL` | — | Active les notifications Slack |
55
+ | `AUTO_MERGE_TARGET_PATTERNS` | (toutes) | Globs séparés par virgule : `develop,release/*` |
56
+ | `AUTO_MERGE_BRANCH_MAP` | — | Mapping source→patterns : `master:develop;develop:feature/*` |
57
+ | `AUTO_MERGE_DEFAULT_REVIEWERS` | — | Reviewers fallback en CSV (UUIDs Bitbucket ou logins GitHub) |
58
+ | `AUTO_MERGE_PR_RUN_LIMIT` | -1 (illimité) | Nombre max de PR créées par run |
59
+
60
+ ## Utilisation en pipeline
61
+
62
+ ### Bitbucket Pipelines
63
+
64
+ ```yaml
65
+ branches:
66
+ "{master}":
67
+ - step:
68
+ name: Back merge
69
+ # image uv officielle (debian bookworm) → uv + git déjà présents
70
+ image: ghcr.io/astral-sh/uv:python3.11-bookworm
71
+ clone:
72
+ depth: full # toutes les branches divergentes
73
+ script:
74
+ - export AUTO_MERGE_TARGET_PATTERNS="develop"
75
+ - uvx lcdp-back-merge
76
+ # Variables (repository/deployment) : BB_AUTH_STRING, ANTHROPIC_API_KEY, SLACK_WEBHOOK_URL
77
+ ```
78
+
79
+ ### GitHub Actions
80
+
81
+ ```yaml
82
+ - uses: actions/checkout@v4
83
+ with:
84
+ fetch-depth: 0 # toutes les branches divergentes
85
+ persist-credentials: false # le script gère l'auth git via le token App
86
+ - uses: astral-sh/setup-uv@v5
87
+ - name: Back-merge
88
+ run: uvx lcdp-back-merge
89
+ env:
90
+ # Noms des secrets GitHub explicites (à gauche = ce que lit le script,
91
+ # à droite = le secret/variable côté repo, nommé sans ambiguïté).
92
+ GITHUB_APP_ID: ${{ vars.LCDP_BACKMERGE_APP_ID }}
93
+ GITHUB_APP_PRIVATE_KEY: ${{ secrets.LCDP_BACKMERGE_APP_PRIVATE_KEY }}
94
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
95
+ SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
96
+ ```
97
+
98
+ ## Architecture
99
+
100
+ ```
101
+ lcdp_back_merge/
102
+ ├── cli.py # argparse, entry point
103
+ ├── runner.py # boucle principale, orchestration
104
+ ├── ai_resolver.py # résolution de conflits via Claude
105
+ ├── slack.py # notifications Slack
106
+ ├── git_utils.py # wrappers subprocess git
107
+ └── platforms/
108
+ ├── base.py # abstract Platform
109
+ ├── bitbucket.py # adaptateur Bitbucket Cloud
110
+ └── github.py # adaptateur GitHub
111
+ ```
112
+
113
+ Ajouter une plateforme = ajouter une sous-classe de `Platform` dans `platforms/`, l'enregistrer dans `platforms.__init__.detect_platform()`.
114
+
@@ -0,0 +1,94 @@
1
+ # lcdp-back-merge
2
+
3
+ Bot de back-merge automatique multi-plateforme (Bitbucket Cloud / GitHub) avec résolution IA des conflits.
4
+
5
+ ## Quoi ça fait
6
+
7
+ Pour chaque branche divergente de la branche source courante :
8
+
9
+ 1. Crée une branche `auto-merge/<source>-into-<target>`
10
+ 2. Tente le merge ; si conflit, demande à Claude une proposition de résolution par fichier
11
+ 3. Ouvre une PR avec une description riche (statut, fichiers en conflit, suggestions IA)
12
+ 4. **Auto-merge** si pas de conflit OU si l'IA a résolu tous les fichiers avec confiance `high`
13
+ 5. **Notifie Slack** sinon (review requise)
14
+
15
+ ## Configuration
16
+
17
+ ### Variables d'environnement requises
18
+
19
+ | Variable | Plateforme | Description |
20
+ |---|---|---|
21
+ | `BB_AUTH_STRING` | Bitbucket | `client_id:client_secret` OAuth pour l'API Bitbucket |
22
+ | `GITHUB_APP_ID` | GitHub | App ID du GitHub App (auth App uniquement, pas de PAT) |
23
+ | `GITHUB_APP_PRIVATE_KEY` | GitHub | Clé privée PEM du GitHub App. Permissions requises : `contents:write` + `pull-requests:write` |
24
+
25
+ La plateforme est détectée automatiquement via les variables CI standard (`BITBUCKET_BRANCH`, `GITHUB_ACTIONS`).
26
+
27
+ > 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.
28
+
29
+ ### Variables d'environnement optionnelles
30
+
31
+ | Variable | Défaut | Description |
32
+ |---|---|---|
33
+ | `ANTHROPIC_API_KEY` | — | Active la résolution IA des conflits |
34
+ | `ANTHROPIC_MODEL` | `claude-sonnet-4-6` | Modèle Claude utilisé pour la résolution |
35
+ | `SLACK_WEBHOOK_URL` | — | Active les notifications Slack |
36
+ | `AUTO_MERGE_TARGET_PATTERNS` | (toutes) | Globs séparés par virgule : `develop,release/*` |
37
+ | `AUTO_MERGE_BRANCH_MAP` | — | Mapping source→patterns : `master:develop;develop:feature/*` |
38
+ | `AUTO_MERGE_DEFAULT_REVIEWERS` | — | Reviewers fallback en CSV (UUIDs Bitbucket ou logins GitHub) |
39
+ | `AUTO_MERGE_PR_RUN_LIMIT` | -1 (illimité) | Nombre max de PR créées par run |
40
+
41
+ ## Utilisation en pipeline
42
+
43
+ ### Bitbucket Pipelines
44
+
45
+ ```yaml
46
+ branches:
47
+ "{master}":
48
+ - step:
49
+ name: Back merge
50
+ # image uv officielle (debian bookworm) → uv + git déjà présents
51
+ image: ghcr.io/astral-sh/uv:python3.11-bookworm
52
+ clone:
53
+ depth: full # toutes les branches divergentes
54
+ script:
55
+ - export AUTO_MERGE_TARGET_PATTERNS="develop"
56
+ - uvx lcdp-back-merge
57
+ # Variables (repository/deployment) : BB_AUTH_STRING, ANTHROPIC_API_KEY, SLACK_WEBHOOK_URL
58
+ ```
59
+
60
+ ### GitHub Actions
61
+
62
+ ```yaml
63
+ - uses: actions/checkout@v4
64
+ with:
65
+ fetch-depth: 0 # toutes les branches divergentes
66
+ persist-credentials: false # le script gère l'auth git via le token App
67
+ - uses: astral-sh/setup-uv@v5
68
+ - name: Back-merge
69
+ run: uvx lcdp-back-merge
70
+ env:
71
+ # Noms des secrets GitHub explicites (à gauche = ce que lit le script,
72
+ # à droite = le secret/variable côté repo, nommé sans ambiguïté).
73
+ GITHUB_APP_ID: ${{ vars.LCDP_BACKMERGE_APP_ID }}
74
+ GITHUB_APP_PRIVATE_KEY: ${{ secrets.LCDP_BACKMERGE_APP_PRIVATE_KEY }}
75
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
76
+ SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
77
+ ```
78
+
79
+ ## Architecture
80
+
81
+ ```
82
+ lcdp_back_merge/
83
+ ├── cli.py # argparse, entry point
84
+ ├── runner.py # boucle principale, orchestration
85
+ ├── ai_resolver.py # résolution de conflits via Claude
86
+ ├── slack.py # notifications Slack
87
+ ├── git_utils.py # wrappers subprocess git
88
+ └── platforms/
89
+ ├── base.py # abstract Platform
90
+ ├── bitbucket.py # adaptateur Bitbucket Cloud
91
+ └── github.py # adaptateur GitHub
92
+ ```
93
+
94
+ Ajouter une plateforme = ajouter une sous-classe de `Platform` dans `platforms/`, l'enregistrer dans `platforms.__init__.detect_platform()`.
@@ -0,0 +1 @@
1
+ """lcdp-back-merge — Cross-platform automated back-merge bot."""
@@ -0,0 +1,4 @@
1
+ from lcdp_back_merge.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,241 @@
1
+ """Résolution de conflits Git via Claude.
2
+
3
+ Port direct de l'ancien ai-resolve-conflict.py. Lit base/ours/theirs depuis
4
+ l'index Git (le merge doit être en cours, c.-à-d. avant tout `git add` /
5
+ `git commit`) pour éviter de parser les marqueurs de conflit (qui peuvent être
6
+ au format standard ou diff3 selon la config locale).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import os
13
+ import re
14
+ import subprocess
15
+ from dataclasses import dataclass
16
+ from pathlib import Path
17
+ from typing import Optional
18
+
19
+ import anthropic
20
+
21
+ # Sonnet 4.6 plutôt qu'Opus 4.7 : la résolution de conflits ne nécessite pas la
22
+ # capacité d'Opus, et Sonnet a une limite TPM plus élevée. Override via
23
+ # ANTHROPIC_MODEL si besoin.
24
+ DEFAULT_MODEL = "claude-sonnet-4-6"
25
+
26
+ MAX_FILE_BYTES = 200_000
27
+ MAX_DIFF_BYTES = 30_000
28
+ MAX_BASE_BYTES = 10_000
29
+ MAX_LOG_COMMITS = 8
30
+
31
+
32
+ @dataclass
33
+ class Resolution:
34
+ confidence: str # "high" | "medium" | "low"
35
+ explanation: str
36
+ should_apply: bool
37
+ resolved_path: Optional[Path] = None
38
+
39
+
40
+ def _git(args: list[str]) -> str:
41
+ try:
42
+ return subprocess.check_output(
43
+ ["git", *args], text=True, stderr=subprocess.DEVNULL
44
+ )
45
+ except subprocess.CalledProcessError:
46
+ return ""
47
+
48
+
49
+ def _get_index_stage(stage: int, file_path: str) -> str:
50
+ return _git(["show", f":{stage}:{file_path}"])
51
+
52
+
53
+ def _get_index_diff(stage_from: int, stage_to: int, file_path: str) -> str:
54
+ out = _git(["diff", f":{stage_from}:{file_path}", f":{stage_to}:{file_path}"])
55
+ if len(out) > MAX_DIFF_BYTES:
56
+ out = out[:MAX_DIFF_BYTES] + f"\n\n[... diff truncated, showed first {MAX_DIFF_BYTES} bytes ...]"
57
+ return out or "(no changes)"
58
+
59
+
60
+ def _get_commit_log(branch: str, n: int = MAX_LOG_COMMITS) -> str:
61
+ out = _git(["log", f"-{n}", "--format=%h %s", branch])
62
+ return out.strip() or "(no log available)"
63
+
64
+
65
+ def is_available() -> bool:
66
+ """True si ANTHROPIC_API_KEY est défini et anthropic est importable."""
67
+ return bool(os.environ.get("ANTHROPIC_API_KEY"))
68
+
69
+
70
+ def resolve(
71
+ file_path: Path,
72
+ source_branch: str,
73
+ target_branch: str,
74
+ out_path: Path,
75
+ ) -> Resolution:
76
+ """Demande à Claude une résolution pour un fichier en conflit.
77
+
78
+ Écrit la résolution dans `out_path` UNIQUEMENT si `should_apply=True`.
79
+ L'orchestrateur applique ensuite ces fichiers selon sa stratégie.
80
+ """
81
+ raw = file_path.read_text(encoding="utf-8", errors="replace")
82
+
83
+ if len(raw.encode("utf-8")) > MAX_FILE_BYTES:
84
+ return Resolution(
85
+ confidence="low",
86
+ explanation="File too large for AI resolution.",
87
+ should_apply=False,
88
+ )
89
+
90
+ file_str = str(file_path)
91
+
92
+ base_content = _get_index_stage(1, file_str)
93
+ diff_ours = _get_index_diff(1, 2, file_str)
94
+ diff_theirs = _get_index_diff(1, 3, file_str)
95
+
96
+ if diff_ours == "(no changes)" and diff_theirs == "(no changes)":
97
+ return Resolution(
98
+ confidence="low",
99
+ explanation="No unmerged stages found for this file in the Git index.",
100
+ should_apply=False,
101
+ )
102
+
103
+ source_log = _get_commit_log(source_branch)
104
+ target_log = _get_commit_log(target_branch)
105
+
106
+ base_section = (
107
+ f"--- Common ancestor version of `{file_str}` ---\n```\n{base_content}\n```"
108
+ if base_content and len(base_content.encode("utf-8")) <= MAX_BASE_BYTES
109
+ else f"--- Common ancestor version of `{file_str}`: omitted (too large or not present) ---"
110
+ )
111
+
112
+ # max_retries=5 : le SDK gère la backoff exponentielle sur les 429 et 5xx.
113
+ # Utile car un back-merge avec beaucoup de fichiers en conflit peut taper
114
+ # sporadiquement les rate limits.
115
+ client = anthropic.Anthropic(max_retries=5)
116
+ model = os.environ.get("ANTHROPIC_MODEL", DEFAULT_MODEL)
117
+
118
+ system_prompt = """You are a senior software engineer resolving Git merge conflicts.
119
+
120
+ You will be given:
121
+ - A file currently containing Git conflict markers (<<<<<<<, =======, >>>>>>>)
122
+ - The diff of changes made on the TARGET branch (where the merge is happening) since the common ancestor, scoped to this file
123
+ - The diff of changes made on the SOURCE branch (the incoming changes) since the common ancestor, scoped to this file
124
+ - Optionally the common ancestor version of the file
125
+ - Recent commit messages from both branches for intent context
126
+
127
+ Your job:
128
+ 1. Use the diffs to understand WHAT each side intended to do — the markers alone hide the
129
+ real intent.
130
+ 2. Resolve every conflict in the file by producing the most likely correct merged version
131
+ that preserves the intent of BOTH sides whenever possible.
132
+ 3. If the two sides are TRULY incompatible (they implement opposite behaviors, or modify
133
+ the same business logic in contradictory ways), say so honestly — do NOT invent a
134
+ resolution that papers over the disagreement.
135
+ 4. Be especially careful with: authentication, authorization, cryptography, payment logic,
136
+ database migrations, security-critical paths. Default to should_apply=false on these
137
+ even if the resolution looks straightforward.
138
+
139
+ Output format — respond EXACTLY like this and nothing else:
140
+
141
+ <RESOLVED_FILE>
142
+ ...the fully resolved file content with no conflict markers...
143
+ </RESOLVED_FILE>
144
+ <METADATA>
145
+ {"confidence": "high"|"medium"|"low", "explanation": "one or two sentences", "should_apply": true|false}
146
+ </METADATA>
147
+
148
+ Be conservative. A bad resolution is worse than no resolution because it can mask bugs
149
+ behind code that compiles and looks reasonable."""
150
+
151
+ user_prompt = f"""Merging source branch `{source_branch}` INTO target branch `{target_branch}`.
152
+ File path: `{file_str}`
153
+
154
+ === Recent commits on `{source_branch}` (the INCOMING changes) ===
155
+ {source_log}
156
+
157
+ === Recent commits on `{target_branch}` (the branch BEING UPDATED) ===
158
+ {target_log}
159
+
160
+ === Changes on `{target_branch}` (ours/target) since common ancestor, for this file ===
161
+ ```diff
162
+ {diff_ours}
163
+ ```
164
+
165
+ === Changes on `{source_branch}` (theirs/source) since common ancestor, for this file ===
166
+ ```diff
167
+ {diff_theirs}
168
+ ```
169
+
170
+ {base_section}
171
+
172
+ === Current file with unresolved conflict markers ===
173
+ In the markers below, "ours" (between <<<<<<< and =======) is the TARGET branch's version,
174
+ and "theirs" (between ======= and >>>>>>>) is the SOURCE branch's version.
175
+
176
+ ```
177
+ {raw}
178
+ ```
179
+
180
+ Resolve all conflicts and respond in the format specified."""
181
+
182
+ # Prompt caching sur le system prompt : identique entre tous les appels d'un
183
+ # même back-merge run. NB : pour que le cache se déclenche, le préfixe doit
184
+ # dépasser ~2048 tokens sur Sonnet 4.6.
185
+ response = client.messages.create(
186
+ model=model,
187
+ max_tokens=8000,
188
+ system=[
189
+ {
190
+ "type": "text",
191
+ "text": system_prompt,
192
+ "cache_control": {"type": "ephemeral"},
193
+ }
194
+ ],
195
+ messages=[{"role": "user", "content": user_prompt}],
196
+ )
197
+
198
+ text = response.content[0].text
199
+
200
+ file_match = re.search(r"<RESOLVED_FILE>\n?(.*?)\n?</RESOLVED_FILE>", text, re.DOTALL)
201
+ meta_match = re.search(r"<METADATA>\n?(.*?)\n?</METADATA>", text, re.DOTALL)
202
+
203
+ if not file_match or not meta_match:
204
+ return Resolution(
205
+ confidence="low",
206
+ explanation="AI response did not match expected format.",
207
+ should_apply=False,
208
+ )
209
+
210
+ resolved_content = file_match.group(1)
211
+ try:
212
+ metadata = json.loads(meta_match.group(1).strip())
213
+ except json.JSONDecodeError:
214
+ return Resolution(
215
+ confidence="low",
216
+ explanation="AI metadata was not valid JSON.",
217
+ should_apply=False,
218
+ )
219
+
220
+ # `<<<<<<<` ne peut pas apparaître naturellement, canary suffisant pour les
221
+ # deux formats (standard et diff3).
222
+ if "<<<<<<<" in resolved_content:
223
+ return Resolution(
224
+ confidence="low",
225
+ explanation="AI output still contains conflict markers; not applied.",
226
+ should_apply=False,
227
+ )
228
+
229
+ should_apply = bool(metadata.get("should_apply"))
230
+ resolved_path = None
231
+ if should_apply:
232
+ out_path.parent.mkdir(parents=True, exist_ok=True)
233
+ out_path.write_text(resolved_content, encoding="utf-8")
234
+ resolved_path = out_path
235
+
236
+ return Resolution(
237
+ confidence=metadata.get("confidence", "low"),
238
+ explanation=metadata.get("explanation", ""),
239
+ should_apply=should_apply,
240
+ resolved_path=resolved_path,
241
+ )
@@ -0,0 +1,139 @@
1
+ """Entry point CLI : `lcdp-back-merge`.
2
+
3
+ Les options peuvent être passées via flags CLI OU variables d'environnement
4
+ (noms historiques `AUTO_MERGE_*` pour compat avec les pipelines existantes).
5
+ Le flag CLI a priorité sur la variable d'env.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import os
12
+ import sys
13
+
14
+ from lcdp_back_merge.platforms import detect_platform
15
+ from lcdp_back_merge.runner import (
16
+ BackMergeRunner,
17
+ Config,
18
+ parse_branch_map,
19
+ parse_csv,
20
+ parse_patterns,
21
+ )
22
+
23
+
24
+ def _build_parser() -> argparse.ArgumentParser:
25
+ parser = argparse.ArgumentParser(
26
+ prog="lcdp-back-merge",
27
+ description="Cross-platform automated back-merge bot (Bitbucket / GitHub).",
28
+ )
29
+ parser.add_argument(
30
+ "--target-patterns",
31
+ default=None,
32
+ help=(
33
+ "Globs de branches cibles, séparés par virgule (ex: 'develop,release/*'). "
34
+ "Par défaut : toutes les branches divergentes. "
35
+ "Override de AUTO_MERGE_TARGET_PATTERNS."
36
+ ),
37
+ )
38
+ parser.add_argument(
39
+ "--branch-map",
40
+ default=None,
41
+ help=(
42
+ "Mapping source→patterns pour piloter plusieurs branches sans dupliquer "
43
+ "la pipeline. Format : 'src1:patterns1;src2:patterns2'. "
44
+ "Override de AUTO_MERGE_BRANCH_MAP."
45
+ ),
46
+ )
47
+ parser.add_argument(
48
+ "--default-reviewers",
49
+ default=None,
50
+ help=(
51
+ "Reviewers fallback en CSV (UUIDs Bitbucket ou logins GitHub). "
52
+ "Override de AUTO_MERGE_DEFAULT_REVIEWERS."
53
+ ),
54
+ )
55
+ parser.add_argument(
56
+ "--pr-limit",
57
+ type=int,
58
+ default=None,
59
+ help="Nombre max de PR créées par run (-1 = illimité). Override de AUTO_MERGE_PR_RUN_LIMIT.",
60
+ )
61
+ parser.add_argument(
62
+ "--dry-run",
63
+ action="store_true",
64
+ help="N'effectue aucun push, aucune PR ni aucun merge — uniquement les logs.",
65
+ )
66
+ parser.add_argument(
67
+ "--bot-name",
68
+ default="Merge Resolver Bot",
69
+ help="Nom du commiter utilisé pour les commits auto-merge.",
70
+ )
71
+ parser.add_argument(
72
+ "--bot-email",
73
+ default="merge-resolver-bot@noreply.local",
74
+ help="Email du commiter utilisé pour les commits auto-merge.",
75
+ )
76
+ return parser
77
+
78
+
79
+ def _build_config(args: argparse.Namespace) -> Config:
80
+ target_patterns = parse_patterns(
81
+ args.target_patterns
82
+ if args.target_patterns is not None
83
+ else os.environ.get("AUTO_MERGE_TARGET_PATTERNS", "")
84
+ )
85
+
86
+ branch_map = parse_branch_map(
87
+ args.branch_map
88
+ if args.branch_map is not None
89
+ else os.environ.get("AUTO_MERGE_BRANCH_MAP", "")
90
+ )
91
+
92
+ default_reviewers = parse_csv(
93
+ args.default_reviewers
94
+ if args.default_reviewers is not None
95
+ else os.environ.get("AUTO_MERGE_DEFAULT_REVIEWERS", "")
96
+ )
97
+
98
+ pr_run_limit = (
99
+ args.pr_limit
100
+ if args.pr_limit is not None
101
+ else int(os.environ.get("AUTO_MERGE_PR_RUN_LIMIT", "-1"))
102
+ )
103
+
104
+ return Config(
105
+ target_patterns=target_patterns,
106
+ branch_map=branch_map,
107
+ default_reviewers=default_reviewers,
108
+ pr_run_limit=pr_run_limit,
109
+ dry_run=args.dry_run,
110
+ bot_name=args.bot_name,
111
+ bot_email=args.bot_email,
112
+ )
113
+
114
+
115
+ def main() -> int:
116
+ args = _build_parser().parse_args()
117
+ config = _build_config(args)
118
+
119
+ try:
120
+ platform = detect_platform()
121
+ except RuntimeError as exc:
122
+ print(f"ERROR: {exc}", file=sys.stderr)
123
+ return 1
124
+
125
+ print(f"Detected platform: {platform.name}")
126
+ print(f" Source branch: {platform.ctx.source_branch}")
127
+ print(f" Repository: {platform.ctx.repo_owner}/{platform.ctx.repo_slug}")
128
+
129
+ runner = BackMergeRunner(config, platform)
130
+ try:
131
+ runner.run()
132
+ except RuntimeError as exc:
133
+ print(f"ERROR: {exc}", file=sys.stderr)
134
+ return 1
135
+ return 0
136
+
137
+
138
+ if __name__ == "__main__":
139
+ sys.exit(main())