A CI/CD-friendly Packagist (and npm + PyPI) dev-workflow handler for polyrepo ecosystems across GitHub orgs, GitLab groups, and Bitbucket workspaces.
gendia is what stands between your local repo state and the world seeing it: it pushes pending commits and tags, fires the Packagist update webhook (or npm publish / twine upload), bumps changelogs, runs your verify pipeline, and audits the fleet for hygiene drift. One config file, one tool, one CLI verb per intent. Works the same on a developer laptop, on a VPS cron, and inside a Docker-based CI runner.
$ gendia status
status β ok
β core main clean ahead=0 behind=0 latest_tag=v1.0.0
β browser main clean ahead=0 behind=0 latest_tag=v1.0.0
β documentation main clean ahead=0 behind=0 latest_tag=v1.0.0
β tabler-icons main clean ahead=0 behind=0 latest_tag=v1.0.0
β bundled-icons main clean ahead=0 behind=0 latest_tag=v1.0.0
β metronic-icons main clean ahead=0 behind=0 latest_tag=v1.0.0
Maintaining a polyrepo Composer / npm / Python ecosystem means juggling: per-repo composer.json validation, CHANGELOG bumps, semver tagging, git push --tags, the Packagist update webhook, occasional Docker-image republishing, sometimes a parallel npm publish to a private registry β all wired together with bash scripts that drift out of sync the moment a repo gets renamed.
Existing multi-repo tools (mr, mu-repo, gita) stop at clone / pull / status; none of them know about Packagist webhooks, npm tokens, or CHANGELOG-bump conventions. gendia fills that gap. It treats a release as a workflow (verify β bump β tag β push β notify) and keeps the dev surface and the CI surface identical β gendia release core 1.0.1 does the same thing on your laptop and inside a CI runner.
- CI/CD-first design: identical CLI on dev box, VPS cron, and Docker-based runners. JSON log format for structured log shippers; exit code reflects all-ok / failed.
- Packagist-aware out of the box:
syncandreleaseknow how to fire the Packagist update endpoint when tags get pushed. npm and PyPI included as first-class push registries. - Six platforms from day one: GitHub (cloud + Enterprise), GitLab (cloud + self-hosted), Bitbucket Cloud + Server/Data Center, Gitea (Forgejo, Codeberg via the same wire format), and Azure DevOps.
- SSH-config aware: detects which forges your
~/.ssh/configalready lists, audits IdentityFile permissions + key strength, generates copy-paste git commands, converts URLs between SSH and HTTPS (alias +insteadOfaware), and bootstraps gendia accounts straight from your SSH setup. - Two transports: SSH (with optional explicit private-key path,
IdentitiesOnly=yesto block agent enumeration) or HTTPS-with-API-token (viaGIT_ASKPASS, never via URL embed, never in argv). - Multi-org / multi-account: any number of accounts in one config; each repo binds to one account. A leak in one account's credentials can't touch another's repos.
- Eighteen first-class verbs:
status,sync,inventory,audit,cleanup,verify,release,mirror,init,config,setup,doctor,identity,conventions,generate,scan,convert,sshβ fleet operations, first-run wizards, per-account git-identity management, JSON-driven repo-hygiene linting, SSH-config-driven command generation, on-disk repo discovery, URL conversion, and SSH-config inspection / bootstrap. - Dry-run on everything:
--dry-runis honoured by every mutating operation. - Zero runtime dependencies: stdlib only.
keyringis an optional extra for OS-keychain-backed secrets. - Docker-native: slim image (~80 MB), runs as non-root, mounts your
~/.config/gendiaand SSH keys;docker compose run gendia syncis the deployable unit.
# Recommended: uv (fast, isolated)
uv tool install gendia
# Or pip
pip install gendia
# From source (development)
git clone git@github.com:simtabi/gendia.git
cd gendia
make install-devgendia setup # first-run wizard; auto-detects the deployment shape
gendia init # scaffold ./gendia.json at the project root, then list your repos
mkdir -p ~/.config/gendia
cp examples/.env.example ~/.config/gendia/.env
chmod 600 ~/.config/gendia/.env # then add the tokens your credential_ref keys name
gendia doctor # preflight: env file, git/ssh binaries, agent, credentialsBoth config files and the .env are described under Configure.
gendia status # one-line summary per repo
gendia sync --dry-run # show what would be pushed and notified, do nothing
gendia release core 1.0.1 # bump CHANGELOG, tag, push, notifyEvery verb and modifier is listed under Commands.
Two files, both JSON. The machine-wide one declares accounts and registries; the per-project one declares the repos.
~/.config/gendia/gendia.json (machine-wide):
{
"accounts": {
"myorg": { "platform": "github", "org": "myorg", "credential_ref": "GITHUB_TOKEN_MYORG" },
"personal": { "platform": "github", "username": "yourhandle", "credential_ref": "GITHUB_TOKEN_PERSONAL" },
"internal": { "platform": "gitlab", "host": "gitlab.example.com", "group": "tools", "credential_ref": "GITLAB_TOKEN_INTERNAL" },
"client-x": { "platform": "bitbucket", "workspace": "client-x-workspace","credential_ref": "BITBUCKET_APP_PASSWORD_CLIENT_X" }
},
"registries": {
"packagist": { "kind": "packagist", "username": "your-packagist-handle", "credential_ref": "PACKAGIST_API_TOKEN" },
"npm": { "kind": "npm", "credential_ref": "NPM_TOKEN" },
"pypi": { "kind": "pypi", "credential_ref": "PYPI_API_TOKEN" }
},
"defaults": { "concurrency": 4, "log_format": "human" }
}./gendia.json (one per project, lives at the project root):
{
"name": "myorg",
"account": "myorg",
"registry": "packagist",
"root": "~/projects/myorg",
"repos": [
{ "dir": "core", "package": "myorg/core", "verify": ["composer validate", "vendor/bin/pest"] },
{ "dir": "browser", "package": "myorg/browser", "verify": ["composer validate", "vendor/bin/pest"] },
{ "dir": "tabler-icons","package": "myorg/tabler-icons","verify": ["composer validate", "vendor/bin/pest"] }
],
"cleanup_globs": ["vendor", "node_modules", ".phpunit.cache", ".DS_Store"]
}Secrets never live in the JSON files. One .env covers every deployment shape β local dev, VPS, Docker, Kubernetes β by mixing two patterns side by side.
| Use case | Location | How gendia finds it |
|---|---|---|
| Local dev (default) | ~/.config/gendia/.env |
XDG-compliant default; no setup |
| VPS / system-wide | /etc/gendia/.env (or anywhere) |
export GENDIA_ENV_FILE=/etc/gendia/.env |
| Per-project override | any path, e.g. <project>/.env.gendia |
GENDIA_ENV_FILE in that shell / Makefile |
| Docker / Compose | host file bind-mounted at /config/.env |
image sets GENDIA_ENV_FILE=/config/.env |
| Kubernetes | secrets at /run/secrets/<name> |
use <KEY>_FILE=/run/secrets/<name> in any of the above |
The file in this repo at examples/.env.example is a TEMPLATE only β never put real secrets there. It's tracked by git. Copy it to the runtime location:
mkdir -p ~/.config/gendia
cp examples/.env.example ~/.config/gendia/.env
chmod 600 ~/.config/gendia/.env
$EDITOR ~/.config/gendia/.envgendia-env warns if your .env has looser permissions than 0640.
Inside the file:
- Direct value for local dev:
GITHUB_TOKEN_MYORG=changeme <KEY>_FILEfor Docker / Compose / Kubernetes secret mounts:GITHUB_TOKEN_MYORG_FILE=/run/secrets/github_token_myorg. The file's contents become the value of<KEY>at runtime β the secret never appears in env vars or process listings. Both forms can coexist;*_FILEwins when both are set.
A small companion script ships in bin/gendia-env (also baked into the Docker image) that handles all of this for you. It sources the env file, resolves any *_FILE references, and runs whatever you exec it with β including docker compose, so ${VAR} interpolation in docker-compose.yml Just Works:
gendia-env docker compose run --rm gendia status # one-shot
gendia-env --check # show keys + masked previews
gendia-env --docker-args # prints --env-file=PATH for docker
eval "$(gendia-env --export)" # source into current shellA third optional resolver (when the keyring extra is installed): pip install gendia[keyring] then keyring set gendia GITHUB_TOKEN_MYORG. Skipped automatically when the package isn't installed (e.g., on a VPS).
Process environment variables override .env file values. The --no-keyring flag forces env-only resolution.
Two distinct credential types are at play. gendia keeps them separate.
- API token (
credential_refin account config): used for HTTPS REST calls to GitHub / GitLab / Bitbucket APIs and Packagist's update endpoint. Always required. - SSH key (
ssh_key_pathin account config, optional): used for git over SSH. When unset, the user's default SSH agent and~/.ssh/configapply β the right answer for dev machines.
Each account can pick its git transport via git_auth:
git_auth |
Behaviour |
|---|---|
"ssh" (default) |
Use SSH agent / ~/.ssh/config / explicit ssh_key_path |
"https" |
Use the API token via GIT_ASKPASS; ideal for containers without an SSH agent |
"auto" |
SSH when a key is available, else HTTPS-with-token |
Tokens are never embedded in URLs. When git_auth=https, the token is supplied via a temporary GIT_ASKPASS script that prints the token to stdin. The token never appears in argv (visible to ps) or in the remote URL (visible in reflogs).
For a VPS deployment, the typical pattern is git_auth: "auto" with an explicit ssh_key_path: /etc/gendia/keys/deploy_ed25519 β falls through to HTTPS-with-token if the key is missing.
# Fleet operations (read your project + iterate every repo):
gendia status # one-line summary per repo
gendia sync # push pending commits + tags + notify webhook registry
gendia inventory # categorise every package per account (synced / pending / untracked / missing / ignored)
gendia release <repo> <version> # bump CHANGELOG, tag, push, notify
gendia audit # hygiene checks
gendia cleanup # rm cleanup_globs across all repos
gendia mirror --to ~/repos # clone every repo defined in config
gendia verify # run each repo's verify[] commands
# Workstation setup (manage gendia's own config + the host's git identity):
gendia init [--out FILE] [--force] # scaffold a fresh gendia.json
gendia setup [--shape=local|vps|project|docker|k8s|ci] # first-run wizard; autodetects the shape when omitted
gendia config {list|get|set|unset|edit|path|doctor} # manage the .env that holds tokens + paths
gendia doctor # full preflight: env file, git/ssh binaries, agent, credentials
gendia identity {list|check|apply|setup|init} # per-account git user.name / user.email / signing key
# Quality control:
gendia conventions [PATH] [--rules FILE] [--strict] [--json]
# lint repo hygiene: GitHub-special files, naming,
# ban-list glyphs, spec filenames, sub-folder readmes,
# shell-script shebangs (JSON-driven, fully tunable)
gendia conventions --list-profiles # 16 bundled rule packs
gendia conventions --explain R005 --profile core
gendia conventions . --no-legacy --profile core --profile lang-node --fix [--dry-run]
# JSON-driven standards engine + autofix
# (21 rules with fix:; idempotent + atomic writes)
# Scaffolds:
gendia init --list-scaffolds # 13 bundled scaffolds
gendia init --scaffold python-uv --target ./foo --var name=foo --var module=foo \
--var description="A package" # render template + git init + license_text + audit
# Interactive menu (every verb with drill-down by category):
./bin/gendia-menu # or: make menu
./bin/gendia-menu --list # non-interactive option dump (CI / docs)
# Modifiers (work with most commands):
--dry-run show what would happen, do nothing
--only repo-a,repo-b operate on a subset
--skip repo-c
--concurrency 8 override default parallelism
--log-format json for CI consumption
--no-keyring skip the OS keyring backend; env / .env only
--config PATH point at a specific gendia.jsonMost polyrepo ecosystems develop a "house style": which files must exist at the root, how markdown is named, which glyphs and emojis are off-limits, where specs live, what the readme has to declare. gendia conventions lints these the same way audit lints git hygiene β one tool, one config, identical on dev and CI.
# Single-repo run
gendia conventions /path/to/repo
# Walk every repo declared in the project's gendia.json
cd ~/projects/myorg && gendia conventions
# Custom rules (anything you omit falls back to the built-in defaults)
gendia conventions --rules examples/conventions.json
# CI mode: JSON output, warnings count as failures
gendia conventions --json --strictThe default rules cover ten categories:
| Rule | Severity | What it catches |
|---|---|---|
github-special |
error / ok | Missing LICENSE, CONTRIBUTING.md, CODE_OF_CONDUCT.md. Reports CODEOWNERS, SECURITY.md, SUPPORT.md when present. |
md-kebab-case |
error | Markdown filenames that aren't lowercase-kebab-case (with sensible exemptions for README.md, CHANGELOG.md, etc.). |
forbidden-chars |
warn | Em-dash β (U+2014) and any other glyph you ban via forbidden_chars. |
readme-frontmatter |
warn | Optional: require **Owner:** and **Last Updated:** in the readme head. |
decorator-emojis |
warn | Ornamental emojis (β π― πΌ β¨ π etc.). Status emojis (β β³ π β π΄ π π‘ π’) are deliberately allowed. |
spec-date-prefix |
error | Date-prefixed spec filenames like 2026-05-08-thing.md (the date belongs in frontmatter). |
spec-number-prefix |
error | Number-prefixed spec filenames like 001-thing.md (sequential IDs go in the readme index). |
subfolder-readme |
warn | Sub-folder readme.md files (one tier index per repo). |
stale-link-pattern |
warn | Markdown links that target the patterns above (numbered specs, sub-folder readmes). |
sh-shebang |
error / warn | *.sh files without a shebang (error) or without the executable bit (warn). |
Optional rules off by default:
forbid_trailing_whitespace_mdβ flags trailing whitespace in markdown.
Override anything via JSON. Every key is optional; omit it to keep the default. Full schema lives in examples/conventions.json:
{
"github_special_required": ["LICENSE", "CONTRIBUTING.md"],
"forbidden_chars": ["β", "β", "β"],
"decorator_emojis_exempt": ["CLAUDE.md", "changelog.md", "ADR-*.md"],
"spec_dir": "docs/specs",
"require_readme_frontmatter": true,
"forbid_trailing_whitespace_md": true,
"exclude_dirs": [".git", "node_modules", "vendor", "build", "dist", ".venv"]
}Exit codes match gendia doctor: 0 = ok, 1 = warnings only (with --strict), 2 = at least one error.
Layered on top of the legacy rules above, gendia conventions ships a JSON-driven standards engine with 16 bundled rule packs (71 rules) and --fix automation for 21 of them. Add a rule by dropping a JSON file in src/gendia/data/standards/; add a check kind by registering one Python function in standards.py. No DSL.
# Explore the bundled packs
gendia conventions --list-profiles # 16 packs (core, community, lang-node, β¦)
gendia conventions --list-rules --profile core # rules in the pack
gendia conventions --explain R005 --profile core # rule details
# Run against any path, composing multiple packs
gendia conventions . --no-legacy \
--profile core --profile community --profile platform-github --profile lang-node
# Autofix where the rule pack has a fix kind
gendia conventions . --no-legacy --profile core --profile community --fix --dry-run
gendia conventions . --no-legacy --profile core --profile community --fixFix-kind coverage today: 21 rules across 6 packs auto-fix to good defaults. Universal essentials (README / .gitignore / .editorconfig / * text=auto in .gitattributes), community health (CONTRIBUTING / CODE_OF_CONDUCT / SECURITY / CHANGELOG with Keep-a-Changelog skeleton), GitHub workflows (CODEOWNERS / PR template / Dependabot / CodeQL / OpenSSF Scorecard / Security Insights), and 5 JSON-mutating fixes for package.json: name (slugified directory basename), version (0.1.0), license (auto-detected from existing LICENSE file), engines.node, packageManager. All fixes are idempotent; mutating fixes use atomic writes + refuse symlinks + tolerate malformed JSON.
gendia init --scaffold renders bundled templates with ${var} substitution and post-actions (git_init, license_text, audit).
gendia init --list-scaffolds # 13 scaffolds
gendia init --scaffold python-uv --target ~/projects/foo \
--var name=foo --var module=foo --var description="A package"| Scaffold | Stack |
|---|---|
bare |
Universal-set repo + git_init + license_text |
python-uv / python-poetry |
Python package, src/ layout, ruff + mypy + pytest |
rust / go |
Cargo + clippy + rustfmt / go.mod + golangci-lint |
node-pnpm / node-bun |
TypeScript + Biome + Vitest / Bun |
php-composer / ruby-gem |
Composer + PHPStan + PHPUnit / Gemspec + RSpec |
dotnet |
.NET SDK + solution + xUnit |
docs-mkdocs |
MkDocs Material |
monorepo-pnpm-turbo |
pnpm workspaces + Turborepo |
java-gradle |
Gradle Kotlin DSL + JUnit 5 (run gradle wrapper once after scaffold) |
17 bundled license texts for the license_text post-action: MIT, Apache-2.0, BSD-2/3-Clause, ISC, Unlicense, GPL/LGPL/AGPL-3.0-or-later, MPL-2.0, MIT-0, Zlib, EUPL-1.2, CC-BY-4.0, BSL-1.1, OFL-1.1, Artistic-2.0.
bin/gendia-menu is a numbered drill-down menu wrapping all 18 verbs by category (Inspect / Release & Sync / Standards / Scaffold / Config). It auto-resolves the local bin/gendia shim during development and falls back to whatever gendia is on PATH.
make menu # launch the menu
./bin/gendia-menu # equivalent
./bin/gendia-menu --list # non-interactive option dump (CI / docs)The menu is shipped inside the Docker image, so make docker-exec drops you into a working toolkit shell.
make help # list targets
make menu # launch the interactive numbered menu
make install # uv tool install (or pip fallback)
make install-dev # editable install with [dev] extras
make test # pytest
make lint # ruff check
make format # ruff format (rewrites files)
make format-check # ruff format --check (mirrors CI)
make typecheck # mypy
make check # lint + format-check + typecheck + test (CI gate)
make sync # gendia sync (extra args via ARGS=...)
make release REPO=core VERSION=1.0.1
make audit / cleanup / verify / mirror / init / status / inventory
# One-shot containers:
make docker-build / docker-shell / docker-run VERB=sync / docker-push REGISTRY=ghcr.io/your-org
# Long-lived compose service:
make docker-up / docker-exec / docker-logs / docker-ps / docker-down / docker-host-shellgendia ships a slim Python image (~80 MB) with the gendia-env helper baked in. The image's ENTRYPOINT is gendia-env gendia, so anything you put in CMD runs after the env file has been sourced and *_FILE secrets have been resolved.
# One-shot from the project root:
make docker-build
docker run --rm \
--env-file ~/.config/gendia/.env \
-v ~/.config/gendia:/config:ro \
-v ~/.ssh:/home/gendia/.ssh:ro \
-v "$PWD":/work \
gendia:latest statusOr via Compose (recommended):
docker compose run --rm gendia status
docker compose run --rm gendia sync
docker compose run --rm gendia release core 1.0.1Or via the Make targets:
make docker-build
make docker-shell # interactive bash inside the container
make docker-run VERB=sync # one-shot any verbVPS cron (every 30 minutes):
*/30 * * * * docker run --rm \
--env-file /home/ops/.config/gendia/.env \
-v /home/ops/.config/gendia:/config:ro \
-v /home/ops/.ssh:/home/gendia/.ssh:ro \
-v /home/ops/repos:/work \
gendia:latest sync --log-format json \
>> /var/log/gendia/sync.log 2>&1For Kubernetes, mount each secret at /run/secrets/<name> via a Secret volume and set <KEY>_FILE=/run/secrets/<name> in the env. See the comments at the top of examples/.env.example for a complete recipe.
gendia/
βββ src/gendia/
β βββ auth/ credential resolvers: env, .env, Docker secrets, keyring
β βββ config/ frozen dataclasses + JSON loader
β βββ git/ GitRepo wrapper, safe subprocess shell, transport-auth env
β βββ providers/ GitProvider abstract base + GitHub / GitLab / Bitbucket
β βββ registries/ PackageRegistry hierarchy (Webhook / Publish capable)
β βββ operations/ Operation abstract base + 9 fleet verbs + conventions linter
β βββ state/ sync-state store (~/.cache/gendia/sync-state.json)
β βββ observability logger.py (human + JSON formatters)
β βββ cli/ argparse setup + dispatcher + sidecar commands (config/setup/doctor/identity)
βββ tests/unit/ schema, loader, auth, git_repo, git_auth, concurrency, identity, sync state, conventions
βββ examples/ single-org.json, multi-org.json, conventions.json, .env.example
βββ bin/gendia bash shim for development use
βββ Dockerfile multi-stage, ~80 MB
βββ docker-compose.yml
βββ Makefile
Each layer depends only on the layer below; SOLID applied across the board. Adding a new VCS = one file in providers/. Adding a new operation = one file in operations/.
git clone git@github.com:simtabi/gendia.git
cd gendia
make install-dev
make testTests must pass before merging. make lint and make typecheck should produce no errors. Conventional Commit messages preferred (feat:, fix:, docs:, refactor:, etc.).
- Tokens are never written to JSON config or argv.
- SSH transport uses
IdentitiesOnly=yesto prevent SSH agent enumeration. - HTTPS transport disables the system credential helper to prevent token caching.
- The Docker image runs as a non-root user.
- Report vulnerabilities to security@simtabi.com β not via public issues.
This project is licensed under the MIT License.
Β© Simtabi LLC