Skip to content

GitHub Action

statio publishes one Action on the Marketplace, accentiostudios/statio@v1. A single step builds and pushes your image, signs it with cosign, installs the pinned statio binary, joins your tailnet as an ephemeral tag:ci node, signs the deploy payload with the same identity, and sends the envelope to your agent.

The action.yml lives at the root of accentiostudios/statio, the same repo that ships the CLI, the agent and the releases.

The job needs all three:

permissions:
id-token: write # keyless cosign signing. Without it the agent answers 403.
packages: write # push the image to GHCR. Also covers pulling a private image.
contents: read # checkout
- uses: actions/checkout@v4
- uses: accentiostudios/statio@v1
with:
target: statio.your-tailnet.ts.net
service: api
image: ghcr.io/your-org/api
ts-oauth-client-id: ${{ secrets.STATIO_TS_OAUTH_CLIENT_ID }}
ts-oauth-secret: ${{ secrets.STATIO_TS_OAUTH_SECRET }}
env: |
DATABASE_URL=${{ secrets.DATABASE_URL }}
InputRequiredMeaning
targetyesThe agent’s MagicDNS host, such as statio.your-tailnet.ts.net. It is the signed audience, which binds the deploy to that server.
serviceyesThe app slot, accepted on the server with statio app add.
imageyesYour image repository. The action builds and pushes here, and it must equal the app’s --image.
ts-oauth-client-idyesCI’s tag:ci OAuth client id.
ts-oauth-secretyesCI’s tag:ci OAuth client secret.
digestnoDeploy this exact digest and skip the build. Use it for a rollback, or with your own build step.
dockerfilenoDockerfile path when the action builds. Default Dockerfile.
contextnoBuild context when the action builds. Default ..
image-tagnoTag to push under. Default ${{ github.sha }}. The deployed reference is always the digest.
signnoSign the image with cosign. Default true. Set it to false only when you sign it yourself.
registry-usernamenoRegistry login when the action pushes. Defaults to the GitHub actor, for GHCR.
registry-passwordnoRegistry token when the action pushes. Defaults to GITHUB_TOKEN, for GHCR.
envnoPer-deploy values, one KEY=${{ secrets.KEY }} per line. GitHub masks them.
statio-filenoPath to statio.yaml. Default statio.yaml.
statio-versionnoBinary version to download. A bare major such as v1, an exact vX.Y.Z, or latest.
timeoutnoDeploy timeout. Default 5m.
strictnoTreat success_degraded as a failure. Default false.
preflightnoRun the read-only readiness check first. Default true.

The Action sets deploy_seq itself, from github.run_number.

The preflight step runs after the tailnet join and before the deploy, sending the same signed payload to the agent’s /preflight endpoint. It verifies the app’s host port is free, and, when statio.yaml asks for a public domain, that NPMplus is configured, reachable, and allows the domain. It changes nothing. A failure stops the job before the deploy touches the server.

Pass digest: ${{ steps.build.outputs.digest }} and the action skips the build, then signs and deploys. Add sign: false when you sign the image in your own step too.

statio init repo writes this when the repo has no CI:

name: deploy
on:
push:
branches: [main]
workflow_dispatch:
inputs:
digest:
description: "Existing image digest to redeploy. Use it to roll back."
required: false
permissions:
id-token: write
packages: write
contents: read
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: false
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: accentiostudios/statio@v1
with:
target: statio.your-tailnet.ts.net
service: api
image: ghcr.io/your-org/api
digest: ${{ inputs.digest }}
ts-oauth-client-id: ${{ secrets.STATIO_TS_OAUTH_CLIENT_ID }}
ts-oauth-secret: ${{ secrets.STATIO_TS_OAUTH_SECRET }}
env: |
DATABASE_URL=${{ secrets.DATABASE_URL }}

With CI already in place, statio init repo prints the step alone and leaves your file untouched.

@v1 moves with every stable 1.x release. Pin @v0.2.6 for reproducible runs. The statio-version input keeps the downloaded binary in step with the wire schema.