Deploy from CI Like You Deploy from Your Laptop
miren deploy is the central experience of Miren. We’ve put a lot of work into making it feel right — you run one command, you see your app build, you see it go live. There’s a lot more to do, but we’re getting to the point where the deploy itself isn’t the thing you’re thinking about. You’re thinking about your app.
That’s from the laptop (or the desktop, or the dev VM — wherever the human is). And from there, we’re pretty happy with where things are.
The CI gap
But here’s the thing: most apps don’t actually get deployed from a laptop. They get deployed from CI. And until recently, our CI story was… fine? It worked. You could download the CLI in your workflow, store API credentials in a GitHub secret, and call miren deploy. But it had that whole “managing deployment credentials” energy — a static, manually provisioned credential sitting in your GitHub secrets, the kind of thing that’ll be annoying to rotate when the time comes. It made us wrinkle our noses. It didn’t feel like the rest of Miren.
We kept looking at it and going, okay, we need to fix this. We want CI deploys to feel as natural as the laptop experience.
As of our most recent release, I think we’re there. Let me show you what it looks like.
The whole workflow
Here’s the GitHub Actions workflow that deploys this website:
name: Deploy
on:
push:
branches: [main]
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: mirendev/actions/deploy@main
with:
cluster: ${{ secrets.MIREN_CLUSTER }}
app: mirendev
You might look at that and think “that’s… it?” And (a) yes it is, and (b) that’s exactly the feeling we’re going for. The GitHub Action handles CLI setup and authentication. There are no stored credentials anywhere in this workflow.
The only “secret” is MIREN_CLUSTER, which isn’t actually secret at all — it’s just your cluster’s address. We store it in GitHub secrets out of convention, but you could just as easily make it a plain variable. Here’s what it looks like:
$ miren cluster export-address
miren.club:8443;sha1:a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2
The address on the left, a certificate fingerprint on the right. That fingerprint is a neat detail — it pins the connection to your cluster, so even if DNS were compromised, your code can’t be redirected somewhere else. Your deploys go where you told them to go.
How this works without secrets
So how do you authenticate without storing credentials? The trick is OIDC. Most CI systems — GitHub Actions, GitLab CI, CircleCI — can mint short-lived tokens that cryptographically prove which repo and workflow is running. The tokens last minutes and can’t be reused. Instead of storing a long-lived credential, Miren validates these tokens directly against the provider’s signing keys.
You set up a binding once from your laptop — “this repo is allowed to deploy this app”:
$ miren auth ci add --app myapp --github acme/myapp
ID oidc-b7k2m
App myapp
Provider github
Issuer https://token.actions.githubusercontent.com
Subject repo:acme/myapp:*
Claim Conditions
CLAIM PATTERN
event_name push,workflow_dispatch
From then on, the CLI detects the CI environment, grabs a token, and authenticates automatically. Here’s what happens under the hood:
The cluster asks GitHub “is this token legit?”, checks that the repo is bound to the right app, and lets the deploy through. No stored secret involved at any point.
What you can do with bindings
Each binding is scoped to one app. A token minted for acme/myapp can deploy myapp and nothing else — you can’t accidentally (or intentionally) deploy to something you didn’t set up. You can see all your bindings with miren auth ci list and remove one with miren auth ci remove, and deploys from that repo stop immediately. It’s a pretty satisfying level of control for something that required zero credential management to set up.
Eating our own cooking, yum
The thing we deploy from CI the most right now is this website. We have two workflows: branches deploy to a preview environment so we can share work in progress, and merges to main deploy to production. Two small YAML files, each pointed at a different cluster, no credentials anywhere.
This post went through both. I was iterating on it on a branch, previewing it on our staging cluster, and when it was ready, it merged and deployed to the site you’re reading now. I didn’t think about the deploy machinery at all, which — if you’ve been reading along — is kind of the whole point.
Full details in the CI/CD deployment docs.
