Skip to content

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.


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.


  1. Get your Aerostack API key

    Go to app.aerostack.dev/account/keys and create (or copy) an account key (starts with ac_).

  2. Add the secret to GitHub

    In your GitHub repo:

    1. Go to SettingsSecrets and variablesActions
    2. Click New repository secret
    3. Name: AEROSTACK_API_KEY
    4. Value: your ac_secret_xxxx key
  3. Structure your repository

    Your function repo should look like this:

    • Directorymy-rate-limiter
      • aerostack.json
      • index.ts
      • README.md
      • Directory.github
        • Directoryworkflows
          • publish-aerostack.yml
  4. Add the workflow file

Create .github/workflows/publish-aerostack.yml in your repository:

.github/workflows/publish-aerostack.yml
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.aerostack.dev/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 }}

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
aerostack.json
{
"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
}
index.ts
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 })
);

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.

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}')

Only publish from protected branches. Avoid publishing from feature branches — use a push trigger scoped to main or use a release event.

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.

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