The shape
Stack's CI story is deliberately small:
- Pull the encrypted vault from Phantom Cloud using two repository secrets.
-
Run
stack doctor --json. Exit code is0if all services are healthy,1if any service fails. - Upload the JSON report as a build artifact for debugging.
That's it. No secrets in GitHub's environment, no plaintext in the logs. The vault stays encrypted end-to-end — Phantom Cloud holds ciphertext, and only the CI job (with the right passphrase) can decrypt locally.
Scaffold the workflow
stack ci init
This writes .github/workflows/stack-ci.yml with a correct-by-default
workflow. Pass --force to overwrite an existing file.
The generated workflow
name: Stack doctor
on:
push:
branches: [main]
pull_request:
schedule:
# Nightly drift check — catches when an upstream key gets revoked
# out of band.
- cron: "0 7 * * *"
workflow_dispatch:
jobs:
doctor:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Phantom Secrets
run: npm install -g phantom-secrets
- name: Install Stack CLI
run: npm install -g @ashlr/stack
- name: Pull vault from Phantom Cloud
env:
PHANTOM_CLOUD_TOKEN: ${{ secrets.PHANTOM_CLOUD_TOKEN }}
PHANTOM_VAULT_PASSPHRASE: ${{ secrets.PHANTOM_VAULT_PASSPHRASE }}
run: phantom cloud pull --force
- name: Run stack doctor
run: stack doctor --json > stack-doctor.json
- name: Upload doctor report
if: always()
uses: actions/upload-artifact@v4
with:
name: stack-doctor-report
path: stack-doctor.json Repository secrets
The workflow needs two secrets. Create them once locally, then copy them into Settings → Secrets and variables → Actions on the repo:
| Secret | Where it comes from |
|---|---|
PHANTOM_VAULT_PASSPHRASE |
The passphrase for Phantom's encrypted-file vault fallback. Phantom
uses this in CI where macOS Keychain / libsecret isn't available.
Choose one when you run phantom login.
|
PHANTOM_CLOUD_TOKEN |
The GitHub OAuth device token Phantom minted when you ran phantom cloud push. View it with phantom cloud status --show-token.
|
Exit-code behaviour
# success — every service healthy
$ stack doctor --json
{ "reports": [ ... ] }
$ echo $?
0
# failure — at least one service errored
$ stack doctor --json
{ "reports": [ { "services": [ { "status": "error", "detail": "..." } ] } ] }
$ echo $?
1
GitHub Actions will fail the job on a non-zero exit, so the default
workflow above is already "red on broken credentials." Pair it with workflow_dispatch so you can re-run manually once you rotate a
key.
JSON report shape
{
"reports": [
{
"project": "cwd",
"path": "/home/runner/work/webapp/webapp",
"services": [
{
"name": "supabase",
"status": "ok",
"latencyMs": 218
},
{
"name": "posthog",
"status": "error",
"detail": "key invalid"
}
]
}
]
}
Each service reports a status of ok, warn, error, or unchecked (the provider didn't
implement a healthcheck yet). Parse this in downstream steps if you want
to post a summary to Slack or fail only on specific services.
Other CI systems
stack ci init only scaffolds GitHub Actions today. For other
providers, the two essential steps to replicate are:
# 1. Pull the vault (needs PHANTOM_CLOUD_TOKEN + PHANTOM_VAULT_PASSPHRASE)
phantom cloud pull --force
# 2. Run the check
stack doctor --json > stack-doctor.json Adapters for GitLab CI, CircleCI, and Buildkite are on the roadmap. Open an issue on GitHub if you want yours prioritised.
Rotating secrets from CI
stack add <service> --force
from CI — it'd re-run the full OAuth / PAT flow, which isn't meaningful in
a headless job. Instead, rotate the underlying credential at the provider
(e.g. generate a new Supabase service key), then run stack login
<provider> locally to refresh your vault. Then phantom cloud push so CI picks up the new value on the next
run.