Skip to content

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.


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.


  1. Get your Aerostack API key

    Go to 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 SettingsCI/CDVariables
    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

    • Directorymy-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:

.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

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

.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

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.

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.

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.

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