How it works
Add a deploy.yml workflow to your repository. The workflow runs your existing quality checks, then calls waft release <app-name> as the final step. Waft auto-detects the runtime from the files you deploy.
Deploy only on green. Waft deploys as the final step — after lint and tests pass. A failing test stops the pipeline before any code reaches production.
Step 1 — Get your API token
-
-
Add it as a GitHub secret
In your GitHub repository: Settings → Secrets and variables → Actions → New repository secret.
Name it WAFT_TOKEN.
Step 2 — Add the workflow
Choose your runtime below and copy the workflow into .github/workflows/deploy.yml.
🐍
Python
Go
Go
JS
Node / TypeScript
Python deploy workflow
Runs ruff for linting and pytest for tests, then deploys. Lambda entry point is handler.py with def handler(event, context).
.github/workflows/deploy.ymlname: Deploy to waft
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install dependencies
run: pip install -e ".[dev]"
- name: Lint
run: ruff check .
- name: Test
run: pytest
- name: Install waft
run: |
curl -sSL https://github.com/waft-dev/waft/releases/latest/download/waft-linux-amd64 \
-o /usr/local/bin/waft
chmod +x /usr/local/bin/waft
- name: Deploy
env:
WAFT_TOKEN: ${{ secrets.WAFT_TOKEN }}
run: waft release my-python-app
Replace my-python-app with your app name — a lowercase slug, e.g. my-api or data-processor. The app is created automatically on first deploy.
What gets deployed?
waft release zips the current directory and uploads it. By default it deploys . — your whole project. To deploy a subdirectory only, pass the path: waft release my-app ./src.
Handler structure
handler.py
def handler(event, context):
return {
"statusCode": 200,
"body": "Hello from waft!",
}
Go deploy workflow
Runs golangci-lint and go test, cross-compiles for Linux, then deploys. The bootstrap binary triggers the provided.al2023 custom runtime.
Cross-compile for Linux. Always build with GOOS=linux GOARCH=amd64 before deploying — a macOS or Windows binary will not run on Lambda.
.github/workflows/deploy.ymlname: Deploy to waft
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Lint
uses: golangci/golangci-lint-action@v6
with:
version: latest
- name: Test
run: go test ./...
- name: Build Lambda binary
run: GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o bootstrap .
- name: Install waft
run: |
curl -sSL https://github.com/waft-dev/waft/releases/latest/download/waft-linux-amd64 \
-o /usr/local/bin/waft
chmod +x /usr/local/bin/waft
- name: Deploy
env:
WAFT_TOKEN: ${{ secrets.WAFT_TOKEN }}
run: waft release my-go-app
Strip debug symbols with -ldflags="-s -w" to keep the binary small. A typical Go Lambda goes from ~11 MB to ~3.3 MB compressed — well within Lambda limits.
Handler structure
main.go
package main
import (
"encoding/json"
"fmt"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"message": "Hello from waft!"})
}
func main() {
fmt.Println("Starting...")
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
Node.js / TypeScript deploy workflow
Runs ESLint, tests, compiles TypeScript, then deploys. The index.js entry point triggers the nodejs20.x runtime.
.github/workflows/deploy.ymlname: Deploy to waft
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Test
run: npm test
- name: Build
run: npm run build
- name: Install waft
run: |
curl -sSL https://github.com/waft-dev/waft/releases/latest/download/waft-linux-amd64 \
-o /usr/local/bin/waft
chmod +x /usr/local/bin/waft
- name: Deploy
env:
WAFT_TOKEN: ${{ secrets.WAFT_TOKEN }}
run: waft release my-node-app ./dist
Deploy the compiled output. The waft release my-node-app ./dist command deploys only the dist/ directory. For plain JavaScript projects without a build step, use waft release my-node-app . instead.
Handler structure
index.ts (compiled to dist/index.js)
export const handler = async (event: any) => {
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: 'Hello from waft!' }),
};
};
Runtime auto-detection
Waft detects the Lambda runtime from the files included in the deployment zip:
| File present in zip | Runtime used |
| bootstrap | provided.al2023 — Go, Rust, or any custom binary |
| index.js | nodejs20.x |
| (anything else) | python3.12 (default) |
Tips
- The waft binary is ~10 MB and downloads in under a second — no caching needed.
waft release exits non-zero on deploy failure, so CI fails automatically on errors.
- The app is created automatically on first deploy — no separate provisioning step required.
- The app name becomes the subdomain:
my-app → https://my-app.waft.dev.
- To deploy on tagged releases instead of every push, change
branches: [main] to tags: ['v*'].