@open-xchange/relic
v0.2.3
Published
GitLab group backup CLI for disaster recovery
Maintainers
Keywords
Readme
Relic
GitLab group backup CLI for disaster recovery. Published as @open-xchange/relic. Backs up all git repositories (mirror clones) and project/group settings from a GitLab instance.
Features
- Mirror clones of all repositories (
--mirror) withgc.auto=0to preserve force-pushed history - Wiki repository backup (separate
.wiki.gitmirrors) - Empty repositories are detected (
empty_repo: true) and the git mirror step is skipped — settings are still backed up - Project settings: CI variables, protected branches/tags, webhooks, labels, milestones, deploy keys/tokens, pipeline schedules, approval rules, integrations, releases
- Optional issue + issue-note backup (per-issue notes saved to
issue_notes/<iid>.json); both default to off because they can be large - Group settings: CI variables, webhooks, labels, milestones, deploy tokens
- Incremental backups — hard-link copies previous mirror, then
git fetch --prune. When the target dir already holds a bare mirror (e.g. an in-place per-project refresh), the copy step is skipped andgit fetchruns against it directly - In-place per-project refresh (
--project … --update-latest/--update <run>) — refresh a single project (looked up with one API call, no group discovery) inside an existing run dir without touching thelatestsymlink or the other projects on disk. The project's fresh result is merged into the existing manifest. Per-file JSON writes are atomic (rename(2)-based) so concurrent readers never see a half-written file - Recursive group/project discovery with glob-based exclusion patterns
- Hierarchical rate limiting (global + per-endpoint token buckets)
- Configurable concurrency (separate pools for git and API operations)
- Backup verification (
git fsck+ JSON validation) - Retention-based pruning of old runs
- YAML config with environment variable substitution
.envfiles in the working directory are auto-loaded by the CLI
Requirements
- Node.js >= 22
- Git
- pnpm
Installation
pnpm install
pnpm buildThe CLI is available at dist/cli/index.js, or via pnpm dev during development:
# development
pnpm dev -- backup --help
# after build
node dist/cli/index.js backup --helpConfiguration
Copy the example config and edit it:
cp config/relic.example.yaml relic.yamlgitlab_url: "https://gitlab.com"
gitlab_token: "${RELIC_GITLAB_TOKEN}" # reads from environment variable
groups:
- "openxchange"
clone_method: "https" # "https" or "ssh"
backup_dir: "/data/backups/gitlab"
concurrency:
git_operations: 4 # parallel git clone/fetch
api_requests: 10 # parallel API calls
backup_scope:
repository: true
wiki: true
ci_variables: true
protected_branches: true
protected_tags: true
webhooks: true
labels: true
milestones: true
deploy_keys: true
deploy_tokens: true
pipeline_schedules: true
approval_rules: true # requires Premium/Ultimate
integrations: true
releases: true
issues: false # can be large; off by default
issue_notes: false # comments on issues; requires issues: true
group_backup_scope:
ci_variables: true
webhooks: true # requires Premium/Ultimate
labels: true
milestones: true
deploy_tokens: true
exclude: [] # e.g. ["openxchange/archived-*"]
retention_count: 0 # 0 = keep all runs
log_level: "info"Set your GitLab token:
export RELIC_GITLAB_TOKEN="<your-gitlab-personal-access-token>"The token needs read_api scope. For backing up CI variables, api scope is required.
Usage
List discovered groups and projects
relic list -c relic.yaml
relic list -c relic.yaml --format json
relic list -c relic.yaml --format csvRun a backup
# full backup
relic backup -c relic.yaml
# dry run (discover only, no writes)
relic backup -c relic.yaml --dry-run
# single project
relic backup -c relic.yaml --project openxchange/some-project
# refresh a single project inside an existing run, in place
# (skips creating a new run dir, doesn't touch the `latest` symlink,
# merges the project's fresh result into the existing manifest)
relic backup -c relic.yaml --project openxchange/some-project --update-latest
relic backup -c relic.yaml --project openxchange/some-project --update 2026-04-28T02-00-00-000Z
# repos only (skip API settings)
relic backup -c relic.yaml --repo-only
# settings only (skip git clones)
relic backup -c relic.yaml --settings-only
# verbose logging
relic backup -c relic.yaml -vVerify a backup
# verify latest run (git fsck + JSON validation)
relic verify -c relic.yaml
# verify a specific run
relic verify -c relic.yaml --run 2026-03-04T08-30-00Z
# only check repos or settings
relic verify -c relic.yaml --check-repos
relic verify -c relic.yaml --check-settingsBackup Directory Layout
/data/backups/gitlab/
├── runs/
│ └── 2026-03-04T08-30-00Z/
│ ├── manifest.json
│ ├── groups/
│ │ └── openxchange/
│ │ ├── _settings/
│ │ └── subgroup/
│ │ └── _settings/
│ └── projects/
│ └── openxchange/subgroup/project/
│ ├── repo.git/
│ ├── wiki.git/
│ ├── _settings/
│ │ ├── project.json
│ │ ├── … # other settings JSON files
│ │ └── issue_notes/<iid>.json # when issue_notes is enabled
│ └── _meta.json
├── latest -> runs/2026-03-04T08-30-00Z
└── state.jsonExit Codes
| Code | Meaning | |------|---------| | 0 | All projects backed up successfully | | 1 | Some projects failed (partial backup) | | 2 | Fatal error (e.g. disk full, config invalid) |
Development
pnpm dev -- list -c relic.yaml # run without building
pnpm test # run tests with coverage (output/coverage/)
pnpm test:watch # watch mode
pnpm build # compile TypeScript
pnpm typecheck # type-check without emittingLayout
src/— library + CLI sourcespec/— vitest test suites, mirroring thesrc/layout. Imports use the@/alias that maps tosrc/.config/— example config filesdist/— compiled output (gitignored)output/coverage/— coverage reports (gitignored)
Releasing
Bump version in package.json on main, then push a tag with the same version (e.g. 0.2.2). The tag pipeline publishes to npm through the openxchange/ci/nodejs/publish component with trusted publishing, so no npm token is stored anywhere. The trusted publisher is configured at https://www.npmjs.com/package/@open-xchange/relic/access.
