Skip to content

CI/CD Integration

1ctl is designed to run in automated pipelines. The key difference from interactive use: pass your API token via the SATUSKY_API_KEY environment variable instead of running 1ctl auth login. No state files, no interactive prompts.

You will also need to set SATUSKY_API_URLSATUSKY_API_KEY alone is not sufficient. Both environment variables must be present for the CLI to reach the API.


Create a dedicated token for your pipeline. A dedicated token lets you revoke CI access independently from your personal account — if a pipeline is compromised, you disable that one token without affecting your own credentials.

Terminal window
1ctl token create --name "github-actions" --expires 90

The --expires value is in days. Use 0 for no expiry, though setting an expiry and rotating regularly is good practice.

The output looks like this:

✅ API token created successfully
ID: 24ae4338-9829-463d-a31b-9627f41c6434
Name: github-actions
❗️ IMPORTANT: Save this token now. You won't be able to see it again!
Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Expires: 2026-07-27

The token value is shown exactly once. Copy it immediately and save it as a secret in your CI system. If you lose it, delete the token and create a new one.


Terminal window
1ctl token list

Output shows one record per token:

API Tokens
──────────
ID: 24ae4338-9829-463d-a31b-9627f41c6434
Name: github-actions
Status: Enabled
Last Used: 2 hours ago
Expires: 2026-07-27
Created: just now
---

For scripting, use JSON output with the global -o json flag. Note that -o json must come before the subcommand:

Terminal window
# Correct: global flag before subcommand
1ctl -o json token list
# Wrong: flag after subcommand is not recognized
1ctl token list -o json

JSON field names returned:

[
{
"token_id": "24ae4338-9829-463d-a31b-9627f41c6434",
"name": "github-actions",
"description": "",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expires_at": "2026-07-27T09:04:35.625037+08:00",
"is_active": true,
"created_at": "2026-04-28T09:04:35.666822+08:00"
}
]

Use token_id (not id) when scripting against this output.

Temporarily block a token without deleting it — useful when rotating credentials:

Terminal window
1ctl token disable <token-id>
# Output: ✅ Token disabled successfully
1ctl token enable <token-id>
# Output: ✅ Token enabled successfully
Terminal window
# --yes must come before the token ID
1ctl token delete --yes <token-id>
# Output: ✅ Token deleted successfully

Without --yes, the CLI prompts for confirmation. In non-interactive scripts, always include --yes before the token ID.


Two variables are required in every CI job that runs 1ctl:

VariablePurpose
SATUSKY_API_KEYYour API token value (the long eyJ... string)
SATUSKY_API_URLThe API endpoint. Production: https://api.satusky.com/v1/cli

SATUSKY_API_KEY overrides any stored credentials from 1ctl auth login. If both a stored session and SATUSKY_API_KEY are present, the environment variable takes precedence.

SATUSKY_API_URL must always be set explicitly in CI — the CLI does not fall back to a default URL.


These YAML workflows run on GitHub Actions runners. Adapt the SATUSKY_API_KEY and SATUSKY_API_URL environment variables to your specific CI system’s secrets mechanism. The 1ctl commands themselves are identical regardless of CI platform.

Add two secrets to your repository: Settings → Secrets and variables → Actions → New repository secret.

  • SATUSKY_API_TOKEN — the token value from 1ctl token create
  • SATUSKY_API_URLhttps://api.satusky.com/v1/cli

Note: The install script at https://install.satusky.com is the production installer. It is not available in local development environments. In CI it downloads the latest 1ctl release.

.github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install 1ctl
run: curl -fsSL https://install.satusky.com | sh
- name: Deploy
env:
SATUSKY_API_KEY: ${{ secrets.SATUSKY_API_TOKEN }}
SATUSKY_API_URL: ${{ secrets.SATUSKY_API_URL }}
run: 1ctl deploy --wait

Why --wait matters in CI: without it, 1ctl deploy exits as soon as the deploy request is accepted by the API — before pods have actually started. The pipeline step goes green while your new version may still be failing to boot. --wait blocks until pods reach Running state (default timeout: 5 minutes), so a failed rollout causes the CI step to fail as expected.

Deploy to staging on every pull request, production only on main. This requires separate config files (satusky.staging.toml, satusky.production.toml) in your repository, and separate secrets for each environment’s API key.

Infrastructure note: multi-environment deploys require separate Satusky namespaces or projects configured per environment. This YAML pattern shows the CI structure; the namespace and resource allocation differences live in your config files.

name: Deploy
on:
push:
branches: [main, staging]
pull_request:
branches: [main]
jobs:
deploy-staging:
if: github.event_name == 'pull_request' || github.ref == 'refs/heads/staging'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install 1ctl
run: curl -fsSL https://install.satusky.com | sh
- name: Deploy to staging
env:
SATUSKY_API_KEY: ${{ secrets.SATUSKY_API_TOKEN_STAGING }}
SATUSKY_API_URL: ${{ secrets.SATUSKY_API_URL }}
run: 1ctl deploy --config satusky.staging.toml --wait
deploy-production:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install 1ctl
run: curl -fsSL https://install.satusky.com | sh
- name: Deploy to production
env:
SATUSKY_API_KEY: ${{ secrets.SATUSKY_API_TOKEN_PRODUCTION }}
SATUSKY_API_URL: ${{ secrets.SATUSKY_API_URL }}
run: 1ctl deploy --config satusky.production.toml --wait

The --config flag accepts a filename or a short name. --config staging resolves to satusky.staging.toml in the working directory.

This pattern captures the deployment ID after a successful deploy, then rolls back to the previous version if any subsequent step fails:

- name: Deploy
id: deploy
env:
SATUSKY_API_KEY: ${{ secrets.SATUSKY_API_TOKEN }}
SATUSKY_API_URL: ${{ secrets.SATUSKY_API_URL }}
run: |
1ctl deploy --wait
# Capture the deployment ID of the most recent deploy for rollback use
DEP_ID=$(1ctl -o json deploy list | jq -r '.[0].deployment_id')
echo "deployment_id=$DEP_ID" >> $GITHUB_OUTPUT
- name: Rollback on failure
if: failure() && steps.deploy.outputs.deployment_id != ''
env:
SATUSKY_API_KEY: ${{ secrets.SATUSKY_API_TOKEN }}
SATUSKY_API_URL: ${{ secrets.SATUSKY_API_URL }}
run: |
1ctl deploy rollback \
--deployment-id ${{ steps.deploy.outputs.deployment_id }} \
--yes

What this does: deploy list returns deployments sorted most-recent-first, so .[0].deployment_id is the deployment just created. If a post-deploy health check or smoke test step fails, the Rollback on failure step runs and reverts to the previous release automatically.

Why --yes on rollback: rollback without --yes prompts for confirmation, which hangs indefinitely in a non-interactive CI runner.

--version is optional: omitting it rolls back to the immediately preceding release, which is what you want in most CI failure scenarios. You can specify --version 3 to target a specific release number from 1ctl deploy releases.


This YAML runs on GitLab CI runners. Set SATUSKY_API_TOKEN and SATUSKY_API_URL in Settings → CI/CD → Variables.

.gitlab-ci.yml
stages:
- deploy
deploy:
stage: deploy
image: alpine:latest
script:
- apk add --no-cache curl
- curl -fsSL https://install.satusky.com | sh
- 1ctl deploy --wait
variables:
SATUSKY_API_KEY: $SATUSKY_API_TOKEN
SATUSKY_API_URL: $SATUSKY_API_URL
only:
- main

Injecting Environment Variables at Deploy Time

Section titled “Injecting Environment Variables at Deploy Time”

You can pass runtime environment variables directly in the deploy command. This is useful when secrets differ per pipeline run or per branch:

Terminal window
1ctl deploy \
--env DATABASE_URL=$DATABASE_URL \
--env REDIS_URL=$REDIS_URL \
--wait

For long-lived secrets (database passwords, API keys that don’t change per-build), pre-configure them with 1ctl secret create. Secrets persist across deployments — you don’t need to pass them on every deploy invocation.


Use the global -o json flag for machine-readable output. The flag must appear before the subcommand:

Terminal window
# Get the deployment ID of your backend-api app
DEPLOYMENT_ID=$(1ctl -o json deploy list | jq -r '.[] | select(.app_label=="backend-api") | .deployment_id')
# Get the most recent deployment ID regardless of app
DEPLOYMENT_ID=$(1ctl -o json deploy list | jq -r '.[0].deployment_id')
# Check deployment status
STATUS=$(1ctl -o json deploy get --config satusky.toml | jq -r '.status')
if [ "$STATUS" != "completed" ]; then
echo "Deploy is not healthy: $STATUS"
exit 1
fi

Correct JSON field names (common mistakes highlighted):

What you wantCorrect fieldWrong field
Deployment identifier.deployment_id.id
App name.app_label.name
Deployment status.status
Token identifier.token_id.id
Token active state.is_active.status

Terminal window
1ctl deploy releases --config satusky.toml

Output:

VERSION IMAGE STATUS DEPLOYED
─────── ──────────── ─────────── ────────────
6 nginx:alpine active 23 hours ago
5 nginx:alpine superseded 23 hours ago
4 nginx:alpine superseded 23 hours ago
1 nginx:alpine rolled_back 23 hours ago

Use the VERSION number with 1ctl deploy rollback --version <n> to target a specific release.


Use dedicated CI tokens — create one token per pipeline (e.g. github-actions, gitlab-ci). This scopes the blast radius if a token is leaked: you revoke one token without disrupting other pipelines or your personal access.

Set a token expiry--expires 90 rotates credentials quarterly. Rotate before expiry with 1ctl token create, update the secret in your CI system, then delete the old token with 1ctl token delete --yes <old-id>.

Always use --wait — without it your pipeline marks the deploy step green before pods have actually started. A bad deploy that crashes on startup will not be caught.

Use --yes on non-interactive commandstoken delete, deploy rollback, and deploy destroy all prompt for confirmation. Always include --yes (before the positional argument) in scripts.

Separate configs per environment — keep satusky.staging.toml and satusky.production.toml with different resource allocations, app names, and domain settings. This prevents a staging pipeline from accidentally deploying to production.

Pin the 1ctl version — the install script installs the latest release. To avoid unexpected behavior from version upgrades, pin a specific version tag in your install step once your pipeline is stable.