CI/CD and Kubernetes
Give each deployment a read-only process token, then choose whether resolved values belong in later CI steps, one command, or a temporary file.
GitHub Actions
Store the process token as the repository or environment secret DYNV_TOKEN. Set the required API URL with the action's url input. The action installs dynv from https://dynv.dev/install.sh, which verifies the binary against SHA256SUMS before running it.
name: test
on: [push]
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- uses: dynv/action@v1
with:
token: ${{ secrets.DYNV_TOKEN }}
url: https://api.dynv.dev
process: api
mode: run
cmd: npm testrun is the default recommendation: values are available to that command, not exported to every later step. The command runs through Bash with -e and pipefail, and its exit code becomes the step's exit code.
Export variables or a temporary file
env mode writes every resolved variable to GITHUB_ENV for later steps and calls ::add-mask:: for each value, including multiline values. Any later step can read these secrets, so use run when you do not need job-wide export.
- uses: dynv/action@v1
with:
token: ${{ secrets.DYNV_TOKEN }}
url: https://api.dynv.dev
process: api
mode: env
- run: npm testfile mode creates a dotenv file with mode 0600. The action's post step removes that same file at job cleanup, without following a replacement symlink.
- uses: dynv/action@v1
with:
token: ${{ secrets.DYNV_TOKEN }}
url: https://api.dynv.dev
process: api
mode: file
path: .env.ci
- run: npm testGitLab CI
Add DYNV_TOKEN as a masked, protected CI/CD variable and DYNV_URL as a variable. This snippet keeps installation in the job workspace and scopes values to the test command:
variables:
DYNV_URL: https://api.dynv.dev
DYNV_INSTALL_DIR: "$CI_PROJECT_DIR/.dynv-bin"
DYNV_PROCESS: api
before_script:
- curl -fsSL https://dynv.dev/install.sh -o /tmp/dynv-install.sh
- sh /tmp/dynv-install.sh
- export PATH="$DYNV_INSTALL_DIR:$PATH"
test:
script:
- dynv run --require -- npm testUse protected branches or environments to limit which jobs receive the token. Revoke and replace it in the Dynv dashboard if the CI project's token is exposed.
Kubernetes init container
An init container installs the verified CLI into a shared emptyDir. The app container starts through dynv run and keeps its ciphertext-only rollback/offline cache in another emptyDir. Replace the image and args with your application's normal start command.
apiVersion: v1
kind: Secret
metadata:
name: api-dynv
type: Opaque
stringData:
token: dynv_pt_REPLACE_ME
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 1
selector:
matchLabels: { app: api }
template:
metadata:
labels: { app: api }
spec:
volumes:
- name: dynv-bin
emptyDir: {}
- name: dynv-cache
emptyDir: {}
- name: dynv-token
secret:
secretName: api-dynv
defaultMode: 0400
initContainers:
- name: install-dynv
image: curlimages/curl:8.12.1
command: ["/bin/sh", "-c"]
args:
- curl -fsSL https://dynv.dev/install.sh -o /tmp/install.sh
&& DYNV_INSTALL_DIR=/dynv sh /tmp/install.sh
volumeMounts:
- { name: dynv-bin, mountPath: /dynv }
containers:
- name: api
image: your-registry.example/api:replace-me
command: ["/dynv/dynv", "run", "--require", "--cache", "/dynv-cache/c", "--"]
args: ["node", "server.js"]
env:
- { name: DYNV_URL, value: https://api.dynv.dev }
- { name: DYNV_PROCESS, value: api }
- { name: DYNV_TOKEN_FILE, value: /var/run/secrets/dynv/token }
volumeMounts:
- { name: dynv-bin, mountPath: /dynv }
- { name: dynv-cache, mountPath: /dynv-cache }
- { name: dynv-token, mountPath: /var/run/secrets/dynv, readOnly: true }A sidecar cannot put environment variables into another container, so wrapping the app command is required. The Kubernetes Secret contains the Dynv process token, not the resolved values; like any Kubernetes Secret it is stored by your cluster (normally base64-encoded in etcd), so enable etcd encryption and restrict RBAC appropriately. The Dynv API still sees only ciphertext.
Use one token per Deployment and DYNV_PROCESS to pin the expected service. --require needs a successful initial live fetch: it refuses cache-only and inherited-only startup. The --cache file still provides rollback protection, and it survives app-container restarts but not pod replacement.