# Apiforge

> **Production-grade API release automation CLI**.  
> From merged code to healthy pods in production — one command.

[![Rust](https://img.shields.io/badge/rust-1.91.1%2B-orange.svg)](https://www.rust-lang.org/)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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`):

| 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](#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:

| 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` | 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.

| 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

```toml
[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
```

### 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<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]`

| 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<string> | `["/*"]` | 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)

```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).
