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.
Keep the three kinds of secrets separate
Section titled “Keep the three kinds of secrets separate”| 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:
1ctl token create github-actions --expires 90Use 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.
Install a pinned, verified CLI
Section titled “Install a pinned, verified CLI”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.
set -euo pipefail
VERSION=v0.11.0CLEAN_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/1ctl1ctl --versionThe 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.
Create an isolated CI session
Section titled “Create an isolated CI session”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.
mkdir -p "${HOME}"1ctl profile create ci --url "${SATUSKY_API_URL:-https://api.satusky.com/v1/cli}"1ctl profile use ci1ctl auth login1ctl auth statusSet 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.
GitHub Actions deployment job
Section titled “GitHub Actions deployment job”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.
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:
set +x1ctl secret create --app "${APP_NAME}" --kv "DATABASE_URL=${DATABASE_URL}"1ctl app status "${APP_NAME}" --watchsecret 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:
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.
Keep marketplace publication separate
Section titled “Keep marketplace publication separate”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.