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
Section titled “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
Section titled “Step-by-Step Setup”-
Get your Aerostack API key
Go to app.aerostack.dev/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:
Directorymy-rate-limiter
- aerostack.json
- index.ts
- README.md
Directory.github
Directoryworkflows
- publish-aerostack.yml
-
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.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 }}name: Publish to Aerostack Hub
on: release: types: [published]
jobs: publish: name: Publish on Release 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: Update version from git tag run: | TAG="${{ github.ref_name }}" # Strip leading 'v' from tag (v1.2.0 → 1.2.0) VERSION="${TAG#v}" echo "Publishing version: $VERSION"
# Update version in aerostack.json jq --arg v "$VERSION" '.version = $v' aerostack.json > tmp.json mv tmp.json aerostack.json
- 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 }}" 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 version ${{ github.ref_name }}!" echo "$BODY" | jq . env: AEROSTACK_API_KEY: ${{ secrets.AEROSTACK_API_KEY }}name: Publish to Aerostack Hub
on: push: branches: - main release: types: [published]
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: Update version from release tag (if release event) if: github.event_name == 'release' run: | TAG="${{ github.ref_name }}" VERSION="${TAG#v}" jq --arg v "$VERSION" '.version = $v' aerostack.json > tmp.json mv tmp.json aerostack.json
- 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 }}" 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 }}Full Working Example
Section titled “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
Section titled “Things to Keep in Mind”Secrets are never exposed
Section titled “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
Section titled “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
Section titled “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
Section titled “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.
Monorepo with multiple functions
Section titled “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