An environment is a named place your CI deploys to โ production, staging, app-store โ with its own secrets and its own rules about who can deploy, from which branches, and on which machines. It's how you keep a signing key or a deploy credential away from the rest of your CI.
CI secrets are available to every workflow on every branch, so anyone who can push can read them. An environment's secrets are much harder to reach. A job receives them only when all of these hold:
- the job declares the environment (
environment: production), - someone allowed to deploy approved that exact commit and workflow,
- the commit is the current head of a branch the environment allows,
- and the job runs on a runner dedicated to the environment, which runs nothing else.
Environment workflows never run on a push or a merge. They run when someone deploys them.
Set one up#
Environments are configured by the repository's managers (organization owners and admins) in the repo's Settings โ Environments, or over the API.
-
Create it. A new environment is enabled, requires a reviewer, and allows deploying from
main. -
Add its secrets and variables. Secret values are encrypted and never shown again; variables are plain values anyone with write access can read.
-
Add reviewers โ the people who may approve a deploy. They need read access to the repo. (Or turn off Required reviewers to let anyone with write access deploy.)
-
Dedicate a runner. Register a runner for it, ideally on a machine that holds nothing else, and bind it to the environment. A bound runner takes only that environment's jobs โ never ordinary CI โ so code from an arbitrary branch never runs where the secrets land.
-
Point a workflow at it. Give the job
environment:and a manual trigger:# .oak/workflows/release.yml on: [workflow_dispatch] jobs: release: runs-on: release-mac environment: production steps: - uses: actions/checkout@v4 - run: ./scripts/release.sh env: SIGNING_KEY: ${{ secrets.SIGNING_KEY }}An environment workflow has exactly one job.
environment:also takes GitHub's mapping form,{name: production, url: https://example.com}. The name must be written out: it can't come from an expression. -
Deploy it (below).
Deploy#
oak ci deploy --environment production # preview, confirm, deploy main's head
oak ci deploy -e production --workflow release --branch main --yes
oak ci deploy -e production --dry-run # just show what would deploy
A deploy has two steps. First Oak shows what would run: the branch head, the workflow and its exact content hash, the environment's current policy version, its reviewers, and anything in the way. Then you approve exactly those values. If anything moves in between, the approval is refused: the branch gets a new commit, the workflow changes, or someone edits the environment. You re-review the new state; nothing runs that you didn't see.
An approval starts one run and is good for 30 minutes. The first runner bound to the environment that polls takes the job. Just before it hands the job over, Oak checks every rule again: environment enabled, same policy, head unchanged, approver still allowed, runner still bound. If anything failed, the run fails and nothing is sent.
The same deploy is available from Settings โ Environments โ Deploy, and over the API as a preview (GET โฆ/release-preview) followed by an authorization (POST โฆ/ci/release-authorizations) carrying the preview's values. An API key used to deploy must carry the explicit release scope.
Protection rules#
| Rule | Default | What it does |
|---|---|---|
| Enabled | on | A disabled environment deploys nothing. |
| Branches | main | Only the current head of one of these branches can be deployed. |
| Required reviewers | on | Only listed reviewers can approve a deploy. Off: anyone with write access can. Removing the last reviewer doesn't open it up; with this on and nobody listed, nobody can deploy. |
| Workflow pins | none | With no pins, any workflow whose job declares the environment can deploy. Add pins to allow only those workflow files. A pin can also fix the file's exact content hash, so even a merged change to the workflow can't deploy until someone re-pins it. |
| Runners | none | The runners that take this environment's jobs. At least one must be online to approve a deploy. |
Every change to an environment moves its policy version, which cancels any approval made against the old settings.
Platform administrators of the Oak instance can also approve deploys.
What the job sees#
- Secrets: the environment's secrets, through
${{ secrets.NAME }}as usual, plus the job's ownGITHUB_TOKEN. Your repo's ordinary CI secrets are not included, so an environment job only ever has what you put in the environment. - Variables: as environment variables, and as
${{ vars.NAME }}. - Who and what: besides the usual
OAK_CI_RUN_ID,OAK_CI_COMMIT, andOAK_CI_BRANCH, the job getsOAK_CI_ENVIRONMENT_NAME,OAK_CI_POLICY_VERSION,OAK_CI_AUTHORIZATION_ID,OAK_CI_WORKFLOW_PATH, andOAK_CI_WORKFLOW_HASH, so a deploy script can record exactly what was approved.
Two rules keep secrets from leaking sideways. A secret and a variable in one environment can't share a name. And an ordinary repo secret with the same name as any environment secret is withheld from ordinary jobs, so deleting a key from your CI secrets and adding it to an environment really moves it. Delete the ordinary copy.
Each line of a secret must be at least 4 characters, so the runner can mask it in logs.
Who can do what#
| Reader | Writer | Owner / admin | |
|---|---|---|---|
| See an environment's settings, variables, and secret names | โ | โ | |
| Create, change, or delete an environment, its secrets, reviewers, and runners | โ | ||
| Deploy | if a reviewer | if a reviewer, or reviewers aren't required | if a reviewer, or reviewers aren't required |
An API key that changes an environment needs the admin scope. One that deploys needs release.
Differences from GitHub#
| GitHub Actions | On Oak |
|---|---|
A job with environment: runs on push and waits for approval | It runs only when someone deploys it, at a commit they approved |
| Any runner with matching labels | Only runners bound to the environment, which take no other jobs |
| Environment secrets plus repo secrets | Environment secrets only |
| Deployment branch rules, wait timers, custom rules | Branch list, required reviewers, workflow pins; no wait timers yet |
| A workflow can mix environment and ordinary jobs | An environment workflow has exactly one job |
environment: from an expression | A fixed name |
API#
All under /api/{owner}/{name}/ci/, authenticated like the rest of the API.
| Method and path | Who | Does |
|---|---|---|
GET environments | write access | List environments with their settings, variables, secret names, reviewers, and runners. |
GET environments/{env} | write access | One environment. |
PUT environments/{env} | owner/admin | Create it, or change some settings: {"enabled", "required_reviewers", "branches": [...], "workflows": [{"path", "hash"}]}. Fields you leave out stay as they are. 201 when created. |
DELETE environments/{env} | owner/admin | Delete it. Its runners go back to taking ordinary CI. |
PUT / DELETE environments/{env}/secrets/{NAME} | owner/admin | Set ({"value": "..."}) or delete a secret. |
PUT / DELETE environments/{env}/variables/{NAME} | owner/admin | Set ({"value": "..."}) or delete a variable. |
PUT / DELETE environments/{env}/reviewers/{username} | owner/admin | Add or remove a reviewer. |
PUT / DELETE environments/{env}/runners/{runner-uuid} | owner/admin | Bind or unbind one of the organization's runners. |
GET environments/{env}/release-preview?workflow=&branch= | read access | What a deploy would carry right now, whether you may approve it, and what's in the way. |
POST release-authorizations | per the environment | Approve and start one deploy: {"environment", "branch", "commit", "workflow_path", "workflow_hash", "event": "manual", "policy_version"}, copied from the preview. Returns {"authorization_id", "run_id"}. |