Production-grade API release automation CLI.
From merged code to healthy pods in production — one command.
- What Apiforge does
- Core concepts
- Installation
- Quick start
- CLI reference
- Release pipeline behavior
- Rollback semantics
- Configuration reference (
apiforge.toml) - Template variables
- CI/CD integration
- Security and reliability model
- Developer guide
- Troubleshooting
- Known limitations
- Contributing
- License
Apiforge automates a full release path for API services:
- Preflight checks for repo and environment.
- Version bump in language-specific version files.
- Optional changelog generation.
- Commit and tag creation.
- Push to git remote.
- Optional Docker build/push.
- Optional Kubernetes image update and rollout wait.
- Optional GitHub release creation.
- Optional health-check verification.
- Automatic rollback of completed steps when a later step fails.
The goal is to make releases repeatable, reviewable, and recoverable.
Every run also:
- streams live progress (spinners with Docker build output, rollout replica counts, health-check attempts) on interactive terminals, degrading to plain lines in CI;
- records an audit entry with per-step results, total duration, and final status (
success,failed,rolled_back); - sends notifications (Slack and/or generic webhook) on success and failure, honoring
notify_on.
Everything the pipeline does is a step — a unit implementing one contract
(src/steps/mod.rs):
| Method | Purpose |
|---|---|
validate() |
Pre-flight checks before anything runs (tooling, auth, state) |
execute() |
The real work |
dry_run() |
Simulation with rich previews (file diffs, image tags, layer estimates) |
rollback() |
Undo a previously successful execution (optional; default no-op) |
Concrete steps: git-preflight, version-bump, changelog, git-commit,
git-tag, git-push, docker-build, docker-push, k8s-update,
k8s-rollout, github-release, health-check, plus Slack/webhook notifiers.
The orchestrator (src/orchestrator/) runs validate() for all steps
first (fail fast before any mutation), then executes steps in order, timing
each. On failure it rolls back completed steps in reverse order, then
returns a RunReport carrying every step's outcome — including the failed
one — so audit records and JSON output reflect what actually happened.
Each step knows how to undo itself. The version bump restores the original file bytes (preserving any unrelated edits), the commit soft-resets, tags are deleted locally and remotely, the Kubernetes deployment reverts to its previous ReplicaSet revision, and a created GitHub release is deleted. Pushed commits are deliberately not force-rewritten — see Rollback semantics.
apiforge rollback without --to picks the newest version older than what
is currently deployed (read from the deployment's image tag), preferring
successful releases from the audit history and falling back to semver git
tags. After the rollout it re-runs the configured health check.
Release history lives in an embedded sled
database under .apiforge/audit (git-ignored; never trips the clean-tree
check). Records are capped, compactable, and queryable via
apiforge history — including failed and rolled-back releases.
--dry-run simulates every step with no side effects: no audit record, no
notifications, no .apiforge/ directory, working tree untouched. Previews
include version-file diffs, resolved image tags, and changelog content.
${VAR} references in secret-bearing config fields (GitHub token/repository,
notification URLs/headers/bodies, health-check URL) are resolved when the
config loads. A missing variable fails immediately, naming the variable —
instead of surfacing later as an opaque auth error mid-release.
The same fields also support AWS SSM Parameter Store references:
[github]
token = "${ssm:/myapp/github-token}"Parameters are fetched (with decryption) at release time using the standard
AWS credential chain. Projects without ${ssm:...} references never touch
AWS, and --dry-run never requires AWS credentials.
With a [cloudfront] section configured, a cloudfront-invalidate step runs
after the Kubernetes rollout so clients stop receiving stale cached responses:
[cloudfront]
distribution_id = "E1ABCD23EFGH45"
paths = ["/api/*"] # defaults to ["/*"]- Rust
1.91.1+(for building/running from source) gitdocker(if using Docker steps)kubectl(if using Kubernetes steps)awsCLI credentials/profile (if using ECR)
cargo install apiforge# Linux (x86_64 / amd64)
curl -L https://github.com/PrazwalR/Apiforge/releases/latest/download/apiforge-linux-amd64.tar.gz -o apiforge.tar.gz
tar -xzf apiforge.tar.gz
chmod +x apiforge
sudo mv apiforge /usr/local/bin/
# Linux (arm64)
curl -L https://github.com/PrazwalR/Apiforge/releases/latest/download/apiforge-linux-arm64.tar.gz -o apiforge.tar.gz
tar -xzf apiforge.tar.gz
chmod +x apiforge
sudo mv apiforge /usr/local/bin/
# macOS (Apple Silicon)
curl -L https://github.com/PrazwalR/Apiforge/releases/latest/download/apiforge-darwin-arm64.tar.gz -o apiforge.tar.gz
tar -xzf apiforge.tar.gz
chmod +x apiforge
sudo mv apiforge /usr/local/bin/
# macOS (Intel / amd64)
curl -L https://github.com/PrazwalR/Apiforge/releases/latest/download/apiforge-darwin-amd64.tar.gz -o apiforge.tar.gz
tar -xzf apiforge.tar.gz
chmod +x apiforge
sudo mv apiforge /usr/local/bin/Windows artifact is published as apiforge-windows-amd64.zip.
git clone https://github.com/PrazwalR/Apiforge.git
cd Apiforge
cargo build --release --locked
./target/release/apiforge --versionapiforge initThis creates apiforge.toml with defaults.
apiforge doctorapiforge release patch --dry-runapiforge release patchapiforge history --limit 20
apiforge statusGlobal flags:
--config <path>: config file path (default:apiforge.toml)--debug: enable debug logs (APIFORGE_DEBUG=truealso works)
Initializes a new config file and adds .apiforge/ (the local audit store)
to .gitignore.
apiforge init [--name my-service] [--force]Checks:
- required tools (
git,docker,kubectl,aws) - config file parse/validation
- repository visibility/basic git status
apiforge doctorRuns the release pipeline.
apiforge release patch \
--dry-run \
--skip-docker \
--skip-k8s \
--skip-github \
--skip-notify \
--no-changelog \
--output json \
--yesFlags:
| Flag | Meaning |
|---|---|
--dry-run |
Simulate pipeline steps without mutating systems |
--skip-docker |
Skip Docker build and push steps |
--skip-k8s |
Skip Kubernetes update and rollout wait |
--skip-cloudfront |
Skip CloudFront cache invalidation |
--skip-github |
Skip GitHub release step |
--skip-notify |
Skip post-release notification dispatch |
--no-changelog |
Skip changelog step even if enabled in config |
| `--output text | json` |
-y, --yes |
Skip confirmation prompt |
Rolls the Kubernetes deployment image back to a target version, then verifies the configured health check (if any).
apiforge rollback # auto-detects the target version
apiforge rollback --dry-run # preview, no cluster access needed
apiforge rollback --to v1.2.3 # explicit target
apiforge rollback --yes # skip confirmation promptAuto-detection picks the newest version older than the currently deployed one
(read from the deployment's image tag), using successful releases from the
audit history first and semver git tags as a fallback. If no older candidate
exists, the command fails with guidance to pass --to.
Reads audit records from .apiforge/audit. Each record carries per-step
results, total duration, and final status. The failed filter includes
rolled-back releases (failures that were recovered automatically).
apiforge history --limit 50 --filter success --output text
apiforge history --filter failed
apiforge history --output jsonShows project metadata, git HEAD/tag, and Kubernetes deployment image/replica state.
apiforge statusWhen you run apiforge release <bump>, step order is:
git-preflightversion-bumpchangelog(if enabled and not skipped)git-commitgit-taggit-pushdocker-build(if not skipped)docker-push(if not skipped)k8s-update(if not skipped)k8s-rollout(if not skipped)cloudfront-invalidate(if configured and not skipped)github-release(if configured and not skipped)health-check(if configured)
After the pipeline finishes, Apiforge sends configured notifications (Slack and/or
generic webhook, honoring notify_on for success/failure) and records a release
audit entry with per-step results, total duration, and final status
(success, failed, or rolled_back). Dry-runs send no notifications and are
not recorded.
Environment variable references like ${GITHUB_TOKEN} in apiforge.toml
(GitHub token/repository, notification URLs/bodies/headers, health-check URL)
are resolved at config load; a missing variable fails fast with its name.
Automatic rollback is triggered when a step fails after prior steps succeeded. Rollback runs in reverse order for completed steps.
| Step | Rollback behavior |
|---|---|
version-bump |
Restores original version-file content captured before mutation |
changelog |
Restores CHANGELOG.md from git checkout |
git-commit |
Soft reset to parent commit (changes remain staged) |
git-tag |
Deletes created tag |
git-push |
Deletes remote/local tag; intentionally does not force-rewrite shared commit history |
github-release |
Deletes created GitHub release when possible |
| docker/k8s/health | Step-specific best-effort behavior or no-op if not applicable |
Important design choice: on git-push rollback, commit history is preserved and only release marker tags are removed.
[project]
name = "my-api"
language = "rust" # rust | node | python | go | java
[git]
main_branch = "main"
tag_format = "v{version}"
changelog = true
commit_message = "chore: release v{{ version }}"
remote = "origin"
require_clean = true
require_main_branch = true
fetch_timeout_secs = 60
push_timeout_secs = 120
operation_timeout_secs = 30
[docker]
registry = "aws_ecr" # aws_ecr | docker_hub | ghcr | custom
repository = "my-api"
dockerfile = "Dockerfile"
context = "."
tags = ["{version}", "{major}.{minor}", "latest", "{git_sha}"]
# build_args = { APP_ENV = "production" }
[kubernetes]
context = "production"
namespace = "default"
deployment = "my-api"
manifest_path = "k8s/deployment.yaml"
image_field = ".spec.template.spec.containers[0].image"
rollout_timeout = 300
min_ready_percent = 100
[aws]
region = "us-east-1"
# profile = "prod"
[github]
repository = "org/repo"
token = "${GITHUB_TOKEN}"
create_release = true
prerelease = false
draft = false
[notifications.slack]
webhook_url = "${SLACK_WEBHOOK_URL}"
message = "{{ status_emoji }} Release {{ version }} of {{ project }}: {{ status }}"
notify_on = "both" # success | failure | both
# Optional generic webhook payload
# [notifications.webhook]
# url = "https://hooks.example.com/release"
# method = "POST"
# headers = { "Authorization" = "Bearer ${WEBHOOK_TOKEN}" }
# body = "{\"project\":\"{{ project }}\",\"version\":\"{{ version }}\",\"status\":\"{{ status }}\"}"
[health_check]
url = "https://api.example.com/health"
method = "GET" # GET | POST | HEAD | PUT
expected_status = 200
# expected_body_field = "/status"
# expected_body_value = "ok"
timeout = 60
interval = 5| Key | Type | Required | Notes |
|---|---|---|---|
name |
string | yes | Displayed in output/messages |
language |
enum | yes | Determines version file (Cargo.toml, package.json, pyproject.toml, go.mod, pom.xml) |
| Key | Type | Default | Notes |
|---|---|---|---|
main_branch |
string | none | Expected release branch |
tag_format |
string | none | Must include {version} |
changelog |
bool | true |
Enable changelog step |
commit_message |
string | none | Supports {{ version }} / {{ project }} |
remote |
string | origin |
Target remote |
require_clean |
bool | true |
Require no unstaged/uncommitted changes |
require_main_branch |
bool | true |
Require release from main_branch |
fetch_timeout_secs |
u64 | 60 |
Timeout for fetch-like operations |
push_timeout_secs |
u64 | 120 |
Timeout for push operations |
operation_timeout_secs |
u64 | 30 |
Timeout for other git operations |
| Key | Type | Default | Notes |
|---|---|---|---|
registry |
enum | none | aws_ecr, docker_hub, ghcr, custom |
repository |
string | none | Required non-empty |
dockerfile |
string | Dockerfile |
Relative to context |
context |
string | . |
Build context path |
tags |
array | none | At least one tag pattern required |
build_args |
table | none | Optional build args |
Docker tag placeholders supported by validation/runtime:
{version}{major}{minor}{patch}{git_sha}{git_sha_full}
| Key | Type | Default | Notes |
|---|---|---|---|
context |
string | none | kube context name |
namespace |
string | none | Required non-empty |
deployment |
string | none | Deployment to patch |
manifest_path |
string | none | Maintained for manifest-oriented workflows |
image_field |
string | none | JSON pointer-like selector for image path |
rollout_timeout |
u64 | 300 |
Max seconds for rollout wait |
min_ready_percent |
u8 | 100 |
Must be 0..=100 |
| Key | Type | Required | Notes |
|---|---|---|---|
region |
string | yes for ECR/CloudFront/SSM | Required when docker.registry = "aws_ecr", [cloudfront] is set, or ${ssm:...} references are used |
profile |
string | no | Optional AWS profile |
| Key | Type | Default | Notes |
|---|---|---|---|
distribution_id |
string | none | CloudFront distribution to invalidate after rollout |
paths |
array | ["/*"] |
Paths to invalidate; each must start with / |
| Key | Type | Default | Notes |
|---|---|---|---|
repository |
string | none | owner/repo |
token |
string | none | GitHub token |
create_release |
bool | true |
Kept for compatibility |
prerelease |
bool | false |
GitHub prerelease flag |
draft |
bool | false |
GitHub draft flag |
Slack:
| Key | Type | Default |
|---|---|---|
webhook_url |
string | none |
message |
string | none |
notify_on |
enum | both |
Webhook:
| Key | Type | Default |
|---|---|---|
url |
string | none |
method |
string | POST |
headers |
table | none |
body |
string | none |
| Key | Type | Default | Notes |
|---|---|---|---|
url |
string | none | Required if section present |
method |
enum | GET |
GET, POST, HEAD, PUT |
expected_status |
u16 | 200 |
Expected HTTP status |
expected_body_field |
string | none | JSON pointer path (e.g. /status) |
expected_body_value |
string | none | Compared against resolved response field |
timeout |
u64 | 60 |
Total check window |
interval |
u64 | 5 |
Retry interval, must be > 0 |
Apiforge uses templates in multiple places. Available keys depend on context:
{{ version }}{{ project }}
{version},{major},{minor},{patch},{git_sha},{git_sha_full}
Commonly provided:
{{ version }}{{ project }}{{ status }}{{ status_emoji }}
{{ version }}{{ project }}
name: Release via Apiforge
on:
workflow_dispatch:
inputs:
bump:
description: "Version bump type"
required: true
default: "patch"
type: choice
options: [patch, minor, major]
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Apiforge
run: |
curl -L https://github.com/PrazwalR/Apiforge/releases/latest/download/apiforge-linux-amd64.tar.gz -o apiforge.tar.gz
tar -xzf apiforge.tar.gz
chmod +x apiforge
sudo mv apiforge /usr/local/bin/
- name: Run release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: apiforge release ${{ inputs.bump }} --yes- Config validation before release execution.
- Timeout wrappers around network-prone git operations.
- Automatic rollback orchestration for completed steps.
- Sanitization of sensitive data in rendered/logged error messages.
- Audit log persistence under
.apiforge/audit.
- Location:
.apiforge/audit - Retention: bounded record count
- Supports compaction and retry-aware writes
Use:
cargo auditIf advisories are intentionally suppressed due transitive ecosystem constraints, they are documented in .cargo/audit.toml.
src/
cli.rs # CLI definition
config.rs # Config model + validation
orchestrator/ # Pipeline execution + rollback orchestration
steps/ # Concrete step implementations
git/
docker/
kubernetes/
github/
health/
integrations/ # Service clients (git, docker, k8s, aws, github)
audit/ # Release history store
output/ # CLI output rendering
utils/ # Helpers (semver/template/retry/sanitize/version)
cargo fmt --all -- --check
cargo test --all-features --locked
cargo clippy --locked --all-targets --all-features -- -D warnings
cargo build --release --locked
cargo doc --no-deps --locked
cargo bench --no-run --locked
cargo auditYour [git].tag_format is invalid. Use a format like:
tag_format = "v{version}"Check:
- endpoint URL and network reachability
- method (
GET/POST/HEAD/PUT) - expected status code
- optional JSON pointer/value match
- timeout/interval values
Verify:
- correct
aws.region - IAM credentials/profile
- ability to call STS/ECR
Check deployment events and image pull/access:
kubectl -n <namespace> describe deploy <name>
kubectl -n <namespace> get pods
kubectl -n <namespace> logs <pod>- Git push rollback intentionally avoids force-rewriting remote commit history; it removes release tags instead.
- Rollback auto-detection needs either local audit history or semver git tags; on a machine that has neither, pass
--to <version>explicitly. - Multi-environment config profiles (e.g. staging vs production overlays) are not yet supported — use separate config files with
--config.
See CONTRIBUTING.md.
MIT — see LICENSE.