Publishing via GitHub Actions
Instead of running aerostack publish manually from your machine, you can set up a GitHub Actions workflow so that every push to main (or every tagged release) automatically publishes your function to the Aerostack Hub.
This is the recommended workflow for teams that use GitHub as their primary source of truth.
How It Works
Developer pushes to GitHub
│
▼
GitHub Actions workflow triggers
│
▼
Reads aerostack.json + source files
│
▼
Calls POST /api/community/functions/ci/publish
with your AEROSTACK_API_KEY
│
▼
Aerostack validates, upserts, and snapshots the version
│
▼
Function is live on hub.aerostack.dev ✅Your aerostack.json in the repository becomes the single source of truth — no manual Hub UI steps are required after first publish.
Step-by-Step Setup
Get your Aerostack API key
Go to aerocall.ai/account/keys and create (or copy) an account key (starts with ac_).
Add the secret to GitHub
In your GitHub repo:
- Go to Settings → Secrets and variables → Actions
- Click New repository secret
- Name:
AEROSTACK_API_KEY - Value: your
ac_secret_xxxxkey
Structure your repository
Your function repo should look like this:
- aerostack.json
- index.ts
- README.md
Add the workflow file
Create .github/workflows/publish-aerostack.yml in your repository:
name: Publish to Aerostack Hub
on:
push:
branches:
- main
jobs:
publish:
name: Publish Function
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Read aerostack.json
id: config
run: |
echo "config=$(cat aerostack.json | tr -d '\n')" >> $GITHUB_OUTPUT
ENTRYPOINT=$(cat aerostack.json | jq -r '.entrypoint // "index.ts"')
echo "entrypoint=$ENTRYPOINT" >> $GITHUB_OUTPUT
- name: Read source file
id: source
run: |
ENTRYPOINT="${{ steps.config.outputs.entrypoint }}"
# Safely escape file content for JSON
CODE=$(cat "$ENTRYPOINT" | jq -Rs .)
echo "code=$CODE" >> $GITHUB_OUTPUT
- name: Publish to Aerostack
run: |
CONFIG='${{ steps.config.outputs.config }}'
CODE=${{ steps.source.outputs.code }}
PAYLOAD=$(jq -n \
--argjson config "$CONFIG" \
--argjson code "$CODE" \
'$config + {code: $code, publish: true}')
RESPONSE=$(curl -s -w "\n%{http_code}" \
-X POST https://api.aerocall.ai/api/community/functions/ci/publish \
-H "X-API-Key: ${{ secrets.AEROSTACK_API_KEY }}" \
-H "Content-Type: application/json" \
-d "$PAYLOAD")
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
BODY=$(echo "$RESPONSE" | head -1)
echo "Response: $BODY"
if [ "$HTTP_CODE" != "200" ] && [ "$HTTP_CODE" != "201" ]; then
echo "❌ Publish failed with HTTP $HTTP_CODE"
exit 1
fi
echo "✅ Published successfully!"
echo "$BODY" | jq .
env:
AEROSTACK_API_KEY: ${{ secrets.AEROSTACK_API_KEY }}Full Working Example
A complete repository you can fork:
my-rate-limiter/
├── .github/
│ └── workflows/
│ └── publish-aerostack.yml ← CI workflow
├── aerostack.json ← function config
├── index.ts ← function code
└── README.md{
"name": "rate-limiter",
"version": "1.0.0",
"description": "Sliding window rate limiting middleware for Hono apps. Uses Cloudflare KV for distributed state.",
"category": "utility",
"language": "typescript",
"runtime": "cloudflare-worker",
"license": "MIT",
"tags": ["rate-limit", "middleware", "security"],
"entrypoint": "index.ts",
"routePath": "/api/rate-limit",
"routeExport": "rateLimiterRoute",
"npmDependencies": [],
"envVars": ["RATE_LIMIT_MAX", "RATE_LIMIT_WINDOW_MS"],
"drizzleSchema": false
}import { Hono } from 'hono';
export interface RateLimitEnv {
KV: KVNamespace;
RATE_LIMIT_MAX?: string;
RATE_LIMIT_WINDOW_MS?: string;
}
export const rateLimiterRoute = new Hono<{ Bindings: RateLimitEnv }>();
/**
* Sliding window rate limiter using Cloudflare KV.
*
* Usage: Mount this route in your main app and call
* GET /rate-limit/check?key=<identifier>
*/
rateLimiterRoute.get('/check', async (c) => {
const key = c.req.query('key');
if (!key) return c.json({ error: 'key is required' }, 400);
const maxRequests = parseInt(c.env.RATE_LIMIT_MAX ?? '100');
const windowMs = parseInt(c.env.RATE_LIMIT_WINDOW_MS ?? '60000');
const now = Date.now();
const windowStart = now - windowMs;
const kvKey = `rl:${key}`;
const raw = await c.env.KV.get(kvKey);
const timestamps: number[] = raw ? JSON.parse(raw) : [];
// Remove expired timestamps
const active = timestamps.filter((ts) => ts > windowStart);
if (active.length >= maxRequests) {
const retryAfter = Math.ceil((active[0] + windowMs - now) / 1000);
return c.json(
{ allowed: false, remaining: 0, retryAfter },
429
);
}
active.push(now);
await c.env.KV.put(kvKey, JSON.stringify(active), {
expirationTtl: Math.ceil(windowMs / 1000),
});
return c.json({
allowed: true,
remaining: maxRequests - active.length,
limit: maxRequests,
});
});
rateLimiterRoute.get('/health', (c) =>
c.json({ module: 'rate-limiter', ok: true })
);Things to Keep in Mind
Secrets are never exposed
Your AEROSTACK_API_KEY secret is stored encrypted by GitHub and is never accessible to forked repositories or exposed in workflow logs. Do not hardcode keys.
Draft vs Auto-publish
The workflow above sends "publish": true in the payload, which immediately makes the function live on publish. If you want it to land as a draft first (so you can edit the README on the Hub before going public), set "publish": false:
PAYLOAD=$(jq -n \
--argjson config "$CONFIG" \
--argjson code "$CODE" \
'$config + {code: $code, publish: false}')Branch protection
Only publish from protected branches. Avoid publishing from feature branches — use a push trigger scoped to main or use a release event.
Version must be bumped on updates
You cannot overwrite an existing published version. Bump version in aerostack.json before each publish. Use GitHub releases (and the tag-triggered workflow) to keep your Git tags and Hub versions in sync.
Use Conventional Commits + semantic-release to automatically bump version in aerostack.json from your commit messages. The release event workflow will then pick up the new version automatically.
Monorepo with multiple functions
If you keep multiple functions in one repository, scope the workflow to only run when relevant files change:
on:
push:
branches: [main]
paths:
- 'functions/my-rate-limiter/**'And adjust the working directory in each step:
- name: Read aerostack.json
run: |
echo "config=$(cat functions/my-rate-limiter/aerostack.json | tr -d '\n')" >> $GITHUB_OUTPUT