npm package: @kinwan/infrascript
Description: Typed TypeScript DSL for generating GitHub Actions and Kubernetes YAML
Latest version: 0.1.1

# infrascript

Typed TypeScript DSL for generating GitHub Actions workflows and Kubernetes manifests.

Write your infrastructure config once in TypeScript — fully typed, with autocomplete and compile-time validation — and let infrascript emit the YAML files exactly where they need to go.

```bash
npx infrascript build
# → .github/workflows/ci.yml
# → .github/workflows/deploy.yml
# → k8s/api-configmap.yaml
# → k8s/api-secret.yaml
# → k8s/api-pvc.yaml
# → k8s/api-deployment.yaml
# → k8s/api-service.yaml
# → k8s/api-ingress.yaml
```

---

## Why

Writing YAML by hand has real costs:

- No type safety — invalid fields and typos fail silently until runtime
- Cross-manifest references (service names, secret names, label selectors) break when you rename things
- K8s requires 5–7 separate files for a single app — all manually kept in sync
- GitHub Actions has no guard against referencing a job that doesn't exist

infrascript fixes all of this. Your config is TypeScript, so your editor catches errors before `kubectl apply` does.

---

## Installation

```bash
npm install -D infrascript ts-node
```

---

## Quick start

Create `infrascript.config.ts` at your project root:

```typescript
import { defineConfig, ActionsWorkflow, K8sApp } from "infrascript";

const ci = new ActionsWorkflow("ci")
  .on("push", { branches: ["main"] })
  .on("pull_request")
  .job("test", (j) =>
    j
      .runsOn("ubuntu-latest")
      .step("Checkout").uses("actions/checkout@v4")
      .step("Install").run("npm ci")
      .step("Test").run("npm test")
  );

const api = new K8sApp("api")
  .image("ghcr.io/myorg/api:latest")
  .replicas(2)
  .port(3000)
  .env({ NODE_ENV: "production" })
  .secret({ DB_PASSWORD: "replace-me" })
  .ingress({ host: "api.myapp.com", tls: true });

export default defineConfig({
  actions: { ci },
  k8s: { apps: { api } },
});
```

Then run:

```bash
npx infrascript build
```

Files are written in-place — `.github/workflows/` and `k8s/` — exactly where GitHub and `kubectl` expect them. Directories are created automatically if they don't exist. Existing files are overwritten; git is your undo.

---

## CLI

```bash
npx infrascript build       # generate all YAML files
npx infrascript validate    # validate config without writing files
```

Build output is colored by result:

```
  ✓ created   .github/workflows/ci.yml
  ↻ updated   k8s/api-deployment.yaml
  – unchanged k8s/api-service.yaml
```

---

## GitHub Actions

### `ActionsWorkflow`

```typescript
const deploy = new ActionsWorkflow("deploy")
  .on("push", { branches: ["main"] })
  .on("schedule", "0 2 * * *")
  .on("workflow_dispatch")
  .permissions({ contents: "read", packages: "write" })
  .concurrency("deploy-${{ github.ref }}", true)
  .env({ NODE_ENV: "production" })
  .job("build", (j) => j
    .runsOn("ubuntu-latest")
    .step("Checkout").uses("actions/checkout@v4")
    .step("Build").run("npm run build")
  )
  .job("deploy", (j) => j
    .runsOn("ubuntu-latest")
    .needs("build")
    .step("Deploy").run("./scripts/deploy.sh")
  );
```

Outputs to `.github/workflows/deploy.yml`.

### `Job`

| Method | Description |
|---|---|
| `.runsOn(runner)` | Runner label — typed union of all GitHub-hosted runners |
| `.needs(...jobIds)` | Job dependencies — must be IDs defined in the same workflow |
| `.step(name)` | Add a step, returns `Step` for chaining |
| `.env(vars)` | Job-level env vars |
| `.permissions(perms)` | Job-level permissions |
| `.matrix(matrix)` | Build matrix strategy |
| `.timeoutMinutes(n)` | Job timeout |
| `.if(condition)` | Conditional expression |

### `Step`

```typescript
// Use a community action
j.step("Login").uses("docker/login-action@v3", {
  registry: "ghcr.io",
  username: "${{ github.actor }}",
  password: "${{ secrets.GITHUB_TOKEN }}",
});

// Run a shell command
j.step("Test").run("npm test");

// With extras
j.step("Deploy")
  .run("kubectl apply -f k8s/")
  .env({ KUBECONFIG: "${{ secrets.KUBECONFIG }}" })
  .if("github.ref == 'refs/heads/main'")
  .workingDirectory("./infra");
```

---

## Kubernetes

### `K8sApp` — recommended

`K8sApp` is a high-level facade. Define your app once and it generates all required manifests with names and cross-references kept consistent automatically.

```typescript
const api = new K8sApp("api")
  .image("ghcr.io/myorg/api:latest")
  .replicas(2)
  .port(3000)
  .namespace("production")
  .imagePullPolicy("Always")
  .env({ NODE_ENV: "production", LOG_LEVEL: "info" })
  .secret({ DB_PASSWORD: "replace-me", JWT_SECRET: "replace-me" })
  .resources({
    requests: { cpu: "100m", memory: "128Mi" },
    limits:   { cpu: "500m", memory: "512Mi" },
  })
  .ingress({ host: "api.myapp.com", tls: true, ingressClassName: "nginx" })
  .volume({ mountPath: "/data", size: "5Gi" });
```

This single definition emits:

| File | What |
|---|---|
| `k8s/api-configmap.yaml` | Plain env vars from `.env()` |
| `k8s/api-secret.yaml` | Secret env vars from `.secret()` |
| `k8s/api-pvc.yaml` | Persistent volume claim from `.volume()` |
| `k8s/api-deployment.yaml` | Deployment with env refs wired to ConfigMap and Secret |
| `k8s/api-service.yaml` | ClusterIP Service |
| `k8s/api-ingress.yaml` | Ingress with optional TLS |

Cross-references — ConfigMap name in envFrom, Secret name in envFrom, PVC name in volumeMounts — are guaranteed consistent because they all derive from the app name.

> **Note on secrets:** `.secret()` values are placeholders. The generated Secret uses `stringData` so you can fill in real values before applying, or replace the file with a sealed secret. Never commit real secret values.

### Low-level escape hatch

For advanced cases, use individual manifest builders directly. Each emits exactly one file.

```typescript
import {
  defineConfig,
  Deployment, K8sService, Ingress,
  ConfigMap, Secret, PVC,
  secretRef, configMapRef,
} from "infrascript";

const worker = new Deployment("worker", "ghcr.io/myorg/worker:latest")
  .replicas(3)
  .port(8080)
  .env({ QUEUE: "jobs" })
  .envFrom("DB_PASS", secretRef("db-secret", "password"))
  .resources({ limits: { cpu: "200m", memory: "256Mi" } })
  .livenessProbe({
    httpGet: { path: "/healthz", port: 8080 },
    initialDelaySeconds: 10,
    periodSeconds: 5,
  });

const workerSvc = new K8sService("worker")
  .type("ClusterIP")
  .port(8080);

export default defineConfig({
  k8s: {
    manifests: { worker, workerSvc },
  },
});
```

### Mixing both

```typescript
export default defineConfig({
  k8s: {
    apps: {
      api,      // high-level: emits 6 files automatically
    },
    manifests: {
      worker,   // low-level: one deployment, explicit control
      workerSvc,
    },
  },
});
```

---

## Output paths

| Config | Output path |
|---|---|
| `actions: { ci }` | `.github/workflows/ci.yml` |
| `actions: { deploy }` | `.github/workflows/deploy.yml` |
| `k8s.apps: { api }` | `k8s/api-*.yaml` (one per manifest type) |
| `k8s.manifests: { worker }` (Deployment) | `k8s/worker-deployment.yaml` |
| `k8s.manifests: { workerSvc }` (Service) | `k8s/worker-service.yaml` |

Namespaced resources are placed in a subdirectory:

```
k8s/production/api-deployment.yaml
k8s/production/api-service.yaml
```

---

## Design philosophy

**One source of truth.** Your TypeScript config is the authoritative definition of your infrastructure. The YAML files are build artifacts — generated, not hand-maintained.

**Files go where tools expect them.** No intermediate output folder to copy from. `.github/workflows/` is where GitHub reads Actions. `k8s/` is where you point `kubectl apply -f`. infrascript writes there directly.

**Git is the safety net.** infrascript overwrites existing files without prompting. Review diffs in git before committing. This mirrors how most YAML changes are reviewed anyway.

**Newbie-friendly, expert-capable.** `K8sApp` covers the common case with zero K8s knowledge required. The low-level builders are there when you need full control.

---

## Roadmap

- `infrascript diff` — show pending changes without writing
- `infrascript watch` — regenerate on config file change
- HorizontalPodAutoscaler builder
- Helm values.yaml output target
- JSON Schema export for editor validation