Skip to content

CI/CD Integration

This guide covers the deployment stage of CI/CD: authenticate a non-interactive runner, deploy one reviewed revision, wait for reconciliation, verify its Gateway route, and use a guarded rollback. Run your application’s build and test stages before this job; do not use a deploy as a substitute for them.

Item Where it belongs What it is for
SATUSKY_API_KEY GitHub Environment secret (for example, production) A dedicated CI credential used only by 1ctl auth login. It is never an application environment variable.
SATUSKY_API_URL Optional GitHub Actions variable An API endpoint override for a non-production control plane. Production already defaults to https://api.satusky.com/v1/cli.
DATABASE_URL and other application secrets A protected input to a one-time 1ctl secret create job, then Satusky runtime secret storage Values available to the deployed workload. Do not put them in satusky.toml, --env, logs, or commit history.

Create a dedicated, expiring token while signed in locally, then save the value printed once as the SATUSKY_API_KEY secret for the target GitHub Environment:

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

Use a separate token and GitHub Environment for staging and production. Rotate by creating the replacement token, updating the Environment secret, confirming a deployment with it, then disabling or deleting the old token.

Pin the CLI to a reviewed release rather than downloading an unreviewed “latest” binary in a production job. At the time this guide was checked, v0.11.0 is the current release. The release publishes 1ctl-<version>-checksums.sha256; verify the exact archive before installing it. Update VERSION deliberately in a review when adopting a later release.

Terminal window
set -euo pipefail
VERSION=v0.11.0
CLEAN_VERSION="${VERSION#v}"
ARCHIVE="1ctl-${CLEAN_VERSION}-linux-amd64.tar.gz"
CHECKSUMS="1ctl-${CLEAN_VERSION}-checksums.sha256"
RELEASE_URL="https://github.com/SatuSkyCloud/1ctl/releases/download/${VERSION}"
curl --fail --silent --show-error --location \
"${RELEASE_URL}/${ARCHIVE}" \
-o "${RUNNER_TEMP}/${ARCHIVE}"
curl --fail --silent --show-error --location \
"${RELEASE_URL}/${CHECKSUMS}" \
-o "${RUNNER_TEMP}/${CHECKSUMS}"
(
cd "${RUNNER_TEMP}"
awk -v archive="${ARCHIVE}" '$2 == archive { print }' "${CHECKSUMS}" > "${ARCHIVE}.sha256"
test -s "${ARCHIVE}.sha256"
sha256sum --check --status "${ARCHIVE}.sha256"
)
tar -xzf "${RUNNER_TEMP}/${ARCHIVE}" -C "${RUNNER_TEMP}"
sudo install -m 0755 "${RUNNER_TEMP}/1ctl" /usr/local/bin/1ctl
1ctl --version

The official install.sh also verifies the downloaded release archive, but it intentionally selects the latest release. Use it for interactive setup; use a pinned release in a protected deployment workflow.

SATUSKY_API_KEY is an input to 1ctl auth login; deployment commands read the authenticated token from the active profile. Give the job its own temporary home directory, create a profile, log in non-interactively, and verify the session. The runner removes this directory when the job ends.

Terminal window
mkdir -p "${HOME}"
1ctl profile create ci --url "${SATUSKY_API_URL:-https://api.satusky.com/v1/cli}"
1ctl profile use ci
1ctl auth login
1ctl auth status

Set HOME to ${{ runner.temp }}/1ctl-home at job scope. SATUSKY_API_URL is optional; set it only when the job must talk to a non-default endpoint.

This example assumes a preceding build-and-test workflow has selected the commit allowed to deploy. It uses a protected production Environment, prevents concurrent production reconciliations, waits for the control plane to report readiness, and verifies the public Gateway route. APP_NAME must match the [app] name in satusky.production.toml; change HEALTH_PATH to your application’s health endpoint.

.github/workflows/deploy-production.yml
name: Deploy production
on:
push:
branches: [main]
permissions:
contents: read
concurrency:
group: production-deploy
cancel-in-progress: false
env:
APP_NAME: backend-api
CONFIG_FILE: satusky.production.toml
HEALTH_PATH: /health
HOME: ${{ runner.temp }}/1ctl-home
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v6
- name: Install verified 1ctl
shell: bash
run: |
set -euo pipefail
VERSION=v0.11.0
CLEAN_VERSION="${VERSION#v}"
ARCHIVE="1ctl-${CLEAN_VERSION}-linux-amd64.tar.gz"
CHECKSUMS="1ctl-${CLEAN_VERSION}-checksums.sha256"
RELEASE_URL="https://github.com/SatuSkyCloud/1ctl/releases/download/${VERSION}"
curl --fail --silent --show-error --location "${RELEASE_URL}/${ARCHIVE}" -o "${RUNNER_TEMP}/${ARCHIVE}"
curl --fail --silent --show-error --location "${RELEASE_URL}/${CHECKSUMS}" -o "${RUNNER_TEMP}/${CHECKSUMS}"
(
cd "${RUNNER_TEMP}"
awk -v archive="${ARCHIVE}" '$2 == archive { print }' "${CHECKSUMS}" > "${ARCHIVE}.sha256"
test -s "${ARCHIVE}.sha256"
sha256sum --check --status "${ARCHIVE}.sha256"
)
tar -xzf "${RUNNER_TEMP}/${ARCHIVE}" -C "${RUNNER_TEMP}"
sudo install -m 0755 "${RUNNER_TEMP}/1ctl" /usr/local/bin/1ctl
1ctl --version
- name: Authenticate the isolated profile
env:
SATUSKY_API_KEY: ${{ secrets.SATUSKY_API_KEY }}
SATUSKY_API_URL: ${{ vars.SATUSKY_API_URL }}
shell: bash
run: |
set -euo pipefail
mkdir -p "${HOME}"
1ctl profile create ci --url "${SATUSKY_API_URL:-https://api.satusky.com/v1/cli}"
1ctl profile use ci
1ctl auth login
1ctl auth status
- name: Deploy and wait for reconciliation
id: deploy
shell: bash
run: |
set -euo pipefail
1ctl deploy --config "${CONFIG_FILE}" --wait
echo "ready=true" >> "${GITHUB_OUTPUT}"
- name: Verify the workload, route, and DNS target
shell: bash
run: |
set -euo pipefail
STATUS_JSON="$(1ctl -o json app status "${APP_NAME}")"
jq -e '
.deployment.status == "ready" and
.status.status == "Running" and
.domain_status.route.attached == true and
.domain_status.route.resource_kind == "HTTPRoute" and
.domain_status.dns.condition.status == "verified"
' <<<"${STATUS_JSON}" > /dev/null
ROUTE_HOST="$(jq -er '.domain_status.route.hostnames[0]' <<<"${STATUS_JSON}")"
curl --fail --retry 5 --retry-all-errors --retry-delay 2 "https://${ROUTE_HOST}${HEALTH_PATH}"
- name: Roll back a release that passed reconciliation but failed verification
if: ${{ failure() && steps.deploy.outputs.ready == 'true' }}
shell: bash
run: |
set -euo pipefail
1ctl app rollback "${APP_NAME}" --yes
1ctl app status "${APP_NAME}" --watch

--wait is the deployment gate: an accepted intent alone is not a successful rollout. The subsequent status check confirms the canonical deployment is ready, the workload is Running, the controller reports an attached HTTPRoute, and the authoritative DNS condition is verified for the reserved target. The HTTP request is the application’s own final health check. DNS, route, and application readiness remain separate checks.

The route assertion is for a public application with a domain. For a private application, keep --wait and workload status verification, then run an authenticated internal probe appropriate to that network. Do not grant the deployment job Kubernetes credentials or apply Ingress/HTTPRoute objects just to perform this check; Satusky owns Gateway routing.

The rollback step is deliberately guarded: it runs only if deployment reconciliation completed and a later verification step failed. It rolls back to the prior release by default. To choose a specific release, inspect 1ctl app releases "$APP_NAME" and use 1ctl app rollback "$APP_NAME" --version <number> --yes.

Provision runtime application secrets separately

Section titled “Provision runtime application secrets separately”

Create runtime secrets after the application exists, from a protected job or a one-time administrator action. Disable shell tracing around this command and never echo the value:

Terminal window
set +x
1ctl secret create --app "${APP_NAME}" --kv "DATABASE_URL=${DATABASE_URL}"
1ctl app status "${APP_NAME}" --watch

secret create restarts the deployment so the secret takes effect. Do not run it on every deploy unless you are intentionally rotating the value. For non-sensitive configuration such as LOG_LEVEL, use the environment/config workflow; --env is not an appropriate transport for passwords, connection strings, or API keys.

Use the same control flow in other CI systems

Section titled “Use the same control flow in other CI systems”

GitLab CI, Buildkite, and other runners use the same sequence: isolate HOME, create and select a profile, expose SATUSKY_API_KEY only to 1ctl auth login, deploy with --wait, query 1ctl -o json app status, and verify the application endpoint. Map GitHub Environment protection to the equivalent protected environment and masked variable features in that CI system.

For machine-readable checks, put the global output flag before the command:

Terminal window
1ctl -o json app status "${APP_NAME}"

Do not parse table output or select the first item from app list: concurrent or unrelated deployments make that unsafe. Address the application by its configured name and assert the explicit status fields shown above.

Publishing a marketplace package is not an application deployment and should not be folded into this rollout job. A package upload is private by default; requesting public review is a separate action, and approval is not a 1ctl command. Keep package authoring in a restricted publisher workflow, then use the normal marketplace and application-status flow for an approved package. See Publish a Private Marketplace Package.