Community Hub→ Via GitHub Actions

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:

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

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:

.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.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
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 })
);

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