Secrets and environment

Per-repo encrypted secrets, environment variables, and the network allowlist.

Runs execute in a sandbox with explicit inputs: the secrets you granted, the environment you set, and the network you allowed. Nothing else leaks in.

Add a secret

Secrets live in repository settings, encrypted at rest. Anything that looks like a secret's value is redacted from run logs, so a stray echo does not publish your deploy token to everyone with read access.

Read secrets in a run

import { defineWorkflow, on, sh, secret } from "@plainalpha/ci";

defineWorkflow("deploy", {
  on: [on.push({ branches: ["main"] })],
  run: async () => {
    if (!secret.has("DEPLOY_TOKEN")) throw new Error("DEPLOY_TOKEN not configured");
    await sh("./scripts/deploy.sh", {
      env: { DEPLOY_TOKEN: secret.get("DEPLOY_TOKEN") },
    });
  },
});

secret.get returns the decrypted value inside the run only. Because access is a function call rather than string interpolation in YAML, "which workflows touch this secret" is a grep.

Environment variables

env.get(name) reads the run's base environment. Per-command environment goes through the env option on sh, as above, which keeps a variable scoped to the one command that needs it.

Network policy

By default a run can reach package registries and nothing else. Workflows that need more declare it:

defineWorkflow("e2e", {
  on: [on.pullRequest()],
  network: { allow: ["api.staging.example.com"] },
  run: async () => {
    /* ... */
  },
});

The allowlist is part of the workflow file, so a dependency that starts phoning home fails loudly in CI instead of succeeding quietly, and network access shows up in code review like any other change.