1---
2title: Actions secrets & variables
3description: Manage GitHub Actions secrets and variables as Stack resources — encrypted secrets, plain-text variables, bulk helpers, and CI credentials provisioned as code.
4---
5
6GitHub Actions workflows read configuration from two places:
7**secrets** (encrypted, never readable back) and **variables**
8(plain text, visible in logs). The GitHub provider manages both as
9resources, so the values CI needs — API tokens, role ARNs, regions —
10are declared next to the infrastructure that produces them instead
11of pasted into repo settings.
12
13Use `Secret` for anything sensitive and `Variable` for everything
14else. `Secrets` and `Variables` are bulk helpers over the same
15resources.
16
17## Create a secret
18
19`GitHub.Secret` manages one encrypted secret. The value must be
20wrapped with `Redacted.make` so it never appears in logs or state:
21
22```typescript
23import * as GitHub from "alchemy/GitHub";
24import * as Redacted from "effect/Redacted";
25
26yield* GitHub.Secret("deploy-token", {
27 owner: "my-org",
28 repository: "my-repo",
29 name: "DEPLOY_TOKEN",
30 value: Redacted.make("my-secret-value"),
31});
32```
33
34The value is encrypted client-side with the repository's public key
35(via libsodium) before it's sent to GitHub. The resource is
36idempotent — redeploying with the same name updates the secret in
37place, and destroying the stack deletes it.
38
39GitHub never returns a secret's value, so there's nothing to diff:
40alchemy re-encrypts and re-uploads on every deploy.
41
42## Scope a secret to an environment
43
44Pass `environment` to scope the secret to a GitHub Actions
45environment (e.g. `production`) instead of the whole repository:
46
47```typescript
48yield* GitHub.Secret("deploy-key", {
49 owner: "my-org",
50 repository: "my-repo",
51 environment: "production",
52 name: "DEPLOY_KEY",
53 value: Redacted.make("my-secret-value"),
54});
55```
56
57Environment secrets are only released to workflows once the
58environment's protection rules are satisfied.
59
60## Manage the environment itself
61
62`GitHub.Environment` manages the deployment environment as a resource —
63including its protection rules — so the environment a secret targets is
64declared in the same deploy:
65
66```typescript
67const production = yield* GitHub.Environment("production", {
68 owner: "my-org",
69 repository: "my-repo",
70 name: "production",
71 waitTimer: 30,
72 preventSelfReview: true,
73 reviewers: {
74 users: ["release-manager"],
75 teams: ["platform"],
76 },
77 deploymentBranchPolicy: {
78 customBranchPolicies: ["main", "release/*"],
79 },
80});
81
82yield* GitHub.Secret("deploy-key", {
83 owner: "my-org",
84 repository: "my-repo",
85 environment: production,
86 name: "DEPLOY_KEY",
87 value: Redacted.make("my-secret-value"),
88});
89```
90
91The `environment` prop on secrets and variables accepts either a name
92string or the `Environment` resource itself. Reviewers are referenced by
93user login and team slug — alchemy resolves them to the IDs GitHub's API
94expects. `deploymentBranchPolicy` restricts
95which branches can deploy: `{ protectedBranches: true }` or a list of
96branch name patterns via `customBranchPolicies`. Environments on private
97repositories are plan-gated (Pro/Team/Enterprise); public repositories
98get them on every plan.
99
100## Create a variable
101
102`GitHub.Variable` manages a plain-text repository variable —
103suitable for non-sensitive configuration like regions, stage labels,
104or feature flags. Workflows read it as `${{ vars.NAME }}`:
105
106```typescript
107yield* GitHub.Variable("aws-region", {
108 owner: "my-org",
109 repository: "my-repo",
110 name: "AWS_REGION",
111 value: "us-east-1",
112});
113```
114
115Unlike secrets, variable values are readable back, so alchemy diffs
116the live value and skips the API call when nothing changed.
117
118Variables take the same `environment` prop as secrets to scope the
119variable to one Actions environment instead of the whole repository:
120
121```typescript
122yield* GitHub.Variable("region", {
123 owner: "my-org",
124 repository: "my-repo",
125 environment: "production",
126 name: "AWS_REGION",
127 value: "us-east-1",
128});
129```
130
131## Wire in outputs from other resources
132
133The values don't have to be literals — pass output attributes from
134other resources and alchemy resolves them at deploy time:
135
136```typescript
137const role = yield* AWS.IAM.Role("ci-role", { ... });
138
139yield* GitHub.Secret("ci-role-arn", {
140 owner: "my-org",
141 repository: "my-repo",
142 name: "AWS_ROLE_ARN",
143 value: Redacted.make(role.roleArn),
144});
145```
146
147This is the whole point: the credential and the secret that stores
148it live in the same deploy, so rotating one rotates the other.
149
150## Bulk-seed with Secrets and Variables
151
152When you have more than a couple of values, `GitHub.Secrets` and
153`GitHub.Variables` fan a map out into individual resources — the map
154key becomes both the logical id and the name:
155
156```typescript
157yield* GitHub.Secrets({
158 owner: "my-org",
159 repository: "my-repo",
160 secrets: {
161 AXIOM_INGEST_TOKEN: tokenValue,
162 AXIOM_DATASET_TRACES: traces.name,
163 },
164});
165
166yield* GitHub.Variables({
167 owner: "my-org",
168 repository: "my-repo",
169 variables: {
170 AWS_ROLE_ARN: role.roleArn,
171 AWS_REGION: region,
172 },
173});
174```
175
176`Secrets` accepts plain strings, `Redacted` values, or unresolved
177inputs (outputs of other resources, `Config` values) — plain strings
178are wrapped with `Redacted.make` automatically. Both helpers take an
179`environment` to scope every entry in the map to one Actions
180environment.
181
182## Provision CI credentials as code
183
184The canonical use is a small, one-shot stack that mints a scoped
185cloud credential and writes it straight into your repo — the raw
186token never touches your terminal or the GitHub UI:
187
188```typescript
189// stacks/github.ts
190import * as Alchemy from "alchemy";
191import * as Cloudflare from "alchemy/Cloudflare";
192import * as GitHub from "alchemy/GitHub";
193import * as Effect from "effect/Effect";
194import * as Layer from "effect/Layer";
195
196export default Alchemy.Stack(
197 "github",
198 {
199 providers: Layer.mergeAll(
200 Cloudflare.providers(),
201 GitHub.providers(),
202 ),
203 state: Cloudflare.state(),
204 },
205 Effect.gen(function* () {
206 const apiToken = yield* Cloudflare.ApiToken.AccountApiToken("CIToken", {
207 // scoped permission policies ...
208 });
209
210 yield* GitHub.Secret("cf-api-token", {
211 owner: "your-org",
212 repository: "your-repo",
213 name: "CLOUDFLARE_API_TOKEN",
214 value: apiToken.value,
215 });
216 }),
217);
218```
219
220Deploy it once from your laptop under an elevated profile:
221
222```sh
223alchemy deploy --config stacks/github.ts --profile admin
224```
225
226Re-run it only when you want to rotate the credential or change its
227permissions. [Part 5: CI/CD](/cloudflare/tutorial/part-5) walks
228through the full Cloudflare version step by step;
229[Continuous Integration](/environments/ci) covers the AWS equivalents
230(OIDC role ARNs as variables, access keys as secrets).
231
232## Token scope
233
234Writing secrets and variables uses the GitHub credential from
235`GitHub.providers()` — env vars, a stored PAT, the `gh` CLI, or
236OAuth (see [Setup](/github/setup)). The token needs the `repo` scope
237for private repositories, or `public_repo` for public ones.
238
239## Where next
240
241- [Continuous Integration](/environments/ci) — the full GitHub Actions
242 setup: deploy workflow, PR previews, and provider credentials.
243- [Part 5: CI/CD (Cloudflare)](/cloudflare/tutorial/part-5) — mint a
244 scoped API token and push it into your repo, step by step.
245- [Part 5: CI/CD (AWS)](/aws/tutorial/part-5) — the OIDC-based AWS
246 version.
247- [Repository](/github/repository) — manage the repo itself as a
248 resource.
249
250Reference:
251
252- [Environment](/providers/github/environment) ·
253 [Secret](/providers/github/secret) ·
254 [Secrets](/providers/github/secrets) ·
255 [Variable](/providers/github/variable) ·
256 [Variables](/providers/github/variables)