tenseleyflow/shithub / 9a1c617

Browse files

docs/api: actions secrets + variables reference

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
9a1c617961d578d9e66cf0298ab1b2f571632d7f
Parents
a9b76c0
Tree
15af2cf

2 changed files

StatusFile+-
M docs/public/SUMMARY.md 1 0
A docs/public/api/actions-secrets.md 170 0
docs/public/SUMMARY.mdmodified
@@ -40,6 +40,7 @@
4040
 - [Actions workflow API](./api/actions.md)
4141
 - [Actions workflows (list/dispatch)](./api/actions-workflows.md)
4242
 - [Actions lifecycle (enable/disable + run delete + artifacts + job logs)](./api/actions-lifecycle.md)
43
+- [Actions secrets + variables](./api/actions-secrets.md)
4344
 - [Actions workflow runs](./api/actions-runs.md)
4445
 - [Actions runner API](./api/actions-runner.md)
4546
 - [Webhooks](./api/webhooks.md)
docs/public/api/actions-secrets.mdadded
@@ -0,0 +1,170 @@
1
+# Actions secrets + variables
2
+
3
+REST surface for the `${{ secrets.NAME }}` and `${{ vars.NAME }}`
4
+substitutions runners apply to workflow files. Secrets carry
5
+ciphertext on the wire (NaCl sealed-box, gh-compatible); variables
6
+are plaintext.
7
+
8
+Scopes:
9
+
10
+- `repo:read` on the read endpoints (including the public-key probe)
11
+- `repo:write` on PUT / POST / PATCH / DELETE
12
+
13
+The org variants live under `/orgs/{org}/actions/...` and follow the
14
+same scope rules.
15
+
16
+## Sealed-box (secrets only)
17
+
18
+shithub never accepts plaintext secret values over REST. Clients
19
+must encrypt with the server's X25519 public key first.
20
+
21
+```
22
+GET /api/v1/repos/{o}/{r}/actions/secrets/public-key
23
+```
24
+
25
+```json
26
+{
27
+  "key_id": "kIaP4w1eTJDhRoxw",
28
+  "key":    "MCowBQYDK2VuAyEA..."
29
+}
30
+```
31
+
32
+`key_id` is a stable identifier for the public key. Clients echo it
33
+on the PUT body so the server can detect a stale local cache and
34
+reject (with HTTP 422 `stale key_id`) rather than silently fail to
35
+decrypt to garbage.
36
+
37
+To encrypt:
38
+
39
+```python
40
+import base64, nacl.public
41
+pub = nacl.public.PublicKey(base64.b64decode(key))
42
+sealed = nacl.public.SealedBox(pub).encrypt(b"my-secret-value")
43
+print(base64.b64encode(sealed).decode())
44
+```
45
+
46
+The Go-side equivalent:
47
+
48
+```go
49
+var pub [32]byte; copy(pub[:], pubKeyBytes)
50
+ct, _ := box.SealAnonymous(nil, []byte("my-secret-value"), &pub, rand.Reader)
51
+```
52
+
53
+## Secrets endpoints
54
+
55
+```
56
+GET    /api/v1/repos/{o}/{r}/actions/secrets/public-key
57
+GET    /api/v1/repos/{o}/{r}/actions/secrets
58
+GET    /api/v1/repos/{o}/{r}/actions/secrets/{name}
59
+PUT    /api/v1/repos/{o}/{r}/actions/secrets/{name}
60
+DELETE /api/v1/repos/{o}/{r}/actions/secrets/{name}
61
+
62
+GET    /api/v1/orgs/{org}/actions/secrets/public-key
63
+GET    /api/v1/orgs/{org}/actions/secrets
64
+GET    /api/v1/orgs/{org}/actions/secrets/{name}
65
+PUT    /api/v1/orgs/{org}/actions/secrets/{name}
66
+DELETE /api/v1/orgs/{org}/actions/secrets/{name}
67
+```
68
+
69
+### List + Get response
70
+
71
+```json
72
+[
73
+  {
74
+    "name":       "DEPLOY_TOKEN",
75
+    "created_at": "2026-05-12T18:00:00Z",
76
+    "updated_at": "2026-05-12T18:00:00Z"
77
+  }
78
+]
79
+```
80
+
81
+The list response **never** carries the value (plaintext or
82
+ciphertext). This is identical to gh's behavior. To use a secret,
83
+inject it via a workflow's `${{ secrets.NAME }}` reference.
84
+
85
+### PUT body
86
+
87
+```
88
+PUT /api/v1/repos/alice/demo/actions/secrets/DEPLOY_TOKEN
89
+Content-Type: application/json
90
+```
91
+
92
+```json
93
+{
94
+  "encrypted_value": "base64-of-sealed-box-output",
95
+  "key_id":          "kIaP4w1eTJDhRoxw"
96
+}
97
+```
98
+
99
+`204 No Content` on success. Errors:
100
+
101
+| Status | Code-shaped meaning                                              |
102
+|------:|-------------------------------------------------------------------|
103
+| 400   | `encrypted_value` is not valid base64.                            |
104
+| 422   | `encrypted_value` is empty, or `key_id` is stale, or the secret name is malformed (`^[A-Za-z_][A-Za-z0-9_]*$`, ≤100 chars). |
105
+| 422   | Sealed-box decode failed (likely a stale local public-key cache). |
106
+| 403   | PAT lacks `repo:write` (or org admin).                            |
107
+| 503   | Operator did not configure the sealed-box keypair on the server.  |
108
+
109
+Server-side: the decoded plaintext is re-encrypted with the shared
110
+storage AEAD (`internal/auth/secretbox`) before INSERT. Plaintext
111
+never lands in postgres.
112
+
113
+## Variables endpoints
114
+
115
+Variables are NOT secrets — they carry plaintext config and the
116
+list/get endpoints return values directly. The runner exposes them
117
+via `${{ vars.NAME }}`.
118
+
119
+```
120
+GET    /api/v1/repos/{o}/{r}/actions/variables
121
+POST   /api/v1/repos/{o}/{r}/actions/variables
122
+GET    /api/v1/repos/{o}/{r}/actions/variables/{name}
123
+PATCH  /api/v1/repos/{o}/{r}/actions/variables/{name}
124
+DELETE /api/v1/repos/{o}/{r}/actions/variables/{name}
125
+
126
+GET    /api/v1/orgs/{org}/actions/variables
127
+POST   /api/v1/orgs/{org}/actions/variables
128
+GET    /api/v1/orgs/{org}/actions/variables/{name}
129
+PATCH  /api/v1/orgs/{org}/actions/variables/{name}
130
+DELETE /api/v1/orgs/{org}/actions/variables/{name}
131
+```
132
+
133
+### Create request
134
+
135
+```json
136
+{ "name": "API_URL", "value": "https://api.example" }
137
+```
138
+
139
+Returns the row shape with `created_at`/`updated_at`:
140
+
141
+```json
142
+{
143
+  "name":       "API_URL",
144
+  "value":      "https://api.example",
145
+  "created_at": "2026-05-12T18:00:00Z",
146
+  "updated_at": "2026-05-12T18:00:00Z"
147
+}
148
+```
149
+
150
+PATCH accepts `{"value": "..."}` and returns the updated row.
151
+
152
+Constraints:
153
+
154
+- `name` matches `^[A-Za-z_][A-Za-z0-9_]*$` and is 1–100 chars.
155
+- `value` is UTF-8 ≤4096 chars.
156
+
157
+## Operator setup
158
+
159
+The sealed-box keypair is operator-supplied via
160
+`SHITHUB_ACTIONS__SECRETS__BOX_PRIVATE_KEY_B64` (base64 of a 32-byte
161
+X25519 private key). Generate one with:
162
+
163
+```sh
164
+openssl rand -base64 32
165
+```
166
+
167
+When unset, the server generates a per-process keypair at startup
168
+and logs a loud warning. Secrets PUT against one process won't be
169
+decryptable by another — production deployments MUST configure
170
+this knob.