Publishing via GitLab CI
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
Section titled “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
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 (prefix
ac_). -
Add the CI variable to GitLab
In your GitLab project:
- Go to Settings → CI/CD → Variables
- Click Add variable
- Key:
AEROSTACK_API_KEY - Value: your
ac_secret_xxxxkey - Type: Variable (not File)
- Enable Mask variable (so it’s hidden in logs)
- Optionally enable Protect variable to restrict it to protected branches only
-
Structure your repository
Directorymy-rate-limiter
- .gitlab-ci.yml
- aerostack.json
- index.ts
- README.md
-
Add the pipeline file
Create .gitlab-ci.yml at the root of your function repository:
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: - mainstages: - 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: - tagsstages: - 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: falseFull Working Example
Section titled “Full Working Example”Complete .gitlab-ci.yml with staging validation + production publish, based on environment:
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: - tagsThings to Keep in Mind
Section titled “Things to Keep in Mind”Protect your CI variable
Section titled “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
Section titled “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
Section titled “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
Section titled “Multi-function monorepo”If your GitLab repo contains multiple function directories, use the changes keyword to only trigger publishing for modified functions:
publish-rate-limiter: stage: publish script: - cd functions/rate-limiter && <curl command here> only: changes: - functions/rate-limiter/** refs: - main