dot-git-ci
v0.1.2
Published
Portable CI/CD over docker, with results stored in git
Readme
git-ci
Portable CI/CD that runs anywhere docker runs, with all results stored in git. No vendor lock-in to GitHub Actions / GitLab CI / Jenkins. The repository is the single source of truth for code, deploy state, build logs, and test history.
Philosophy
- One source of truth: the git repo. Code, env compositions, logs, deploy state — all live in branches.
- No central service: anything
git-cidoes, you can also do by hand withgit,ssh,rsync,docker compose. - Each project is self-contained: no group/multi-repo logic baked in. If you have several related repos, you run
git-ciin each. - Docker as the only runtime dependency on the host. The local CLI is Node ≥18.
Mental model
main canonical .git-ci/*.yml definitions (env shapes, gates)
feat/* may carry their own .git-ci/config.yml (how-to-test)
ci-state orphan branch: concrete state — which branches are mixed into each env
ci-logs orphan branch: all run/promote/deploy/demote logs, streamed every 5sTwo deploy transports, picked per command:
| command | source of state | transport | branch mix |
|---|---|---|---|
| run | workdir | local docker | none |
| deploy [env] | workdir | rsync + ssh | none |
| promote <env> [branch] | git origin | ssh + host-side git fetch | on host |
| demote <env> [branch] | git origin | ssh + host-side git fetch | on host |
| install <user@host> | — | ssh setup | — |
Quick start
# install
cd ~/okneigres-repos/git-ci
./install.sh # symlinks git-ci + gci into ~/.local/bin
# or:
npm install -g .
# in your project
mkdir -p .git-ci
cat > .git-ci/config.yml <<'EOF'
service: app
run: npm test
EOF
# run tests locally inside docker
git-ci run
# prepare a remote host
git-ci install [email protected]
# describe an environment (on main)
cat > .git-ci/preprod.yml <<'EOF'
host: [email protected]
service: app
protected: true
gates:
- tests-passed
EOF
git add .git-ci/preprod.yml && git commit -m "add preprod env"
# promote a feature into preprod
git checkout feat/login
git-ci promote preprod # appends feat/login to preprod.yml on ci-state branch,
# then ssh's the host and rebuilds the env from scratchCommands
| command | what it does |
|---|---|
| run | reads .git-ci/config.yml, runs docker compose build <service> && docker compose run --rm <service> sh -c <run>. Local-only by default. |
| deploy [env] | rsync current workdir + sibling repos to <env>'s host under ~/.git-ci/<project>/deploy/<env>/, docker compose -p <project>-<env> up -d --build there. No branch mix. Refuses on protected: true envs. |
| promote <env> [branch] | append branch to ci-state:<env>.yml, ssh host, fetch + merge branches in order, rebuild. Gates from <env>.yml are checked first. |
| demote <env> [branch] | remove branch from ci-state:<env>.yml, rebuild without it. |
| install <user@host> | ensure docker/git/rsync on host, clone repo into ~/.git-ci/<project>/repo, prepare workdir. |
Sibling projects
A project may pull in code from sibling project dirs by symlinking them under node_modules/:
~/my-group/
├─ project-a/ # current project (we deploy this)
│ ├─ docker-compose.yml # build.context: ..
│ └─ node_modules/
│ └─ lib-b -> ../../lib-b # symlink to sibling
└─ lib-b/ # sibling — copied to host but not rungit-ci deploy detects these symlinks (target starts with ../../) and rsyncs each sibling to the host alongside the current project. They are placed as static files only — not built, not run — so the current project's docker-compose.yml can reference them via build.context: .. or similar. This is the same convention used by ../rsync-docker-compose.
Remote layout under ~/.git-ci/<project>/deploy/<env>/:
~/.git-ci/project-a/deploy/feat-login/
├─ project-a/ # current
└─ lib-b/ # sibling (static)Stale sibling dirs (no longer in node_modules) are cleaned up on next deploy.
Config files
.git-ci/config.yml — read from the current branch, describes how to test/build that branch.
service: app # name from docker-compose.yml
run: npm ci && npm test # shell command inside the container.git-ci/<env>.yml — read from the canonical branch (main, falling back to master), describes the shape of an environment. deploy also accepts a workdir copy; promote/demote are strict.
host: [email protected]
service: app
protected: true # disallow `git-ci deploy <env>`; only promote/demote can change it
gates:
- tests-passed # branch must have a successful ci-logs entry on the env's build commit
- in-env: preprod # (on prod.yml) branch must currently be in preprod
env:
DATABASE_URL: postgres://...
PUBLIC_URL: https://preprod.example.com.git-ci/[branch].yml — template, rendered per branch. {{branch_slug}} expands to the branch name with / → -.
host: [email protected]
service: app
env:
PUBLIC_URL: https://{{branch_slug}}.preview.example.com
DATABASE_URL: postgres://.../{{branch_slug}}_db
ttl: 7dStatus
Implemented: run, install, deploy (with [branch].yml template fallback and live file watcher). Not yet: promote, demote, and the ci-logs / ci-state plumbing. See AGENTS.md for the architecture invariants.
License
MIT.
