N Nightship
Guides / Publishing builds
// publishing builds

Publish from CI

Push a build from your pipeline on every green run, with a scoped identity that can only touch its own channels — so a leaked CI secret has a small blast radius.

Make a service account for CI

Don’t give CI an admin token, and don’t give it a person’s token either. Create a service account — a publisher that nobody logs in as, limited to the channels it publishes — and mint its token. In the desktop app: Admin → Users → Create user, pick Service (CI), then Create token in its inspector. From a terminal (as admin):

nightship user create ci --service
nightship user set-role ci publisher --channels latest
nightship token create ci-token --user ci

Service accounts don’t count as seats. A leaked token can only touch its own channels — never users, other channels, or admin state. On a self-hosted box the same works offline: nightshipd user create ci --role publisher --channels latest --data-dir /var/lib/nightship followed by nightshipd token create ci-token --user ci --data-dir /var/lib/nightship.

Cargo in CI

Expose the token as NIGHTSHIP_TOKEN and push the build folder:

nightship push ./package --channel latest --server https://builds.example.com

Tide in CI

Tide uploads straight from the runner to your bucket, so CI holds two secrets: the scoped bucket write key (TIDE_S3_*) and the publisher token. Bytes never pass through nightshipd.

On GitHub, the composite action detects the runner OS, downloads the matching nightship-tide binary, and runs the push:

- uses: nightship-tools/tide-push@v1
  with:
    dir: ./package
    channel: latest
    register-url: https://builds.example.com
    register-token: ${{ secrets.NIGHTSHIP_PUBLISHER_TOKEN }}
    sign-key:      ${{ secrets.TIDE_SIGN_KEY }}
    s3-endpoint:   ${{ secrets.TIDE_S3_ENDPOINT }}
    s3-bucket:     ${{ secrets.TIDE_S3_BUCKET }}
    s3-key:        ${{ secrets.TIDE_S3_KEY }}
    s3-secret:     ${{ secrets.TIDE_S3_SECRET }}

Everything goes under with:. The action declares these as inputs and sets the TIDE_* environment itself, so values passed under env: on the step are overwritten with empty strings and the push fails with “missing env vars”.

Sign your builds. Without sign-key the manifest is published unsigned, and a client cannot tell an unsigned manifest from a tampered one — anyone who can write to the bucket can replace it with one listing chunks of their own, and every chunk hash still checks out because they wrote both. nightship storage init generates the keypair and prints the private half with the other CI secrets. After this publisher’s first signed push, put the public half on the channel so clients verify it:

curl -X POST -H "Authorization: Bearer <admin token>" \
  "https://builds.example.com/api/channels/latest/verify-key?value=<public key>"

Order matters: a channel that has a key rejects an unsigned manifest, so set it after the first signed build has landed, not before.

Any other CI works too — curl the nightship-tide binary for the runner’s OS and run push --register directly (the same command as Push a build).

Give the bucket key write access to just the one bucket. Combined with the scoped publisher token, that’s the smallest credential CI needs to ship a release.

← All guides