# Publishing via GitHub Actions

> Automate Aerostack function publishing directly from your GitHub repository using GitHub Actions CI/CD.

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

1. **Get your Aerostack API key**

   Go to [app.aerostack.dev/account/keys](https://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 **Settings** → **Secrets and variables** → **Actions**
   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:

   
   - my-rate-limiter
     - aerostack.json
     - index.ts
     - README.md
     - .github
       - workflows
         - publish-aerostack.yml
   

4. **Add the workflow file**

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

  
```yaml title=".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 }}
```
  
  
```yaml title=".github/workflows/publish-aerostack.yml"
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 }}
```
  
  
```yaml title=".github/workflows/publish-aerostack.yml"
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

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
```

```json title="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
}
```

```typescript title="index.ts"

  KV: KVNamespace;
  RATE_LIMIT_MAX?: string;
  RATE_LIMIT_WINDOW_MS?: string;
}

/**
 * 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`:

```yaml
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:

```yaml
on:
  push:
    branches: [main]
    paths:
      - 'functions/my-rate-limiter/**'
```

And adjust the working directory in each step:

```yaml
- name: Read aerostack.json
  run: |
    echo "config=$(cat functions/my-rate-limiter/aerostack.json | tr -d '\n')" >> $GITHUB_OUTPUT
```
