Configuration-Driven CI/CD That Works Everywhere
# macOS / Linux / WSL - Fully automated setup
curl -sL https://raw.githubusercontent.com/orchestrate-solutions/universal-ci/main/install-ci.sh | sh
# Windows PowerShell
irm https://raw.githubusercontent.com/orchestrate-solutions/universal-ci/main/install-ci.ps1 | iexThat's it. The installer will:
- β Download the latest CI runner scripts
- β Auto-detect your project type (Node, Python, Go, Rust, .NET, Java, Kotlin, Scala, Swift, C++, Dart, Ruby, PHP)
- β
Generate
universal-ci.config.jsonwith smart defaults (safely detects existing config) - β Set up/Update Git pre-push hooks (including Semantic Versioning)
- β Run initial CI verification
Already using Universal CI? Simply re-run the installation command above to update your scripts and hooks to the latest version.
# Skip prompts (fully automatic)
curl -sL .../install-ci.sh | sh -s -- -y
# Include GitHub Actions workflow
curl -sL .../install-ci.sh | sh -s -- --github-actions
# Include Docker setup for isolated runs
curl -sL .../install-ci.sh | sh -s -- --docker
# Force a specific project type
curl -sL .../install-ci.sh | sh -s -- --type nodejs
# Full setup (everything)
curl -sL .../install-ci.sh | sh -s -- -y --github-actions --dockerUniversal CI is a lightweight, configuration-driven CI/CD tool that runs the same way locally and in the cloud. Define your build, test, and deployment tasks in simple JSON configuration files, then execute them consistently across environments.
Zero dependencies. Works on any system with a shell (macOS, Linux) or PowerShell (Windows).
- πͺΆ Zero Dependencies: Pure shell script - no Python, Node, or runtime required
- π§ Config-Driven: Define tasks in
universal-ci.config.json- no complex YAML - π Local First: Test your CI locally before pushing
- π¦ Multi-Stage: Separate
testandreleasestage configurations - π Cross-Platform: Shell script for macOS/Linux, PowerShell for Windows
- π― Task-Based: Each task specifies its working directory and command
- π Smart Detection: Auto-detects 16+ programming languages (Node, Python, Go, Rust, .NET, Java, Kotlin, Scala, Swift, C++, Dart, Ruby, PHP, and more)
The installer automatically detects and configures:
| Language | Files Detected | Package Manager | Tasks Generated |
|---|---|---|---|
| Node.js | package.json |
npm, pnpm, yarn, bun | install, test, lint, build |
| Python | pyproject.toml, requirements.txt |
pip, poetry, pipenv, uv | install, lint, test |
| Go | go.mod |
go modules | download, vet, test, build |
| Rust | Cargo.toml |
cargo | check, clippy, test, build |
| .NET | *.csproj, *.fsproj |
dotnet | restore, build, test |
| Java (Maven) | pom.xml |
maven | compile, test, package |
| Java (Gradle) | build.gradle* |
gradle | compile, test, build |
| Kotlin | build.gradle.kts, src/main/kotlin |
gradle | build, test, assemble |
| Scala | build.sbt, src/main/scala |
sbt/gradle | compile, test, package |
| Swift | Package.swift, Sources/ |
swiftpm | build, test, release |
| C++ | CMakeLists.txt, *.cpp |
cmake/make | configure, build, test |
| Dart | pubspec.yaml, *.dart |
pub | get, analyze, test, build |
| Ruby | Gemfile |
bundler | install, rubocop, test |
| PHP | composer.json |
composer | install, phpstan, test |
| Makefile | Makefile |
make | build, test |
| Generic | - | - | Hello World example |
After running the installer, you'll have a universal-ci.config.json like this:
{
"tasks": [
{
"name": "Install Dependencies",
"working_directory": ".",
"command": "npm ci",
"stage": "test"
},
{
"name": "Run Tests",
"working_directory": ".",
"command": "npm test",
"stage": "test"
},
{
"name": "Lint",
"working_directory": ".",
"command": "npm run lint",
"stage": "test"
},
{
"name": "Build",
"working_directory": ".",
"command": "npm run build",
"stage": "release"
}
]
}Run it locally:
./run-ci.sh# Install as dev dependency
npm install --save-dev @orchestrate-solutions/universal-ci
# Initialize config with auto-detection
npx @orchestrate-solutions/universal-ci init
# Run CI
npx @orchestrate-solutions/universal-ci
# or use npm script
npm run ci# macOS / Linux / WSL
curl -sL https://raw.githubusercontent.com/orchestrate-solutions/universal-ci/main/install-ci.sh | sh
# Windows PowerShell
irm https://raw.githubusercontent.com/orchestrate-solutions/universal-ci/main/install-ci.ps1 | iex# macOS / Linux
curl -sL https://raw.githubusercontent.com/orchestrate-solutions/universal-ci/main/run-ci.sh -o run-ci.sh && chmod +x run-ci.sh
# Windows (PowerShell)
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/orchestrate-solutions/universal-ci/main/run-ci.ps1" -OutFile "run-ci.ps1"git clone https://github.com/orchestrate-solutions/universal-ci.git
cd universal-ci
./run-ci.sh # or .\run-ci.ps1 on Windows./run-ci.sh --init # Creates config for current project
./run-ci.sh --init --type go # Force specific project typeThe one-liner above handles everything automatically. Or if you want to start manually:
# macOS / Linux
./run-ci.sh
# Windows
.\run-ci.ps1name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Universal CI
run: |
curl -sL https://raw.githubusercontent.com/orchestrate-solutions/universal-ci/main/run-ci.sh -o run-ci.sh
chmod +x run-ci.sh
./run-ci.shEdit universal-ci.config.json to add your own tasks:
{
"tasks": [
{
"name": "Install Dependencies",
"working_directory": ".",
"command": "npm install"
},
{
"name": "Run Tests",
"working_directory": ".",
"command": "npm test"
},
{
"name": "Build",
"working_directory": ".",
"command": "npm run build"
}
]
}{
"name": "Task Name",
"working_directory": ".",
"command": "echo 'Hello World'"
}{
"tasks": [
{
"name": "Unit Tests",
"working_directory": ".",
"command": "npm test",
"stage": "test"
},
{
"name": "Deploy to Production",
"working_directory": ".",
"command": "npm run deploy",
"stage": "release"
}
]
}Test across multiple versions without duplicating tasks:
{
"tasks": [
{
"name": "Test on Python {version}",
"working_directory": ".",
"command": "python{version} -m pytest",
"stage": "test",
"versions": ["3.9", "3.10", "3.11", "3.12", "3.13"]
},
{
"name": "Test on Node {version}",
"working_directory": ".",
"command": "node{version} --version && npm test",
"stage": "test",
"versions": ["16", "18", "20", "22"]
}
]
}When versions is specified, Universal CI automatically creates separate tasks for each version, replacing {version} with the actual value. This is equivalent to GitHub Actions' matrix strategy but in your config file.
| Property | Required | Default | Description |
|---|---|---|---|
name |
β | - | Human-readable task identifier (can include {version} placeholder) |
working_directory |
β | - | Directory to execute command from |
command |
β | - | Shell command to run (can include {version} placeholder) |
stage |
β | "test" |
"test" or "release" |
versions |
β | - | Array of versions to test (e.g., ["3.9", "3.10", "3.11"]) |
cache |
β | - | Cache configuration with key and paths |
if |
β | - | Conditional expression (skip if false) |
requires_approval |
β | false |
Require explicit approval to run task |
Skip expensive tasks using hash-based caching:
{
"tasks": [
{
"name": "Install Dependencies",
"working_directory": ".",
"command": "npm install",
"cache": {
"key": "npm-${{ hashFiles('package-lock.json') }}",
"paths": ["node_modules"]
}
},
{
"name": "Run Tests",
"working_directory": ".",
"command": "npm test"
}
]
}How it works:
${{ hashFiles('file1', 'file2') }}computes MD5/SHA256 hash of files- Cache key is used to create
.universal-ci-cache/{key}/directory - If cache exists, task is skipped (files already installed/built)
- After successful task, cache is written for next run
- Hash changes automatically invalidate old cache
Run tasks only when specific conditions are met:
{
"tasks": [
{
"name": "Test Locally",
"working_directory": ".",
"command": "npm test",
"if": "env.CI == '' || env.CI == 'false'"
},
{
"name": "Build Release",
"working_directory": ".",
"command": "npm run build --production",
"stage": "release",
"if": "${{ github.ref }} == 'refs/heads/main' && env.PRODUCTION == 'true'"
},
{
"name": "Deploy if Flag Exists",
"working_directory": ".",
"command": "npm run deploy",
"if": "file(.deploy-flag)"
},
{
"name": "Mac-Only Task",
"working_directory": ".",
"command": "xcode-select --install",
"if": "os(macos)"
}
]
}Condition Syntax:
- Environment variables:
env.VAR_NAME(e.g.,env.CI,env.DEPLOY) - File existence:
file(path)(e.g.,file(.deploy-flag)) - Operating system:
os(linux),os(macos),os(windows) - Git branch:
branch(main)(e.g.,branch(main)) - GitHub context:
${{ github.ref }}(when running in GitHub Actions) - Boolean logic:
&&(and),||(or), parentheses for grouping - String comparison:
==,!=operators
Tasks with unmet conditions are skipped with transparency logging.
List tasks as JSON and run only selected ones - perfect for AI automation:
# List all tasks as JSON (AI reads and decides which to run)
./run-ci.sh --list-tasks
# Output:
# {"tasks":[{"name":"Build","directory":".","command":"npm run build"},{"name":"Test","directory":".","command":"npm test"},...]}
# Run only specific tasks (AI selects via JSON array)
./run-ci.sh --select-tasks '["Build","Test"]'
# Approve tasks requiring approval
./run-ci.sh --stage release --approve-task "Deploy to Production"
# Skip tasks without running them
./run-ci.sh --skip-task "Slow Integration Tests"Interactive CLI Commands:
--interactive- Enable interactive mode--list-tasks- Output all parsed tasks as JSON (for AI to read)--select-tasks '["task1","task2"]'- Run only named tasks--approve-task "name"- Approve task requiring approval (repeatable)--skip-task "name"- Skip task by name (repeatable)
Example: AI-Driven Workflow
# AI gets list of available tasks
tasks=$(./run-ci.sh --list-tasks | jq .tasks)
# AI analyzes and decides which tasks to run
# AI constructs JSON selection based on logic
# AI executes only the selected tasks
./run-ci.sh --select-tasks '["Lint","Test","Build"]'
# If deploy task requires approval, AI requests it
if ./run-ci.sh --stage release --list-tasks | jq '.tasks[] | select(.requires_approval == true)'; then
./run-ci.sh --stage release --approve-task "Deploy" --approve-task "Notify"
fi# macOS / Linux
./run-ci.sh [OPTIONS]
# Windows
.\run-ci.ps1 [OPTIONS]| Option | Description |
|---|---|
--init |
Initialize config for current project (auto-detect type) |
--config <path> |
Path to config file (default: universal-ci.config.json) |
--stage <stage> |
Stage to run: test or release (default: test) |
--type <type> |
Force project type for --init |
--interactive |
Interactive mode (for AI automation) |
--list-tasks |
Output all tasks as JSON (use with --interactive) |
--select-tasks <json> |
Run only specified tasks (e.g., '["task1","task2"]') |
--approve-task <name> |
Approve task requiring approval (repeatable) |
--skip-task <name> |
Skip task by name (repeatable) |
--help |
Show help message |
# Initialize a new project
./run-ci.sh --init
# Run with default config
./run-ci.sh
# Run specific config
./run-ci.sh --config my-project.json
# Run release tasks
./run-ci.sh --stage release
# Windows equivalent
.\run-ci.ps1 -Config my-project.json -Stage release| Script | Platform | Dependencies |
|---|---|---|
install-ci.sh |
macOS, Linux, WSL | POSIX shell + curl |
install-ci.ps1 |
Windows | PowerShell 5.1+ |
run-ci.sh |
macOS, Linux, WSL | POSIX shell (sh/bash) |
run-ci.ps1 |
Windows, macOS*, Linux* | PowerShell 5.1+ |
universal-ci-testing-env/verify.py |
Any | Python 3.8+ |
*PowerShell Core required on macOS/Linux
The installer sets this up automatically, but you can also do it manually:
# Create hook
cat > .git/hooks/pre-push << 'EOF'
#!/bin/sh
echo "π Running Universal CI verification..."
./run-ci.sh || exit 1
EOF
chmod +x .git/hooks/pre-pushRun verification in an isolated container:
# Use the installer to set up Docker
curl -sL .../install-ci.sh | sh -s -- --docker
# Then run with Docker Compose
docker-compose -f docker-compose.ci.yml run ciThe Docker setup creates:
Dockerfile.ci: Alpine-based container with basic toolsdocker-compose.ci.yml: Compose file for easy execution
Universal CI automatically analyzes your git history and determines version bumps - no manual version management needed.
- Before each push, git pre-push hook runs semantic analyzer
- Analyzes commits using conventional commit format (feat:, fix:, BREAKING:)
- Detects version bump type:
feat:β minor version bump (new features)fix:β patch version bump (bug fixes)BREAKING CHANGE:β major version bump (breaking changes)
- Prompts for breaking changes if needed (forces user/AI response)
- Auto-updates VERSION, CHANGELOG.md, package.json
- Stages files for push
- Push proceeds with versioning already done
Write commit messages following this pattern. See full Commit Standard documentation.
# New feature (triggers minor bump)
git commit -m "feat: Add user authentication system"
# Bug fix (triggers patch bump)
git commit -m "fix: Resolve infinite loop in data processor"
# Breaking change (triggers major bump)
git commit -m "feat!: Redesign API response format
This is a breaking change - all clients must update their parsers.
BREAKING CHANGE: Response format changed from array to object"
# Or simplified breaking change
git commit -m "BREAKING CHANGE: Removed deprecated login endpoint"When commits suggest version bumps, you'll be prompted:
βΉ Analyzing commits for semantic versioning...
β Found: 2 features, 1 fixes, 0 breaking changes
β Suggested version bump: minor
π Detecting breaking changes...
Has any breaking change been made to the API, CLI, or configuration?
β’ API changes that aren't backward compatible
β’ CLI flag/argument removal or significant changes
β’ Configuration format changes
β’ Database schema changes
Has breaking change? [yes/no]:
The push won't proceed until you respond. This ensures you never forget to document breaking changes.
Control semantic versioning in universal-ci.config.json:
{
"tasks": [...],
"semver": {
"enabled": true,
"auto_update_version": true,
"require_breaking_change_confirmation": true
}
}enabled- Enable/disable semantic versioning (default:true)auto_update_version- Automatically update VERSION file (default:true)require_breaking_change_confirmation- Force breaking change prompt (default:true)
To disable automatic version bumping:
{
"semver": {
"enabled": false
}
}The pre-push hook will skip semantic versioning but still run verification.
For agents/CI systems, get JSON output:
./.github/scripts/semantic-version.sh --analyzeOutput:
{
"bump_type": "minor",
"changes": {
"breaking": 0,
"features": 2,
"fixes": 1
},
"commits": [
"feat: Add user authentication",
"feat: Add two-factor auth",
"fix: Resolve login timeout"
],
"requires_input": false,
"breaking_change_response": ""
}When semantic versioning runs, it updates:
- VERSION file - Contains current version (e.g.,
1.0.0) - CHANGELOG.md - Adds new dated release section with commit list
- package.json -
versionfield synced automatically - Git staging area - All updated files staged for push
Nothing needs manual editing - the system decides everything.
# 1. Make changes and commits (using conventional format)
git add .
git commit -m "feat: Add dark mode theme"
git commit -m "fix: Resolve color contrast issues"
# 2. Push triggers semantic versioning
git push origin main
# 3. Pre-push hook runs:
# β Analyzes commits β Detects minor version bump
# β Updates VERSION: 1.0.0 β 1.1.0
# β Updates CHANGELOG.md with new section
# β Updates package.json version field
# β Stages VERSION, CHANGELOG.md, package.json
# β Runs full CI verification
# β Push completes
# 4. GitHub Actions publishes to npm automatically
# β Detects VERSION change
# β Publishes to npm as v1.1.0
# β Creates GitHub release with changelog
# β Tags commit as v1.1.0In monorepos, place semantic versioning at your workspace root or per package:
monorepo/
βββ VERSION # Workspace version (optional)
βββ CHANGELOG.md # Shared changelog
βββ package-a/
β βββ VERSION # Package-specific version
β βββ CHANGELOG.md
β βββ package.json
βββ package-b/
βββ VERSION
βββ CHANGELOG.md
βββ package.json
Each VERSION file is independent - changes to one don't affect others.
- VERSION - Single source of truth (e.g.,
1.0.1) - CHANGELOG.md - Dated release notes
- package.json - Automatically synced with VERSION
# 1. Bump version (auto-updates package.json and CHANGELOG.md)
.github/scripts/bump-version.sh patch # or minor, major
# 2. Edit CHANGELOG.md to describe changes
vim CHANGELOG.md
# Add details under the new version section
# 3. Commit and push
git add VERSION CHANGELOG.md package.json
git commit -m "release: v1.0.1"
git push origin mainWhen you push to main with VERSION file change:
- Workflow detects VERSION file update
- Publishes to npm with new version
- Creates GitHub release with changelog
- Tags commit with version number
- Updates package.json automatically
- Generate npm access token at https://npmjs.com/settings/tokens
- Add to GitHub: Settings β Secrets and variables β
NPM_TOKEN - That's it!
Whether you have:
- Single repo: VERSION is the app version
- Monorepo: Keep VERSION at workspace root (or create per-package)
- Multiple projects: Each can have its own VERSION file
$ .github/scripts/bump-version.sh minor
Current version: 1.0.0
New version: 1.1.0
β
Version bumped: 1.0.0 β 1.1.0
Next steps:
1. Edit CHANGELOG.md to describe changes
2. git add VERSION CHANGELOG.md package.json
3. git commit -m "release: v1.1.0"
4. git push origin main
# After push:
# β
npm publishes v1.1.0
# β
GitHub creates release
# β
Commit tagged as v1.1.0Universal CI automatically publishes to npm when changes are pushed to main.
- Create an npm account at https://npmjs.com
- Generate an access token (with publish permission)
- Add it to GitHub: Repository Settings β Secrets and variables β New repository secret
- Name:
NPM_TOKEN - Value: Your npm access token
- Name:
Every push to main that modifies package.json triggers automatic publishing:
-
Manual version bump in
package.json:npm version patch # or minor, major git push origin main -
Automatic workflow:
- Detects version change in package.json
- Publishes to npm with provenance
- Creates GitHub release
- Tags commit with version
Edit package.json to increment the version:
{
"version": "1.0.1" // Change this, push to main
}The workflow automatically publishes and creates GitHub releases at:
- https://www.npmjs.com/package/@orchestrate-solutions/universal-ci
- https://github.com/orchestrate-solutions/universal-ci/releases
Simple, Predictable, Everywhere
- No Magic: Tasks are just shell commands with working directories
- No Dependencies: Works with any language, framework, or tooling
- No Lock-in: Use it locally, in GitHub Actions, or anywhere else
- No Complexity: JSON configuration that anyone can understand
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Ensure all tests pass:
./run-ci.sh - Submit a pull request
Licensed under the Apache License, Version 2.0. See LICENSE for details.
Universal CI: Because CI/CD should be as simple as writing a config file. π