OXYGENHUB
# 5ef6cf72cd19 oxygenhub / docs / backups.md
9.5 KiB · Markdown · last changed Backups: a restore brings back the records, and release assets travel 22 days ago renderedsource blame history raw
1
# Backups: per site, organisation, user and repository
2
3
Status: built 2026-10-13/14 (oxygenhub#169): user and organisation
4
settings, the hourly and daily schedule with "back up now" and TEST,
5
directory destinations and keyless GCS destinations, every repository's
6
and wiki's store byte for byte, the records as JSON, retention, failure
7
notices, and restore (`oxygenhub admin backup run|restore`). Since
8
2026-10-14 (oxygenhub#175) a restore brings back the records too (§7) and
9
release assets travel with the stores. Next: Admin › Backups,
10
per-repository settings, S3.
11
12
## 1. Why
13
14
A forge is where people's work lives. Before Andy moves his GitHub
15
repositories here, and before the hub moves to GCP, every owner must be
16
able to say "my repositories are copied, every hour, to a place I
17
control, and I can bring them back". GitHub leaves this to its users
18
(clone everything somewhere, and lose the issues and reviews); GitLab
19
backs up the whole instance or nothing. Here an owner backs up their own
20
repositories, with everything around them, to their own bucket, without a
21
key file anywhere.
22
23
## 2. What a backup holds
24
25
For each repository the owner has (a user's own, or an organisation's):
26
27
- **Its objects:** the store's content-addressed objects (changes,
28
  blobs, chunks, states, identities, signatures, seals, policies). An
29
  object never changes once written, so a run sends only the objects the
30
  destination does not already have: the first run copies everything,
31
  later ones only what arrived since. Nothing is re-encoded, so a
32
  restored repository is byte for byte the original, every signature
33
  still verifying.
34
- **Its refs at that moment:** bookmarks, tags and the trust policy's
35
  serial, as a small manifest per run. Each run's manifest is kept, so a
36
  restore can go back to any run that is still kept.
37
- **Its hub records, as JSON:** issues and their comments and events;
38
  proposals, their reviews, threads and approvals; labels, milestones,
39
  projects; releases and their assets (assets are blobs, sent like
40
  objects); the wiki (a repository of its own, backed up with it); and
41
  repository settings and webhooks. Secret values (webhook secrets, Flow
42
  secrets, tokens) are never written, only their names, so a restored
43
  repository asks for them again.
44
- **Its owner:** for an organisation, its members, teams and their
45
  permissions; for a user, their profile and linked identity keys
46
  (public keys only).
47
48
Not included: Flow runs' logs and artifacts, which belong to the run, not
49
the repository (a later choice); caches; anything of another owner.
50
51
## 3. Where it goes
52
53
A **destination** is one of:
54
55
- **A GCS bucket, keylessly.** The hub is already an OIDC issuer (spec
56
  §13.5). Built: the token lives 10 minutes, its audience is the
57
  provider's name over https (Google's default), the exchange asks for
58
  `devstorage.read_write`, and the access token is exchanged again five
59
  minutes before it runs out. A refusal says what to grant. For a backup it mints a short-lived token with
60
  `sub = backup:OWNER` and audience the owner's workload identity
61
  provider. Google's STS exchanges it for a short-lived access token,
62
  which writes to the bucket. Nothing secret is stored: no service
63
  account key exists (Andy's organisation blocks them anyway), and if the
64
  hub leaks a token it lasts minutes and is scoped to that owner's
65
  prefix. Setup is §6.
66
- **A directory on the hub's host**, for the site administrator: the
67
  directories an owner may choose are listed in `hub.toml`
68
  (`[backups] directories = ["/srv/backups"]`), so nobody can write
69
  anywhere else on the host. Each owner gets `DIR/OWNER/`.
70
- **Later:** S3 (the same keyless exchange with AWS STS), and another hub.
71
72
Layout at a destination (`PREFIX` = the owner's chosen prefix):
73
74
```
75
PREFIX/objects/<repository id>/<object hash>    content-addressed, written once
76
PREFIX/runs/<run time>/<repository id>.json     the refs at that run
77
PREFIX/runs/<run time>/records/<repository id>.json
78
PREFIX/runs/<run time>/owner.json
79
PREFIX/runs/<run time>/DONE                     written last: the run is whole
80
```
81
82
A run without `DONE` was interrupted; a restore ignores it.
83
84
## 4. When, how long and how it went
85
86
- **How often:** hourly to begin with (off, hourly, daily); finer
87
  later. The hub's scheduler runs each due backup, at most two at once,
88
  at low priority, spread across the hour so owners' runs do not
89
  collide.
90
- **Back up now:** a button, and `POST /api/v1/{user|orgs/NAME}/backups/run`.
91
- **Kept:** runs' manifests for 30 days by default (7, 30 or 90).
92
  Objects no kept run refers to are pruned weekly. With GCS, the
93
  bucket's own versioning and lifecycle rules work too.
94
- **Status:** the last run's start, length, objects and bytes sent,
95
  repositories, and outcome (ok, partial with the repositories that
96
  failed and why, or failed with the reason, such as "GCS refused the
97
  token: check the provider's attribute condition"). The last 50 runs
98
  are listed. A failure notifies the owner (an organisation's owners)
99
  in the inbox, once per kind of failure until it succeeds again.
100
101
## 5. Who sets it, and where
102
103
- **A user:** Settings › Backups, for the repositories they own.
104
- **An organisation's owners:** the organisation's Settings › Backups.
105
- **The site administrator:** Admin › Backups, a table of every owner's
106
  backup (destination, schedule, last run, outcome) and the site's own
107
  backup. They may not read an owner's bucket, only see that it ran.
108
- **A repository** (later): opt a repository out of its owner's backup,
109
  or give it its own schedule.
110
111
The API mirrors the pages: `GET/PUT /api/v1/user/backups`,
112
`GET/PUT /api/v1/orgs/{name}/backups`, the run endpoint above, and
113
`GET …/backups/runs`. `oh backup status|run|restore` follows.
114
115
## 6. Setting up a GCS destination (for Andy, by hand)
116
117
Once per GCP project:
118
119
```
120
PROJECT=doomsdayone-oxy
121
NUM=$(gcloud projects describe $PROJECT --format='value(projectNumber)')
122
gcloud iam workload-identity-pools create oxygenhub \
123
  --project=$PROJECT --location=global --display-name="OxygenHub"
124
gcloud iam workload-identity-pools providers create-oidc oxygenhub-io \
125
  --project=$PROJECT --location=global --workload-identity-pool=oxygenhub \
126
  --issuer-uri=https://oxygenhub.io \
127
  --attribute-mapping=google.subject=assertion.sub \
128
  --attribute-condition="assertion.sub.startsWith('backup:')"
129
```
130
131
Once per owner and bucket (here `andymccall`):
132
133
```
134
gcloud storage buckets create gs://andymccall-oxygen-backup \
135
  --project=$PROJECT --location=europe-west2 \
136
  --uniform-bucket-level-access --public-access-prevention
137
gcloud storage buckets update gs://andymccall-oxygen-backup --versioning
138
gcloud storage buckets add-iam-policy-binding gs://andymccall-oxygen-backup \
139
  --role=roles/storage.objectAdmin \
140
  --member="principal://iam.googleapis.com/projects/$NUM/locations/global/workloadIdentityPools/oxygenhub/subject/backup:andymccall"
141
```
142
143
**Done for doomsdayone-oxy on 2026-10-13:** the pool `oxygenhub`, the
144
provider `oxygenhub-io` and the buckets `andymccall-oxygen-backup` and
145
`doomsdayone-oxygen-backup`, each granted to its own `backup:` subject.
146
147
Then, in Settings › Backups: destination GCS, bucket
148
`andymccall-oxygen-backup`, provider
149
`//iam.googleapis.com/projects/NUM/locations/global/workloadIdentityPools/oxygenhub/providers/oxygenhub-io`,
150
and TEST, which exchanges a token and writes and deletes a probe object.
151
An organisation does the same with `backup:ORG`. Only that principal can
152
write the bucket; the hub never holds a key.
153
154
## 7. Restore
155
156
**As built (oxygenhub#175).** `oxygenhub admin backup restore --from DIR
157
[--run AT]` into a hub that lacks the repositories: each store and wiki
158
byte for byte (every file checked against its hash), then the
159
repository's row with its settings as they were (visibility, default
160
bookmark, Flow and Diffusion settings and the rest), then its records.
161
Each run's records carry `_names`, the username, organisation or
162
`org/team` behind every account, team and organisation id their rows
163
point at, so the restore finds them again by name on any hub. Tables go
164
in parents first; every row gets a new id and every reference follows
165
it. A reference to an account this hub lacks is left blank where the
166
column allows it (the author's name stays in the row) and the row left
167
out where it does not (an assignment, a review request); the restore
168
says how many. Webhooks come back without their secrets, each named to be
169
entered again. Release assets, which live beside the stores rather than
170
in them, are sent once each as blobs and written back checked against
171
their hash. Search is rebuilt for what came back. Left out on purpose:
172
inboxes (notifications), webhook deliveries, runners' declines.
173
174
The design as first written:
175
176
`oxygenhub admin restore --from gs://BUCKET/PREFIX [--run TIME] [--repo NAME]`
177
(or a directory) on any hub:
178
179
1. reads the run's manifests (the latest whole run by default);
180
2. creates each repository, or refuses an existing one unless `--into`
181
   names a new owner;
182
3. copies its objects, verifying every hash, and sets its refs;
183
4. imports its records, then reports what needs re-entering (secret
184
   values, webhook secrets).
185
186
A restore into a fresh hub, compared with the original, is the end-to-end
187
test (BK2), as is restoring one repository from a run before a bad push.
188
189
## 8. Order of work
190
191
1. **BK2:** settings for users and organisations, the scheduler and
192
   "back up now", objects, manifests and records to a directory
193
   destination, status and notifications, and restore with its
194
   end-to-end test.
195
2. **BK3:** the GCS destination with the keyless exchange (tested against
196
   a fake STS and a fake GCS), and TEST.
197
3. Admin › Backups, then per-repository settings, then S3.