# Publishing via GitLab CI

> Automate Aerostack function publishing from your GitLab repository using GitLab CI/CD pipelines.

If your team uses **GitLab** as the primary source of truth, you can set up a GitLab CI pipeline to automatically publish your function to the Aerostack Hub on every push to `main` or on a tagged release.

---

## How It Works

```
Developer pushes to GitLab
        │
        ▼
GitLab CI pipeline triggers
        │
        ▼
Reads aerostack.json + source files
        │
        ▼
Calls POST /api/community/functions/ci/publish
  with your AEROSTACK_API_KEY CI variable
        │
        ▼
Function is live on hub.aerostack.dev ✅
```

No Aerostack-specific runner or plugin is required — the pipeline uses plain `curl` and `jq` which are available in any standard CI image.

---

## 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** (prefix `ac_`).

2. **Add the CI variable to GitLab**

   In your GitLab project:

   1. Go to **Settings** → **CI/CD** → **Variables**
   2. Click **Add variable**
   3. Key: `AEROSTACK_API_KEY`
   4. Value: your `ac_secret_xxxx` key
   5. Type: **Variable** (not File)
   6. Enable **Mask variable** (so it's hidden in logs)
   7. Optionally enable **Protect variable** to restrict it to protected branches only

3. **Structure your repository**

   
   - my-rate-limiter
     - .gitlab-ci.yml
     - aerostack.json
     - index.ts
     - README.md
   

4. **Add the pipeline file**

Create `.gitlab-ci.yml` at the root of your function repository:

  
```yaml title=".gitlab-ci.yml"
stages:
  - publish

publish-to-aerostack:
  stage: publish
  image: node:20-alpine
  before_script:
    - apk add --no-cache curl jq
  script:
    - |
      # Read aerostack.json
      CONFIG=$(cat aerostack.json)
      ENTRYPOINT=$(echo "$CONFIG" | jq -r '.entrypoint // "index.ts"')

      # Read source file and escape for JSON
      CODE=$(cat "$ENTRYPOINT" | jq -Rs .)

      # Build payload: merge config + code + publish flag
      PAYLOAD=$(jq -n \
        --argjson config "$CONFIG" \
        --argjson code "$CODE" \
        '$config + {code: $code, publish: true}')

      echo "Publishing $(echo "$CONFIG" | jq -r '.name') v$(echo "$CONFIG" | jq -r '.version')..."

      # Call Aerostack publish API
      RESPONSE=$(curl -s -w "\n%{http_code}" \
        -X POST "https://api.aerostack.dev/api/community/functions/ci/publish" \
        -H "X-API-Key: $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 .
  only:
    - main
```
  
  
```yaml title=".gitlab-ci.yml"
stages:
  - publish

publish-to-aerostack:
  stage: publish
  image: node:20-alpine
  before_script:
    - apk add --no-cache curl jq
  script:
    - |
      # Strip 'v' prefix from git tag (v1.2.0 → 1.2.0)
      VERSION="${CI_COMMIT_TAG#v}"
      echo "Publishing version: $VERSION"

      # Patch aerostack.json version to match git tag
      jq --arg v "$VERSION" '.version = $v' aerostack.json > tmp.json
      mv tmp.json aerostack.json

      CONFIG=$(cat aerostack.json)
      ENTRYPOINT=$(echo "$CONFIG" | jq -r '.entrypoint // "index.ts"')
      CODE=$(cat "$ENTRYPOINT" | jq -Rs .)

      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: $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 successfully!"
      echo "$BODY" | jq .
  only:
    - tags
```
  
  
```yaml title=".gitlab-ci.yml"
stages:
  - publish

publish-to-aerostack:
  stage: publish
  image: node:20-alpine
  before_script:
    - apk add --no-cache curl jq
  script:
    - |
      CONFIG=$(cat aerostack.json)
      ENTRYPOINT=$(echo "$CONFIG" | jq -r '.entrypoint // "index.ts"')
      CODE=$(cat "$ENTRYPOINT" | jq -Rs .)

      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: $AEROSTACK_API_KEY" \
        -H "Content-Type: application/json" \
        -d "$PAYLOAD")

      HTTP_CODE=$(echo "$RESPONSE" | tail -1)
      BODY=$(echo "$RESPONSE" | head -1)

      if [ "$HTTP_CODE" != "200" ] && [ "$HTTP_CODE" != "201" ]; then
        echo "❌ Publish failed with HTTP $HTTP_CODE"
        exit 1
      fi

      echo "✅ Published successfully!"
      echo "$BODY" | jq .
  # Manual trigger only — run from GitLab CI/CD > Pipelines > Run Pipeline
  when: manual
  allow_failure: false
```
  

---

## Full Working Example

Complete `.gitlab-ci.yml` with staging validation + production publish, based on environment:

```yaml title=".gitlab-ci.yml"
stages:
  - validate
  - publish

variables:
  AEROSTACK_API: "https://api.aerostack.dev/api/community/functions/ci/publish"

# ── Validate on all feature branches ──────────────────────────────────────────
validate-function:
  stage: validate
  image: node:20-alpine
  before_script:
    - apk add --no-cache jq
  script:
    - |
      echo "Validating aerostack.json..."
      
      # Check required fields exist
      for field in name version description category language entrypoint; do
        VALUE=$(cat aerostack.json | jq -r ".$field")
        if [ "$VALUE" = "null" ] || [ -z "$VALUE" ]; then
          echo "❌ Missing required field: $field"
          exit 1
        fi
      done
      
      ENTRYPOINT=$(cat aerostack.json | jq -r '.entrypoint')
      if [ ! -f "$ENTRYPOINT" ]; then
        echo "❌ Entrypoint file not found: $ENTRYPOINT"
        exit 1
      fi
      
      echo "✅ aerostack.json is valid"
      echo "Function: $(cat aerostack.json | jq -r '.name') v$(cat aerostack.json | jq -r '.version')"
  except:
    - main
    - tags

# ── Publish draft on push to main ─────────────────────────────────────────────
publish-draft:
  stage: publish
  image: node:20-alpine
  before_script:
    - apk add --no-cache curl jq
  script:
    - |
      CONFIG=$(cat aerostack.json)
      ENTRYPOINT=$(echo "$CONFIG" | jq -r '.entrypoint // "index.ts"')
      CODE=$(cat "$ENTRYPOINT" | jq -Rs .)

      # Publish as draft — review on Hub before making public
      PAYLOAD=$(jq -n \
        --argjson config "$CONFIG" \
        --argjson code "$CODE" \
        '$config + {code: $code, publish: false}')

      RESPONSE=$(curl -s -w "\n%{http_code}" \
        -X POST "$AEROSTACK_API" \
        -H "X-API-Key: $AEROSTACK_API_KEY" \
        -H "Content-Type: application/json" \
        -d "$PAYLOAD")

      HTTP_CODE=$(echo "$RESPONSE" | tail -1)
      echo "$(echo "$RESPONSE" | head -1)" | jq .

      [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "201" ] || exit 1
      echo "📝 Published as draft. Go to hub.aerostack.dev/my-functions to review."
  only:
    - main

# ── Publish live on git tag ────────────────────────────────────────────────────
publish-release:
  stage: publish
  image: node:20-alpine
  before_script:
    - apk add --no-cache curl jq
  script:
    - |
      VERSION="${CI_COMMIT_TAG#v}"
      jq --arg v "$VERSION" '.version = $v' aerostack.json > tmp.json && mv tmp.json aerostack.json

      CONFIG=$(cat aerostack.json)
      ENTRYPOINT=$(echo "$CONFIG" | jq -r '.entrypoint // "index.ts"')
      CODE=$(cat "$ENTRYPOINT" | jq -Rs .)

      PAYLOAD=$(jq -n \
        --argjson config "$CONFIG" \
        --argjson code "$CODE" \
        '$config + {code: $code, publish: true}')

      RESPONSE=$(curl -s -w "\n%{http_code}" \
        -X POST "$AEROSTACK_API" \
        -H "X-API-Key: $AEROSTACK_API_KEY" \
        -H "Content-Type: application/json" \
        -d "$PAYLOAD")

      HTTP_CODE=$(echo "$RESPONSE" | tail -1)
      echo "$(echo "$RESPONSE" | head -1)" | jq .

      [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "201" ] || exit 1
      echo "🚀 Published $VERSION to Aerostack Hub!"
  only:
    - tags
```

---

## Things to Keep in Mind

### Protect your CI variable

Always mark `AEROSTACK_API_KEY` as a **masked** variable in GitLab. If you also protect it, it will only be available on protected branches and tags — a good default for production publishing.

### Version must always be bumped

The Aerostack platform stores a snapshot for every published version. If you try to publish the same version string twice it will still succeed but update the snapshot in place. **Consumers who installed the old version will not be automatically updated** — version bumping is how you signal an intentional new release.

### Draft vs live on push

The `publish-draft` job above sends `publish: false`, so a push to `main` creates/updates the function code as a **draft**. Only a tagged release (via `publish-release`) makes it live. This separation is intentional — it lets you review on the Hub UI before going public.

If you prefer **push to main = immediately live**, change `publish: false` to `publish: true` in the `publish-draft` job.

### Multi-function monorepo

If your GitLab repo contains multiple function directories, use the `changes` keyword to only trigger publishing for modified functions:

```yaml
publish-rate-limiter:
  stage: publish
  script:
    - cd functions/rate-limiter && <curl command here>
  only:
    changes:
      - functions/rate-limiter/**
    refs:
      - main
```

  **GitLab environments** — You can use GitLab's built-in `environment` keyword on your publish job to get deployment tracking, rollback history, and approval gates directly in GitLab's UI. This works independently of Aerostack's version history.
