Skip to content
simtabiPublic

About

CI/CD-friendly dev-workflow handler for polyrepo ecosystems: push, publish, changelog, and audit across GitHub, GitLab, and Bitbucket.

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

gendia

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

Why

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.

Highlights

  • 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: sync and release know 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/config already lists, audits IdentityFile permissions + key strength, generates copy-paste git commands, converts URLs between SSH and HTTPS (alias + insteadOf aware), and bootstraps gendia accounts straight from your SSH setup.
  • Two transports: SSH (with optional explicit private-key path, IdentitiesOnly=yes to block agent enumeration) or HTTPS-with-API-token (via GIT_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-run is honoured by every mutating operation.
  • Zero runtime dependencies: stdlib only. keyring is an optional extra for OS-keychain-backed secrets.
  • Docker-native: slim image (~80 MB), runs as non-root, mounts your ~/.config/gendia and SSH keys; docker compose run gendia sync is the deployable unit.

Install

# 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-dev

Quick start guide and usage

Getting started

gendia 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, credentials

Both config files and the .env are described under Configure.

Usage

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, notify

Every verb and modifier is listed under Commands.

Configure

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.

Where does the .env live?

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/.env

gendia-env warns if your .env has looser permissions than 0640.

Inside the file:

  • Direct value for local dev: GITHUB_TOKEN_MYORG=changeme
  • <KEY>_FILE for 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; *_FILE wins 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 shell

A 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.

Auth: SSH vs API token

Two distinct credential types are at play. gendia keeps them separate.

  • API token (credential_ref in account config): used for HTTPS REST calls to GitHub / GitLab / Bitbucket APIs and Packagist's update endpoint. Always required.
  • SSH key (ssh_key_path in account config, optional): used for git over SSH. When unset, the user's default SSH agent and ~/.ssh/config apply β€” 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.

Commands

# 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.json

Quality control: gendia conventions

Most 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 --strict

The 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.

JSON-driven standards engine + autofix

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 --fix

Fix-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.

Repo scaffolds

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.

Interactive menu

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 targets

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-shell

Docker / VPS

gendia 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 status

Or via Compose (recommended):

docker compose run --rm gendia status
docker compose run --rm gendia sync
docker compose run --rm gendia release core 1.0.1

Or via the Make targets:

make docker-build
make docker-shell                   # interactive bash inside the container
make docker-run VERB=sync           # one-shot any verb

VPS 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>&1

For 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.

Architecture

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/.

Contributing

git clone git@github.com:simtabi/gendia.git
cd gendia
make install-dev
make test

Tests must pass before merging. make lint and make typecheck should produce no errors. Conventional Commit messages preferred (feat:, fix:, docs:, refactor:, etc.).

Security

  • Tokens are never written to JSON config or argv.
  • SSH transport uses IdentitiesOnly=yes to 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.

License

This project is licensed under the MIT License.

Β© Simtabi LLC

About

CI/CD-friendly dev-workflow handler for polyrepo ecosystems: push, publish, changelog, and audit across GitHub, GitLab, and Bitbucket.

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages