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.
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.
Expose the token as NIGHTSHIP_TOKEN and push the build folder:
nightship push ./package --channel latest --server https://builds.example.com
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).