Apiforge
Production-grade API release automation CLI.
From merged code to healthy pods in production — one command.
Table of Contents
- 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
What Apiforge does
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.
Core concepts
Steps
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.
Orchestrator
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.
Automatic rollback
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.
Smart rollback targeting
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.
Audit store
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
--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.
Environment and secret resolution
${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:
[]
= "${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.
CloudFront invalidation
With a [cloudfront] section configured, a cloudfront-invalidate step runs
after the Kubernetes rollout so clients stop receiving stale cached responses:
[]
= "E1ABCD23EFGH45"
= ["/api/*"] # defaults to ["/*"]
Installation
Prerequisites
- 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)
Option 1: Install from Cargo
Option 2: Download release archives
# Linux (x86_64 / amd64)
# Linux (arm64)
# macOS (Apple Silicon)
# macOS (Intel / amd64)
Windows artifact is published as apiforge-windows-amd64.zip.
Option 3: Build from source
Quick start
1. Initialize config
This creates apiforge.toml with defaults.
2. Validate setup
3. Preview a release (no side effects)
4. Execute release
5. Inspect history and status
CLI reference
Global flags:
--config <path>: config file path (default:apiforge.toml)--debug: enable debug logs (APIFORGE_DEBUG=truealso works)
apiforge init
Initializes a new config file and adds .apiforge/ (the local audit store)
to .gitignore.
apiforge doctor
Checks:
- required tools (
git,docker,kubectl,aws) - config file parse/validation
- repository visibility/basic git status
apiforge release <major|minor|patch>
Runs the release pipeline.
Flags:
| 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 |
apiforge rollback
Rolls the Kubernetes deployment image back to a target version, then verifies the configured health check (if any).
Auto-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.
apiforge history
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 status
Shows project metadata, git HEAD/tag, and Kubernetes deployment image/replica state.
Release pipeline behavior
When 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.
Rollback semantics
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.
Configuration reference (apiforge.toml)
Full example
[]
= "my-api"
= "rust" # rust | node | python | go | java
[]
= "main"
= "v{version}"
= true
= "chore: release v{{ version }}"
= "origin"
= true
= true
= 60
= 120
= 30
[]
= "aws_ecr" # aws_ecr | docker_hub | ghcr | custom
= "my-api"
= "Dockerfile"
= "."
= ["{version}", "{major}.{minor}", "latest", "{git_sha}"]
# build_args = { APP_ENV = "production" }
[]
= "production"
= "default"
= "my-api"
= "k8s/deployment.yaml"
= ".spec.template.spec.containers[0].image"
= 300
= 100
[]
= "us-east-1"
# profile = "prod"
[]
= "org/repo"
= "${GITHUB_TOKEN}"
= true
= false
= false
[]
= "${SLACK_WEBHOOK_URL}"
= "{{ status_emoji }} Release {{ version }} of {{ project }}: {{ status }}"
= "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 }}\"}"
[]
= "https://api.example.com/health"
= "GET" # GET | POST | HEAD | PUT
= 200
# expected_body_field = "/status"
# expected_body_value = "ok"
= 60
= 5
Field details
[project]
| 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) |
[git]
| 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 |
[docker]
| 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}
[kubernetes]
| 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 |
[aws]
| 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 |
[cloudfront] (optional)
| Key | Type | Default | Notes |
|---|---|---|---|
distribution_id |
string | none | CloudFront distribution to invalidate after rollout |
paths |
array | ["/*"] |
Paths to invalidate; each must start with / |
[github] (optional)
| 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 |
[notifications] (optional)
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 |
[health_check] (optional)
| 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 |
Template variables
Apiforge uses templates in multiple places. Available keys depend on context:
Commit message templates (git.commit_message)
{{ version }}{{ project }}
Docker tag templates (docker.tags)
{version},{major},{minor},{patch},{git_sha},{git_sha_full}
Notification templates (message/body)
Commonly provided:
{{ version }}{{ project }}{{ status }}{{ status_emoji }}
Health-check templates (health_check.url, expected_body_value)
{{ version }}{{ project }}
CI/CD integration
GitHub Actions (example)
name: Release via Apiforge
on:
workflow_dispatch:
inputs:
bump:
description: "Version bump type"
required: true
default: "patch"
type: choice
options:
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
Security and reliability model
Built-in protections
- 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.
Audit storage
- Location:
.apiforge/audit - Retention: bounded record count
- Supports compaction and retry-aware writes
Vulnerability scanning
Use:
If advisories are intentionally suppressed due transitive ecosystem constraints, they are documented in .cargo/audit.toml.
Developer guide
Repository structure
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)
Local quality gates
Troubleshooting
git.tag_format must contain {version}
Your [git].tag_format is invalid. Use a format like:
= "v{version}"
Health-check never succeeds
Check:
- endpoint URL and network reachability
- method (
GET/POST/HEAD/PUT) - expected status code
- optional JSON pointer/value match
- timeout/interval values
ECR or AWS auth issues
Verify:
- correct
aws.region - IAM credentials/profile
- ability to call STS/ECR
Kubernetes rollout timeout
Check deployment events and image pull/access:
Known limitations
- 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.
Contributing
See CONTRIBUTING.md.
License
MIT — see LICENSE.