agmh 0.2.0__py3-none-any.whl
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.
- agmh-0.2.0.dist-info/METADATA +1575 -0
- agmh-0.2.0.dist-info/RECORD +34 -0
- agmh-0.2.0.dist-info/WHEEL +5 -0
- agmh-0.2.0.dist-info/entry_points.txt +2 -0
- agmh-0.2.0.dist-info/licenses/LICENSE +24 -0
- agmh-0.2.0.dist-info/top_level.txt +1 -0
- anti_gh_ms_hysteria/__init__.py +4 -0
- anti_gh_ms_hysteria/__main__.py +6 -0
- anti_gh_ms_hysteria/cli.py +413 -0
- anti_gh_ms_hysteria/config.py +521 -0
- anti_gh_ms_hysteria/destinations/__init__.py +27 -0
- anti_gh_ms_hysteria/destinations/base.py +102 -0
- anti_gh_ms_hysteria/destinations/bitbucket.py +66 -0
- anti_gh_ms_hysteria/destinations/forgejo.py +80 -0
- anti_gh_ms_hysteria/destinations/github.py +76 -0
- anti_gh_ms_hysteria/destinations/gitlab.py +96 -0
- anti_gh_ms_hysteria/destinations/sourcehut.py +98 -0
- anti_gh_ms_hysteria/git_ops.py +310 -0
- anti_gh_ms_hysteria/http.py +251 -0
- anti_gh_ms_hysteria/models.py +187 -0
- anti_gh_ms_hysteria/notifications.py +325 -0
- anti_gh_ms_hysteria/py.typed +1 -0
- anti_gh_ms_hysteria/runner.py +771 -0
- anti_gh_ms_hysteria/sources/__init__.py +68 -0
- anti_gh_ms_hysteria/sources/base.py +50 -0
- anti_gh_ms_hysteria/sources/bitbucket.py +80 -0
- anti_gh_ms_hysteria/sources/forgejo.py +101 -0
- anti_gh_ms_hysteria/sources/github.py +107 -0
- anti_gh_ms_hysteria/sources/gitlab.py +94 -0
- anti_gh_ms_hysteria/sources/sourcehut.py +108 -0
- anti_gh_ms_hysteria/state.py +69 -0
- anti_gh_ms_hysteria/tokens.py +65 -0
- anti_gh_ms_hysteria/ui.py +68 -0
- anti_gh_ms_hysteria/utils.py +128 -0
|
@@ -0,0 +1,1575 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agmh
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: AGMH, a local GitHub backup and repository mirroring CLI.
|
|
5
|
+
Author-email: extencil <extencil@segfault.net>
|
|
6
|
+
Maintainer-email: extencil <extencil@segfault.net>
|
|
7
|
+
License-Expression: Unlicense
|
|
8
|
+
Project-URL: Homepage, https://github.com/haltman-io/agmh
|
|
9
|
+
Project-URL: Repository, https://github.com/haltman-io/agmh
|
|
10
|
+
Project-URL: Issues, https://github.com/haltman-io/agmh/issues
|
|
11
|
+
Keywords: backup,git,github,mirror,repository
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Provides-Extra: tui
|
|
26
|
+
Requires-Dist: rich>=13.7; extra == "tui"
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
29
|
+
Requires-Dist: twine>=5.1; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# AGMH
|
|
33
|
+
|
|
34
|
+

|
|
35
|
+

|
|
36
|
+

|
|
37
|
+

|
|
38
|
+

|
|
39
|
+
|
|
40
|
+
AGMH means **ANTI GITHUB & MICROSOFT HYSTERIA**.
|
|
41
|
+
|
|
42
|
+
Repository: [haltman-io/agmh](https://github.com/haltman-io/agmh)
|
|
43
|
+
|
|
44
|
+
AGMH is a local backup and repository mirroring CLI built to help researchers,
|
|
45
|
+
maintainers, and software teams pull their work out of a forge quickly and push
|
|
46
|
+
it to another forge without losing years of history, branches, tags, or research.
|
|
47
|
+
|
|
48
|
+
It discovers repositories from supported source profiles, organizations, groups,
|
|
49
|
+
namespaces, or workspaces; clones them locally as mirrors; adds a small
|
|
50
|
+
provenance marker file when enabled; creates matching repositories on
|
|
51
|
+
destination platforms; and pushes the backup to GitHub, GitLab,
|
|
52
|
+
Codeberg/Forgejo, SourceHut, Bitbucket, or compatible Git remotes.
|
|
53
|
+
|
|
54
|
+
The primary CLI command is:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
agmh
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The legacy typo `aghm` was removed. AGMH intentionally uses `agmh` for commands,
|
|
61
|
+
default config files, state directories, logs, and generated marker files.
|
|
62
|
+
|
|
63
|
+
## Contents
|
|
64
|
+
|
|
65
|
+
- [Capabilities](#capabilities)
|
|
66
|
+
- [Supported Platforms](#supported-platforms)
|
|
67
|
+
- [System Requirements](#system-requirements)
|
|
68
|
+
- [Installation](#installation)
|
|
69
|
+
- [Quick Start](#quick-start)
|
|
70
|
+
- [Workflow Modes](#workflow-modes)
|
|
71
|
+
- [Input Files](#input-files)
|
|
72
|
+
- [Configuration](#configuration)
|
|
73
|
+
- [Operations](#operations)
|
|
74
|
+
- [Troubleshooting](#troubleshooting)
|
|
75
|
+
- [Security Notes](#security-notes)
|
|
76
|
+
- [Development](#development)
|
|
77
|
+
- [Project Background](#project-background)
|
|
78
|
+
|
|
79
|
+
## Capabilities
|
|
80
|
+
|
|
81
|
+
AGMH can:
|
|
82
|
+
|
|
83
|
+
- Read source profile, organization, group, namespace, or workspace URLs from a text file.
|
|
84
|
+
- Discover accessible public and private repositories from GitHub, GitLab, Forgejo/Gitea, Bitbucket, and SourceHut.
|
|
85
|
+
- Use one or more source tokens to increase API limits and access private repos.
|
|
86
|
+
- Rotate tokens when rate limits or authorization failures happen.
|
|
87
|
+
- Clone each repository locally using `git clone --mirror`.
|
|
88
|
+
- Run in local-only mode to download/update mirrors without pushing anywhere.
|
|
89
|
+
- Run in remote-only mode to push existing local mirrors to configured destinations later.
|
|
90
|
+
- Keep a local backup under `backups/` by default.
|
|
91
|
+
- Optionally add `agmh.txt` to the default branch before remote mirroring.
|
|
92
|
+
- Create destination repositories through platform APIs.
|
|
93
|
+
- Preserve repository name and public/private visibility where supported.
|
|
94
|
+
- Push mirrors to GitHub, GitLab, Codeberg/Forgejo, SourceHut, Bitbucket, and similar Git destinations.
|
|
95
|
+
- Use resumable state in `.agmh/state.json`.
|
|
96
|
+
- Write detailed logs to `.agmh/logs/`.
|
|
97
|
+
- Run dry-run simulations.
|
|
98
|
+
- Use proxies.
|
|
99
|
+
- Disable TLS verification when needed for intercepting proxies.
|
|
100
|
+
- Use SSH keys for destinations such as SourceHut.
|
|
101
|
+
- Keep going when one repository fails instead of aborting the whole run.
|
|
102
|
+
|
|
103
|
+
## Supported Platforms
|
|
104
|
+
|
|
105
|
+
Source support:
|
|
106
|
+
|
|
107
|
+
| Platform | Discovery scope | Private repos | Notes |
|
|
108
|
+
| --- | --- | --- | --- |
|
|
109
|
+
| GitHub | User or organization | Yes, with token | Uses GitHub REST repositories API. GitHub Enterprise can use `api_base`. |
|
|
110
|
+
| GitLab | User, group, or subgroup | Yes, with token | Group discovery includes subgroups. `internal` repositories are treated as non-public when mirrored elsewhere. |
|
|
111
|
+
| Codeberg | User or organization | Yes, with token | Uses the Forgejo adapter. |
|
|
112
|
+
| Forgejo/Gitea | User or organization | Yes, with token | Works with compatible `/api/v1` instances. |
|
|
113
|
+
| Bitbucket | Workspace | Yes, with token | Uses Bitbucket Cloud workspaces and repository pagination. |
|
|
114
|
+
| SourceHut | User | Yes, with token | Uses the git.sr.ht GraphQL API. `unlisted` visibility is preserved when the destination supports it. |
|
|
115
|
+
|
|
116
|
+
Destination support:
|
|
117
|
+
|
|
118
|
+
| Platform | API create | HTTPS push | SSH push | Notes |
|
|
119
|
+
| --- | --- | --- | --- | --- |
|
|
120
|
+
| GitHub | Yes | Yes | Possible with custom URL | Use a destination token with repo creation and push permissions. GitHub Enterprise can use `api_base`. |
|
|
121
|
+
| GitLab | Yes | Yes | Possible with custom URL | Hidden repo names such as `.github` are mapped to valid GitLab paths such as `dot-github`. |
|
|
122
|
+
| Codeberg | Yes | Yes | Possible with custom URL | Uses Forgejo API. GitHub `refs/pull/*` refs are excluded in portable mirror mode because Codeberg rejects hidden refs. |
|
|
123
|
+
| Forgejo/Gitea | Yes | Yes | Possible with custom URL | Same adapter family as Codeberg. |
|
|
124
|
+
| SourceHut | Yes | Optional | Yes | API token creates repositories, SSH is recommended for Git pushes. |
|
|
125
|
+
| Bitbucket | Yes | Yes | Possible with custom URL | Requires Bitbucket-compatible credentials. |
|
|
126
|
+
|
|
127
|
+
## System Requirements
|
|
128
|
+
|
|
129
|
+
- Python 3.11 or newer.
|
|
130
|
+
- Git available in `PATH`.
|
|
131
|
+
- Network access to source and destination forges.
|
|
132
|
+
- Destination accounts and tokens with enough permission to create repositories and push Git refs.
|
|
133
|
+
- Optional: `git-lfs` if you enable `lfs = true`.
|
|
134
|
+
- Optional: `ssh-agent` and SSH keys for SourceHut or SSH-based destinations.
|
|
135
|
+
|
|
136
|
+
Ubuntu system packages:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
sudo apt update
|
|
140
|
+
sudo apt install -y python3 python3-venv python3-pip git ca-certificates openssh-client
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Optional packages:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
sudo apt install -y git-lfs curl
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Notes:
|
|
150
|
+
|
|
151
|
+
- `git-lfs` is only required when AGMH is configured with `lfs = true`.
|
|
152
|
+
- `curl` is used only for the troubleshooting and proxy test commands shown in this README.
|
|
153
|
+
- If your Ubuntu release provides Python older than 3.11, install Python 3.11 or newer before creating the virtual environment.
|
|
154
|
+
|
|
155
|
+
## Installation
|
|
156
|
+
|
|
157
|
+
Clone the repository:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
git clone https://github.com/haltman-io/agmh.git
|
|
161
|
+
cd agmh
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Create a virtual environment:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
python3 -m venv .venv
|
|
168
|
+
source .venv/bin/activate
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Install AGMH in editable mode:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
python -m pip install -U pip
|
|
175
|
+
python -m pip install -e ".[tui]"
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Check the CLI:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
agmh --help
|
|
182
|
+
agmh run --help
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
If you do not install the package, you can run it with `PYTHONPATH`:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
PYTHONPATH=src python3 -m anti_gh_ms_hysteria run --help
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Quick Start
|
|
192
|
+
|
|
193
|
+
Create a starter config:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
cp config.example.toml agmh.config.toml
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Create `sources.txt` from the public-safe example:
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
cp sources.example.txt sources.txt
|
|
203
|
+
$EDITOR sources.txt
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
If you prefer to keep destinations in a separate file instead of inline TOML,
|
|
207
|
+
start from the destination example:
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
cp destinations.example.txt destinations.txt
|
|
211
|
+
$EDITOR destinations.txt
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Export tokens:
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
export GITHUB_TOKEN="github_token_here"
|
|
218
|
+
export GITHUB_DEST_TOKEN="github_destination_token_here"
|
|
219
|
+
export GITLAB_TOKEN="gitlab_token_here"
|
|
220
|
+
export CODEBERG_TOKEN="codeberg_token_here"
|
|
221
|
+
export SOURCEHUT_TOKEN="sourcehut_token_here"
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Run a dry-run first:
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
agmh run --config agmh.config.toml --dry-run --verbose
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Run the real backup:
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
agmh run --config agmh.config.toml --verbose
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Check state:
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
agmh state --config agmh.config.toml
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Workflow Modes
|
|
243
|
+
|
|
244
|
+
Use this table to choose the right command:
|
|
245
|
+
|
|
246
|
+
| Goal | Command | Reads sources | Pushes destinations |
|
|
247
|
+
| --- | --- | --- | --- |
|
|
248
|
+
| Download mirrors and push them now | `agmh run --config agmh.config.toml` | Yes | Yes |
|
|
249
|
+
| Download or update local mirrors only | `agmh local-mirror --config agmh.config.toml` | Yes | No |
|
|
250
|
+
| Push mirrors that already exist locally | `agmh remote-mirror --config agmh.config.toml` | No | Yes |
|
|
251
|
+
| Keep polling sources and react to changes | `agmh watching --config agmh.config.toml` | Yes | Depends on `watch.action` |
|
|
252
|
+
|
|
253
|
+
Default full workflow:
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
agmh run --config agmh.config.toml --verbose
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
This discovers source repositories, clones or updates local mirrors, adds the
|
|
260
|
+
marker commit when `backup.marker_enabled` is true, creates destination
|
|
261
|
+
repositories, and pushes mirrors.
|
|
262
|
+
|
|
263
|
+
Local mirror only:
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
agmh local-mirror --config agmh.config.toml --verbose
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Equivalent:
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
agmh run --config agmh.config.toml --mode local --verbose
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
This discovers source repositories and only clones or updates local bare mirrors
|
|
276
|
+
under `backup.local_dir`. It does not create marker commits and does not contact
|
|
277
|
+
destination forges, even if destinations are present in the config.
|
|
278
|
+
|
|
279
|
+
Remote mirror from existing local mirrors:
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
agmh remote-mirror --config agmh.config.toml --verbose
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Equivalent:
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
agmh run --config agmh.config.toml --mode remote --verbose
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
This does not discover or clone from source forges. It reads mirrors recorded in
|
|
292
|
+
`.agmh/state.json`, falls back to scanning `backup.local_dir`, adds the marker
|
|
293
|
+
commit if enabled and needed, creates destination repositories, and pushes the
|
|
294
|
+
local mirrors.
|
|
295
|
+
When AGMH has to scan local mirrors without state metadata, repository privacy is
|
|
296
|
+
unknown, so it treats those repositories as private by default.
|
|
297
|
+
|
|
298
|
+
By default, remote mirror follows the source repository visibility. You can
|
|
299
|
+
override destination visibility for the whole remote mirror run:
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
agmh remote-mirror --config agmh.config.toml --destination-visibility mirror
|
|
303
|
+
agmh remote-mirror --config agmh.config.toml --destination-visibility public
|
|
304
|
+
agmh remote-mirror --config agmh.config.toml --destination-visibility private
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
`mirror` applies the same visibility recorded from the source. `public` creates
|
|
308
|
+
destination repositories as public regardless of source visibility. `private`
|
|
309
|
+
creates destination repositories as private regardless of source visibility. If
|
|
310
|
+
a destination repository already exists, AGMH uses the existing repository as-is.
|
|
311
|
+
The same override is available with `agmh run --mode remote`.
|
|
312
|
+
|
|
313
|
+
Watching mode:
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
agmh watching --config agmh.config.toml --verbose
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
Equivalent:
|
|
320
|
+
|
|
321
|
+
```bash
|
|
322
|
+
agmh run --config agmh.config.toml --mode watching --verbose
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Watching mode runs until interrupted. It polls enabled sources, compares the
|
|
326
|
+
current source metadata with `.agmh/state.json`, and runs the configured action
|
|
327
|
+
only for first-seen or changed repositories. The default action is `full`.
|
|
328
|
+
The change fingerprint uses source API update fields such as GitHub `pushed_at`,
|
|
329
|
+
GitLab `last_activity_at`, Forgejo `updated_at`, Bitbucket `updated_on`, and
|
|
330
|
+
SourceHut `updated`. GitLab documents that `last_activity_at` can lag by up to
|
|
331
|
+
one hour, so GitLab polling is eventually consistent rather than instant.
|
|
332
|
+
|
|
333
|
+
Watching actions:
|
|
334
|
+
|
|
335
|
+
| Action | Behavior |
|
|
336
|
+
| --- | --- |
|
|
337
|
+
| `full` | Clone/update the local mirror, ensure the marker when enabled, create destinations, and push. |
|
|
338
|
+
| `local` | Clone/update the local mirror only. |
|
|
339
|
+
| `remote` | Push an existing local mirror for the changed repository. If the local mirror is missing, the action fails for that repository. |
|
|
340
|
+
|
|
341
|
+
Configure polling globally:
|
|
342
|
+
|
|
343
|
+
```toml
|
|
344
|
+
[watch]
|
|
345
|
+
interval_seconds = 300
|
|
346
|
+
action = "full" # full, local, or remote
|
|
347
|
+
initial_run = true # process repositories the first time they are seen
|
|
348
|
+
once = false # useful for tests or supervised one-shot runs
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Override polling per source:
|
|
352
|
+
|
|
353
|
+
```toml
|
|
354
|
+
[[sources]]
|
|
355
|
+
url = "https://gitlab.com/haltman-io"
|
|
356
|
+
platform = "gitlab"
|
|
357
|
+
tokens = [{ env = "GITLAB_SOURCE_TOKEN" }]
|
|
358
|
+
watch = true
|
|
359
|
+
watch_interval_seconds = 120
|
|
360
|
+
watch_action = "local"
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
CLI overrides:
|
|
364
|
+
|
|
365
|
+
```bash
|
|
366
|
+
agmh watching \
|
|
367
|
+
--config agmh.config.toml \
|
|
368
|
+
--watch-interval 120 \
|
|
369
|
+
--watch-action full
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Use `--no-watch-initial-run` when you want AGMH to record the current state
|
|
373
|
+
without processing existing repositories on the first polling cycle. Use
|
|
374
|
+
`--watch-once` to run one polling cycle and exit.
|
|
375
|
+
|
|
376
|
+
You can also set the mode in TOML:
|
|
377
|
+
|
|
378
|
+
```toml
|
|
379
|
+
mode = "full" # full, local, remote, or watching
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
## Input Files
|
|
383
|
+
|
|
384
|
+
AGMH reads source profiles from a plain text file. Use one profile,
|
|
385
|
+
organization, group, namespace, or workspace URL per line:
|
|
386
|
+
|
|
387
|
+
```txt
|
|
388
|
+
https://github.com/extencil/
|
|
389
|
+
https://github.com/haltman-io/
|
|
390
|
+
https://gitlab.com/haltman-io/
|
|
391
|
+
https://codeberg.org/haltman/
|
|
392
|
+
https://bitbucket.org/example-workspace/
|
|
393
|
+
https://git.sr.ht/~extencil/
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
Blank lines and lines beginning with `#` are ignored.
|
|
397
|
+
|
|
398
|
+
You can also pass source profiles directly:
|
|
399
|
+
|
|
400
|
+
```bash
|
|
401
|
+
agmh run --source https://github.com/extencil/ --source https://github.com/haltman-io/
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
For private non-GitHub sources, prefer inline `[[sources]]` entries so each
|
|
405
|
+
source can declare its own `tokens` and `api_base`.
|
|
406
|
+
|
|
407
|
+
Destinations can be configured in TOML or in a plain text file:
|
|
408
|
+
|
|
409
|
+
```txt
|
|
410
|
+
https://gitlab.com/haltman-io
|
|
411
|
+
https://codeberg.org/haltman
|
|
412
|
+
https://git.sr.ht/~extencil
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
## Configuration
|
|
416
|
+
|
|
417
|
+
Most users should start from `config.example.toml`, then edit `sources_file`,
|
|
418
|
+
`[[sources]]`, and `[[destinations]]` for their environment. Use environment
|
|
419
|
+
variables for tokens and webhook URLs.
|
|
420
|
+
|
|
421
|
+
### Full Example
|
|
422
|
+
|
|
423
|
+
```toml
|
|
424
|
+
workspace = ".agmh"
|
|
425
|
+
mode = "full"
|
|
426
|
+
dry_run = false
|
|
427
|
+
verbose = 0
|
|
428
|
+
tui = true
|
|
429
|
+
insecure_tls = false
|
|
430
|
+
sources_file = "sources.txt"
|
|
431
|
+
|
|
432
|
+
[github]
|
|
433
|
+
tokens = [
|
|
434
|
+
{ env = "GITHUB_TOKEN", name = "github-primary" },
|
|
435
|
+
# { env = "GITHUB_TOKEN_2", name = "github-secondary" },
|
|
436
|
+
]
|
|
437
|
+
|
|
438
|
+
[watch]
|
|
439
|
+
interval_seconds = 300
|
|
440
|
+
action = "full"
|
|
441
|
+
initial_run = true
|
|
442
|
+
once = false
|
|
443
|
+
|
|
444
|
+
[notifications]
|
|
445
|
+
enabled = false
|
|
446
|
+
events = ["*"]
|
|
447
|
+
fail_silently = true
|
|
448
|
+
timeout_seconds = 10
|
|
449
|
+
|
|
450
|
+
# [[webhooks]]
|
|
451
|
+
# name = "ops-discord"
|
|
452
|
+
# platform = "discord"
|
|
453
|
+
# url_env = "DISCORD_WEBHOOK_URL"
|
|
454
|
+
# events = ["start", "finish", "error", "local_saved", "remote_saved", "watch_check", "watch_update", "watch_none"]
|
|
455
|
+
# username = "AGMH"
|
|
456
|
+
#
|
|
457
|
+
# [[webhooks]]
|
|
458
|
+
# name = "ops-telegram"
|
|
459
|
+
# platform = "telegram"
|
|
460
|
+
# bot_token_env = "TELEGRAM_BOT_TOKEN"
|
|
461
|
+
# chat_id_env = "TELEGRAM_CHAT_ID"
|
|
462
|
+
# events = ["start", "finish", "error", "watch_update"]
|
|
463
|
+
# parse_mode = "HTML"
|
|
464
|
+
|
|
465
|
+
# Inline sources are useful when a non-GitHub source needs a token or api_base.
|
|
466
|
+
[[sources]]
|
|
467
|
+
url = "https://gitlab.com/haltman-io"
|
|
468
|
+
platform = "gitlab"
|
|
469
|
+
tokens = [{ env = "GITLAB_SOURCE_TOKEN", name = "gitlab-source" }]
|
|
470
|
+
watch_interval_seconds = 120
|
|
471
|
+
watch_action = "local"
|
|
472
|
+
|
|
473
|
+
[backup]
|
|
474
|
+
local_dir = "backups"
|
|
475
|
+
clone_protocol = "https"
|
|
476
|
+
include_archived = true
|
|
477
|
+
include_forks = true
|
|
478
|
+
include_private_for_authenticated_user = true
|
|
479
|
+
lfs = false
|
|
480
|
+
marker_enabled = true
|
|
481
|
+
push_mode = "mirror"
|
|
482
|
+
|
|
483
|
+
[retry]
|
|
484
|
+
max_retries = 5
|
|
485
|
+
base_delay_seconds = 1.5
|
|
486
|
+
max_delay_seconds = 60
|
|
487
|
+
request_timeout_seconds = 15
|
|
488
|
+
rate_limit_sleep_seconds = 300
|
|
489
|
+
wait_on_rate_limit = true
|
|
490
|
+
|
|
491
|
+
[git]
|
|
492
|
+
author_name = "extencil"
|
|
493
|
+
author_email = "extencil@segfault.net"
|
|
494
|
+
commit_message = "Backuping with AGMH v{version}"
|
|
495
|
+
# ssh_identity_file = "/home/user/.ssh/sourcehut_ed25519"
|
|
496
|
+
# ssh_identities_only = true
|
|
497
|
+
# ssh_batch_mode = false
|
|
498
|
+
# ssh_strict_host_key_checking = "accept-new"
|
|
499
|
+
# ssh_command = "ssh -i /home/user/.ssh/sourcehut_ed25519 -o IdentitiesOnly=yes"
|
|
500
|
+
|
|
501
|
+
[[destinations]]
|
|
502
|
+
url = "https://github.com/haltman-io-mirror"
|
|
503
|
+
platform = "github"
|
|
504
|
+
tokens = [{ env = "GITHUB_DEST_TOKEN", name = "github-destination" }]
|
|
505
|
+
visibility = "mirror"
|
|
506
|
+
push_mode = "mirror"
|
|
507
|
+
|
|
508
|
+
[[destinations]]
|
|
509
|
+
url = "https://gitlab.com/haltman-io"
|
|
510
|
+
platform = "gitlab"
|
|
511
|
+
tokens = [{ env = "GITLAB_TOKEN", name = "gitlab-primary" }]
|
|
512
|
+
visibility = "mirror"
|
|
513
|
+
push_mode = "mirror"
|
|
514
|
+
|
|
515
|
+
[[destinations]]
|
|
516
|
+
url = "https://codeberg.org/haltman"
|
|
517
|
+
platform = "forgejo"
|
|
518
|
+
tokens = [{ env = "CODEBERG_TOKEN", name = "codeberg-primary" }]
|
|
519
|
+
visibility = "mirror"
|
|
520
|
+
push_mode = "mirror"
|
|
521
|
+
|
|
522
|
+
[[destinations]]
|
|
523
|
+
url = "https://git.sr.ht/~extencil"
|
|
524
|
+
platform = "sourcehut"
|
|
525
|
+
tokens = [{ env = "SOURCEHUT_TOKEN", name = "sourcehut-primary" }]
|
|
526
|
+
visibility = "mirror"
|
|
527
|
+
push_mode = "mirror"
|
|
528
|
+
push_url_template = "git@git.sr.ht:~{owner}/{repo}"
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
### Reference
|
|
532
|
+
|
|
533
|
+
Top-level options:
|
|
534
|
+
|
|
535
|
+
| Key | Meaning |
|
|
536
|
+
| --- | --- |
|
|
537
|
+
| `mode` | Workflow mode: `full`, `local`, `remote`, or `watching`. Default: `full`. |
|
|
538
|
+
| `workspace` | Local state and logs directory. Default: `.agmh`. |
|
|
539
|
+
| `dry_run` | Plan actions without cloning, creating, or pushing. |
|
|
540
|
+
| `verbose` | Default verbosity level. CLI `-v` can override it. |
|
|
541
|
+
| `tui` | Use Rich console rendering when installed. |
|
|
542
|
+
| `proxy` | Optional HTTP/HTTPS proxy URL. |
|
|
543
|
+
| `insecure_tls` | Disable TLS certificate verification for API calls and Git HTTPS operations. |
|
|
544
|
+
| `resume` | Reuse `.agmh/state.json` and skip completed steps. |
|
|
545
|
+
| `force` | Redo steps even if state says they are complete. |
|
|
546
|
+
| `sources_file` | Text file containing source profile/org/group/workspace URLs. |
|
|
547
|
+
|
|
548
|
+
GitHub source shortcut options:
|
|
549
|
+
|
|
550
|
+
| Key | Meaning |
|
|
551
|
+
| --- | --- |
|
|
552
|
+
| `api_base` | GitHub API base URL. Default: `https://api.github.com`. |
|
|
553
|
+
| `profiles_file` | Text file containing GitHub source profile/org URLs. Prefer top-level `sources_file` for mixed providers. |
|
|
554
|
+
| `profiles` | Inline list of GitHub source profile/org URLs. Prefer `[[sources]]` for mixed providers. |
|
|
555
|
+
| `tokens` | GitHub source token entries. These are also attached to GitHub URLs read from `sources_file`. |
|
|
556
|
+
|
|
557
|
+
Source options:
|
|
558
|
+
|
|
559
|
+
| Key | Meaning |
|
|
560
|
+
| --- | --- |
|
|
561
|
+
| `url` | Source profile, org, group, namespace, or workspace URL. |
|
|
562
|
+
| `platform` | `github`, `gitlab`, `forgejo`, `sourcehut`, or `bitbucket`. Usually inferred from `url`. |
|
|
563
|
+
| `api_base` | Optional API override for self-hosted or enterprise instances. |
|
|
564
|
+
| `owner` | Optional owner/namespace/workspace override. |
|
|
565
|
+
| `tokens` | Source API and HTTPS clone tokens. Use `env` instead of hardcoding secrets. |
|
|
566
|
+
| `watch` | Enable or disable this source in watching mode. Default: `true`. |
|
|
567
|
+
| `watch_interval_seconds` | Per-source polling interval override. |
|
|
568
|
+
| `watch_action` | Per-source action override: `full`, `local`, or `remote`. |
|
|
569
|
+
|
|
570
|
+
Watch options:
|
|
571
|
+
|
|
572
|
+
| Key | Meaning |
|
|
573
|
+
| --- | --- |
|
|
574
|
+
| `interval_seconds` | Default polling interval for sources in watching mode. |
|
|
575
|
+
| `action` | Default action for changed repositories: `full`, `local`, or `remote`. |
|
|
576
|
+
| `initial_run` | Process repositories the first time they are seen. If `false`, AGMH records current fingerprints and waits for later changes. |
|
|
577
|
+
| `once` | Run one polling cycle and exit. Mainly useful for tests, cron-like runs, or supervised debugging. |
|
|
578
|
+
|
|
579
|
+
Backup options:
|
|
580
|
+
|
|
581
|
+
| Key | Meaning |
|
|
582
|
+
| --- | --- |
|
|
583
|
+
| `local_dir` | Local mirror storage directory. |
|
|
584
|
+
| `clone_protocol` | `https` or `ssh` for source clone URLs. |
|
|
585
|
+
| `include_archived` | Include archived repositories. |
|
|
586
|
+
| `include_forks` | Include forked repositories. |
|
|
587
|
+
| `include_private_for_authenticated_user` | When the token belongs to the source user, include private repositories. |
|
|
588
|
+
| `lfs` | Run `git lfs fetch --all` after mirror updates. |
|
|
589
|
+
| `marker_enabled` | Write a provenance marker commit before remote mirrors. Default: `true`. Set to `false` to avoid modifying mirrored repositories. |
|
|
590
|
+
| `marker_filename` | Marker file name. Default: `agmh.txt`. |
|
|
591
|
+
| `push_mode` | `mirror`, `portable-mirror`, `all`, or `default`. |
|
|
592
|
+
|
|
593
|
+
Retry options:
|
|
594
|
+
|
|
595
|
+
| Key | Meaning |
|
|
596
|
+
| --- | --- |
|
|
597
|
+
| `max_retries` | Maximum retry attempts for transient API and Git network failures. |
|
|
598
|
+
| `base_delay_seconds` | Initial retry delay. |
|
|
599
|
+
| `max_delay_seconds` | Maximum exponential backoff delay. |
|
|
600
|
+
| `request_timeout_seconds` | API request timeout. |
|
|
601
|
+
| `rate_limit_sleep_seconds` | Sleep interval when rate limited and no reset time is available. |
|
|
602
|
+
| `wait_on_rate_limit` | Wait and resume instead of failing on rate limits. |
|
|
603
|
+
|
|
604
|
+
Git options:
|
|
605
|
+
|
|
606
|
+
| Key | Meaning |
|
|
607
|
+
| --- | --- |
|
|
608
|
+
| `author_name` | Git author name for marker commits. |
|
|
609
|
+
| `author_email` | Git author email for marker commits. |
|
|
610
|
+
| `commit_message` | Commit message for the marker commit. Supports `{version}`. Default: `Backuping with AGMH v{version}`. |
|
|
611
|
+
| `ssh_identity_file` | Private key for Git SSH operations. |
|
|
612
|
+
| `ssh_command` | Full `GIT_SSH_COMMAND` override. |
|
|
613
|
+
| `ssh_identities_only` | Add `-o IdentitiesOnly=yes` when using `ssh_identity_file`. |
|
|
614
|
+
| `ssh_batch_mode` | Add `-o BatchMode=yes`, useful for non-interactive jobs. |
|
|
615
|
+
| `ssh_strict_host_key_checking` | `yes`, `no`, or `accept-new`. |
|
|
616
|
+
|
|
617
|
+
Destination options:
|
|
618
|
+
|
|
619
|
+
| Key | Meaning |
|
|
620
|
+
| --- | --- |
|
|
621
|
+
| `url` | Destination account, group, org, or namespace URL. |
|
|
622
|
+
| `platform` | `github`, `gitlab`, `forgejo`, `sourcehut`, or `bitbucket`. |
|
|
623
|
+
| `api_base` | Optional API override for self-hosted instances. |
|
|
624
|
+
| `owner` | Optional owner/namespace override. |
|
|
625
|
+
| `tokens` | Destination API/Git tokens. |
|
|
626
|
+
| `visibility` | `mirror`, `public`, `private`, or `unlisted`. |
|
|
627
|
+
| `push_mode` | Override push mode for that destination. |
|
|
628
|
+
| `create` | Create repositories through the destination API. |
|
|
629
|
+
| `allow_existing` | Treat existing repositories as usable. |
|
|
630
|
+
| `git_username` | Username for HTTPS Git push URLs. |
|
|
631
|
+
| `push_url_template` | Custom push URL, for example SourceHut SSH. |
|
|
632
|
+
|
|
633
|
+
Notification options:
|
|
634
|
+
|
|
635
|
+
| Key | Meaning |
|
|
636
|
+
| --- | --- |
|
|
637
|
+
| `enabled` | Enable webhook notifications. Default: `false`. |
|
|
638
|
+
| `events` | Global event filter. Use `["*"]` for all enabled events. |
|
|
639
|
+
| `fail_silently` | Log webhook delivery errors instead of failing the workflow. Default: `true`. |
|
|
640
|
+
| `timeout_seconds` | HTTP timeout for webhook delivery. |
|
|
641
|
+
|
|
642
|
+
Supported notification events:
|
|
643
|
+
|
|
644
|
+
| Event | When it fires |
|
|
645
|
+
| --- | --- |
|
|
646
|
+
| `start` | Workflow starts, with a sanitized config snapshot. |
|
|
647
|
+
| `finish` | Workflow finishes, with exit code. |
|
|
648
|
+
| `local_saved` | A repository mirror was cloned or updated locally. |
|
|
649
|
+
| `remote_saved` | A repository was pushed to a destination. |
|
|
650
|
+
| `watch_check` | Watching mode starts checking a source for updates. |
|
|
651
|
+
| `watch_update` | Watching mode found a changed or first-seen repository and includes the next action. |
|
|
652
|
+
| `watch_none` | Watching mode found no updates and includes the next polling interval. |
|
|
653
|
+
| `error` | A source discovery, clone, marker, create, push, or workflow error happened. |
|
|
654
|
+
|
|
655
|
+
Webhook options:
|
|
656
|
+
|
|
657
|
+
| Key | Meaning |
|
|
658
|
+
| --- | --- |
|
|
659
|
+
| `name` | Human-readable webhook name used in local warnings. |
|
|
660
|
+
| `platform` | `generic`, `discord`, or `telegram`. |
|
|
661
|
+
| `enabled` | Per-webhook enable switch. Default: `true`. |
|
|
662
|
+
| `events` | Per-webhook event filter. Use `["*"]` for all events allowed globally. |
|
|
663
|
+
| `url` / `url_env` | Generic or Discord webhook URL. Prefer `url_env`. |
|
|
664
|
+
| `headers` | Extra headers for generic webhooks. |
|
|
665
|
+
| `username` | Discord webhook username override. |
|
|
666
|
+
| `avatar_url` | Discord webhook avatar override. |
|
|
667
|
+
| `thread_id` | Discord forum/thread selector query parameter. |
|
|
668
|
+
| `bot_token` / `bot_token_env` | Telegram bot token. Prefer `bot_token_env`. |
|
|
669
|
+
| `chat_id` / `chat_id_env` | Telegram chat ID. |
|
|
670
|
+
| `api_base` | Telegram API base URL. Default: `https://api.telegram.org`. |
|
|
671
|
+
| `parse_mode` | Telegram parse mode, for example `HTML`. |
|
|
672
|
+
| `message_thread_id` | Telegram forum topic ID. |
|
|
673
|
+
| `disable_web_page_preview` | Telegram link preview switch. Default: `true`. |
|
|
674
|
+
|
|
675
|
+
Example webhooks:
|
|
676
|
+
|
|
677
|
+
```toml
|
|
678
|
+
[notifications]
|
|
679
|
+
enabled = true
|
|
680
|
+
events = ["*"]
|
|
681
|
+
fail_silently = true
|
|
682
|
+
|
|
683
|
+
[[webhooks]]
|
|
684
|
+
name = "ops-discord"
|
|
685
|
+
platform = "discord"
|
|
686
|
+
url_env = "DISCORD_WEBHOOK_URL"
|
|
687
|
+
events = ["start", "finish", "error", "local_saved", "remote_saved", "watch_update"]
|
|
688
|
+
username = "AGMH"
|
|
689
|
+
|
|
690
|
+
[[webhooks]]
|
|
691
|
+
name = "ops-telegram"
|
|
692
|
+
platform = "telegram"
|
|
693
|
+
bot_token_env = "TELEGRAM_BOT_TOKEN"
|
|
694
|
+
chat_id_env = "TELEGRAM_CHAT_ID"
|
|
695
|
+
events = ["error", "watch_update", "watch_none"]
|
|
696
|
+
parse_mode = "HTML"
|
|
697
|
+
|
|
698
|
+
[[webhooks]]
|
|
699
|
+
name = "ops-generic"
|
|
700
|
+
platform = "generic"
|
|
701
|
+
url_env = "AGMH_WEBHOOK_URL"
|
|
702
|
+
events = ["*"]
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
Webhook notifications never include token values, webhook URLs, Telegram bot
|
|
706
|
+
tokens, or destination push URLs containing credentials. The `start` event
|
|
707
|
+
includes sources, destinations, modes, counts, and other operational settings
|
|
708
|
+
from a sanitized config snapshot.
|
|
709
|
+
|
|
710
|
+
### Tokens
|
|
711
|
+
|
|
712
|
+
Use environment variables. Do not hardcode tokens into config files committed to
|
|
713
|
+
Git.
|
|
714
|
+
|
|
715
|
+
GitHub:
|
|
716
|
+
|
|
717
|
+
```bash
|
|
718
|
+
export GITHUB_TOKEN="..."
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
GitHub destination:
|
|
722
|
+
|
|
723
|
+
```bash
|
|
724
|
+
export GITHUB_DEST_TOKEN="..."
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
GitLab:
|
|
728
|
+
|
|
729
|
+
```bash
|
|
730
|
+
export GITLAB_TOKEN="..."
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
Codeberg:
|
|
734
|
+
|
|
735
|
+
```bash
|
|
736
|
+
export CODEBERG_TOKEN="..."
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
SourceHut:
|
|
740
|
+
|
|
741
|
+
```bash
|
|
742
|
+
export SOURCEHUT_TOKEN="..."
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
Webhooks:
|
|
746
|
+
|
|
747
|
+
```bash
|
|
748
|
+
export DISCORD_WEBHOOK_URL="..."
|
|
749
|
+
export TELEGRAM_BOT_TOKEN="..."
|
|
750
|
+
export TELEGRAM_CHAT_ID="..."
|
|
751
|
+
export AGMH_WEBHOOK_URL="..."
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
You can pass extra tokens from the CLI:
|
|
755
|
+
|
|
756
|
+
```bash
|
|
757
|
+
agmh run \
|
|
758
|
+
--source https://github.com/haltman-io/ \
|
|
759
|
+
--github-token env:GITHUB_TOKEN \
|
|
760
|
+
--source-token gitlab:env:GITLAB_SOURCE_TOKEN \
|
|
761
|
+
--destination https://gitlab.com/haltman-io \
|
|
762
|
+
--destination-token gitlab:env:GITLAB_TOKEN
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
Use `--github-token` as a shortcut for GitHub sources. Use `--source-token platform:...`
|
|
766
|
+
for other source providers, for example `gitlab:env:GITLAB_TOKEN`,
|
|
767
|
+
`forgejo:env:CODEBERG_TOKEN`, `bitbucket:env:BITBUCKET_TOKEN`,
|
|
768
|
+
`bitbucket:you@example.com:env:BITBUCKET_TOKEN`, or `sourcehut:env:SOURCEHUT_TOKEN`.
|
|
769
|
+
|
|
770
|
+
Multiple tokens are allowed. AGMH rotates tokens when a token is rejected, rate
|
|
771
|
+
limited, or temporarily unusable.
|
|
772
|
+
|
|
773
|
+
In TOML arrays, every token entry must be separated by a comma:
|
|
774
|
+
|
|
775
|
+
```toml
|
|
776
|
+
[github]
|
|
777
|
+
tokens = [
|
|
778
|
+
{ env = "GITHUB_TOKEN", name = "github-primary" },
|
|
779
|
+
{ env = "GITHUB_TOKEN_2", name = "github-secondary" },
|
|
780
|
+
]
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
You can also use a named token table:
|
|
784
|
+
|
|
785
|
+
```toml
|
|
786
|
+
[github.tokens]
|
|
787
|
+
github-primary = { env = "GITHUB_TOKEN" }
|
|
788
|
+
github-secondary = "env:GITHUB_TOKEN_2"
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
## Operations
|
|
792
|
+
|
|
793
|
+
### Marker File
|
|
794
|
+
|
|
795
|
+
By default, before pushing to destinations, AGMH writes a marker file into the
|
|
796
|
+
default branch of the local mirror:
|
|
797
|
+
|
|
798
|
+
```txt
|
|
799
|
+
agmh.txt
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
The marker contains:
|
|
803
|
+
|
|
804
|
+
```txt
|
|
805
|
+
source_url=https://github.com/owner/repo
|
|
806
|
+
downloaded_at=2026-06-12T00:00:00Z
|
|
807
|
+
marker_created_at=2026-06-12T00:00:01Z
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
This is intentional. It makes it clear where the backup came from and when the
|
|
811
|
+
backup process created the provenance marker.
|
|
812
|
+
|
|
813
|
+
Disable this repository modification with:
|
|
814
|
+
|
|
815
|
+
```toml
|
|
816
|
+
[backup]
|
|
817
|
+
marker_enabled = false
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
When `marker_enabled` is `false`, AGMH does not create or update the marker file
|
|
821
|
+
and does not create a marker commit before pushing to destinations.
|
|
822
|
+
|
|
823
|
+
### Push Modes
|
|
824
|
+
|
|
825
|
+
`mirror`:
|
|
826
|
+
|
|
827
|
+
Uses `git push --mirror` when the destination accepts every ref.
|
|
828
|
+
|
|
829
|
+
`portable-mirror`:
|
|
830
|
+
|
|
831
|
+
Pushes branches and tags while excluding platform-specific refs such as
|
|
832
|
+
GitHub pull request refs under `refs/pull/*`. This is useful for Codeberg and
|
|
833
|
+
Forgejo, which can reject hidden refs.
|
|
834
|
+
|
|
835
|
+
`all`:
|
|
836
|
+
|
|
837
|
+
Runs `git push --all` and then `git push --tags`.
|
|
838
|
+
|
|
839
|
+
`default`:
|
|
840
|
+
|
|
841
|
+
Pushes only the default branch.
|
|
842
|
+
|
|
843
|
+
Recommended defaults:
|
|
844
|
+
|
|
845
|
+
| Destination | Recommended push mode |
|
|
846
|
+
| --- | --- |
|
|
847
|
+
| GitHub | `mirror`, automatically translated to `portable-mirror` |
|
|
848
|
+
| GitLab | `mirror` |
|
|
849
|
+
| Codeberg/Forgejo | `mirror`, automatically translated to `portable-mirror` |
|
|
850
|
+
| SourceHut | `mirror` over SSH, or `portable-mirror` if hidden refs cause rejection |
|
|
851
|
+
| Bitbucket | `portable-mirror` |
|
|
852
|
+
|
|
853
|
+
### Proxy Usage
|
|
854
|
+
|
|
855
|
+
Use an HTTP or HTTPS proxy:
|
|
856
|
+
|
|
857
|
+
```bash
|
|
858
|
+
agmh run --config agmh.config.toml --proxy http://127.0.0.1:8080 --verbose
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
Use a remote proxy:
|
|
862
|
+
|
|
863
|
+
```bash
|
|
864
|
+
agmh run --config agmh.config.toml --proxy http://83.143.242.45:31343 --verbose
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
If the proxy intercepts TLS and you know what you are doing:
|
|
868
|
+
|
|
869
|
+
```bash
|
|
870
|
+
agmh run --config agmh.config.toml --proxy http://127.0.0.1:8080 --insecure --verbose
|
|
871
|
+
```
|
|
872
|
+
|
|
873
|
+
`--insecure` is equivalent to `-k`:
|
|
874
|
+
|
|
875
|
+
```bash
|
|
876
|
+
agmh run --config agmh.config.toml --proxy http://127.0.0.1:8080 -k
|
|
877
|
+
```
|
|
878
|
+
|
|
879
|
+
This disables certificate verification for API calls and sets
|
|
880
|
+
`GIT_SSL_NO_VERIFY=true` for Git HTTPS operations.
|
|
881
|
+
|
|
882
|
+
#### Segfault.net Proxy Route
|
|
883
|
+
|
|
884
|
+
When GitHub API access is being rate-limited, blocked, or degraded from your
|
|
885
|
+
local network, you can route AGMH through a temporary HTTP proxy exposed from a
|
|
886
|
+
Segfault.net disposable root server.
|
|
887
|
+
|
|
888
|
+
Segfault.net is a project from The Hacker's Choice (THC). THC is an international hacker and IT security research group founded in 1995.
|
|
889
|
+
Segfault.net provides disposable root servers: each SSH login creates a root
|
|
890
|
+
server inside a virtual machine, with a public reverse TCP/UDP port option and
|
|
891
|
+
outbound traffic routed through upstream VPN networks. This makes it useful as a
|
|
892
|
+
temporary network exit path when the GitHub API is unusable from your current
|
|
893
|
+
IP address.
|
|
894
|
+
|
|
895
|
+
Open a Segfault.net shell:
|
|
896
|
+
|
|
897
|
+
```bash
|
|
898
|
+
ssh root@segfault.net
|
|
899
|
+
```
|
|
900
|
+
|
|
901
|
+
The public demo password is:
|
|
902
|
+
|
|
903
|
+
```text
|
|
904
|
+
segfault
|
|
905
|
+
```
|
|
906
|
+
|
|
907
|
+
Inside the Segfault.net server, request a public reverse port:
|
|
908
|
+
|
|
909
|
+
```bash
|
|
910
|
+
curl sf/port
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
Example output:
|
|
914
|
+
|
|
915
|
+
```text
|
|
916
|
+
Tip: Type cat /config/self/reverse_* for details.
|
|
917
|
+
Tip: Type rshell to start listening.
|
|
918
|
+
Tip: Type curl sf/port to assign a new port.
|
|
919
|
+
Your reverse Port is 83.143.242.45 31343 [83.143.242.45:31343]
|
|
920
|
+
```
|
|
921
|
+
|
|
922
|
+
Start an HTTP proxy listener on that assigned port:
|
|
923
|
+
|
|
924
|
+
```bash
|
|
925
|
+
gost -L http://:31343
|
|
926
|
+
```
|
|
927
|
+
|
|
928
|
+
Keep this SSH session open. The proxy exists only while the Segfault.net
|
|
929
|
+
environment and the `gost` process are alive. If the shell is closed or a new
|
|
930
|
+
Segfault.net server is created, request a new port and update the AGMH command.
|
|
931
|
+
|
|
932
|
+
On your workstation, run AGMH through the public proxy:
|
|
933
|
+
|
|
934
|
+
```bash
|
|
935
|
+
agmh run \
|
|
936
|
+
--config agmh.config.toml \
|
|
937
|
+
--verbose \
|
|
938
|
+
--proxy http://83.143.242.45:31343 \
|
|
939
|
+
--insecure
|
|
940
|
+
```
|
|
941
|
+
|
|
942
|
+
Use `--insecure` or `-k` only when TLS verification fails because of the proxy
|
|
943
|
+
path or interception layer. If normal certificate validation works through the
|
|
944
|
+
proxy, remove `--insecure`.
|
|
945
|
+
|
|
946
|
+
Before running a full migration, test the proxy path directly:
|
|
947
|
+
|
|
948
|
+
```bash
|
|
949
|
+
curl -I \
|
|
950
|
+
--proxy http://83.143.242.45:31343 \
|
|
951
|
+
https://api.github.com/users/extencil
|
|
952
|
+
```
|
|
953
|
+
|
|
954
|
+
Then run a short AGMH dry run with retries disabled:
|
|
955
|
+
|
|
956
|
+
```bash
|
|
957
|
+
agmh run \
|
|
958
|
+
--config agmh.config.toml \
|
|
959
|
+
--dry-run \
|
|
960
|
+
--verbose \
|
|
961
|
+
--proxy http://83.143.242.45:31343 \
|
|
962
|
+
--insecure \
|
|
963
|
+
--request-timeout 5 \
|
|
964
|
+
--max-retries 0
|
|
965
|
+
```
|
|
966
|
+
|
|
967
|
+
This proxy path affects AGMH API calls and Git HTTPS operations. It does not
|
|
968
|
+
automatically proxy SSH pushes such as `git@git.sr.ht:~user/repo`, because SSH
|
|
969
|
+
does not use the HTTP proxy settings.
|
|
970
|
+
|
|
971
|
+
### SSH Usage
|
|
972
|
+
|
|
973
|
+
SourceHut is best used with SSH for Git pushes.
|
|
974
|
+
|
|
975
|
+
SourceHut destination:
|
|
976
|
+
|
|
977
|
+
```toml
|
|
978
|
+
[[destinations]]
|
|
979
|
+
url = "https://git.sr.ht/~extencil"
|
|
980
|
+
platform = "sourcehut"
|
|
981
|
+
tokens = [{ env = "SOURCEHUT_TOKEN", name = "sourcehut-primary" }]
|
|
982
|
+
visibility = "mirror"
|
|
983
|
+
push_mode = "mirror"
|
|
984
|
+
push_url_template = "git@git.sr.ht:~{owner}/{repo}"
|
|
985
|
+
```
|
|
986
|
+
|
|
987
|
+
If your SSH key is not a default key, configure it:
|
|
988
|
+
|
|
989
|
+
```toml
|
|
990
|
+
[git]
|
|
991
|
+
ssh_identity_file = "/home/user/.ssh/sourcehut_ed25519"
|
|
992
|
+
ssh_identities_only = true
|
|
993
|
+
ssh_strict_host_key_checking = "accept-new"
|
|
994
|
+
```
|
|
995
|
+
|
|
996
|
+
Or pass it at runtime:
|
|
997
|
+
|
|
998
|
+
```bash
|
|
999
|
+
agmh run --config agmh.config.toml \
|
|
1000
|
+
--ssh-key ~/.ssh/sourcehut_ed25519 \
|
|
1001
|
+
--ssh-strict-host-key-checking accept-new
|
|
1002
|
+
```
|
|
1003
|
+
|
|
1004
|
+
If the key has a passphrase, use `ssh-agent`:
|
|
1005
|
+
|
|
1006
|
+
```bash
|
|
1007
|
+
eval "$(ssh-agent -s)"
|
|
1008
|
+
ssh-add ~/.ssh/sourcehut_ed25519
|
|
1009
|
+
```
|
|
1010
|
+
|
|
1011
|
+
Test before running AGMH:
|
|
1012
|
+
|
|
1013
|
+
```bash
|
|
1014
|
+
ssh -T -i ~/.ssh/sourcehut_ed25519 -o IdentitiesOnly=yes git@git.sr.ht
|
|
1015
|
+
```
|
|
1016
|
+
|
|
1017
|
+
Do not use a private key directly from a Windows-mounted path such as
|
|
1018
|
+
`/mnt/c/Users/...` if OpenSSH reports permissive permissions. Copy it into the
|
|
1019
|
+
Linux filesystem and lock permissions:
|
|
1020
|
+
|
|
1021
|
+
```bash
|
|
1022
|
+
mkdir -p ~/.ssh
|
|
1023
|
+
cp /mnt/c/Users/andre/.ssh/private_id_ed25519 ~/.ssh/sourcehut_ed25519
|
|
1024
|
+
chmod 700 ~/.ssh
|
|
1025
|
+
chmod 600 ~/.ssh/sourcehut_ed25519
|
|
1026
|
+
```
|
|
1027
|
+
|
|
1028
|
+
### Dry Run
|
|
1029
|
+
|
|
1030
|
+
Dry-run still calls platform APIs for discovery. It does not create repos,
|
|
1031
|
+
clone, add marker commits, or push.
|
|
1032
|
+
|
|
1033
|
+
```bash
|
|
1034
|
+
agmh run --config agmh.config.toml --dry-run --verbose
|
|
1035
|
+
```
|
|
1036
|
+
|
|
1037
|
+
Fail fast while debugging network/proxy issues:
|
|
1038
|
+
|
|
1039
|
+
```bash
|
|
1040
|
+
agmh run --config agmh.config.toml \
|
|
1041
|
+
--dry-run \
|
|
1042
|
+
--verbose \
|
|
1043
|
+
--request-timeout 5 \
|
|
1044
|
+
--max-retries 0
|
|
1045
|
+
```
|
|
1046
|
+
|
|
1047
|
+
### Resume and State
|
|
1048
|
+
|
|
1049
|
+
AGMH stores resumable state here:
|
|
1050
|
+
|
|
1051
|
+
```txt
|
|
1052
|
+
.agmh/state.json
|
|
1053
|
+
```
|
|
1054
|
+
|
|
1055
|
+
Logs are stored here:
|
|
1056
|
+
|
|
1057
|
+
```txt
|
|
1058
|
+
.agmh/logs/
|
|
1059
|
+
```
|
|
1060
|
+
|
|
1061
|
+
Show a state summary:
|
|
1062
|
+
|
|
1063
|
+
```bash
|
|
1064
|
+
agmh state --config agmh.config.toml
|
|
1065
|
+
```
|
|
1066
|
+
|
|
1067
|
+
Force completed steps to rerun:
|
|
1068
|
+
|
|
1069
|
+
```bash
|
|
1070
|
+
agmh run --config agmh.config.toml --force
|
|
1071
|
+
```
|
|
1072
|
+
|
|
1073
|
+
Ignore existing state:
|
|
1074
|
+
|
|
1075
|
+
```bash
|
|
1076
|
+
agmh run --config agmh.config.toml --no-resume
|
|
1077
|
+
```
|
|
1078
|
+
|
|
1079
|
+
### Examples
|
|
1080
|
+
|
|
1081
|
+
Discover only:
|
|
1082
|
+
|
|
1083
|
+
```bash
|
|
1084
|
+
agmh discover --sources sources.txt
|
|
1085
|
+
```
|
|
1086
|
+
|
|
1087
|
+
Write discovery output to JSON:
|
|
1088
|
+
|
|
1089
|
+
```bash
|
|
1090
|
+
agmh discover --sources sources.txt --output discovered-repos.json
|
|
1091
|
+
```
|
|
1092
|
+
|
|
1093
|
+
Back up one GitHub org to GitLab:
|
|
1094
|
+
|
|
1095
|
+
```bash
|
|
1096
|
+
export GITHUB_TOKEN="..."
|
|
1097
|
+
export GITLAB_TOKEN="..."
|
|
1098
|
+
|
|
1099
|
+
agmh run \
|
|
1100
|
+
--source https://github.com/haltman-io/ \
|
|
1101
|
+
--destination https://gitlab.com/haltman-io \
|
|
1102
|
+
--destination-token gitlab:env:GITLAB_TOKEN \
|
|
1103
|
+
--github-token env:GITHUB_TOKEN \
|
|
1104
|
+
--verbose
|
|
1105
|
+
```
|
|
1106
|
+
|
|
1107
|
+
Back up one GitLab group to GitHub:
|
|
1108
|
+
|
|
1109
|
+
```bash
|
|
1110
|
+
export GITLAB_SOURCE_TOKEN="..."
|
|
1111
|
+
export GITHUB_DEST_TOKEN="..."
|
|
1112
|
+
|
|
1113
|
+
agmh run \
|
|
1114
|
+
--source https://gitlab.com/haltman-io/ \
|
|
1115
|
+
--source-token gitlab:env:GITLAB_SOURCE_TOKEN \
|
|
1116
|
+
--destination https://github.com/haltman-io-mirror \
|
|
1117
|
+
--destination-token github:env:GITHUB_DEST_TOKEN \
|
|
1118
|
+
--verbose
|
|
1119
|
+
```
|
|
1120
|
+
|
|
1121
|
+
Back up one Codeberg account to GitLab:
|
|
1122
|
+
|
|
1123
|
+
```bash
|
|
1124
|
+
export CODEBERG_SOURCE_TOKEN="..."
|
|
1125
|
+
export GITLAB_TOKEN="..."
|
|
1126
|
+
|
|
1127
|
+
agmh run \
|
|
1128
|
+
--source https://codeberg.org/haltman/ \
|
|
1129
|
+
--source-token forgejo:env:CODEBERG_SOURCE_TOKEN \
|
|
1130
|
+
--destination https://gitlab.com/haltman-codeberg-mirror \
|
|
1131
|
+
--destination-token gitlab:env:GITLAB_TOKEN \
|
|
1132
|
+
--verbose
|
|
1133
|
+
```
|
|
1134
|
+
|
|
1135
|
+
Use Codeberg:
|
|
1136
|
+
|
|
1137
|
+
```bash
|
|
1138
|
+
export CODEBERG_TOKEN="..."
|
|
1139
|
+
|
|
1140
|
+
agmh run \
|
|
1141
|
+
--source https://github.com/haltman-io/ \
|
|
1142
|
+
--destination https://codeberg.org/haltman \
|
|
1143
|
+
--destination-token forgejo:env:CODEBERG_TOKEN \
|
|
1144
|
+
--verbose
|
|
1145
|
+
```
|
|
1146
|
+
|
|
1147
|
+
Use GitHub as a destination:
|
|
1148
|
+
|
|
1149
|
+
```bash
|
|
1150
|
+
export GITHUB_TOKEN="..."
|
|
1151
|
+
export GITHUB_DEST_TOKEN="..."
|
|
1152
|
+
|
|
1153
|
+
agmh run \
|
|
1154
|
+
--source https://github.com/haltman-io/ \
|
|
1155
|
+
--destination https://github.com/haltman-io-mirror \
|
|
1156
|
+
--destination-token github:env:GITHUB_DEST_TOKEN \
|
|
1157
|
+
--github-token env:GITHUB_TOKEN \
|
|
1158
|
+
--verbose
|
|
1159
|
+
```
|
|
1160
|
+
|
|
1161
|
+
Watch sources and mirror updates:
|
|
1162
|
+
|
|
1163
|
+
```bash
|
|
1164
|
+
agmh watching \
|
|
1165
|
+
--config agmh.config.toml \
|
|
1166
|
+
--watch-interval 300 \
|
|
1167
|
+
--watch-action full \
|
|
1168
|
+
--verbose
|
|
1169
|
+
```
|
|
1170
|
+
|
|
1171
|
+
Use SourceHut over SSH:
|
|
1172
|
+
|
|
1173
|
+
```bash
|
|
1174
|
+
export SOURCEHUT_TOKEN="..."
|
|
1175
|
+
|
|
1176
|
+
agmh run \
|
|
1177
|
+
--source https://github.com/haltman-io/ \
|
|
1178
|
+
--destination https://git.sr.ht/~extencil \
|
|
1179
|
+
--destination-token sourcehut:env:SOURCEHUT_TOKEN \
|
|
1180
|
+
--ssh-key ~/.ssh/sourcehut_ed25519 \
|
|
1181
|
+
--ssh-strict-host-key-checking accept-new \
|
|
1182
|
+
--verbose
|
|
1183
|
+
```
|
|
1184
|
+
|
|
1185
|
+
Use a proxy and ignore TLS validation:
|
|
1186
|
+
|
|
1187
|
+
```bash
|
|
1188
|
+
agmh run \
|
|
1189
|
+
--config agmh.config.toml \
|
|
1190
|
+
--proxy http://127.0.0.1:8080 \
|
|
1191
|
+
--insecure \
|
|
1192
|
+
--verbose
|
|
1193
|
+
```
|
|
1194
|
+
|
|
1195
|
+
## Troubleshooting
|
|
1196
|
+
|
|
1197
|
+
### `No module named anti_gh_ms_hysteria`
|
|
1198
|
+
|
|
1199
|
+
You are running from a source checkout without installing the package.
|
|
1200
|
+
|
|
1201
|
+
Fix:
|
|
1202
|
+
|
|
1203
|
+
```bash
|
|
1204
|
+
python -m pip install -e ".[tui]"
|
|
1205
|
+
```
|
|
1206
|
+
|
|
1207
|
+
Or run with:
|
|
1208
|
+
|
|
1209
|
+
```bash
|
|
1210
|
+
PYTHONPATH=src python3 -m anti_gh_ms_hysteria run --help
|
|
1211
|
+
```
|
|
1212
|
+
|
|
1213
|
+
### It hangs on source discovery
|
|
1214
|
+
|
|
1215
|
+
The first source API request is waiting on network, DNS, proxy, or TLS.
|
|
1216
|
+
|
|
1217
|
+
Run:
|
|
1218
|
+
|
|
1219
|
+
```bash
|
|
1220
|
+
agmh run --config agmh.config.toml --dry-run -v --request-timeout 5 --max-retries 0
|
|
1221
|
+
```
|
|
1222
|
+
|
|
1223
|
+
Check the source API directly. For GitHub:
|
|
1224
|
+
|
|
1225
|
+
```bash
|
|
1226
|
+
curl -I --max-time 10 https://api.github.com/users/extencil
|
|
1227
|
+
```
|
|
1228
|
+
|
|
1229
|
+
### Proxy returns HTTP 500
|
|
1230
|
+
|
|
1231
|
+
Use verbose mode. AGMH prints full HTTP response details for failed HTTP
|
|
1232
|
+
responses:
|
|
1233
|
+
|
|
1234
|
+
```bash
|
|
1235
|
+
agmh run --config agmh.config.toml --proxy http://127.0.0.1:8080 --insecure --verbose --max-retries 0
|
|
1236
|
+
```
|
|
1237
|
+
|
|
1238
|
+
If the response body is from the proxy, fix the proxy. If it is from the
|
|
1239
|
+
destination, inspect the API response.
|
|
1240
|
+
|
|
1241
|
+
### GitLab rejects `.github`
|
|
1242
|
+
|
|
1243
|
+
GitLab does not allow project paths starting with a dot. AGMH maps `.github` to
|
|
1244
|
+
`dot-github` for GitLab destinations.
|
|
1245
|
+
|
|
1246
|
+
If old state points to the wrong path, AGMH rechecks create when destination
|
|
1247
|
+
path mapping changes.
|
|
1248
|
+
|
|
1249
|
+
### Codeberg rejects `refs/pull/*`
|
|
1250
|
+
|
|
1251
|
+
Codeberg/Forgejo can reject hidden GitHub pull request refs:
|
|
1252
|
+
|
|
1253
|
+
```txt
|
|
1254
|
+
deny updating a hidden ref
|
|
1255
|
+
```
|
|
1256
|
+
|
|
1257
|
+
AGMH maps Forgejo/Codeberg `mirror` mode to `portable-mirror`, which pushes
|
|
1258
|
+
branches and tags without GitHub `refs/pull/*`.
|
|
1259
|
+
|
|
1260
|
+
### `gnutls_handshake() failed`
|
|
1261
|
+
|
|
1262
|
+
This is usually a transient network, proxy, or TLS interruption during Git
|
|
1263
|
+
push. AGMH retries this class of Git network failure.
|
|
1264
|
+
|
|
1265
|
+
You can also reduce concurrency outside AGMH and rerun. Failed state entries are
|
|
1266
|
+
not marked `done`, so reruns continue from the failed push.
|
|
1267
|
+
|
|
1268
|
+
### SourceHut says `Permission denied (publickey,keyboard-interactive)`
|
|
1269
|
+
|
|
1270
|
+
Your Git subprocess did not use the correct SSH key, or the key was rejected.
|
|
1271
|
+
|
|
1272
|
+
Test as the same user, without `sudo`:
|
|
1273
|
+
|
|
1274
|
+
```bash
|
|
1275
|
+
ssh -T -i ~/.ssh/sourcehut_ed25519 -o IdentitiesOnly=yes git@git.sr.ht
|
|
1276
|
+
```
|
|
1277
|
+
|
|
1278
|
+
Then run:
|
|
1279
|
+
|
|
1280
|
+
```bash
|
|
1281
|
+
agmh run --config agmh.config.toml --ssh-key ~/.ssh/sourcehut_ed25519
|
|
1282
|
+
```
|
|
1283
|
+
|
|
1284
|
+
### OpenSSH says `UNPROTECTED PRIVATE KEY FILE`
|
|
1285
|
+
|
|
1286
|
+
Private key permissions are too open. Fix:
|
|
1287
|
+
|
|
1288
|
+
```bash
|
|
1289
|
+
chmod 700 ~/.ssh
|
|
1290
|
+
chmod 600 ~/.ssh/sourcehut_ed25519
|
|
1291
|
+
```
|
|
1292
|
+
|
|
1293
|
+
If the key is under `/mnt/c`, copy it to the Linux filesystem first.
|
|
1294
|
+
|
|
1295
|
+
### `HTTP 401`
|
|
1296
|
+
|
|
1297
|
+
The token is invalid, expired, missing required scopes, or belongs to the wrong
|
|
1298
|
+
account.
|
|
1299
|
+
|
|
1300
|
+
Check the environment:
|
|
1301
|
+
|
|
1302
|
+
```bash
|
|
1303
|
+
printenv GITHUB_TOKEN
|
|
1304
|
+
printenv GITHUB_DEST_TOKEN
|
|
1305
|
+
printenv GITLAB_TOKEN
|
|
1306
|
+
printenv CODEBERG_TOKEN
|
|
1307
|
+
printenv SOURCEHUT_TOKEN
|
|
1308
|
+
printenv DISCORD_WEBHOOK_URL
|
|
1309
|
+
printenv TELEGRAM_BOT_TOKEN
|
|
1310
|
+
printenv TELEGRAM_CHAT_ID
|
|
1311
|
+
```
|
|
1312
|
+
|
|
1313
|
+
Do not paste tokens into logs or issues.
|
|
1314
|
+
|
|
1315
|
+
### `HTTP 403` or `HTTP 429`
|
|
1316
|
+
|
|
1317
|
+
You hit rate limits or permission limits. AGMH rotates available tokens and, if
|
|
1318
|
+
configured, waits for the reset window.
|
|
1319
|
+
|
|
1320
|
+
Use more tokens:
|
|
1321
|
+
|
|
1322
|
+
```toml
|
|
1323
|
+
[github]
|
|
1324
|
+
tokens = [
|
|
1325
|
+
{ env = "GITHUB_TOKEN", name = "github-primary" },
|
|
1326
|
+
{ env = "GITHUB_TOKEN_2", name = "github-secondary" },
|
|
1327
|
+
]
|
|
1328
|
+
```
|
|
1329
|
+
|
|
1330
|
+
### Repository already exists
|
|
1331
|
+
|
|
1332
|
+
By default, `allow_existing = true` lets AGMH continue and push into an existing
|
|
1333
|
+
destination repository.
|
|
1334
|
+
|
|
1335
|
+
### Existing state skips something you want to retry
|
|
1336
|
+
|
|
1337
|
+
Use:
|
|
1338
|
+
|
|
1339
|
+
```bash
|
|
1340
|
+
agmh run --config agmh.config.toml --force
|
|
1341
|
+
```
|
|
1342
|
+
|
|
1343
|
+
or edit `.agmh/state.json` carefully.
|
|
1344
|
+
|
|
1345
|
+
## Security Notes
|
|
1346
|
+
|
|
1347
|
+
- Prefer environment variables for secrets.
|
|
1348
|
+
- Do not commit `.agmh/`, `backups/`, `agmh.config.toml`, `sources.txt`, `destinations.txt`, or private config files.
|
|
1349
|
+
- Logs scrub configured token secrets.
|
|
1350
|
+
- If a token was ever printed before scrubbing existed, rotate it.
|
|
1351
|
+
- `--insecure` is useful for debugging or intercepting proxies, but it disables TLS verification.
|
|
1352
|
+
- SSH private keys must be readable only by your user.
|
|
1353
|
+
|
|
1354
|
+
## Repository Layout
|
|
1355
|
+
|
|
1356
|
+
```txt
|
|
1357
|
+
src/anti_gh_ms_hysteria/
|
|
1358
|
+
cli.py CLI entrypoint
|
|
1359
|
+
runner.py workflow orchestration
|
|
1360
|
+
config.py TOML and CLI config loading
|
|
1361
|
+
git_ops.py clone, marker commit, push operations
|
|
1362
|
+
http.py API client, retries, proxy, TLS handling
|
|
1363
|
+
state.py resumable state file
|
|
1364
|
+
sources/ GitHub, GitLab, Forgejo, Bitbucket, SourceHut discovery adapters
|
|
1365
|
+
destinations/ GitHub, GitLab, Forgejo, Bitbucket, SourceHut adapters
|
|
1366
|
+
tests/
|
|
1367
|
+
test_config_and_utils.py
|
|
1368
|
+
.github/workflows/
|
|
1369
|
+
ci.yml tests and package checks
|
|
1370
|
+
release-please.yml release PR, changelog, tag, GitHub Release, PyPI publish
|
|
1371
|
+
publish-pypi.yml manual PyPI publish fallback through Trusted Publishing
|
|
1372
|
+
publish-testpypi.yml manual TestPyPI publish through Trusted Publishing
|
|
1373
|
+
pyproject.toml package metadata and build configuration
|
|
1374
|
+
MANIFEST.in source distribution file list
|
|
1375
|
+
release-please-config.json
|
|
1376
|
+
.release-please-manifest.json
|
|
1377
|
+
```
|
|
1378
|
+
|
|
1379
|
+
## Development
|
|
1380
|
+
|
|
1381
|
+
Install:
|
|
1382
|
+
|
|
1383
|
+
```bash
|
|
1384
|
+
python -m pip install -e ".[tui]"
|
|
1385
|
+
```
|
|
1386
|
+
|
|
1387
|
+
Run tests:
|
|
1388
|
+
|
|
1389
|
+
```bash
|
|
1390
|
+
PYTHONPATH=src python -m unittest discover -s tests -v
|
|
1391
|
+
```
|
|
1392
|
+
|
|
1393
|
+
Compile check:
|
|
1394
|
+
|
|
1395
|
+
```bash
|
|
1396
|
+
PYTHONPATH=src python -m compileall -q src tests
|
|
1397
|
+
```
|
|
1398
|
+
|
|
1399
|
+
CLI smoke test:
|
|
1400
|
+
|
|
1401
|
+
```bash
|
|
1402
|
+
PYTHONPATH=src python -m anti_gh_ms_hysteria run --help
|
|
1403
|
+
```
|
|
1404
|
+
|
|
1405
|
+
Package check:
|
|
1406
|
+
|
|
1407
|
+
```bash
|
|
1408
|
+
python -m build
|
|
1409
|
+
python -m twine check --strict dist/*
|
|
1410
|
+
```
|
|
1411
|
+
|
|
1412
|
+
## Project Background
|
|
1413
|
+
|
|
1414
|
+
### Risk Model
|
|
1415
|
+
|
|
1416
|
+
This tool exists because important work should not depend on a single platform
|
|
1417
|
+
remaining available, cooperative, or operational forever.
|
|
1418
|
+
|
|
1419
|
+
AGMH was built after past platform access incidents made us reassess the risk of
|
|
1420
|
+
being locked out of a large technology platform without enough time to preserve
|
|
1421
|
+
our work or coordinate with the people closest to a project. The risk is similar
|
|
1422
|
+
to an abrupt offboarding process where access is disabled so quickly that a
|
|
1423
|
+
person cannot even send a final email to close colleagues.
|
|
1424
|
+
|
|
1425
|
+
For software projects, that access risk is broader than any single account or
|
|
1426
|
+
provider. A team can lose continuity because of enforcement actions, sanctions,
|
|
1427
|
+
provider policy changes, operational outages, service degradation, acquisition
|
|
1428
|
+
risk, or a platform eventually disappearing. The critical issue is
|
|
1429
|
+
centralization: if repository history, issues, branches, tags, release metadata,
|
|
1430
|
+
and collaboration context all live in one place, a disruption in that place can
|
|
1431
|
+
have a large impact on the surrounding ecosystem.
|
|
1432
|
+
|
|
1433
|
+
This is not a personal fight with GitHub. It is a risk-management and business
|
|
1434
|
+
continuity problem. AGMH provides a practical way to keep local mirrors, move
|
|
1435
|
+
repositories between forges, preserve Git history, and maintain high
|
|
1436
|
+
availability of project information when a centralized platform becomes
|
|
1437
|
+
unavailable or unsuitable.
|
|
1438
|
+
|
|
1439
|
+
AGMH is produced by **Haltman.IO** and released freely so others can protect
|
|
1440
|
+
their own work.
|
|
1441
|
+
|
|
1442
|
+
This tool has already been used to back up repositories from `@extencil` and
|
|
1443
|
+
`@haltman-io` to GitLab, Codeberg, and SourceHut successfully.
|
|
1444
|
+
|
|
1445
|
+
### Continuity Incident Timeline
|
|
1446
|
+
|
|
1447
|
+
AGMH was used to move the work of `@extencil` and `@haltman-io` away from a
|
|
1448
|
+
single forge dependency and into independent mirrors:
|
|
1449
|
+
|
|
1450
|
+
- GitLab: https://gitlab.com/extencil
|
|
1451
|
+
- Codeberg: https://codeberg.org/extencil
|
|
1452
|
+
- SourceHut: https://git.sr.ht/~extencil
|
|
1453
|
+
- GitLab: https://gitlab.com/haltman-io
|
|
1454
|
+
- Codeberg: https://codeberg.org/haltman
|
|
1455
|
+
|
|
1456
|
+
The account access incident that reinforced this risk model followed this
|
|
1457
|
+
timeline:
|
|
1458
|
+
|
|
1459
|
+
| Event | Time |
|
|
1460
|
+
| --- | --- |
|
|
1461
|
+
| Account suspended/banned by platform enforcement | Monday, 2026-06-08, around 04:00 `America/Sao_Paulo` (`UTC-03:00`), approximately 2026-06-08 07:00 UTC |
|
|
1462
|
+
| Review ticket opened | 2026-06-08 07:59 UTC, 2026-06-08 04:59 `America/Sao_Paulo` |
|
|
1463
|
+
| Priority follow-up sent by our side | 2026-06-11 16:47 UTC, 2026-06-11 13:47 `America/Sao_Paulo` |
|
|
1464
|
+
| Case reviewed and reverted by GitHub | 2026-06-12 11:19 UTC, 2026-06-12 08:19 `America/Sao_Paulo` |
|
|
1465
|
+
|
|
1466
|
+
The incident would have been significantly more damaging without continuity
|
|
1467
|
+
procedures already in place. When Haltman.IO created its GitHub organization,
|
|
1468
|
+
other Haltman.IO members were assigned as organization owners. That avoided a
|
|
1469
|
+
complete lockout scenario.
|
|
1470
|
+
|
|
1471
|
+
Someone who is not part of an organization, or who is not an organization owner
|
|
1472
|
+
or repository administrator, cannot reliably operate that organization. They
|
|
1473
|
+
cannot recover organization-level access, manage owners and teams, change
|
|
1474
|
+
organization settings, manage repository permissions, configure secrets,
|
|
1475
|
+
webhooks, deploy keys, branch protection, or security settings, create or
|
|
1476
|
+
transfer repositories, publish releases, or consistently triage and merge work
|
|
1477
|
+
across the organization.
|
|
1478
|
+
|
|
1479
|
+
This matters because the affected work is operational, not cosmetic. Haltman.IO
|
|
1480
|
+
voluntarily sustains email-forwarding infrastructure associated with The
|
|
1481
|
+
Hacker's Choice, in collaboration with Phrack, Eurocompton, team-teso, Antisec,
|
|
1482
|
+
pwnbuffer, and other groups connected to cybersecurity research. A complete
|
|
1483
|
+
organization lockout would have affected the ability to manage the many
|
|
1484
|
+
repositories behind that email-forwarding stack.
|
|
1485
|
+
|
|
1486
|
+
That impact is not about minor product changes or visual polish. It affects the
|
|
1487
|
+
ability to coordinate proper vulnerability disclosure for people who self-host
|
|
1488
|
+
the email-forwarding stack, publish fixes, document operational changes, and
|
|
1489
|
+
credit researchers correctly when they report vulnerabilities.
|
|
1490
|
+
|
|
1491
|
+
It also affects our internal service expectations. There is no legal or
|
|
1492
|
+
commercial SLA: we do not sell this work, and the output is public work for the
|
|
1493
|
+
public. Still, we prefer to respond to issues and pull requests quickly. Acting
|
|
1494
|
+
like a large platform with effectively unbounded response times is neither our
|
|
1495
|
+
role nor consistent with Haltman.IO's operating values.
|
|
1496
|
+
|
|
1497
|
+
### Haltman.IO
|
|
1498
|
+
|
|
1499
|
+
Haltman is a group of Brazilian hackers. Friends for over a decade, building
|
|
1500
|
+
public, privacy-first infrastructure and free software.
|
|
1501
|
+
|
|
1502
|
+
We build, break, audit, and publish.
|
|
1503
|
+
|
|
1504
|
+
We do not sell platforms. We do not run franchises.
|
|
1505
|
+
|
|
1506
|
+
We do not ask permission.
|
|
1507
|
+
|
|
1508
|
+
Haltman.IO links:
|
|
1509
|
+
|
|
1510
|
+
- Website: https://haltman.io/
|
|
1511
|
+
- Alternate website: https://haltman.org/
|
|
1512
|
+
- Contact: root@haltman.io, root@haltman.org
|
|
1513
|
+
- Join Haltman.IO: https://haltman.io/join/
|
|
1514
|
+
- Telegram group: https://t.me/haltman_group
|
|
1515
|
+
|
|
1516
|
+
### Operating Values
|
|
1517
|
+
|
|
1518
|
+
| Doctrine | Value |
|
|
1519
|
+
| --- | --- |
|
|
1520
|
+
| 01 Independence | We answer to no one. No board. No investors. No sponsors. Our independence guarantees our freedom. |
|
|
1521
|
+
| 02 Transparency | Every tool is open source. Every decision is visible. No back rooms. No hidden agendas. |
|
|
1522
|
+
| 03 Public Output | We publish. We document. We release. Our work speaks for itself. Not our marketing. |
|
|
1523
|
+
| 04 No Hierarchy | Flat structure. No leaders. No bosses. No titles. No org charts. Respect is earned by output. |
|
|
1524
|
+
| 05 Mutual Aid | When one of us needs help, the others show up. No invoices. No politics. Just engineering. |
|
|
1525
|
+
| 06 No Compromise | We do not water down our principles for comfort, profit, or acceptance. Those who trade freedom for security end up with neither. |
|
|
1526
|
+
|
|
1527
|
+
## Project Files
|
|
1528
|
+
|
|
1529
|
+
- [CHANGELOG.md](CHANGELOG.md): release notes and pending changes.
|
|
1530
|
+
- [SECURITY.md](SECURITY.md): private vulnerability reporting policy.
|
|
1531
|
+
- [CONTRIBUTING.md](CONTRIBUTING.md): development setup and contribution checks.
|
|
1532
|
+
- [RELEASING.md](RELEASING.md): Release Please and PyPI publishing process.
|
|
1533
|
+
- [SUPPORT.md](SUPPORT.md): support paths for bugs, questions, and security reports.
|
|
1534
|
+
- [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md): collaboration expectations.
|
|
1535
|
+
- [MAINTAINERS.md](MAINTAINERS.md): maintainer and security contact information.
|
|
1536
|
+
|
|
1537
|
+
## References
|
|
1538
|
+
|
|
1539
|
+
- Unlicense: https://unlicense.org/
|
|
1540
|
+
- GitHub personal access tokens: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens
|
|
1541
|
+
- GitHub repositories REST API: https://docs.github.com/en/rest/repos/repos
|
|
1542
|
+
- GitHub REST API rate limits: https://docs.github.com/rest/rate-limit/rate-limit
|
|
1543
|
+
- GitLab personal access tokens: https://docs.gitlab.com/user/profile/personal_access_tokens/
|
|
1544
|
+
- GitLab Projects API: https://docs.gitlab.com/api/projects/
|
|
1545
|
+
- GitLab Groups API: https://docs.gitlab.com/api/groups/
|
|
1546
|
+
- Codeberg access tokens: https://docs.codeberg.org/advanced/access-token/
|
|
1547
|
+
- Forgejo API usage: https://forgejo.org/docs/latest/user/api-usage/
|
|
1548
|
+
- Bitbucket repositories API: https://developer.atlassian.com/cloud/bitbucket/rest/api-group-repositories/
|
|
1549
|
+
- SourceHut git.sr.ht GraphQL API docs: https://docs.sourcehut.org/git.sr.ht/
|
|
1550
|
+
- SourceHut GraphQL API docs: https://docs.sourcehut.org/
|
|
1551
|
+
- SourceHut: https://sourcehut.org/
|
|
1552
|
+
- Discord Webhook Resource: https://docs.discord.com/developers/resources/webhook
|
|
1553
|
+
- Telegram Bot API: https://core.telegram.org/bots/api
|
|
1554
|
+
- The Hacker's Choice: https://www.thc.org/
|
|
1555
|
+
- Segfault.net disposable root servers: https://www.thc.org/segfault/
|
|
1556
|
+
- Segfault.net service notes: https://www.thc.org/segfault/free/
|
|
1557
|
+
- Segfault.net Server Centre source: https://github.com/hackerschoice/segfault
|
|
1558
|
+
|
|
1559
|
+
## License
|
|
1560
|
+
|
|
1561
|
+
AGMH is released under the **Unlicense**.
|
|
1562
|
+
|
|
1563
|
+
This means the project is dedicated to the public domain to the fullest extent
|
|
1564
|
+
possible. See:
|
|
1565
|
+
|
|
1566
|
+
- https://unlicense.org/
|
|
1567
|
+
- [LICENSE](LICENSE)
|
|
1568
|
+
|
|
1569
|
+
## Author
|
|
1570
|
+
|
|
1571
|
+
Author: **extencil** <extencil@segfault.net>
|
|
1572
|
+
|
|
1573
|
+
Repository: [haltman-io/agmh](https://github.com/haltman-io/agmh)
|
|
1574
|
+
|
|
1575
|
+
Produced by **Haltman.IO**.
|