# Apiforge
> **Production-grade API release automation CLI**.
> From merged code to healthy pods in production — one command.
[](https://www.rust-lang.org/)
[](LICENSE)
---
## Table of Contents
1. [What Apiforge does](#what-apiforge-does)
2. [Core concepts](#core-concepts)
3. [Installation](#installation)
4. [Quick start](#quick-start)
5. [CLI reference](#cli-reference)
6. [Release pipeline behavior](#release-pipeline-behavior)
7. [Rollback semantics](#rollback-semantics)
8. [Configuration reference (`apiforge.toml`)](#configuration-reference-apiforgetoml)
9. [Template variables](#template-variables)
10. [CI/CD integration](#cicd-integration)
11. [Security and reliability model](#security-and-reliability-model)
12. [Developer guide](#developer-guide)
13. [Troubleshooting](#troubleshooting)
14. [Known limitations](#known-limitations)
15. [Contributing](#contributing)
16. [License](#license)
---
## What Apiforge does
Apiforge automates a full release path for API services:
1. Preflight checks for repo and environment.
2. Version bump in language-specific version files.
3. Optional changelog generation.
4. Commit and tag creation.
5. Push to git remote.
6. Optional Docker build/push.
7. Optional Kubernetes image update and rollout wait.
8. Optional GitHub release creation.
9. Optional health-check verification.
10. 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`):
| `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](#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](https://github.com/spacejam/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:
```toml
[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.
### CloudFront invalidation
With a `[cloudfront]` section configured, a `cloudfront-invalidate` step runs
after the Kubernetes rollout so clients stop receiving stale cached responses:
```toml
[cloudfront]
distribution_id = "E1ABCD23EFGH45"
paths = ["/api/*"] # defaults to ["/*"]
```
---
## Installation
### Prerequisites
- Rust `1.91.1+` (for building/running from source)
- `git`
- `docker` (if using Docker steps)
- `kubectl` (if using Kubernetes steps)
- `aws` CLI credentials/profile (if using ECR)
### Option 1: Install from Cargo
```bash
cargo install apiforge
```
### Option 2: Download release archives
```bash
# 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`.
### Option 3: Build from source
```bash
git clone https://github.com/PrazwalR/Apiforge.git
cd Apiforge
cargo build --release --locked
./target/release/apiforge --version
```
---
## Quick start
### 1. Initialize config
```bash
apiforge init
```
This creates `apiforge.toml` with defaults.
### 2. Validate setup
```bash
apiforge doctor
```
### 3. Preview a release (no side effects)
```bash
apiforge release patch --dry-run
```
### 4. Execute release
```bash
apiforge release patch
```
### 5. Inspect history and status
```bash
apiforge history --limit 20
apiforge status
```
---
## CLI reference
Global flags:
- `--config <path>`: config file path (default: `apiforge.toml`)
- `--debug`: enable debug logs (`APIFORGE_DEBUG=true` also works)
### `apiforge init`
Initializes a new config file and adds `.apiforge/` (the local audit store)
to `.gitignore`.
```bash
apiforge init [--name my-service] [--force]
```
### `apiforge doctor`
Checks:
- required tools (`git`, `docker`, `kubectl`, `aws`)
- config file parse/validation
- repository visibility/basic git status
```bash
apiforge doctor
```
### `apiforge release <major|minor|patch>`
Runs the release pipeline.
```bash
apiforge release patch \
--dry-run \
--skip-docker \
--skip-k8s \
--skip-github \
--skip-notify \
--no-changelog \
--output json \
--yes
```
Flags:
| `--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` | Output mode (`json` emits only the JSON document on stdout — plan/progress go to stderr, so it pipes cleanly into `jq`) |
| `-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).
```bash
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 prompt
```
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).
```bash
apiforge history --limit 50 --filter success --output text
apiforge history --filter failed
apiforge history --output json
```
### `apiforge status`
Shows project metadata, git HEAD/tag, and Kubernetes deployment image/replica state.
```bash
apiforge status
```
---
## Release pipeline behavior
When you run `apiforge release <bump>`, step order is:
1. `git-preflight`
2. `version-bump`
3. `changelog` *(if enabled and not skipped)*
4. `git-commit`
5. `git-tag`
6. `git-push`
7. `docker-build` *(if not skipped)*
8. `docker-push` *(if not skipped)*
9. `k8s-update` *(if not skipped)*
10. `k8s-rollout` *(if not skipped)*
11. `cloudfront-invalidate` *(if configured and not skipped)*
12. `github-release` *(if configured and not skipped)*
13. `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.
| `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
```toml
[project]
name = "my-api"
[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]
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"
# expected_body_field = "/status"
# expected_body_value = "ok"
timeout = 60
interval = 5
```
### Field details
#### `[project]`
| `name` | string | yes | Displayed in output/messages |
| `language` | enum | yes | Determines version file (`Cargo.toml`, `package.json`, `pyproject.toml`, `go.mod`, `pom.xml`) |
#### `[git]`
| `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]`
| `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<string> | 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]`
| `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]`
| `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)*
| `distribution_id` | string | none | CloudFront distribution to invalidate after rollout |
| `paths` | array<string> | `["/*"]` | Paths to invalidate; each must start with `/` |
#### `[github]` *(optional)*
| `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:
| `webhook_url` | string | none |
| `message` | string | none |
| `notify_on` | enum | `both` |
Webhook:
| `url` | string | none |
| `method` | string | `POST` |
| `headers` | table | none |
| `body` | string | none |
#### `[health_check]` *(optional)*
| `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)
```yaml
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
```
---
## 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:
```bash
cargo audit
```
If advisories are intentionally suppressed due transitive ecosystem constraints, they are documented in `.cargo/audit.toml`.
---
## Developer guide
### Repository structure
```text
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
```bash
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 audit
```
---
## Troubleshooting
### `git.tag_format must contain {version}`
Your `[git].tag_format` is invalid. Use a format like:
```toml
tag_format = "v{version}"
```
### Health-check never succeeds
Check:
1. endpoint URL and network reachability
2. method (`GET`/`POST`/`HEAD`/`PUT`)
3. expected status code
4. optional JSON pointer/value match
5. 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:
```bash
kubectl -n <namespace> describe deploy <name>
kubectl -n <namespace> get pods
kubectl -n <namespace> logs <pod>
```
---
## 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](CONTRIBUTING.md).
---
## License
MIT — see [LICENSE](LICENSE).